@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
package/dist/index.js
CHANGED
|
@@ -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,29 @@ 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/index.ts
|
|
222
|
+
init_esm_shims();
|
|
223
|
+
|
|
224
|
+
// src/cache-components/index.ts
|
|
225
|
+
init_esm_shims();
|
|
226
|
+
|
|
227
|
+
// src/cache-components/handler.ts
|
|
228
|
+
init_esm_shims();
|
|
229
|
+
|
|
173
230
|
// src/shared/abort.ts
|
|
231
|
+
init_esm_shims();
|
|
174
232
|
var CacheTimeoutError = class extends Error {
|
|
175
233
|
constructor(opName, ms) {
|
|
176
234
|
super(`[@leejpsd/nextjs-cache-handler] ${opName} timed out after ${ms}ms`);
|
|
@@ -202,7 +260,41 @@ async function withAbortSignal(opName, ms, fn) {
|
|
|
202
260
|
}
|
|
203
261
|
}
|
|
204
262
|
|
|
263
|
+
// src/shared/compress.ts
|
|
264
|
+
init_esm_shims();
|
|
265
|
+
var gzipAsync = promisify(gzip);
|
|
266
|
+
var gunzipAsync = promisify(gunzip);
|
|
267
|
+
var brotliCompressAsync = promisify(brotliCompress);
|
|
268
|
+
var brotliDecompressAsync = promisify(brotliDecompress);
|
|
269
|
+
var GZIP_MARKER = "__ncgz__:";
|
|
270
|
+
var BROTLI_MARKER = "__ncbr__:";
|
|
271
|
+
var MIN_COMPRESS_BYTES = 1024;
|
|
272
|
+
var BROTLI_OPTS = {
|
|
273
|
+
params: { [constants.BROTLI_PARAM_QUALITY]: 5 }
|
|
274
|
+
};
|
|
275
|
+
async function compressValue(value, algo) {
|
|
276
|
+
if (!algo || Buffer.byteLength(value) < MIN_COMPRESS_BYTES) return value;
|
|
277
|
+
const input = Buffer.from(value);
|
|
278
|
+
if (algo === "gzip") {
|
|
279
|
+
return GZIP_MARKER + (await gzipAsync(input)).toString("base64");
|
|
280
|
+
}
|
|
281
|
+
return BROTLI_MARKER + (await brotliCompressAsync(input, BROTLI_OPTS)).toString("base64");
|
|
282
|
+
}
|
|
283
|
+
async function decompressValue(value) {
|
|
284
|
+
if (value === null) return null;
|
|
285
|
+
if (value.startsWith(GZIP_MARKER)) {
|
|
286
|
+
const buf = Buffer.from(value.slice(GZIP_MARKER.length), "base64");
|
|
287
|
+
return (await gunzipAsync(buf)).toString("utf8");
|
|
288
|
+
}
|
|
289
|
+
if (value.startsWith(BROTLI_MARKER)) {
|
|
290
|
+
const buf = Buffer.from(value.slice(BROTLI_MARKER.length), "base64");
|
|
291
|
+
return (await brotliDecompressAsync(buf)).toString("utf8");
|
|
292
|
+
}
|
|
293
|
+
return value;
|
|
294
|
+
}
|
|
295
|
+
|
|
205
296
|
// src/shared/client/index.ts
|
|
297
|
+
init_esm_shims();
|
|
206
298
|
async function buildClient(source) {
|
|
207
299
|
if (typeof source === "function") {
|
|
208
300
|
return await source();
|
|
@@ -220,6 +312,10 @@ async function buildClient(source) {
|
|
|
220
312
|
const { adaptCluster: adaptCluster2, createIoredisCluster: createIoredisCluster2 } = await Promise.resolve().then(() => (init_adapter_ioredis(), adapter_ioredis_exports));
|
|
221
313
|
return adaptCluster2(createIoredisCluster2(source));
|
|
222
314
|
}
|
|
315
|
+
case "sentinel": {
|
|
316
|
+
const { adaptIoredis: adaptIoredis2, createIoredisSentinel: createIoredisSentinel2 } = await Promise.resolve().then(() => (init_adapter_ioredis(), adapter_ioredis_exports));
|
|
317
|
+
return adaptIoredis2(createIoredisSentinel2(source));
|
|
318
|
+
}
|
|
223
319
|
default: {
|
|
224
320
|
const exhaustive = source;
|
|
225
321
|
throw new Error(
|
|
@@ -229,28 +325,45 @@ async function buildClient(source) {
|
|
|
229
325
|
}
|
|
230
326
|
}
|
|
231
327
|
var ConnectionManager = class {
|
|
232
|
-
constructor(source, onError) {
|
|
328
|
+
constructor(source, onError, retry = {}) {
|
|
233
329
|
this.source = source;
|
|
234
330
|
this.onError = onError;
|
|
331
|
+
this.baseCooldownMs = retry.baseCooldownMs ?? 1e3;
|
|
332
|
+
this.maxCooldownMs = retry.maxCooldownMs ?? 3e4;
|
|
235
333
|
}
|
|
236
334
|
source;
|
|
237
335
|
onError;
|
|
238
336
|
client = null;
|
|
239
337
|
connecting = null;
|
|
240
|
-
|
|
338
|
+
consecutiveFailures = 0;
|
|
339
|
+
lastFailureAt = 0;
|
|
340
|
+
baseCooldownMs;
|
|
341
|
+
maxCooldownMs;
|
|
342
|
+
cooldownMs() {
|
|
343
|
+
if (this.consecutiveFailures === 0) return 0;
|
|
344
|
+
return Math.min(
|
|
345
|
+
this.maxCooldownMs,
|
|
346
|
+
this.baseCooldownMs * 2 ** (this.consecutiveFailures - 1)
|
|
347
|
+
);
|
|
348
|
+
}
|
|
241
349
|
async getOrConnect() {
|
|
242
350
|
if (this.client?.isOpen) return this.client;
|
|
243
|
-
if (this.failed) return null;
|
|
244
351
|
if (this.connecting) return this.connecting;
|
|
352
|
+
if (Date.now() - this.lastFailureAt < this.cooldownMs()) return null;
|
|
353
|
+
const stale = this.client;
|
|
354
|
+
this.client = null;
|
|
245
355
|
this.connecting = (async () => {
|
|
356
|
+
disposeQuietly(stale);
|
|
246
357
|
try {
|
|
247
358
|
const c = await buildClient(this.source);
|
|
248
359
|
c.on("error", (err) => this.onError(err));
|
|
249
360
|
if (!c.isOpen) await c.connect();
|
|
250
361
|
this.client = c;
|
|
362
|
+
this.consecutiveFailures = 0;
|
|
251
363
|
return c;
|
|
252
364
|
} catch (err) {
|
|
253
|
-
this.
|
|
365
|
+
this.consecutiveFailures += 1;
|
|
366
|
+
this.lastFailureAt = Date.now();
|
|
254
367
|
this.onError(err);
|
|
255
368
|
return null;
|
|
256
369
|
} finally {
|
|
@@ -260,13 +373,24 @@ var ConnectionManager = class {
|
|
|
260
373
|
return this.connecting;
|
|
261
374
|
}
|
|
262
375
|
reset() {
|
|
376
|
+
disposeQuietly(this.client);
|
|
263
377
|
this.client = null;
|
|
264
378
|
this.connecting = null;
|
|
265
|
-
this.
|
|
379
|
+
this.consecutiveFailures = 0;
|
|
380
|
+
this.lastFailureAt = 0;
|
|
266
381
|
}
|
|
267
382
|
};
|
|
383
|
+
function disposeQuietly(client) {
|
|
384
|
+
if (!client) return;
|
|
385
|
+
const c = client;
|
|
386
|
+
try {
|
|
387
|
+
(c.dispose ?? c.destroy ?? c.disconnect)?.call(c);
|
|
388
|
+
} catch {
|
|
389
|
+
}
|
|
390
|
+
}
|
|
268
391
|
|
|
269
392
|
// src/shared/logger.ts
|
|
393
|
+
init_esm_shims();
|
|
270
394
|
var defaultLogger = {
|
|
271
395
|
debug: () => {
|
|
272
396
|
},
|
|
@@ -282,6 +406,9 @@ var defaultLogger = {
|
|
|
282
406
|
}
|
|
283
407
|
};
|
|
284
408
|
|
|
409
|
+
// src/shared/lua/index.ts
|
|
410
|
+
init_esm_shims();
|
|
411
|
+
|
|
285
412
|
// src/shared/lua/set-with-tags.lua
|
|
286
413
|
var set_with_tags_default = `-- set-with-tags.lua
|
|
287
414
|
--
|
|
@@ -385,13 +512,19 @@ async function execLuaScript(client, name, keys, args) {
|
|
|
385
512
|
}
|
|
386
513
|
|
|
387
514
|
// src/shared/memory-fallback.ts
|
|
515
|
+
init_esm_shims();
|
|
388
516
|
var MAX_TIMER_MS = 2147483647;
|
|
389
517
|
function clampTimerMs(ms) {
|
|
390
518
|
return Math.min(Math.max(0, ms), MAX_TIMER_MS);
|
|
391
519
|
}
|
|
520
|
+
var DEFAULT_MAX_ENTRIES = 1e3;
|
|
392
521
|
var MemoryStore = class {
|
|
393
522
|
kv = /* @__PURE__ */ new Map();
|
|
394
523
|
timers = /* @__PURE__ */ new Map();
|
|
524
|
+
maxEntries;
|
|
525
|
+
constructor(maxEntries = DEFAULT_MAX_ENTRIES) {
|
|
526
|
+
this.maxEntries = Math.max(1, maxEntries);
|
|
527
|
+
}
|
|
395
528
|
set(key, value, ttlSeconds) {
|
|
396
529
|
this.delete(key);
|
|
397
530
|
const expiresAt = Date.now() + ttlSeconds * 1e3;
|
|
@@ -401,6 +534,10 @@ var MemoryStore = class {
|
|
|
401
534
|
timer.unref();
|
|
402
535
|
}
|
|
403
536
|
this.timers.set(key, timer);
|
|
537
|
+
while (this.kv.size > this.maxEntries) {
|
|
538
|
+
const oldest = this.kv.keys().next().value;
|
|
539
|
+
this.delete(oldest);
|
|
540
|
+
}
|
|
404
541
|
}
|
|
405
542
|
get(key) {
|
|
406
543
|
const entry = this.kv.get(key);
|
|
@@ -409,6 +546,8 @@ var MemoryStore = class {
|
|
|
409
546
|
this.delete(key);
|
|
410
547
|
return null;
|
|
411
548
|
}
|
|
549
|
+
this.kv.delete(key);
|
|
550
|
+
this.kv.set(key, entry);
|
|
412
551
|
return entry.value;
|
|
413
552
|
}
|
|
414
553
|
delete(key) {
|
|
@@ -443,13 +582,19 @@ var MemorySetStore = class {
|
|
|
443
582
|
sets = /* @__PURE__ */ new Map();
|
|
444
583
|
expiresAt = /* @__PURE__ */ new Map();
|
|
445
584
|
timers = /* @__PURE__ */ new Map();
|
|
585
|
+
maxSets;
|
|
586
|
+
constructor(maxSets = DEFAULT_MAX_ENTRIES) {
|
|
587
|
+
this.maxSets = Math.max(1, maxSets);
|
|
588
|
+
}
|
|
446
589
|
sAdd(key, member) {
|
|
447
590
|
const members = Array.isArray(member) ? member : [member];
|
|
448
591
|
let set = this.sets.get(key);
|
|
449
592
|
if (!set) {
|
|
450
593
|
set = /* @__PURE__ */ new Set();
|
|
451
|
-
|
|
594
|
+
} else {
|
|
595
|
+
this.sets.delete(key);
|
|
452
596
|
}
|
|
597
|
+
this.sets.set(key, set);
|
|
453
598
|
let added = 0;
|
|
454
599
|
for (const m of members) {
|
|
455
600
|
if (!set.has(m)) {
|
|
@@ -457,6 +602,10 @@ var MemorySetStore = class {
|
|
|
457
602
|
added += 1;
|
|
458
603
|
}
|
|
459
604
|
}
|
|
605
|
+
while (this.sets.size > this.maxSets) {
|
|
606
|
+
const oldest = this.sets.keys().next().value;
|
|
607
|
+
this.delete(oldest);
|
|
608
|
+
}
|
|
460
609
|
return added;
|
|
461
610
|
}
|
|
462
611
|
sMembers(key) {
|
|
@@ -509,6 +658,7 @@ var MemorySetStore = class {
|
|
|
509
658
|
};
|
|
510
659
|
|
|
511
660
|
// src/shared/metrics.ts
|
|
661
|
+
init_esm_shims();
|
|
512
662
|
var counters = /* @__PURE__ */ new Map();
|
|
513
663
|
function bump(event) {
|
|
514
664
|
const existing = counters.get(event.type);
|
|
@@ -548,6 +698,7 @@ function getMetricSnapshot() {
|
|
|
548
698
|
}
|
|
549
699
|
|
|
550
700
|
// src/shared/namespace.ts
|
|
701
|
+
init_esm_shims();
|
|
551
702
|
var FALLBACK = "unversioned";
|
|
552
703
|
function resolveBuildNamespace(override) {
|
|
553
704
|
if (override !== void 0) {
|
|
@@ -561,6 +712,7 @@ function buildKey(prefix, namespace, rest, hashTag = false) {
|
|
|
561
712
|
}
|
|
562
713
|
|
|
563
714
|
// src/shared/build-phase.ts
|
|
715
|
+
init_esm_shims();
|
|
564
716
|
var PHASE_PRODUCTION_BUILD = "phase-production-build";
|
|
565
717
|
function isBuildPhase() {
|
|
566
718
|
return process.env.NEXT_PHASE === PHASE_PRODUCTION_BUILD;
|
|
@@ -573,6 +725,7 @@ function shouldUseRedis(opts) {
|
|
|
573
725
|
}
|
|
574
726
|
|
|
575
727
|
// src/cache-components/serialize.ts
|
|
728
|
+
init_esm_shims();
|
|
576
729
|
async function readStreamFully(stream) {
|
|
577
730
|
const reader = stream.getReader();
|
|
578
731
|
const chunks = [];
|
|
@@ -615,6 +768,7 @@ function decodeEnvelope(raw) {
|
|
|
615
768
|
}
|
|
616
769
|
|
|
617
770
|
// src/cache-components/swr.ts
|
|
771
|
+
init_esm_shims();
|
|
618
772
|
var SECOND_MS = 1e3;
|
|
619
773
|
function partitionEntry(entry, now = Date.now()) {
|
|
620
774
|
const ageMs = now - entry.timestamp;
|
|
@@ -641,7 +795,9 @@ var DEFAULT_KEY_PREFIX = "next-cache:";
|
|
|
641
795
|
var ENTRY_SUFFIX = "entry:";
|
|
642
796
|
var TAG_SUFFIX = "tag:";
|
|
643
797
|
var TAG_EXP_SUFFIX = "tag-expiration:";
|
|
798
|
+
var LOCK_SUFFIX = "lock:";
|
|
644
799
|
var DEFAULT_ABORT_MS = 1500;
|
|
800
|
+
var DEFAULT_LOCK_TTL_SEC = 10;
|
|
645
801
|
var ONE_YEAR_SEC = 60 * 60 * 24 * 365;
|
|
646
802
|
var FALLBACK_TTL_SEC = 3600;
|
|
647
803
|
var TAG_EXP_MARKER_TTL_SEC = 60 * 60 * 24 * 7;
|
|
@@ -664,16 +820,19 @@ function init(opts) {
|
|
|
664
820
|
staleWhileRevalidate: opts.staleWhileRevalidate ?? true,
|
|
665
821
|
hashTag: opts.hashTag ?? false,
|
|
666
822
|
keyPrefix: opts.keyPrefix ?? DEFAULT_KEY_PREFIX,
|
|
823
|
+
singleFlight: opts.singleFlight ?? false,
|
|
824
|
+
singleFlightLockTtlSec: opts.singleFlightLockTtlSec ?? DEFAULT_LOCK_TTL_SEC,
|
|
667
825
|
rest: opts
|
|
668
826
|
},
|
|
669
827
|
logger,
|
|
670
828
|
emit,
|
|
671
829
|
conn,
|
|
672
|
-
memEntries: new MemoryStore(),
|
|
673
|
-
memTags: new MemorySetStore(),
|
|
830
|
+
memEntries: new MemoryStore(opts.memoryMaxEntries),
|
|
831
|
+
memTags: new MemorySetStore(opts.memoryMaxEntries),
|
|
674
832
|
memTagExp: /* @__PURE__ */ new Map(),
|
|
675
833
|
localTagTimestamps: /* @__PURE__ */ new Map(),
|
|
676
|
-
resolveNs: () => resolveBuildNamespace(opts.buildNamespace)
|
|
834
|
+
resolveNs: () => resolveBuildNamespace(opts.buildNamespace),
|
|
835
|
+
instanceId: process.env.HOSTNAME || process.env.ECS_TASK_ID || `pid-${process.pid}`
|
|
677
836
|
};
|
|
678
837
|
}
|
|
679
838
|
function entryKey(state, cacheKey) {
|
|
@@ -700,6 +859,40 @@ function tagExpKey(state, tag) {
|
|
|
700
859
|
state.opts.hashTag
|
|
701
860
|
);
|
|
702
861
|
}
|
|
862
|
+
function lockKey(state, cacheKey) {
|
|
863
|
+
return buildKey(
|
|
864
|
+
`${state.opts.keyPrefix}${LOCK_SUFFIX}`,
|
|
865
|
+
state.resolveNs(),
|
|
866
|
+
cacheKey,
|
|
867
|
+
state.opts.hashTag
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
async function tryAcquireRefreshLock(state, cacheKey) {
|
|
871
|
+
if (!state.opts.singleFlight) return false;
|
|
872
|
+
if (!gateRedis(state)) return false;
|
|
873
|
+
try {
|
|
874
|
+
return await withAbortSignal(
|
|
875
|
+
"cacheHandlers.singleFlight.lock",
|
|
876
|
+
state.opts.abortTimeoutMs,
|
|
877
|
+
async () => {
|
|
878
|
+
const client = await state.conn.getOrConnect();
|
|
879
|
+
if (!client) return false;
|
|
880
|
+
const result = await execLuaScript(
|
|
881
|
+
client,
|
|
882
|
+
"refreshTagLock",
|
|
883
|
+
[lockKey(state, cacheKey)],
|
|
884
|
+
[
|
|
885
|
+
state.instanceId,
|
|
886
|
+
String(state.opts.singleFlightLockTtlSec)
|
|
887
|
+
]
|
|
888
|
+
);
|
|
889
|
+
return result === 1;
|
|
890
|
+
}
|
|
891
|
+
);
|
|
892
|
+
} catch {
|
|
893
|
+
return false;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
703
896
|
function resolveTtlSeconds(entry) {
|
|
704
897
|
if (Number.isFinite(entry.expire) && entry.expire > 0) {
|
|
705
898
|
return Math.max(60, Math.ceil(entry.expire));
|
|
@@ -752,6 +945,12 @@ async function getImpl(state, cacheKey, softTags) {
|
|
|
752
945
|
state.emit({ type: "cache.miss", ms: Date.now() - start });
|
|
753
946
|
return void 0;
|
|
754
947
|
}
|
|
948
|
+
try {
|
|
949
|
+
raw = await decompressValue(raw);
|
|
950
|
+
} catch {
|
|
951
|
+
state.emit({ type: "cache.miss", meta: { reason: "decode-error" } });
|
|
952
|
+
return void 0;
|
|
953
|
+
}
|
|
755
954
|
envelope = decodeEnvelope(raw);
|
|
756
955
|
if (!envelope) {
|
|
757
956
|
state.emit({ type: "cache.miss", meta: { reason: "decode-error" } });
|
|
@@ -794,11 +993,28 @@ async function getImpl(state, cacheKey, softTags) {
|
|
|
794
993
|
return void 0;
|
|
795
994
|
}
|
|
796
995
|
}
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
996
|
+
if (partition.freshness === "stale") {
|
|
997
|
+
if (state.opts.singleFlight) {
|
|
998
|
+
const isLeader = await tryAcquireRefreshLock(state, cacheKey);
|
|
999
|
+
state.emit({
|
|
1000
|
+
type: isLeader ? "cache.stale.refresh.leader" : "cache.stale.refresh.follower",
|
|
1001
|
+
ms: Date.now() - start,
|
|
1002
|
+
meta: { freshness: partition.freshness }
|
|
1003
|
+
});
|
|
1004
|
+
} else {
|
|
1005
|
+
state.emit({
|
|
1006
|
+
type: "cache.stale",
|
|
1007
|
+
ms: Date.now() - start,
|
|
1008
|
+
meta: { freshness: partition.freshness }
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
} else {
|
|
1012
|
+
state.emit({
|
|
1013
|
+
type: "cache.hit",
|
|
1014
|
+
ms: Date.now() - start,
|
|
1015
|
+
meta: { freshness: partition.freshness }
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
802
1018
|
return {
|
|
803
1019
|
value: bufferToStream(Buffer.from(envelope.value, "base64")),
|
|
804
1020
|
tags: envelope.tags,
|
|
@@ -863,10 +1079,14 @@ async function setImpl(state, cacheKey, pendingEntry) {
|
|
|
863
1079
|
}
|
|
864
1080
|
return;
|
|
865
1081
|
}
|
|
1082
|
+
const wirePayload = await compressValue(
|
|
1083
|
+
payload,
|
|
1084
|
+
state.opts.rest.compression
|
|
1085
|
+
);
|
|
866
1086
|
if (entry.tags.length === 0) {
|
|
867
|
-
await client.set(eKey,
|
|
1087
|
+
await client.set(eKey, wirePayload, { EX: ttl });
|
|
868
1088
|
} else {
|
|
869
|
-
await luaSetWithTags(client, state, eKey,
|
|
1089
|
+
await luaSetWithTags(client, state, eKey, wirePayload, ttl, entry.tags);
|
|
870
1090
|
}
|
|
871
1091
|
}
|
|
872
1092
|
);
|
|
@@ -902,28 +1122,36 @@ async function refreshTagsImpl(state) {
|
|
|
902
1122
|
async () => {
|
|
903
1123
|
const client = await state.conn.getOrConnect();
|
|
904
1124
|
if (!client) return;
|
|
905
|
-
const
|
|
906
|
-
const
|
|
1125
|
+
const ns = state.opts.hashTag ? `{${state.resolveNs()}}` : state.resolveNs();
|
|
1126
|
+
const pattern = `${escapeMatch(
|
|
1127
|
+
`${state.opts.keyPrefix}${TAG_EXP_SUFFIX}${ns}:`
|
|
1128
|
+
)}*`;
|
|
1129
|
+
const scanStartMs = Date.now();
|
|
1130
|
+
const fresh = /* @__PURE__ */ new Map();
|
|
907
1131
|
for await (const chunk of client.scanIterator({
|
|
908
1132
|
MATCH: pattern,
|
|
909
1133
|
COUNT: 100
|
|
910
1134
|
})) {
|
|
911
1135
|
const keys = Array.isArray(chunk) ? chunk.map(String) : [String(chunk)];
|
|
912
|
-
|
|
913
|
-
|
|
1136
|
+
if (keys.length === 0) continue;
|
|
1137
|
+
const values = await client.mGet(keys);
|
|
1138
|
+
for (let i = 0; i < keys.length; i += 1) {
|
|
1139
|
+
const v = values[i];
|
|
1140
|
+
if (!v) continue;
|
|
1141
|
+
const ts = Number.parseInt(v, 10);
|
|
1142
|
+
if (!Number.isFinite(ts)) continue;
|
|
1143
|
+
const key = keys[i];
|
|
1144
|
+
if (key === void 0) continue;
|
|
1145
|
+
const tag = stripExpPrefix(state, key);
|
|
1146
|
+
if (tag) fresh.set(tag, ts);
|
|
1147
|
+
}
|
|
914
1148
|
}
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
if (!v) continue;
|
|
920
|
-
const ts = Number.parseInt(v, 10);
|
|
921
|
-
if (!Number.isFinite(ts)) continue;
|
|
922
|
-
const key = seen[i];
|
|
923
|
-
if (key === void 0) continue;
|
|
924
|
-
const tag = stripExpPrefix(state, key);
|
|
925
|
-
if (tag) state.localTagTimestamps.set(tag, ts);
|
|
1149
|
+
for (const [tag, ts] of state.localTagTimestamps) {
|
|
1150
|
+
if (ts >= scanStartMs && (fresh.get(tag) ?? 0) < ts) {
|
|
1151
|
+
fresh.set(tag, ts);
|
|
1152
|
+
}
|
|
926
1153
|
}
|
|
1154
|
+
state.localTagTimestamps = fresh;
|
|
927
1155
|
}
|
|
928
1156
|
);
|
|
929
1157
|
} catch (err) {
|
|
@@ -932,6 +1160,19 @@ async function refreshTagsImpl(state) {
|
|
|
932
1160
|
});
|
|
933
1161
|
}
|
|
934
1162
|
}
|
|
1163
|
+
function escapeMatch(s) {
|
|
1164
|
+
return s.replace(/[\\*?[\]]/g, "\\$&");
|
|
1165
|
+
}
|
|
1166
|
+
var MAX_LOCAL_TAG_ENTRIES = 1e4;
|
|
1167
|
+
function pruneOldestByTimestamp(map, maxEntries) {
|
|
1168
|
+
if (map.size <= maxEntries) return;
|
|
1169
|
+
const byAge = [...map.entries()].sort((a, b) => a[1] - b[1]);
|
|
1170
|
+
const drop = map.size - maxEntries;
|
|
1171
|
+
for (let i = 0; i < drop; i += 1) {
|
|
1172
|
+
const entry = byAge[i];
|
|
1173
|
+
if (entry) map.delete(entry[0]);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
935
1176
|
function stripExpPrefix(state, fullKey) {
|
|
936
1177
|
const ns = state.resolveNs();
|
|
937
1178
|
const prefix = state.opts.hashTag ? `${state.opts.keyPrefix}${TAG_EXP_SUFFIX}{${ns}}:` : `${state.opts.keyPrefix}${TAG_EXP_SUFFIX}${ns}:`;
|
|
@@ -983,6 +1224,7 @@ async function updateTagsImpl(state, tags, durations) {
|
|
|
983
1224
|
state.localTagTimestamps.set(t, now);
|
|
984
1225
|
state.memTagExp.set(t, now);
|
|
985
1226
|
}
|
|
1227
|
+
pruneOldestByTimestamp(state.memTagExp, MAX_LOCAL_TAG_ENTRIES);
|
|
986
1228
|
if (!gateRedis(state)) {
|
|
987
1229
|
if (isHardExpire) {
|
|
988
1230
|
for (const t of tags) {
|
|
@@ -1012,20 +1254,18 @@ async function updateTagsImpl(state, tags, durations) {
|
|
|
1012
1254
|
}
|
|
1013
1255
|
return;
|
|
1014
1256
|
}
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1257
|
+
await Promise.all(
|
|
1258
|
+
tags.map(
|
|
1259
|
+
(t) => isHardExpire ? execLuaScript(
|
|
1018
1260
|
client,
|
|
1019
1261
|
"revalidateHard",
|
|
1020
1262
|
[tagKey(state, t), tagExpKey(state, t)],
|
|
1021
1263
|
[String(now), String(TAG_EXP_MARKER_TTL_SEC)]
|
|
1022
|
-
)
|
|
1023
|
-
} else {
|
|
1024
|
-
await client.set(tagExpKey(state, t), String(now), {
|
|
1264
|
+
) : client.set(tagExpKey(state, t), String(now), {
|
|
1025
1265
|
EX: TAG_EXP_MARKER_TTL_SEC
|
|
1026
|
-
})
|
|
1027
|
-
|
|
1028
|
-
|
|
1266
|
+
})
|
|
1267
|
+
)
|
|
1268
|
+
);
|
|
1029
1269
|
}
|
|
1030
1270
|
);
|
|
1031
1271
|
state.emit({
|
|
@@ -1055,7 +1295,14 @@ function createCacheComponentsHandler(opts) {
|
|
|
1055
1295
|
};
|
|
1056
1296
|
}
|
|
1057
1297
|
|
|
1298
|
+
// src/incremental/index.ts
|
|
1299
|
+
init_esm_shims();
|
|
1300
|
+
|
|
1301
|
+
// src/incremental/handler.ts
|
|
1302
|
+
init_esm_shims();
|
|
1303
|
+
|
|
1058
1304
|
// src/incremental/serialize.ts
|
|
1305
|
+
init_esm_shims();
|
|
1059
1306
|
function isEnvelope(value) {
|
|
1060
1307
|
return typeof value === "object" && value !== null && "__type" in value && typeof value.__type === "string";
|
|
1061
1308
|
}
|
|
@@ -1137,7 +1384,7 @@ function init2(opts) {
|
|
|
1137
1384
|
logger,
|
|
1138
1385
|
emit,
|
|
1139
1386
|
conn,
|
|
1140
|
-
memEntries: new MemoryStore(),
|
|
1387
|
+
memEntries: new MemoryStore(opts.memoryMaxEntries),
|
|
1141
1388
|
memTagStates: /* @__PURE__ */ new Map(),
|
|
1142
1389
|
resolveNs: () => resolveBuildNamespace(opts.buildNamespace)
|
|
1143
1390
|
};
|
|
@@ -1196,7 +1443,7 @@ function normalizeTtlSeconds(data, ctx) {
|
|
|
1196
1443
|
return ONE_YEAR_SEC2;
|
|
1197
1444
|
}
|
|
1198
1445
|
}
|
|
1199
|
-
const revalidate = ctx?.cacheControl?.revalidate;
|
|
1446
|
+
const revalidate = ctx?.cacheControl?.revalidate ?? ctx?.revalidate;
|
|
1200
1447
|
if (revalidate === false || revalidate === void 0) return ONE_YEAR_SEC2;
|
|
1201
1448
|
if (typeof revalidate === "number" && revalidate > 0) {
|
|
1202
1449
|
return Math.min(Math.max(Math.ceil(revalidate), MIN_TTL_SEC), ONE_YEAR_SEC2);
|
|
@@ -1234,10 +1481,49 @@ async function readTagStates(state, tags) {
|
|
|
1234
1481
|
var IncrementalRedisCacheHandler = class {
|
|
1235
1482
|
state;
|
|
1236
1483
|
revalidatedTags;
|
|
1484
|
+
/**
|
|
1485
|
+
* Request-scoped memo of Redis entry reads. Next.js instantiates this class
|
|
1486
|
+
* per request (and calls resetRequestCache() at request boundaries when it
|
|
1487
|
+
* reuses an instance), so entries live at most one request. Bounded by the
|
|
1488
|
+
* number of distinct cache keys a single request touches.
|
|
1489
|
+
*/
|
|
1490
|
+
requestMemo = /* @__PURE__ */ new Map();
|
|
1237
1491
|
constructor(state, ctx) {
|
|
1238
1492
|
this.state = state;
|
|
1239
1493
|
this.revalidatedTags = normalizeTags(ctx?.revalidatedTags);
|
|
1240
1494
|
}
|
|
1495
|
+
/**
|
|
1496
|
+
* Redis entry read, deduplicated per request: parallel RSC renders or
|
|
1497
|
+
* repeated fetch() calls hitting the same cache key share one GET. The
|
|
1498
|
+
* memoized promise never rejects — errors are converted to a miss (with
|
|
1499
|
+
* the same timeout metric the inline path emitted before).
|
|
1500
|
+
*/
|
|
1501
|
+
readEntryDeduped(eKey) {
|
|
1502
|
+
const memoized = this.requestMemo.get(eKey);
|
|
1503
|
+
if (memoized) {
|
|
1504
|
+
this.state.emit({ type: "cache.get.deduped" });
|
|
1505
|
+
return memoized;
|
|
1506
|
+
}
|
|
1507
|
+
const read = withAbortSignal(
|
|
1508
|
+
"incremental.get",
|
|
1509
|
+
this.state.opts.abortTimeoutMs,
|
|
1510
|
+
async () => {
|
|
1511
|
+
const client = await this.state.conn.getOrConnect();
|
|
1512
|
+
if (!client) return null;
|
|
1513
|
+
return await decompressValue(await client.get(eKey));
|
|
1514
|
+
}
|
|
1515
|
+
).catch((err) => {
|
|
1516
|
+
if (err instanceof CacheTimeoutError) {
|
|
1517
|
+
this.state.emit({
|
|
1518
|
+
type: "redis.timeout",
|
|
1519
|
+
meta: { op: "incremental.get" }
|
|
1520
|
+
});
|
|
1521
|
+
}
|
|
1522
|
+
return null;
|
|
1523
|
+
});
|
|
1524
|
+
this.requestMemo.set(eKey, read);
|
|
1525
|
+
return read;
|
|
1526
|
+
}
|
|
1241
1527
|
async get(cacheKey, ctx) {
|
|
1242
1528
|
try {
|
|
1243
1529
|
const requestedTags = normalizeTags([
|
|
@@ -1249,24 +1535,11 @@ var IncrementalRedisCacheHandler = class {
|
|
|
1249
1535
|
const useRedis = !useLocal && gateRedis2(this.state);
|
|
1250
1536
|
let raw = null;
|
|
1251
1537
|
if (useRedis) {
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
async () => {
|
|
1257
|
-
const client = await this.state.conn.getOrConnect();
|
|
1258
|
-
if (!client) return null;
|
|
1259
|
-
return await client.get(eKey);
|
|
1260
|
-
}
|
|
1261
|
-
);
|
|
1262
|
-
} catch (err) {
|
|
1263
|
-
if (err instanceof CacheTimeoutError) {
|
|
1264
|
-
this.state.emit({ type: "redis.timeout", meta: { op: "incremental.get" } });
|
|
1265
|
-
}
|
|
1266
|
-
raw = null;
|
|
1267
|
-
}
|
|
1538
|
+
raw = await this.readEntryDeduped(eKey);
|
|
1539
|
+
}
|
|
1540
|
+
if (raw === null && (useLocal || this.state.opts.fallback !== "never")) {
|
|
1541
|
+
raw = this.state.memEntries.get(eKey);
|
|
1268
1542
|
}
|
|
1269
|
-
if (raw === null) raw = this.state.memEntries.get(eKey);
|
|
1270
1543
|
if (raw === null) {
|
|
1271
1544
|
this.state.emit({ type: "cache.miss" });
|
|
1272
1545
|
return null;
|
|
@@ -1279,7 +1552,7 @@ var IncrementalRedisCacheHandler = class {
|
|
|
1279
1552
|
return null;
|
|
1280
1553
|
}
|
|
1281
1554
|
const tagStates = await readTagStates(this.state, tags);
|
|
1282
|
-
const isFetch = ctx?.kind === "FETCH";
|
|
1555
|
+
const isFetch = ctx?.kind === "FETCH" || ctx?.kindHint?.toLowerCase() === "fetch" || typeof parsed.value === "object" && parsed.value !== null && parsed.value.kind === "FETCH";
|
|
1283
1556
|
if (tagStates.some((s) => isTagStateExpired(s, parsed.lastModified, isFetch))) {
|
|
1284
1557
|
this.state.emit({ type: "cache.miss", meta: { reason: "tag-state-expired" } });
|
|
1285
1558
|
return null;
|
|
@@ -1302,6 +1575,7 @@ var IncrementalRedisCacheHandler = class {
|
|
|
1302
1575
|
const record = { lastModified: Date.now(), value: data };
|
|
1303
1576
|
const serialized = serializeCacheRecord(record);
|
|
1304
1577
|
const eKey = entryKey2(this.state, cacheKey);
|
|
1578
|
+
this.requestMemo.delete(eKey);
|
|
1305
1579
|
const useRedis = !useLocal && gateRedis2(this.state);
|
|
1306
1580
|
if (useRedis) {
|
|
1307
1581
|
try {
|
|
@@ -1311,10 +1585,19 @@ var IncrementalRedisCacheHandler = class {
|
|
|
1311
1585
|
async () => {
|
|
1312
1586
|
const client = await this.state.conn.getOrConnect();
|
|
1313
1587
|
if (!client) {
|
|
1314
|
-
this.state.
|
|
1588
|
+
if (this.state.opts.fallback !== "never") {
|
|
1589
|
+
this.state.memEntries.set(eKey, serialized, ttl);
|
|
1590
|
+
}
|
|
1315
1591
|
return;
|
|
1316
1592
|
}
|
|
1317
|
-
await client.set(
|
|
1593
|
+
await client.set(
|
|
1594
|
+
eKey,
|
|
1595
|
+
await compressValue(
|
|
1596
|
+
serialized,
|
|
1597
|
+
this.state.opts.rest.compression
|
|
1598
|
+
),
|
|
1599
|
+
{ EX: ttl }
|
|
1600
|
+
);
|
|
1318
1601
|
}
|
|
1319
1602
|
);
|
|
1320
1603
|
this.state.emit({ type: "cache.set", meta: { backend: "redis" } });
|
|
@@ -1325,8 +1608,15 @@ var IncrementalRedisCacheHandler = class {
|
|
|
1325
1608
|
}
|
|
1326
1609
|
}
|
|
1327
1610
|
}
|
|
1328
|
-
this.state.
|
|
1329
|
-
|
|
1611
|
+
if (useLocal || this.state.opts.fallback !== "never") {
|
|
1612
|
+
this.state.memEntries.set(eKey, serialized, ttl);
|
|
1613
|
+
this.state.emit({ type: "cache.set", meta: { backend: "memory" } });
|
|
1614
|
+
} else {
|
|
1615
|
+
this.state.emit({
|
|
1616
|
+
type: "cache.set.failed",
|
|
1617
|
+
meta: { reason: "no-fallback" }
|
|
1618
|
+
});
|
|
1619
|
+
}
|
|
1330
1620
|
} catch (err) {
|
|
1331
1621
|
this.state.logger.error("set() error", {
|
|
1332
1622
|
message: err.message
|
|
@@ -1380,6 +1670,7 @@ var IncrementalRedisCacheHandler = class {
|
|
|
1380
1670
|
}
|
|
1381
1671
|
}
|
|
1382
1672
|
resetRequestCache() {
|
|
1673
|
+
this.requestMemo.clear();
|
|
1383
1674
|
}
|
|
1384
1675
|
};
|
|
1385
1676
|
function createIncrementalCacheHandler(opts) {
|