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.
@@ -0,0 +1,313 @@
1
+ // src/index.ts
2
+ import { createClient } from "@libsql/client";
3
+ var RETRY_BACKOFF_MS = 3e4;
4
+ var DEFAULT_MAX_ENTRIES = 2e3;
5
+ var DEFAULT_STAMPS_TTL_MS = 3e3;
6
+ var DEFAULT_STAMPS_RETENTION_MS = 30 * 24 * 60 * 60 * 1e3;
7
+ var now = () => Date.now();
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) {
32
+ const stamps = /* @__PURE__ */ new Map();
33
+ let stampsSyncedAt = 0;
34
+ let client;
35
+ let ready;
36
+ let retryAt = 0;
37
+ let warned = false;
38
+ function warnOnce(message) {
39
+ if (!warned) {
40
+ warned = true;
41
+ opts.warn(message);
42
+ }
43
+ }
44
+ function observe(tag, at) {
45
+ if (at > (stamps.get(tag) ?? 0)) stamps.set(tag, at);
46
+ }
47
+ function ensureStore() {
48
+ if (ready) return ready;
49
+ if (!opts.url) {
50
+ warnOnce("no CACHEPLANK_URL / BUNNY_DATABASE_URL set; tag stamps are process-local only");
51
+ return Promise.resolve(false);
52
+ }
53
+ if (now() < retryAt) return Promise.resolve(false);
54
+ ready = (async () => {
55
+ try {
56
+ const created = createClient({ url: opts.url, authToken: opts.authToken });
57
+ await created.execute(
58
+ "CREATE TABLE IF NOT EXISTS tag_stamps (tag TEXT PRIMARY KEY, revalidated_at INTEGER NOT NULL)"
59
+ );
60
+ await created.execute(
61
+ "CREATE INDEX IF NOT EXISTS tag_stamps_revalidated_at ON tag_stamps(revalidated_at)"
62
+ );
63
+ client = created;
64
+ return true;
65
+ } catch (error) {
66
+ retryAt = now() + RETRY_BACKOFF_MS;
67
+ ready = void 0;
68
+ warnOnce(`tag-stamp store unavailable; continuing fail-open (${error.message})`);
69
+ return false;
70
+ }
71
+ })();
72
+ return ready;
73
+ }
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})`);
126
+ }
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);
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();
152
+ const pendingSets = /* @__PURE__ */ new Map();
153
+ return {
154
+ async get(cacheKey, softTags) {
155
+ const key = opts.prefix + cacheKey;
156
+ const pending = pendingSets.get(key);
157
+ if (pending) await pending.catch(() => {
158
+ });
159
+ const entry = entries.get(key);
160
+ if (!entry) return void 0;
161
+ entries.delete(key);
162
+ entries.set(key, entry);
163
+ if (entry.expire < 0) {
164
+ entries.delete(key);
165
+ return void 0;
166
+ }
167
+ const age = now() - entry.timestamp;
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)) {
172
+ return void 0;
173
+ }
174
+ await store.sync();
175
+ for (const tag of entry.tags) {
176
+ if ((store.stamps.get(tag) ?? 0) >= entry.timestamp) return void 0;
177
+ }
178
+ for (const tag of softTags) {
179
+ if ((store.stamps.get(tag) ?? 0) >= entry.timestamp) return void 0;
180
+ }
181
+ const bytes = entry.bytes;
182
+ return {
183
+ value: new ReadableStream({
184
+ start(controller) {
185
+ controller.enqueue(bytes);
186
+ controller.close();
187
+ }
188
+ }),
189
+ tags: entry.tags,
190
+ stale: entry.stale,
191
+ timestamp: entry.timestamp,
192
+ expire: entry.expire,
193
+ revalidate: entry.revalidate
194
+ };
195
+ },
196
+ async set(cacheKey, pendingEntry) {
197
+ const key = opts.prefix + cacheKey;
198
+ let release = () => {
199
+ };
200
+ const gate = new Promise((resolve) => {
201
+ release = resolve;
202
+ });
203
+ pendingSets.set(key, gate);
204
+ try {
205
+ const entry = await pendingEntry;
206
+ if (entry.expire === 0 && !process.env.__NEXT_DEV_SERVER) return;
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
+ );
221
+ } catch (error) {
222
+ opts.warn(`failed to buffer a cache entry (${error.message})`);
223
+ } finally {
224
+ release();
225
+ pendingSets.delete(key);
226
+ }
227
+ },
228
+ async refreshTags() {
229
+ await store.sync();
230
+ },
231
+ async getExpiration(tags) {
232
+ return store.expiration(tags);
233
+ },
234
+ async updateTags(tags, _durations) {
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;
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() {
278
+ }
279
+ };
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
+ }
305
+ var index_default = createCacheHandler();
306
+
307
+ // src/cache-handler.ts
308
+ var cache_handler_default = createIncrementalCacheHandler();
309
+ export {
310
+ createIncrementalCacheHandler,
311
+ cache_handler_default as default
312
+ };
313
+ //# sourceMappingURL=cache-handler.mjs.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/index.ts", "../src/cache-handler.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, 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", "/**\n * cacheplank/cache-handler \u2014 the singular `cacheHandler` entry.\n *\n * Wire this as `cacheHandler` in `next.config` to give Next's legacy\n * incremental/ISR cache (prerendered static `APP_PAGE` routes, `APP_ROUTE`,\n * `PAGES`, fetch cache) the same shared-invalidation behaviour the plural\n * `cacheHandlers` entry already has. Both share one `tag_stamps` table, so a\n * `revalidateTag` from any pod converges every entry kind on every pod.\n *\n * // next.config.js\n * module.exports = {\n * cacheComponents: true,\n * cacheHandlers: { default: require.resolve('cacheplank') },\n * cacheHandler: require.resolve('cacheplank/cache-handler'),\n * };\n *\n * Next resolves this path against `.next/`, so it must be **absolute** or a\n * `file://` URL \u2014 `require.resolve(...)` yields an absolute path (README note\n * #6).\n */\nimport { createIncrementalCacheHandler } from './index';\n\n/** Next does `new CurCacheHandler(ctx)`, and loads `mod.default || mod`. */\nexport default createIncrementalCacheHandler();\n\nexport { createIncrementalCacheHandler } from './index';\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }