@leejpsd/nextjs-cache-handler 0.1.1 → 0.3.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/CHANGELOG.md +117 -2
- package/README.md +131 -30
- package/dist/cache-components/index.cjs +297 -52
- package/dist/cache-components/index.cjs.map +1 -1
- package/dist/cache-components/index.d.cts +1 -1
- package/dist/cache-components/index.d.ts +1 -1
- package/dist/cache-components/index.js +289 -52
- package/dist/cache-components/index.js.map +1 -1
- package/dist/incremental/index.cjs +231 -41
- package/dist/incremental/index.cjs.map +1 -1
- package/dist/incremental/index.d.cts +42 -3
- package/dist/incremental/index.d.ts +42 -3
- package/dist/incremental/index.js +223 -41
- package/dist/incremental/index.js.map +1 -1
- package/dist/index.cjs +375 -76
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +367 -76
- package/dist/index.js.map +1 -1
- package/dist/ops/index.d.cts +1 -1
- package/dist/ops/index.d.ts +1 -1
- package/dist/otel/index.cjs +72 -0
- package/dist/otel/index.cjs.map +1 -0
- package/dist/otel/index.d.cts +72 -0
- package/dist/otel/index.d.ts +72 -0
- package/dist/otel/index.js +64 -0
- package/dist/otel/index.js.map +1 -0
- package/dist/shared/client/adapter-ioredis.cjs +49 -10
- package/dist/shared/client/adapter-ioredis.cjs.map +1 -1
- package/dist/shared/client/adapter-ioredis.d.cts +5 -2
- package/dist/shared/client/adapter-ioredis.d.ts +5 -2
- package/dist/shared/client/adapter-ioredis.js +43 -11
- package/dist/shared/client/adapter-ioredis.js.map +1 -1
- package/dist/shared/client/adapter-redis.cjs +22 -7
- package/dist/shared/client/adapter-redis.cjs.map +1 -1
- package/dist/shared/client/adapter-redis.d.cts +1 -1
- package/dist/shared/client/adapter-redis.d.ts +1 -1
- package/dist/shared/client/adapter-redis.js +16 -7
- package/dist/shared/client/adapter-redis.js.map +1 -1
- package/dist/{types-CgriT7My.d.cts → types-Bl-rkLKX.d.cts} +59 -1
- package/dist/{types-CgriT7My.d.ts → types-Bl-rkLKX.d.ts} +59 -1
- package/package.json +23 -4
|
@@ -1,13 +1,12 @@
|
|
|
1
|
+
import path2 from 'path';
|
|
2
|
+
import 'url';
|
|
3
|
+
import { createRequire } from 'module';
|
|
4
|
+
import { promisify } from 'util';
|
|
5
|
+
import { gzip, gunzip, brotliCompress, brotliDecompress, constants } from 'zlib';
|
|
1
6
|
import { createHash } from 'crypto';
|
|
2
7
|
|
|
3
8
|
var __defProp = Object.defineProperty;
|
|
4
9
|
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
10
|
var __esm = (fn, res) => function __init() {
|
|
12
11
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
13
12
|
};
|
|
@@ -15,6 +14,27 @@ var __export = (target, all) => {
|
|
|
15
14
|
for (var name in all)
|
|
16
15
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
17
16
|
};
|
|
17
|
+
var init_esm_shims = __esm({
|
|
18
|
+
"node_modules/tsup/assets/esm_shims.js"() {
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
function nodeRequire(id) {
|
|
22
|
+
try {
|
|
23
|
+
return packageRequire(id);
|
|
24
|
+
} catch {
|
|
25
|
+
return cwdRequire(id);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
var packageRequire, cwdRequire;
|
|
29
|
+
var init_node_require = __esm({
|
|
30
|
+
"src/shared/node-require.ts"() {
|
|
31
|
+
init_esm_shims();
|
|
32
|
+
packageRequire = createRequire(import.meta.url);
|
|
33
|
+
cwdRequire = createRequire(
|
|
34
|
+
path2.join(process.cwd(), "__resolve-anchor__.js")
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
18
38
|
|
|
19
39
|
// src/shared/client/adapter-redis.ts
|
|
20
40
|
var adapter_redis_exports = {};
|
|
@@ -26,7 +46,7 @@ function adaptRedisV5(client) {
|
|
|
26
46
|
return client;
|
|
27
47
|
}
|
|
28
48
|
function createRedisV5Client(config) {
|
|
29
|
-
const { createClient } =
|
|
49
|
+
const { createClient } = nodeRequire("redis");
|
|
30
50
|
const useTls = config.tls ?? config.url.startsWith("rediss://");
|
|
31
51
|
const opts = {
|
|
32
52
|
url: config.url,
|
|
@@ -41,6 +61,8 @@ function createRedisV5Client(config) {
|
|
|
41
61
|
}
|
|
42
62
|
var init_adapter_redis = __esm({
|
|
43
63
|
"src/shared/client/adapter-redis.ts"() {
|
|
64
|
+
init_esm_shims();
|
|
65
|
+
init_node_require();
|
|
44
66
|
}
|
|
45
67
|
});
|
|
46
68
|
|
|
@@ -50,7 +72,8 @@ __export(adapter_ioredis_exports, {
|
|
|
50
72
|
adaptCluster: () => adaptCluster,
|
|
51
73
|
adaptIoredis: () => adaptIoredis,
|
|
52
74
|
createIoredisClient: () => createIoredisClient,
|
|
53
|
-
createIoredisCluster: () => createIoredisCluster
|
|
75
|
+
createIoredisCluster: () => createIoredisCluster,
|
|
76
|
+
createIoredisSentinel: () => createIoredisSentinel
|
|
54
77
|
});
|
|
55
78
|
function adapt(client, isClusterClient = false) {
|
|
56
79
|
return {
|
|
@@ -133,6 +156,9 @@ function adapt(client, isClusterClient = false) {
|
|
|
133
156
|
on: (event, listener) => {
|
|
134
157
|
client.on(event, listener);
|
|
135
158
|
return client;
|
|
159
|
+
},
|
|
160
|
+
dispose: () => {
|
|
161
|
+
client.disconnect();
|
|
136
162
|
}
|
|
137
163
|
};
|
|
138
164
|
}
|
|
@@ -143,18 +169,36 @@ function adaptCluster(cluster) {
|
|
|
143
169
|
return adapt(cluster, true);
|
|
144
170
|
}
|
|
145
171
|
function createIoredisClient(config) {
|
|
146
|
-
const IORedis =
|
|
172
|
+
const IORedis = nodeRequire("ioredis");
|
|
147
173
|
const Ctor = IORedis.default ?? IORedis;
|
|
148
174
|
return new Ctor(config.url, {
|
|
149
175
|
password: config.password,
|
|
150
176
|
connectTimeout: config.connectTimeout ?? 1500,
|
|
151
177
|
tls: config.tls ? {} : void 0,
|
|
152
178
|
lazyConnect: true,
|
|
153
|
-
maxRetriesPerRequest: 1
|
|
179
|
+
maxRetriesPerRequest: 1,
|
|
180
|
+
// Batch same-tick commands into one RTT (matches redis@5's default
|
|
181
|
+
// behavior) — makes Promise.all fan-outs a single pipeline.
|
|
182
|
+
enableAutoPipelining: true
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
function createIoredisSentinel(config) {
|
|
186
|
+
const IORedis = nodeRequire("ioredis");
|
|
187
|
+
const Ctor = IORedis.default ?? IORedis;
|
|
188
|
+
return new Ctor({
|
|
189
|
+
sentinels: config.sentinels,
|
|
190
|
+
name: config.name,
|
|
191
|
+
password: config.password,
|
|
192
|
+
sentinelPassword: config.sentinelPassword,
|
|
193
|
+
connectTimeout: config.connectTimeout ?? 1500,
|
|
194
|
+
tls: config.tls ? {} : void 0,
|
|
195
|
+
lazyConnect: true,
|
|
196
|
+
maxRetriesPerRequest: 1,
|
|
197
|
+
enableAutoPipelining: true
|
|
154
198
|
});
|
|
155
199
|
}
|
|
156
200
|
function createIoredisCluster(config) {
|
|
157
|
-
const IORedis =
|
|
201
|
+
const IORedis = nodeRequire("ioredis");
|
|
158
202
|
const ClusterCtor = IORedis.Cluster ?? IORedis.default.Cluster;
|
|
159
203
|
return new ClusterCtor(config.nodes, {
|
|
160
204
|
redisOptions: {
|
|
@@ -162,15 +206,26 @@ function createIoredisCluster(config) {
|
|
|
162
206
|
tls: config.tls ? {} : void 0
|
|
163
207
|
},
|
|
164
208
|
enableReadyCheck: true,
|
|
165
|
-
lazyConnect: true
|
|
209
|
+
lazyConnect: true,
|
|
210
|
+
// Same-slot commands issued in the same tick share one pipeline.
|
|
211
|
+
enableAutoPipelining: true
|
|
166
212
|
});
|
|
167
213
|
}
|
|
168
214
|
var init_adapter_ioredis = __esm({
|
|
169
215
|
"src/shared/client/adapter-ioredis.ts"() {
|
|
216
|
+
init_esm_shims();
|
|
217
|
+
init_node_require();
|
|
170
218
|
}
|
|
171
219
|
});
|
|
172
220
|
|
|
221
|
+
// src/cache-components/index.ts
|
|
222
|
+
init_esm_shims();
|
|
223
|
+
|
|
224
|
+
// src/cache-components/handler.ts
|
|
225
|
+
init_esm_shims();
|
|
226
|
+
|
|
173
227
|
// src/shared/abort.ts
|
|
228
|
+
init_esm_shims();
|
|
174
229
|
var CacheTimeoutError = class extends Error {
|
|
175
230
|
constructor(opName, ms) {
|
|
176
231
|
super(`[@leejpsd/nextjs-cache-handler] ${opName} timed out after ${ms}ms`);
|
|
@@ -202,7 +257,41 @@ async function withAbortSignal(opName, ms, fn) {
|
|
|
202
257
|
}
|
|
203
258
|
}
|
|
204
259
|
|
|
260
|
+
// src/shared/compress.ts
|
|
261
|
+
init_esm_shims();
|
|
262
|
+
var gzipAsync = promisify(gzip);
|
|
263
|
+
var gunzipAsync = promisify(gunzip);
|
|
264
|
+
var brotliCompressAsync = promisify(brotliCompress);
|
|
265
|
+
var brotliDecompressAsync = promisify(brotliDecompress);
|
|
266
|
+
var GZIP_MARKER = "__ncgz__:";
|
|
267
|
+
var BROTLI_MARKER = "__ncbr__:";
|
|
268
|
+
var MIN_COMPRESS_BYTES = 1024;
|
|
269
|
+
var BROTLI_OPTS = {
|
|
270
|
+
params: { [constants.BROTLI_PARAM_QUALITY]: 5 }
|
|
271
|
+
};
|
|
272
|
+
async function compressValue(value, algo) {
|
|
273
|
+
if (!algo || Buffer.byteLength(value) < MIN_COMPRESS_BYTES) return value;
|
|
274
|
+
const input = Buffer.from(value);
|
|
275
|
+
if (algo === "gzip") {
|
|
276
|
+
return GZIP_MARKER + (await gzipAsync(input)).toString("base64");
|
|
277
|
+
}
|
|
278
|
+
return BROTLI_MARKER + (await brotliCompressAsync(input, BROTLI_OPTS)).toString("base64");
|
|
279
|
+
}
|
|
280
|
+
async function decompressValue(value) {
|
|
281
|
+
if (value === null) return null;
|
|
282
|
+
if (value.startsWith(GZIP_MARKER)) {
|
|
283
|
+
const buf = Buffer.from(value.slice(GZIP_MARKER.length), "base64");
|
|
284
|
+
return (await gunzipAsync(buf)).toString("utf8");
|
|
285
|
+
}
|
|
286
|
+
if (value.startsWith(BROTLI_MARKER)) {
|
|
287
|
+
const buf = Buffer.from(value.slice(BROTLI_MARKER.length), "base64");
|
|
288
|
+
return (await brotliDecompressAsync(buf)).toString("utf8");
|
|
289
|
+
}
|
|
290
|
+
return value;
|
|
291
|
+
}
|
|
292
|
+
|
|
205
293
|
// src/shared/client/index.ts
|
|
294
|
+
init_esm_shims();
|
|
206
295
|
async function buildClient(source) {
|
|
207
296
|
if (typeof source === "function") {
|
|
208
297
|
return await source();
|
|
@@ -220,6 +309,10 @@ async function buildClient(source) {
|
|
|
220
309
|
const { adaptCluster: adaptCluster2, createIoredisCluster: createIoredisCluster2 } = await Promise.resolve().then(() => (init_adapter_ioredis(), adapter_ioredis_exports));
|
|
221
310
|
return adaptCluster2(createIoredisCluster2(source));
|
|
222
311
|
}
|
|
312
|
+
case "sentinel": {
|
|
313
|
+
const { adaptIoredis: adaptIoredis2, createIoredisSentinel: createIoredisSentinel2 } = await Promise.resolve().then(() => (init_adapter_ioredis(), adapter_ioredis_exports));
|
|
314
|
+
return adaptIoredis2(createIoredisSentinel2(source));
|
|
315
|
+
}
|
|
223
316
|
default: {
|
|
224
317
|
const exhaustive = source;
|
|
225
318
|
throw new Error(
|
|
@@ -229,28 +322,45 @@ async function buildClient(source) {
|
|
|
229
322
|
}
|
|
230
323
|
}
|
|
231
324
|
var ConnectionManager = class {
|
|
232
|
-
constructor(source, onError) {
|
|
325
|
+
constructor(source, onError, retry = {}) {
|
|
233
326
|
this.source = source;
|
|
234
327
|
this.onError = onError;
|
|
328
|
+
this.baseCooldownMs = retry.baseCooldownMs ?? 1e3;
|
|
329
|
+
this.maxCooldownMs = retry.maxCooldownMs ?? 3e4;
|
|
235
330
|
}
|
|
236
331
|
source;
|
|
237
332
|
onError;
|
|
238
333
|
client = null;
|
|
239
334
|
connecting = null;
|
|
240
|
-
|
|
335
|
+
consecutiveFailures = 0;
|
|
336
|
+
lastFailureAt = 0;
|
|
337
|
+
baseCooldownMs;
|
|
338
|
+
maxCooldownMs;
|
|
339
|
+
cooldownMs() {
|
|
340
|
+
if (this.consecutiveFailures === 0) return 0;
|
|
341
|
+
return Math.min(
|
|
342
|
+
this.maxCooldownMs,
|
|
343
|
+
this.baseCooldownMs * 2 ** (this.consecutiveFailures - 1)
|
|
344
|
+
);
|
|
345
|
+
}
|
|
241
346
|
async getOrConnect() {
|
|
242
347
|
if (this.client?.isOpen) return this.client;
|
|
243
|
-
if (this.failed) return null;
|
|
244
348
|
if (this.connecting) return this.connecting;
|
|
349
|
+
if (Date.now() - this.lastFailureAt < this.cooldownMs()) return null;
|
|
350
|
+
const stale = this.client;
|
|
351
|
+
this.client = null;
|
|
245
352
|
this.connecting = (async () => {
|
|
353
|
+
disposeQuietly(stale);
|
|
246
354
|
try {
|
|
247
355
|
const c = await buildClient(this.source);
|
|
248
356
|
c.on("error", (err) => this.onError(err));
|
|
249
357
|
if (!c.isOpen) await c.connect();
|
|
250
358
|
this.client = c;
|
|
359
|
+
this.consecutiveFailures = 0;
|
|
251
360
|
return c;
|
|
252
361
|
} catch (err) {
|
|
253
|
-
this.
|
|
362
|
+
this.consecutiveFailures += 1;
|
|
363
|
+
this.lastFailureAt = Date.now();
|
|
254
364
|
this.onError(err);
|
|
255
365
|
return null;
|
|
256
366
|
} finally {
|
|
@@ -260,13 +370,24 @@ var ConnectionManager = class {
|
|
|
260
370
|
return this.connecting;
|
|
261
371
|
}
|
|
262
372
|
reset() {
|
|
373
|
+
disposeQuietly(this.client);
|
|
263
374
|
this.client = null;
|
|
264
375
|
this.connecting = null;
|
|
265
|
-
this.
|
|
376
|
+
this.consecutiveFailures = 0;
|
|
377
|
+
this.lastFailureAt = 0;
|
|
266
378
|
}
|
|
267
379
|
};
|
|
380
|
+
function disposeQuietly(client) {
|
|
381
|
+
if (!client) return;
|
|
382
|
+
const c = client;
|
|
383
|
+
try {
|
|
384
|
+
(c.dispose ?? c.destroy ?? c.disconnect)?.call(c);
|
|
385
|
+
} catch {
|
|
386
|
+
}
|
|
387
|
+
}
|
|
268
388
|
|
|
269
389
|
// src/shared/logger.ts
|
|
390
|
+
init_esm_shims();
|
|
270
391
|
var defaultLogger = {
|
|
271
392
|
debug: () => {
|
|
272
393
|
},
|
|
@@ -282,6 +403,9 @@ var defaultLogger = {
|
|
|
282
403
|
}
|
|
283
404
|
};
|
|
284
405
|
|
|
406
|
+
// src/shared/lua/index.ts
|
|
407
|
+
init_esm_shims();
|
|
408
|
+
|
|
285
409
|
// src/shared/lua/set-with-tags.lua
|
|
286
410
|
var set_with_tags_default = `-- set-with-tags.lua
|
|
287
411
|
--
|
|
@@ -385,13 +509,19 @@ async function execLuaScript(client, name, keys, args) {
|
|
|
385
509
|
}
|
|
386
510
|
|
|
387
511
|
// src/shared/memory-fallback.ts
|
|
512
|
+
init_esm_shims();
|
|
388
513
|
var MAX_TIMER_MS = 2147483647;
|
|
389
514
|
function clampTimerMs(ms) {
|
|
390
515
|
return Math.min(Math.max(0, ms), MAX_TIMER_MS);
|
|
391
516
|
}
|
|
517
|
+
var DEFAULT_MAX_ENTRIES = 1e3;
|
|
392
518
|
var MemoryStore = class {
|
|
393
519
|
kv = /* @__PURE__ */ new Map();
|
|
394
520
|
timers = /* @__PURE__ */ new Map();
|
|
521
|
+
maxEntries;
|
|
522
|
+
constructor(maxEntries = DEFAULT_MAX_ENTRIES) {
|
|
523
|
+
this.maxEntries = Math.max(1, maxEntries);
|
|
524
|
+
}
|
|
395
525
|
set(key, value, ttlSeconds) {
|
|
396
526
|
this.delete(key);
|
|
397
527
|
const expiresAt = Date.now() + ttlSeconds * 1e3;
|
|
@@ -401,6 +531,10 @@ var MemoryStore = class {
|
|
|
401
531
|
timer.unref();
|
|
402
532
|
}
|
|
403
533
|
this.timers.set(key, timer);
|
|
534
|
+
while (this.kv.size > this.maxEntries) {
|
|
535
|
+
const oldest = this.kv.keys().next().value;
|
|
536
|
+
this.delete(oldest);
|
|
537
|
+
}
|
|
404
538
|
}
|
|
405
539
|
get(key) {
|
|
406
540
|
const entry = this.kv.get(key);
|
|
@@ -409,6 +543,8 @@ var MemoryStore = class {
|
|
|
409
543
|
this.delete(key);
|
|
410
544
|
return null;
|
|
411
545
|
}
|
|
546
|
+
this.kv.delete(key);
|
|
547
|
+
this.kv.set(key, entry);
|
|
412
548
|
return entry.value;
|
|
413
549
|
}
|
|
414
550
|
delete(key) {
|
|
@@ -443,13 +579,19 @@ var MemorySetStore = class {
|
|
|
443
579
|
sets = /* @__PURE__ */ new Map();
|
|
444
580
|
expiresAt = /* @__PURE__ */ new Map();
|
|
445
581
|
timers = /* @__PURE__ */ new Map();
|
|
582
|
+
maxSets;
|
|
583
|
+
constructor(maxSets = DEFAULT_MAX_ENTRIES) {
|
|
584
|
+
this.maxSets = Math.max(1, maxSets);
|
|
585
|
+
}
|
|
446
586
|
sAdd(key, member) {
|
|
447
587
|
const members = Array.isArray(member) ? member : [member];
|
|
448
588
|
let set = this.sets.get(key);
|
|
449
589
|
if (!set) {
|
|
450
590
|
set = /* @__PURE__ */ new Set();
|
|
451
|
-
|
|
591
|
+
} else {
|
|
592
|
+
this.sets.delete(key);
|
|
452
593
|
}
|
|
594
|
+
this.sets.set(key, set);
|
|
453
595
|
let added = 0;
|
|
454
596
|
for (const m of members) {
|
|
455
597
|
if (!set.has(m)) {
|
|
@@ -457,6 +599,10 @@ var MemorySetStore = class {
|
|
|
457
599
|
added += 1;
|
|
458
600
|
}
|
|
459
601
|
}
|
|
602
|
+
while (this.sets.size > this.maxSets) {
|
|
603
|
+
const oldest = this.sets.keys().next().value;
|
|
604
|
+
this.delete(oldest);
|
|
605
|
+
}
|
|
460
606
|
return added;
|
|
461
607
|
}
|
|
462
608
|
sMembers(key) {
|
|
@@ -509,6 +655,7 @@ var MemorySetStore = class {
|
|
|
509
655
|
};
|
|
510
656
|
|
|
511
657
|
// src/shared/metrics.ts
|
|
658
|
+
init_esm_shims();
|
|
512
659
|
var counters = /* @__PURE__ */ new Map();
|
|
513
660
|
function bump(event) {
|
|
514
661
|
const existing = counters.get(event.type);
|
|
@@ -537,6 +684,7 @@ function createEmitter(onMetric) {
|
|
|
537
684
|
}
|
|
538
685
|
|
|
539
686
|
// src/shared/namespace.ts
|
|
687
|
+
init_esm_shims();
|
|
540
688
|
var FALLBACK = "unversioned";
|
|
541
689
|
function resolveBuildNamespace(override) {
|
|
542
690
|
if (override !== void 0) {
|
|
@@ -550,6 +698,7 @@ function buildKey(prefix, namespace, rest, hashTag = false) {
|
|
|
550
698
|
}
|
|
551
699
|
|
|
552
700
|
// src/shared/build-phase.ts
|
|
701
|
+
init_esm_shims();
|
|
553
702
|
var PHASE_PRODUCTION_BUILD = "phase-production-build";
|
|
554
703
|
function isBuildPhase() {
|
|
555
704
|
return process.env.NEXT_PHASE === PHASE_PRODUCTION_BUILD;
|
|
@@ -562,6 +711,7 @@ function shouldUseRedis(opts) {
|
|
|
562
711
|
}
|
|
563
712
|
|
|
564
713
|
// src/cache-components/serialize.ts
|
|
714
|
+
init_esm_shims();
|
|
565
715
|
async function readStreamFully(stream) {
|
|
566
716
|
const reader = stream.getReader();
|
|
567
717
|
const chunks = [];
|
|
@@ -604,6 +754,7 @@ function decodeEnvelope(raw) {
|
|
|
604
754
|
}
|
|
605
755
|
|
|
606
756
|
// src/cache-components/swr.ts
|
|
757
|
+
init_esm_shims();
|
|
607
758
|
var SECOND_MS = 1e3;
|
|
608
759
|
function partitionEntry(entry, now = Date.now()) {
|
|
609
760
|
const ageMs = now - entry.timestamp;
|
|
@@ -630,7 +781,9 @@ var DEFAULT_KEY_PREFIX = "next-cache:";
|
|
|
630
781
|
var ENTRY_SUFFIX = "entry:";
|
|
631
782
|
var TAG_SUFFIX = "tag:";
|
|
632
783
|
var TAG_EXP_SUFFIX = "tag-expiration:";
|
|
784
|
+
var LOCK_SUFFIX = "lock:";
|
|
633
785
|
var DEFAULT_ABORT_MS = 1500;
|
|
786
|
+
var DEFAULT_LOCK_TTL_SEC = 10;
|
|
634
787
|
var ONE_YEAR_SEC = 60 * 60 * 24 * 365;
|
|
635
788
|
var FALLBACK_TTL_SEC = 3600;
|
|
636
789
|
var TAG_EXP_MARKER_TTL_SEC = 60 * 60 * 24 * 7;
|
|
@@ -653,16 +806,19 @@ function init(opts) {
|
|
|
653
806
|
staleWhileRevalidate: opts.staleWhileRevalidate ?? true,
|
|
654
807
|
hashTag: opts.hashTag ?? false,
|
|
655
808
|
keyPrefix: opts.keyPrefix ?? DEFAULT_KEY_PREFIX,
|
|
809
|
+
singleFlight: opts.singleFlight ?? false,
|
|
810
|
+
singleFlightLockTtlSec: opts.singleFlightLockTtlSec ?? DEFAULT_LOCK_TTL_SEC,
|
|
656
811
|
rest: opts
|
|
657
812
|
},
|
|
658
813
|
logger,
|
|
659
814
|
emit,
|
|
660
815
|
conn,
|
|
661
|
-
memEntries: new MemoryStore(),
|
|
662
|
-
memTags: new MemorySetStore(),
|
|
816
|
+
memEntries: new MemoryStore(opts.memoryMaxEntries),
|
|
817
|
+
memTags: new MemorySetStore(opts.memoryMaxEntries),
|
|
663
818
|
memTagExp: /* @__PURE__ */ new Map(),
|
|
664
819
|
localTagTimestamps: /* @__PURE__ */ new Map(),
|
|
665
|
-
resolveNs: () => resolveBuildNamespace(opts.buildNamespace)
|
|
820
|
+
resolveNs: () => resolveBuildNamespace(opts.buildNamespace),
|
|
821
|
+
instanceId: process.env.HOSTNAME || process.env.ECS_TASK_ID || `pid-${process.pid}`
|
|
666
822
|
};
|
|
667
823
|
}
|
|
668
824
|
function entryKey(state, cacheKey) {
|
|
@@ -689,6 +845,40 @@ function tagExpKey(state, tag) {
|
|
|
689
845
|
state.opts.hashTag
|
|
690
846
|
);
|
|
691
847
|
}
|
|
848
|
+
function lockKey(state, cacheKey) {
|
|
849
|
+
return buildKey(
|
|
850
|
+
`${state.opts.keyPrefix}${LOCK_SUFFIX}`,
|
|
851
|
+
state.resolveNs(),
|
|
852
|
+
cacheKey,
|
|
853
|
+
state.opts.hashTag
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
async function tryAcquireRefreshLock(state, cacheKey) {
|
|
857
|
+
if (!state.opts.singleFlight) return false;
|
|
858
|
+
if (!gateRedis(state)) return false;
|
|
859
|
+
try {
|
|
860
|
+
return await withAbortSignal(
|
|
861
|
+
"cacheHandlers.singleFlight.lock",
|
|
862
|
+
state.opts.abortTimeoutMs,
|
|
863
|
+
async () => {
|
|
864
|
+
const client = await state.conn.getOrConnect();
|
|
865
|
+
if (!client) return false;
|
|
866
|
+
const result = await execLuaScript(
|
|
867
|
+
client,
|
|
868
|
+
"refreshTagLock",
|
|
869
|
+
[lockKey(state, cacheKey)],
|
|
870
|
+
[
|
|
871
|
+
state.instanceId,
|
|
872
|
+
String(state.opts.singleFlightLockTtlSec)
|
|
873
|
+
]
|
|
874
|
+
);
|
|
875
|
+
return result === 1;
|
|
876
|
+
}
|
|
877
|
+
);
|
|
878
|
+
} catch {
|
|
879
|
+
return false;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
692
882
|
function resolveTtlSeconds(entry) {
|
|
693
883
|
if (Number.isFinite(entry.expire) && entry.expire > 0) {
|
|
694
884
|
return Math.max(60, Math.ceil(entry.expire));
|
|
@@ -741,6 +931,12 @@ async function getImpl(state, cacheKey, softTags) {
|
|
|
741
931
|
state.emit({ type: "cache.miss", ms: Date.now() - start });
|
|
742
932
|
return void 0;
|
|
743
933
|
}
|
|
934
|
+
try {
|
|
935
|
+
raw = await decompressValue(raw);
|
|
936
|
+
} catch {
|
|
937
|
+
state.emit({ type: "cache.miss", meta: { reason: "decode-error" } });
|
|
938
|
+
return void 0;
|
|
939
|
+
}
|
|
744
940
|
envelope = decodeEnvelope(raw);
|
|
745
941
|
if (!envelope) {
|
|
746
942
|
state.emit({ type: "cache.miss", meta: { reason: "decode-error" } });
|
|
@@ -783,11 +979,28 @@ async function getImpl(state, cacheKey, softTags) {
|
|
|
783
979
|
return void 0;
|
|
784
980
|
}
|
|
785
981
|
}
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
982
|
+
if (partition.freshness === "stale") {
|
|
983
|
+
if (state.opts.singleFlight) {
|
|
984
|
+
const isLeader = await tryAcquireRefreshLock(state, cacheKey);
|
|
985
|
+
state.emit({
|
|
986
|
+
type: isLeader ? "cache.stale.refresh.leader" : "cache.stale.refresh.follower",
|
|
987
|
+
ms: Date.now() - start,
|
|
988
|
+
meta: { freshness: partition.freshness }
|
|
989
|
+
});
|
|
990
|
+
} else {
|
|
991
|
+
state.emit({
|
|
992
|
+
type: "cache.stale",
|
|
993
|
+
ms: Date.now() - start,
|
|
994
|
+
meta: { freshness: partition.freshness }
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
} else {
|
|
998
|
+
state.emit({
|
|
999
|
+
type: "cache.hit",
|
|
1000
|
+
ms: Date.now() - start,
|
|
1001
|
+
meta: { freshness: partition.freshness }
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
791
1004
|
return {
|
|
792
1005
|
value: bufferToStream(Buffer.from(envelope.value, "base64")),
|
|
793
1006
|
tags: envelope.tags,
|
|
@@ -852,10 +1065,14 @@ async function setImpl(state, cacheKey, pendingEntry) {
|
|
|
852
1065
|
}
|
|
853
1066
|
return;
|
|
854
1067
|
}
|
|
1068
|
+
const wirePayload = await compressValue(
|
|
1069
|
+
payload,
|
|
1070
|
+
state.opts.rest.compression
|
|
1071
|
+
);
|
|
855
1072
|
if (entry.tags.length === 0) {
|
|
856
|
-
await client.set(eKey,
|
|
1073
|
+
await client.set(eKey, wirePayload, { EX: ttl });
|
|
857
1074
|
} else {
|
|
858
|
-
await luaSetWithTags(client, state, eKey,
|
|
1075
|
+
await luaSetWithTags(client, state, eKey, wirePayload, ttl, entry.tags);
|
|
859
1076
|
}
|
|
860
1077
|
}
|
|
861
1078
|
);
|
|
@@ -891,28 +1108,36 @@ async function refreshTagsImpl(state) {
|
|
|
891
1108
|
async () => {
|
|
892
1109
|
const client = await state.conn.getOrConnect();
|
|
893
1110
|
if (!client) return;
|
|
894
|
-
const
|
|
895
|
-
const
|
|
1111
|
+
const ns = state.opts.hashTag ? `{${state.resolveNs()}}` : state.resolveNs();
|
|
1112
|
+
const pattern = `${escapeMatch(
|
|
1113
|
+
`${state.opts.keyPrefix}${TAG_EXP_SUFFIX}${ns}:`
|
|
1114
|
+
)}*`;
|
|
1115
|
+
const scanStartMs = Date.now();
|
|
1116
|
+
const fresh = /* @__PURE__ */ new Map();
|
|
896
1117
|
for await (const chunk of client.scanIterator({
|
|
897
1118
|
MATCH: pattern,
|
|
898
1119
|
COUNT: 100
|
|
899
1120
|
})) {
|
|
900
1121
|
const keys = Array.isArray(chunk) ? chunk.map(String) : [String(chunk)];
|
|
901
|
-
|
|
902
|
-
|
|
1122
|
+
if (keys.length === 0) continue;
|
|
1123
|
+
const values = await client.mGet(keys);
|
|
1124
|
+
for (let i = 0; i < keys.length; i += 1) {
|
|
1125
|
+
const v = values[i];
|
|
1126
|
+
if (!v) continue;
|
|
1127
|
+
const ts = Number.parseInt(v, 10);
|
|
1128
|
+
if (!Number.isFinite(ts)) continue;
|
|
1129
|
+
const key = keys[i];
|
|
1130
|
+
if (key === void 0) continue;
|
|
1131
|
+
const tag = stripExpPrefix(state, key);
|
|
1132
|
+
if (tag) fresh.set(tag, ts);
|
|
1133
|
+
}
|
|
903
1134
|
}
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
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);
|
|
1135
|
+
for (const [tag, ts] of state.localTagTimestamps) {
|
|
1136
|
+
if (ts >= scanStartMs && (fresh.get(tag) ?? 0) < ts) {
|
|
1137
|
+
fresh.set(tag, ts);
|
|
1138
|
+
}
|
|
915
1139
|
}
|
|
1140
|
+
state.localTagTimestamps = fresh;
|
|
916
1141
|
}
|
|
917
1142
|
);
|
|
918
1143
|
} catch (err) {
|
|
@@ -921,6 +1146,19 @@ async function refreshTagsImpl(state) {
|
|
|
921
1146
|
});
|
|
922
1147
|
}
|
|
923
1148
|
}
|
|
1149
|
+
function escapeMatch(s) {
|
|
1150
|
+
return s.replace(/[\\*?[\]]/g, "\\$&");
|
|
1151
|
+
}
|
|
1152
|
+
var MAX_LOCAL_TAG_ENTRIES = 1e4;
|
|
1153
|
+
function pruneOldestByTimestamp(map, maxEntries) {
|
|
1154
|
+
if (map.size <= maxEntries) return;
|
|
1155
|
+
const byAge = [...map.entries()].sort((a, b) => a[1] - b[1]);
|
|
1156
|
+
const drop = map.size - maxEntries;
|
|
1157
|
+
for (let i = 0; i < drop; i += 1) {
|
|
1158
|
+
const entry = byAge[i];
|
|
1159
|
+
if (entry) map.delete(entry[0]);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
924
1162
|
function stripExpPrefix(state, fullKey) {
|
|
925
1163
|
const ns = state.resolveNs();
|
|
926
1164
|
const prefix = state.opts.hashTag ? `${state.opts.keyPrefix}${TAG_EXP_SUFFIX}{${ns}}:` : `${state.opts.keyPrefix}${TAG_EXP_SUFFIX}${ns}:`;
|
|
@@ -972,6 +1210,7 @@ async function updateTagsImpl(state, tags, durations) {
|
|
|
972
1210
|
state.localTagTimestamps.set(t, now);
|
|
973
1211
|
state.memTagExp.set(t, now);
|
|
974
1212
|
}
|
|
1213
|
+
pruneOldestByTimestamp(state.memTagExp, MAX_LOCAL_TAG_ENTRIES);
|
|
975
1214
|
if (!gateRedis(state)) {
|
|
976
1215
|
if (isHardExpire) {
|
|
977
1216
|
for (const t of tags) {
|
|
@@ -1001,20 +1240,18 @@ async function updateTagsImpl(state, tags, durations) {
|
|
|
1001
1240
|
}
|
|
1002
1241
|
return;
|
|
1003
1242
|
}
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1243
|
+
await Promise.all(
|
|
1244
|
+
tags.map(
|
|
1245
|
+
(t) => isHardExpire ? execLuaScript(
|
|
1007
1246
|
client,
|
|
1008
1247
|
"revalidateHard",
|
|
1009
1248
|
[tagKey(state, t), tagExpKey(state, t)],
|
|
1010
1249
|
[String(now), String(TAG_EXP_MARKER_TTL_SEC)]
|
|
1011
|
-
)
|
|
1012
|
-
} else {
|
|
1013
|
-
await client.set(tagExpKey(state, t), String(now), {
|
|
1250
|
+
) : client.set(tagExpKey(state, t), String(now), {
|
|
1014
1251
|
EX: TAG_EXP_MARKER_TTL_SEC
|
|
1015
|
-
})
|
|
1016
|
-
|
|
1017
|
-
|
|
1252
|
+
})
|
|
1253
|
+
)
|
|
1254
|
+
);
|
|
1018
1255
|
}
|
|
1019
1256
|
);
|
|
1020
1257
|
state.emit({
|