@leejpsd/nextjs-cache-handler 0.1.1 → 0.2.0

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.d.cts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { CacheComponentsHandler, createCacheComponentsHandler, partitionEntry, shouldServeStale } from './cache-components/index.cjs';
2
2
  export { IncrementalCtx, IncrementalRedisCacheHandler, createIncrementalCacheHandler } from './incremental/index.cjs';
3
3
  export { getMetricSnapshot } from './ops/index.cjs';
4
- export { C as CacheComponentsEntry, b as CacheHandlerOptions, F as FallbackStrategy, I as IncrementalCacheData, L as Logger, M as MetricEmitter, c as MetricEvent, d as MetricEventType, a as RedisClientConfig, e as RedisClientFactory, R as RedisClientLike } from './types-CgriT7My.cjs';
4
+ export { C as CacheComponentsEntry, b as CacheHandlerOptions, F as FallbackStrategy, I as IncrementalCacheData, L as Logger, M as MetricEmitter, c as MetricEvent, d as MetricEventType, a as RedisClientConfig, e as RedisClientFactory, R as RedisClientLike } from './types-Dq1q6qXd.cjs';
5
5
 
6
6
  /**
7
7
  * Timeout-bounded operation wrapper. Surrounds every Redis call so a hung
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { CacheComponentsHandler, createCacheComponentsHandler, partitionEntry, shouldServeStale } from './cache-components/index.js';
2
2
  export { IncrementalCtx, IncrementalRedisCacheHandler, createIncrementalCacheHandler } from './incremental/index.js';
3
3
  export { getMetricSnapshot } from './ops/index.js';
4
- export { C as CacheComponentsEntry, b as CacheHandlerOptions, F as FallbackStrategy, I as IncrementalCacheData, L as Logger, M as MetricEmitter, c as MetricEvent, d as MetricEventType, a as RedisClientConfig, e as RedisClientFactory, R as RedisClientLike } from './types-CgriT7My.js';
4
+ export { C as CacheComponentsEntry, b as CacheHandlerOptions, F as FallbackStrategy, I as IncrementalCacheData, L as Logger, M as MetricEmitter, c as MetricEvent, d as MetricEventType, a as RedisClientConfig, e as RedisClientFactory, R as RedisClientLike } from './types-Dq1q6qXd.js';
5
5
 
6
6
  /**
7
7
  * Timeout-bounded operation wrapper. Surrounds every Redis call so a hung
package/dist/index.js CHANGED
@@ -641,7 +641,9 @@ var DEFAULT_KEY_PREFIX = "next-cache:";
641
641
  var ENTRY_SUFFIX = "entry:";
642
642
  var TAG_SUFFIX = "tag:";
643
643
  var TAG_EXP_SUFFIX = "tag-expiration:";
644
+ var LOCK_SUFFIX = "lock:";
644
645
  var DEFAULT_ABORT_MS = 1500;
646
+ var DEFAULT_LOCK_TTL_SEC = 10;
645
647
  var ONE_YEAR_SEC = 60 * 60 * 24 * 365;
646
648
  var FALLBACK_TTL_SEC = 3600;
647
649
  var TAG_EXP_MARKER_TTL_SEC = 60 * 60 * 24 * 7;
@@ -664,6 +666,8 @@ function init(opts) {
664
666
  staleWhileRevalidate: opts.staleWhileRevalidate ?? true,
665
667
  hashTag: opts.hashTag ?? false,
666
668
  keyPrefix: opts.keyPrefix ?? DEFAULT_KEY_PREFIX,
669
+ singleFlight: opts.singleFlight ?? false,
670
+ singleFlightLockTtlSec: opts.singleFlightLockTtlSec ?? DEFAULT_LOCK_TTL_SEC,
667
671
  rest: opts
668
672
  },
669
673
  logger,
@@ -673,7 +677,8 @@ function init(opts) {
673
677
  memTags: new MemorySetStore(),
674
678
  memTagExp: /* @__PURE__ */ new Map(),
675
679
  localTagTimestamps: /* @__PURE__ */ new Map(),
676
- resolveNs: () => resolveBuildNamespace(opts.buildNamespace)
680
+ resolveNs: () => resolveBuildNamespace(opts.buildNamespace),
681
+ instanceId: process.env.HOSTNAME || process.env.ECS_TASK_ID || `pid-${process.pid}`
677
682
  };
678
683
  }
679
684
  function entryKey(state, cacheKey) {
@@ -700,6 +705,40 @@ function tagExpKey(state, tag) {
700
705
  state.opts.hashTag
701
706
  );
702
707
  }
708
+ function lockKey(state, cacheKey) {
709
+ return buildKey(
710
+ `${state.opts.keyPrefix}${LOCK_SUFFIX}`,
711
+ state.resolveNs(),
712
+ cacheKey,
713
+ state.opts.hashTag
714
+ );
715
+ }
716
+ async function tryAcquireRefreshLock(state, cacheKey) {
717
+ if (!state.opts.singleFlight) return false;
718
+ if (!gateRedis(state)) return false;
719
+ try {
720
+ return await withAbortSignal(
721
+ "cacheHandlers.singleFlight.lock",
722
+ state.opts.abortTimeoutMs,
723
+ async () => {
724
+ const client = await state.conn.getOrConnect();
725
+ if (!client) return false;
726
+ const result = await execLuaScript(
727
+ client,
728
+ "refreshTagLock",
729
+ [lockKey(state, cacheKey)],
730
+ [
731
+ state.instanceId,
732
+ String(state.opts.singleFlightLockTtlSec)
733
+ ]
734
+ );
735
+ return result === 1;
736
+ }
737
+ );
738
+ } catch {
739
+ return false;
740
+ }
741
+ }
703
742
  function resolveTtlSeconds(entry) {
704
743
  if (Number.isFinite(entry.expire) && entry.expire > 0) {
705
744
  return Math.max(60, Math.ceil(entry.expire));
@@ -794,11 +833,28 @@ async function getImpl(state, cacheKey, softTags) {
794
833
  return void 0;
795
834
  }
796
835
  }
797
- state.emit({
798
- type: partition.freshness === "stale" ? "cache.stale" : "cache.hit",
799
- ms: Date.now() - start,
800
- meta: { freshness: partition.freshness }
801
- });
836
+ if (partition.freshness === "stale") {
837
+ if (state.opts.singleFlight) {
838
+ const isLeader = await tryAcquireRefreshLock(state, cacheKey);
839
+ state.emit({
840
+ type: isLeader ? "cache.stale.refresh.leader" : "cache.stale.refresh.follower",
841
+ ms: Date.now() - start,
842
+ meta: { freshness: partition.freshness }
843
+ });
844
+ } else {
845
+ state.emit({
846
+ type: "cache.stale",
847
+ ms: Date.now() - start,
848
+ meta: { freshness: partition.freshness }
849
+ });
850
+ }
851
+ } else {
852
+ state.emit({
853
+ type: "cache.hit",
854
+ ms: Date.now() - start,
855
+ meta: { freshness: partition.freshness }
856
+ });
857
+ }
802
858
  return {
803
859
  value: bufferToStream(Buffer.from(envelope.value, "base64")),
804
860
  tags: envelope.tags,