mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 13:05:32 +09:00
refactor(pkgs): share vitest config and dedupe helpers (#3384)
This commit is contained in:
1 parent
5695510980
commit
575a5adbef
43 files changed
+172
-874
No files matched your search
@@ -85,11 +85,6 @@ services:
|
||||
target: /workspaces/fluxer/packages/hono/node_modules
|
||||
volume:
|
||||
nocopy: true
|
||||
- type: volume
|
||||
source: package-hono-types-node-modules
|
||||
target: /workspaces/fluxer/packages/hono_types/node_modules
|
||||
volume:
|
||||
nocopy: true
|
||||
- type: volume
|
||||
source: package-i18n-node-modules
|
||||
target: /workspaces/fluxer/packages/i18n/node_modules
|
||||
@@ -349,7 +344,6 @@ volumes:
|
||||
package-errors-node-modules:
|
||||
package-geo-utils-node-modules:
|
||||
package-hono-node-modules:
|
||||
package-hono-types-node-modules:
|
||||
package-i18n-node-modules:
|
||||
package-instance-bootstrap-node-modules:
|
||||
package-ip-utils-node-modules:
|
||||
|
||||
@@ -67,9 +67,6 @@ p:geo-utils:
|
||||
p:hono:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: packages/hono/**/*
|
||||
p:hono-types:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: packages/hono_types/**/*
|
||||
p:i18n:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: packages/i18n/**/*
|
||||
|
||||
+1
-16
@@ -1,18 +1,3 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {defineConfig} from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
resolve: {tsconfigPaths: true},
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
include: ['**/*.{test,spec}.{ts,tsx}'],
|
||||
exclude: ['node_modules', 'dist'],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html'],
|
||||
exclude: ['**/*.test.tsx', '**/*.spec.tsx', 'node_modules/'],
|
||||
},
|
||||
},
|
||||
});
|
||||
export {default} from '../../../tsconfigs/vitest.base.mts';
|
||||
@@ -1,18 +1,3 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {defineConfig} from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
resolve: {tsconfigPaths: true},
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
include: ['**/*.{test,spec}.{ts,tsx}'],
|
||||
exclude: ['node_modules', 'dist'],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html'],
|
||||
exclude: ['**/*.test.tsx', '**/*.spec.tsx', 'node_modules/'],
|
||||
},
|
||||
},
|
||||
});
|
||||
export {default} from '../../../tsconfigs/vitest.base.mts';
|
||||
@@ -1,18 +1,3 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {defineConfig} from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
resolve: {tsconfigPaths: true},
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
include: ['**/*.{test,spec}.{ts,tsx}'],
|
||||
exclude: ['node_modules', 'dist'],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html'],
|
||||
exclude: ['**/*.test.tsx', '**/*.spec.tsx', 'node_modules/'],
|
||||
},
|
||||
},
|
||||
});
|
||||
export {default} from '../../../tsconfigs/vitest.base.mts';
|
||||
@@ -1,18 +1,3 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {defineConfig} from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
resolve: {tsconfigPaths: true},
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
include: ['**/*.{test,spec}.{ts,tsx}'],
|
||||
exclude: ['node_modules', 'dist'],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html'],
|
||||
exclude: ['**/*.test.tsx', '**/*.spec.tsx', 'node_modules/'],
|
||||
},
|
||||
},
|
||||
});
|
||||
export {default} from '../../../tsconfigs/vitest.base.mts';
|
||||
@@ -1,18 +1,3 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {defineConfig} from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
resolve: {tsconfigPaths: true},
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
include: ['**/*.{test,spec}.{ts,tsx}'],
|
||||
exclude: ['node_modules', 'dist'],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html'],
|
||||
exclude: ['**/*.test.tsx', '**/*.spec.tsx', 'node_modules/'],
|
||||
},
|
||||
},
|
||||
});
|
||||
export {default} from '../../../tsconfigs/vitest.base.mts';
|
||||
@@ -1,18 +1,3 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {defineConfig} from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
resolve: {tsconfigPaths: true},
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
include: ['**/*.{test,spec}.{ts,tsx}'],
|
||||
exclude: ['node_modules', 'dist'],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html'],
|
||||
exclude: ['**/*.test.tsx', '**/*.spec.tsx', 'node_modules/'],
|
||||
},
|
||||
},
|
||||
});
|
||||
export {default} from '../../../tsconfigs/vitest.base.mts';
|
||||
@@ -3,7 +3,7 @@
|
||||
import type {GuildMember} from '@app/api/models/GuildMember';
|
||||
import type {User} from '@app/api/models/User';
|
||||
import type {SearchableGuildMember} from '@fluxer/schema/src/contracts/search/SearchDocumentTypes';
|
||||
import {extractTimestampFromSnowflake} from '@fluxer/snowflake/src/SnowflakeUtils';
|
||||
import {extractTimestamp} from '@fluxer/snowflake/src/SnowflakeUtils';
|
||||
|
||||
const MIN_USERNAME_SUFFIX_LENGTH = 2;
|
||||
|
||||
@@ -34,7 +34,7 @@ export function convertToSearchableGuildMember(member: GuildMember, user: User):
|
||||
joinSourceType: member.joinSourceType,
|
||||
sourceInviteCode: member.sourceInviteCode?.toString() ?? null,
|
||||
inviterId: member.inviterId?.toString() ?? null,
|
||||
userCreatedAt: Math.floor(extractTimestampFromSnowflake(member.userId.toString()) / 1000),
|
||||
userCreatedAt: Math.floor(extractTimestamp(member.userId.toString()) / 1000),
|
||||
isBot: user.isBot,
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"extends": "./tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"allowImportingTsExtensions": true,
|
||||
"paths": {
|
||||
"@app/*": ["./src/*"],
|
||||
"@fluxer/*": ["./../packages/*", "./../packages/*/src/index.ts"],
|
||||
|
||||
@@ -6,50 +6,12 @@ import {getNativeLocaleIdentifier} from '@app/features/platform/types/Platform';
|
||||
import {Logger} from '@app/features/platform/utils/AppLogger';
|
||||
import {loadLazyModule} from '@app/features/platform/utils/LazyModuleLoader';
|
||||
import {noteLocale} from '@app/features/theme/fonts/ScriptFontLoader';
|
||||
import {AllLocales, type LocaleCode} from '@fluxer/constants/src/Locales';
|
||||
import {type I18n, i18n, type Messages} from '@lingui/core';
|
||||
import {createAtom, runInAction} from 'mobx';
|
||||
|
||||
const supportedLocales = [
|
||||
'ar',
|
||||
'bg',
|
||||
'cs',
|
||||
'da',
|
||||
'de',
|
||||
'el',
|
||||
'en-GB',
|
||||
'en-US',
|
||||
'es-ES',
|
||||
'es-419',
|
||||
'fi',
|
||||
'fr',
|
||||
'he',
|
||||
'hi',
|
||||
'hr',
|
||||
'hu',
|
||||
'id',
|
||||
'it',
|
||||
'ja',
|
||||
'ko',
|
||||
'lt',
|
||||
'nl',
|
||||
'no',
|
||||
'pl',
|
||||
'pt-BR',
|
||||
'ro',
|
||||
'ru',
|
||||
'sv-SE',
|
||||
'th',
|
||||
'tr',
|
||||
'uk',
|
||||
'vi',
|
||||
'zh-CN',
|
||||
'zh-TW',
|
||||
] as const;
|
||||
|
||||
type LocaleCode = (typeof supportedLocales)[number];
|
||||
|
||||
const DEFAULT_LOCALE: LocaleCode = 'en-US';
|
||||
const supportedLocaleSet = new Set<LocaleCode>(supportedLocales);
|
||||
const supportedLocaleSet = new Set<LocaleCode>(AllLocales);
|
||||
const logger = new Logger('i18n');
|
||||
const LANGUAGE_OVERRIDES: Record<string, LocaleCode> = {
|
||||
en: 'en-US',
|
||||
@@ -157,7 +119,7 @@ function resolveLocale(value: string): LocaleCode | null {
|
||||
if (override) {
|
||||
return override;
|
||||
}
|
||||
const fallback = supportedLocales.find((code) => code.split('-')[0].toLowerCase() === language);
|
||||
const fallback = AllLocales.find((code) => code.split('-')[0].toLowerCase() === language);
|
||||
if (fallback) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import {initializeStore} from '@app/features/platform/utils/StoreInitialization'
|
||||
import {makeSyncedField} from '@app/features/user/state/SyncedField';
|
||||
import Users from '@app/features/user/state/Users';
|
||||
import {PrivacyPreferencesSchema} from '@fluxer/schema/src/gen/fluxer/user/preferences/v1/preferences_pb';
|
||||
import {extractTimestampFromSnowflake} from '@fluxer/snowflake/src/SnowflakeUtils';
|
||||
import {extractTimestamp} from '@fluxer/snowflake/src/SnowflakeUtils';
|
||||
import {makeAutoObservable} from 'mobx';
|
||||
|
||||
const SHOW_ACTIVE_NOW_DEFAULT = true;
|
||||
@@ -15,7 +15,7 @@ function getPreuploadMessageAttachmentsDefaultForUserId(userId: string | null |
|
||||
if (!userId) {
|
||||
return false;
|
||||
}
|
||||
const createdAtMs = extractTimestampFromSnowflake(userId);
|
||||
const createdAtMs = extractTimestamp(userId);
|
||||
return Number.isFinite(createdAtMs) && createdAtMs >= PREUPLOAD_MESSAGE_ATTACHMENTS_DEFAULT_ON_AFTER_MS;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import {Buffer} from 'node:buffer';
|
||||
import dns from 'node:dns';
|
||||
import http from 'node:http';
|
||||
import https from 'node:https';
|
||||
import {isIPv4, isIPv6, type LookupFunction} from 'node:net';
|
||||
import type {LookupFunction} from 'node:net';
|
||||
import {Readable, Transform} from 'node:stream';
|
||||
import type {ReadableStream as NodeReadableStream} from 'node:stream/web';
|
||||
import {createChildLogger} from '@electron/common/Logger';
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from '@electron/main/DesktopSessionHTTP';
|
||||
import {resolveDesktopTrustedCertificates} from '@electron/main/DesktopTrustedCertificates';
|
||||
import {normalizeHTTPNetworkOrigin} from '@fluxer/instance_bootstrap/src/NetworkOrigin';
|
||||
import {isPublicIpAddress, parseIpAddress} from '@fluxer/ip_utils/src/IpAddress';
|
||||
|
||||
const logger = createChildLogger('DesktopOutboundHTTP');
|
||||
|
||||
@@ -54,8 +55,6 @@ const DESKTOP_OUTBOUND_HTTP_TRANSPORT_MESSAGE = 'The request could not be comple
|
||||
const DESKTOP_OUTBOUND_HTTP_TIMEOUT_MESSAGE = 'The request timed out';
|
||||
const DESKTOP_OUTBOUND_HTTP_CAPACITY_MESSAGE = 'Too many downloads are already in progress';
|
||||
|
||||
const IPV6_GROUP_COUNT = 8;
|
||||
|
||||
const DesktopOutboundBlockReason = Object.freeze({
|
||||
INSECURE_TRANSPORT: 'insecure-transport',
|
||||
INVALID_TARGET: 'invalid-target',
|
||||
@@ -299,143 +298,13 @@ function blocked(
|
||||
return new DesktopOutboundHTTPBlockedError();
|
||||
}
|
||||
|
||||
function stripIPv6ZoneIdentifier(value: string): string {
|
||||
const zoneIndex = value.indexOf('%');
|
||||
if (zoneIndex === -1) {
|
||||
return value;
|
||||
}
|
||||
const addressPart = value.slice(0, zoneIndex);
|
||||
return addressPart.includes(':') ? addressPart : value;
|
||||
}
|
||||
|
||||
function normalizeIPv6(value: string): string {
|
||||
try {
|
||||
const hostname = new URL(`http://[${value}]`).hostname;
|
||||
return hostname.startsWith('[') && hostname.endsWith(']') ? hostname.slice(1, -1) : hostname;
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
function parseIPAddress(value: string): PinnedAddress | null {
|
||||
const trimmed = value.trim();
|
||||
const unbracketed = trimmed.startsWith('[') && trimmed.endsWith(']') ? trimmed.slice(1, -1) : trimmed;
|
||||
const unzoned = stripIPv6ZoneIdentifier(unbracketed);
|
||||
if (isIPv4(unzoned)) {
|
||||
return {address: unzoned, family: 4};
|
||||
}
|
||||
if (isIPv6(unzoned)) {
|
||||
return {address: normalizeIPv6(unzoned), family: 6};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function parseIPv4Octets(address: string): Array<number> | null {
|
||||
const parts = address.split('.');
|
||||
if (parts.length !== 4) {
|
||||
return null;
|
||||
}
|
||||
const octets = parts.map((part) => (/^\d{1,3}$/u.test(part) ? Number.parseInt(part, 10) : Number.NaN));
|
||||
if (octets.some((octet) => !Number.isInteger(octet) || octet < 0 || octet > 255)) {
|
||||
return null;
|
||||
}
|
||||
return octets;
|
||||
}
|
||||
|
||||
function ipv4Value(octets: ReadonlyArray<number>): number {
|
||||
return ((octets[0] << 24) | (octets[1] << 16) | (octets[2] << 8) | octets[3]) >>> 0;
|
||||
}
|
||||
|
||||
function isIPv4InCIDR(value: number, base: number, prefixLength: number): boolean {
|
||||
const mask = prefixLength === 0 ? 0 : (0xffffffff << (32 - prefixLength)) >>> 0;
|
||||
return (value & mask) >>> 0 === (base & mask) >>> 0;
|
||||
}
|
||||
|
||||
const IPV4_NON_PUBLIC_RANGES: ReadonlyArray<readonly [base: number, prefixLength: number]> = Object.freeze([
|
||||
[0x00000000, 8],
|
||||
[0x0a000000, 8],
|
||||
[0x64400000, 10],
|
||||
[0x7f000000, 8],
|
||||
[0xa9fe0000, 16],
|
||||
[0xac100000, 12],
|
||||
[0xc0000000, 24],
|
||||
[0xc0000200, 24],
|
||||
[0xc0a80000, 16],
|
||||
[0xc6120000, 15],
|
||||
[0xc6336400, 24],
|
||||
[0xcb007100, 24],
|
||||
[0xe0000000, 4],
|
||||
[0xf0000000, 4],
|
||||
]);
|
||||
|
||||
function isPublicIPv4Address(address: string): boolean {
|
||||
const octets = parseIPv4Octets(address);
|
||||
if (octets == null) {
|
||||
return false;
|
||||
}
|
||||
const value = ipv4Value(octets);
|
||||
return !IPV4_NON_PUBLIC_RANGES.some(([base, prefixLength]) => isIPv4InCIDR(value, base, prefixLength));
|
||||
}
|
||||
|
||||
function expandIPv6Groups(address: string): Array<string> {
|
||||
const halves = address.split('::');
|
||||
if (halves.length === 2) {
|
||||
const left = halves[0].length > 0 ? halves[0].split(':') : [];
|
||||
const right = halves[1].length > 0 ? halves[1].split(':') : [];
|
||||
const missing = Math.max(IPV6_GROUP_COUNT - left.length - right.length, 0);
|
||||
return [...left, ...Array<string>(missing).fill('0'), ...right].map((group) => group.padStart(4, '0'));
|
||||
}
|
||||
return address.split(':').map((group) => group.padStart(4, '0'));
|
||||
}
|
||||
|
||||
function ipv4FromMappedIPv6(groups: ReadonlyArray<string>): string | null {
|
||||
const isMapped =
|
||||
groups[0] === '0000' &&
|
||||
groups[1] === '0000' &&
|
||||
groups[2] === '0000' &&
|
||||
groups[3] === '0000' &&
|
||||
groups[4] === '0000' &&
|
||||
groups[5] === 'ffff';
|
||||
if (!isMapped) {
|
||||
return null;
|
||||
}
|
||||
const high = Number.parseInt(groups[6], 16);
|
||||
const low = Number.parseInt(groups[7], 16);
|
||||
return `${(high >> 8) & 0xff}.${high & 0xff}.${(low >> 8) & 0xff}.${low & 0xff}`;
|
||||
}
|
||||
|
||||
function isPublicIPv6Address(address: string): boolean {
|
||||
const groups = expandIPv6Groups(address);
|
||||
if (groups.length !== IPV6_GROUP_COUNT) {
|
||||
return false;
|
||||
}
|
||||
const mapped = ipv4FromMappedIPv6(groups);
|
||||
if (mapped != null) {
|
||||
return isPublicIPv4Address(mapped);
|
||||
}
|
||||
const first = Number.parseInt(groups[0], 16);
|
||||
const second = Number.parseInt(groups[1], 16);
|
||||
const last = Number.parseInt(groups[7], 16);
|
||||
if (groups.slice(0, 7).every((group) => group === '0000') && (last === 0 || last === 1)) {
|
||||
return false;
|
||||
}
|
||||
if ((first & 0xe000) !== 0x2000) {
|
||||
return false;
|
||||
}
|
||||
if ((first & 0xffc0) === 0xfe80) {
|
||||
return false;
|
||||
}
|
||||
if ((first & 0xfe00) === 0xfc00) {
|
||||
return false;
|
||||
}
|
||||
if ((first & 0xff00) === 0xff00) {
|
||||
return false;
|
||||
}
|
||||
return !(first === 0x2001 && second === 0x0db8);
|
||||
const parsed = parseIpAddress(value);
|
||||
return parsed == null ? null : {address: parsed.normalized, family: parsed.family === 'ipv4' ? 4 : 6};
|
||||
}
|
||||
|
||||
function isPublicPinnedAddress(pinned: PinnedAddress): boolean {
|
||||
return pinned.family === 4 ? isPublicIPv4Address(pinned.address) : isPublicIPv6Address(pinned.address);
|
||||
return isPublicIpAddress(pinned.address);
|
||||
}
|
||||
|
||||
function proxiedHostScope(hostname: string): DesktopOriginAddressScope {
|
||||
|
||||
@@ -30,6 +30,7 @@ function transform(name) {
|
||||
}
|
||||
|
||||
const happyEyeballsSource = transform('DesktopHappyEyeballs.ts');
|
||||
const ipAddressSource = transform('../../../packages/ip_utils/src/IpAddress.ts');
|
||||
const outboundSource = transform('DesktopOutboundHTTP.ts');
|
||||
const fileDownloadsSource = transform('FileDownloads.ts');
|
||||
|
||||
@@ -112,6 +113,11 @@ async function loadFileDownloads({registerOrigin = true} = {}) {
|
||||
vm.runInContext(happyEyeballsSource.code, happyEyeballsContext, {filename: happyEyeballsSource.path});
|
||||
stubs['@electron/main/DesktopHappyEyeballs'] = happyEyeballsModule.exports;
|
||||
|
||||
const ipAddressModule = {exports: {}};
|
||||
const ipAddressContext = vm.createContext({...sandbox, exports: ipAddressModule.exports, module: ipAddressModule});
|
||||
vm.runInContext(ipAddressSource.code, ipAddressContext, {filename: ipAddressSource.path});
|
||||
stubs['@fluxer/ip_utils/src/IpAddress'] = ipAddressModule.exports;
|
||||
|
||||
const outboundModule = {exports: {}};
|
||||
sandbox.module = outboundModule;
|
||||
sandbox.exports = outboundModule.exports;
|
||||
|
||||
@@ -11,9 +11,7 @@ const {GatewayOriginRegistry} = await import('./GatewayOriginRegistry.ts');
|
||||
const {DesktopOutboundHTTP} = await import('./DesktopOutboundHTTP.ts');
|
||||
const {RendererDocumentOwnerFactory} = await import('./RendererDocumentOwner.ts');
|
||||
const {parseNativeGatewayTransportEvent} = await import('./NativeGatewayTransportEventParser.ts');
|
||||
const {websocketHTTPOrigin, websocketOrigin} = await import('./WebSocketOrigin.ts');
|
||||
const {getNativeGatewayDisableReason} = await import('./LaunchOptions.ts');
|
||||
const {reconstructNativeGatewayTransportEvent} = await import('../preload/PreloadNativeGatewayEvent.ts');
|
||||
const {createNativeGatewayPreloadAPI} = await import('../preload/NativeGatewayPreload.ts');
|
||||
const {
|
||||
NATIVE_GATEWAY_TRANSPORT_AVAILABLE_RENDERER_ARG,
|
||||
@@ -874,18 +872,6 @@ describe('GatewayOriginRegistry', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('WebSocketOrigin', () => {
|
||||
test('derives both the ws and the http form with default ports collapsed', () => {
|
||||
assert.equal(websocketOrigin('wss://gateway.fluxer.app/socket?v=9'), 'wss://gateway.fluxer.app');
|
||||
assert.equal(websocketOrigin('https://gateway.fluxer.app'), 'wss://gateway.fluxer.app');
|
||||
assert.equal(websocketOrigin('ws://127.0.0.1:8080/'), 'ws://127.0.0.1:8080');
|
||||
assert.equal(websocketOrigin('file:///etc/passwd'), null);
|
||||
assert.equal(websocketHTTPOrigin('wss://gateway.fluxer.app/socket'), 'https://gateway.fluxer.app');
|
||||
assert.equal(websocketHTTPOrigin('ws://192.168.1.5:8080/'), 'http://192.168.1.5:8080');
|
||||
assert.equal(websocketHTTPOrigin('nope'), null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('NativeGatewayTransportEventParser', () => {
|
||||
test('accepts the addon exact seven field shape and normalises binary to ArrayBuffer', () => {
|
||||
const parsed = parseNativeGatewayTransportEvent(
|
||||
@@ -922,59 +908,6 @@ describe('NativeGatewayTransportEventParser', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('PreloadNativeGatewayEvent', () => {
|
||||
test('independently revalidates a well formed main event', () => {
|
||||
const event = {
|
||||
connectionId: nextConnectionId(),
|
||||
kind: 'message',
|
||||
data: '{"op":11}',
|
||||
binary: null,
|
||||
code: null,
|
||||
reason: null,
|
||||
wasClean: null,
|
||||
message: null,
|
||||
};
|
||||
assert.equal(reconstructNativeGatewayTransportEvent(event).data, '{"op":11}');
|
||||
});
|
||||
|
||||
test('rejects a forged connection id, a missing key and an unknown kind', () => {
|
||||
const base = {
|
||||
connectionId: nextConnectionId(),
|
||||
kind: 'open',
|
||||
data: null,
|
||||
binary: null,
|
||||
code: null,
|
||||
reason: null,
|
||||
wasClean: null,
|
||||
message: null,
|
||||
};
|
||||
assert.throws(
|
||||
() => reconstructNativeGatewayTransportEvent({...base, connectionId: 'gateway-renderer-x:y'}),
|
||||
/connection id has an invalid format/u,
|
||||
);
|
||||
const {message: _message, ...missing} = base;
|
||||
assert.throws(() => reconstructNativeGatewayTransportEvent(missing), /exactly the declared keys/u);
|
||||
assert.throws(() => reconstructNativeGatewayTransportEvent({...base, kind: 'voice'}), /event kind is unknown/u);
|
||||
});
|
||||
|
||||
test('normalises a byte view to a standalone ArrayBuffer', () => {
|
||||
const source = new Uint8Array([9, 8, 7, 6]);
|
||||
const event = {
|
||||
connectionId: nextConnectionId(),
|
||||
kind: 'binary',
|
||||
data: null,
|
||||
binary: source.subarray(1, 3),
|
||||
code: null,
|
||||
reason: null,
|
||||
wasClean: null,
|
||||
message: null,
|
||||
};
|
||||
const reconstructed = reconstructNativeGatewayTransportEvent(event);
|
||||
assert.ok(reconstructed.binary instanceof ArrayBuffer);
|
||||
assert.deepEqual([...new Uint8Array(reconstructed.binary)], [8, 7]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('native gateway kill switches', () => {
|
||||
test('names the launch flag, the environment variable and safe mode as disable reasons', () => {
|
||||
assert.equal(getNativeGatewayDisableReason([]), null);
|
||||
|
||||
@@ -69,11 +69,13 @@ describe('reconstructing a native gateway transport event in the preload', () =>
|
||||
});
|
||||
|
||||
test('a binary frame is copied out of a view so the sender cannot mutate it afterwards', () => {
|
||||
const view = new Uint8Array([1, 2, 3, 4]);
|
||||
const source = new Uint8Array([9, 1, 2, 3, 4, 9]);
|
||||
const view = source.subarray(1, 5);
|
||||
const event = reconstructNativeGatewayTransportEvent(
|
||||
frame({kind: NativeGatewayTransportEventKind.BINARY, binary: view}),
|
||||
);
|
||||
view[0] = 99;
|
||||
assert.ok(event.binary instanceof ArrayBuffer);
|
||||
assert.deepEqual(Array.from(new Uint8Array(event.binary)), [1, 2, 3, 4]);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,18 +1,3 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {defineConfig} from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
resolve: {tsconfigPaths: true},
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
include: ['**/*.{test,spec}.{ts,tsx}'],
|
||||
exclude: ['node_modules', 'dist'],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html'],
|
||||
exclude: ['**/*.test.tsx', '**/*.spec.tsx', 'node_modules/'],
|
||||
},
|
||||
},
|
||||
});
|
||||
export {default} from '../../tsconfigs/vitest.base.mts';
|
||||
@@ -13,7 +13,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@fluxer/constants": "workspace:*",
|
||||
"@fluxer/hono_types": "workspace:*",
|
||||
"@fluxer/i18n": "workspace:*",
|
||||
"@fluxer/logger": "workspace:*",
|
||||
"@fluxer/schema": "workspace:*",
|
||||
|
||||
@@ -1,34 +1,29 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes';
|
||||
import {BadRequestError} from '@fluxer/errors/src/domains/core/BadRequestError';
|
||||
import {AppErrorHandler} from '@fluxer/errors/src/domains/core/ErrorHandlers';
|
||||
import {AppErrorHandler, type BaseHonoEnv} from '@fluxer/errors/src/domains/core/ErrorHandlers';
|
||||
import {ServiceUnavailableError} from '@fluxer/errors/src/HttpErrors';
|
||||
import type {BaseHonoEnv} from '@fluxer/hono_types/src/HonoTypes';
|
||||
import {Hono} from 'hono';
|
||||
import {beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
|
||||
type LogCall = [Record<string, unknown>, string | undefined];
|
||||
|
||||
const logCalls = vi.hoisted(() => ({
|
||||
debug: [] as Array<LogCall>,
|
||||
warn: [] as Array<LogCall>,
|
||||
error: [] as Array<LogCall>,
|
||||
}));
|
||||
|
||||
vi.mock('@fluxer/logger/src/Logger', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@fluxer/logger/src/Logger')>();
|
||||
const record = (bucket: Array<LogCall>) => (obj: Record<string, unknown>, msg?: string) => {
|
||||
bucket.push([obj, msg]);
|
||||
};
|
||||
return {
|
||||
...actual,
|
||||
createLogger: () => ({
|
||||
trace: () => {},
|
||||
debug: record(logCalls.debug),
|
||||
debug: () => {},
|
||||
info: () => {},
|
||||
warn: record(logCalls.warn),
|
||||
error: record(logCalls.error),
|
||||
warn: () => {},
|
||||
error: (obj: Record<string, unknown>, msg?: string) => {
|
||||
logCalls.error.push([obj, msg]);
|
||||
},
|
||||
fatal: () => {},
|
||||
}),
|
||||
};
|
||||
@@ -42,8 +37,6 @@ function createApp(): Hono<BaseHonoEnv> {
|
||||
|
||||
describe('AppErrorHandler logging', () => {
|
||||
beforeEach(() => {
|
||||
logCalls.debug.length = 0;
|
||||
logCalls.warn.length = 0;
|
||||
logCalls.error.length = 0;
|
||||
});
|
||||
|
||||
@@ -75,20 +68,6 @@ describe('AppErrorHandler logging', () => {
|
||||
expect(loggedError.cause).toBe(cause);
|
||||
});
|
||||
|
||||
it('logs 4xx FluxerErrors at debug rather than error', async () => {
|
||||
const app = createApp();
|
||||
app.get('/thing', () => {
|
||||
throw new BadRequestError({code: APIErrorCodes.BAD_REQUEST});
|
||||
});
|
||||
const response = await app.request('/thing');
|
||||
expect(response.status).toBe(400);
|
||||
expect(logCalls.error).toHaveLength(0);
|
||||
expect(logCalls.debug).toHaveLength(1);
|
||||
const [details, message] = logCalls.debug[0]!;
|
||||
expect(message).toBe('Request rejected');
|
||||
expect(details.status).toBe(400);
|
||||
});
|
||||
|
||||
it('logs the matched route pattern instead of the request path', async () => {
|
||||
const app = createApp();
|
||||
app.get('/reset/:token', () => {
|
||||
@@ -99,15 +78,4 @@ describe('AppErrorHandler logging', () => {
|
||||
expect(logCalls.error).toHaveLength(1);
|
||||
expect(logCalls.error[0]![0].path).toBe('/reset/:token');
|
||||
});
|
||||
|
||||
it('still reports unexpected errors as unhandled', async () => {
|
||||
const app = createApp();
|
||||
app.get('/thing', () => {
|
||||
throw new Error('boom');
|
||||
});
|
||||
const response = await app.request('/thing');
|
||||
expect(response.status).toBe(500);
|
||||
expect(logCalls.error).toHaveLength(1);
|
||||
expect(logCalls.error[0]![1]).toBe('Unhandled error occurred');
|
||||
});
|
||||
});
|
||||
@@ -3,9 +3,8 @@
|
||||
import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes';
|
||||
import {Locales} from '@fluxer/constants/src/Locales';
|
||||
import {BadRequestError} from '@fluxer/errors/src/domains/core/BadRequestError';
|
||||
import {AppErrorHandler} from '@fluxer/errors/src/domains/core/ErrorHandlers';
|
||||
import {AppErrorHandler, type BaseHonoEnv} from '@fluxer/errors/src/domains/core/ErrorHandlers';
|
||||
import {getErrorMessageUnsafe} from '@fluxer/errors/src/i18n/ErrorI18n';
|
||||
import type {BaseHonoEnv} from '@fluxer/hono_types/src/HonoTypes';
|
||||
import {Logger} from '@fluxer/logger/src/Logger';
|
||||
import {Hono} from 'hono';
|
||||
import {afterEach, describe, expect, it, vi} from 'vitest';
|
||||
|
||||
@@ -20,11 +20,27 @@ import {FluxerError} from '@fluxer/errors/src/FluxerError';
|
||||
import {ErrorCodeToI18nKey} from '@fluxer/errors/src/i18n/ErrorCodeMappings';
|
||||
import {getErrorMessageUnsafe} from '@fluxer/errors/src/i18n/ErrorI18n';
|
||||
import type {ErrorI18nKey} from '@fluxer/errors/src/i18n/ErrorI18nMessages';
|
||||
import type {BaseHonoEnv, ErrorI18nService} from '@fluxer/hono_types/src/HonoTypes';
|
||||
import {createLogger} from '@fluxer/logger/src/Logger';
|
||||
import type {Context} from 'hono';
|
||||
import {HTTPException} from 'hono/http-exception';
|
||||
|
||||
interface ErrorI18nService {
|
||||
getMessage(
|
||||
key: string,
|
||||
locale: string | null | undefined,
|
||||
variables?: Record<string, unknown>,
|
||||
fallbackMessage?: string,
|
||||
): string;
|
||||
}
|
||||
|
||||
export interface BaseHonoEnv {
|
||||
Variables: {
|
||||
errorI18nService?: ErrorI18nService;
|
||||
requestLocale?: string;
|
||||
requestId?: string;
|
||||
};
|
||||
}
|
||||
|
||||
const logger = createLogger('errors');
|
||||
const LOCALE_LOOKUP = new Map<string, string>(Object.values(Locales).map((locale) => [locale.toLowerCase(), locale]));
|
||||
|
||||
|
||||
@@ -1,18 +1,3 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {defineConfig} from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
resolve: {tsconfigPaths: true},
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
include: ['**/*.{test,spec}.{ts,tsx}'],
|
||||
exclude: ['node_modules', 'dist'],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html'],
|
||||
exclude: ['**/*.test.tsx', '**/*.spec.tsx', 'node_modules/'],
|
||||
},
|
||||
},
|
||||
});
|
||||
export {default} from '../../tsconfigs/vitest.base.mts';
|
||||
@@ -1,27 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
const ASCII_UPPER_A = 65;
|
||||
const ASCII_UPPER_Z = 90;
|
||||
const REGION_CODE_LENGTH = 2;
|
||||
|
||||
function isAsciiUpperAlpha2(value: string): boolean {
|
||||
return (
|
||||
value.length === REGION_CODE_LENGTH &&
|
||||
value.charCodeAt(0) >= ASCII_UPPER_A &&
|
||||
value.charCodeAt(0) <= ASCII_UPPER_Z &&
|
||||
value.charCodeAt(1) >= ASCII_UPPER_A &&
|
||||
value.charCodeAt(1) <= ASCII_UPPER_Z
|
||||
);
|
||||
}
|
||||
|
||||
export function normalizeRegionCode(regionCode: string): string | undefined {
|
||||
const trimmedRegionCode = regionCode.trim();
|
||||
if (trimmedRegionCode.length !== REGION_CODE_LENGTH) {
|
||||
return undefined;
|
||||
}
|
||||
const upperRegionCode = trimmedRegionCode.toUpperCase();
|
||||
if (!isAsciiUpperAlpha2(upperRegionCode)) {
|
||||
return undefined;
|
||||
}
|
||||
return upperRegionCode;
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {Locales} from '@fluxer/constants/src/Locales';
|
||||
import {normalizeRegionCode} from '@fluxer/geo_utils/src/RegionCodeValidation';
|
||||
|
||||
const DISPLAY_NAME_TYPE: Intl.DisplayNamesOptions['type'] = 'region';
|
||||
const DISPLAY_NAME_FALLBACK: Intl.DisplayNamesOptions['fallback'] = 'none';
|
||||
const displayNamesByLocale = new Map<string, Intl.DisplayNames>();
|
||||
|
||||
function resolveLocale(locale?: string): string {
|
||||
const trimmedLocale = locale?.trim();
|
||||
if (trimmedLocale && trimmedLocale.length > 0) {
|
||||
return trimmedLocale;
|
||||
}
|
||||
return Locales.EN_US;
|
||||
}
|
||||
|
||||
function getDisplayNames(locale?: string): Intl.DisplayNames {
|
||||
const localeCode = resolveLocale(locale);
|
||||
const cachedDisplayNames = displayNamesByLocale.get(localeCode);
|
||||
if (cachedDisplayNames) {
|
||||
return cachedDisplayNames;
|
||||
}
|
||||
const displayNames = new Intl.DisplayNames([localeCode], {
|
||||
type: DISPLAY_NAME_TYPE,
|
||||
fallback: DISPLAY_NAME_FALLBACK,
|
||||
});
|
||||
displayNamesByLocale.set(localeCode, displayNames);
|
||||
return displayNames;
|
||||
}
|
||||
|
||||
function resolveRegionDisplayNameFromFormatter(
|
||||
regionCode: string,
|
||||
displayNames: Intl.DisplayNames,
|
||||
): string | undefined {
|
||||
const normalizedRegionCode = normalizeRegionCode(regionCode);
|
||||
if (!normalizedRegionCode) {
|
||||
return undefined;
|
||||
}
|
||||
return displayNames.of(normalizedRegionCode) ?? undefined;
|
||||
}
|
||||
|
||||
export function resolveRegionDisplayName(regionCode: string, locale?: string): string | undefined {
|
||||
const displayNames = getDisplayNames(locale);
|
||||
return resolveRegionDisplayNameFromFormatter(regionCode, displayNames);
|
||||
}
|
||||
@@ -1,33 +1,28 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {normalizeRegionCode} from '@fluxer/geo_utils/src/RegionCodeValidation';
|
||||
import {resolveRegionDisplayName} from '@fluxer/geo_utils/src/RegionDisplayNameResolver';
|
||||
import {Locales} from '@fluxer/constants/src/Locales';
|
||||
|
||||
interface RegionDisplayNameOptions {
|
||||
locale?: string;
|
||||
fallbackToRegionCode?: boolean;
|
||||
const displayNamesByLocale = new Map<string, Intl.DisplayNames>();
|
||||
|
||||
function getDisplayNames(locale?: string): Intl.DisplayNames {
|
||||
const localeCode = locale?.trim() || Locales.EN_US;
|
||||
let displayNames = displayNamesByLocale.get(localeCode);
|
||||
if (!displayNames) {
|
||||
displayNames = new Intl.DisplayNames([localeCode], {type: 'region', fallback: 'none'});
|
||||
displayNamesByLocale.set(localeCode, displayNames);
|
||||
}
|
||||
return displayNames;
|
||||
}
|
||||
|
||||
function applyRegionCodeFallback(
|
||||
regionCode: string,
|
||||
displayName: string | undefined,
|
||||
options?: RegionDisplayNameOptions,
|
||||
): string | undefined {
|
||||
if (displayName) {
|
||||
return displayName;
|
||||
}
|
||||
if (!options?.fallbackToRegionCode) {
|
||||
export function getRegionDisplayName(regionCode: string, options?: {locale?: string}): string | undefined {
|
||||
const displayNames = getDisplayNames(options?.locale);
|
||||
const trimmedRegionCode = regionCode.trim();
|
||||
if (trimmedRegionCode.length !== 2) {
|
||||
return undefined;
|
||||
}
|
||||
const normalizedRegionCode = normalizeRegionCode(regionCode);
|
||||
if (normalizedRegionCode) {
|
||||
return normalizedRegionCode;
|
||||
const upperRegionCode = trimmedRegionCode.toUpperCase();
|
||||
if (!/^[A-Z]{2}$/.test(upperRegionCode)) {
|
||||
return undefined;
|
||||
}
|
||||
const trimmedRegionCode = regionCode.trim();
|
||||
return trimmedRegionCode.length > 0 ? trimmedRegionCode : undefined;
|
||||
}
|
||||
|
||||
export function getRegionDisplayName(regionCode: string, options?: RegionDisplayNameOptions): string | undefined {
|
||||
const displayName = resolveRegionDisplayName(regionCode, options?.locale);
|
||||
return applyRegionCodeFallback(regionCode, displayName, options);
|
||||
return displayNames.of(upperRegionCode) || undefined;
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"name": "@fluxer/hono_types",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "catalog:ts7"
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
export interface ErrorI18nService {
|
||||
getMessage(
|
||||
key: string,
|
||||
locale: string | null | undefined,
|
||||
variables?: Record<string, unknown>,
|
||||
fallbackMessage?: string,
|
||||
): string;
|
||||
}
|
||||
|
||||
export interface BaseHonoEnv {
|
||||
Variables: {
|
||||
errorI18nService?: ErrorI18nService;
|
||||
requestLocale?: string;
|
||||
requestId?: string;
|
||||
};
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfigs/package.json",
|
||||
"compilerOptions": {
|
||||
"types": []
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
@@ -1,18 +1,3 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {defineConfig} from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
resolve: {tsconfigPaths: true},
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
include: ['**/*.{test,spec}.{ts,tsx}'],
|
||||
exclude: ['node_modules', 'dist'],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html'],
|
||||
exclude: ['**/*.test.tsx', '**/*.spec.tsx', 'node_modules/'],
|
||||
},
|
||||
},
|
||||
});
|
||||
export {default} from '../../tsconfigs/vitest.base.mts';
|
||||
@@ -1,18 +1,3 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {defineConfig} from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
resolve: {tsconfigPaths: true},
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
include: ['**/*.{test,spec}.{ts,tsx}'],
|
||||
exclude: ['node_modules', 'dist'],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html'],
|
||||
exclude: ['**/*.test.tsx', '**/*.spec.tsx', 'node_modules/'],
|
||||
},
|
||||
},
|
||||
});
|
||||
export {default} from '../../tsconfigs/vitest.base.mts';
|
||||
@@ -1,18 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
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';
|
||||
|
||||
const WIRE_COMPATIBILITY_LIMIT_KEYS: ReadonlySet<LimitKey> = new Set<LimitKey>([
|
||||
'max_guild_emojis',
|
||||
'max_guild_emojis_animated_more',
|
||||
'max_guild_emojis_animated',
|
||||
'max_guild_emojis_static_more',
|
||||
'max_guild_emojis_static',
|
||||
'max_guild_stickers_more',
|
||||
'max_guild_stickers',
|
||||
]);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private addWireCompatibilityOverrides(
|
||||
overrides: Partial<Record<LimitKey, number>>,
|
||||
fullLimits: Partial<Record<LimitKey, number>>,
|
||||
): Partial<Record<LimitKey, number>> {
|
||||
for (const key of WIRE_COMPATIBILITY_LIMIT_KEYS) {
|
||||
const value = fullLimits[key];
|
||||
if (value !== undefined) {
|
||||
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.addWireCompatibilityOverrides(this.computeOverrides(rule.limits, defaults), rule.limits);
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,76 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import type {LimitKey} from '@fluxer/constants/src/LimitConfigMetadata';
|
||||
import {LimitConfigCodec} from '@fluxer/limits/src/LimitConfigCodec';
|
||||
import {LIMIT_KEYS, type LimitKey} from '@fluxer/constants/src/LimitConfigMetadata';
|
||||
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';
|
||||
|
||||
const defaultLimitConfigCodec = new LimitConfigCodec();
|
||||
const WIRE_COMPATIBILITY_LIMIT_KEYS: ReadonlySet<LimitKey> = new Set<LimitKey>([
|
||||
'max_guild_emojis',
|
||||
'max_guild_emojis_animated_more',
|
||||
'max_guild_emojis_animated',
|
||||
'max_guild_emojis_static_more',
|
||||
'max_guild_emojis_static',
|
||||
'max_guild_stickers_more',
|
||||
'max_guild_stickers',
|
||||
]);
|
||||
|
||||
export function computeOverrides(
|
||||
fullLimits: Partial<Record<LimitKey, number>>,
|
||||
defaults: Record<LimitKey, number>,
|
||||
): Partial<Record<LimitKey, number>> {
|
||||
return defaultLimitConfigCodec.computeOverrides(fullLimits, defaults);
|
||||
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;
|
||||
}
|
||||
|
||||
export function computeWireFormat(config: LimitConfigSnapshot): LimitConfigWireFormat {
|
||||
return defaultLimitConfigCodec.toWireFormat(config);
|
||||
const defaultsHash = computeDefaultsHash();
|
||||
const rules = config.rules.map((rule) => {
|
||||
const overrides = computeOverrides(rule.limits, DEFAULT_FREE_LIMITS);
|
||||
for (const key of WIRE_COMPATIBILITY_LIMIT_KEYS) {
|
||||
const value = rule.limits[key];
|
||||
if (value !== undefined) {
|
||||
overrides[key] = value;
|
||||
}
|
||||
}
|
||||
return {
|
||||
id: rule.id,
|
||||
filters: rule.filters,
|
||||
overrides,
|
||||
};
|
||||
});
|
||||
return {
|
||||
version: 2,
|
||||
traitDefinitions: config.traitDefinitions,
|
||||
rules,
|
||||
defaultsHash,
|
||||
};
|
||||
}
|
||||
|
||||
export function expandWireFormat(wireFormat: LimitConfigWireFormat): LimitConfigSnapshot {
|
||||
return defaultLimitConfigCodec.fromWireFormat(wireFormat);
|
||||
const rules = wireFormat.rules.map((rule) => {
|
||||
const limits: Partial<Record<LimitKey, number>> = {
|
||||
...DEFAULT_FREE_LIMITS,
|
||||
...rule.overrides,
|
||||
};
|
||||
return {
|
||||
id: rule.id,
|
||||
filters: rule.filters,
|
||||
limits,
|
||||
};
|
||||
});
|
||||
return {
|
||||
version: wireFormat.version,
|
||||
traitDefinitions: wireFormat.traitDefinitions,
|
||||
rules,
|
||||
};
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
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];
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import type {LimitKey} from '@fluxer/constants/src/LimitConfigMetadata';
|
||||
import type {ILimitEvaluator} from '@fluxer/limits/src/ILimitEvaluator';
|
||||
import {LimitEvaluator} from '@fluxer/limits/src/LimitEvaluator';
|
||||
import {DEFAULT_FREE_LIMITS} from '@fluxer/limits/src/LimitDefaults';
|
||||
import {applyRuleToResolvedLimits, ruleMatches, sortRulesBySpecificity} from '@fluxer/limits/src/LimitRuleRuntime';
|
||||
import type {
|
||||
LimitConfigSnapshot,
|
||||
LimitEvaluationOptions,
|
||||
@@ -10,17 +10,19 @@ import type {
|
||||
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);
|
||||
const evaluationContext = options?.evaluationContext ?? 'user';
|
||||
const resolvedLimits = {...(options?.baseLimits ?? DEFAULT_FREE_LIMITS)};
|
||||
for (const rule of sortRulesBySpecificity(snapshot.rules)) {
|
||||
if (ruleMatches(rule.filters, ctx)) {
|
||||
applyRuleToResolvedLimits(resolvedLimits, rule, evaluationContext);
|
||||
}
|
||||
}
|
||||
return {limits: resolvedLimits};
|
||||
}
|
||||
|
||||
export function resolveLimit(
|
||||
@@ -29,6 +31,5 @@ export function resolveLimit(
|
||||
key: LimitKey,
|
||||
options?: LimitEvaluationOptions,
|
||||
): number {
|
||||
const evaluator = createLimitEvaluator(snapshot);
|
||||
return evaluator.resolveOne(ctx, key, options);
|
||||
return resolveLimits(snapshot, ctx, options).limits[key];
|
||||
}
|
||||
@@ -1,18 +1,3 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {defineConfig} from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
resolve: {tsconfigPaths: true},
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
include: ['**/*.{test,spec}.{ts,tsx}'],
|
||||
exclude: ['node_modules', 'dist'],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html'],
|
||||
exclude: ['**/*.test.tsx', '**/*.spec.tsx', 'node_modules/'],
|
||||
},
|
||||
},
|
||||
});
|
||||
export {default} from '../../tsconfigs/vitest.base.mts';
|
||||
@@ -3,50 +3,10 @@
|
||||
import {
|
||||
ExperimentRolloutCountryCodesSchema,
|
||||
type ExperimentTargeting,
|
||||
experimentBucket,
|
||||
experimentRolloutCountryIncludes,
|
||||
} from '@fluxer/schema/src/domains/experiment/ExperimentBucket';
|
||||
import {describe, expect, test} from 'vitest';
|
||||
|
||||
const TARGETED_USER_ID = '1000000000000000001';
|
||||
const OTHER_USER_ID = '1000000000000000002';
|
||||
|
||||
function syntheticUserIds(count: number): Array<string> {
|
||||
const ids: Array<string> = [];
|
||||
for (let index = 0; index < count; index++) {
|
||||
ids.push((1400000000000000000n + BigInt(index)).toString());
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
describe('experimentBucket', () => {
|
||||
test('stays inside the basis point range for every synthetic id', () => {
|
||||
for (const userId of syntheticUserIds(500)) {
|
||||
const bucket = experimentBucket(userId, 'voice-ns-v1');
|
||||
expect(Number.isInteger(bucket)).toBe(true);
|
||||
expect(bucket).toBeGreaterThanOrEqual(0);
|
||||
expect(bucket).toBeLessThan(10000);
|
||||
}
|
||||
});
|
||||
|
||||
test.each([
|
||||
{userId: TARGETED_USER_ID, salt: 'voice-ns-v1', expected: 8241},
|
||||
{userId: TARGETED_USER_ID, salt: 'voice-ns-v2', expected: 8500},
|
||||
{userId: OTHER_USER_ID, salt: 'voice-ns-v1', expected: 5384},
|
||||
{userId: OTHER_USER_ID, salt: 'voice-ns-v2', expected: 1357},
|
||||
])('preserves the assignment bucket for $userId with salt $salt', ({userId, salt, expected}) => {
|
||||
expect(experimentBucket(userId, salt)).toBe(expected);
|
||||
});
|
||||
|
||||
test('changes with the salt for at least most user ids', () => {
|
||||
const userIds = syntheticUserIds(200);
|
||||
const changed = userIds.filter(
|
||||
(userId) => experimentBucket(userId, 'voice-ns-v1') !== experimentBucket(userId, 'voice-ns-v2'),
|
||||
);
|
||||
expect(changed.length).toBeGreaterThan(userIds.length - 5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ExperimentRolloutCountryCodesSchema', () => {
|
||||
test('accepts unique uppercase alpha-2 codes', () => {
|
||||
expect(ExperimentRolloutCountryCodesSchema.parse(['SE', 'BR'])).toEqual(['SE', 'BR']);
|
||||
|
||||
@@ -1,18 +1,3 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {defineConfig} from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
resolve: {tsconfigPaths: true},
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
include: ['**/*.{test,spec}.{ts,tsx}'],
|
||||
exclude: ['node_modules', 'dist'],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html'],
|
||||
exclude: ['**/*.test.tsx', '**/*.spec.tsx', 'node_modules/'],
|
||||
},
|
||||
},
|
||||
});
|
||||
export {default} from '../../tsconfigs/vitest.base.mts';
|
||||
@@ -16,7 +16,11 @@ function toClampedTimestamp(timestamp: number): number {
|
||||
}
|
||||
|
||||
export function extractTimestamp(snowflake: string): number {
|
||||
return extractTimestampFromSnowflake(snowflake);
|
||||
try {
|
||||
return extractTimestampWithEpoch(BigInt(snowflake), FLUXER_EPOCH);
|
||||
} catch (_error) {
|
||||
return Number.NaN;
|
||||
}
|
||||
}
|
||||
|
||||
export function extractTimestampBigInt(snowflake: bigint): number {
|
||||
@@ -86,17 +90,8 @@ export function age(snowflake: string): number {
|
||||
return Date.now() - timestamp;
|
||||
}
|
||||
|
||||
export function extractTimestampFromSnowflake(snowflake: string, epoch?: string | bigint): number {
|
||||
try {
|
||||
const epochBigInt = epoch != null ? (typeof epoch === 'string' ? BigInt(epoch) : epoch) : FLUXER_EPOCH;
|
||||
return extractTimestampWithEpoch(BigInt(snowflake), epochBigInt);
|
||||
} catch (_error) {
|
||||
return Number.NaN;
|
||||
}
|
||||
}
|
||||
|
||||
export function extractTimestampFromSnowflakeAsDate(snowflake: string, epoch?: string | bigint): Date {
|
||||
const timestamp = extractTimestampFromSnowflake(snowflake, epoch);
|
||||
export function extractTimestampFromSnowflakeAsDate(snowflake: string): Date {
|
||||
const timestamp = extractTimestamp(snowflake);
|
||||
if (Number.isNaN(timestamp)) {
|
||||
return new Date();
|
||||
}
|
||||
|
||||
@@ -1,18 +1,3 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {defineConfig} from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
resolve: {tsconfigPaths: true},
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
include: ['**/*.{test,spec}.{ts,tsx}'],
|
||||
exclude: ['node_modules', 'dist'],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html'],
|
||||
exclude: ['**/*.test.tsx', '**/*.spec.tsx', 'node_modules/'],
|
||||
},
|
||||
},
|
||||
});
|
||||
export {default} from '../../tsconfigs/vitest.base.mts';
|
||||
Generated
-9
@@ -1938,9 +1938,6 @@ importers:
|
||||
'@fluxer/constants':
|
||||
specifier: workspace:*
|
||||
version: link:../constants
|
||||
'@fluxer/hono_types':
|
||||
specifier: workspace:*
|
||||
version: link:../hono_types
|
||||
'@fluxer/i18n':
|
||||
specifier: workspace:*
|
||||
version: link:../i18n
|
||||
@@ -2008,12 +2005,6 @@ importers:
|
||||
specifier: 'catalog:'
|
||||
version: 5.0.1(@opentelemetry/[email protected])(@types/[email protected])(@vitest/[email protected])([email protected])([email protected](@types/[email protected])([email protected]))([email protected](@types/[email protected])([email protected])([email protected])([email protected])([email protected]))
|
||||
|
||||
packages/hono_types:
|
||||
devDependencies:
|
||||
typescript:
|
||||
specifier: catalog:ts7
|
||||
version: 7.0.2
|
||||
|
||||
packages/i18n:
|
||||
dependencies:
|
||||
'@messageformat/core':
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
export default {
|
||||
resolve: {tsconfigPaths: true},
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
include: ['**/*.{test,spec}.{ts,tsx}'],
|
||||
exclude: ['node_modules', 'dist'],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html'],
|
||||
exclude: ['**/*.test.tsx', '**/*.spec.tsx', 'node_modules/'],
|
||||
},
|
||||
},
|
||||
};
|
||||
Reference in new issue
Block a user