From 575a5adbeff0b46390fb08901c7166649255204c Mon Sep 17 00:00:00 2001 From: Hampus Date: Sat, 10 Oct 2026 23:23:34 +0200 Subject: [PATCH] refactor(pkgs): share vitest config and dedupe helpers (#3384) --- .devcontainer/docker-compose.yml | 6 - .github/labeller.yaml | 3 - fluxer_api/pkgs/cache/vitest.config.ts | 17 +-- fluxer_api/pkgs/http_client/vitest.config.ts | 17 +-- fluxer_api/pkgs/kv_client/vitest.config.ts | 17 +-- fluxer_api/pkgs/locale/vitest.config.ts | 17 +-- .../pkgs/media_proxy_utils/vitest.config.ts | 17 +-- fluxer_api/pkgs/rate_limit/vitest.config.ts | 17 +-- .../GuildMemberSearchSerializer.ts | 4 +- fluxer_api/tsconfig.json | 1 + fluxer_app/src/app/I18n.ts | 44 +----- .../features/user/state/PrivacyPreferences.ts | 4 +- .../src/main/DesktopOutboundHTTP.ts | 141 +----------------- .../src/main/FileDownloads.test.mjs | 6 + .../src/main/NativeGatewayTransport.test.mjs | 67 --------- .../PreloadNativeGatewayEvent.test.mjs | 4 +- packages/config/vitest.config.ts | 17 +-- packages/errors/package.json | 1 - .../__tests__/AppErrorHandlerLogging.test.ts | 44 +----- .../src/__tests__/AppErrorHandlers.test.ts | 3 +- .../errors/src/domains/core/ErrorHandlers.ts | 18 ++- packages/errors/vitest.config.ts | 17 +-- .../geo_utils/src/RegionCodeValidation.ts | 27 ---- .../src/RegionDisplayNameResolver.ts | 46 ------ packages/geo_utils/src/RegionFormatting.ts | 43 +++--- packages/hono_types/package.json | 12 -- packages/hono_types/src/HonoTypes.ts | 18 --- packages/hono_types/tsconfig.json | 7 - packages/i18n/vitest.config.ts | 17 +-- packages/ip_utils/vitest.config.ts | 17 +-- packages/limits/src/ILimitConfigCodec.ts | 18 --- packages/limits/src/ILimitEvaluator.ts | 9 -- packages/limits/src/LimitConfigCodec.ts | 88 ----------- packages/limits/src/LimitDiffer.ts | 66 +++++++- packages/limits/src/LimitEvaluator.ts | 40 ----- packages/limits/src/LimitResolver.ts | 21 +-- packages/logger/vitest.config.ts | 17 +-- .../experiment/ExperimentBucket.test.ts | 40 ----- packages/schema/vitest.config.ts | 17 +-- packages/snowflake/src/SnowflakeUtils.ts | 19 +-- packages/snowflake/vitest.config.ts | 17 +-- pnpm-lock.yaml | 9 -- tsconfigs/vitest.base.mts | 16 ++ 43 files changed, 172 insertions(+), 874 deletions(-) delete mode 100644 packages/geo_utils/src/RegionCodeValidation.ts delete mode 100644 packages/geo_utils/src/RegionDisplayNameResolver.ts delete mode 100644 packages/hono_types/package.json delete mode 100644 packages/hono_types/src/HonoTypes.ts delete mode 100644 packages/hono_types/tsconfig.json delete mode 100644 packages/limits/src/ILimitConfigCodec.ts delete mode 100644 packages/limits/src/ILimitEvaluator.ts delete mode 100644 packages/limits/src/LimitConfigCodec.ts delete mode 100644 packages/limits/src/LimitEvaluator.ts create mode 100644 tsconfigs/vitest.base.mts diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 0fdfcfb8f..8fd2394f8 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -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: diff --git a/.github/labeller.yaml b/.github/labeller.yaml index 3f981f6b6..7b67193a6 100644 --- a/.github/labeller.yaml +++ b/.github/labeller.yaml @@ -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/**/* diff --git a/fluxer_api/pkgs/cache/vitest.config.ts b/fluxer_api/pkgs/cache/vitest.config.ts index f94671ff6..bd00766f4 100644 --- a/fluxer_api/pkgs/cache/vitest.config.ts +++ b/fluxer_api/pkgs/cache/vitest.config.ts @@ -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'; diff --git a/fluxer_api/pkgs/http_client/vitest.config.ts b/fluxer_api/pkgs/http_client/vitest.config.ts index f94671ff6..bd00766f4 100644 --- a/fluxer_api/pkgs/http_client/vitest.config.ts +++ b/fluxer_api/pkgs/http_client/vitest.config.ts @@ -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'; diff --git a/fluxer_api/pkgs/kv_client/vitest.config.ts b/fluxer_api/pkgs/kv_client/vitest.config.ts index f94671ff6..bd00766f4 100644 --- a/fluxer_api/pkgs/kv_client/vitest.config.ts +++ b/fluxer_api/pkgs/kv_client/vitest.config.ts @@ -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'; diff --git a/fluxer_api/pkgs/locale/vitest.config.ts b/fluxer_api/pkgs/locale/vitest.config.ts index f94671ff6..bd00766f4 100644 --- a/fluxer_api/pkgs/locale/vitest.config.ts +++ b/fluxer_api/pkgs/locale/vitest.config.ts @@ -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'; diff --git a/fluxer_api/pkgs/media_proxy_utils/vitest.config.ts b/fluxer_api/pkgs/media_proxy_utils/vitest.config.ts index f94671ff6..bd00766f4 100644 --- a/fluxer_api/pkgs/media_proxy_utils/vitest.config.ts +++ b/fluxer_api/pkgs/media_proxy_utils/vitest.config.ts @@ -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'; diff --git a/fluxer_api/pkgs/rate_limit/vitest.config.ts b/fluxer_api/pkgs/rate_limit/vitest.config.ts index f94671ff6..bd00766f4 100644 --- a/fluxer_api/pkgs/rate_limit/vitest.config.ts +++ b/fluxer_api/pkgs/rate_limit/vitest.config.ts @@ -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'; diff --git a/fluxer_api/src/api/search/guild_member/GuildMemberSearchSerializer.ts b/fluxer_api/src/api/search/guild_member/GuildMemberSearchSerializer.ts index a35d4af5c..fa88d7a4d 100644 --- a/fluxer_api/src/api/search/guild_member/GuildMemberSearchSerializer.ts +++ b/fluxer_api/src/api/search/guild_member/GuildMemberSearchSerializer.ts @@ -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, }; } diff --git a/fluxer_api/tsconfig.json b/fluxer_api/tsconfig.json index 4157ada37..73a32d9a8 100644 --- a/fluxer_api/tsconfig.json +++ b/fluxer_api/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "./tsconfig.base.json", "compilerOptions": { + "allowImportingTsExtensions": true, "paths": { "@app/*": ["./src/*"], "@fluxer/*": ["./../packages/*", "./../packages/*/src/index.ts"], diff --git a/fluxer_app/src/app/I18n.ts b/fluxer_app/src/app/I18n.ts index f7c16bebb..a08d19282 100644 --- a/fluxer_app/src/app/I18n.ts +++ b/fluxer_app/src/app/I18n.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(supportedLocales); +const supportedLocaleSet = new Set(AllLocales); const logger = new Logger('i18n'); const LANGUAGE_OVERRIDES: Record = { 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; } diff --git a/fluxer_app/src/features/user/state/PrivacyPreferences.ts b/fluxer_app/src/features/user/state/PrivacyPreferences.ts index f3f241a02..a971d6e00 100644 --- a/fluxer_app/src/features/user/state/PrivacyPreferences.ts +++ b/fluxer_app/src/features/user/state/PrivacyPreferences.ts @@ -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; } diff --git a/fluxer_desktop/src/main/DesktopOutboundHTTP.ts b/fluxer_desktop/src/main/DesktopOutboundHTTP.ts index 85d5e8c62..245de0424 100644 --- a/fluxer_desktop/src/main/DesktopOutboundHTTP.ts +++ b/fluxer_desktop/src/main/DesktopOutboundHTTP.ts @@ -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 | 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 { - 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 = 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 { - 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(missing).fill('0'), ...right].map((group) => group.padStart(4, '0')); - } - return address.split(':').map((group) => group.padStart(4, '0')); -} - -function ipv4FromMappedIPv6(groups: ReadonlyArray): 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 { diff --git a/fluxer_desktop/src/main/FileDownloads.test.mjs b/fluxer_desktop/src/main/FileDownloads.test.mjs index 1b6c5f5ca..d71383e80 100644 --- a/fluxer_desktop/src/main/FileDownloads.test.mjs +++ b/fluxer_desktop/src/main/FileDownloads.test.mjs @@ -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; diff --git a/fluxer_desktop/src/main/NativeGatewayTransport.test.mjs b/fluxer_desktop/src/main/NativeGatewayTransport.test.mjs index 751e8f333..5d9935a80 100644 --- a/fluxer_desktop/src/main/NativeGatewayTransport.test.mjs +++ b/fluxer_desktop/src/main/NativeGatewayTransport.test.mjs @@ -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); diff --git a/fluxer_desktop/src/preload/PreloadNativeGatewayEvent.test.mjs b/fluxer_desktop/src/preload/PreloadNativeGatewayEvent.test.mjs index b6028bc6b..847a70ae1 100644 --- a/fluxer_desktop/src/preload/PreloadNativeGatewayEvent.test.mjs +++ b/fluxer_desktop/src/preload/PreloadNativeGatewayEvent.test.mjs @@ -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]); }); diff --git a/packages/config/vitest.config.ts b/packages/config/vitest.config.ts index f94671ff6..99766af5f 100644 --- a/packages/config/vitest.config.ts +++ b/packages/config/vitest.config.ts @@ -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'; diff --git a/packages/errors/package.json b/packages/errors/package.json index e1d4d47ab..c985519be 100644 --- a/packages/errors/package.json +++ b/packages/errors/package.json @@ -13,7 +13,6 @@ }, "dependencies": { "@fluxer/constants": "workspace:*", - "@fluxer/hono_types": "workspace:*", "@fluxer/i18n": "workspace:*", "@fluxer/logger": "workspace:*", "@fluxer/schema": "workspace:*", diff --git a/packages/errors/src/__tests__/AppErrorHandlerLogging.test.ts b/packages/errors/src/__tests__/AppErrorHandlerLogging.test.ts index 103d960bf..9181c8a22 100644 --- a/packages/errors/src/__tests__/AppErrorHandlerLogging.test.ts +++ b/packages/errors/src/__tests__/AppErrorHandlerLogging.test.ts @@ -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 | undefined]; const logCalls = vi.hoisted(() => ({ - debug: [] as Array, - warn: [] as Array, error: [] as Array, })); vi.mock('@fluxer/logger/src/Logger', async (importOriginal) => { const actual = await importOriginal(); - const record = (bucket: Array) => (obj: Record, 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, msg?: string) => { + logCalls.error.push([obj, msg]); + }, fatal: () => {}, }), }; @@ -42,8 +37,6 @@ function createApp(): Hono { 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'); - }); }); diff --git a/packages/errors/src/__tests__/AppErrorHandlers.test.ts b/packages/errors/src/__tests__/AppErrorHandlers.test.ts index f7347b0ca..28939bfc1 100644 --- a/packages/errors/src/__tests__/AppErrorHandlers.test.ts +++ b/packages/errors/src/__tests__/AppErrorHandlers.test.ts @@ -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'; diff --git a/packages/errors/src/domains/core/ErrorHandlers.ts b/packages/errors/src/domains/core/ErrorHandlers.ts index 2d8346014..d350f047b 100644 --- a/packages/errors/src/domains/core/ErrorHandlers.ts +++ b/packages/errors/src/domains/core/ErrorHandlers.ts @@ -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, + fallbackMessage?: string, + ): string; +} + +export interface BaseHonoEnv { + Variables: { + errorI18nService?: ErrorI18nService; + requestLocale?: string; + requestId?: string; + }; +} + const logger = createLogger('errors'); const LOCALE_LOOKUP = new Map(Object.values(Locales).map((locale) => [locale.toLowerCase(), locale])); diff --git a/packages/errors/vitest.config.ts b/packages/errors/vitest.config.ts index f94671ff6..99766af5f 100644 --- a/packages/errors/vitest.config.ts +++ b/packages/errors/vitest.config.ts @@ -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'; diff --git a/packages/geo_utils/src/RegionCodeValidation.ts b/packages/geo_utils/src/RegionCodeValidation.ts deleted file mode 100644 index d67684c14..000000000 --- a/packages/geo_utils/src/RegionCodeValidation.ts +++ /dev/null @@ -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; -} diff --git a/packages/geo_utils/src/RegionDisplayNameResolver.ts b/packages/geo_utils/src/RegionDisplayNameResolver.ts deleted file mode 100644 index ee966c4c6..000000000 --- a/packages/geo_utils/src/RegionDisplayNameResolver.ts +++ /dev/null @@ -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(); - -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); -} diff --git a/packages/geo_utils/src/RegionFormatting.ts b/packages/geo_utils/src/RegionFormatting.ts index ec505e6e3..4730a0a1e 100644 --- a/packages/geo_utils/src/RegionFormatting.ts +++ b/packages/geo_utils/src/RegionFormatting.ts @@ -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(); + +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; } diff --git a/packages/hono_types/package.json b/packages/hono_types/package.json deleted file mode 100644 index ad7f2f33f..000000000 --- a/packages/hono_types/package.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "@fluxer/hono_types", - "version": "0.0.0", - "private": true, - "type": "module", - "scripts": { - "typecheck": "tsc --noEmit" - }, - "devDependencies": { - "typescript": "catalog:ts7" - } -} diff --git a/packages/hono_types/src/HonoTypes.ts b/packages/hono_types/src/HonoTypes.ts deleted file mode 100644 index 864a24e4e..000000000 --- a/packages/hono_types/src/HonoTypes.ts +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -export interface ErrorI18nService { - getMessage( - key: string, - locale: string | null | undefined, - variables?: Record, - fallbackMessage?: string, - ): string; -} - -export interface BaseHonoEnv { - Variables: { - errorI18nService?: ErrorI18nService; - requestLocale?: string; - requestId?: string; - }; -} diff --git a/packages/hono_types/tsconfig.json b/packages/hono_types/tsconfig.json deleted file mode 100644 index bf76f9263..000000000 --- a/packages/hono_types/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "../../tsconfigs/package.json", - "compilerOptions": { - "types": [] - }, - "include": ["src/**/*"] -} diff --git a/packages/i18n/vitest.config.ts b/packages/i18n/vitest.config.ts index f94671ff6..99766af5f 100644 --- a/packages/i18n/vitest.config.ts +++ b/packages/i18n/vitest.config.ts @@ -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'; diff --git a/packages/ip_utils/vitest.config.ts b/packages/ip_utils/vitest.config.ts index f94671ff6..99766af5f 100644 --- a/packages/ip_utils/vitest.config.ts +++ b/packages/ip_utils/vitest.config.ts @@ -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'; diff --git a/packages/limits/src/ILimitConfigCodec.ts b/packages/limits/src/ILimitConfigCodec.ts deleted file mode 100644 index 1d06bac66..000000000 --- a/packages/limits/src/ILimitConfigCodec.ts +++ /dev/null @@ -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; - defaultsHash?: string; -} - -export interface ILimitConfigCodec { - computeOverrides( - fullLimits: Partial>, - defaults: Record, - ): Partial>; - toWireFormat(config: LimitConfigSnapshot, options?: LimitConfigCodecOptions): LimitConfigWireFormat; - fromWireFormat(wireFormat: LimitConfigWireFormat, options?: LimitConfigCodecOptions): LimitConfigSnapshot; -} diff --git a/packages/limits/src/ILimitEvaluator.ts b/packages/limits/src/ILimitEvaluator.ts deleted file mode 100644 index 0073eea57..000000000 --- a/packages/limits/src/ILimitEvaluator.ts +++ /dev/null @@ -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; -} diff --git a/packages/limits/src/LimitConfigCodec.ts b/packages/limits/src/LimitConfigCodec.ts deleted file mode 100644 index b935657c0..000000000 --- a/packages/limits/src/LimitConfigCodec.ts +++ /dev/null @@ -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 = new Set([ - '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>, - defaults: Record, - ): Partial> { - const overrides: Partial> = {}; - 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>, - fullLimits: Partial>, - ): Partial> { - 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> = { - ...defaults, - ...rule.overrides, - }; - return { - id: rule.id, - filters: rule.filters, - limits, - }; - }); - return { - version: wireFormat.version, - traitDefinitions: wireFormat.traitDefinitions, - rules, - }; - } -} diff --git a/packages/limits/src/LimitDiffer.ts b/packages/limits/src/LimitDiffer.ts index 9bd2e5dee..f1f59c826 100644 --- a/packages/limits/src/LimitDiffer.ts +++ b/packages/limits/src/LimitDiffer.ts @@ -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 = new Set([ + '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>, defaults: Record, ): Partial> { - return defaultLimitConfigCodec.computeOverrides(fullLimits, defaults); + const overrides: Partial> = {}; + 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> = { + ...DEFAULT_FREE_LIMITS, + ...rule.overrides, + }; + return { + id: rule.id, + filters: rule.filters, + limits, + }; + }); + return { + version: wireFormat.version, + traitDefinitions: wireFormat.traitDefinitions, + rules, + }; } diff --git a/packages/limits/src/LimitEvaluator.ts b/packages/limits/src/LimitEvaluator.ts deleted file mode 100644 index a56b2dead..000000000 --- a/packages/limits/src/LimitEvaluator.ts +++ /dev/null @@ -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; - - 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]; - } -} diff --git a/packages/limits/src/LimitResolver.ts b/packages/limits/src/LimitResolver.ts index 6844ea9ed..e6240b753 100644 --- a/packages/limits/src/LimitResolver.ts +++ b/packages/limits/src/LimitResolver.ts @@ -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]; } diff --git a/packages/logger/vitest.config.ts b/packages/logger/vitest.config.ts index f94671ff6..99766af5f 100644 --- a/packages/logger/vitest.config.ts +++ b/packages/logger/vitest.config.ts @@ -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'; diff --git a/packages/schema/src/domains/experiment/ExperimentBucket.test.ts b/packages/schema/src/domains/experiment/ExperimentBucket.test.ts index 95e985f1b..5e189ba24 100644 --- a/packages/schema/src/domains/experiment/ExperimentBucket.test.ts +++ b/packages/schema/src/domains/experiment/ExperimentBucket.test.ts @@ -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 { - const ids: Array = []; - 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']); diff --git a/packages/schema/vitest.config.ts b/packages/schema/vitest.config.ts index f94671ff6..99766af5f 100644 --- a/packages/schema/vitest.config.ts +++ b/packages/schema/vitest.config.ts @@ -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'; diff --git a/packages/snowflake/src/SnowflakeUtils.ts b/packages/snowflake/src/SnowflakeUtils.ts index 25ed38f01..5cd653bff 100644 --- a/packages/snowflake/src/SnowflakeUtils.ts +++ b/packages/snowflake/src/SnowflakeUtils.ts @@ -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(); } diff --git a/packages/snowflake/vitest.config.ts b/packages/snowflake/vitest.config.ts index f94671ff6..99766af5f 100644 --- a/packages/snowflake/vitest.config.ts +++ b/packages/snowflake/vitest.config.ts @@ -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'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a48ccb587..e27b4a611 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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/api@1.9.1)(@types/node@26.6.1)(@vitest/coverage-v8@5.0.1)(happy-dom@20.14.5)(msw@2.15.0(@types/node@26.6.1)(typescript@7.0.2))(vite@8.3.0(@types/node@26.6.1)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.1)) - packages/hono_types: - devDependencies: - typescript: - specifier: catalog:ts7 - version: 7.0.2 - packages/i18n: dependencies: '@messageformat/core': diff --git a/tsconfigs/vitest.base.mts b/tsconfigs/vitest.base.mts new file mode 100644 index 000000000..257efd785 --- /dev/null +++ b/tsconfigs/vitest.base.mts @@ -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/'], + }, + }, +};