* Cache wrapper for functions whose result may legitimately be `null`. * * Stores results in a marked envelope so cached nulls are distinct from misses. * * @param fn - Function to execute (and optionally cache); may return null. * @param key - Cache key * @param ttlMs - Time to li
(
fn: () => Promise<T | null>,
key: CacheKey,
ttlMs: number
)
| 283 | * @returns Cached value if present (including a cached `null`), otherwise fresh result from fn() |
| 284 | */ |
| 285 | async withCacheNullable<T extends NonNullable<unknown>>( |
| 286 | fn: () => Promise<T | null>, |
| 287 | key: CacheKey, |
| 288 | ttlMs: number |
| 289 | ): Promise<T | null> { |
| 290 | if (!this.canUseCache(key, ttlMs)) { |
| 291 | return await fn(); |
| 292 | } |
| 293 | |
| 294 | const cachedValue = await this.tryGetCachedValue<unknown>(key, ttlMs); |
| 295 | if (cachedValue !== undefined) { |
| 296 | if (this.isNullableCacheBox<T>(cachedValue)) { |
| 297 | return cachedValue.value; |
| 298 | } |
| 299 | |
| 300 | logger.debug( |
| 301 | { key, ttlMs }, |
| 302 | "Nullable cache entry is missing marker or value field; treating as cache miss and refreshing" |
| 303 | ); |
| 304 | } |
| 305 | |
| 306 | const fresh = await fn(); |
| 307 | // Guard against type-erased callers resolving undefined. |
| 308 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- types exclude undefined; this guards against type-erasure bugs |
| 309 | if (fresh !== undefined) { |
| 310 | const box: NullableCacheBox<T> = { [NULLABLE_BOX_MARKER]: true, value: fresh }; |
| 311 | await this.trySetCache(key, box, ttlMs); |
| 312 | } |
| 313 | return fresh; |
| 314 | } |
| 315 | |
| 316 | /** Returns false when callers should bypass cache and execute directly. */ |
| 317 | private canUseCache(key: CacheKey, ttlMs: number): boolean { |
no test coverage detected