@leejpsd/nextjs-cache-handler 0.1.0-rc.1

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.
Files changed (42) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/LICENSE +21 -0
  3. package/README.md +259 -0
  4. package/dist/cache-components/index.cjs +1053 -0
  5. package/dist/cache-components/index.cjs.map +1 -0
  6. package/dist/cache-components/index.d.cts +85 -0
  7. package/dist/cache-components/index.d.ts +85 -0
  8. package/dist/cache-components/index.js +1049 -0
  9. package/dist/cache-components/index.js.map +1 -0
  10. package/dist/incremental/index.cjs +734 -0
  11. package/dist/incremental/index.cjs.map +1 -0
  12. package/dist/incremental/index.d.cts +131 -0
  13. package/dist/incremental/index.d.ts +131 -0
  14. package/dist/incremental/index.js +731 -0
  15. package/dist/incremental/index.js.map +1 -0
  16. package/dist/index.cjs +1408 -0
  17. package/dist/index.cjs.map +1 -0
  18. package/dist/index.d.cts +59 -0
  19. package/dist/index.d.ts +59 -0
  20. package/dist/index.js +1397 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/ops/index.cjs +19 -0
  23. package/dist/ops/index.cjs.map +1 -0
  24. package/dist/ops/index.d.cts +22 -0
  25. package/dist/ops/index.d.ts +22 -0
  26. package/dist/ops/index.js +17 -0
  27. package/dist/ops/index.js.map +1 -0
  28. package/dist/shared/client/adapter-ioredis.cjs +130 -0
  29. package/dist/shared/client/adapter-ioredis.cjs.map +1 -0
  30. package/dist/shared/client/adapter-ioredis.d.cts +22 -0
  31. package/dist/shared/client/adapter-ioredis.d.ts +22 -0
  32. package/dist/shared/client/adapter-ioredis.js +125 -0
  33. package/dist/shared/client/adapter-ioredis.js.map +1 -0
  34. package/dist/shared/client/adapter-redis.cjs +32 -0
  35. package/dist/shared/client/adapter-redis.cjs.map +1 -0
  36. package/dist/shared/client/adapter-redis.d.cts +19 -0
  37. package/dist/shared/client/adapter-redis.d.ts +19 -0
  38. package/dist/shared/client/adapter-redis.js +29 -0
  39. package/dist/shared/client/adapter-redis.js.map +1 -0
  40. package/dist/types-CgriT7My.d.cts +193 -0
  41. package/dist/types-CgriT7My.d.ts +193 -0
  42. package/package.json +178 -0
@@ -0,0 +1,1049 @@
1
+ import { createHash } from 'crypto';
2
+
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
6
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
7
+ }) : x)(function(x) {
8
+ if (typeof require !== "undefined") return require.apply(this, arguments);
9
+ throw Error('Dynamic require of "' + x + '" is not supported');
10
+ });
11
+ var __esm = (fn, res) => function __init() {
12
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
13
+ };
14
+ var __export = (target, all) => {
15
+ for (var name in all)
16
+ __defProp(target, name, { get: all[name], enumerable: true });
17
+ };
18
+
19
+ // src/shared/client/adapter-redis.ts
20
+ var adapter_redis_exports = {};
21
+ __export(adapter_redis_exports, {
22
+ adaptRedisV5: () => adaptRedisV5,
23
+ createRedisV5Client: () => createRedisV5Client
24
+ });
25
+ function adaptRedisV5(client) {
26
+ return client;
27
+ }
28
+ function createRedisV5Client(config) {
29
+ const { createClient } = __require("redis");
30
+ const useTls = config.tls ?? config.url.startsWith("rediss://");
31
+ const opts = {
32
+ url: config.url,
33
+ socket: {
34
+ connectTimeout: config.connectTimeout ?? 1500,
35
+ reconnectStrategy: false,
36
+ ...useTls ? { tls: true } : {}
37
+ }
38
+ };
39
+ if (config.password !== void 0) opts.password = config.password;
40
+ return createClient(opts);
41
+ }
42
+ var init_adapter_redis = __esm({
43
+ "src/shared/client/adapter-redis.ts"() {
44
+ }
45
+ });
46
+
47
+ // src/shared/client/adapter-ioredis.ts
48
+ var adapter_ioredis_exports = {};
49
+ __export(adapter_ioredis_exports, {
50
+ adaptCluster: () => adaptCluster,
51
+ adaptIoredis: () => adaptIoredis,
52
+ createIoredisClient: () => createIoredisClient,
53
+ createIoredisCluster: () => createIoredisCluster
54
+ });
55
+ function adapt(client, isClusterClient = false) {
56
+ return {
57
+ get isOpen() {
58
+ return client.status === "ready";
59
+ },
60
+ connect: async () => {
61
+ if (client.status !== "ready" && client.status !== "connecting") {
62
+ await client.connect();
63
+ }
64
+ },
65
+ get: (k) => client.get(k),
66
+ set: (k, v, opts) => opts?.EX !== void 0 ? client.set(k, v, "EX", opts.EX) : client.set(k, v),
67
+ del: async (keys) => {
68
+ const arr = Array.isArray(keys) ? keys : [keys];
69
+ if (arr.length === 0) return 0;
70
+ return client.del(...arr);
71
+ },
72
+ sAdd: async (k, m) => {
73
+ const arr = Array.isArray(m) ? m : [m];
74
+ if (arr.length === 0) return 0;
75
+ return client.sadd(k, ...arr);
76
+ },
77
+ sMembers: (k) => client.smembers(k),
78
+ expire: async (k, s) => {
79
+ const r = await client.expire(k, s);
80
+ return r;
81
+ },
82
+ mGet: async (keys) => {
83
+ if (keys.length === 0) return [];
84
+ return client.mget(...keys);
85
+ },
86
+ eval: (script, opts) => client.eval(
87
+ script,
88
+ opts.keys.length,
89
+ ...opts.keys,
90
+ ...opts.arguments
91
+ ),
92
+ evalSha: (sha, opts) => client.evalsha(
93
+ sha,
94
+ opts.keys.length,
95
+ ...opts.keys,
96
+ ...opts.arguments
97
+ ),
98
+ scriptLoad: async (script) => await client.script("LOAD", script),
99
+ scanIterator: async function* ({ MATCH, COUNT }) {
100
+ if (isClusterClient) {
101
+ const cluster = client;
102
+ for (const node of cluster.nodes("master")) {
103
+ let cursor = "0";
104
+ do {
105
+ const [next, batch] = await node.scan(
106
+ cursor,
107
+ "MATCH",
108
+ MATCH,
109
+ "COUNT",
110
+ COUNT ?? 100
111
+ );
112
+ cursor = next;
113
+ if (batch.length > 0) yield batch;
114
+ } while (cursor !== "0");
115
+ }
116
+ } else {
117
+ const redis = client;
118
+ let cursor = "0";
119
+ do {
120
+ const [next, batch] = await redis.scan(
121
+ cursor,
122
+ "MATCH",
123
+ MATCH,
124
+ "COUNT",
125
+ COUNT ?? 100
126
+ );
127
+ cursor = next;
128
+ if (batch.length > 0) yield batch;
129
+ } while (cursor !== "0");
130
+ }
131
+ },
132
+ ping: () => client.ping(),
133
+ on: (event, listener) => {
134
+ client.on(event, listener);
135
+ return client;
136
+ }
137
+ };
138
+ }
139
+ function adaptIoredis(client) {
140
+ return adapt(client, false);
141
+ }
142
+ function adaptCluster(cluster) {
143
+ return adapt(cluster, true);
144
+ }
145
+ function createIoredisClient(config) {
146
+ const IORedis = __require("ioredis");
147
+ const Ctor = IORedis.default ?? IORedis;
148
+ return new Ctor(config.url, {
149
+ password: config.password,
150
+ connectTimeout: config.connectTimeout ?? 1500,
151
+ tls: config.tls ? {} : void 0,
152
+ lazyConnect: true,
153
+ maxRetriesPerRequest: 1
154
+ });
155
+ }
156
+ function createIoredisCluster(config) {
157
+ const IORedis = __require("ioredis");
158
+ const ClusterCtor = IORedis.Cluster ?? IORedis.default.Cluster;
159
+ return new ClusterCtor(config.nodes, {
160
+ redisOptions: {
161
+ password: config.password,
162
+ tls: config.tls ? {} : void 0
163
+ },
164
+ enableReadyCheck: true,
165
+ lazyConnect: true
166
+ });
167
+ }
168
+ var init_adapter_ioredis = __esm({
169
+ "src/shared/client/adapter-ioredis.ts"() {
170
+ }
171
+ });
172
+
173
+ // src/shared/abort.ts
174
+ var CacheTimeoutError = class extends Error {
175
+ constructor(opName, ms) {
176
+ super(`[@leejpsd/nextjs-cache-handler] ${opName} timed out after ${ms}ms`);
177
+ this.opName = opName;
178
+ this.ms = ms;
179
+ }
180
+ opName;
181
+ ms;
182
+ name = "CacheTimeoutError";
183
+ };
184
+ async function withAbortSignal(opName, ms, fn) {
185
+ const ctrl = new AbortController();
186
+ const timer = setTimeout(() => ctrl.abort(), ms);
187
+ if (typeof timer === "object" && "unref" in timer) {
188
+ timer.unref();
189
+ }
190
+ try {
191
+ const timeoutPromise = new Promise((_, reject) => {
192
+ const onAbort = () => {
193
+ ctrl.signal.removeEventListener("abort", onAbort);
194
+ reject(new CacheTimeoutError(opName, ms));
195
+ };
196
+ if (ctrl.signal.aborted) onAbort();
197
+ else ctrl.signal.addEventListener("abort", onAbort, { once: true });
198
+ });
199
+ return await Promise.race([fn(ctrl.signal), timeoutPromise]);
200
+ } finally {
201
+ clearTimeout(timer);
202
+ }
203
+ }
204
+
205
+ // src/shared/client/index.ts
206
+ async function buildClient(source) {
207
+ if (typeof source === "function") {
208
+ return await source();
209
+ }
210
+ switch (source.type) {
211
+ case "redis": {
212
+ const { adaptRedisV5: adaptRedisV52, createRedisV5Client: createRedisV5Client2 } = await Promise.resolve().then(() => (init_adapter_redis(), adapter_redis_exports));
213
+ return adaptRedisV52(createRedisV5Client2(source));
214
+ }
215
+ case "ioredis": {
216
+ const { adaptIoredis: adaptIoredis2, createIoredisClient: createIoredisClient2 } = await Promise.resolve().then(() => (init_adapter_ioredis(), adapter_ioredis_exports));
217
+ return adaptIoredis2(createIoredisClient2(source));
218
+ }
219
+ case "cluster": {
220
+ const { adaptCluster: adaptCluster2, createIoredisCluster: createIoredisCluster2 } = await Promise.resolve().then(() => (init_adapter_ioredis(), adapter_ioredis_exports));
221
+ return adaptCluster2(createIoredisCluster2(source));
222
+ }
223
+ default: {
224
+ const exhaustive = source;
225
+ throw new Error(
226
+ `[next-cache] unknown client config: ${JSON.stringify(exhaustive)}`
227
+ );
228
+ }
229
+ }
230
+ }
231
+ var ConnectionManager = class {
232
+ constructor(source, onError) {
233
+ this.source = source;
234
+ this.onError = onError;
235
+ }
236
+ source;
237
+ onError;
238
+ client = null;
239
+ connecting = null;
240
+ failed = false;
241
+ async getOrConnect() {
242
+ if (this.client?.isOpen) return this.client;
243
+ if (this.failed) return null;
244
+ if (this.connecting) return this.connecting;
245
+ this.connecting = (async () => {
246
+ try {
247
+ const c = await buildClient(this.source);
248
+ c.on("error", (err) => this.onError(err));
249
+ if (!c.isOpen) await c.connect();
250
+ this.client = c;
251
+ return c;
252
+ } catch (err) {
253
+ this.failed = true;
254
+ this.onError(err);
255
+ return null;
256
+ } finally {
257
+ this.connecting = null;
258
+ }
259
+ })();
260
+ return this.connecting;
261
+ }
262
+ reset() {
263
+ this.client = null;
264
+ this.connecting = null;
265
+ this.failed = false;
266
+ }
267
+ };
268
+
269
+ // src/shared/logger.ts
270
+ var defaultLogger = {
271
+ debug: () => {
272
+ },
273
+ info: () => {
274
+ },
275
+ warn: (message, meta) => {
276
+ if (meta) console.warn(`[next-cache] ${message}`, meta);
277
+ else console.warn(`[next-cache] ${message}`);
278
+ },
279
+ error: (message, meta) => {
280
+ if (meta) console.error(`[next-cache] ${message}`, meta);
281
+ else console.error(`[next-cache] ${message}`);
282
+ }
283
+ };
284
+
285
+ // src/shared/lua/set-with-tags.lua
286
+ var set_with_tags_default = `-- set-with-tags.lua
287
+ --
288
+ -- Atomically write a cache entry and update the tag indices that point at it.
289
+ -- Replaces the non-atomic SET \u2192 SADD \u2192 EXPIRE sequence in the reference
290
+ -- implementation, which had a race window where another instance's hard
291
+ -- invalidate could leave dangling tag members.
292
+ --
293
+ -- KEYS[1] : entry key (e.g. "next-cache:entry:abc123")
294
+ -- KEYS[2..N] : tag set keys (e.g. "next-cache:tag:posts")
295
+ -- ARGV[1] : serialized entry payload
296
+ -- ARGV[2] : entry TTL in seconds
297
+ -- ARGV[3] : tag TTL in seconds (typically same as entry TTL)
298
+ -- ARGV[4] : tag count (number of KEYS after KEYS[1])
299
+ --
300
+ -- Returns: 1 on success.
301
+
302
+ local entryKey = KEYS[1]
303
+ local payload = ARGV[1]
304
+ local entryTtl = tonumber(ARGV[2])
305
+ local tagTtl = tonumber(ARGV[3])
306
+ local tagCount = tonumber(ARGV[4])
307
+
308
+ redis.call('SET', entryKey, payload, 'EX', entryTtl)
309
+
310
+ for i = 1, tagCount do
311
+ local tagKey = KEYS[i + 1]
312
+ redis.call('SADD', tagKey, entryKey)
313
+ -- EXPIRE on every SADD: ensures the tag set never outlives its entries
314
+ -- and survives at least as long as the longest associated entry.
315
+ redis.call('EXPIRE', tagKey, tagTtl)
316
+ end
317
+
318
+ return 1
319
+ `;
320
+
321
+ // src/shared/lua/revalidate-hard.lua
322
+ var revalidate_hard_default = "-- revalidate-hard.lua\n--\n-- Atomically remove every entry referenced by a tag and stamp the tag's\n-- expiration marker. Used by `updateTags(tags, { expire: 0 })` \u2014 the hard\n-- invalidation path triggered by `updateTag()` server actions in Next.js 16.\n--\n-- KEYS[1] : tag set key (e.g. \"next-cache:tag:posts\")\n-- KEYS[2] : tag-expiration marker key (e.g. \"next-cache:tag-expiration:posts\")\n-- ARGV[1] : current timestamp in milliseconds\n-- ARGV[2] : marker TTL in seconds (default: 1 day so cross-deployment\n-- readers can still observe the invalidation)\n--\n-- Returns: number of entries that were deleted.\n\nlocal tagKey = KEYS[1]\nlocal markerKey = KEYS[2]\nlocal nowMs = ARGV[1]\nlocal markerTtl = tonumber(ARGV[2])\n\nlocal entries = redis.call('SMEMBERS', tagKey)\nlocal deleted = 0\n\nif #entries > 0 then\n -- DEL accepts varargs; unpack the entry list.\n redis.call('DEL', unpack(entries))\n deleted = #entries\nend\n\n-- Drop the tag set itself; on next SET it'll be recreated by set-with-tags.\nredis.call('DEL', tagKey)\n\n-- Stamp the invalidation timestamp. Readers compare this against an entry's\n-- timestamp inside `getExpiration` to decide whether soft-tag-only entries\n-- should be considered stale.\nredis.call('SET', markerKey, nowMs, 'EX', markerTtl)\n\nreturn deleted\n";
323
+
324
+ // src/shared/lua/refresh-tag-lock.lua
325
+ var refresh_tag_lock_default = `-- refresh-tag-lock.lua
326
+ --
327
+ -- Single-flight lock acquisition for background refresh.
328
+ -- Reserved for v0.2's stampede-protection feature. Currently unused by the
329
+ -- handler runtime, but bundled so v0.2 can ship without a major bump.
330
+ --
331
+ -- KEYS[1] : lock key (e.g. "next-cache:lock:abc123")
332
+ -- ARGV[1] : owner identifier (instance id; written for observability)
333
+ -- ARGV[2] : lock TTL in seconds (10s recommended \u2014 long enough for refresh
334
+ -- to complete, short enough to recover from a crashed owner)
335
+ --
336
+ -- Returns: 1 if lock acquired, 0 if another owner already holds it.
337
+
338
+ local lockKey = KEYS[1]
339
+ local owner = ARGV[1]
340
+ local ttl = tonumber(ARGV[2])
341
+
342
+ if redis.call('GET', lockKey) then
343
+ return 0
344
+ end
345
+
346
+ redis.call('SET', lockKey, owner, 'EX', ttl)
347
+ return 1
348
+ `;
349
+
350
+ // src/shared/lua/index.ts
351
+ var SCRIPTS = {
352
+ setWithTags: set_with_tags_default,
353
+ revalidateHard: revalidate_hard_default,
354
+ refreshTagLock: refresh_tag_lock_default
355
+ };
356
+ var shaCache = /* @__PURE__ */ new Map();
357
+ function sha1(body) {
358
+ return createHash("sha1").update(body).digest("hex");
359
+ }
360
+ function isNoScriptError(err) {
361
+ if (!err || typeof err !== "object") return false;
362
+ const msg = err.message ?? "";
363
+ return msg.includes("NOSCRIPT");
364
+ }
365
+ async function execLuaScript(client, name, keys, args) {
366
+ const body = SCRIPTS[name];
367
+ let cachedSha = shaCache.get(name);
368
+ if (cachedSha === void 0) {
369
+ cachedSha = sha1(body);
370
+ shaCache.set(name, cachedSha);
371
+ }
372
+ if (typeof client.evalSha === "function") {
373
+ try {
374
+ return await client.evalSha(cachedSha, {
375
+ keys,
376
+ arguments: args
377
+ });
378
+ } catch (err) {
379
+ if (!isNoScriptError(err)) {
380
+ throw err;
381
+ }
382
+ }
383
+ }
384
+ return await client.eval(body, { keys, arguments: args });
385
+ }
386
+
387
+ // src/shared/memory-fallback.ts
388
+ var MAX_TIMER_MS = 2147483647;
389
+ function clampTimerMs(ms) {
390
+ return Math.min(Math.max(0, ms), MAX_TIMER_MS);
391
+ }
392
+ var MemoryStore = class {
393
+ kv = /* @__PURE__ */ new Map();
394
+ timers = /* @__PURE__ */ new Map();
395
+ set(key, value, ttlSeconds) {
396
+ this.delete(key);
397
+ const expiresAt = Date.now() + ttlSeconds * 1e3;
398
+ this.kv.set(key, { value, expiresAt });
399
+ const timer = setTimeout(() => this.delete(key), clampTimerMs(ttlSeconds * 1e3));
400
+ if (typeof timer === "object" && "unref" in timer) {
401
+ timer.unref();
402
+ }
403
+ this.timers.set(key, timer);
404
+ }
405
+ get(key) {
406
+ const entry = this.kv.get(key);
407
+ if (!entry) return null;
408
+ if (Date.now() > entry.expiresAt) {
409
+ this.delete(key);
410
+ return null;
411
+ }
412
+ return entry.value;
413
+ }
414
+ delete(key) {
415
+ const timer = this.timers.get(key);
416
+ if (timer !== void 0) {
417
+ clearTimeout(timer);
418
+ this.timers.delete(key);
419
+ }
420
+ return this.kv.delete(key);
421
+ }
422
+ has(key) {
423
+ return this.get(key) !== null;
424
+ }
425
+ keys() {
426
+ const now = Date.now();
427
+ const live = [];
428
+ for (const [key, entry] of this.kv.entries()) {
429
+ if (entry.expiresAt >= now) live.push(key);
430
+ }
431
+ return live;
432
+ }
433
+ size() {
434
+ return this.keys().length;
435
+ }
436
+ clear() {
437
+ for (const timer of this.timers.values()) clearTimeout(timer);
438
+ this.timers.clear();
439
+ this.kv.clear();
440
+ }
441
+ };
442
+ var MemorySetStore = class {
443
+ sets = /* @__PURE__ */ new Map();
444
+ expiresAt = /* @__PURE__ */ new Map();
445
+ timers = /* @__PURE__ */ new Map();
446
+ sAdd(key, member) {
447
+ const members = Array.isArray(member) ? member : [member];
448
+ let set = this.sets.get(key);
449
+ if (!set) {
450
+ set = /* @__PURE__ */ new Set();
451
+ this.sets.set(key, set);
452
+ }
453
+ let added = 0;
454
+ for (const m of members) {
455
+ if (!set.has(m)) {
456
+ set.add(m);
457
+ added += 1;
458
+ }
459
+ }
460
+ return added;
461
+ }
462
+ sMembers(key) {
463
+ if (this.isExpired(key)) {
464
+ this.delete(key);
465
+ return [];
466
+ }
467
+ const set = this.sets.get(key);
468
+ return set ? Array.from(set) : [];
469
+ }
470
+ expire(key, ttlSeconds) {
471
+ if (!this.sets.has(key)) return false;
472
+ const prev = this.timers.get(key);
473
+ if (prev !== void 0) clearTimeout(prev);
474
+ const expiresAt = Date.now() + ttlSeconds * 1e3;
475
+ this.expiresAt.set(key, expiresAt);
476
+ const timer = setTimeout(() => this.delete(key), clampTimerMs(ttlSeconds * 1e3));
477
+ if (typeof timer === "object" && "unref" in timer) {
478
+ timer.unref();
479
+ }
480
+ this.timers.set(key, timer);
481
+ return true;
482
+ }
483
+ delete(key) {
484
+ const timer = this.timers.get(key);
485
+ if (timer !== void 0) {
486
+ clearTimeout(timer);
487
+ this.timers.delete(key);
488
+ }
489
+ this.expiresAt.delete(key);
490
+ return this.sets.delete(key);
491
+ }
492
+ keys() {
493
+ const live = [];
494
+ for (const key of this.sets.keys()) {
495
+ if (!this.isExpired(key)) live.push(key);
496
+ }
497
+ return live;
498
+ }
499
+ clear() {
500
+ for (const timer of this.timers.values()) clearTimeout(timer);
501
+ this.timers.clear();
502
+ this.expiresAt.clear();
503
+ this.sets.clear();
504
+ }
505
+ isExpired(key) {
506
+ const at = this.expiresAt.get(key);
507
+ return at !== void 0 && Date.now() > at;
508
+ }
509
+ };
510
+
511
+ // src/shared/metrics.ts
512
+ var counters = /* @__PURE__ */ new Map();
513
+ function bump(event) {
514
+ const existing = counters.get(event.type);
515
+ if (existing) {
516
+ existing.count += 1;
517
+ if (event.ms !== void 0) existing.totalMs += event.ms;
518
+ existing.lastTs = Date.now();
519
+ return;
520
+ }
521
+ counters.set(event.type, {
522
+ count: 1,
523
+ totalMs: event.ms ?? 0,
524
+ lastTs: Date.now()
525
+ });
526
+ }
527
+ function createEmitter(onMetric) {
528
+ return (event) => {
529
+ bump(event);
530
+ if (onMetric) {
531
+ try {
532
+ onMetric(event);
533
+ } catch {
534
+ }
535
+ }
536
+ };
537
+ }
538
+
539
+ // src/shared/namespace.ts
540
+ var FALLBACK = "unversioned";
541
+ function resolveBuildNamespace(override) {
542
+ if (override !== void 0) {
543
+ return typeof override === "function" ? override() : override;
544
+ }
545
+ return process.env.DEPLOYMENT_VERSION ?? process.env.GIT_HASH ?? process.env.NEXT_DEPLOYMENT_ID ?? FALLBACK;
546
+ }
547
+ function buildKey(prefix, namespace, rest, hashTag = false) {
548
+ const ns = hashTag ? `{${namespace}}` : namespace;
549
+ return `${prefix}${ns}:${rest}`;
550
+ }
551
+
552
+ // src/shared/build-phase.ts
553
+ var PHASE_PRODUCTION_BUILD = "phase-production-build";
554
+ function isBuildPhase() {
555
+ return process.env.NEXT_PHASE === PHASE_PRODUCTION_BUILD;
556
+ }
557
+ function shouldUseRedis(opts) {
558
+ if (opts.fallback === "always") return false;
559
+ const detector = opts.isBuildPhase ?? isBuildPhase;
560
+ if (detector()) return false;
561
+ return true;
562
+ }
563
+
564
+ // src/cache-components/serialize.ts
565
+ async function readStreamFully(stream) {
566
+ const reader = stream.getReader();
567
+ const chunks = [];
568
+ try {
569
+ while (true) {
570
+ const { done, value } = await reader.read();
571
+ if (done) break;
572
+ if (value) chunks.push(value);
573
+ }
574
+ } finally {
575
+ reader.releaseLock();
576
+ }
577
+ return Buffer.concat(chunks.map((c) => Buffer.from(c)));
578
+ }
579
+ function bufferToStream(buf) {
580
+ return new ReadableStream({
581
+ start(controller) {
582
+ controller.enqueue(new Uint8Array(buf));
583
+ controller.close();
584
+ }
585
+ });
586
+ }
587
+ function encodeEnvelope(buf, meta) {
588
+ const envelope = {
589
+ value: buf.toString("base64"),
590
+ ...meta
591
+ };
592
+ return JSON.stringify(envelope);
593
+ }
594
+ function decodeEnvelope(raw) {
595
+ try {
596
+ const parsed = JSON.parse(raw);
597
+ if (!parsed || typeof parsed !== "object" || typeof parsed.value !== "string") {
598
+ return null;
599
+ }
600
+ return parsed;
601
+ } catch {
602
+ return null;
603
+ }
604
+ }
605
+
606
+ // src/cache-components/swr.ts
607
+ var SECOND_MS = 1e3;
608
+ function partitionEntry(entry, now = Date.now()) {
609
+ const ageMs = now - entry.timestamp;
610
+ if (ageMs < 0) {
611
+ return { freshness: "fresh", ageMs };
612
+ }
613
+ const revalidateMs = Math.max(0, entry.revalidate) * SECOND_MS;
614
+ const rawExpireMs = entry.expire === 0 ? Number.POSITIVE_INFINITY : Math.max(0, entry.expire) * SECOND_MS;
615
+ const expireMs = Math.max(rawExpireMs, revalidateMs);
616
+ if (ageMs <= revalidateMs) {
617
+ return { freshness: "fresh", ageMs };
618
+ }
619
+ if (ageMs <= expireMs) {
620
+ return { freshness: "stale", ageMs };
621
+ }
622
+ return { freshness: "expired", ageMs };
623
+ }
624
+ function shouldServeStale(result, staleWhileRevalidate) {
625
+ return result.freshness === "fresh" || result.freshness === "stale" && staleWhileRevalidate;
626
+ }
627
+
628
+ // src/cache-components/handler.ts
629
+ var DEFAULT_KEY_PREFIX = "next-cache:";
630
+ var ENTRY_SUFFIX = "entry:";
631
+ var TAG_SUFFIX = "tag:";
632
+ var TAG_EXP_SUFFIX = "tag-expiration:";
633
+ var DEFAULT_ABORT_MS = 1500;
634
+ var ONE_YEAR_SEC = 60 * 60 * 24 * 365;
635
+ var FALLBACK_TTL_SEC = 3600;
636
+ var TAG_EXP_MARKER_TTL_SEC = 60 * 60 * 24 * 7;
637
+ function gateRedis(state) {
638
+ const fb = state.opts.fallback;
639
+ const ibp = state.opts.rest.isBuildPhase;
640
+ return ibp ? shouldUseRedis({ fallback: fb, isBuildPhase: ibp }) : shouldUseRedis({ fallback: fb });
641
+ }
642
+ function init(opts) {
643
+ const logger = opts.logger ?? defaultLogger;
644
+ const emit = createEmitter(opts.onMetric);
645
+ const conn = new ConnectionManager(opts.client, (err) => {
646
+ logger.warn("Redis client error", { message: err.message });
647
+ emit({ type: "redis.connect.failed", meta: { message: err.message } });
648
+ });
649
+ return {
650
+ opts: {
651
+ abortTimeoutMs: opts.abortTimeoutMs ?? DEFAULT_ABORT_MS,
652
+ fallback: opts.fallback ?? "auto",
653
+ staleWhileRevalidate: opts.staleWhileRevalidate ?? true,
654
+ hashTag: opts.hashTag ?? false,
655
+ keyPrefix: opts.keyPrefix ?? DEFAULT_KEY_PREFIX,
656
+ rest: opts
657
+ },
658
+ logger,
659
+ emit,
660
+ conn,
661
+ memEntries: new MemoryStore(),
662
+ memTags: new MemorySetStore(),
663
+ memTagExp: /* @__PURE__ */ new Map(),
664
+ localTagTimestamps: /* @__PURE__ */ new Map(),
665
+ resolveNs: () => resolveBuildNamespace(opts.buildNamespace)
666
+ };
667
+ }
668
+ function entryKey(state, cacheKey) {
669
+ return buildKey(
670
+ `${state.opts.keyPrefix}${ENTRY_SUFFIX}`,
671
+ state.resolveNs(),
672
+ cacheKey,
673
+ state.opts.hashTag
674
+ );
675
+ }
676
+ function tagKey(state, tag) {
677
+ return buildKey(
678
+ `${state.opts.keyPrefix}${TAG_SUFFIX}`,
679
+ state.resolveNs(),
680
+ tag,
681
+ state.opts.hashTag
682
+ );
683
+ }
684
+ function tagExpKey(state, tag) {
685
+ return buildKey(
686
+ `${state.opts.keyPrefix}${TAG_EXP_SUFFIX}`,
687
+ state.resolveNs(),
688
+ tag,
689
+ state.opts.hashTag
690
+ );
691
+ }
692
+ function resolveTtlSeconds(entry) {
693
+ if (Number.isFinite(entry.expire) && entry.expire > 0) {
694
+ return Math.max(60, Math.ceil(entry.expire));
695
+ }
696
+ if (Number.isFinite(entry.revalidate) && entry.revalidate > 0) {
697
+ return Math.max(60, Math.ceil(entry.revalidate));
698
+ }
699
+ if (entry.expire === Number.POSITIVE_INFINITY) {
700
+ return ONE_YEAR_SEC;
701
+ }
702
+ return FALLBACK_TTL_SEC;
703
+ }
704
+ async function getImpl(state, cacheKey, softTags) {
705
+ const start = Date.now();
706
+ const useRedis = gateRedis(state);
707
+ if (!useRedis && state.opts.fallback === "never") {
708
+ state.emit({ type: "cache.miss", meta: { reason: "no-redis" } });
709
+ return void 0;
710
+ }
711
+ const eKey = entryKey(state, cacheKey);
712
+ let envelope = null;
713
+ let raw = null;
714
+ if (useRedis) {
715
+ try {
716
+ raw = await withAbortSignal(
717
+ "cacheHandlers.get",
718
+ state.opts.abortTimeoutMs,
719
+ async () => {
720
+ const client = await state.conn.getOrConnect();
721
+ if (!client) return null;
722
+ return await client.get(eKey);
723
+ }
724
+ );
725
+ } catch (err) {
726
+ if (err instanceof CacheTimeoutError) {
727
+ state.emit({ type: "redis.timeout", meta: { op: "get" } });
728
+ } else {
729
+ state.logger.warn("get() Redis error, falling back", {
730
+ message: err.message
731
+ });
732
+ }
733
+ raw = null;
734
+ }
735
+ }
736
+ if (raw === null && state.opts.fallback !== "never") {
737
+ raw = state.memEntries.get(eKey);
738
+ if (raw !== null) state.emit({ type: "fallback.activated", meta: { op: "get" } });
739
+ }
740
+ if (raw === null) {
741
+ state.emit({ type: "cache.miss", ms: Date.now() - start });
742
+ return void 0;
743
+ }
744
+ envelope = decodeEnvelope(raw);
745
+ if (!envelope) {
746
+ state.emit({ type: "cache.miss", meta: { reason: "decode-error" } });
747
+ return void 0;
748
+ }
749
+ const partition = partitionEntry(envelope, Date.now());
750
+ if (!shouldServeStale(partition, state.opts.staleWhileRevalidate)) {
751
+ if (useRedis) {
752
+ try {
753
+ await withAbortSignal(
754
+ "cacheHandlers.get.evict",
755
+ state.opts.abortTimeoutMs,
756
+ async () => {
757
+ const c = await state.conn.getOrConnect();
758
+ if (c) await c.del(eKey);
759
+ }
760
+ );
761
+ } catch {
762
+ }
763
+ } else {
764
+ state.memEntries.delete(eKey);
765
+ }
766
+ state.emit({
767
+ type: "cache.miss",
768
+ meta: { reason: "expired", freshness: partition.freshness }
769
+ });
770
+ return void 0;
771
+ }
772
+ if (softTags.length > 0) {
773
+ let mostRecent = 0;
774
+ for (const t of softTags) {
775
+ const ts = state.localTagTimestamps.get(t);
776
+ if (ts !== void 0 && ts > mostRecent) mostRecent = ts;
777
+ }
778
+ if (mostRecent > envelope.timestamp) {
779
+ state.emit({
780
+ type: "cache.miss",
781
+ meta: { reason: "soft-tag-invalidated" }
782
+ });
783
+ return void 0;
784
+ }
785
+ }
786
+ state.emit({
787
+ type: partition.freshness === "stale" ? "cache.stale" : "cache.hit",
788
+ ms: Date.now() - start,
789
+ meta: { freshness: partition.freshness }
790
+ });
791
+ return {
792
+ value: bufferToStream(Buffer.from(envelope.value, "base64")),
793
+ tags: envelope.tags,
794
+ stale: envelope.stale,
795
+ timestamp: envelope.timestamp,
796
+ expire: envelope.expire,
797
+ revalidate: envelope.revalidate
798
+ };
799
+ }
800
+ async function setImpl(state, cacheKey, pendingEntry) {
801
+ let entry;
802
+ try {
803
+ entry = await pendingEntry;
804
+ } catch (err) {
805
+ state.logger.warn("pendingEntry rejected, skipping set()", {
806
+ message: err.message
807
+ });
808
+ state.emit({ type: "cache.set.failed", meta: { reason: "pending-rejected" } });
809
+ return;
810
+ }
811
+ let buf;
812
+ try {
813
+ buf = await readStreamFully(entry.value);
814
+ } catch (err) {
815
+ state.logger.warn("stream errored mid-read, discarding entry", {
816
+ message: err.message
817
+ });
818
+ state.emit({ type: "cache.set.failed", meta: { reason: "partial-stream" } });
819
+ return;
820
+ }
821
+ const eKey = entryKey(state, cacheKey);
822
+ const ttl = resolveTtlSeconds(entry);
823
+ const payload = encodeEnvelope(buf, {
824
+ tags: entry.tags,
825
+ stale: entry.stale,
826
+ timestamp: entry.timestamp,
827
+ expire: entry.expire,
828
+ revalidate: entry.revalidate
829
+ });
830
+ const useRedis = gateRedis(state);
831
+ if (!useRedis) {
832
+ state.emit({ type: "build_phase.skip", meta: { op: "set" } });
833
+ if (state.opts.fallback !== "never") {
834
+ state.memEntries.set(eKey, payload, ttl);
835
+ for (const t of entry.tags) {
836
+ state.memTags.sAdd(tagKey(state, t), eKey);
837
+ state.memTags.expire(tagKey(state, t), ttl);
838
+ }
839
+ }
840
+ state.emit({ type: "cache.set", meta: { backend: "memory" } });
841
+ return;
842
+ }
843
+ try {
844
+ await withAbortSignal(
845
+ "cacheHandlers.set",
846
+ state.opts.abortTimeoutMs,
847
+ async () => {
848
+ const client = await state.conn.getOrConnect();
849
+ if (!client) {
850
+ if (state.opts.fallback !== "never") {
851
+ state.memEntries.set(eKey, payload, ttl);
852
+ }
853
+ return;
854
+ }
855
+ if (entry.tags.length === 0) {
856
+ await client.set(eKey, payload, { EX: ttl });
857
+ } else {
858
+ await luaSetWithTags(client, state, eKey, payload, ttl, entry.tags);
859
+ }
860
+ }
861
+ );
862
+ state.emit({ type: "cache.set", meta: { backend: "redis" } });
863
+ } catch (err) {
864
+ if (err instanceof CacheTimeoutError) {
865
+ state.emit({ type: "redis.timeout", meta: { op: "set" } });
866
+ } else {
867
+ state.logger.warn("set() failed (best-effort)", {
868
+ message: err.message
869
+ });
870
+ }
871
+ state.emit({ type: "cache.set.failed", meta: { reason: "redis-error" } });
872
+ }
873
+ }
874
+ async function luaSetWithTags(client, state, eKey, payload, ttl, tags) {
875
+ const tagKeys = tags.map((t) => tagKey(state, t));
876
+ await execLuaScript(
877
+ client,
878
+ "setWithTags",
879
+ [eKey, ...tagKeys],
880
+ [payload, String(ttl), String(ttl), String(tags.length)]
881
+ );
882
+ }
883
+ async function refreshTagsImpl(state) {
884
+ if (!gateRedis(state)) {
885
+ return;
886
+ }
887
+ try {
888
+ await withAbortSignal(
889
+ "cacheHandlers.refreshTags",
890
+ state.opts.abortTimeoutMs,
891
+ async () => {
892
+ const client = await state.conn.getOrConnect();
893
+ if (!client) return;
894
+ const pattern = `${state.opts.keyPrefix}${TAG_EXP_SUFFIX}*`;
895
+ const seen = [];
896
+ for await (const chunk of client.scanIterator({
897
+ MATCH: pattern,
898
+ COUNT: 100
899
+ })) {
900
+ const keys = Array.isArray(chunk) ? chunk.map(String) : [String(chunk)];
901
+ seen.push(...keys);
902
+ if (seen.length >= 500) break;
903
+ }
904
+ if (seen.length === 0) return;
905
+ const values = await client.mGet(seen);
906
+ for (let i = 0; i < seen.length; i += 1) {
907
+ const v = values[i];
908
+ if (!v) continue;
909
+ const ts = Number.parseInt(v, 10);
910
+ if (!Number.isFinite(ts)) continue;
911
+ const key = seen[i];
912
+ if (key === void 0) continue;
913
+ const tag = stripExpPrefix(state, key);
914
+ if (tag) state.localTagTimestamps.set(tag, ts);
915
+ }
916
+ }
917
+ );
918
+ } catch (err) {
919
+ state.logger.warn("refreshTags() error swallowed", {
920
+ message: err.message
921
+ });
922
+ }
923
+ }
924
+ function stripExpPrefix(state, fullKey) {
925
+ const ns = state.resolveNs();
926
+ const prefix = state.opts.hashTag ? `${state.opts.keyPrefix}${TAG_EXP_SUFFIX}{${ns}}:` : `${state.opts.keyPrefix}${TAG_EXP_SUFFIX}${ns}:`;
927
+ return fullKey.startsWith(prefix) ? fullKey.slice(prefix.length) : null;
928
+ }
929
+ async function getExpirationImpl(state, tags) {
930
+ if (tags.length === 0) return 0;
931
+ let max = 0;
932
+ for (const t of tags) {
933
+ const local = state.localTagTimestamps.get(t);
934
+ if (local !== void 0 && local > max) max = local;
935
+ const mem = state.memTagExp.get(t);
936
+ if (mem !== void 0 && mem > max) max = mem;
937
+ }
938
+ if (max > 0) {
939
+ state.emit({ type: "tag.expiration.read", meta: { source: "local", count: tags.length } });
940
+ return max;
941
+ }
942
+ if (!gateRedis(state)) {
943
+ return 0;
944
+ }
945
+ try {
946
+ return await withAbortSignal(
947
+ "cacheHandlers.getExpiration",
948
+ state.opts.abortTimeoutMs,
949
+ async () => {
950
+ const client = await state.conn.getOrConnect();
951
+ if (!client) return 0;
952
+ const keys = tags.map((t) => tagExpKey(state, t));
953
+ const values = await client.mGet(keys);
954
+ let m = 0;
955
+ for (const v of values) {
956
+ if (!v) continue;
957
+ const n = Number.parseInt(v, 10);
958
+ if (Number.isFinite(n) && n > m) m = n;
959
+ }
960
+ return m;
961
+ }
962
+ );
963
+ } catch {
964
+ return 0;
965
+ }
966
+ }
967
+ async function updateTagsImpl(state, tags, durations) {
968
+ if (tags.length === 0) return;
969
+ const isHardExpire = durations?.expire === 0;
970
+ const now = Date.now();
971
+ for (const t of tags) {
972
+ state.localTagTimestamps.set(t, now);
973
+ state.memTagExp.set(t, now);
974
+ }
975
+ if (!gateRedis(state)) {
976
+ if (isHardExpire) {
977
+ for (const t of tags) {
978
+ const tk = tagKey(state, t);
979
+ const members = state.memTags.sMembers(tk);
980
+ for (const m of members) state.memEntries.delete(m);
981
+ state.memTags.delete(tk);
982
+ }
983
+ }
984
+ state.emit({
985
+ type: isHardExpire ? "tag.invalidate.hard" : "tag.invalidate.soft",
986
+ meta: { count: tags.length, backend: "memory" }
987
+ });
988
+ return;
989
+ }
990
+ try {
991
+ await withAbortSignal(
992
+ "cacheHandlers.updateTags",
993
+ state.opts.abortTimeoutMs,
994
+ async () => {
995
+ const client = await state.conn.getOrConnect();
996
+ if (!client) {
997
+ if (isHardExpire) {
998
+ throw new Error(
999
+ "[next-cache] hard updateTags requested but Redis unavailable"
1000
+ );
1001
+ }
1002
+ return;
1003
+ }
1004
+ for (const t of tags) {
1005
+ if (isHardExpire) {
1006
+ await execLuaScript(
1007
+ client,
1008
+ "revalidateHard",
1009
+ [tagKey(state, t), tagExpKey(state, t)],
1010
+ [String(now), String(TAG_EXP_MARKER_TTL_SEC)]
1011
+ );
1012
+ } else {
1013
+ await client.set(tagExpKey(state, t), String(now), {
1014
+ EX: TAG_EXP_MARKER_TTL_SEC
1015
+ });
1016
+ }
1017
+ }
1018
+ }
1019
+ );
1020
+ state.emit({
1021
+ type: isHardExpire ? "tag.invalidate.hard" : "tag.invalidate.soft",
1022
+ meta: { count: tags.length, backend: "redis" }
1023
+ });
1024
+ } catch (err) {
1025
+ if (err instanceof CacheTimeoutError) {
1026
+ state.emit({ type: "redis.timeout", meta: { op: "updateTags" } });
1027
+ }
1028
+ if (isHardExpire) {
1029
+ throw err;
1030
+ }
1031
+ state.logger.warn("updateTags() soft path failed (best-effort)", {
1032
+ message: err.message
1033
+ });
1034
+ }
1035
+ }
1036
+ function createCacheComponentsHandler(opts) {
1037
+ const state = init(opts);
1038
+ return {
1039
+ get: (cacheKey, softTags) => getImpl(state, cacheKey, softTags),
1040
+ set: (cacheKey, pendingEntry) => setImpl(state, cacheKey, pendingEntry),
1041
+ refreshTags: () => refreshTagsImpl(state),
1042
+ getExpiration: (tags) => getExpirationImpl(state, tags),
1043
+ updateTags: (tags, durations) => updateTagsImpl(state, tags, durations)
1044
+ };
1045
+ }
1046
+
1047
+ export { createCacheComponentsHandler, partitionEntry, shouldServeStale };
1048
+ //# sourceMappingURL=index.js.map
1049
+ //# sourceMappingURL=index.js.map