(key: CacheKey, value: unknown, ttlMs: number)
| 358 | } |
| 359 | |
| 360 | private async trySetCache(key: CacheKey, value: unknown, ttlMs: number): Promise<void> { |
| 361 | // Never persist null/undefined. The read path (tryGetCachedValue) treats a |
| 362 | // stored JSON `null` as a cache miss, so writing one is not just useless — |
| 363 | // it causes a recompute-and-rewrite loop on every request for a hot key. |
| 364 | // This also hardens withCache against a `null` slipping past its |
| 365 | // `NonNullable<T>` constraint via type erasure (unknown/any plumbing, casts), |
| 366 | // which is the exact class of bug this change set fixes. withCacheNullable is |
| 367 | // unaffected: it always writes a non-null boxed envelope, never raw null. |
| 368 | if (value === undefined || value === null) { |
| 369 | logger.debug({ key, ttlMs }, "Refusing to cache nullish value; treating as non-cacheable"); |
| 370 | return; |
| 371 | } |
| 372 | |
| 373 | try { |
| 374 | const setResult = await this.set(key, value, ttlMs); |
| 375 | if (!setResult.ok) { |
| 376 | logger.debug( |
| 377 | { error: setResult.error, key, ttlMs }, |
| 378 | "Failed to cache fresh data, but returning result" |
| 379 | ); |
| 380 | } |
| 381 | } catch (error) { |
| 382 | logger.debug({ error, key, ttlMs }, "Cache set threw; returning fresh result"); |
| 383 | } |
| 384 | } |
| 385 | |
| 386 | /** |
| 387 | * Check if Redis is available and healthy by testing connectivity with ping |
no test coverage detected