refactor progress

This commit is contained in:
Hampus Kraft
2026-02-17 12:22:36 +00:00
parent cb31608523
commit d5abd1a7e4
8257 changed files with 1190207 additions and 761040 deletions

View File

@@ -0,0 +1,35 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import type {LimitKey} from '@fluxer/constants/src/LimitConfigMetadata';
import type {LimitConfigSnapshot, LimitConfigWireFormat} from '@fluxer/limits/src/LimitTypes';
export interface LimitConfigCodecOptions {
defaults?: Record<LimitKey, number>;
defaultsHash?: string;
}
export interface ILimitConfigCodec {
computeOverrides(
fullLimits: Partial<Record<LimitKey, number>>,
defaults: Record<LimitKey, number>,
): Partial<Record<LimitKey, number>>;
toWireFormat(config: LimitConfigSnapshot, options?: LimitConfigCodecOptions): LimitConfigWireFormat;
fromWireFormat(wireFormat: LimitConfigWireFormat, options?: LimitConfigCodecOptions): LimitConfigSnapshot;
}

View File

@@ -0,0 +1,26 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import type {LimitKey} from '@fluxer/constants/src/LimitConfigMetadata';
import type {LimitEvaluationOptions, LimitEvaluationResult, LimitMatchContext} from '@fluxer/limits/src/LimitTypes';
export interface ILimitEvaluator {
resolveAll(ctx: LimitMatchContext, options?: LimitEvaluationOptions): LimitEvaluationResult;
resolveOne(ctx: LimitMatchContext, key: LimitKey, options?: LimitEvaluationOptions): number;
}

View File

@@ -0,0 +1,89 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {LIMIT_KEYS, type LimitKey} from '@fluxer/constants/src/LimitConfigMetadata';
import type {ILimitConfigCodec, LimitConfigCodecOptions} from '@fluxer/limits/src/ILimitConfigCodec';
import {DEFAULT_FREE_LIMITS} from '@fluxer/limits/src/LimitDefaults';
import {computeDefaultsHash} from '@fluxer/limits/src/LimitHashing';
import type {LimitConfigSnapshot, LimitConfigWireFormat} from '@fluxer/limits/src/LimitTypes';
export class LimitConfigCodec implements ILimitConfigCodec {
computeOverrides(
fullLimits: Partial<Record<LimitKey, number>>,
defaults: Record<LimitKey, number>,
): Partial<Record<LimitKey, number>> {
const overrides: Partial<Record<LimitKey, number>> = {};
for (const key of LIMIT_KEYS) {
const value = fullLimits[key];
if (value === undefined) {
continue;
}
if (value !== defaults[key]) {
overrides[key] = value;
}
}
return overrides;
}
toWireFormat(config: LimitConfigSnapshot, options?: LimitConfigCodecOptions): LimitConfigWireFormat {
const defaults = options?.defaults ?? DEFAULT_FREE_LIMITS;
const defaultsHash = options?.defaultsHash ?? computeDefaultsHash();
const rules = config.rules.map((rule) => {
const overrides = this.computeOverrides(rule.limits, defaults);
return {
id: rule.id,
filters: rule.filters,
overrides,
};
});
return {
version: 2,
traitDefinitions: config.traitDefinitions,
rules,
defaultsHash,
};
}
fromWireFormat(wireFormat: LimitConfigWireFormat, options?: LimitConfigCodecOptions): LimitConfigSnapshot {
const defaults = options?.defaults ?? DEFAULT_FREE_LIMITS;
const rules = wireFormat.rules.map((rule) => {
const limits: Partial<Record<LimitKey, number>> = {
...defaults,
...rule.overrides,
};
return {
id: rule.id,
filters: rule.filters,
limits,
};
});
return {
version: wireFormat.version,
traitDefinitions: wireFormat.traitDefinitions,
rules,
};
}
}

View File

@@ -0,0 +1,132 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import type {LimitKey} from '@fluxer/constants/src/LimitConfigMetadata';
import {
ATTACHMENT_MAX_SIZE_NON_PREMIUM,
ATTACHMENT_MAX_SIZE_PREMIUM,
AVATAR_MAX_SIZE,
EMOJI_MAX_SIZE,
MAX_ATTACHMENTS_PER_MESSAGE,
MAX_BIO_LENGTH,
MAX_BOOKMARKS_NON_PREMIUM,
MAX_BOOKMARKS_PREMIUM,
MAX_CHANNELS_PER_CATEGORY,
MAX_CREATED_PACKS_NON_PREMIUM,
MAX_CREATED_PACKS_PREMIUM,
MAX_EMBEDS_PER_MESSAGE,
MAX_FAVORITE_MEME_TAGS,
MAX_FAVORITE_MEMES_NON_PREMIUM,
MAX_FAVORITE_MEMES_PREMIUM,
MAX_GROUP_DM_RECIPIENTS,
MAX_GROUP_DMS_PER_USER,
MAX_GUILD_CHANNELS,
MAX_GUILD_EMOJIS_ANIMATED,
MAX_GUILD_EMOJIS_ANIMATED_MORE_EMOJI,
MAX_GUILD_EMOJIS_STATIC,
MAX_GUILD_EMOJIS_STATIC_MORE_EMOJI,
MAX_GUILD_INVITES,
MAX_GUILD_MEMBERS,
MAX_GUILD_ROLES,
MAX_GUILD_STICKERS,
MAX_GUILD_STICKERS_MORE_STICKERS,
MAX_INSTALLED_PACKS_NON_PREMIUM,
MAX_INSTALLED_PACKS_PREMIUM,
MAX_MESSAGE_LENGTH_NON_PREMIUM,
MAX_MESSAGE_LENGTH_PREMIUM,
MAX_PACK_EXPRESSIONS,
MAX_PRIVATE_CHANNELS_PER_USER,
MAX_REACTIONS_PER_MESSAGE,
MAX_RELATIONSHIPS,
MAX_USERS_PER_MESSAGE_REACTION,
MAX_VOICE_MESSAGE_DURATION,
MAX_WEBHOOKS_PER_CHANNEL,
MAX_WEBHOOKS_PER_GUILD,
STICKER_MAX_SIZE,
} from '@fluxer/constants/src/LimitConstants';
export const DEFAULT_FREE_LIMITS: Record<LimitKey, number> = {
avatar_max_size: AVATAR_MAX_SIZE,
emoji_max_size: EMOJI_MAX_SIZE,
feature_animated_avatar: 0,
feature_animated_banner: 0,
feature_custom_discriminator: 0,
feature_custom_notification_sounds: 0,
feature_early_access: 0,
feature_global_expressions: 0,
feature_higher_video_quality: 0,
feature_per_guild_profiles: 0,
feature_voice_entrance_sounds: 0,
max_attachment_file_size: ATTACHMENT_MAX_SIZE_NON_PREMIUM,
max_attachments_per_message: MAX_ATTACHMENTS_PER_MESSAGE,
max_bio_length: MAX_BIO_LENGTH,
max_bookmarks: MAX_BOOKMARKS_NON_PREMIUM,
max_channels_per_category: MAX_CHANNELS_PER_CATEGORY,
max_created_packs: MAX_CREATED_PACKS_NON_PREMIUM,
max_custom_backgrounds: 1,
max_embeds_per_message: MAX_EMBEDS_PER_MESSAGE,
max_favorite_meme_tags: MAX_FAVORITE_MEME_TAGS,
max_favorite_memes: MAX_FAVORITE_MEMES_NON_PREMIUM,
max_group_dm_recipients: MAX_GROUP_DM_RECIPIENTS,
max_group_dms_per_user: MAX_GROUP_DMS_PER_USER,
max_guild_channels: MAX_GUILD_CHANNELS,
max_guild_emojis_animated_more: MAX_GUILD_EMOJIS_ANIMATED_MORE_EMOJI,
max_guild_emojis_animated: MAX_GUILD_EMOJIS_ANIMATED,
max_guild_emojis_static_more: MAX_GUILD_EMOJIS_STATIC_MORE_EMOJI,
max_guild_emojis_static: MAX_GUILD_EMOJIS_STATIC,
max_guild_invites: MAX_GUILD_INVITES,
max_guild_members: MAX_GUILD_MEMBERS,
max_guild_roles: MAX_GUILD_ROLES,
max_guild_stickers_more: MAX_GUILD_STICKERS_MORE_STICKERS,
max_guild_stickers: MAX_GUILD_STICKERS,
max_guilds: 100,
max_installed_packs: MAX_INSTALLED_PACKS_NON_PREMIUM,
max_message_length: MAX_MESSAGE_LENGTH_NON_PREMIUM,
max_pack_expressions: MAX_PACK_EXPRESSIONS,
max_private_channels_per_user: MAX_PRIVATE_CHANNELS_PER_USER,
max_reactions_per_message: MAX_REACTIONS_PER_MESSAGE,
max_relationships: MAX_RELATIONSHIPS,
max_users_per_message_reaction: MAX_USERS_PER_MESSAGE_REACTION,
max_voice_message_duration: MAX_VOICE_MESSAGE_DURATION,
max_webhooks_per_channel: MAX_WEBHOOKS_PER_CHANNEL,
max_webhooks_per_guild: MAX_WEBHOOKS_PER_GUILD,
sticker_max_size: STICKER_MAX_SIZE,
};
export const DEFAULT_PREMIUM_LIMITS: Record<LimitKey, number> = {
...DEFAULT_FREE_LIMITS,
feature_animated_avatar: 1,
feature_animated_banner: 1,
feature_custom_discriminator: 1,
feature_custom_notification_sounds: 1,
feature_early_access: 1,
feature_global_expressions: 1,
feature_higher_video_quality: 1,
feature_per_guild_profiles: 1,
feature_voice_entrance_sounds: 1,
max_attachment_file_size: ATTACHMENT_MAX_SIZE_PREMIUM,
max_bio_length: MAX_BIO_LENGTH,
max_bookmarks: MAX_BOOKMARKS_PREMIUM,
max_created_packs: MAX_CREATED_PACKS_PREMIUM,
max_custom_backgrounds: 15,
max_favorite_memes: MAX_FAVORITE_MEMES_PREMIUM,
max_guilds: 200,
max_installed_packs: MAX_INSTALLED_PACKS_PREMIUM,
max_message_length: MAX_MESSAGE_LENGTH_PREMIUM,
};

View File

@@ -0,0 +1,44 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import type {LimitKey} from '@fluxer/constants/src/LimitConfigMetadata';
import type {ILimitConfigCodec} from '@fluxer/limits/src/ILimitConfigCodec';
import {LimitConfigCodec} from '@fluxer/limits/src/LimitConfigCodec';
import type {LimitConfigSnapshot, LimitConfigWireFormat} from '@fluxer/limits/src/LimitTypes';
const defaultLimitConfigCodec = new LimitConfigCodec();
export function createLimitConfigCodec(): ILimitConfigCodec {
return new LimitConfigCodec();
}
export function computeOverrides(
fullLimits: Partial<Record<LimitKey, number>>,
defaults: Record<LimitKey, number>,
): Partial<Record<LimitKey, number>> {
return defaultLimitConfigCodec.computeOverrides(fullLimits, defaults);
}
export function computeWireFormat(config: LimitConfigSnapshot): LimitConfigWireFormat {
return defaultLimitConfigCodec.toWireFormat(config);
}
export function expandWireFormat(wireFormat: LimitConfigWireFormat): LimitConfigSnapshot {
return defaultLimitConfigCodec.fromWireFormat(wireFormat);
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import type {LimitKey} from '@fluxer/constants/src/LimitConfigMetadata';
import type {ILimitEvaluator} from '@fluxer/limits/src/ILimitEvaluator';
import {DEFAULT_FREE_LIMITS} from '@fluxer/limits/src/LimitDefaults';
import {applyRuleToResolvedLimits, ruleMatches, sortRulesBySpecificity} from '@fluxer/limits/src/LimitRuleRuntime';
import type {
LimitConfigSnapshot,
LimitEvaluationOptions,
LimitEvaluationResult,
LimitMatchContext,
LimitRule,
} from '@fluxer/limits/src/LimitTypes';
export class LimitEvaluator implements ILimitEvaluator {
private readonly sortedRules: Array<LimitRule>;
constructor(snapshot: LimitConfigSnapshot) {
this.sortedRules = sortRulesBySpecificity(snapshot.rules);
}
resolveAll(ctx: LimitMatchContext, options?: LimitEvaluationOptions): LimitEvaluationResult {
const evaluationContext = options?.evaluationContext ?? 'user';
const baseLimits = options?.baseLimits ?? DEFAULT_FREE_LIMITS;
const resolvedLimits = {...baseLimits};
for (const rule of this.sortedRules) {
if (!ruleMatches(rule.filters, ctx)) {
continue;
}
applyRuleToResolvedLimits(resolvedLimits, rule, evaluationContext);
}
return {
limits: resolvedLimits,
};
}
resolveOne(ctx: LimitMatchContext, key: LimitKey, options?: LimitEvaluationOptions): number {
return this.resolveAll(ctx, options).limits[key];
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {DEFAULT_FREE_LIMITS, DEFAULT_PREMIUM_LIMITS} from '@fluxer/limits/src/LimitDefaults';
function simpleHash(str: string): string {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash;
}
return Math.abs(hash).toString(36);
}
export function computeDefaultsHash(): string {
const combined = {
free: DEFAULT_FREE_LIMITS,
premium: DEFAULT_PREMIUM_LIMITS,
};
return simpleHash(JSON.stringify(combined));
}

View File

@@ -0,0 +1,25 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {ruleMatches as evaluateRuleMatch} from '@fluxer/limits/src/LimitRuleRuntime';
import type {LimitFilter, LimitMatchContext} from '@fluxer/limits/src/LimitTypes';
export function ruleMatches(filters: LimitFilter | undefined, ctx: LimitMatchContext): boolean {
return evaluateRuleMatch(filters, ctx);
}

View File

@@ -0,0 +1,30 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import type {LimitKey} from '@fluxer/constants/src/LimitConfigMetadata';
import {applyRuleToResolvedLimits} from '@fluxer/limits/src/LimitRuleRuntime';
import type {EvaluationContext, LimitRule} from '@fluxer/limits/src/LimitTypes';
export function mergeRuleIntoResolved(
resolved: Record<LimitKey, number>,
rule: LimitRule,
evaluationContext: EvaluationContext,
): void {
applyRuleToResolvedLimits(resolved, rule, evaluationContext);
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import type {LimitKey} from '@fluxer/constants/src/LimitConfigMetadata';
import type {ILimitEvaluator} from '@fluxer/limits/src/ILimitEvaluator';
import {LimitEvaluator} from '@fluxer/limits/src/LimitEvaluator';
import type {
LimitConfigSnapshot,
LimitEvaluationOptions,
LimitEvaluationResult,
LimitMatchContext,
} from '@fluxer/limits/src/LimitTypes';
export function createLimitEvaluator(snapshot: LimitConfigSnapshot): ILimitEvaluator {
return new LimitEvaluator(snapshot);
}
export function resolveLimits(
snapshot: LimitConfigSnapshot,
ctx: LimitMatchContext,
options?: LimitEvaluationOptions,
): LimitEvaluationResult {
const evaluator = createLimitEvaluator(snapshot);
return evaluator.resolveAll(ctx, options);
}
export function resolveLimit(
snapshot: LimitConfigSnapshot,
ctx: LimitMatchContext,
key: LimitKey,
options?: LimitEvaluationOptions,
): number {
const evaluator = createLimitEvaluator(snapshot);
return evaluator.resolveOne(ctx, key, options);
}

View File

@@ -0,0 +1,150 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {LIMIT_KEY_SCOPES, LIMIT_KEYS, type LimitKey, type LimitScope} from '@fluxer/constants/src/LimitConfigMetadata';
import type {EvaluationContext, LimitFilter, LimitMatchContext, LimitRule} from '@fluxer/limits/src/LimitTypes';
interface RankedRule {
rule: LimitRule;
specificity: number;
originalIndex: number;
}
function areRequiredEntriesPresent(required: Array<string> | undefined, available: Set<string>): boolean {
if (!required || required.length === 0) {
return true;
}
for (const entry of required) {
if (!available.has(entry)) {
return false;
}
}
return true;
}
function shouldApplyLimitForContext(
scope: LimitScope,
evaluationContext: EvaluationContext,
hasTraitFilters: boolean,
hasGuildFilters: boolean,
): boolean {
if (evaluationContext === 'user') {
return scope === 'user' || scope === 'both';
}
if (scope === 'both') {
return true;
}
if (scope === 'user') {
return !hasGuildFilters;
}
if (hasGuildFilters) {
return true;
}
return !hasTraitFilters;
}
function isValidLimitValue(value: number | undefined): value is number {
if (typeof value !== 'number') {
return false;
}
if (!Number.isFinite(value)) {
return false;
}
return value >= 0;
}
export function ruleMatches(filters: LimitFilter | undefined, ctx: LimitMatchContext): boolean {
if (!filters) {
return true;
}
if (!areRequiredEntriesPresent(filters.traits, ctx.traits)) {
return false;
}
if (!areRequiredEntriesPresent(filters.guildFeatures, ctx.guildFeatures)) {
return false;
}
return true;
}
export function calculateSpecificity(filters: LimitFilter | undefined): number {
if (!filters) {
return 0;
}
const traitCount = filters.traits?.length ?? 0;
const guildFeatureCount = filters.guildFeatures?.length ?? 0;
return traitCount + guildFeatureCount;
}
export function compareSpecificity(a: LimitFilter | undefined, b: LimitFilter | undefined): number {
return calculateSpecificity(a) - calculateSpecificity(b);
}
export function sortRulesBySpecificity(rules: Array<LimitRule>): Array<LimitRule> {
const rankedRules: Array<RankedRule> = rules.map((rule, index) => ({
rule,
specificity: calculateSpecificity(rule.filters),
originalIndex: index,
}));
rankedRules.sort((a, b) => {
if (a.specificity !== b.specificity) {
return a.specificity - b.specificity;
}
return a.originalIndex - b.originalIndex;
});
return rankedRules.map((rankedRule) => rankedRule.rule);
}
export function applyRuleToResolvedLimits(
resolved: Record<LimitKey, number>,
rule: LimitRule,
evaluationContext: EvaluationContext,
): void {
const hasGuildFilters = (rule.filters?.guildFeatures?.length ?? 0) > 0;
const hasTraitFilters = (rule.filters?.traits?.length ?? 0) > 0;
for (const key of LIMIT_KEYS) {
const value = rule.limits[key];
if (!isValidLimitValue(value)) {
continue;
}
const scope = LIMIT_KEY_SCOPES[key];
if (!shouldApplyLimitForContext(scope, evaluationContext, hasTraitFilters, hasGuildFilters)) {
continue;
}
const currentValue = resolved[key] ?? 0;
resolved[key] = Math.max(currentValue, value);
}
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {
calculateSpecificity as calculateRuntimeSpecificity,
compareSpecificity as compareRuntimeSpecificity,
} from '@fluxer/limits/src/LimitRuleRuntime';
import type {LimitFilter} from '@fluxer/limits/src/LimitTypes';
export function calculateSpecificity(filters: LimitFilter | undefined): number {
return calculateRuntimeSpecificity(filters);
}
export function compareSpecificity(a: LimitFilter | undefined, b: LimitFilter | undefined): number {
return compareRuntimeSpecificity(a, b);
}

View File

@@ -0,0 +1,65 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import type {LimitKey} from '@fluxer/constants/src/LimitConfigMetadata';
export interface LimitFilter {
traits?: Array<string>;
guildFeatures?: Array<string>;
}
export interface LimitRule {
id: string;
filters?: LimitFilter;
limits: Partial<Record<LimitKey, number>>;
modifiedFields?: Array<LimitKey>;
}
export interface LimitConfigSnapshot {
version?: number;
traitDefinitions: Array<string>;
rules: Array<LimitRule>;
}
export interface LimitConfigWireFormat {
version: 2;
traitDefinitions: Array<string>;
rules: Array<{
id: string;
filters?: LimitFilter;
overrides: Partial<Record<LimitKey, number>>;
}>;
defaultsHash: string;
}
export interface LimitMatchContext {
traits: Set<string>;
guildFeatures: Set<string>;
}
export type EvaluationContext = 'user' | 'guild';
export interface LimitEvaluationOptions {
evaluationContext?: EvaluationContext;
baseLimits?: Record<LimitKey, number>;
}
export interface LimitEvaluationResult {
limits: Record<LimitKey, number>;
}

View File

@@ -0,0 +1,131 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {DEFAULT_FREE_LIMITS, DEFAULT_PREMIUM_LIMITS} from '@fluxer/limits/src/LimitDefaults';
import {computeOverrides, computeWireFormat, expandWireFormat} from '@fluxer/limits/src/LimitDiffer';
import type {LimitConfigSnapshot} from '@fluxer/limits/src/LimitTypes';
import {describe, expect, test} from 'vitest';
describe('LimitDiffer', () => {
test('computeOverrides returns empty object when limits match defaults', () => {
const overrides = computeOverrides(DEFAULT_FREE_LIMITS, DEFAULT_FREE_LIMITS);
expect(overrides).toEqual({});
});
test('computeOverrides extracts differences from defaults', () => {
const customLimits = {
...DEFAULT_FREE_LIMITS,
max_guilds: 150,
max_message_length: 3000,
};
const overrides = computeOverrides(customLimits, DEFAULT_FREE_LIMITS);
expect(overrides).toEqual({
max_guilds: 150,
max_message_length: 3000,
});
});
test('computeWireFormat creates wire format with overrides', () => {
const config: LimitConfigSnapshot = {
traitDefinitions: ['premium'],
rules: [
{
id: 'default',
limits: {...DEFAULT_FREE_LIMITS},
},
{
id: 'premium',
filters: {traits: ['premium']},
limits: {...DEFAULT_PREMIUM_LIMITS},
},
],
};
const wireFormat = computeWireFormat(config);
expect(wireFormat.version).toBe(2);
expect(wireFormat.traitDefinitions).toEqual(['premium']);
expect(wireFormat.rules).toHaveLength(2);
expect(wireFormat.defaultsHash).toBeTruthy();
expect(wireFormat.rules[0].id).toBe('default');
expect(wireFormat.rules[0].overrides).toEqual({});
expect(wireFormat.rules[1].id).toBe('premium');
expect(Object.keys(wireFormat.rules[1].overrides).length).toBeGreaterThan(0);
expect(wireFormat.rules[1].overrides.max_guilds).toBe(200);
});
test('expandWireFormat reconstructs full config from overrides', () => {
const config: LimitConfigSnapshot = {
traitDefinitions: ['premium'],
rules: [
{
id: 'default',
limits: {...DEFAULT_FREE_LIMITS},
},
{
id: 'premium',
filters: {traits: ['premium']},
limits: {...DEFAULT_PREMIUM_LIMITS},
},
],
};
const wireFormat = computeWireFormat(config);
const expanded = expandWireFormat(wireFormat);
expect(expanded.version).toBe(2);
expect(expanded.traitDefinitions).toEqual(['premium']);
expect(expanded.rules).toHaveLength(2);
expect(expanded.rules[0].limits.max_guilds).toBe(100);
expect(expanded.rules[1].limits.max_guilds).toBe(200);
expect(expanded.rules[1].limits.max_message_length).toBe(4000);
});
test('roundtrip: expand(compute(config)) equals config', () => {
const config: LimitConfigSnapshot = {
traitDefinitions: ['premium'],
rules: [
{
id: 'default',
limits: {...DEFAULT_FREE_LIMITS},
},
{
id: 'premium',
filters: {traits: ['premium']},
limits: {
...DEFAULT_PREMIUM_LIMITS,
max_guilds: 250,
},
},
],
};
const wireFormat = computeWireFormat(config);
const expanded = expandWireFormat(wireFormat);
expect(expanded.rules[0].limits).toEqual(config.rules[0].limits);
expect(expanded.rules[1].limits.max_guilds).toBe(250);
expect(expanded.rules[1].limits.max_message_length).toBe(4000);
});
});

View File

@@ -0,0 +1,178 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {DEFAULT_FREE_LIMITS, DEFAULT_PREMIUM_LIMITS} from '@fluxer/limits/src/LimitDefaults';
import {resolveLimit, resolveLimits} from '@fluxer/limits/src/LimitResolver';
import type {LimitConfigSnapshot, LimitMatchContext} from '@fluxer/limits/src/LimitTypes';
import {describe, expect, test} from 'vitest';
describe('LimitResolver', () => {
test('resolveLimits returns default free limits for empty snapshot', () => {
const snapshot: LimitConfigSnapshot = {
traitDefinitions: [],
rules: [],
};
const ctx: LimitMatchContext = {
traits: new Set(),
guildFeatures: new Set(),
};
const result = resolveLimits(snapshot, ctx);
expect(result.limits).toEqual(DEFAULT_FREE_LIMITS);
});
test('resolveLimits applies premium limits for premium trait', () => {
const snapshot: LimitConfigSnapshot = {
traitDefinitions: ['premium'],
rules: [
{
id: 'default',
limits: {...DEFAULT_FREE_LIMITS},
},
{
id: 'premium',
filters: {traits: ['premium']},
limits: {...DEFAULT_PREMIUM_LIMITS},
},
],
};
const ctx: LimitMatchContext = {
traits: new Set(['premium']),
guildFeatures: new Set(),
};
const result = resolveLimits(snapshot, ctx);
expect(result.limits.max_guilds).toBe(200);
expect(result.limits.max_message_length).toBe(4000);
expect(result.limits.feature_animated_avatar).toBe(1);
});
test('resolveLimits merges with Math.max', () => {
const snapshot: LimitConfigSnapshot = {
traitDefinitions: ['premium', 'special'],
rules: [
{
id: 'default',
limits: {max_guilds: 100},
},
{
id: 'premium',
filters: {traits: ['premium']},
limits: {max_guilds: 200},
},
{
id: 'special',
filters: {traits: ['special']},
limits: {max_guilds: 150},
},
],
};
const ctx: LimitMatchContext = {
traits: new Set(['premium', 'special']),
guildFeatures: new Set(),
};
const result = resolveLimits(snapshot, ctx);
expect(result.limits.max_guilds).toBe(200);
});
test('resolveLimits applies rules by specificity order', () => {
const snapshot: LimitConfigSnapshot = {
traitDefinitions: ['premium'],
rules: [
{
id: 'combined',
filters: {traits: ['premium'], guildFeatures: ['MORE_EMOJI']},
limits: {max_guild_emojis_static: 500},
},
{
id: 'premium',
filters: {traits: ['premium']},
limits: {max_guild_emojis_static: 100},
},
{
id: 'default',
limits: {max_guild_emojis_static: 50},
},
],
};
const ctx: LimitMatchContext = {
traits: new Set(['premium']),
guildFeatures: new Set(['MORE_EMOJI']),
};
const result = resolveLimits(snapshot, ctx, {evaluationContext: 'guild'});
expect(result.limits.max_guild_emojis_static).toBe(500);
});
test('resolveLimit returns specific limit value', () => {
const snapshot: LimitConfigSnapshot = {
traitDefinitions: ['premium'],
rules: [
{
id: 'premium',
filters: {traits: ['premium']},
limits: {max_guilds: 200},
},
],
};
const ctx: LimitMatchContext = {
traits: new Set(['premium']),
guildFeatures: new Set(),
};
const result = resolveLimit(snapshot, ctx, 'max_guilds');
expect(result).toBe(200);
});
test('resolveLimits ignores non-matching rules', () => {
const snapshot: LimitConfigSnapshot = {
traitDefinitions: ['premium', 'special'],
rules: [
{
id: 'default',
limits: {max_guilds: 100},
},
{
id: 'premium',
filters: {traits: ['premium']},
limits: {max_guilds: 200},
},
{
id: 'special',
filters: {traits: ['special']},
limits: {max_guilds: 300},
},
],
};
const ctx: LimitMatchContext = {
traits: new Set(['premium']),
guildFeatures: new Set(),
};
const result = resolveLimits(snapshot, ctx);
expect(result.limits.max_guilds).toBe(200);
});
});

View File

@@ -0,0 +1,66 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {calculateSpecificity, compareSpecificity} from '@fluxer/limits/src/LimitSpecificity';
import {describe, expect, test} from 'vitest';
describe('LimitSpecificity', () => {
test('calculateSpecificity returns 0 for undefined filters', () => {
expect(calculateSpecificity(undefined)).toBe(0);
});
test('calculateSpecificity returns 0 for empty filters', () => {
expect(calculateSpecificity({})).toBe(0);
});
test('calculateSpecificity counts traits', () => {
expect(calculateSpecificity({traits: ['premium']})).toBe(1);
expect(calculateSpecificity({traits: ['premium', 'verified']})).toBe(2);
});
test('calculateSpecificity counts guild features', () => {
expect(calculateSpecificity({guildFeatures: ['MORE_EMOJI']})).toBe(1);
expect(calculateSpecificity({guildFeatures: ['MORE_EMOJI', 'MORE_STICKERS']})).toBe(2);
});
test('calculateSpecificity counts combined traits and guild features', () => {
expect(calculateSpecificity({traits: ['premium'], guildFeatures: ['MORE_EMOJI']})).toBe(2);
expect(
calculateSpecificity({
traits: ['premium', 'verified'],
guildFeatures: ['MORE_EMOJI', 'MORE_STICKERS'],
}),
).toBe(4);
});
test('compareSpecificity returns negative when a is less specific', () => {
expect(compareSpecificity(undefined, {traits: ['premium']})).toBeLessThan(0);
expect(compareSpecificity({traits: ['premium']}, {traits: ['premium', 'verified']})).toBeLessThan(0);
});
test('compareSpecificity returns 0 when equal specificity', () => {
expect(compareSpecificity(undefined, undefined)).toBe(0);
expect(compareSpecificity({traits: ['premium']}, {traits: ['verified']})).toBe(0);
});
test('compareSpecificity returns positive when a is more specific', () => {
expect(compareSpecificity({traits: ['premium']}, undefined)).toBeGreaterThan(0);
expect(compareSpecificity({traits: ['premium', 'verified']}, {traits: ['premium']})).toBeGreaterThan(0);
});
});