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