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 fb8c833f16..11fa390673 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 6c008b107e..7cc21101bf 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 befe2a0a89..b29056c624 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 267ff2d92d..7f8acf735f 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 479d2fce1b..c0064a08f0 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 0000000000..63b1d5cd4b --- /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 19c67d398e..e4bef2f438 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 11fa390673..a8fe3ccdc0 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 46396fda41..0e2205e413 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 a835d8d2a0..76f12a8774 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 2d4cc8b1f2..431a0062db 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 41a43b3a69..75b404745a 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 d83da275dc..2fc4c8c5c1 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 f51d863267..d962a57426 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 cda697b968..85767ed895 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 cbae7e8468..e9205cd000 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 7f8acf735f..e26f0578bf 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 0000000000..7b9580a19f --- /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 0000000000..e07f05e842 --- /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 e26f0578bf..02ba86e1fd 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