From 820377b82d049ab94e9deeecb3879402cbdaccf1 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Mon, 26 Jan 2026 15:30:20 +0000 Subject: [PATCH 1/5] fix(engine): key by the queue ID or the env/queue name instead of the run ID --- .../run-engine/src/engine/systems/runAttemptSystem.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal-packages/run-engine/src/engine/systems/runAttemptSystem.ts b/internal-packages/run-engine/src/engine/systems/runAttemptSystem.ts index fb8c833f160..11fa390673f 100644 --- a/internal-packages/run-engine/src/engine/systems/runAttemptSystem.ts +++ b/internal-packages/run-engine/src/engine/systems/runAttemptSystem.ts @@ -237,7 +237,6 @@ export class RunAttemptSystem { filePath: "unknown", }), this.#resolveTaskRunExecutionQueue({ - runId, lockedQueueId: run.lockedQueueId ?? undefined, queueName: run.queue, runtimeEnvironmentId: run.runtimeEnvironment.id, @@ -538,7 +537,6 @@ export class RunAttemptSystem { }), this.#resolveTaskRunExecutionTask(taskRun.lockedById), this.#resolveTaskRunExecutionQueue({ - runId, lockedQueueId: updatedRun.lockedQueueId ?? undefined, queueName: updatedRun.queue, runtimeEnvironmentId: updatedRun.runtimeEnvironment.id, @@ -1868,12 +1866,14 @@ export class RunAttemptSystem { } async #resolveTaskRunExecutionQueue(params: { - runId: string; lockedQueueId?: string; queueName: string; runtimeEnvironmentId: string; }): Promise { - const result = await this.cache.queues.swr(params.runId, async () => { + // Cache key should be based on queue identity, not run ID + // Using lockedQueueId if available, otherwise environment + queue name + const cacheKey = params.lockedQueueId ?? `${params.runtimeEnvironmentId}:${params.queueName}`; + const result = await this.cache.queues.swr(cacheKey, async () => { const queue = params.lockedQueueId ? await this.$.readOnlyPrisma.taskQueue.findFirst({ where: { From 96731ef1a15b554ce78c19854fdf7fec1bdc50cf Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Mon, 26 Jan 2026 16:14:17 +0000 Subject: [PATCH 2/5] implement lru cache to replace unkey memory store, much better performance and no elu blocking --- .../realtime/v1StreamsGlobal.server.ts | 4 +- .../worker/workerGroupTokenService.server.ts | 4 +- internal-packages/cache/package.json | 1 + internal-packages/cache/src/index.ts | 5 + .../cache/src/stores/lruMemory.ts | 167 +++++++++ .../run-engine/src/engine/billingCache.ts | 5 +- .../src/engine/systems/runAttemptSystem.ts | 5 +- .../run-queue/fairQueueSelectionStrategy.ts | 5 +- pnpm-lock.yaml | 330 ++++++++---------- 9 files changed, 332 insertions(+), 194 deletions(-) create mode 100644 internal-packages/cache/src/stores/lruMemory.ts diff --git a/apps/webapp/app/services/realtime/v1StreamsGlobal.server.ts b/apps/webapp/app/services/realtime/v1StreamsGlobal.server.ts index 6c008b107eb..7cc21101bf2 100644 --- a/apps/webapp/app/services/realtime/v1StreamsGlobal.server.ts +++ b/apps/webapp/app/services/realtime/v1StreamsGlobal.server.ts @@ -1,6 +1,6 @@ import { createCache, - createMemoryStore, + createLRUMemoryStore, DefaultStatefulContext, Namespace, RedisCacheStore, @@ -97,7 +97,7 @@ function initializeS2RealtimeStreamsCache() { useModernCacheKeyBuilder: true, }); - const memoryStore = createMemoryStore(5000, 0.001); + const memoryStore = createLRUMemoryStore(5000); return createCache({ accessToken: new Namespace(ctx, { diff --git a/apps/webapp/app/v3/services/worker/workerGroupTokenService.server.ts b/apps/webapp/app/v3/services/worker/workerGroupTokenService.server.ts index befe2a0a892..b29056c6244 100644 --- a/apps/webapp/app/v3/services/worker/workerGroupTokenService.server.ts +++ b/apps/webapp/app/v3/services/worker/workerGroupTokenService.server.ts @@ -1,4 +1,4 @@ -import { createCache, createMemoryStore, DefaultStatefulContext, Namespace } from "@internal/cache"; +import { createCache, createLRUMemoryStore, DefaultStatefulContext, Namespace } from "@internal/cache"; import { CheckpointInput, CompleteRunAttemptResult, @@ -39,7 +39,7 @@ function createAuthenticatedWorkerInstanceCache() { authenticatedWorkerInstance: new Namespace( new DefaultStatefulContext(), { - stores: [createMemoryStore(1000, 0.001)], + stores: [createLRUMemoryStore(1000)], fresh: 60_000 * 10, // 10 minutes stale: 60_000 * 11, // 11 minutes } diff --git a/internal-packages/cache/package.json b/internal-packages/cache/package.json index 267ff2d92d9..7f8acf735f9 100644 --- a/internal-packages/cache/package.json +++ b/internal-packages/cache/package.json @@ -10,6 +10,7 @@ "@trigger.dev/core": "workspace:*", "@unkey/cache": "^1.5.0", "@unkey/error": "^0.2.0", + "lru-cache": "^11.2.4", "superjson": "^2.2.1" }, "scripts": { diff --git a/internal-packages/cache/src/index.ts b/internal-packages/cache/src/index.ts index 479d2fce1ba..c0064a08f0e 100644 --- a/internal-packages/cache/src/index.ts +++ b/internal-packages/cache/src/index.ts @@ -8,3 +8,8 @@ export { export { type Result, Ok, Err } from "@unkey/error"; export { RedisCacheStore } from "./stores/redis.js"; export { createMemoryStore, type MemoryStore } from "./stores/memory.js"; +export { + LRUMemoryStore, + createLRUMemoryStore, + type LRUMemoryStoreConfig, +} from "./stores/lruMemory.js"; diff --git a/internal-packages/cache/src/stores/lruMemory.ts b/internal-packages/cache/src/stores/lruMemory.ts new file mode 100644 index 00000000000..63b1d5cd4b5 --- /dev/null +++ b/internal-packages/cache/src/stores/lruMemory.ts @@ -0,0 +1,167 @@ +import { LRUCache } from "lru-cache"; +import { CacheError } from "@unkey/cache"; +import type { Store, Entry } from "@unkey/cache/stores"; +import { Ok, Err, type Result } from "@unkey/error"; + +export type LRUMemoryStoreConfig = { + /** + * Maximum number of items to store in the cache. + * This is a hard limit - the cache will never exceed this size. + */ + max: number; + + /** + * Name for metrics/tracing. + * @default "lru-memory" + */ + name?: string; +}; + +/** + * A memory store implementation using lru-cache. + * + * This provides O(1) get/set/delete operations and automatic LRU eviction + * without blocking the event loop (unlike @unkey/cache's MemoryStore which + * uses O(n) synchronous iteration for eviction). + * + * TTL is checked lazily on get() - expired items are not proactively removed + * but will be evicted by LRU when the cache is full. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export class LRUMemoryStore + implements Store +{ + readonly name: string; + private readonly cache: LRUCache>; + + constructor(config: LRUMemoryStoreConfig) { + this.name = config.name ?? "lru-memory"; + this.cache = new LRUCache>({ + max: config.max, + // Don't use ttlAutopurge - it creates a setTimeout per item which + // doesn't scale well at high throughput (thousands of items/second). + // Instead, we check TTL lazily on get(). + ttlAutopurge: false, + // Allow returning stale values - the cache layer handles SWR semantics + allowStale: true, + // Use the staleUntil timestamp for TTL calculation + ttl: 1, // Placeholder, we set per-item TTL in set() + }); + } + + private buildCacheKey(namespace: TNamespace, key: string): string { + return `${namespace}::${key}`; + } + + async get( + namespace: TNamespace, + key: string + ): Promise | undefined, CacheError>> { + try { + const cacheKey = this.buildCacheKey(namespace, key); + const entry = this.cache.get(cacheKey); + + if (!entry) { + return Ok(undefined); + } + + // Check if entry is expired (past staleUntil) + // The cache layer will handle fresh vs stale semantics + if (entry.staleUntil <= Date.now()) { + // Remove expired entry + this.cache.delete(cacheKey); + return Ok(undefined); + } + + return Ok(entry); + } catch (err) { + return Err( + new CacheError({ + tier: this.name, + key, + message: err instanceof Error ? err.message : String(err), + }) + ); + } + } + + async set( + namespace: TNamespace, + key: string, + entry: Entry + ): Promise> { + try { + const cacheKey = this.buildCacheKey(namespace, key); + + // Calculate TTL from staleUntil timestamp + const ttl = Math.max(0, entry.staleUntil - Date.now()); + + this.cache.set(cacheKey, entry, { ttl }); + + return Ok(undefined as void); + } catch (err) { + return Err( + new CacheError({ + tier: this.name, + key, + message: err instanceof Error ? err.message : String(err), + }) + ); + } + } + + async remove( + namespace: TNamespace, + keys: string | string[] + ): Promise> { + try { + const keyArray = Array.isArray(keys) ? keys : [keys]; + + for (const key of keyArray) { + const cacheKey = this.buildCacheKey(namespace, key); + this.cache.delete(cacheKey); + } + + return Ok(undefined as void); + } catch (err) { + return Err( + new CacheError({ + tier: this.name, + key: Array.isArray(keys) ? keys.join(",") : keys, + message: err instanceof Error ? err.message : String(err), + }) + ); + } + } + + /** + * Returns the current number of items in the cache. + */ + get size(): number { + return this.cache.size; + } + + /** + * Clears all items from the cache. + */ + clear(): void { + this.cache.clear(); + } +} + +/** + * Creates an LRU memory store with the specified maximum size. + * + * This is a drop-in replacement for createMemoryStore() that uses lru-cache + * instead of @unkey/cache's MemoryStore, providing: + * - O(1) operations (vs O(n) eviction in MemoryStore) + * - No event loop blocking + * - Strict memory bounds (hard max vs soft cap) + * + * @param maxItems Maximum number of items to store + * @param name Optional name for metrics/tracing (default: "lru-memory") + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function createLRUMemoryStore(maxItems: number, name?: string): LRUMemoryStore { + return new LRUMemoryStore({ max: maxItems, name }); +} diff --git a/internal-packages/run-engine/src/engine/billingCache.ts b/internal-packages/run-engine/src/engine/billingCache.ts index 19c67d398e4..e4bef2f4386 100644 --- a/internal-packages/run-engine/src/engine/billingCache.ts +++ b/internal-packages/run-engine/src/engine/billingCache.ts @@ -1,14 +1,13 @@ import { createCache, + createLRUMemoryStore, DefaultStatefulContext, - MemoryStore, Namespace, Ok, RedisCacheStore, type UnkeyCache, type CacheError, type Result, - createMemoryStore, } from "@internal/cache"; import type { RedisOptions } from "@internal/redis"; import type { Logger } from "@trigger.dev/core/logger"; @@ -53,7 +52,7 @@ export class BillingCache { this.cache = createCache({ currentPlan: new Namespace(ctx, { - stores: [createMemoryStore(1000), redisCacheStore], + stores: [createLRUMemoryStore(1000), redisCacheStore], fresh: BILLING_FRESH_TTL, stale: BILLING_STALE_TTL, }), diff --git a/internal-packages/run-engine/src/engine/systems/runAttemptSystem.ts b/internal-packages/run-engine/src/engine/systems/runAttemptSystem.ts index 11fa390673f..a8fe3ccdc03 100644 --- a/internal-packages/run-engine/src/engine/systems/runAttemptSystem.ts +++ b/internal-packages/run-engine/src/engine/systems/runAttemptSystem.ts @@ -1,8 +1,7 @@ import { createCache, - createMemoryStore, + createLRUMemoryStore, DefaultStatefulContext, - MemoryStore, Namespace, RedisCacheStore, UnkeyCache, @@ -130,7 +129,7 @@ export class RunAttemptSystem { this.delayedRunSystem = options.delayedRunSystem; const ctx = new DefaultStatefulContext(); - const memory = createMemoryStore(5000, 0.001); + const memory = createLRUMemoryStore(5000); const redisCacheStore = new RedisCacheStore({ name: "run-attempt-system", connection: { diff --git a/internal-packages/run-engine/src/run-queue/fairQueueSelectionStrategy.ts b/internal-packages/run-engine/src/run-queue/fairQueueSelectionStrategy.ts index 46396fda41f..0e2205e413a 100644 --- a/internal-packages/run-engine/src/run-queue/fairQueueSelectionStrategy.ts +++ b/internal-packages/run-engine/src/run-queue/fairQueueSelectionStrategy.ts @@ -2,11 +2,10 @@ import { createRedisClient, Redis, type RedisOptions } from "@internal/redis"; import { startSpan, type Tracer } from "@internal/tracing"; import { createCache, + createLRUMemoryStore, DefaultStatefulContext, Namespace, type UnkeyCache, - MemoryStore, - createMemoryStore, } from "@internal/cache"; import { randomUUID } from "crypto"; import seedrandom from "seedrandom"; @@ -107,7 +106,7 @@ export class FairQueueSelectionStrategy implements RunQueueSelectionStrategy { constructor(private options: FairQueueSelectionStrategyOptions) { const ctx = new DefaultStatefulContext(); - const memory = createMemoryStore(1000); + const memory = createLRUMemoryStore(1000); this._cache = createCache({ concurrencyLimit: new Namespace(ctx, { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a835d8d2a0e..76f12a8774d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -432,31 +432,31 @@ importers: version: 3.7.1(react@18.2.0) '@remix-run/express': specifier: 2.1.0 - version: 2.1.0(express@4.20.0)(typescript@5.9.3) + version: 2.1.0(express@4.20.0)(typescript@5.5.4) '@remix-run/node': specifier: 2.1.0 - version: 2.1.0(typescript@5.9.3) + version: 2.1.0(typescript@5.5.4) '@remix-run/react': specifier: 2.1.0 - version: 2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3) + version: 2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.5.4) '@remix-run/router': specifier: ^1.15.3 version: 1.15.3 '@remix-run/serve': specifier: 2.1.0 - version: 2.1.0(typescript@5.9.3) + version: 2.1.0(typescript@5.5.4) '@remix-run/server-runtime': specifier: 2.1.0 - version: 2.1.0(typescript@5.9.3) + version: 2.1.0(typescript@5.5.4) '@remix-run/v1-meta': specifier: ^0.1.3 - version: 0.1.3(@remix-run/react@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))(@remix-run/server-runtime@2.1.0(typescript@5.9.3)) + version: 0.1.3(@remix-run/react@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.5.4))(@remix-run/server-runtime@2.1.0(typescript@5.5.4)) '@s2-dev/streamstore': specifier: ^0.17.2 - version: 0.17.3(typescript@5.9.3) + version: 0.17.3(typescript@5.5.4) '@sentry/remix': specifier: 9.46.0 - version: 9.46.0(patch_hash=146126b032581925294aaed63ab53ce3f5e0356a755f1763d7a9a76b9846943b)(@remix-run/node@2.1.0(typescript@5.9.3))(@remix-run/react@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))(@remix-run/server-runtime@2.1.0(typescript@5.9.3))(encoding@0.1.13)(react@18.2.0) + version: 9.46.0(patch_hash=146126b032581925294aaed63ab53ce3f5e0356a755f1763d7a9a76b9846943b)(@remix-run/node@2.1.0(typescript@5.5.4))(@remix-run/react@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.5.4))(@remix-run/server-runtime@2.1.0(typescript@5.5.4))(encoding@0.1.13)(react@18.2.0) '@slack/web-api': specifier: 7.9.1 version: 7.9.1 @@ -531,7 +531,7 @@ importers: version: 1.0.18 class-variance-authority: specifier: ^0.5.2 - version: 0.5.2(typescript@5.9.3) + version: 0.5.2(typescript@5.5.4) clsx: specifier: ^1.2.1 version: 1.2.1 @@ -582,7 +582,7 @@ importers: version: 10.12.11(react-dom@18.2.0(react@18.2.0))(react@18.2.0) graphile-worker: specifier: 0.16.6 - version: 0.16.6(patch_hash=798129c99ed02177430fc90a1fdef800ec94e5fd1d491b931297dc52f4c98ab1)(typescript@5.9.3) + version: 0.16.6(patch_hash=798129c99ed02177430fc90a1fdef800ec94e5fd1d491b931297dc52f4c98ab1)(typescript@5.5.4) humanize-duration: specifier: ^3.27.3 version: 3.27.3 @@ -711,22 +711,22 @@ importers: version: 2.0.1 remix-auth: specifier: ^3.6.0 - version: 3.6.0(@remix-run/react@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))(@remix-run/server-runtime@2.1.0(typescript@5.9.3)) + version: 3.6.0(@remix-run/react@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.5.4))(@remix-run/server-runtime@2.1.0(typescript@5.5.4)) remix-auth-email-link: specifier: 2.0.2 - version: 2.0.2(@remix-run/server-runtime@2.1.0(typescript@5.9.3))(remix-auth@3.6.0(@remix-run/react@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))(@remix-run/server-runtime@2.1.0(typescript@5.9.3))) + version: 2.0.2(@remix-run/server-runtime@2.1.0(typescript@5.5.4))(remix-auth@3.6.0(@remix-run/react@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.5.4))(@remix-run/server-runtime@2.1.0(typescript@5.5.4))) remix-auth-github: specifier: ^1.6.0 - version: 1.6.0(@remix-run/server-runtime@2.1.0(typescript@5.9.3))(remix-auth@3.6.0(@remix-run/react@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))(@remix-run/server-runtime@2.1.0(typescript@5.9.3))) + version: 1.6.0(@remix-run/server-runtime@2.1.0(typescript@5.5.4))(remix-auth@3.6.0(@remix-run/react@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.5.4))(@remix-run/server-runtime@2.1.0(typescript@5.5.4))) remix-auth-google: specifier: ^2.0.0 - version: 2.0.0(@remix-run/server-runtime@2.1.0(typescript@5.9.3))(remix-auth@3.6.0(@remix-run/react@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))(@remix-run/server-runtime@2.1.0(typescript@5.9.3))) + version: 2.0.0(@remix-run/server-runtime@2.1.0(typescript@5.5.4))(remix-auth@3.6.0(@remix-run/react@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.5.4))(@remix-run/server-runtime@2.1.0(typescript@5.5.4))) remix-typedjson: specifier: 0.3.1 - version: 0.3.1(@remix-run/react@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))(@remix-run/server-runtime@2.1.0(typescript@5.9.3))(react@18.2.0) + version: 0.3.1(@remix-run/react@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.5.4))(@remix-run/server-runtime@2.1.0(typescript@5.5.4))(react@18.2.0) remix-utils: specifier: ^7.7.0 - version: 7.7.0(@remix-run/node@2.1.0(typescript@5.9.3))(@remix-run/react@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))(@remix-run/router@1.15.3)(crypto-js@4.2.0)(intl-parse-accept-language@1.0.0)(react@18.2.0)(zod@3.25.76) + version: 7.7.0(@remix-run/node@2.1.0(typescript@5.5.4))(@remix-run/react@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.5.4))(@remix-run/router@1.15.3)(crypto-js@4.2.0)(intl-parse-accept-language@1.0.0)(react@18.2.0)(zod@3.25.76) seedrandom: specifier: ^3.0.5 version: 3.0.5 @@ -811,13 +811,13 @@ importers: version: link:../../internal-packages/testcontainers '@remix-run/dev': specifier: 2.1.0 - version: 2.1.0(@remix-run/serve@2.1.0(typescript@5.9.3))(@types/node@22.13.9)(bufferutil@4.0.9)(encoding@0.1.13)(lightningcss@1.29.2)(terser@5.44.1)(typescript@5.9.3) + version: 2.1.0(@remix-run/serve@2.1.0(typescript@5.5.4))(@types/node@22.13.9)(bufferutil@4.0.9)(encoding@0.1.13)(lightningcss@1.29.2)(terser@5.44.1)(typescript@5.5.4) '@remix-run/eslint-config': specifier: 2.1.0 - version: 2.1.0(eslint@8.31.0)(react@18.2.0)(typescript@5.9.3) + version: 2.1.0(eslint@8.31.0)(react@18.2.0)(typescript@5.5.4) '@remix-run/testing': specifier: ^2.1.0 - version: 2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3) + version: 2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.5.4) '@sentry/cli': specifier: 2.50.2 version: 2.50.2(encoding@0.1.13) @@ -916,10 +916,10 @@ importers: version: 8.5.4 '@typescript-eslint/eslint-plugin': specifier: ^5.59.6 - version: 5.59.6(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.9.3))(eslint@8.31.0)(typescript@5.9.3) + version: 5.59.6(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint@8.31.0)(typescript@5.5.4) '@typescript-eslint/parser': specifier: ^5.59.6 - version: 5.59.6(eslint@8.31.0)(typescript@5.9.3) + version: 5.59.6(eslint@8.31.0)(typescript@5.5.4) autoevals: specifier: ^0.0.130 version: 0.0.130(encoding@0.1.13)(ws@8.12.0(bufferutil@4.0.9)) @@ -946,7 +946,7 @@ importers: version: 8.6.0(eslint@8.31.0) eslint-plugin-import: specifier: ^2.29.1 - version: 2.29.1(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.9.3))(eslint-import-resolver-typescript@3.5.5)(eslint@8.31.0) + version: 2.29.1(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.5.5)(eslint@8.31.0) eslint-plugin-react-hooks: specifier: ^4.6.2 version: 4.6.2(eslint@8.31.0) @@ -964,7 +964,7 @@ importers: version: 16.0.1(postcss@8.5.6) postcss-loader: specifier: ^8.1.1 - version: 8.1.1(postcss@8.5.6)(typescript@5.9.3)(webpack@5.102.1(@swc/core@1.3.26)(esbuild@0.15.18)) + version: 8.1.1(postcss@8.5.6)(typescript@5.5.4)(webpack@5.102.1(@swc/core@1.3.26)(esbuild@0.15.18)) prettier: specifier: ^2.8.8 version: 2.8.8 @@ -997,7 +997,7 @@ importers: version: 4.20.6 vite-tsconfig-paths: specifier: ^4.0.5 - version: 4.0.5(typescript@5.9.3) + version: 4.0.5(typescript@5.5.4) docs: {} @@ -1015,6 +1015,9 @@ importers: '@unkey/error': specifier: ^0.2.0 version: 0.2.0 + lru-cache: + specifier: ^11.2.4 + version: 11.2.4 superjson: specifier: ^2.2.1 version: 2.2.1 @@ -1686,7 +1689,7 @@ importers: version: 1.36.0 '@s2-dev/streamstore': specifier: 0.17.3 - version: 0.17.3(typescript@5.9.3) + version: 0.17.3(typescript@5.5.4) dequal: specifier: ^2.0.3 version: 2.0.3 @@ -1774,7 +1777,7 @@ importers: version: 3.0.2 ts-essentials: specifier: 10.0.1 - version: 10.0.1(typescript@5.9.3) + version: 10.0.1(typescript@5.5.4) tshy: specifier: ^3.0.2 version: 3.0.2 @@ -5069,6 +5072,7 @@ packages: '@fal-ai/serverless-client@0.15.0': resolution: {integrity: sha512-4Vuocu0342OijAN6xO/lwohDV7h90LbkTnOAEwH+pYvMFVC6RYmHS4GILc/wnOWBTw+iFlZFEKlljEVolkjVfg==} engines: {node: '>=18.0.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. '@fast-csv/parse@5.0.2': resolution: {integrity: sha512-gMu1Btmm99TP+wc0tZnlH30E/F1Gw1Tah3oMDBHNPe9W8S68ixVHjt89Wg5lh7d9RuQMtwN+sGl5kxR891+fzw==} @@ -10944,6 +10948,7 @@ packages: '@vercel/postgres@0.10.0': resolution: {integrity: sha512-fSD23DxGND40IzSkXjcFcxr53t3Tiym59Is0jSYIFpG4/0f0KO9SGtcp1sXiebvPaGe7N/tU05cH4yt2S6/IPg==} engines: {node: '>=18.14'} + deprecated: '@vercel/postgres is deprecated. You can either choose an alternate storage solution from the Vercel Marketplace if you want to set up a new database. Or you can follow this guide to migrate your existing Vercel Postgres db: https://neon.com/docs/guides/vercel-postgres-transition-guide' '@vitest/coverage-v8@3.1.4': resolution: {integrity: sha512-G4p6OtioySL+hPV7Y6JHlhpsODbJzt1ndwHAFkyk6vVjpK03PFsKnauZIzcd0PrK4zAbc5lc+jeZ+eNGiMA+iw==} @@ -15209,6 +15214,10 @@ packages: resolution: {integrity: sha512-Qv32eSV1RSCfhY3fpPE2GNZ8jgM9X7rdAfemLWqTUxwiyIC4jJ6Sy0fZ8H+oLWevO6i4/bizg7c8d8i6bxrzbA==} engines: {node: 20 || >=22} + lru-cache@11.2.4: + resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==} + engines: {node: 20 || >=22} + lru-cache@4.1.5: resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==} @@ -15923,6 +15932,7 @@ packages: next@14.1.0: resolution: {integrity: sha512-wlzrsbfeSU48YQBjZhDzOwhWhGsy+uQycR8bHAOt1LY1bn3zZEcDyHQOEoN3aWzQ8LHCAJ1nqrWCc9XF2+O45Q==} engines: {node: '>=18.17.0'} + deprecated: This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/security-update-2025-12-11 for more details. hasBin: true peerDependencies: '@opentelemetry/api': ^1.1.0 @@ -15938,6 +15948,7 @@ packages: next@14.2.21: resolution: {integrity: sha512-rZmLwucLHr3/zfDMYbJXbw0ZeoBpirxkXuvsJbk7UPorvPYZhP7vq7aHbKnU7dQNCYIimRrbB2pp3xmf+wsYUg==} engines: {node: '>=18.17.0'} + deprecated: This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/security-update-2025-12-11 for more details. hasBin: true peerDependencies: '@opentelemetry/api': ^1.1.0 @@ -15978,6 +15989,7 @@ packages: next@15.4.8: resolution: {integrity: sha512-jwOXTz/bo0Pvlf20FSb6VXVeWRssA2vbvq9SdrOPEg9x8E1B27C2rQtvriAn600o9hH61kjrVRexEffv3JybuA==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} + deprecated: This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/security-update-2025-12-11 for more details. hasBin: true peerDependencies: '@opentelemetry/api': ^1.1.0 @@ -18642,14 +18654,17 @@ packages: tar@6.1.13: resolution: {integrity: sha512-jdIBIN6LTIe2jqzay/2vtYLlBHa3JF42ot3h1dW8Q0PaAG4v8rm0cvpVePtau5C6OKXGGcgO9q2AMNSWxiLqKw==} engines: {node: '>=10'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me tar@6.2.1: resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} engines: {node: '>=10'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me tar@7.4.3: resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} engines: {node: '>=18'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me tdigest@0.1.2: resolution: {integrity: sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==} @@ -19125,11 +19140,6 @@ packages: engines: {node: '>=14.17'} hasBin: true - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} - engines: {node: '>=14.17'} - hasBin: true - ufo@1.5.4: resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} @@ -28503,7 +28513,7 @@ snapshots: transitivePeerDependencies: - encoding - '@remix-run/dev@2.1.0(@remix-run/serve@2.1.0(typescript@5.9.3))(@types/node@22.13.9)(bufferutil@4.0.9)(encoding@0.1.13)(lightningcss@1.29.2)(terser@5.44.1)(typescript@5.9.3)': + '@remix-run/dev@2.1.0(@remix-run/serve@2.1.0(typescript@5.5.4))(@types/node@22.13.9)(bufferutil@4.0.9)(encoding@0.1.13)(lightningcss@1.29.2)(terser@5.44.1)(typescript@5.5.4)': dependencies: '@babel/core': 7.22.17 '@babel/generator': 7.24.7 @@ -28514,7 +28524,7 @@ snapshots: '@babel/traverse': 7.24.7 '@mdx-js/mdx': 2.3.0 '@npmcli/package-json': 4.0.1 - '@remix-run/server-runtime': 2.1.0(typescript@5.9.3) + '@remix-run/server-runtime': 2.1.0(typescript@5.5.4) '@types/mdx': 2.0.5 '@vanilla-extract/integration': 6.2.1(@types/node@22.13.9)(lightningcss@1.29.2)(terser@5.44.1) arg: 5.0.2 @@ -28554,8 +28564,8 @@ snapshots: tsconfig-paths: 4.2.0 ws: 7.5.9(bufferutil@4.0.9) optionalDependencies: - '@remix-run/serve': 2.1.0(typescript@5.9.3) - typescript: 5.9.3 + '@remix-run/serve': 2.1.0(typescript@5.5.4) + typescript: 5.5.4 transitivePeerDependencies: - '@types/node' - bluebird @@ -28571,43 +28581,43 @@ snapshots: - ts-node - utf-8-validate - '@remix-run/eslint-config@2.1.0(eslint@8.31.0)(react@18.2.0)(typescript@5.9.3)': + '@remix-run/eslint-config@2.1.0(eslint@8.31.0)(react@18.2.0)(typescript@5.5.4)': dependencies: '@babel/core': 7.22.17 '@babel/eslint-parser': 7.21.8(@babel/core@7.22.17)(eslint@8.31.0) '@babel/preset-react': 7.18.6(@babel/core@7.22.17) '@rushstack/eslint-patch': 1.2.0 - '@typescript-eslint/eslint-plugin': 5.59.6(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.9.3))(eslint@8.31.0)(typescript@5.9.3) - '@typescript-eslint/parser': 5.59.6(eslint@8.31.0)(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 5.59.6(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint@8.31.0)(typescript@5.5.4) + '@typescript-eslint/parser': 5.59.6(eslint@8.31.0)(typescript@5.5.4) eslint: 8.31.0 eslint-import-resolver-node: 0.3.7 - eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.9.3))(eslint-import-resolver-node@0.3.7)(eslint-plugin-import@2.29.1)(eslint@8.31.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.9.3))(eslint-import-resolver-typescript@3.5.5)(eslint@8.31.0) - eslint-plugin-jest: 26.9.0(@typescript-eslint/eslint-plugin@5.59.6(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.9.3))(eslint@8.31.0)(typescript@5.9.3))(eslint@8.31.0)(typescript@5.9.3) + eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.7)(eslint-plugin-import@2.29.1)(eslint@8.31.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.5.5)(eslint@8.31.0) + eslint-plugin-jest: 26.9.0(@typescript-eslint/eslint-plugin@5.59.6(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint@8.31.0)(typescript@5.5.4))(eslint@8.31.0)(typescript@5.5.4) eslint-plugin-jest-dom: 4.0.3(eslint@8.31.0) eslint-plugin-jsx-a11y: 6.7.1(eslint@8.31.0) eslint-plugin-node: 11.1.0(eslint@8.31.0) eslint-plugin-react: 7.32.2(eslint@8.31.0) eslint-plugin-react-hooks: 4.6.2(eslint@8.31.0) - eslint-plugin-testing-library: 5.11.0(eslint@8.31.0)(typescript@5.9.3) + eslint-plugin-testing-library: 5.11.0(eslint@8.31.0)(typescript@5.5.4) react: 18.2.0 optionalDependencies: - typescript: 5.9.3 + typescript: 5.5.4 transitivePeerDependencies: - eslint-import-resolver-webpack - jest - supports-color - '@remix-run/express@2.1.0(express@4.20.0)(typescript@5.9.3)': + '@remix-run/express@2.1.0(express@4.20.0)(typescript@5.5.4)': dependencies: - '@remix-run/node': 2.1.0(typescript@5.9.3) + '@remix-run/node': 2.1.0(typescript@5.5.4) express: 4.20.0 optionalDependencies: - typescript: 5.9.3 + typescript: 5.5.4 - '@remix-run/node@2.1.0(typescript@5.9.3)': + '@remix-run/node@2.1.0(typescript@5.5.4)': dependencies: - '@remix-run/server-runtime': 2.1.0(typescript@5.9.3) + '@remix-run/server-runtime': 2.1.0(typescript@5.5.4) '@remix-run/web-fetch': 4.4.1 '@remix-run/web-file': 3.1.0 '@remix-run/web-stream': 1.1.0 @@ -28616,26 +28626,26 @@ snapshots: source-map-support: 0.5.21 stream-slice: 0.1.2 optionalDependencies: - typescript: 5.9.3 + typescript: 5.5.4 - '@remix-run/react@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)': + '@remix-run/react@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.5.4)': dependencies: '@remix-run/router': 1.10.0 - '@remix-run/server-runtime': 2.1.0(typescript@5.9.3) + '@remix-run/server-runtime': 2.1.0(typescript@5.5.4) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) react-router-dom: 6.17.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) optionalDependencies: - typescript: 5.9.3 + typescript: 5.5.4 '@remix-run/router@1.10.0': {} '@remix-run/router@1.15.3': {} - '@remix-run/serve@2.1.0(typescript@5.9.3)': + '@remix-run/serve@2.1.0(typescript@5.5.4)': dependencies: - '@remix-run/express': 2.1.0(express@4.20.0)(typescript@5.9.3) - '@remix-run/node': 2.1.0(typescript@5.9.3) + '@remix-run/express': 2.1.0(express@4.20.0)(typescript@5.5.4) + '@remix-run/node': 2.1.0(typescript@5.5.4) chokidar: 3.6.0 compression: 1.7.4 express: 4.20.0 @@ -28646,7 +28656,7 @@ snapshots: - supports-color - typescript - '@remix-run/server-runtime@2.1.0(typescript@5.9.3)': + '@remix-run/server-runtime@2.1.0(typescript@5.5.4)': dependencies: '@remix-run/router': 1.10.0 '@types/cookie': 0.4.1 @@ -28655,24 +28665,24 @@ snapshots: set-cookie-parser: 2.6.0 source-map: 0.7.4 optionalDependencies: - typescript: 5.9.3 + typescript: 5.5.4 - '@remix-run/testing@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)': + '@remix-run/testing@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.5.4)': dependencies: - '@remix-run/node': 2.1.0(typescript@5.9.3) - '@remix-run/react': 2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3) + '@remix-run/node': 2.1.0(typescript@5.5.4) + '@remix-run/react': 2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.5.4) '@remix-run/router': 1.10.0 react: 18.2.0 react-router-dom: 6.17.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) optionalDependencies: - typescript: 5.9.3 + typescript: 5.5.4 transitivePeerDependencies: - react-dom - '@remix-run/v1-meta@0.1.3(@remix-run/react@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))(@remix-run/server-runtime@2.1.0(typescript@5.9.3))': + '@remix-run/v1-meta@0.1.3(@remix-run/react@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.5.4))(@remix-run/server-runtime@2.1.0(typescript@5.5.4))': dependencies: - '@remix-run/react': 2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3) - '@remix-run/server-runtime': 2.1.0(typescript@5.9.3) + '@remix-run/react': 2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.5.4) + '@remix-run/server-runtime': 2.1.0(typescript@5.5.4) '@remix-run/web-blob@3.1.0': dependencies: @@ -28767,10 +28777,10 @@ snapshots: '@rushstack/eslint-patch@1.2.0': {} - '@s2-dev/streamstore@0.17.3(typescript@5.9.3)': + '@s2-dev/streamstore@0.17.3(typescript@5.5.4)': dependencies: '@protobuf-ts/runtime': 2.11.1 - typescript: 5.9.3 + typescript: 5.5.4 '@s2-dev/streamstore@0.17.6': dependencies: @@ -28924,15 +28934,15 @@ snapshots: hoist-non-react-statics: 3.3.2 react: 18.2.0 - '@sentry/remix@9.46.0(patch_hash=146126b032581925294aaed63ab53ce3f5e0356a755f1763d7a9a76b9846943b)(@remix-run/node@2.1.0(typescript@5.9.3))(@remix-run/react@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))(@remix-run/server-runtime@2.1.0(typescript@5.9.3))(encoding@0.1.13)(react@18.2.0)': + '@sentry/remix@9.46.0(patch_hash=146126b032581925294aaed63ab53ce3f5e0356a755f1763d7a9a76b9846943b)(@remix-run/node@2.1.0(typescript@5.5.4))(@remix-run/react@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.5.4))(@remix-run/server-runtime@2.1.0(typescript@5.5.4))(encoding@0.1.13)(react@18.2.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.36.0 - '@remix-run/node': 2.1.0(typescript@5.9.3) - '@remix-run/react': 2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3) + '@remix-run/node': 2.1.0(typescript@5.5.4) + '@remix-run/react': 2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.5.4) '@remix-run/router': 1.15.3 - '@remix-run/server-runtime': 2.1.0(typescript@5.9.3) + '@remix-run/server-runtime': 2.1.0(typescript@5.5.4) '@sentry/cli': 2.50.2(encoding@0.1.13) '@sentry/core': 9.46.0 '@sentry/node': 9.46.0 @@ -30833,34 +30843,34 @@ snapshots: '@types/node': 20.14.14 optional: true - '@typescript-eslint/eslint-plugin@5.59.6(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.9.3))(eslint@8.31.0)(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@5.59.6(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint@8.31.0)(typescript@5.5.4)': dependencies: '@eslint-community/regexpp': 4.5.1 - '@typescript-eslint/parser': 5.59.6(eslint@8.31.0)(typescript@5.9.3) + '@typescript-eslint/parser': 5.59.6(eslint@8.31.0)(typescript@5.5.4) '@typescript-eslint/scope-manager': 5.59.6 - '@typescript-eslint/type-utils': 5.59.6(eslint@8.31.0)(typescript@5.9.3) - '@typescript-eslint/utils': 5.59.6(eslint@8.31.0)(typescript@5.9.3) + '@typescript-eslint/type-utils': 5.59.6(eslint@8.31.0)(typescript@5.5.4) + '@typescript-eslint/utils': 5.59.6(eslint@8.31.0)(typescript@5.5.4) debug: 4.3.4 eslint: 8.31.0 grapheme-splitter: 1.0.4 ignore: 5.2.4 natural-compare-lite: 1.4.0 semver: 7.6.3 - tsutils: 3.21.0(typescript@5.9.3) + tsutils: 3.21.0(typescript@5.5.4) optionalDependencies: - typescript: 5.9.3 + typescript: 5.5.4 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.9.3)': + '@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4)': dependencies: '@typescript-eslint/scope-manager': 5.59.6 '@typescript-eslint/types': 5.59.6 - '@typescript-eslint/typescript-estree': 5.59.6(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 5.59.6(typescript@5.5.4) debug: 4.4.0 eslint: 8.31.0 optionalDependencies: - typescript: 5.9.3 + typescript: 5.5.4 transitivePeerDependencies: - supports-color @@ -30869,21 +30879,21 @@ snapshots: '@typescript-eslint/types': 5.59.6 '@typescript-eslint/visitor-keys': 5.59.6 - '@typescript-eslint/type-utils@5.59.6(eslint@8.31.0)(typescript@5.9.3)': + '@typescript-eslint/type-utils@5.59.6(eslint@8.31.0)(typescript@5.5.4)': dependencies: - '@typescript-eslint/typescript-estree': 5.59.6(typescript@5.9.3) - '@typescript-eslint/utils': 5.59.6(eslint@8.31.0)(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 5.59.6(typescript@5.5.4) + '@typescript-eslint/utils': 5.59.6(eslint@8.31.0)(typescript@5.5.4) debug: 4.4.1(supports-color@10.0.0) eslint: 8.31.0 - tsutils: 3.21.0(typescript@5.9.3) + tsutils: 3.21.0(typescript@5.5.4) optionalDependencies: - typescript: 5.9.3 + typescript: 5.5.4 transitivePeerDependencies: - supports-color '@typescript-eslint/types@5.59.6': {} - '@typescript-eslint/typescript-estree@5.59.6(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@5.59.6(typescript@5.5.4)': dependencies: '@typescript-eslint/types': 5.59.6 '@typescript-eslint/visitor-keys': 5.59.6 @@ -30891,20 +30901,20 @@ snapshots: globby: 11.1.0 is-glob: 4.0.3 semver: 7.7.3 - tsutils: 3.21.0(typescript@5.9.3) + tsutils: 3.21.0(typescript@5.5.4) optionalDependencies: - typescript: 5.9.3 + typescript: 5.5.4 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@5.59.6(eslint@8.31.0)(typescript@5.9.3)': + '@typescript-eslint/utils@5.59.6(eslint@8.31.0)(typescript@5.5.4)': dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@8.31.0) '@types/json-schema': 7.0.13 '@types/semver': 7.5.1 '@typescript-eslint/scope-manager': 5.59.6 '@typescript-eslint/types': 5.59.6 - '@typescript-eslint/typescript-estree': 5.59.6(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 5.59.6(typescript@5.5.4) eslint: 8.31.0 eslint-scope: 5.1.1 semver: 7.7.3 @@ -32222,9 +32232,9 @@ snapshots: cjs-module-lexer@1.2.3: {} - class-variance-authority@0.5.2(typescript@5.9.3): + class-variance-authority@0.5.2(typescript@5.5.4): optionalDependencies: - typescript: 5.9.3 + typescript: 5.5.4 class-variance-authority@0.7.0: dependencies: @@ -32485,23 +32495,14 @@ snapshots: optionalDependencies: typescript: 5.5.4 - cosmiconfig@8.3.6(typescript@5.9.3): - dependencies: - import-fresh: 3.3.0 - js-yaml: 4.1.1 - parse-json: 5.2.0 - path-type: 4.0.0 - optionalDependencies: - typescript: 5.9.3 - - cosmiconfig@9.0.0(typescript@5.9.3): + cosmiconfig@9.0.0(typescript@5.5.4): dependencies: env-paths: 2.2.1 import-fresh: 3.3.0 js-yaml: 4.1.1 parse-json: 5.2.0 optionalDependencies: - typescript: 5.9.3 + typescript: 5.5.4 cp-file@10.0.0: dependencies: @@ -33700,13 +33701,13 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.5.5(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.9.3))(eslint-import-resolver-node@0.3.7)(eslint-plugin-import@2.29.1)(eslint@8.31.0): + eslint-import-resolver-typescript@3.5.5(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.7)(eslint-plugin-import@2.29.1)(eslint@8.31.0): dependencies: debug: 4.4.1(supports-color@10.0.0) enhanced-resolve: 5.15.0 eslint: 8.31.0 - eslint-module-utils: 2.7.4(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.9.3))(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5)(eslint@8.31.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.9.3))(eslint-import-resolver-typescript@3.5.5)(eslint@8.31.0) + eslint-module-utils: 2.7.4(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5)(eslint@8.31.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.5.5)(eslint@8.31.0) get-tsconfig: 4.7.2 globby: 13.2.2 is-core-module: 2.14.0 @@ -33718,25 +33719,25 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.7.4(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.9.3))(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5)(eslint@8.31.0): + eslint-module-utils@2.7.4(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5)(eslint@8.31.0): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 5.59.6(eslint@8.31.0)(typescript@5.9.3) + '@typescript-eslint/parser': 5.59.6(eslint@8.31.0)(typescript@5.5.4) eslint: 8.31.0 eslint-import-resolver-node: 0.3.7 - eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.9.3))(eslint-import-resolver-node@0.3.7)(eslint-plugin-import@2.29.1)(eslint@8.31.0) + eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.7)(eslint-plugin-import@2.29.1)(eslint@8.31.0) transitivePeerDependencies: - supports-color - eslint-module-utils@2.8.1(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.5.5)(eslint@8.31.0): + eslint-module-utils@2.8.1(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.5.5)(eslint@8.31.0): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 5.59.6(eslint@8.31.0)(typescript@5.9.3) + '@typescript-eslint/parser': 5.59.6(eslint@8.31.0)(typescript@5.5.4) eslint: 8.31.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.9.3))(eslint-import-resolver-node@0.3.7)(eslint-plugin-import@2.29.1)(eslint@8.31.0) + eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.7)(eslint-plugin-import@2.29.1)(eslint@8.31.0) transitivePeerDependencies: - supports-color @@ -33746,7 +33747,7 @@ snapshots: eslint-utils: 2.1.0 regexpp: 3.2.0 - eslint-plugin-import@2.29.1(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.9.3))(eslint-import-resolver-typescript@3.5.5)(eslint@8.31.0): + eslint-plugin-import@2.29.1(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.5.5)(eslint@8.31.0): dependencies: array-includes: 3.1.8 array.prototype.findlastindex: 1.2.5 @@ -33756,7 +33757,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.31.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.5.5)(eslint@8.31.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.5.5)(eslint@8.31.0) hasown: 2.0.2 is-core-module: 2.14.0 is-glob: 4.0.3 @@ -33767,7 +33768,7 @@ snapshots: semver: 6.3.1 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 5.59.6(eslint@8.31.0)(typescript@5.9.3) + '@typescript-eslint/parser': 5.59.6(eslint@8.31.0)(typescript@5.5.4) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -33780,12 +33781,12 @@ snapshots: eslint: 8.31.0 requireindex: 1.2.0 - eslint-plugin-jest@26.9.0(@typescript-eslint/eslint-plugin@5.59.6(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.9.3))(eslint@8.31.0)(typescript@5.9.3))(eslint@8.31.0)(typescript@5.9.3): + eslint-plugin-jest@26.9.0(@typescript-eslint/eslint-plugin@5.59.6(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint@8.31.0)(typescript@5.5.4))(eslint@8.31.0)(typescript@5.5.4): dependencies: - '@typescript-eslint/utils': 5.59.6(eslint@8.31.0)(typescript@5.9.3) + '@typescript-eslint/utils': 5.59.6(eslint@8.31.0)(typescript@5.5.4) eslint: 8.31.0 optionalDependencies: - '@typescript-eslint/eslint-plugin': 5.59.6(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.9.3))(eslint@8.31.0)(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 5.59.6(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint@8.31.0)(typescript@5.5.4) transitivePeerDependencies: - supports-color - typescript @@ -33843,9 +33844,9 @@ snapshots: semver: 6.3.1 string.prototype.matchall: 4.0.8 - eslint-plugin-testing-library@5.11.0(eslint@8.31.0)(typescript@5.9.3): + eslint-plugin-testing-library@5.11.0(eslint@8.31.0)(typescript@5.5.4): dependencies: - '@typescript-eslint/utils': 5.59.6(eslint@8.31.0)(typescript@5.9.3) + '@typescript-eslint/utils': 5.59.6(eslint@8.31.0)(typescript@5.5.4) eslint: 8.31.0 transitivePeerDependencies: - supports-color @@ -34781,22 +34782,6 @@ snapshots: - supports-color - typescript - graphile-worker@0.16.6(patch_hash=798129c99ed02177430fc90a1fdef800ec94e5fd1d491b931297dc52f4c98ab1)(typescript@5.9.3): - dependencies: - '@graphile/logger': 0.2.0 - '@types/debug': 4.1.12 - '@types/pg': 8.11.6 - cosmiconfig: 8.3.6(typescript@5.9.3) - graphile-config: 0.0.1-beta.8 - json5: 2.2.3 - pg: 8.11.5 - tslib: 2.6.2 - yargs: 17.7.2 - transitivePeerDependencies: - - pg-native - - supports-color - - typescript - graphql@16.6.0: {} gunzip-maybe@1.4.2: @@ -35904,6 +35889,8 @@ snapshots: lru-cache@11.0.0: {} + lru-cache@11.2.4: {} + lru-cache@4.1.5: dependencies: pseudomap: 1.0.2 @@ -37997,9 +37984,9 @@ snapshots: tsx: 4.17.0 yaml: 2.7.1 - postcss-loader@8.1.1(postcss@8.5.6)(typescript@5.9.3)(webpack@5.102.1(@swc/core@1.3.26)(esbuild@0.15.18)): + postcss-loader@8.1.1(postcss@8.5.6)(typescript@5.5.4)(webpack@5.102.1(@swc/core@1.3.26)(esbuild@0.15.18)): dependencies: - cosmiconfig: 9.0.0(typescript@5.9.3) + cosmiconfig: 9.0.0(typescript@5.5.4) jiti: 1.21.0 postcss: 8.5.6 semver: 7.6.3 @@ -39176,54 +39163,54 @@ snapshots: mdast-util-to-markdown: 2.1.2 unified: 11.0.5 - remix-auth-email-link@2.0.2(@remix-run/server-runtime@2.1.0(typescript@5.9.3))(remix-auth@3.6.0(@remix-run/react@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))(@remix-run/server-runtime@2.1.0(typescript@5.9.3))): + remix-auth-email-link@2.0.2(@remix-run/server-runtime@2.1.0(typescript@5.5.4))(remix-auth@3.6.0(@remix-run/react@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.5.4))(@remix-run/server-runtime@2.1.0(typescript@5.5.4))): dependencies: - '@remix-run/server-runtime': 2.1.0(typescript@5.9.3) + '@remix-run/server-runtime': 2.1.0(typescript@5.5.4) crypto-js: 4.1.1 - remix-auth: 3.6.0(@remix-run/react@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))(@remix-run/server-runtime@2.1.0(typescript@5.9.3)) + remix-auth: 3.6.0(@remix-run/react@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.5.4))(@remix-run/server-runtime@2.1.0(typescript@5.5.4)) - remix-auth-github@1.6.0(@remix-run/server-runtime@2.1.0(typescript@5.9.3))(remix-auth@3.6.0(@remix-run/react@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))(@remix-run/server-runtime@2.1.0(typescript@5.9.3))): + remix-auth-github@1.6.0(@remix-run/server-runtime@2.1.0(typescript@5.5.4))(remix-auth@3.6.0(@remix-run/react@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.5.4))(@remix-run/server-runtime@2.1.0(typescript@5.5.4))): dependencies: - '@remix-run/server-runtime': 2.1.0(typescript@5.9.3) - remix-auth: 3.6.0(@remix-run/react@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))(@remix-run/server-runtime@2.1.0(typescript@5.9.3)) - remix-auth-oauth2: 1.11.0(@remix-run/server-runtime@2.1.0(typescript@5.9.3))(remix-auth@3.6.0(@remix-run/react@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))(@remix-run/server-runtime@2.1.0(typescript@5.9.3))) + '@remix-run/server-runtime': 2.1.0(typescript@5.5.4) + remix-auth: 3.6.0(@remix-run/react@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.5.4))(@remix-run/server-runtime@2.1.0(typescript@5.5.4)) + remix-auth-oauth2: 1.11.0(@remix-run/server-runtime@2.1.0(typescript@5.5.4))(remix-auth@3.6.0(@remix-run/react@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.5.4))(@remix-run/server-runtime@2.1.0(typescript@5.5.4))) transitivePeerDependencies: - supports-color - remix-auth-google@2.0.0(@remix-run/server-runtime@2.1.0(typescript@5.9.3))(remix-auth@3.6.0(@remix-run/react@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))(@remix-run/server-runtime@2.1.0(typescript@5.9.3))): + remix-auth-google@2.0.0(@remix-run/server-runtime@2.1.0(typescript@5.5.4))(remix-auth@3.6.0(@remix-run/react@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.5.4))(@remix-run/server-runtime@2.1.0(typescript@5.5.4))): dependencies: - '@remix-run/server-runtime': 2.1.0(typescript@5.9.3) - remix-auth: 3.6.0(@remix-run/react@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))(@remix-run/server-runtime@2.1.0(typescript@5.9.3)) - remix-auth-oauth2: 1.11.0(@remix-run/server-runtime@2.1.0(typescript@5.9.3))(remix-auth@3.6.0(@remix-run/react@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))(@remix-run/server-runtime@2.1.0(typescript@5.9.3))) + '@remix-run/server-runtime': 2.1.0(typescript@5.5.4) + remix-auth: 3.6.0(@remix-run/react@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.5.4))(@remix-run/server-runtime@2.1.0(typescript@5.5.4)) + remix-auth-oauth2: 1.11.0(@remix-run/server-runtime@2.1.0(typescript@5.5.4))(remix-auth@3.6.0(@remix-run/react@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.5.4))(@remix-run/server-runtime@2.1.0(typescript@5.5.4))) transitivePeerDependencies: - supports-color - remix-auth-oauth2@1.11.0(@remix-run/server-runtime@2.1.0(typescript@5.9.3))(remix-auth@3.6.0(@remix-run/react@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))(@remix-run/server-runtime@2.1.0(typescript@5.9.3))): + remix-auth-oauth2@1.11.0(@remix-run/server-runtime@2.1.0(typescript@5.5.4))(remix-auth@3.6.0(@remix-run/react@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.5.4))(@remix-run/server-runtime@2.1.0(typescript@5.5.4))): dependencies: - '@remix-run/server-runtime': 2.1.0(typescript@5.9.3) + '@remix-run/server-runtime': 2.1.0(typescript@5.5.4) debug: 4.4.1(supports-color@10.0.0) - remix-auth: 3.6.0(@remix-run/react@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))(@remix-run/server-runtime@2.1.0(typescript@5.9.3)) + remix-auth: 3.6.0(@remix-run/react@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.5.4))(@remix-run/server-runtime@2.1.0(typescript@5.5.4)) transitivePeerDependencies: - supports-color - remix-auth@3.6.0(@remix-run/react@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))(@remix-run/server-runtime@2.1.0(typescript@5.9.3)): + remix-auth@3.6.0(@remix-run/react@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.5.4))(@remix-run/server-runtime@2.1.0(typescript@5.5.4)): dependencies: - '@remix-run/react': 2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3) - '@remix-run/server-runtime': 2.1.0(typescript@5.9.3) + '@remix-run/react': 2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.5.4) + '@remix-run/server-runtime': 2.1.0(typescript@5.5.4) uuid: 8.3.2 - remix-typedjson@0.3.1(@remix-run/react@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))(@remix-run/server-runtime@2.1.0(typescript@5.9.3))(react@18.2.0): + remix-typedjson@0.3.1(@remix-run/react@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.5.4))(@remix-run/server-runtime@2.1.0(typescript@5.5.4))(react@18.2.0): dependencies: - '@remix-run/react': 2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3) - '@remix-run/server-runtime': 2.1.0(typescript@5.9.3) + '@remix-run/react': 2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.5.4) + '@remix-run/server-runtime': 2.1.0(typescript@5.5.4) react: 18.2.0 - remix-utils@7.7.0(@remix-run/node@2.1.0(typescript@5.9.3))(@remix-run/react@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))(@remix-run/router@1.15.3)(crypto-js@4.2.0)(intl-parse-accept-language@1.0.0)(react@18.2.0)(zod@3.25.76): + remix-utils@7.7.0(@remix-run/node@2.1.0(typescript@5.5.4))(@remix-run/react@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.5.4))(@remix-run/router@1.15.3)(crypto-js@4.2.0)(intl-parse-accept-language@1.0.0)(react@18.2.0)(zod@3.25.76): dependencies: type-fest: 4.33.0 optionalDependencies: - '@remix-run/node': 2.1.0(typescript@5.9.3) - '@remix-run/react': 2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3) + '@remix-run/node': 2.1.0(typescript@5.5.4) + '@remix-run/react': 2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.5.4) '@remix-run/router': 1.15.3 crypto-js: 4.2.0 intl-parse-accept-language: 1.0.0 @@ -40724,10 +40711,6 @@ snapshots: optionalDependencies: typescript: 5.5.4 - ts-essentials@10.0.1(typescript@5.9.3): - optionalDependencies: - typescript: 5.9.3 - ts-expose-internals-conditionally@1.0.0-empty.0: {} ts-interface-checker@0.1.13: {} @@ -40754,10 +40737,6 @@ snapshots: optionalDependencies: typescript: 5.5.4 - tsconfck@2.1.2(typescript@5.9.3): - optionalDependencies: - typescript: 5.9.3 - tsconfck@3.1.3(typescript@5.5.4): optionalDependencies: typescript: 5.5.4 @@ -40834,10 +40813,10 @@ snapshots: - tsx - yaml - tsutils@3.21.0(typescript@5.9.3): + tsutils@3.21.0(typescript@5.5.4): dependencies: tslib: 1.14.1 - typescript: 5.9.3 + typescript: 5.5.4 tsx@3.12.2: dependencies: @@ -40995,8 +40974,6 @@ snapshots: typescript@5.5.4: {} - typescript@5.9.3: {} - ufo@1.5.4: {} ufo@1.6.1: {} @@ -41392,15 +41369,6 @@ snapshots: - supports-color - typescript - vite-tsconfig-paths@4.0.5(typescript@5.9.3): - dependencies: - debug: 4.3.7(supports-color@10.0.0) - globrex: 0.1.2 - tsconfck: 2.1.2(typescript@5.9.3) - transitivePeerDependencies: - - supports-color - - typescript - vite@4.4.9(@types/node@22.13.9)(lightningcss@1.29.2)(terser@5.44.1): dependencies: esbuild: 0.18.11 From 5f09a6a792d9f13b60ed4ef44638c76d4386e361 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Mon, 26 Jan 2026 16:43:17 +0000 Subject: [PATCH 3/5] replace all memory stores with the lru memory store --- .../authorizationRateLimitMiddleware.server.ts | 7 ++----- .../app/services/betterstack/betterstack.server.ts | 4 ++-- apps/webapp/app/services/platform.v3.server.ts | 10 ++-------- apps/webapp/app/services/realtimeClient.server.ts | 7 ++----- apps/webapp/app/services/requestIdempotency.server.ts | 10 ++-------- .../app/v3/marqs/fairDequeuingStrategy.server.ts | 10 ++-------- 6 files changed, 12 insertions(+), 36 deletions(-) diff --git a/apps/webapp/app/services/authorizationRateLimitMiddleware.server.ts b/apps/webapp/app/services/authorizationRateLimitMiddleware.server.ts index 2d4cc8b1f2b..431a0062db8 100644 --- a/apps/webapp/app/services/authorizationRateLimitMiddleware.server.ts +++ b/apps/webapp/app/services/authorizationRateLimitMiddleware.server.ts @@ -1,5 +1,5 @@ import { createCache, DefaultStatefulContext, Namespace, Cache as UnkeyCache } from "@unkey/cache"; -import { MemoryStore } from "@unkey/cache/stores"; +import { createLRUMemoryStore } from "@internal/cache"; import { Ratelimit } from "@upstash/ratelimit"; import { Request as ExpressRequest, Response as ExpressResponse, NextFunction } from "express"; import { createHash } from "node:crypto"; @@ -157,10 +157,7 @@ export function authorizationRateLimitMiddleware({ limiterConfigOverride, }: Options) { const ctx = new DefaultStatefulContext(); - const memory = new MemoryStore({ - persistentMap: new Map(), - unstableEvictOnSet: { frequency: 0.001, maxItems: limiterCache?.maxItems ?? 1000 }, - }); + const memory = createLRUMemoryStore(limiterCache?.maxItems ?? 1000); const redisCacheStore = new RedisCacheStore({ connection: { keyPrefix: `cache:${keyPrefix}:rate-limit-cache:`, diff --git a/apps/webapp/app/services/betterstack/betterstack.server.ts b/apps/webapp/app/services/betterstack/betterstack.server.ts index 41a43b3a699..75b404745a7 100644 --- a/apps/webapp/app/services/betterstack/betterstack.server.ts +++ b/apps/webapp/app/services/betterstack/betterstack.server.ts @@ -1,6 +1,6 @@ import { type ApiResult, wrapZodFetch } from "@trigger.dev/core/v3/zodfetch"; import { createCache, DefaultStatefulContext, Namespace } from "@unkey/cache"; -import { MemoryStore } from "@unkey/cache/stores"; +import { createLRUMemoryStore } from "@internal/cache"; import { z } from "zod"; import { env } from "~/env.server"; @@ -17,7 +17,7 @@ const IncidentSchema = z.object({ export type Incident = z.infer; const ctx = new DefaultStatefulContext(); -const memory = new MemoryStore({ persistentMap: new Map() }); +const memory = createLRUMemoryStore(100); const cache = createCache({ query: new Namespace>(ctx, { diff --git a/apps/webapp/app/services/platform.v3.server.ts b/apps/webapp/app/services/platform.v3.server.ts index d83da275dc5..2fc4c8c5c1f 100644 --- a/apps/webapp/app/services/platform.v3.server.ts +++ b/apps/webapp/app/services/platform.v3.server.ts @@ -15,7 +15,7 @@ import { type CurrentPlan, } from "@trigger.dev/platform"; import { createCache, DefaultStatefulContext, Namespace } from "@unkey/cache"; -import { MemoryStore } from "@unkey/cache/stores"; +import { createLRUMemoryStore } from "@internal/cache"; import { existsSync, readFileSync } from "node:fs"; import { redirect } from "remix-typedjson"; import { z } from "zod"; @@ -45,13 +45,7 @@ const client = singleton("billingClient", initializeClient); function initializePlatformCache() { const ctx = new DefaultStatefulContext(); - const memory = new MemoryStore({ - persistentMap: new Map(), - unstableEvictOnSet: { - frequency: 0.01, - maxItems: 1000, - }, - }); + const memory = createLRUMemoryStore(1000); const redisCacheStore = new RedisCacheStore({ connection: { keyPrefix: "tr:cache:platform:v3", diff --git a/apps/webapp/app/services/realtimeClient.server.ts b/apps/webapp/app/services/realtimeClient.server.ts index f51d863267a..d962a57426e 100644 --- a/apps/webapp/app/services/realtimeClient.server.ts +++ b/apps/webapp/app/services/realtimeClient.server.ts @@ -8,7 +8,7 @@ import { longPollingFetch } from "~/utils/longPollingFetch"; import { logger } from "./logger.server"; import { jumpHash } from "@trigger.dev/core/v3/serverOnly"; import { Cache, createCache, DefaultStatefulContext, Namespace } from "@unkey/cache"; -import { MemoryStore } from "@unkey/cache/stores"; +import { createLRUMemoryStore } from "@internal/cache"; import { RedisCacheStore } from "./unkey/redisCacheStore.server"; import { env } from "~/env.server"; import { API_VERSIONS, CURRENT_API_VERSION } from "~/api/versions"; @@ -84,10 +84,7 @@ export class RealtimeClient { this.#registerCommands(); const ctx = new DefaultStatefulContext(); - const memory = new MemoryStore({ - persistentMap: new Map(), - unstableEvictOnSet: { frequency: 0.01, maxItems: 1000 }, - }); + const memory = createLRUMemoryStore(1000); const redisCacheStore = new RedisCacheStore({ connection: { keyPrefix: "tr:cache:realtime", diff --git a/apps/webapp/app/services/requestIdempotency.server.ts b/apps/webapp/app/services/requestIdempotency.server.ts index cda697b9688..85767ed8951 100644 --- a/apps/webapp/app/services/requestIdempotency.server.ts +++ b/apps/webapp/app/services/requestIdempotency.server.ts @@ -1,6 +1,6 @@ import { Logger, LogLevel } from "@trigger.dev/core/logger"; import { createCache, DefaultStatefulContext, Namespace, Cache as UnkeyCache } from "@unkey/cache"; -import { MemoryStore } from "@unkey/cache/stores"; +import { createLRUMemoryStore } from "@internal/cache"; import { RedisCacheStore } from "./unkey/redisCacheStore.server"; import { RedisWithClusterOptions } from "~/redis.server"; import { validate as uuidValidate, version as uuidVersion } from "uuid"; @@ -33,13 +33,7 @@ export class RequestIdempotencyService { : "request-idempotency:"; const ctx = new DefaultStatefulContext(); - const memory = new MemoryStore({ - persistentMap: new Map(), - unstableEvictOnSet: { - frequency: 0.001, - maxItems: 1000, - }, - }); + const memory = createLRUMemoryStore(1000); const redisCacheStore = new RedisCacheStore({ name: "request-idempotency", connection: { diff --git a/apps/webapp/app/v3/marqs/fairDequeuingStrategy.server.ts b/apps/webapp/app/v3/marqs/fairDequeuingStrategy.server.ts index cbae7e84683..e9205cd000e 100644 --- a/apps/webapp/app/v3/marqs/fairDequeuingStrategy.server.ts +++ b/apps/webapp/app/v3/marqs/fairDequeuingStrategy.server.ts @@ -1,5 +1,5 @@ import { createCache, DefaultStatefulContext, Namespace, Cache as UnkeyCache } from "@unkey/cache"; -import { MemoryStore } from "@unkey/cache/stores"; +import { createLRUMemoryStore } from "@internal/cache"; import { randomUUID } from "crypto"; import { Redis } from "ioredis"; import { EnvQueues, MarQSFairDequeueStrategy, MarQSKeyProducer } from "./types"; @@ -99,13 +99,7 @@ export class FairDequeuingStrategy implements MarQSFairDequeueStrategy { constructor(private options: FairDequeuingStrategyOptions) { const ctx = new DefaultStatefulContext(); - const memory = new MemoryStore({ - persistentMap: new Map(), - unstableEvictOnSet: { - frequency: 0.01, - maxItems: 500, - }, - }); + const memory = createLRUMemoryStore(500); this._cache = createCache({ concurrencyLimit: new Namespace(ctx, { From 2871f4ab35b17cea464220570ef6ae280e2d1138 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Mon, 26 Jan 2026 16:58:24 +0000 Subject: [PATCH 4/5] add lru memory tests --- internal-packages/cache/package.json | 4 +- .../cache/src/stores/lruMemory.test.ts | 333 ++++++++++++++++++ internal-packages/cache/vitest.config.ts | 10 + 3 files changed, 346 insertions(+), 1 deletion(-) create mode 100644 internal-packages/cache/src/stores/lruMemory.test.ts create mode 100644 internal-packages/cache/vitest.config.ts diff --git a/internal-packages/cache/package.json b/internal-packages/cache/package.json index 7f8acf735f9..e26f0578bf2 100644 --- a/internal-packages/cache/package.json +++ b/internal-packages/cache/package.json @@ -14,6 +14,8 @@ "superjson": "^2.2.1" }, "scripts": { - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "test": "vitest --run", + "test:watch": "vitest" } } \ No newline at end of file diff --git a/internal-packages/cache/src/stores/lruMemory.test.ts b/internal-packages/cache/src/stores/lruMemory.test.ts new file mode 100644 index 00000000000..7b9580a19ff --- /dev/null +++ b/internal-packages/cache/src/stores/lruMemory.test.ts @@ -0,0 +1,333 @@ +import { describe, it, expect, beforeEach, vi } from "vitest"; +import { LRUMemoryStore, createLRUMemoryStore } from "./lruMemory.js"; +import type { Entry } from "@unkey/cache/stores"; + +function createEntry(value: T, freshUntil: number, staleUntil: number): Entry { + return { value, freshUntil, staleUntil }; +} + +describe("LRUMemoryStore", () => { + let store: LRUMemoryStore; + + beforeEach(() => { + store = new LRUMemoryStore({ max: 5, name: "test-store" }); + }); + + describe("basic operations", () => { + it("should set and get a value", async () => { + const entry = createEntry("test-value", Date.now() + 60000, Date.now() + 120000); + + const setResult = await store.set("ns", "key1", entry); + expect(setResult.err).toBeUndefined(); + + const getResult = await store.get("ns", "key1"); + expect(getResult.err).toBeUndefined(); + expect(getResult.val).toEqual(entry); + }); + + it("should return undefined for missing keys", async () => { + const result = await store.get("ns", "nonexistent"); + expect(result.err).toBeUndefined(); + expect(result.val).toBeUndefined(); + }); + + it("should remove a single key", async () => { + const entry = createEntry("value", Date.now() + 60000, Date.now() + 120000); + await store.set("ns", "key1", entry); + + const removeResult = await store.remove("ns", "key1"); + expect(removeResult.err).toBeUndefined(); + + const getResult = await store.get("ns", "key1"); + expect(getResult.val).toBeUndefined(); + }); + + it("should remove multiple keys", async () => { + const entry = createEntry("value", Date.now() + 60000, Date.now() + 120000); + await store.set("ns", "key1", entry); + await store.set("ns", "key2", entry); + await store.set("ns", "key3", entry); + + const removeResult = await store.remove("ns", ["key1", "key2"]); + expect(removeResult.err).toBeUndefined(); + + expect((await store.get("ns", "key1")).val).toBeUndefined(); + expect((await store.get("ns", "key2")).val).toBeUndefined(); + expect((await store.get("ns", "key3")).val).not.toBeUndefined(); + }); + }); + + describe("namespace isolation", () => { + it("should isolate keys by namespace", async () => { + const entry1 = createEntry("value1", Date.now() + 60000, Date.now() + 120000); + const entry2 = createEntry("value2", Date.now() + 60000, Date.now() + 120000); + + await store.set("ns1", "key", entry1); + await store.set("ns2", "key", entry2); + + const result1 = await store.get("ns1", "key"); + const result2 = await store.get("ns2", "key"); + + expect(result1.val?.value).toBe("value1"); + expect(result2.val?.value).toBe("value2"); + }); + }); + + describe("TTL expiration", () => { + it("should return undefined for expired entries (past staleUntil)", async () => { + const entry = createEntry("value", Date.now() - 2000, Date.now() - 1000); // Already expired + + await store.set("ns", "expired-key", entry); + + const result = await store.get("ns", "expired-key"); + expect(result.val).toBeUndefined(); + }); + + it("should return entry that is stale but not expired", async () => { + const now = Date.now(); + // Fresh until 1 second ago, stale until 1 hour from now + const entry = createEntry("value", now - 1000, now + 3600000); + + await store.set("ns", "stale-key", entry); + + const result = await store.get("ns", "stale-key"); + expect(result.val).not.toBeUndefined(); + expect(result.val?.value).toBe("value"); + }); + + it("should delete expired entry on get", async () => { + const entry = createEntry("value", Date.now() - 2000, Date.now() - 1000); + await store.set("ns", "key", entry); + + // First get should return undefined and delete + await store.get("ns", "key"); + + // Size should reflect deletion + expect(store.size).toBe(0); + }); + }); + + describe("LRU eviction", () => { + it("should evict least recently used items when at capacity", async () => { + const entry = (val: string) => createEntry(val, Date.now() + 60000, Date.now() + 120000); + + // Fill the cache (max: 5) + await store.set("ns", "key1", entry("value1")); + await store.set("ns", "key2", entry("value2")); + await store.set("ns", "key3", entry("value3")); + await store.set("ns", "key4", entry("value4")); + await store.set("ns", "key5", entry("value5")); + + expect(store.size).toBe(5); + + // Add one more - should evict key1 (least recently used) + await store.set("ns", "key6", entry("value6")); + + expect(store.size).toBe(5); + expect((await store.get("ns", "key1")).val).toBeUndefined(); // Evicted + expect((await store.get("ns", "key6")).val?.value).toBe("value6"); // Present + }); + + it("should update LRU order on get", async () => { + const entry = (val: string) => createEntry(val, Date.now() + 60000, Date.now() + 120000); + + // Fill the cache + await store.set("ns", "key1", entry("value1")); + await store.set("ns", "key2", entry("value2")); + await store.set("ns", "key3", entry("value3")); + await store.set("ns", "key4", entry("value4")); + await store.set("ns", "key5", entry("value5")); + + // Access key1 to make it recently used + await store.get("ns", "key1"); + + // Add new item - should evict key2 (now least recently used) + await store.set("ns", "key6", entry("value6")); + + expect((await store.get("ns", "key1")).val?.value).toBe("value1"); // Still present + expect((await store.get("ns", "key2")).val).toBeUndefined(); // Evicted + }); + + it("should update LRU order on set (update existing)", async () => { + const entry = (val: string) => createEntry(val, Date.now() + 60000, Date.now() + 120000); + + // Fill the cache + await store.set("ns", "key1", entry("value1")); + await store.set("ns", "key2", entry("value2")); + await store.set("ns", "key3", entry("value3")); + await store.set("ns", "key4", entry("value4")); + await store.set("ns", "key5", entry("value5")); + + // Update key1 to make it recently used + await store.set("ns", "key1", entry("updated-value1")); + + // Add new item - should evict key2 (now least recently used) + await store.set("ns", "key6", entry("value6")); + + expect((await store.get("ns", "key1")).val?.value).toBe("updated-value1"); + expect((await store.get("ns", "key2")).val).toBeUndefined(); // Evicted + }); + }); + + describe("hard limit enforcement", () => { + it("should never exceed max size regardless of write rate", async () => { + const smallStore = new LRUMemoryStore({ max: 10 }); + const entry = (val: number) => createEntry(val, Date.now() + 60000, Date.now() + 120000); + + // Write 1000 items rapidly + for (let i = 0; i < 1000; i++) { + await smallStore.set("ns", `key${i}`, entry(i)); + // Verify size never exceeds max + expect(smallStore.size).toBeLessThanOrEqual(10); + } + + expect(smallStore.size).toBe(10); + }); + + it("should maintain most recent items when at capacity", async () => { + const smallStore = new LRUMemoryStore({ max: 3 }); + const entry = (val: number) => createEntry(val, Date.now() + 60000, Date.now() + 120000); + + // Write items sequentially + await smallStore.set("ns", "key1", entry(1)); + await smallStore.set("ns", "key2", entry(2)); + await smallStore.set("ns", "key3", entry(3)); + await smallStore.set("ns", "key4", entry(4)); + await smallStore.set("ns", "key5", entry(5)); + + // Only the 3 most recent should remain + expect((await smallStore.get("ns", "key1")).val).toBeUndefined(); + expect((await smallStore.get("ns", "key2")).val).toBeUndefined(); + expect((await smallStore.get("ns", "key3")).val?.value).toBe(3); + expect((await smallStore.get("ns", "key4")).val?.value).toBe(4); + expect((await smallStore.get("ns", "key5")).val?.value).toBe(5); + }); + }); + + describe("utility methods", () => { + it("should report correct size", async () => { + const entry = createEntry("value", Date.now() + 60000, Date.now() + 120000); + + expect(store.size).toBe(0); + + await store.set("ns", "key1", entry); + expect(store.size).toBe(1); + + await store.set("ns", "key2", entry); + expect(store.size).toBe(2); + + await store.remove("ns", "key1"); + expect(store.size).toBe(1); + }); + + it("should clear all items", async () => { + const entry = createEntry("value", Date.now() + 60000, Date.now() + 120000); + + await store.set("ns1", "key1", entry); + await store.set("ns2", "key2", entry); + await store.set("ns3", "key3", entry); + + expect(store.size).toBe(3); + + store.clear(); + + expect(store.size).toBe(0); + expect((await store.get("ns1", "key1")).val).toBeUndefined(); + }); + + it("should use custom name", () => { + const customStore = new LRUMemoryStore({ max: 10, name: "custom-name" }); + expect(customStore.name).toBe("custom-name"); + }); + + it("should use default name when not provided", () => { + const defaultStore = new LRUMemoryStore({ max: 10 }); + expect(defaultStore.name).toBe("lru-memory"); + }); + }); + + describe("createLRUMemoryStore helper", () => { + it("should create a store with specified max size", async () => { + const helperStore = createLRUMemoryStore(3); + const entry = (val: number) => createEntry(val, Date.now() + 60000, Date.now() + 120000); + + await helperStore.set("ns", "key1", entry(1)); + await helperStore.set("ns", "key2", entry(2)); + await helperStore.set("ns", "key3", entry(3)); + await helperStore.set("ns", "key4", entry(4)); + + expect(helperStore.size).toBe(3); + expect((await helperStore.get("ns", "key1")).val).toBeUndefined(); + }); + + it("should accept custom name", () => { + const namedStore = createLRUMemoryStore(10, "my-cache"); + expect(namedStore.name).toBe("my-cache"); + }); + }); + + describe("complex value types", () => { + it("should handle object values", async () => { + const objectStore = new LRUMemoryStore({ max: 5 }); + const complexValue = { id: 123, data: ["a", "b", "c"] }; + const entry = createEntry(complexValue, Date.now() + 60000, Date.now() + 120000); + + await objectStore.set("ns", "obj-key", entry); + + const result = await objectStore.get("ns", "obj-key"); + expect(result.val?.value).toEqual(complexValue); + }); + + it("should handle null and undefined values", async () => { + const nullStore = new LRUMemoryStore({ max: 5 }); + + const nullEntry = createEntry(null, Date.now() + 60000, Date.now() + 120000); + const undefinedEntry = createEntry(undefined, Date.now() + 60000, Date.now() + 120000); + + await nullStore.set("ns", "null-key", nullEntry); + await nullStore.set("ns", "undefined-key", undefinedEntry); + + expect((await nullStore.get("ns", "null-key")).val?.value).toBeNull(); + expect((await nullStore.get("ns", "undefined-key")).val?.value).toBeUndefined(); + }); + }); + + describe("concurrent operations", () => { + it("should handle concurrent writes safely", async () => { + const concurrentStore = new LRUMemoryStore({ max: 100 }); + const entry = (val: number) => createEntry(val, Date.now() + 60000, Date.now() + 120000); + + // Simulate concurrent writes + const writes = Array.from({ length: 50 }, (_, i) => + concurrentStore.set("ns", `key${i}`, entry(i)) + ); + + await Promise.all(writes); + + expect(concurrentStore.size).toBe(50); + }); + + it("should handle concurrent reads and writes", async () => { + const concurrentStore = new LRUMemoryStore({ max: 100 }); + const entry = (val: number) => createEntry(val, Date.now() + 60000, Date.now() + 120000); + + // Pre-populate + for (let i = 0; i < 50; i++) { + await concurrentStore.set("ns", `key${i}`, entry(i)); + } + + // Mix of reads and writes + const operations = [ + ...Array.from({ length: 25 }, (_, i) => concurrentStore.get("ns", `key${i}`)), + ...Array.from({ length: 25 }, (_, i) => + concurrentStore.set("ns", `new-key${i}`, entry(i + 100)) + ), + ]; + + await Promise.all(operations); + + // Should not exceed max + expect(concurrentStore.size).toBeLessThanOrEqual(100); + }); + }); +}); diff --git a/internal-packages/cache/vitest.config.ts b/internal-packages/cache/vitest.config.ts new file mode 100644 index 00000000000..e07f05e842b --- /dev/null +++ b/internal-packages/cache/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["**/*.test.ts"], + globals: true, + isolate: true, + testTimeout: 10_000, + }, +}); From f1be5b3194d466a5c39de39584dd02fd04eda017 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Mon, 26 Jan 2026 17:17:04 +0000 Subject: [PATCH 5/5] fix cache vitests --- internal-packages/cache/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal-packages/cache/package.json b/internal-packages/cache/package.json index e26f0578bf2..02ba86e1fd6 100644 --- a/internal-packages/cache/package.json +++ b/internal-packages/cache/package.json @@ -15,7 +15,7 @@ }, "scripts": { "typecheck": "tsc --noEmit", - "test": "vitest --run", + "test": "vitest", "test:watch": "vitest" } } \ No newline at end of file