layercache 1.2.1 → 1.2.2

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.
@@ -156,6 +156,7 @@ interface CacheSingleFlightExecutionOptions {
156
156
  leaseMs: number;
157
157
  waitTimeoutMs: number;
158
158
  pollIntervalMs: number;
159
+ renewIntervalMs?: number;
159
160
  }
160
161
  interface CacheSingleFlightCoordinator {
161
162
  execute<T>(key: string, options: CacheSingleFlightExecutionOptions, worker: () => Promise<T>, waiter: () => Promise<T>): Promise<T>;
@@ -189,6 +190,7 @@ interface CacheStackOptions {
189
190
  singleFlightLeaseMs?: number;
190
191
  singleFlightTimeoutMs?: number;
191
192
  singleFlightPollMs?: number;
193
+ singleFlightRenewIntervalMs?: number;
192
194
  /**
193
195
  * Maximum number of entries in `accessProfiles` and `circuitBreakers` maps
194
196
  * before the oldest entries are pruned. Prevents unbounded memory growth.
@@ -219,6 +221,8 @@ interface CacheRateLimitOptions {
219
221
  maxConcurrent?: number;
220
222
  intervalMs?: number;
221
223
  maxPerInterval?: number;
224
+ scope?: 'global' | 'key' | 'fetcher';
225
+ bucketKey?: string;
222
226
  }
223
227
  interface CacheWriteBehindOptions {
224
228
  flushIntervalMs?: number;
@@ -527,6 +531,7 @@ declare class CacheStack extends EventEmitter {
527
531
  private validateWriteOptions;
528
532
  private validateLayerNumberOption;
529
533
  private validatePositiveNumber;
534
+ private validateRateLimitOptions;
530
535
  private validateNonNegativeNumber;
531
536
  private validateCacheKey;
532
537
  private validateTtlPolicy;
@@ -562,11 +562,12 @@ var CircuitBreakerManager = class {
562
562
 
563
563
  // ../../src/internal/FetchRateLimiter.ts
564
564
  var FetchRateLimiter = class {
565
- active = 0;
566
565
  queue = [];
567
- startedAt = [];
566
+ buckets = /* @__PURE__ */ new Map();
567
+ fetcherBuckets = /* @__PURE__ */ new WeakMap();
568
+ nextFetcherBucketId = 0;
568
569
  drainTimer;
569
- async schedule(options, task) {
570
+ async schedule(options, context, task) {
570
571
  if (!options) {
571
572
  return task();
572
573
  }
@@ -575,7 +576,13 @@ var FetchRateLimiter = class {
575
576
  return task();
576
577
  }
577
578
  return new Promise((resolve, reject) => {
578
- this.queue.push({ options: normalized, task, resolve, reject });
579
+ this.queue.push({
580
+ bucketKey: this.resolveBucketKey(normalized, context),
581
+ options: normalized,
582
+ task,
583
+ resolve,
584
+ reject
585
+ });
579
586
  this.drain();
580
587
  });
581
588
  }
@@ -589,63 +596,109 @@ var FetchRateLimiter = class {
589
596
  return {
590
597
  maxConcurrent,
591
598
  intervalMs,
592
- maxPerInterval
599
+ maxPerInterval,
600
+ scope: options.scope ?? "global",
601
+ bucketKey: options.bucketKey
593
602
  };
594
603
  }
604
+ resolveBucketKey(options, context) {
605
+ if (options.bucketKey) {
606
+ return `custom:${options.bucketKey}`;
607
+ }
608
+ if (options.scope === "key") {
609
+ return `key:${context.key}`;
610
+ }
611
+ if (options.scope === "fetcher") {
612
+ const existing = this.fetcherBuckets.get(context.fetcher);
613
+ if (existing) {
614
+ return existing;
615
+ }
616
+ const bucket = `fetcher:${this.nextFetcherBucketId}`;
617
+ this.nextFetcherBucketId += 1;
618
+ this.fetcherBuckets.set(context.fetcher, bucket);
619
+ return bucket;
620
+ }
621
+ return "global";
622
+ }
595
623
  drain() {
596
624
  if (this.drainTimer) {
597
625
  clearTimeout(this.drainTimer);
598
626
  this.drainTimer = void 0;
599
627
  }
600
628
  while (this.queue.length > 0) {
601
- const next = this.queue[0];
602
- if (!next) {
629
+ let nextIndex = -1;
630
+ let nextWaitMs = Number.POSITIVE_INFINITY;
631
+ for (let index = 0; index < this.queue.length; index += 1) {
632
+ const next2 = this.queue[index];
633
+ if (!next2) {
634
+ continue;
635
+ }
636
+ const waitMs = this.waitTime(next2.bucketKey, next2.options);
637
+ if (waitMs <= 0) {
638
+ nextIndex = index;
639
+ break;
640
+ }
641
+ nextWaitMs = Math.min(nextWaitMs, waitMs);
642
+ }
643
+ if (nextIndex < 0) {
644
+ if (Number.isFinite(nextWaitMs)) {
645
+ this.drainTimer = setTimeout(() => {
646
+ this.drainTimer = void 0;
647
+ this.drain();
648
+ }, nextWaitMs);
649
+ this.drainTimer.unref?.();
650
+ }
603
651
  return;
604
652
  }
605
- const waitMs = this.waitTime(next.options);
606
- if (waitMs > 0) {
607
- this.drainTimer = setTimeout(() => {
608
- this.drainTimer = void 0;
609
- this.drain();
610
- }, waitMs);
611
- this.drainTimer.unref?.();
653
+ const next = this.queue.splice(nextIndex, 1)[0];
654
+ if (!next) {
612
655
  return;
613
656
  }
614
- this.queue.shift();
615
- this.active += 1;
616
- this.startedAt.push(Date.now());
657
+ const bucket = this.bucketState(next.bucketKey);
658
+ bucket.active += 1;
659
+ bucket.startedAt.push(Date.now());
617
660
  void next.task().then(next.resolve, next.reject).finally(() => {
618
- this.active -= 1;
661
+ bucket.active -= 1;
619
662
  this.drain();
620
663
  });
621
664
  }
622
665
  }
623
- waitTime(options) {
666
+ waitTime(bucketKey, options) {
667
+ const bucket = this.bucketState(bucketKey);
624
668
  const now = Date.now();
625
- if (options.maxConcurrent && this.active >= options.maxConcurrent) {
669
+ if (options.maxConcurrent && bucket.active >= options.maxConcurrent) {
626
670
  return 1;
627
671
  }
628
672
  if (!options.intervalMs || !options.maxPerInterval) {
629
673
  return 0;
630
674
  }
631
- this.prune(now, options.intervalMs);
632
- if (this.startedAt.length < options.maxPerInterval) {
675
+ this.prune(bucket, now, options.intervalMs);
676
+ if (bucket.startedAt.length < options.maxPerInterval) {
633
677
  return 0;
634
678
  }
635
- const oldest = this.startedAt[0];
679
+ const oldest = bucket.startedAt[0];
636
680
  if (!oldest) {
637
681
  return 0;
638
682
  }
639
683
  return Math.max(1, options.intervalMs - (now - oldest));
640
684
  }
641
- prune(now, intervalMs) {
642
- while (this.startedAt.length > 0) {
643
- const startedAt = this.startedAt[0];
685
+ prune(bucket, now, intervalMs) {
686
+ while (bucket.startedAt.length > 0) {
687
+ const startedAt = bucket.startedAt[0];
644
688
  if (startedAt === void 0 || now - startedAt < intervalMs) {
645
689
  break;
646
690
  }
647
- this.startedAt.shift();
691
+ bucket.startedAt.shift();
692
+ }
693
+ }
694
+ bucketState(bucketKey) {
695
+ const existing = this.buckets.get(bucketKey);
696
+ if (existing) {
697
+ return existing;
648
698
  }
699
+ const bucket = { active: 0, startedAt: [] };
700
+ this.buckets.set(bucketKey, bucket);
701
+ return bucket;
649
702
  }
650
703
  };
651
704
 
@@ -1751,6 +1804,7 @@ var CacheStack = class extends EventEmitter {
1751
1804
  try {
1752
1805
  fetched = await this.fetchRateLimiter.schedule(
1753
1806
  options?.fetcherRateLimit ?? this.options.fetcherRateLimit,
1807
+ { key, fetcher },
1754
1808
  fetcher
1755
1809
  );
1756
1810
  this.circuitBreakerManager.recordSuccess(key);
@@ -2008,7 +2062,8 @@ var CacheStack = class extends EventEmitter {
2008
2062
  return {
2009
2063
  leaseMs: this.options.singleFlightLeaseMs ?? DEFAULT_SINGLE_FLIGHT_LEASE_MS,
2010
2064
  waitTimeoutMs: this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS,
2011
- pollIntervalMs: this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS
2065
+ pollIntervalMs: this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS,
2066
+ renewIntervalMs: this.options.singleFlightRenewIntervalMs
2012
2067
  };
2013
2068
  }
2014
2069
  async deleteKeys(keys) {
@@ -2213,6 +2268,8 @@ var CacheStack = class extends EventEmitter {
2213
2268
  this.validatePositiveNumber("singleFlightLeaseMs", this.options.singleFlightLeaseMs);
2214
2269
  this.validatePositiveNumber("singleFlightTimeoutMs", this.options.singleFlightTimeoutMs);
2215
2270
  this.validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
2271
+ this.validatePositiveNumber("singleFlightRenewIntervalMs", this.options.singleFlightRenewIntervalMs);
2272
+ this.validateRateLimitOptions("fetcherRateLimit", this.options.fetcherRateLimit);
2216
2273
  this.validateAdaptiveTtlOptions(this.options.adaptiveTtl);
2217
2274
  this.validateCircuitBreakerOptions(this.options.circuitBreaker);
2218
2275
  if (this.options.generation !== void 0) {
@@ -2232,6 +2289,7 @@ var CacheStack = class extends EventEmitter {
2232
2289
  this.validateTtlPolicy("options.ttlPolicy", options.ttlPolicy);
2233
2290
  this.validateAdaptiveTtlOptions(options.adaptiveTtl);
2234
2291
  this.validateCircuitBreakerOptions(options.circuitBreaker);
2292
+ this.validateRateLimitOptions("options.fetcherRateLimit", options.fetcherRateLimit);
2235
2293
  }
2236
2294
  validateLayerNumberOption(name, value) {
2237
2295
  if (value === void 0) {
@@ -2256,6 +2314,20 @@ var CacheStack = class extends EventEmitter {
2256
2314
  throw new Error(`${name} must be a positive finite number.`);
2257
2315
  }
2258
2316
  }
2317
+ validateRateLimitOptions(name, options) {
2318
+ if (!options) {
2319
+ return;
2320
+ }
2321
+ this.validatePositiveNumber(`${name}.maxConcurrent`, options.maxConcurrent);
2322
+ this.validatePositiveNumber(`${name}.intervalMs`, options.intervalMs);
2323
+ this.validatePositiveNumber(`${name}.maxPerInterval`, options.maxPerInterval);
2324
+ if (options.scope && !["global", "key", "fetcher"].includes(options.scope)) {
2325
+ throw new Error(`${name}.scope must be one of "global", "key", or "fetcher".`);
2326
+ }
2327
+ if (options.bucketKey !== void 0 && options.bucketKey.length === 0) {
2328
+ throw new Error(`${name}.bucketKey must not be empty.`);
2329
+ }
2330
+ }
2259
2331
  validateNonNegativeNumber(name, value) {
2260
2332
  if (!Number.isFinite(value) || value < 0) {
2261
2333
  throw new Error(`${name} must be a non-negative finite number.`);