@adaptic/utils 0.0.1003 → 0.0.1005

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -63157,6 +63157,7 @@ class StampedeProtectedCache {
63157
63157
  coalescedRequests: 0,
63158
63158
  backgroundRefreshes: 0,
63159
63159
  refreshErrors: 0,
63160
+ loadTimeouts: 0,
63160
63161
  };
63161
63162
  constructor(options) {
63162
63163
  this.options = {
@@ -63430,6 +63431,7 @@ class StampedeProtectedCache {
63430
63431
  coalescedRequests: this.stats.coalescedRequests,
63431
63432
  backgroundRefreshes: this.stats.backgroundRefreshes,
63432
63433
  refreshErrors: this.stats.refreshErrors,
63434
+ loadTimeouts: this.stats.loadTimeouts,
63433
63435
  };
63434
63436
  }
63435
63437
  /**
@@ -63480,8 +63482,9 @@ class StampedeProtectedCache {
63480
63482
  this.options.logger.debug("Request coalesced", { key });
63481
63483
  return existingPromise;
63482
63484
  }
63483
- // Create new promise and store it
63484
- const promise = this.loadAndCache(key, loader, ttl);
63485
+ // Create new promise and store it — bounded by loadTimeoutMs so a
63486
+ // never-settling loader cannot pin the key (see loadWithTimeout).
63487
+ const promise = this.loadWithTimeout(key, loader, ttl);
63485
63488
  this.pendingRefreshes.set(key, promise);
63486
63489
  try {
63487
63490
  const result = await promise;
@@ -63492,6 +63495,51 @@ class StampedeProtectedCache {
63492
63495
  this.pendingRefreshes.delete(key);
63493
63496
  }
63494
63497
  }
63498
+ /**
63499
+ * Race {@link loadAndCache} against the configured load timeout.
63500
+ *
63501
+ * On timeout: rejects with a typed timeout error, evicts the pending
63502
+ * single-flight entry so the NEXT caller retries with a fresh loader
63503
+ * (instead of coalescing onto the hung one), and attaches a settlement
63504
+ * observer to the abandoned loader so its eventual resolution/rejection
63505
+ * is logged rather than surfacing as an unhandled rejection.
63506
+ */
63507
+ async loadWithTimeout(key, loader, ttl) {
63508
+ const timeoutMs = this.options.loadTimeoutMs ?? 30000;
63509
+ const loadPromise = this.loadAndCache(key, loader, ttl);
63510
+ let timeoutHandle;
63511
+ const timeoutPromise = new Promise((_, reject) => {
63512
+ timeoutHandle = setTimeout(() => {
63513
+ this.stats.loadTimeouts++;
63514
+ // Evict the pin so the next caller retries fresh.
63515
+ this.pendingRefreshes.delete(key);
63516
+ this.options.logger.warn("Cache loader timed out — pin evicted", {
63517
+ key,
63518
+ timeoutMs,
63519
+ });
63520
+ // Observe the abandoned loader; never let it become unhandled.
63521
+ loadPromise
63522
+ .then(() => {
63523
+ this.options.logger.warn("Abandoned cache loader eventually resolved", { key });
63524
+ })
63525
+ .catch((error) => {
63526
+ this.options.logger.warn("Abandoned cache loader eventually rejected", {
63527
+ key,
63528
+ error: error instanceof Error ? error.message : String(error),
63529
+ });
63530
+ });
63531
+ reject(new Error(`StampedeProtectedCache loader timed out after ${timeoutMs}ms for key "${key}"`));
63532
+ }, timeoutMs);
63533
+ });
63534
+ try {
63535
+ return await Promise.race([loadPromise, timeoutPromise]);
63536
+ }
63537
+ finally {
63538
+ if (timeoutHandle !== undefined) {
63539
+ clearTimeout(timeoutHandle);
63540
+ }
63541
+ }
63542
+ }
63495
63543
  /**
63496
63544
  * Load data and cache it
63497
63545
  */
@@ -63641,6 +63689,7 @@ const DEFAULT_CACHE_OPTIONS = {
63641
63689
  minJitter: 0.9, // 90%
63642
63690
  maxJitter: 1.1, // 110%
63643
63691
  enableBackgroundRefresh: true,
63692
+ loadTimeoutMs: 30000, // 30s hard loader ceiling (anti-pinning)
63644
63693
  };
63645
63694
 
63646
63695
  /**