cacheplank 0.1.2 → 0.1.4
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/README.md +106 -22
- package/dist/cache-handler.cjs +340 -0
- package/dist/cache-handler.cjs.map +7 -0
- package/dist/cache-handler.d.ts +4 -0
- package/dist/cache-handler.mjs +313 -0
- package/dist/cache-handler.mjs.map +7 -0
- package/dist/index.cjs +205 -102
- package/dist/index.cjs.map +2 -2
- package/dist/index.d.ts +60 -4
- package/dist/index.mjs +203 -101
- package/dist/index.mjs.map +2 -2
- package/package.json +7 -1
package/dist/index.mjs
CHANGED
|
@@ -1,36 +1,36 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { createClient } from "@libsql/client";
|
|
3
|
-
var TAG_CHECK_TTL_MS = (() => {
|
|
4
|
-
const configured = Number(process.env.CACHEPLANK_STAMPS_TTL_MS);
|
|
5
|
-
return Number.isFinite(configured) && configured >= 0 ? configured : 3e3;
|
|
6
|
-
})();
|
|
7
3
|
var RETRY_BACKOFF_MS = 3e4;
|
|
8
4
|
var DEFAULT_MAX_ENTRIES = 2e3;
|
|
5
|
+
var DEFAULT_STAMPS_TTL_MS = 3e3;
|
|
6
|
+
var DEFAULT_STAMPS_RETENTION_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
9
7
|
var now = () => Date.now();
|
|
10
|
-
function
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
8
|
+
function envNumber(name) {
|
|
9
|
+
const raw = process.env[name]?.trim();
|
|
10
|
+
if (!raw) return void 0;
|
|
11
|
+
const value = Number(raw);
|
|
12
|
+
return Number.isFinite(value) ? value : void 0;
|
|
13
|
+
}
|
|
14
|
+
function resolveOptions(options) {
|
|
15
|
+
const configuredMax = envNumber("CACHEPLANK_MAX_ENTRIES");
|
|
16
|
+
const configuredTtl = envNumber("CACHEPLANK_STAMPS_TTL_MS");
|
|
17
|
+
const configuredRetention = envNumber("CACHEPLANK_STAMPS_RETENTION_MS");
|
|
18
|
+
return {
|
|
19
|
+
url: options.url ?? process.env.CACHEPLANK_URL ?? process.env.BUNNY_DATABASE_URL,
|
|
20
|
+
authToken: options.authToken ?? process.env.CACHEPLANK_AUTH_TOKEN ?? process.env.BUNNY_DATABASE_AUTH_TOKEN,
|
|
21
|
+
prefix: options.prefix ?? process.env.CACHEPLANK_PREFIX ?? "",
|
|
22
|
+
// `>= 0` so `CACHEPLANK_MAX_ENTRIES=0` disables entry storage, mirroring
|
|
23
|
+
// next's own `maxSize === 0` no-op handler; blank/garbage falls back.
|
|
24
|
+
maxEntries: options.maxEntries ?? (configuredMax !== void 0 && configuredMax >= 0 ? configuredMax : DEFAULT_MAX_ENTRIES),
|
|
25
|
+
tagCheckTtlMs: options.stampsTtlMs ?? (configuredTtl !== void 0 && configuredTtl >= 0 ? configuredTtl : DEFAULT_STAMPS_TTL_MS),
|
|
26
|
+
// Window reads need a positive horizon; zero/blank/garbage → the default.
|
|
27
|
+
stampsRetentionMs: options.stampsRetentionMs ?? (configuredRetention !== void 0 && configuredRetention > 0 ? configuredRetention : DEFAULT_STAMPS_RETENTION_MS),
|
|
28
|
+
warn: options.warn ?? ((message) => console.warn(`[cacheplank] ${message}`))
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function createStampStore(opts) {
|
|
29
32
|
const stamps = /* @__PURE__ */ new Map();
|
|
30
33
|
let stampsSyncedAt = 0;
|
|
31
|
-
function observeStamp(tag, at) {
|
|
32
|
-
if (at > (stamps.get(tag) ?? 0)) stamps.set(tag, at);
|
|
33
|
-
}
|
|
34
34
|
let client;
|
|
35
35
|
let ready;
|
|
36
36
|
let retryAt = 0;
|
|
@@ -38,22 +38,28 @@ function createCacheHandler(options = {}) {
|
|
|
38
38
|
function warnOnce(message) {
|
|
39
39
|
if (!warned) {
|
|
40
40
|
warned = true;
|
|
41
|
-
warn(message);
|
|
41
|
+
opts.warn(message);
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
|
+
function observe(tag, at) {
|
|
45
|
+
if (at > (stamps.get(tag) ?? 0)) stamps.set(tag, at);
|
|
46
|
+
}
|
|
44
47
|
function ensureStore() {
|
|
45
48
|
if (ready) return ready;
|
|
46
|
-
if (!url) {
|
|
49
|
+
if (!opts.url) {
|
|
47
50
|
warnOnce("no CACHEPLANK_URL / BUNNY_DATABASE_URL set; tag stamps are process-local only");
|
|
48
51
|
return Promise.resolve(false);
|
|
49
52
|
}
|
|
50
53
|
if (now() < retryAt) return Promise.resolve(false);
|
|
51
54
|
ready = (async () => {
|
|
52
55
|
try {
|
|
53
|
-
const created = createClient({ url, authToken });
|
|
56
|
+
const created = createClient({ url: opts.url, authToken: opts.authToken });
|
|
54
57
|
await created.execute(
|
|
55
58
|
"CREATE TABLE IF NOT EXISTS tag_stamps (tag TEXT PRIMARY KEY, revalidated_at INTEGER NOT NULL)"
|
|
56
59
|
);
|
|
60
|
+
await created.execute(
|
|
61
|
+
"CREATE INDEX IF NOT EXISTS tag_stamps_revalidated_at ON tag_stamps(revalidated_at)"
|
|
62
|
+
);
|
|
57
63
|
client = created;
|
|
58
64
|
return true;
|
|
59
65
|
} catch (error) {
|
|
@@ -65,26 +71,88 @@ function createCacheHandler(options = {}) {
|
|
|
65
71
|
})();
|
|
66
72
|
return ready;
|
|
67
73
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (
|
|
74
|
+
let syncing;
|
|
75
|
+
function sync() {
|
|
76
|
+
if (now() - stampsSyncedAt < opts.tagCheckTtlMs) return Promise.resolve();
|
|
77
|
+
if (syncing) return syncing;
|
|
78
|
+
stampsSyncedAt = now();
|
|
79
|
+
syncing = (async () => {
|
|
80
|
+
try {
|
|
81
|
+
if (!await ensureStore() || !client) return;
|
|
82
|
+
const result = await client.execute({
|
|
83
|
+
sql: "SELECT tag, revalidated_at FROM tag_stamps WHERE revalidated_at > ?",
|
|
84
|
+
args: [now() - opts.stampsRetentionMs]
|
|
85
|
+
});
|
|
86
|
+
for (const row of result.rows) {
|
|
87
|
+
const at = Number(row.revalidated_at);
|
|
88
|
+
if (Number.isFinite(at)) observe(String(row.tag), at);
|
|
89
|
+
}
|
|
90
|
+
} catch (error) {
|
|
91
|
+
retryAt = now() + RETRY_BACKOFF_MS;
|
|
92
|
+
warnOnce(`tag-stamp read failed; serving local state (${error.message})`);
|
|
93
|
+
} finally {
|
|
94
|
+
syncing = void 0;
|
|
95
|
+
}
|
|
96
|
+
})();
|
|
97
|
+
return syncing;
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
stamps,
|
|
101
|
+
sync,
|
|
102
|
+
async expiration(tags) {
|
|
103
|
+
await sync();
|
|
104
|
+
let max = 0;
|
|
105
|
+
for (const tag of tags) {
|
|
106
|
+
const at = stamps.get(tag) ?? 0;
|
|
107
|
+
if (at > max) max = at;
|
|
108
|
+
}
|
|
109
|
+
return max;
|
|
110
|
+
},
|
|
111
|
+
async update(tags) {
|
|
112
|
+
const at = now();
|
|
113
|
+
for (const tag of tags) observe(tag, at);
|
|
114
|
+
if (tags.length === 0) return;
|
|
115
|
+
try {
|
|
116
|
+
if (!await ensureStore() || !client) return;
|
|
117
|
+
await client.batch(
|
|
118
|
+
tags.map((tag) => ({
|
|
119
|
+
sql: "INSERT INTO tag_stamps(tag, revalidated_at) VALUES (?, ?) ON CONFLICT(tag) DO UPDATE SET revalidated_at = MAX(revalidated_at, excluded.revalidated_at)",
|
|
120
|
+
args: [tag, at]
|
|
121
|
+
})),
|
|
122
|
+
"write"
|
|
123
|
+
);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
warnOnce(`tag-stamp write failed; local state kept (${error.message})`);
|
|
76
126
|
}
|
|
77
|
-
} catch (error) {
|
|
78
|
-
retryAt = now() + RETRY_BACKOFF_MS;
|
|
79
|
-
warnOnce(`tag-stamp read failed; serving local state (${error.message})`);
|
|
80
|
-
} finally {
|
|
81
|
-
stampsSyncedAt = now();
|
|
82
127
|
}
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function remember(entries, key, value, max) {
|
|
131
|
+
entries.delete(key);
|
|
132
|
+
entries.set(key, value);
|
|
133
|
+
while (entries.size > max) {
|
|
134
|
+
const oldest = entries.keys().next().value;
|
|
135
|
+
if (oldest === void 0) break;
|
|
136
|
+
entries.delete(oldest);
|
|
83
137
|
}
|
|
138
|
+
}
|
|
139
|
+
function valueTags(value) {
|
|
140
|
+
if (!value) return [];
|
|
141
|
+
const tags = value.tags;
|
|
142
|
+
if (Array.isArray(tags)) return tags.filter((t) => typeof t === "string");
|
|
143
|
+
const headers = value.headers;
|
|
144
|
+
const header = headers?.["x-next-cache-tags"];
|
|
145
|
+
if (typeof header === "string") return header.split(",");
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
function createCacheHandler(options = {}) {
|
|
149
|
+
const opts = resolveOptions(options);
|
|
150
|
+
const store = createStampStore(opts);
|
|
151
|
+
const entries = /* @__PURE__ */ new Map();
|
|
84
152
|
const pendingSets = /* @__PURE__ */ new Map();
|
|
85
153
|
return {
|
|
86
154
|
async get(cacheKey, softTags) {
|
|
87
|
-
const key = prefix + cacheKey;
|
|
155
|
+
const key = opts.prefix + cacheKey;
|
|
88
156
|
const pending = pendingSets.get(key);
|
|
89
157
|
if (pending) await pending.catch(() => {
|
|
90
158
|
});
|
|
@@ -97,16 +165,18 @@ function createCacheHandler(options = {}) {
|
|
|
97
165
|
return void 0;
|
|
98
166
|
}
|
|
99
167
|
const age = now() - entry.timestamp;
|
|
100
|
-
const
|
|
101
|
-
|
|
168
|
+
const dev = Boolean(process.env.__NEXT_DEV_SERVER);
|
|
169
|
+
const maxAgeSeconds = dev ? Math.max(entry.expire, 300) : entry.revalidate;
|
|
170
|
+
const expireBoundSeconds = dev ? Math.max(entry.expire, 300) : entry.expire;
|
|
171
|
+
if (!(age < maxAgeSeconds * 1e3) || entry.expire >= 0 && !(age < expireBoundSeconds * 1e3)) {
|
|
102
172
|
return void 0;
|
|
103
173
|
}
|
|
104
|
-
await
|
|
174
|
+
await store.sync();
|
|
105
175
|
for (const tag of entry.tags) {
|
|
106
|
-
if ((stamps.get(tag) ?? 0) >= entry.timestamp) return void 0;
|
|
176
|
+
if ((store.stamps.get(tag) ?? 0) >= entry.timestamp) return void 0;
|
|
107
177
|
}
|
|
108
178
|
for (const tag of softTags) {
|
|
109
|
-
if ((stamps.get(tag) ?? 0) >= entry.timestamp) return void 0;
|
|
179
|
+
if ((store.stamps.get(tag) ?? 0) >= entry.timestamp) return void 0;
|
|
110
180
|
}
|
|
111
181
|
const bytes = entry.bytes;
|
|
112
182
|
return {
|
|
@@ -124,7 +194,7 @@ function createCacheHandler(options = {}) {
|
|
|
124
194
|
};
|
|
125
195
|
},
|
|
126
196
|
async set(cacheKey, pendingEntry) {
|
|
127
|
-
const key = prefix + cacheKey;
|
|
197
|
+
const key = opts.prefix + cacheKey;
|
|
128
198
|
let release = () => {
|
|
129
199
|
};
|
|
130
200
|
const gate = new Promise((resolve) => {
|
|
@@ -134,76 +204,108 @@ function createCacheHandler(options = {}) {
|
|
|
134
204
|
try {
|
|
135
205
|
const entry = await pendingEntry;
|
|
136
206
|
if (entry.expire === 0 && !process.env.__NEXT_DEV_SERVER) return;
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
const bytes = new Uint8Array(size);
|
|
153
|
-
let offset = 0;
|
|
154
|
-
for (const chunk of chunks) {
|
|
155
|
-
bytes.set(chunk, offset);
|
|
156
|
-
offset += chunk.byteLength;
|
|
157
|
-
}
|
|
158
|
-
remember(key, {
|
|
159
|
-
bytes,
|
|
160
|
-
tags: entry.tags ?? [],
|
|
161
|
-
timestamp: entry.timestamp,
|
|
162
|
-
expire: entry.expire,
|
|
163
|
-
revalidate: entry.revalidate,
|
|
164
|
-
stale: entry.stale
|
|
165
|
-
});
|
|
207
|
+
const bytes = await drainStream(entry.value);
|
|
208
|
+
remember(
|
|
209
|
+
entries,
|
|
210
|
+
key,
|
|
211
|
+
{
|
|
212
|
+
bytes,
|
|
213
|
+
tags: entry.tags ?? [],
|
|
214
|
+
timestamp: entry.timestamp,
|
|
215
|
+
expire: entry.expire,
|
|
216
|
+
revalidate: entry.revalidate,
|
|
217
|
+
stale: entry.stale
|
|
218
|
+
},
|
|
219
|
+
opts.maxEntries
|
|
220
|
+
);
|
|
166
221
|
} catch (error) {
|
|
167
|
-
|
|
222
|
+
opts.warn(`failed to buffer a cache entry (${error.message})`);
|
|
168
223
|
} finally {
|
|
169
224
|
release();
|
|
170
225
|
pendingSets.delete(key);
|
|
171
226
|
}
|
|
172
227
|
},
|
|
173
228
|
async refreshTags() {
|
|
174
|
-
await
|
|
229
|
+
await store.sync();
|
|
175
230
|
},
|
|
176
231
|
async getExpiration(tags) {
|
|
177
|
-
|
|
178
|
-
let max = 0;
|
|
179
|
-
for (const tag of tags) {
|
|
180
|
-
const at = stamps.get(tag) ?? 0;
|
|
181
|
-
if (at > max) max = at;
|
|
182
|
-
}
|
|
183
|
-
return max;
|
|
232
|
+
return store.expiration(tags);
|
|
184
233
|
},
|
|
185
234
|
async updateTags(tags, _durations) {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
235
|
+
await store.update(tags);
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function createIncrementalCacheHandler(options = {}) {
|
|
240
|
+
const opts = resolveOptions(options);
|
|
241
|
+
const store = createStampStore(opts);
|
|
242
|
+
const entries = /* @__PURE__ */ new Map();
|
|
243
|
+
return class CacheplankIncrementalCacheHandler {
|
|
244
|
+
// Next only ever passes its own context; we read none of it here (the
|
|
245
|
+
// meaningful context is supplied per call to `get`/`set`).
|
|
246
|
+
constructor(_ctx) {
|
|
247
|
+
}
|
|
248
|
+
async get(cacheKey, ctx) {
|
|
249
|
+
const key = opts.prefix + cacheKey;
|
|
250
|
+
const entry = entries.get(key);
|
|
251
|
+
if (!entry) return null;
|
|
252
|
+
entries.delete(key);
|
|
253
|
+
entries.set(key, entry);
|
|
254
|
+
await store.sync();
|
|
255
|
+
const tags = valueTags(entry.value);
|
|
256
|
+
if (ctx.tags) tags.push(...ctx.tags);
|
|
257
|
+
if (ctx.softTags) tags.push(...ctx.softTags);
|
|
258
|
+
for (const tag of tags) {
|
|
259
|
+
if ((store.stamps.get(tag) ?? 0) >= entry.lastModified) {
|
|
260
|
+
entries.delete(key);
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return { lastModified: entry.lastModified, value: entry.value };
|
|
265
|
+
}
|
|
266
|
+
async set(cacheKey, data, _ctx) {
|
|
267
|
+
const key = opts.prefix + cacheKey;
|
|
268
|
+
if (data == null) {
|
|
269
|
+
entries.delete(key);
|
|
270
|
+
return;
|
|
200
271
|
}
|
|
272
|
+
remember(entries, key, { lastModified: now(), value: data }, opts.maxEntries);
|
|
273
|
+
}
|
|
274
|
+
async revalidateTag(tags, _durations) {
|
|
275
|
+
await store.update(typeof tags === "string" ? [tags] : tags);
|
|
276
|
+
}
|
|
277
|
+
resetRequestCache() {
|
|
201
278
|
}
|
|
202
279
|
};
|
|
203
280
|
}
|
|
281
|
+
async function drainStream(stream) {
|
|
282
|
+
const reader = stream.getReader();
|
|
283
|
+
const chunks = [];
|
|
284
|
+
let size = 0;
|
|
285
|
+
try {
|
|
286
|
+
for (; ; ) {
|
|
287
|
+
const { done, value } = await reader.read();
|
|
288
|
+
if (done) break;
|
|
289
|
+
if (value) {
|
|
290
|
+
chunks.push(value);
|
|
291
|
+
size += value.byteLength;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
} finally {
|
|
295
|
+
reader.releaseLock();
|
|
296
|
+
}
|
|
297
|
+
const bytes = new Uint8Array(size);
|
|
298
|
+
let offset = 0;
|
|
299
|
+
for (const chunk of chunks) {
|
|
300
|
+
bytes.set(chunk, offset);
|
|
301
|
+
offset += chunk.byteLength;
|
|
302
|
+
}
|
|
303
|
+
return bytes;
|
|
304
|
+
}
|
|
204
305
|
var index_default = createCacheHandler();
|
|
205
306
|
export {
|
|
206
307
|
createCacheHandler,
|
|
308
|
+
createIncrementalCacheHandler,
|
|
207
309
|
index_default as default
|
|
208
310
|
};
|
|
209
311
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/index.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * cacheplank \u2014 \"the shared plank your pods walk across.\"\n *\n * A single-file, tag-based distributed cache handler for Next.js 16's\n * `cacheHandlers` API, backed by libSQL.\n *\n * Cache *entries* stay local to each process (an in-memory LRU). Only tag\n * *invalidation* is shared, through one tiny libSQL table (`tag_stamps`). Any\n * number of pods/regions converge because every `get` compares the entry's\n * timestamp against the shared invalidation stamps.\n *\n * Ground truth for the contract below: the installed `next@16.3.8` package \u2014\n * `dist/server/lib/cache-handlers/{types,default}.js`,\n * `dist/server/lib/incremental-cache/tags-manifest.external.js`, and\n * `dist/server/use-cache/{use-cache-wrapper,handlers}.js`. See README\n * \"Compatibility notes\".\n */\nimport { createClient, type Client } from '@libsql/client';\n\n/* ------------------------------------------------------------------------- *\n * Next 16 cache-handler contract. *\n * Hand-written: Next does not export these types publicly (README note #1). *\n * ------------------------------------------------------------------------- */\n\n/** A timestamp in milliseconds elapsed since the epoch. */\nexport type Timestamp = number;\n\n/** Mirrors `CacheEntry` in next@16.3.8. Durations are in seconds. */\nexport interface CacheEntry {\n /** The stored value. May be partially written / error while pending. */\n value: ReadableStream<Uint8Array>;\n /** Tags configured for the entry, excluding soft tags. */\n tags: string[];\n /** Client hint only; not used to compute expiration. [seconds] */\n stale: number;\n /** When the entry was created. [ms epoch] */\n timestamp: Timestamp;\n /** How long the entry may be used. [seconds] */\n expire: number;\n /** How long until the entry should be revalidated. [seconds] */\n revalidate: number;\n}\n\n/** Mirrors `CacheHandler` in next@16.3.8. */\nexport interface CacheHandler {\n get(cacheKey: string, softTags: string[]): Promise<CacheEntry | undefined>;\n set(cacheKey: string, pendingEntry: Promise<CacheEntry>): Promise<void>;\n refreshTags(): Promise<void>;\n getExpiration(tags: string[]): Promise<Timestamp>;\n updateTags(tags: string[], durations?: { expire?: number }): Promise<void>;\n}\n\nexport interface CacheplankOptions {\n /** libSQL URL. Defaults to `CACHEPLANK_URL`, then `BUNNY_DATABASE_URL`. */\n url?: string;\n /** libSQL auth token. Defaults to `CACHEPLANK_AUTH_TOKEN`, then `BUNNY_DATABASE_AUTH_TOKEN`. */\n authToken?: string;\n /** Key namespace. Defaults to `CACHEPLANK_PREFIX` (may be empty). */\n prefix?: string;\n /** Max local entries. Defaults to `CACHEPLANK_MAX_ENTRIES`, else 2000. */\n maxEntries?: number;\n /**\n * How long an in-process tag-stamp memo may be reused before the shared table\n * is re-read. This bounds how stale a pod's view of invalidation can get.\n * Defaults to `CACHEPLANK_STAMPS_TTL_MS`, else 3000. [ms]\n */\n stampsTtlMs?: number;\n /** Sink for the single fail-open warning. Defaults to `console.warn`. */\n warn?: (message: string) => void;\n}\n\ninterface StoredEntry {\n bytes: Uint8Array;\n tags: string[];\n timestamp: number;\n expire: number;\n revalidate: number;\n stale: number;\n}\n\n/**\n * Soft TTL for the in-process tag-stamp memo. [ms]. Defaults to 3000; override\n * with `CACHEPLANK_STAMPS_TTL_MS` (e.g. `0` to read the shared table on every\n * `get`, which the cross-process test relies on).\n */\nconst TAG_CHECK_TTL_MS = (() => {\n const configured = Number(process.env.CACHEPLANK_STAMPS_TTL_MS);\n return Number.isFinite(configured) && configured >= 0 ? configured : 3000;\n})();\n/** Fail-open backoff after a stamp-store error, so we don't hammer a dead DB. [ms] */\nconst RETRY_BACKOFF_MS = 30_000;\nconst DEFAULT_MAX_ENTRIES = 2000;\n\nconst now = (): number => Date.now();\n\n/**\n * Create a cache handler. The package's `default` export is one of these,\n * configured from the environment.\n */\nexport function createCacheHandler(options: CacheplankOptions = {}): CacheHandler {\n const url = options.url ?? process.env.CACHEPLANK_URL ?? process.env.BUNNY_DATABASE_URL;\n const authToken =\n options.authToken ?? process.env.CACHEPLANK_AUTH_TOKEN ?? process.env.BUNNY_DATABASE_AUTH_TOKEN;\n const prefix = options.prefix ?? process.env.CACHEPLANK_PREFIX ?? '';\n const configuredMax = Number(process.env.CACHEPLANK_MAX_ENTRIES);\n const maxEntries =\n options.maxEntries ??\n (Number.isFinite(configuredMax) && configuredMax > 0 ? configuredMax : DEFAULT_MAX_ENTRIES);\n const configuredTtl = Number(process.env.CACHEPLANK_STAMPS_TTL_MS);\n const tagCheckTtlMs =\n options.stampsTtlMs ??\n (Number.isFinite(configuredTtl) && configuredTtl >= 0 ? configuredTtl : TAG_CHECK_TTL_MS);\n const warn = options.warn ?? ((message: string) => console.warn(`[cacheplank] ${message}`));\n\n // ---- local entry store, LRU via Map insertion order ----\n const entries = new Map<string, StoredEntry>();\n function remember(key: string, entry: StoredEntry): void {\n entries.delete(key);\n entries.set(key, entry);\n while (entries.size > maxEntries) {\n const oldest = entries.keys().next().value;\n if (oldest === undefined) break;\n entries.delete(oldest);\n }\n }\n\n // ---- shared tag stamps (in-process memo; monotonically increasing) ----\n const stamps = new Map<string, number>();\n let stampsSyncedAt = 0;\n function observeStamp(tag: string, at: number): void {\n if (at > (stamps.get(tag) ?? 0)) stamps.set(tag, at);\n }\n\n // ---- libSQL client: lazy, memoized, and never allowed to throw ----\n let client: Client | undefined;\n let ready: Promise<boolean> | undefined;\n let retryAt = 0;\n let warned = false;\n function warnOnce(message: string): void {\n if (!warned) {\n warned = true;\n warn(message);\n }\n }\n\n function ensureStore(): Promise<boolean> {\n if (ready) return ready;\n if (!url) {\n warnOnce('no CACHEPLANK_URL / BUNNY_DATABASE_URL set; tag stamps are process-local only');\n return Promise.resolve(false);\n }\n if (now() < retryAt) return Promise.resolve(false);\n ready = (async () => {\n try {\n const created = createClient({ url, authToken });\n await created.execute(\n 'CREATE TABLE IF NOT EXISTS tag_stamps (tag TEXT PRIMARY KEY, revalidated_at INTEGER NOT NULL)',\n );\n client = created;\n return true;\n } catch (error) {\n retryAt = now() + RETRY_BACKOFF_MS;\n ready = undefined;\n warnOnce(`tag-stamp store unavailable; continuing fail-open (${(error as Error).message})`);\n return false;\n }\n })();\n return ready;\n }\n\n async function syncStamps(): Promise<void> {\n if (now() - stampsSyncedAt < tagCheckTtlMs) return;\n try {\n if (!(await ensureStore()) || !client) return;\n const result = await client.execute('SELECT tag, revalidated_at FROM tag_stamps');\n for (const row of result.rows) {\n const at = Number(row.revalidated_at);\n if (Number.isFinite(at)) observeStamp(String(row.tag), at);\n }\n } catch (error) {\n retryAt = now() + RETRY_BACKOFF_MS;\n warnOnce(`tag-stamp read failed; serving local state (${(error as Error).message})`);\n } finally {\n stampsSyncedAt = now();\n }\n }\n\n // In-flight `set` calls, so a `get` racing a `set` waits instead of missing.\n const pendingSets = new Map<string, Promise<void>>();\n\n return {\n async get(cacheKey, softTags) {\n const key = prefix + cacheKey;\n const pending = pendingSets.get(key);\n if (pending) await pending.catch(() => {});\n\n const entry = entries.get(key);\n if (!entry) return undefined;\n\n // Touch for LRU recency.\n entries.delete(key);\n entries.set(key, entry);\n\n // A negative `expire` is the tiered-cache eviction sentinel Next's tiered\n // handler uses in dev (the interface has no per-key delete); treat it as\n // missing, independently of the retention bounds below.\n if (entry.expire < 0) {\n entries.delete(key);\n return undefined;\n }\n // Mirror Next's effective expiry. `set` already dropped `expire: 0` in\n // production. The wrapper discards an entry once\n // `currentTime > timestamp + expire*1000` (always) or past `revalidate`\n // during static generation; the default handler additionally drops past\n // `revalidate` in production (SWR background revalidation then takes\n // over). Dropping at either bound here matches what Next would reject\n // anyway and never serves a value the wrapper throws away. In dev the\n // default handler retains short-`expire` entries for at least\n // MIN_PRERENDERABLE_EXPIRE (300s) so reloads hit; mirror that. A\n // `revalidate <= 0` (including the stale-while-revalidate `-1`) drops.\n const age = now() - entry.timestamp;\n const maxAgeSeconds = process.env.__NEXT_DEV_SERVER\n ? Math.max(entry.expire, 300)\n : entry.revalidate;\n if (!(age < maxAgeSeconds * 1000) || (entry.expire >= 0 && !(age < entry.expire * 1000))) {\n return undefined;\n }\n\n // Shared invalidation: any tag (own or route soft tag) stamped at or\n // after this entry was written means another pod revalidated it \u2014 miss,\n // so Next regenerates. `>=` mirrors Next's wrapper-side discard\n // (`entry.timestamp <= implicitTagsExpiration`): for an entry created in\n // the same millisecond as a stamp the ordering is unknowable, so we\n // conservatively regenerate.\n await syncStamps();\n for (const tag of entry.tags) {\n if ((stamps.get(tag) ?? 0) >= entry.timestamp) return undefined;\n }\n for (const tag of softTags) {\n if ((stamps.get(tag) ?? 0) >= entry.timestamp) return undefined;\n }\n\n const bytes = entry.bytes;\n return {\n value: new ReadableStream<Uint8Array>({\n start(controller) {\n controller.enqueue(bytes);\n controller.close();\n },\n }),\n tags: entry.tags,\n stale: entry.stale,\n timestamp: entry.timestamp,\n expire: entry.expire,\n revalidate: entry.revalidate,\n };\n },\n\n async set(cacheKey, pendingEntry) {\n const key = prefix + cacheKey;\n let release: () => void = () => {};\n const gate = new Promise<void>((resolve) => {\n release = resolve;\n });\n pendingSets.set(key, gate);\n try {\n const entry = await pendingEntry;\n // In production an `expire: 0` entry is dynamic and never served back.\n if (entry.expire === 0 && !process.env.__NEXT_DEV_SERVER) return;\n\n const reader = entry.value.getReader();\n const chunks: Uint8Array[] = [];\n let size = 0;\n try {\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n if (value) {\n chunks.push(value);\n size += value.byteLength;\n }\n }\n } finally {\n reader.releaseLock();\n }\n const bytes = new Uint8Array(size);\n let offset = 0;\n for (const chunk of chunks) {\n bytes.set(chunk, offset);\n offset += chunk.byteLength;\n }\n\n remember(key, {\n bytes,\n tags: entry.tags ?? [],\n timestamp: entry.timestamp,\n expire: entry.expire,\n revalidate: entry.revalidate,\n stale: entry.stale,\n });\n } catch (error) {\n warnOnce(`failed to buffer a cache entry (${(error as Error).message})`);\n } finally {\n release();\n pendingSets.delete(key);\n }\n },\n\n async refreshTags() {\n await syncStamps();\n },\n\n async getExpiration(tags) {\n await syncStamps();\n let max = 0;\n for (const tag of tags) {\n const at = stamps.get(tag) ?? 0;\n if (at > max) max = at;\n }\n return max;\n },\n\n async updateTags(tags, _durations) {\n // `durations.expire` is accepted but not persisted: the shared table holds\n // a single monotone stamp per tag. A profile'd `revalidateTag` therefore\n // invalidates immediately rather than stale-while-revalidate \u2014 safe, and\n // faithful to the one-table model. See README compatibility note #4.\n const at = now();\n for (const tag of tags) observeStamp(tag, at);\n if (tags.length === 0) return;\n try {\n if (!(await ensureStore()) || !client) return;\n await client.batch(\n tags.map((tag) => ({\n sql:\n 'INSERT INTO tag_stamps(tag, revalidated_at) VALUES (?, ?) ' +\n 'ON CONFLICT(tag) DO UPDATE SET revalidated_at = MAX(revalidated_at, excluded.revalidated_at)',\n args: [tag, at],\n })),\n 'write',\n );\n } catch (error) {\n warnOnce(`tag-stamp write failed; local state kept (${(error as Error).message})`);\n }\n },\n };\n}\n\n/**\n * Default singleton. Next loads the module via `interopDefault`, so this\n * `default` export is the handler Next uses.\n */\nexport default createCacheHandler();\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["/**\n * cacheplank \u2014 \"the shared plank your pods walk across.\"\n *\n * A single-file, tag-based distributed cache handler for Next.js 16, backed by\n * libSQL.\n *\n * Cache *entries* stay local to each process (an in-memory LRU). Only tag\n * *invalidation* is shared, through one tiny libSQL table (`tag_stamps`). Any\n * number of pods/regions converge because every `get` compares the entry's\n * timestamp against the shared invalidation stamps.\n *\n * Two handlers, one invariant. Next 16 exposes two distinct, unrelated cache\n * interfaces and resolves them from two different config keys:\n *\n * 1. `cacheHandlers` (plural) \u2014 a five-method API used for `\"use cache\"` /\n * fetch-path entries. cacheplank's `default` export implements this.\n * 2. `cacheHandler` (singular) \u2014 the legacy incremental/ISR handler\n * (`get`/`set`/`revalidateTag`/`resetRequestCache`) that sits under\n * prerendered `APP_PAGE` / `APP_ROUTE` / `PAGES` routes and the fetch\n * cache. The `cacheplank/cache-handler` entry implements this.\n *\n * Both share the same `tag_stamps` table and the same convergence rule, so a\n * `revalidateTag` from any pod invalidates both kinds of entry everywhere.\n *\n * Ground truth for the contracts below: the installed `next@16.3.8` package \u2014\n * `dist/server/lib/cache-handlers/{types,default}.js`,\n * `dist/server/lib/incremental-cache/{index,file-system-cache}.js`,\n * `dist/server/lib/incremental-cache/tags-manifest.external.js`, and\n * `dist/server/use-cache/{use-cache-wrapper,handlers}.js`. See README\n * \"Compatibility notes\".\n */\nimport { createClient, type Client } from '@libsql/client';\n\n/* ------------------------------------------------------------------------- *\n * Next 16 `cacheHandlers` (plural) contract \u2014 the five-method, `use cache` API.*\n * Hand-written: Next does not export these types publicly (README note #1). *\n * ------------------------------------------------------------------------- */\n\n/** A timestamp in milliseconds elapsed since the epoch. */\nexport type Timestamp = number;\n\n/** Mirrors `CacheEntry` in next@16.3.8. Durations are in seconds. */\nexport interface CacheEntry {\n /** The stored value. May be partially written / error while pending. */\n value: ReadableStream<Uint8Array>;\n /** Tags configured for the entry, excluding soft tags. */\n tags: string[];\n /** Client hint only; not used to compute expiration. [seconds] */\n stale: number;\n /** When the entry was created. [ms epoch] */\n timestamp: Timestamp;\n /** How long the entry may be used. [seconds] */\n expire: number;\n /** How long until the entry should be revalidated. [seconds] */\n revalidate: number;\n}\n\n/** Mirrors `CacheHandler` in next@16.3.8. */\nexport interface CacheHandler {\n get(cacheKey: string, softTags: string[]): Promise<CacheEntry | undefined>;\n set(cacheKey: string, pendingEntry: Promise<CacheEntry>): Promise<void>;\n refreshTags(): Promise<void>;\n getExpiration(tags: string[]): Promise<Timestamp>;\n updateTags(tags: string[], durations?: { expire?: number }): Promise<void>;\n}\n\n/* ------------------------------------------------------------------------- *\n * Next 16 `cacheHandler` (singular) contract \u2014 the legacy incremental/ISR API. *\n * Mirrors `CacheHandler`/`CacheHandlerValue` in *\n * `next/dist/server/lib/incremental-cache/index.d.ts`. Values are opaque *\n * JSON-ish objects (Buffers/headers/segment maps); we never introspect them, *\n * only the tags they carry. *\n * ------------------------------------------------------------------------- */\n\n/** Mirrors `GetIncremental*Context` (union) in next@16.3.8 \u2014 the fields we read. */\nexport interface IncrementalCacheContext {\n kind: string;\n route?: string;\n revalidate?: number;\n /** Fetch-cache entry tags (only for `kind: 'FETCH'`). */\n tags?: string[];\n /** Route-path-derived tags Next passes for staleness checks. */\n softTags?: string[];\n fetchCache?: boolean;\n isFallback?: boolean;\n isRoutePPREnabled?: boolean;\n}\n\n/** A stored incremental value. Opaque to us beyond `kind` and any tags. */\nexport interface IncrementalCacheValue {\n kind: string;\n}\n\n/** Mirrors `CacheHandlerValue` in next@16.3.8. */\nexport interface IncrementalCacheHandlerValue {\n lastModified: number;\n age?: number;\n cacheState?: string;\n value: IncrementalCacheValue | null;\n}\n\n/** Mirrors the singular `CacheHandler` class in next@16.3.8. */\nexport interface IncrementalCacheHandler {\n get(cacheKey: string, ctx: IncrementalCacheContext): Promise<IncrementalCacheHandlerValue | null>;\n set(cacheKey: string, data: IncrementalCacheValue | null, ctx: IncrementalCacheContext): Promise<void>;\n revalidateTag(tags: string | string[], durations?: { expire?: number }): Promise<void>;\n resetRequestCache(): void;\n}\n\n/* ------------------------------------------------------------------------- *\n * Options. *\n * ------------------------------------------------------------------------- */\n\nexport interface CacheplankOptions {\n /** libSQL URL. Defaults to `CACHEPLANK_URL`, then `BUNNY_DATABASE_URL`. */\n url?: string;\n /** libSQL auth token. Defaults to `CACHEPLANK_AUTH_TOKEN`, then `BUNNY_DATABASE_AUTH_TOKEN`. */\n authToken?: string;\n /** Key namespace. Defaults to `CACHEPLANK_PREFIX` (may be empty). */\n prefix?: string;\n /** Max local entries. Defaults to `CACHEPLANK_MAX_ENTRIES`, else 2000. */\n maxEntries?: number;\n /**\n * How long an in-process tag-stamp memo may be reused before the shared table\n * is re-read. This bounds how stale a pod's view of invalidation can get.\n * Defaults to `CACHEPLANK_STAMPS_TTL_MS`, else 3000. [ms]\n */\n stampsTtlMs?: number;\n /**\n * Stamp retention window: stamps older than this are treated as inert and\n * excluded from shared-table reads. Bounds the shared table to tags touched\n * within the window. MUST be \u2265 the app's longest entry lifetime (e.g. the\n * longest `cacheLife` expire), else entries that outlive the window can\n * resurrect stale. Defaults to `CACHEPLANK_STAMPS_RETENTION_MS`, else\n * 30 days. [ms]\n */\n stampsRetentionMs?: number;\n /** Sink for the single fail-open warning. Defaults to `console.warn`. */\n warn?: (message: string) => void;\n}\n\n/** Fail-open backoff after a stamp-store error, so we don't hammer a dead DB. [ms] */\nconst RETRY_BACKOFF_MS = 30_000;\nconst DEFAULT_MAX_ENTRIES = 2000;\n/** Default soft TTL for the in-process tag-stamp memo. [ms] */\nconst DEFAULT_STAMPS_TTL_MS = 3000;\n/**\n * Default stamp retention window. Stamps older than this are ignored by\n * shared-table reads, bounding the table to tags touched within the window.\n * Generous enough to exceed any realistic max entry TTL (next's own `max`\n * cacheLife profile is a year \u2014 apps using it should raise this).\n * [ms]\n */\nconst DEFAULT_STAMPS_RETENTION_MS = 30 * 24 * 60 * 60 * 1000;\n\nconst now = (): number => Date.now();\n\n/** Trims an env value; returns its numeric value, or undefined if blank/invalid. */\nfunction envNumber(name: string): number | undefined {\n const raw = process.env[name]?.trim();\n if (!raw) return undefined;\n const value = Number(raw);\n return Number.isFinite(value) ? value : undefined;\n}\n\ninterface ResolvedOptions {\n url?: string;\n authToken?: string;\n prefix: string;\n maxEntries: number;\n tagCheckTtlMs: number;\n stampsRetentionMs: number;\n warn: (message: string) => void;\n}\n\nfunction resolveOptions(options: CacheplankOptions): ResolvedOptions {\n const configuredMax = envNumber('CACHEPLANK_MAX_ENTRIES');\n const configuredTtl = envNumber('CACHEPLANK_STAMPS_TTL_MS');\n const configuredRetention = envNumber('CACHEPLANK_STAMPS_RETENTION_MS');\n return {\n url: options.url ?? process.env.CACHEPLANK_URL ?? process.env.BUNNY_DATABASE_URL,\n authToken:\n options.authToken ?? process.env.CACHEPLANK_AUTH_TOKEN ?? process.env.BUNNY_DATABASE_AUTH_TOKEN,\n prefix: options.prefix ?? process.env.CACHEPLANK_PREFIX ?? '',\n // `>= 0` so `CACHEPLANK_MAX_ENTRIES=0` disables entry storage, mirroring\n // next's own `maxSize === 0` no-op handler; blank/garbage falls back.\n maxEntries:\n options.maxEntries ??\n (configuredMax !== undefined && configuredMax >= 0 ? configuredMax : DEFAULT_MAX_ENTRIES),\n tagCheckTtlMs:\n options.stampsTtlMs ??\n (configuredTtl !== undefined && configuredTtl >= 0 ? configuredTtl : DEFAULT_STAMPS_TTL_MS),\n // Window reads need a positive horizon; zero/blank/garbage \u2192 the default.\n stampsRetentionMs:\n options.stampsRetentionMs ??\n (configuredRetention !== undefined && configuredRetention > 0\n ? configuredRetention\n : DEFAULT_STAMPS_RETENTION_MS),\n warn: options.warn ?? ((message: string) => console.warn(`[cacheplank] ${message}`)),\n };\n}\n\n/* ------------------------------------------------------------------------- *\n * Shared tag-stamp store \u2014 the only shared state. *\n * ------------------------------------------------------------------------- */\n\ninterface StampStore {\n /** Monotonic in-process memo of tag \u2192 latest invalidation timestamp. */\n readonly stamps: Map<string, number>;\n /** Refresh the memo from the shared table, respecting the soft TTL. */\n sync(): Promise<void>;\n /** Latest stamp across `tags` (0 if none seen). */\n expiration(tags: string[]): Promise<Timestamp>;\n /** Write a monotone stamp for every tag, and make it self-visible at once. */\n update(tags: string[]): Promise<void>;\n}\n\n/**\n * One libSQL table holds invalidation stamps. Reads and writes are fail-open:\n * a dead store degrades to process-local invalidation, never an exception.\n */\nfunction createStampStore(opts: ResolvedOptions): StampStore {\n const stamps = new Map<string, number>();\n let stampsSyncedAt = 0;\n\n let client: Client | undefined;\n let ready: Promise<boolean> | undefined;\n let retryAt = 0;\n let warned = false;\n function warnOnce(message: string): void {\n if (!warned) {\n warned = true;\n opts.warn(message);\n }\n }\n\n function observe(tag: string, at: number): void {\n if (at > (stamps.get(tag) ?? 0)) stamps.set(tag, at);\n }\n\n function ensureStore(): Promise<boolean> {\n if (ready) return ready;\n if (!opts.url) {\n warnOnce('no CACHEPLANK_URL / BUNNY_DATABASE_URL set; tag stamps are process-local only');\n return Promise.resolve(false);\n }\n if (now() < retryAt) return Promise.resolve(false);\n ready = (async () => {\n try {\n const created = createClient({ url: opts.url as string, authToken: opts.authToken });\n await created.execute(\n 'CREATE TABLE IF NOT EXISTS tag_stamps (tag TEXT PRIMARY KEY, revalidated_at INTEGER NOT NULL)',\n );\n // Makes the retention-windowed sync read a range scan over live rows\n // instead of a full-table scan (rows_read stays O(window), not\n // O(every tag ever stamped)). Idempotent; rides the same lazy init.\n await created.execute(\n 'CREATE INDEX IF NOT EXISTS tag_stamps_revalidated_at ON tag_stamps(revalidated_at)',\n );\n client = created;\n return true;\n } catch (error) {\n retryAt = now() + RETRY_BACKOFF_MS;\n ready = undefined;\n warnOnce(`tag-stamp store unavailable; continuing fail-open (${(error as Error).message})`);\n return false;\n }\n })();\n return ready;\n }\n\n // Single-flight: one in-progress refresh; concurrent callers await it instead\n // of each issuing their own full-table SELECT at a TTL boundary.\n let syncing: Promise<void> | undefined;\n\n function sync(): Promise<void> {\n if (now() - stampsSyncedAt < opts.tagCheckTtlMs) return Promise.resolve();\n if (syncing) return syncing;\n // Claim the freshness window synchronously, so a burst of `get`s that all\n // observe the expired memo together joins the one refresh below.\n stampsSyncedAt = now();\n syncing = (async () => {\n try {\n if (!(await ensureStore()) || !client) return;\n // Retention window: stamps older than the window are provably inert\n // (every entry they could invalidate has expired by then), so they are\n // excluded here. That bounds memo size and read cost to tags touched\n // within the window instead of every tag ever stamped, and keeps the\n // range scan index-backed (see ensureStore).\n const result = await client.execute({\n sql: 'SELECT tag, revalidated_at FROM tag_stamps WHERE revalidated_at > ?',\n args: [now() - opts.stampsRetentionMs],\n });\n for (const row of result.rows) {\n const at = Number(row.revalidated_at);\n if (Number.isFinite(at)) observe(String(row.tag), at);\n }\n } catch (error) {\n retryAt = now() + RETRY_BACKOFF_MS;\n warnOnce(`tag-stamp read failed; serving local state (${(error as Error).message})`);\n } finally {\n syncing = undefined;\n }\n })();\n return syncing;\n }\n\n return {\n stamps,\n sync,\n async expiration(tags) {\n await sync();\n let max = 0;\n for (const tag of tags) {\n const at = stamps.get(tag) ?? 0;\n if (at > max) max = at;\n }\n return max;\n },\n async update(tags) {\n const at = now();\n for (const tag of tags) observe(tag, at);\n if (tags.length === 0) return;\n try {\n if (!(await ensureStore()) || !client) return;\n await client.batch(\n tags.map((tag) => ({\n sql:\n 'INSERT INTO tag_stamps(tag, revalidated_at) VALUES (?, ?) ' +\n 'ON CONFLICT(tag) DO UPDATE SET revalidated_at = MAX(revalidated_at, excluded.revalidated_at)',\n args: [tag, at],\n })),\n 'write',\n );\n } catch (error) {\n warnOnce(`tag-stamp write failed; local state kept (${(error as Error).message})`);\n }\n },\n };\n}\n\n/** LRU insert via Map insertion order, capped at `max`. */\nfunction remember<V>(entries: Map<string, V>, key: string, value: V, max: number): void {\n entries.delete(key);\n entries.set(key, value);\n while (entries.size > max) {\n const oldest = entries.keys().next().value;\n if (oldest === undefined) break;\n entries.delete(oldest);\n }\n}\n\n/** Extract the tag list a stored incremental value carries, if any. */\nfunction valueTags(value: IncrementalCacheValue | null): string[] {\n if (!value) return [];\n const tags = (value as { tags?: unknown }).tags;\n if (Array.isArray(tags)) return tags.filter((t): t is string => typeof t === 'string');\n const headers = (value as { headers?: Record<string, string | string[]> }).headers;\n const header = headers?.['x-next-cache-tags'];\n if (typeof header === 'string') return header.split(',');\n return [];\n}\n\n/* ------------------------------------------------------------------------- *\n * Handler 1 \u2014 `cacheHandlers` (plural). Entries are byte streams. *\n * ------------------------------------------------------------------------- */\n\ninterface StoredEntry {\n bytes: Uint8Array;\n tags: string[];\n timestamp: number;\n expire: number;\n revalidate: number;\n stale: number;\n}\n\n/**\n * Create a `cacheHandlers` (plural) handler. The package's `default` export is\n * one of these, configured from the environment.\n */\nexport function createCacheHandler(options: CacheplankOptions = {}): CacheHandler {\n const opts = resolveOptions(options);\n const store = createStampStore(opts);\n\n // Local entry store, LRU via Map insertion order.\n const entries = new Map<string, StoredEntry>();\n // In-flight `set` calls, so a `get` racing a `set` waits instead of missing.\n const pendingSets = new Map<string, Promise<void>>();\n\n return {\n async get(cacheKey, softTags) {\n const key = opts.prefix + cacheKey;\n const pending = pendingSets.get(key);\n if (pending) await pending.catch(() => {});\n\n const entry = entries.get(key);\n if (!entry) return undefined;\n\n // Touch for LRU recency.\n entries.delete(key);\n entries.set(key, entry);\n\n // A negative `expire` is a tombstone: dropped for good here rather than\n // re-checked on every read (and independently of the retention bounds).\n if (entry.expire < 0) {\n entries.delete(key);\n return undefined;\n }\n // Mirror Next's effective expiry. `set` already dropped `expire: 0` in\n // production. The wrapper discards an entry once\n // `currentTime > timestamp + expire*1000` (always) or past `revalidate`\n // during static generation; the default handler additionally drops past\n // `revalidate` in production (SWR background revalidation takes over).\n // Dropping at either bound here matches what Next would reject anyway and\n // never serves a value the wrapper throws away. In dev next widens the\n // expire bound to MIN_PRERENDERABLE_EXPIRE (300s) \u2014 the default\n // handler's single dev max-age and the wrapper's dev expire check use\n // the same formula \u2014 so reloads of short-`expire` entries still hit; we\n // widen the expire-bound check to match instead of only the max-age\n // one. A `revalidate <= 0` (including SWR's `-1`) drops in production.\n const age = now() - entry.timestamp;\n const dev = Boolean(process.env.__NEXT_DEV_SERVER);\n const maxAgeSeconds = dev ? Math.max(entry.expire, 300) : entry.revalidate;\n const expireBoundSeconds = dev ? Math.max(entry.expire, 300) : entry.expire;\n if (\n !(age < maxAgeSeconds * 1000) ||\n (entry.expire >= 0 && !(age < expireBoundSeconds * 1000))\n ) {\n return undefined;\n }\n\n // Shared invalidation: any tag (own or route soft tag) stamped at or\n // after this entry was written means another pod revalidated it \u2192 miss,\n // so Next regenerates. `>=` mirrors Next's own wrapper-side discard.\n await store.sync();\n for (const tag of entry.tags) {\n if ((store.stamps.get(tag) ?? 0) >= entry.timestamp) return undefined;\n }\n for (const tag of softTags) {\n if ((store.stamps.get(tag) ?? 0) >= entry.timestamp) return undefined;\n }\n\n const bytes = entry.bytes;\n return {\n value: new ReadableStream<Uint8Array>({\n start(controller) {\n controller.enqueue(bytes);\n controller.close();\n },\n }),\n tags: entry.tags,\n stale: entry.stale,\n timestamp: entry.timestamp,\n expire: entry.expire,\n revalidate: entry.revalidate,\n };\n },\n\n async set(cacheKey, pendingEntry) {\n const key = opts.prefix + cacheKey;\n let release: () => void = () => {};\n const gate = new Promise<void>((resolve) => {\n release = resolve;\n });\n pendingSets.set(key, gate);\n try {\n const entry = await pendingEntry;\n // In production an `expire: 0` entry is dynamic and never served back.\n if (entry.expire === 0 && !process.env.__NEXT_DEV_SERVER) return;\n\n const bytes = await drainStream(entry.value);\n remember(\n entries,\n key,\n {\n bytes,\n tags: entry.tags ?? [],\n timestamp: entry.timestamp,\n expire: entry.expire,\n revalidate: entry.revalidate,\n stale: entry.stale,\n },\n opts.maxEntries,\n );\n } catch (error) {\n opts.warn(`failed to buffer a cache entry (${(error as Error).message})`);\n } finally {\n release();\n pendingSets.delete(key);\n }\n },\n\n async refreshTags() {\n await store.sync();\n },\n\n async getExpiration(tags) {\n return store.expiration(tags);\n },\n\n async updateTags(tags, _durations) {\n // `durations.expire` is accepted but not persisted: the shared table holds\n // a single monotone stamp per tag. A profile'd `revalidateTag` therefore\n // invalidates immediately rather than stale-while-revalidate \u2014 safe, and\n // faithful to the one-table model. See README compatibility note #4.\n await store.update(tags);\n },\n };\n}\n\n/* ------------------------------------------------------------------------- *\n * Handler 2 \u2014 `cacheHandler` (singular). Entries are opaque JSON-ish values. *\n * ------------------------------------------------------------------------- */\n\ninterface StoredIncrementalEntry {\n lastModified: number;\n value: IncrementalCacheValue;\n}\n\n/**\n * Create a `cacheHandler` (singular, incremental/ISR) handler **class**.\n *\n * Next instantiates this class once per request (`new CurCacheHandler(ctx)` in\n * `IncrementalCache`) and imports the module once, so all state lives in this\n * closure \u2014 shared across the per-request instances by construction.\n *\n * Unlike the plural handler we do *not* re-implement time-based expiry: the\n * incremental wrapper owns `revalidate`/`expire`/`isStale` semantics (it reads\n * them from the prerender manifest's cache controls), so we implement exactly\n * one policy \u2014 shared tag invalidation \u2014 and let Next decide everything else.\n * That is what makes fully-static `APP_PAGE` routes converge across pods.\n */\nexport function createIncrementalCacheHandler(\n options: CacheplankOptions = {},\n): new (ctx: unknown) => IncrementalCacheHandler {\n const opts = resolveOptions(options);\n const store = createStampStore(opts);\n const entries = new Map<string, StoredIncrementalEntry>();\n\n return class CacheplankIncrementalCacheHandler implements IncrementalCacheHandler {\n // Next only ever passes its own context; we read none of it here (the\n // meaningful context is supplied per call to `get`/`set`).\n constructor(_ctx: unknown) {}\n\n async get(cacheKey: string, ctx: IncrementalCacheContext) {\n const key = opts.prefix + cacheKey;\n const entry = entries.get(key);\n if (!entry) return null;\n\n // Touch for LRU recency.\n entries.delete(key);\n entries.set(key, entry);\n\n // One shared rule, identical to the plural handler: a tag stamped at or\n // after the entry was written means some pod revalidated it \u2192 miss, so\n // Next regenerates (through the same response-cache path that wrote it).\n await store.sync();\n const tags = valueTags(entry.value);\n if (ctx.tags) tags.push(...ctx.tags);\n if (ctx.softTags) tags.push(...ctx.softTags);\n for (const tag of tags) {\n if ((store.stamps.get(tag) ?? 0) >= entry.lastModified) {\n entries.delete(key);\n return null;\n }\n }\n\n return { lastModified: entry.lastModified, value: entry.value };\n }\n\n async set(\n cacheKey: string,\n data: IncrementalCacheValue | null,\n _ctx: IncrementalCacheContext,\n ) {\n const key = opts.prefix + cacheKey;\n // A null datum means \"delete this key\" (Next uses it to drop entries).\n if (data == null) {\n entries.delete(key);\n return;\n }\n // The incremental values are plain objects (Buffers, headers, segment\n // Maps) that Next does not mutate after writing, so we retain the\n // reference \u2014 exactly like Next's own in-memory `FileSystemCache`. Only\n // invalidation is shared; the entry itself never leaves this process.\n remember(entries, key, { lastModified: now(), value: data }, opts.maxEntries);\n }\n\n async revalidateTag(tags: string | string[], _durations?: { expire?: number }) {\n // Same table, same monotone upsert as the plural handler, so a\n // `revalidateTag` invalidates both entry kinds across every pod.\n await store.update(typeof tags === 'string' ? [tags] : tags);\n }\n\n resetRequestCache() {\n // No per-request cache state to reset.\n }\n };\n}\n\n/* ------------------------------------------------------------------------- *\n * Small helpers. *\n * ------------------------------------------------------------------------- */\n\nasync function drainStream(stream: ReadableStream<Uint8Array>): Promise<Uint8Array> {\n const reader = stream.getReader();\n const chunks: Uint8Array[] = [];\n let size = 0;\n try {\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n if (value) {\n chunks.push(value);\n size += value.byteLength;\n }\n }\n } finally {\n reader.releaseLock();\n }\n const bytes = new Uint8Array(size);\n let offset = 0;\n for (const chunk of chunks) {\n bytes.set(chunk, offset);\n offset += chunk.byteLength;\n }\n return bytes;\n}\n\n/**\n * Default singleton for the plural `cacheHandlers` API. Next loads the module\n * via `interopDefault`, so this `default` export is the handler Next uses.\n */\nexport default createCacheHandler();\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cacheplank",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "The shared plank your pods walk across: a single-file Next.js 16 cache handler where entries stay local and only tag invalidation is shared, via one libSQL table.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
"types": "./dist/index.d.ts",
|
|
13
13
|
"import": "./dist/index.mjs",
|
|
14
14
|
"require": "./dist/index.cjs"
|
|
15
|
+
},
|
|
16
|
+
"./cache-handler": {
|
|
17
|
+
"types": "./dist/cache-handler.d.ts",
|
|
18
|
+
"import": "./dist/cache-handler.mjs",
|
|
19
|
+
"require": "./dist/cache-handler.cjs"
|
|
15
20
|
}
|
|
16
21
|
},
|
|
17
22
|
"files": [
|
|
@@ -25,6 +30,7 @@
|
|
|
25
30
|
"scripts": {
|
|
26
31
|
"build": "node build.mjs && tsc -p tsconfig.json",
|
|
27
32
|
"test": "node --test test/*.test.mjs",
|
|
33
|
+
"test:e2e": "node test/e2e-static-route.mjs",
|
|
28
34
|
"typecheck": "tsc -p tsconfig.typecheck.json",
|
|
29
35
|
"prepublishOnly": "npm run build && npm test && npm run typecheck"
|
|
30
36
|
},
|