refactor progress
This commit is contained in:
20
packages/limits/package.json
Normal file
20
packages/limits/package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "@fluxer/limits",
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./src/*": "./src/*",
|
||||
"./*": "./*"
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit",
|
||||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fluxer/constants": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"vitest": "catalog:"
|
||||
}
|
||||
}
|
||||
35
packages/limits/src/ILimitConfigCodec.tsx
Normal file
35
packages/limits/src/ILimitConfigCodec.tsx
Normal 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;
|
||||
}
|
||||
26
packages/limits/src/ILimitEvaluator.tsx
Normal file
26
packages/limits/src/ILimitEvaluator.tsx
Normal 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;
|
||||
}
|
||||
89
packages/limits/src/LimitConfigCodec.tsx
Normal file
89
packages/limits/src/LimitConfigCodec.tsx
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
132
packages/limits/src/LimitDefaults.tsx
Normal file
132
packages/limits/src/LimitDefaults.tsx
Normal 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,
|
||||
};
|
||||
44
packages/limits/src/LimitDiffer.tsx
Normal file
44
packages/limits/src/LimitDiffer.tsx
Normal 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);
|
||||
}
|
||||
60
packages/limits/src/LimitEvaluator.tsx
Normal file
60
packages/limits/src/LimitEvaluator.tsx
Normal 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];
|
||||
}
|
||||
}
|
||||
38
packages/limits/src/LimitHashing.tsx
Normal file
38
packages/limits/src/LimitHashing.tsx
Normal 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));
|
||||
}
|
||||
25
packages/limits/src/LimitMatcher.tsx
Normal file
25
packages/limits/src/LimitMatcher.tsx
Normal 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);
|
||||
}
|
||||
30
packages/limits/src/LimitMerger.tsx
Normal file
30
packages/limits/src/LimitMerger.tsx
Normal 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);
|
||||
}
|
||||
51
packages/limits/src/LimitResolver.tsx
Normal file
51
packages/limits/src/LimitResolver.tsx
Normal 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);
|
||||
}
|
||||
150
packages/limits/src/LimitRuleRuntime.tsx
Normal file
150
packages/limits/src/LimitRuleRuntime.tsx
Normal 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);
|
||||
}
|
||||
}
|
||||
32
packages/limits/src/LimitSpecificity.tsx
Normal file
32
packages/limits/src/LimitSpecificity.tsx
Normal 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);
|
||||
}
|
||||
65
packages/limits/src/LimitTypes.tsx
Normal file
65
packages/limits/src/LimitTypes.tsx
Normal 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>;
|
||||
}
|
||||
131
packages/limits/src/__tests__/LimitDiffer.test.tsx
Normal file
131
packages/limits/src/__tests__/LimitDiffer.test.tsx
Normal 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);
|
||||
});
|
||||
});
|
||||
178
packages/limits/src/__tests__/LimitResolver.test.tsx
Normal file
178
packages/limits/src/__tests__/LimitResolver.test.tsx
Normal 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);
|
||||
});
|
||||
});
|
||||
66
packages/limits/src/__tests__/LimitSpecificity.test.tsx
Normal file
66
packages/limits/src/__tests__/LimitSpecificity.test.tsx
Normal 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);
|
||||
});
|
||||
});
|
||||
8
packages/limits/tsconfig.json
Normal file
8
packages/limits/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfigs/package.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
Reference in New Issue
Block a user