@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.
- package/CHANGELOG.md +51 -0
- package/LICENSE +21 -0
- package/README.md +259 -0
- package/dist/cache-components/index.cjs +1053 -0
- package/dist/cache-components/index.cjs.map +1 -0
- package/dist/cache-components/index.d.cts +85 -0
- package/dist/cache-components/index.d.ts +85 -0
- package/dist/cache-components/index.js +1049 -0
- package/dist/cache-components/index.js.map +1 -0
- package/dist/incremental/index.cjs +734 -0
- package/dist/incremental/index.cjs.map +1 -0
- package/dist/incremental/index.d.cts +131 -0
- package/dist/incremental/index.d.ts +131 -0
- package/dist/incremental/index.js +731 -0
- package/dist/incremental/index.js.map +1 -0
- package/dist/index.cjs +1408 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +59 -0
- package/dist/index.d.ts +59 -0
- package/dist/index.js +1397 -0
- package/dist/index.js.map +1 -0
- package/dist/ops/index.cjs +19 -0
- package/dist/ops/index.cjs.map +1 -0
- package/dist/ops/index.d.cts +22 -0
- package/dist/ops/index.d.ts +22 -0
- package/dist/ops/index.js +17 -0
- package/dist/ops/index.js.map +1 -0
- package/dist/shared/client/adapter-ioredis.cjs +130 -0
- package/dist/shared/client/adapter-ioredis.cjs.map +1 -0
- package/dist/shared/client/adapter-ioredis.d.cts +22 -0
- package/dist/shared/client/adapter-ioredis.d.ts +22 -0
- package/dist/shared/client/adapter-ioredis.js +125 -0
- package/dist/shared/client/adapter-ioredis.js.map +1 -0
- package/dist/shared/client/adapter-redis.cjs +32 -0
- package/dist/shared/client/adapter-redis.cjs.map +1 -0
- package/dist/shared/client/adapter-redis.d.cts +19 -0
- package/dist/shared/client/adapter-redis.d.ts +19 -0
- package/dist/shared/client/adapter-redis.js +29 -0
- package/dist/shared/client/adapter-redis.js.map +1 -0
- package/dist/types-CgriT7My.d.cts +193 -0
- package/dist/types-CgriT7My.d.ts +193 -0
- package/package.json +178 -0
|
@@ -0,0 +1,734 @@
|
|
|
1
|
+
'use strict';
|
|
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/memory-fallback.ts
|
|
286
|
+
var MAX_TIMER_MS = 2147483647;
|
|
287
|
+
function clampTimerMs(ms) {
|
|
288
|
+
return Math.min(Math.max(0, ms), MAX_TIMER_MS);
|
|
289
|
+
}
|
|
290
|
+
var MemoryStore = class {
|
|
291
|
+
kv = /* @__PURE__ */ new Map();
|
|
292
|
+
timers = /* @__PURE__ */ new Map();
|
|
293
|
+
set(key, value, ttlSeconds) {
|
|
294
|
+
this.delete(key);
|
|
295
|
+
const expiresAt = Date.now() + ttlSeconds * 1e3;
|
|
296
|
+
this.kv.set(key, { value, expiresAt });
|
|
297
|
+
const timer = setTimeout(() => this.delete(key), clampTimerMs(ttlSeconds * 1e3));
|
|
298
|
+
if (typeof timer === "object" && "unref" in timer) {
|
|
299
|
+
timer.unref();
|
|
300
|
+
}
|
|
301
|
+
this.timers.set(key, timer);
|
|
302
|
+
}
|
|
303
|
+
get(key) {
|
|
304
|
+
const entry = this.kv.get(key);
|
|
305
|
+
if (!entry) return null;
|
|
306
|
+
if (Date.now() > entry.expiresAt) {
|
|
307
|
+
this.delete(key);
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
return entry.value;
|
|
311
|
+
}
|
|
312
|
+
delete(key) {
|
|
313
|
+
const timer = this.timers.get(key);
|
|
314
|
+
if (timer !== void 0) {
|
|
315
|
+
clearTimeout(timer);
|
|
316
|
+
this.timers.delete(key);
|
|
317
|
+
}
|
|
318
|
+
return this.kv.delete(key);
|
|
319
|
+
}
|
|
320
|
+
has(key) {
|
|
321
|
+
return this.get(key) !== null;
|
|
322
|
+
}
|
|
323
|
+
keys() {
|
|
324
|
+
const now = Date.now();
|
|
325
|
+
const live = [];
|
|
326
|
+
for (const [key, entry] of this.kv.entries()) {
|
|
327
|
+
if (entry.expiresAt >= now) live.push(key);
|
|
328
|
+
}
|
|
329
|
+
return live;
|
|
330
|
+
}
|
|
331
|
+
size() {
|
|
332
|
+
return this.keys().length;
|
|
333
|
+
}
|
|
334
|
+
clear() {
|
|
335
|
+
for (const timer of this.timers.values()) clearTimeout(timer);
|
|
336
|
+
this.timers.clear();
|
|
337
|
+
this.kv.clear();
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
// src/shared/metrics.ts
|
|
342
|
+
var counters = /* @__PURE__ */ new Map();
|
|
343
|
+
function bump(event) {
|
|
344
|
+
const existing = counters.get(event.type);
|
|
345
|
+
if (existing) {
|
|
346
|
+
existing.count += 1;
|
|
347
|
+
if (event.ms !== void 0) existing.totalMs += event.ms;
|
|
348
|
+
existing.lastTs = Date.now();
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
counters.set(event.type, {
|
|
352
|
+
count: 1,
|
|
353
|
+
totalMs: event.ms ?? 0,
|
|
354
|
+
lastTs: Date.now()
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
function createEmitter(onMetric) {
|
|
358
|
+
return (event) => {
|
|
359
|
+
bump(event);
|
|
360
|
+
if (onMetric) {
|
|
361
|
+
try {
|
|
362
|
+
onMetric(event);
|
|
363
|
+
} catch {
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// src/shared/namespace.ts
|
|
370
|
+
var FALLBACK = "unversioned";
|
|
371
|
+
function resolveBuildNamespace(override) {
|
|
372
|
+
if (override !== void 0) {
|
|
373
|
+
return typeof override === "function" ? override() : override;
|
|
374
|
+
}
|
|
375
|
+
return process.env.DEPLOYMENT_VERSION ?? process.env.GIT_HASH ?? process.env.NEXT_DEPLOYMENT_ID ?? FALLBACK;
|
|
376
|
+
}
|
|
377
|
+
function buildKey(prefix, namespace, rest, hashTag = false) {
|
|
378
|
+
const ns = hashTag ? `{${namespace}}` : namespace;
|
|
379
|
+
return `${prefix}${ns}:${rest}`;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// src/shared/build-phase.ts
|
|
383
|
+
var PHASE_PRODUCTION_BUILD = "phase-production-build";
|
|
384
|
+
function isBuildPhase() {
|
|
385
|
+
return process.env.NEXT_PHASE === PHASE_PRODUCTION_BUILD;
|
|
386
|
+
}
|
|
387
|
+
function shouldUseRedis(opts) {
|
|
388
|
+
if (opts.fallback === "always") return false;
|
|
389
|
+
const detector = opts.isBuildPhase ?? isBuildPhase;
|
|
390
|
+
if (detector()) return false;
|
|
391
|
+
return true;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// src/incremental/serialize.ts
|
|
395
|
+
function isEnvelope(value) {
|
|
396
|
+
return typeof value === "object" && value !== null && "__type" in value && typeof value.__type === "string";
|
|
397
|
+
}
|
|
398
|
+
function isLegacyBufferShape(value) {
|
|
399
|
+
return typeof value === "object" && value !== null && value.type === "Buffer" && Array.isArray(value.data);
|
|
400
|
+
}
|
|
401
|
+
function replacer(_key, value) {
|
|
402
|
+
if (Buffer.isBuffer(value)) {
|
|
403
|
+
const env = {
|
|
404
|
+
__type: "Buffer",
|
|
405
|
+
data: value.toString("base64")
|
|
406
|
+
};
|
|
407
|
+
return env;
|
|
408
|
+
}
|
|
409
|
+
if (value instanceof Map) {
|
|
410
|
+
const env = {
|
|
411
|
+
__type: "Map",
|
|
412
|
+
entries: Array.from(value.entries())
|
|
413
|
+
};
|
|
414
|
+
return env;
|
|
415
|
+
}
|
|
416
|
+
return value;
|
|
417
|
+
}
|
|
418
|
+
function reviver(_key, value) {
|
|
419
|
+
if (isLegacyBufferShape(value)) {
|
|
420
|
+
return Buffer.from(value.data);
|
|
421
|
+
}
|
|
422
|
+
if (!isEnvelope(value)) return value;
|
|
423
|
+
if (value.__type === "Buffer") {
|
|
424
|
+
return Buffer.from(value.data, "base64");
|
|
425
|
+
}
|
|
426
|
+
if (value.__type === "Map") {
|
|
427
|
+
return new Map(value.entries);
|
|
428
|
+
}
|
|
429
|
+
return value;
|
|
430
|
+
}
|
|
431
|
+
function serializeCacheRecord(record) {
|
|
432
|
+
return JSON.stringify(record, replacer);
|
|
433
|
+
}
|
|
434
|
+
function deserializeCacheRecord(serialized) {
|
|
435
|
+
try {
|
|
436
|
+
return JSON.parse(serialized, reviver);
|
|
437
|
+
} catch {
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// src/incremental/handler.ts
|
|
443
|
+
var DEFAULT_KEY_PREFIX = "next-incremental:";
|
|
444
|
+
var ENTRY_SUFFIX = "entry:";
|
|
445
|
+
var TAG_SUFFIX = "tag:";
|
|
446
|
+
var INSTANCE_LOCAL_TAG_PREFIX = "instance-local:";
|
|
447
|
+
var ONE_YEAR_SEC = 60 * 60 * 24 * 365;
|
|
448
|
+
var MIN_TTL_SEC = 60;
|
|
449
|
+
var DEFAULT_ABORT_MS = 1500;
|
|
450
|
+
function gateRedis(state) {
|
|
451
|
+
const fb = state.opts.fallback;
|
|
452
|
+
const ibp = state.opts.rest.isBuildPhase;
|
|
453
|
+
return ibp ? shouldUseRedis({ fallback: fb, isBuildPhase: ibp }) : shouldUseRedis({ fallback: fb });
|
|
454
|
+
}
|
|
455
|
+
function gateRedisInit(state) {
|
|
456
|
+
return gateRedis(state);
|
|
457
|
+
}
|
|
458
|
+
function init(opts) {
|
|
459
|
+
const logger = opts.logger ?? defaultLogger;
|
|
460
|
+
const emit = createEmitter(opts.onMetric);
|
|
461
|
+
const conn = new ConnectionManager(opts.client, (err) => {
|
|
462
|
+
logger.warn("Redis client error (incremental)", { message: err.message });
|
|
463
|
+
emit({ type: "redis.connect.failed", meta: { message: err.message } });
|
|
464
|
+
});
|
|
465
|
+
return {
|
|
466
|
+
opts: {
|
|
467
|
+
abortTimeoutMs: opts.abortTimeoutMs ?? DEFAULT_ABORT_MS,
|
|
468
|
+
fallback: opts.fallback ?? "auto",
|
|
469
|
+
hashTag: opts.hashTag ?? false,
|
|
470
|
+
keyPrefix: opts.keyPrefix ?? DEFAULT_KEY_PREFIX,
|
|
471
|
+
rest: opts
|
|
472
|
+
},
|
|
473
|
+
logger,
|
|
474
|
+
emit,
|
|
475
|
+
conn,
|
|
476
|
+
memEntries: new MemoryStore(),
|
|
477
|
+
memTagStates: /* @__PURE__ */ new Map(),
|
|
478
|
+
resolveNs: () => resolveBuildNamespace(opts.buildNamespace)
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
function entryKey(state, cacheKey) {
|
|
482
|
+
return buildKey(
|
|
483
|
+
`${state.opts.keyPrefix}${ENTRY_SUFFIX}`,
|
|
484
|
+
state.resolveNs(),
|
|
485
|
+
cacheKey,
|
|
486
|
+
state.opts.hashTag
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
function tagMetaKey(state, tag) {
|
|
490
|
+
return `${state.opts.keyPrefix}${TAG_SUFFIX}${tag}`;
|
|
491
|
+
}
|
|
492
|
+
function normalizeTags(tags) {
|
|
493
|
+
return Array.isArray(tags) ? tags.filter((t) => typeof t === "string" && t.length > 0) : [];
|
|
494
|
+
}
|
|
495
|
+
function shouldUseLocalIncrementalStore(tags) {
|
|
496
|
+
return tags.some((t) => t.startsWith(INSTANCE_LOCAL_TAG_PREFIX));
|
|
497
|
+
}
|
|
498
|
+
function extractTagsFromValue(value, ctx) {
|
|
499
|
+
if (!value || typeof value !== "object") return [];
|
|
500
|
+
const v = value;
|
|
501
|
+
if (v.kind === "FETCH") {
|
|
502
|
+
return normalizeTags([
|
|
503
|
+
...normalizeTags(v.tags),
|
|
504
|
+
...normalizeTags(ctx?.tags),
|
|
505
|
+
...normalizeTags(ctx?.softTags)
|
|
506
|
+
]);
|
|
507
|
+
}
|
|
508
|
+
const headers = v.headers;
|
|
509
|
+
if (!headers) return [];
|
|
510
|
+
const tagsHeader = headers["x-next-cache-tags"];
|
|
511
|
+
if (typeof tagsHeader === "string") {
|
|
512
|
+
return tagsHeader.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
|
|
513
|
+
}
|
|
514
|
+
if (Array.isArray(tagsHeader)) {
|
|
515
|
+
return tagsHeader.flatMap((part) => String(part).split(",")).map((t) => t.trim()).filter((t) => t.length > 0);
|
|
516
|
+
}
|
|
517
|
+
return [];
|
|
518
|
+
}
|
|
519
|
+
function isTagStateExpired(state, lastModified, isFetch) {
|
|
520
|
+
if (!state || typeof lastModified !== "number") return false;
|
|
521
|
+
if (typeof state.expired === "number" && state.expired >= lastModified) return true;
|
|
522
|
+
if (isFetch && typeof state.stale === "number" && state.stale >= lastModified) return true;
|
|
523
|
+
return false;
|
|
524
|
+
}
|
|
525
|
+
function normalizeTtlSeconds(data, ctx) {
|
|
526
|
+
if (data && typeof data === "object") {
|
|
527
|
+
const d = data;
|
|
528
|
+
if (d.kind === "FETCH") {
|
|
529
|
+
if (typeof d.revalidate === "number" && d.revalidate > 0) {
|
|
530
|
+
return Math.min(Math.max(Math.ceil(d.revalidate), MIN_TTL_SEC), ONE_YEAR_SEC);
|
|
531
|
+
}
|
|
532
|
+
return ONE_YEAR_SEC;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
const revalidate = ctx?.cacheControl?.revalidate;
|
|
536
|
+
if (revalidate === false || revalidate === void 0) return ONE_YEAR_SEC;
|
|
537
|
+
if (typeof revalidate === "number" && revalidate > 0) {
|
|
538
|
+
return Math.min(Math.max(Math.ceil(revalidate), MIN_TTL_SEC), ONE_YEAR_SEC);
|
|
539
|
+
}
|
|
540
|
+
return MIN_TTL_SEC;
|
|
541
|
+
}
|
|
542
|
+
async function readTagStates(state, tags) {
|
|
543
|
+
if (tags.length === 0) return [];
|
|
544
|
+
const useRedis = gateRedisInit(state);
|
|
545
|
+
if (!useRedis) {
|
|
546
|
+
return tags.map((t) => state.memTagStates.get(t) ?? null);
|
|
547
|
+
}
|
|
548
|
+
try {
|
|
549
|
+
return await withAbortSignal(
|
|
550
|
+
"incremental.readTagStates",
|
|
551
|
+
state.opts.abortTimeoutMs,
|
|
552
|
+
async () => {
|
|
553
|
+
const client = await state.conn.getOrConnect();
|
|
554
|
+
if (!client) return tags.map((t) => state.memTagStates.get(t) ?? null);
|
|
555
|
+
const values = await client.mGet(tags.map((t) => tagMetaKey(state, t)));
|
|
556
|
+
return values.map((v) => {
|
|
557
|
+
if (!v) return null;
|
|
558
|
+
try {
|
|
559
|
+
return JSON.parse(v);
|
|
560
|
+
} catch {
|
|
561
|
+
return null;
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
);
|
|
566
|
+
} catch {
|
|
567
|
+
return tags.map((t) => state.memTagStates.get(t) ?? null);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
var IncrementalRedisCacheHandler = class {
|
|
571
|
+
state;
|
|
572
|
+
revalidatedTags;
|
|
573
|
+
constructor(state, ctx) {
|
|
574
|
+
this.state = state;
|
|
575
|
+
this.revalidatedTags = normalizeTags(ctx?.revalidatedTags);
|
|
576
|
+
}
|
|
577
|
+
async get(cacheKey, ctx) {
|
|
578
|
+
try {
|
|
579
|
+
const requestedTags = normalizeTags([
|
|
580
|
+
...ctx?.tags ?? [],
|
|
581
|
+
...ctx?.softTags ?? []
|
|
582
|
+
]);
|
|
583
|
+
const useLocal = shouldUseLocalIncrementalStore(requestedTags);
|
|
584
|
+
const eKey = entryKey(this.state, cacheKey);
|
|
585
|
+
const useRedis = !useLocal && gateRedis(this.state);
|
|
586
|
+
let raw = null;
|
|
587
|
+
if (useRedis) {
|
|
588
|
+
try {
|
|
589
|
+
raw = await withAbortSignal(
|
|
590
|
+
"incremental.get",
|
|
591
|
+
this.state.opts.abortTimeoutMs,
|
|
592
|
+
async () => {
|
|
593
|
+
const client = await this.state.conn.getOrConnect();
|
|
594
|
+
if (!client) return null;
|
|
595
|
+
return await client.get(eKey);
|
|
596
|
+
}
|
|
597
|
+
);
|
|
598
|
+
} catch (err) {
|
|
599
|
+
if (err instanceof CacheTimeoutError) {
|
|
600
|
+
this.state.emit({ type: "redis.timeout", meta: { op: "incremental.get" } });
|
|
601
|
+
}
|
|
602
|
+
raw = null;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
if (raw === null) raw = this.state.memEntries.get(eKey);
|
|
606
|
+
if (raw === null) {
|
|
607
|
+
this.state.emit({ type: "cache.miss" });
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
610
|
+
const parsed = deserializeCacheRecord(raw);
|
|
611
|
+
if (!parsed || typeof parsed !== "object" || !parsed.value) return null;
|
|
612
|
+
const tags = extractTagsFromValue(parsed.value, ctx);
|
|
613
|
+
if (tags.some((t) => this.revalidatedTags.includes(t))) {
|
|
614
|
+
this.state.emit({ type: "cache.miss", meta: { reason: "revalidated-this-request" } });
|
|
615
|
+
return null;
|
|
616
|
+
}
|
|
617
|
+
const tagStates = await readTagStates(this.state, tags);
|
|
618
|
+
const isFetch = ctx?.kind === "FETCH";
|
|
619
|
+
if (tagStates.some((s) => isTagStateExpired(s, parsed.lastModified, isFetch))) {
|
|
620
|
+
this.state.emit({ type: "cache.miss", meta: { reason: "tag-state-expired" } });
|
|
621
|
+
return null;
|
|
622
|
+
}
|
|
623
|
+
this.state.emit({ type: "cache.hit" });
|
|
624
|
+
return { ...parsed, tags };
|
|
625
|
+
} catch (err) {
|
|
626
|
+
this.state.logger.error("get() unexpected error", {
|
|
627
|
+
message: err.message
|
|
628
|
+
});
|
|
629
|
+
return null;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
async set(cacheKey, data, ctx) {
|
|
633
|
+
if (!data) return;
|
|
634
|
+
try {
|
|
635
|
+
const tags = extractTagsFromValue(data, ctx);
|
|
636
|
+
const useLocal = shouldUseLocalIncrementalStore(tags);
|
|
637
|
+
const ttl = normalizeTtlSeconds(data, ctx);
|
|
638
|
+
const record = { lastModified: Date.now(), value: data };
|
|
639
|
+
const serialized = serializeCacheRecord(record);
|
|
640
|
+
const eKey = entryKey(this.state, cacheKey);
|
|
641
|
+
const useRedis = !useLocal && gateRedis(this.state);
|
|
642
|
+
if (useRedis) {
|
|
643
|
+
try {
|
|
644
|
+
await withAbortSignal(
|
|
645
|
+
"incremental.set",
|
|
646
|
+
this.state.opts.abortTimeoutMs,
|
|
647
|
+
async () => {
|
|
648
|
+
const client = await this.state.conn.getOrConnect();
|
|
649
|
+
if (!client) {
|
|
650
|
+
this.state.memEntries.set(eKey, serialized, ttl);
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
await client.set(eKey, serialized, { EX: ttl });
|
|
654
|
+
}
|
|
655
|
+
);
|
|
656
|
+
this.state.emit({ type: "cache.set", meta: { backend: "redis" } });
|
|
657
|
+
return;
|
|
658
|
+
} catch (err) {
|
|
659
|
+
if (err instanceof CacheTimeoutError) {
|
|
660
|
+
this.state.emit({ type: "redis.timeout", meta: { op: "incremental.set" } });
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
this.state.memEntries.set(eKey, serialized, ttl);
|
|
665
|
+
this.state.emit({ type: "cache.set", meta: { backend: "memory" } });
|
|
666
|
+
} catch (err) {
|
|
667
|
+
this.state.logger.error("set() error", {
|
|
668
|
+
message: err.message
|
|
669
|
+
});
|
|
670
|
+
this.state.emit({ type: "cache.set.failed" });
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
async revalidateTag(tags, durations) {
|
|
674
|
+
const list = normalizeTags([tags].flat());
|
|
675
|
+
if (list.length === 0) return;
|
|
676
|
+
const useLocal = shouldUseLocalIncrementalStore(list);
|
|
677
|
+
const useRedis = !useLocal && gateRedis(this.state);
|
|
678
|
+
const now = Date.now();
|
|
679
|
+
const payload = durations ? {
|
|
680
|
+
stale: now,
|
|
681
|
+
...durations.expire !== void 0 ? { expired: now + durations.expire * 1e3 } : {}
|
|
682
|
+
} : { expired: now };
|
|
683
|
+
for (const t of list) this.state.memTagStates.set(t, payload);
|
|
684
|
+
if (!useRedis) {
|
|
685
|
+
this.state.emit({
|
|
686
|
+
type: payload.expired ? "tag.invalidate.hard" : "tag.invalidate.soft",
|
|
687
|
+
meta: { count: list.length, backend: "memory" }
|
|
688
|
+
});
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
try {
|
|
692
|
+
await withAbortSignal(
|
|
693
|
+
"incremental.revalidateTag",
|
|
694
|
+
this.state.opts.abortTimeoutMs,
|
|
695
|
+
async () => {
|
|
696
|
+
const client = await this.state.conn.getOrConnect();
|
|
697
|
+
if (!client) return;
|
|
698
|
+
await Promise.all(
|
|
699
|
+
list.map(
|
|
700
|
+
(t) => client.set(tagMetaKey(this.state, t), JSON.stringify(payload), {
|
|
701
|
+
EX: ONE_YEAR_SEC
|
|
702
|
+
})
|
|
703
|
+
)
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
);
|
|
707
|
+
this.state.emit({
|
|
708
|
+
type: payload.expired ? "tag.invalidate.hard" : "tag.invalidate.soft",
|
|
709
|
+
meta: { count: list.length, backend: "redis" }
|
|
710
|
+
});
|
|
711
|
+
} catch (err) {
|
|
712
|
+
this.state.logger.warn("revalidateTag() failed", {
|
|
713
|
+
message: err.message
|
|
714
|
+
});
|
|
715
|
+
if (payload.expired !== void 0) throw err;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
resetRequestCache() {
|
|
719
|
+
}
|
|
720
|
+
};
|
|
721
|
+
function createIncrementalCacheHandler(opts) {
|
|
722
|
+
const state = init(opts);
|
|
723
|
+
class BoundIncrementalCacheHandler extends IncrementalRedisCacheHandler {
|
|
724
|
+
constructor(ctx) {
|
|
725
|
+
super(state, ctx);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
return BoundIncrementalCacheHandler;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
exports.IncrementalRedisCacheHandler = IncrementalRedisCacheHandler;
|
|
732
|
+
exports.createIncrementalCacheHandler = createIncrementalCacheHandler;
|
|
733
|
+
//# sourceMappingURL=index.cjs.map
|
|
734
|
+
//# sourceMappingURL=index.cjs.map
|