@vinext/cloudflare 1.0.0-beta.1 → 1.0.0-beta.3
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/dist/cache/kv-data-adapter.runtime.d.ts +4 -3
- package/dist/cache/kv-data-adapter.runtime.js +15 -12
- package/dist/cache/kv-key.d.ts +15 -0
- package/dist/cache/kv-key.js +43 -0
- package/dist/deploy.js +2 -2
- package/dist/prerender-kv-populate.js +4 -6
- package/dist/tpr.d.ts +1 -1
- package/dist/tpr.js +4 -3
- package/dist/worker-deployment-url.d.ts +4 -0
- package/dist/worker-deployment-url.js +23 -0
- package/package.json +3 -3
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { KvDataAdapterOptions } from "./kv-data-adapter.js";
|
|
2
|
+
import { ENTRY_PREFIX } from "./kv-key.js";
|
|
2
3
|
import { ExecutionContextLike } from "vinext/shims/request-context";
|
|
3
4
|
import { CacheHandler, CacheHandlerValue, IncrementalCacheValue } from "vinext/shims/cache";
|
|
4
5
|
|
|
@@ -28,8 +29,6 @@ type KVNamespace = {
|
|
|
28
29
|
cursor?: string;
|
|
29
30
|
}>;
|
|
30
31
|
};
|
|
31
|
-
/** Key prefix for cache entries. */
|
|
32
|
-
declare const ENTRY_PREFIX = "cache:";
|
|
33
32
|
/**
|
|
34
33
|
* Cloudflare KV data cache handler.
|
|
35
34
|
*
|
|
@@ -53,7 +52,7 @@ declare const ENTRY_PREFIX = "cache:";
|
|
|
53
52
|
*/
|
|
54
53
|
declare class KVCacheHandler implements CacheHandler {
|
|
55
54
|
private kv;
|
|
56
|
-
private
|
|
55
|
+
private keySpace;
|
|
57
56
|
private ctx;
|
|
58
57
|
private ttlSeconds;
|
|
59
58
|
/** Local in-memory cache for tag invalidation timestamps. Avoids redundant KV reads. */
|
|
@@ -66,6 +65,8 @@ declare class KVCacheHandler implements CacheHandler {
|
|
|
66
65
|
ttlSeconds?: number; /** TTL in milliseconds for the local tag cache. Defaults to 5000ms. */
|
|
67
66
|
tagCacheTtlMs?: number;
|
|
68
67
|
});
|
|
68
|
+
private _entryKey;
|
|
69
|
+
private _tagKey;
|
|
69
70
|
get(key: string, _ctx?: Record<string, unknown>): Promise<CacheHandlerValue | null>;
|
|
70
71
|
/**
|
|
71
72
|
* Check tag invalidation markers for stored tags or read-time soft tags.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { ENTRY_PREFIX, createKvKeySpace } from "./kv-key.js";
|
|
1
2
|
import { isUnknownRecord, readCacheControlNumberField } from "../utils/cache-control-metadata.js";
|
|
2
|
-
import { Buffer } from "node:buffer";
|
|
3
3
|
import { getRequestExecutionContext } from "vinext/shims/request-context";
|
|
4
|
+
import { Buffer } from "node:buffer";
|
|
4
5
|
//#region src/cache/kv-data-adapter.runtime.ts
|
|
5
6
|
/**
|
|
6
7
|
* Cloudflare KV data cache — implementation + runtime factory.
|
|
@@ -23,10 +24,6 @@ import { getRequestExecutionContext } from "vinext/shims/request-context";
|
|
|
23
24
|
*/
|
|
24
25
|
/** Default KV namespace binding name read from the Worker `env`. */
|
|
25
26
|
const DEFAULT_BINDING = "VINEXT_KV_CACHE";
|
|
26
|
-
/** Key prefix for tag invalidation timestamps. */
|
|
27
|
-
const TAG_PREFIX = "__tag:";
|
|
28
|
-
/** Key prefix for cache entries. */
|
|
29
|
-
const ENTRY_PREFIX = "cache:";
|
|
30
27
|
/** Prefix used by revalidatePath for path-based tags. */
|
|
31
28
|
const PATH_TAG_PREFIX = "_N_T_";
|
|
32
29
|
/** Max tag length to prevent KV key abuse. */
|
|
@@ -96,7 +93,7 @@ function isPathChildOf(path, prefix) {
|
|
|
96
93
|
*/
|
|
97
94
|
var KVCacheHandler = class {
|
|
98
95
|
kv;
|
|
99
|
-
|
|
96
|
+
keySpace;
|
|
100
97
|
ctx;
|
|
101
98
|
ttlSeconds;
|
|
102
99
|
/** Local in-memory cache for tag invalidation timestamps. Avoids redundant KV reads. */
|
|
@@ -105,13 +102,19 @@ var KVCacheHandler = class {
|
|
|
105
102
|
_tagCacheTtl;
|
|
106
103
|
constructor(kvNamespace, options) {
|
|
107
104
|
this.kv = kvNamespace;
|
|
108
|
-
this.
|
|
105
|
+
this.keySpace = createKvKeySpace(options?.appPrefix);
|
|
109
106
|
this.ctx = options?.ctx;
|
|
110
107
|
this.ttlSeconds = options?.ttlSeconds ?? 720 * 3600;
|
|
111
108
|
this._tagCacheTtl = options?.tagCacheTtlMs ?? 5e3;
|
|
112
109
|
}
|
|
110
|
+
_entryKey(key) {
|
|
111
|
+
return this.keySpace.entryKey(key);
|
|
112
|
+
}
|
|
113
|
+
_tagKey(tag) {
|
|
114
|
+
return this.keySpace.tagKey(tag);
|
|
115
|
+
}
|
|
113
116
|
async get(key, _ctx) {
|
|
114
|
-
const kvKey = this.
|
|
117
|
+
const kvKey = this._entryKey(key);
|
|
115
118
|
const raw = await this.kv.get(kvKey);
|
|
116
119
|
if (!raw) return null;
|
|
117
120
|
let parsed;
|
|
@@ -178,7 +181,7 @@ var KVCacheHandler = class {
|
|
|
178
181
|
}
|
|
179
182
|
}
|
|
180
183
|
if (uncachedTags.length > 0) {
|
|
181
|
-
const tagResults = await Promise.all(uncachedTags.map((tag) => this.kv.get(this.
|
|
184
|
+
const tagResults = await Promise.all(uncachedTags.map((tag) => this.kv.get(this._tagKey(tag))));
|
|
182
185
|
for (let i = 0; i < uncachedTags.length; i++) {
|
|
183
186
|
const tagTime = tagResults[i];
|
|
184
187
|
const tagTimestamp = tagTime ? Number(tagTime) : 0;
|
|
@@ -229,7 +232,7 @@ var KVCacheHandler = class {
|
|
|
229
232
|
};
|
|
230
233
|
const expirationTtl = revalidateAt !== null ? this.ttlSeconds : void 0;
|
|
231
234
|
const metadata = JSON.stringify({ tags }).length <= 1024 ? { tags } : void 0;
|
|
232
|
-
return this._put(this.
|
|
235
|
+
return this._put(this._entryKey(key), JSON.stringify(entry), {
|
|
233
236
|
expirationTtl,
|
|
234
237
|
metadata
|
|
235
238
|
});
|
|
@@ -238,7 +241,7 @@ var KVCacheHandler = class {
|
|
|
238
241
|
const tagList = Array.isArray(tags) ? tags : [tags];
|
|
239
242
|
const now = Date.now();
|
|
240
243
|
const validTags = tagList.filter((t) => validateTag(t) !== null);
|
|
241
|
-
await Promise.all(validTags.map((tag) => this.kv.put(this.
|
|
244
|
+
await Promise.all(validTags.map((tag) => this.kv.put(this._tagKey(tag), String(now), { expirationTtl: 720 * 3600 })));
|
|
242
245
|
for (const tag of validTags) this._tagCache.set(tag, {
|
|
243
246
|
timestamp: now,
|
|
244
247
|
fetchedAt: now
|
|
@@ -263,7 +266,7 @@ var KVCacheHandler = class {
|
|
|
263
266
|
async revalidateByPathPrefix(pathPrefix) {
|
|
264
267
|
const tagsToInvalidate = /* @__PURE__ */ new Set();
|
|
265
268
|
let cursor;
|
|
266
|
-
const listPrefix = this.
|
|
269
|
+
const listPrefix = this.keySpace.entryPrefix;
|
|
267
270
|
do {
|
|
268
271
|
const page = await this.kv.list({
|
|
269
272
|
prefix: listPrefix,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//#region src/cache/kv-key.d.ts
|
|
2
|
+
/** Key prefix for cache entries. */
|
|
3
|
+
declare const ENTRY_PREFIX = "cache:";
|
|
4
|
+
type KvKeySpace = {
|
|
5
|
+
/** Prefix shared by every cache entry, including entries with hashed logical keys. */entryPrefix: string;
|
|
6
|
+
entryKey(logicalKey: string): string;
|
|
7
|
+
tagKey(tag: string): string;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Create the deterministic key namespace shared by runtime cache operations
|
|
11
|
+
* and deploy-time prerender population.
|
|
12
|
+
*/
|
|
13
|
+
declare function createKvKeySpace(appPrefix: string | undefined): KvKeySpace;
|
|
14
|
+
//#endregion
|
|
15
|
+
export { ENTRY_PREFIX, KvKeySpace, createKvKeySpace };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { fnv1a64 } from "vinext/internal/utils/hash";
|
|
2
|
+
//#region src/cache/kv-key.ts
|
|
3
|
+
/** Key prefix for tag invalidation timestamps. */
|
|
4
|
+
const TAG_PREFIX = "__tag:";
|
|
5
|
+
/** Key prefix for cache entries. */
|
|
6
|
+
const ENTRY_PREFIX = "cache:";
|
|
7
|
+
/** Cloudflare KV's maximum UTF-8 encoded key length. */
|
|
8
|
+
const KV_KEY_MAX_BYTES = 512;
|
|
9
|
+
/** Marker for logical keys that must be hashed to fit in KV. */
|
|
10
|
+
const HASHED_KEY_PREFIX = "__hash:";
|
|
11
|
+
const KV_KEY_ENCODER = new TextEncoder();
|
|
12
|
+
function kvKeyByteLength(key) {
|
|
13
|
+
return KV_KEY_ENCODER.encode(key).length;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Keep short app prefixes readable, but bound the namespace portion so a
|
|
17
|
+
* hashed entry or tag key is always able to fit within Cloudflare KV's limit.
|
|
18
|
+
*/
|
|
19
|
+
function normalizeAppPrefix(appPrefix) {
|
|
20
|
+
if (!appPrefix) return "";
|
|
21
|
+
const prefix = `${appPrefix}:`;
|
|
22
|
+
if (kvKeyByteLength(`${prefix}cache:${HASHED_KEY_PREFIX}${fnv1a64("")}`) <= KV_KEY_MAX_BYTES) return prefix;
|
|
23
|
+
return `__app:${fnv1a64(appPrefix)}:`;
|
|
24
|
+
}
|
|
25
|
+
function buildStorageKey(prefix, categoryPrefix, logicalKey) {
|
|
26
|
+
const key = `${prefix}${categoryPrefix}${logicalKey}`;
|
|
27
|
+
if (kvKeyByteLength(key) <= KV_KEY_MAX_BYTES) return key;
|
|
28
|
+
return `${prefix}${categoryPrefix}${HASHED_KEY_PREFIX}${fnv1a64(logicalKey)}`;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Create the deterministic key namespace shared by runtime cache operations
|
|
32
|
+
* and deploy-time prerender population.
|
|
33
|
+
*/
|
|
34
|
+
function createKvKeySpace(appPrefix) {
|
|
35
|
+
const prefix = normalizeAppPrefix(appPrefix);
|
|
36
|
+
return {
|
|
37
|
+
entryPrefix: `${prefix}${ENTRY_PREFIX}`,
|
|
38
|
+
entryKey: (logicalKey) => buildStorageKey(prefix, ENTRY_PREFIX, logicalKey),
|
|
39
|
+
tagKey: (tag) => buildStorageKey(prefix, TAG_PREFIX, tag)
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
export { ENTRY_PREFIX, createKvKeySpace };
|
package/dist/deploy.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { readPrerenderWarmPaths, warmCdnCache } from "./cdn-warm.js";
|
|
2
2
|
import { parseWranglerConfig, runTPR } from "./tpr.js";
|
|
3
3
|
import { formatImageOptimizationHint, formatMissingCacheAdapterError, resolveKvDataAdapterConfig, viteConfigHasCacheAdapter, viteConfigHasCloudflarePlugin, viteConfigHasImageAdapter, workerEntryHasCacheHandler } from "./deploy-config.js";
|
|
4
|
-
import { parseWorkersDevUrl } from "./workers-dev-url.js";
|
|
5
4
|
import { runWranglerDeploymentStatus, runWranglerTriggersDeploy, runWranglerVersionDeploy, runWranglerVersionUpload } from "./version-deploy.js";
|
|
5
|
+
import { parseWorkerDeploymentUrl } from "./worker-deployment-url.js";
|
|
6
6
|
import { buildPrerenderKVPairs } from "./prerender-kv-populate.js";
|
|
7
7
|
import { createRequire } from "node:module";
|
|
8
8
|
import path from "node:path";
|
|
@@ -344,7 +344,7 @@ async function runWranglerDeploy(root, options, execute = spawn) {
|
|
|
344
344
|
reject(/* @__PURE__ */ new Error(`Wrangler deploy failed with ${exitReason}.`));
|
|
345
345
|
});
|
|
346
346
|
});
|
|
347
|
-
return
|
|
347
|
+
return parseWorkerDeploymentUrl(output) ?? "(URL not detected in wrangler output)";
|
|
348
348
|
}
|
|
349
349
|
async function deployWithCdnWarmup(root, paths, options) {
|
|
350
350
|
const upload = runWranglerVersionUpload(root, options);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createKvKeySpace } from "./cache/kv-key.js";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import { getRenderedAppRoutes, readPrerenderManifest } from "vinext/internal/server/prerender-manifest";
|
|
@@ -28,9 +28,6 @@ function formatUnknownError(error) {
|
|
|
28
28
|
if (error instanceof Error && error.message) return error.message;
|
|
29
29
|
return String(error);
|
|
30
30
|
}
|
|
31
|
-
function buildKVKey(appPrefix, cacheKey) {
|
|
32
|
-
return `${appPrefix ? `${appPrefix}:` : ""}${ENTRY_PREFIX}${cacheKey}`;
|
|
33
|
-
}
|
|
34
31
|
function buildCacheEntry(value, tags, now, revalidateSeconds, expireSeconds) {
|
|
35
32
|
const cacheControl = revalidateSeconds === void 0 ? void 0 : expireSeconds === void 0 ? { revalidate: revalidateSeconds } : {
|
|
36
33
|
revalidate: revalidateSeconds,
|
|
@@ -64,6 +61,7 @@ function buildPrerenderKVPairs(serverDir, options) {
|
|
|
64
61
|
const now = options?.now ?? Date.now();
|
|
65
62
|
const ttlSeconds = options?.ttlSeconds ?? DEFAULT_KV_TTL_SECONDS;
|
|
66
63
|
const trailingSlash = manifest.trailingSlash ?? false;
|
|
64
|
+
const keySpace = createKvKeySpace(options?.appPrefix);
|
|
67
65
|
let routeCount = 0;
|
|
68
66
|
for (const route of getRenderedAppRoutes(manifest.routes)) {
|
|
69
67
|
const artifactPathname = route.path ?? route.route;
|
|
@@ -87,7 +85,7 @@ function buildPrerenderKVPairs(serverDir, options) {
|
|
|
87
85
|
const htmlKey = appIsrCacheKey(cachePathname, "html", manifest.buildId);
|
|
88
86
|
const rscKey = appIsrCacheKey(cachePathname, "rsc", manifest.buildId);
|
|
89
87
|
pairs.push({
|
|
90
|
-
key:
|
|
88
|
+
key: keySpace.entryKey(htmlKey),
|
|
91
89
|
value: buildCacheEntry({
|
|
92
90
|
kind: "APP_PAGE",
|
|
93
91
|
html: fs.readFileSync(htmlPath, "utf-8"),
|
|
@@ -99,7 +97,7 @@ function buildPrerenderKVPairs(serverDir, options) {
|
|
|
99
97
|
if (fs.existsSync(rscPath)) {
|
|
100
98
|
const rscData = fs.readFileSync(rscPath).toString("base64");
|
|
101
99
|
pairs.push({
|
|
102
|
-
key:
|
|
100
|
+
key: keySpace.entryKey(rscKey),
|
|
103
101
|
value: buildCacheEntry({
|
|
104
102
|
kind: "APP_PAGE",
|
|
105
103
|
html: "",
|
package/dist/tpr.d.ts
CHANGED
|
@@ -71,7 +71,7 @@ declare function resolveVinextProdServerPath(root: string): string;
|
|
|
71
71
|
* Build KV bulk API pairs from pre-rendered entries.
|
|
72
72
|
*
|
|
73
73
|
* Key format matches the runtime KVCacheHandler exactly:
|
|
74
|
-
*
|
|
74
|
+
* createKvKeySpace().entryKey(isrCacheKey("app", pathname, buildId) + ":html")
|
|
75
75
|
* → "cache:app:<buildId>:<pathname>:html"
|
|
76
76
|
*/
|
|
77
77
|
declare function buildTprKVPairs(entries: Map<string, PrerenderResult>, buildId: string | undefined, defaultRevalidateSeconds: number): Array<{
|
package/dist/tpr.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createKvKeySpace } from "./cache/kv-key.js";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import fs from "node:fs";
|
|
@@ -578,12 +578,13 @@ const MAX_KV_TTL_SECONDS = 720 * 3600;
|
|
|
578
578
|
* Build KV bulk API pairs from pre-rendered entries.
|
|
579
579
|
*
|
|
580
580
|
* Key format matches the runtime KVCacheHandler exactly:
|
|
581
|
-
*
|
|
581
|
+
* createKvKeySpace().entryKey(isrCacheKey("app", pathname, buildId) + ":html")
|
|
582
582
|
* → "cache:app:<buildId>:<pathname>:html"
|
|
583
583
|
*/
|
|
584
584
|
function buildTprKVPairs(entries, buildId, defaultRevalidateSeconds) {
|
|
585
585
|
const now = Date.now();
|
|
586
586
|
const pairs = [];
|
|
587
|
+
const keySpace = createKvKeySpace(void 0);
|
|
587
588
|
for (const [routePath, result] of entries) {
|
|
588
589
|
if (!isTprCacheable(result.headers)) continue;
|
|
589
590
|
const revalidateSeconds = readTprRevalidateHeader(result.headers) ?? defaultRevalidateSeconds;
|
|
@@ -602,7 +603,7 @@ function buildTprKVPairs(entries, buildId, defaultRevalidateSeconds) {
|
|
|
602
603
|
lastModified: now,
|
|
603
604
|
revalidateAt
|
|
604
605
|
};
|
|
605
|
-
const cacheKey =
|
|
606
|
+
const cacheKey = keySpace.entryKey(isrCacheKey("app", routePath, buildId) + ":html");
|
|
606
607
|
pairs.push({
|
|
607
608
|
key: cacheKey,
|
|
608
609
|
value: JSON.stringify(entry),
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { parseWorkersDevUrl } from "./workers-dev-url.js";
|
|
2
|
+
//#region src/worker-deployment-url.ts
|
|
3
|
+
function parseWorkerDeploymentUrl(output) {
|
|
4
|
+
return parseWorkersDevUrl(output) ?? parseCustomDomainUrl(output);
|
|
5
|
+
}
|
|
6
|
+
function parseCustomDomainUrl(output) {
|
|
7
|
+
for (const line of output.split(/\r?\n/)) {
|
|
8
|
+
const match = /^\s*(\S+)\s+\(custom domain(?: - zone (?:id|name): [^)]+)?\)(?: \[([^\]\r\n]+)\])?\s*$/.exec(line);
|
|
9
|
+
const hostname = match?.[1];
|
|
10
|
+
if (!hostname) continue;
|
|
11
|
+
if (match[2]?.split(",").some((flag) => flag.trim() === "disabled")) continue;
|
|
12
|
+
try {
|
|
13
|
+
const url = new URL(`https://${hostname}`);
|
|
14
|
+
if (url.username !== "" || url.password !== "" || url.port !== "" || url.pathname !== "/" || url.search !== "" || url.hash !== "") continue;
|
|
15
|
+
return url.origin;
|
|
16
|
+
} catch {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
//#endregion
|
|
23
|
+
export { parseWorkerDeploymentUrl };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vinext/cloudflare",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.3",
|
|
4
4
|
"description": "Cloudflare adapters for vinext (KV data cache and Images-backed image optimization).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -54,10 +54,10 @@
|
|
|
54
54
|
"@types/node": "^25.9.2",
|
|
55
55
|
"vite": "npm:@voidzero-dev/vite-plus-core@0.2.2",
|
|
56
56
|
"vite-plus": "0.2.2",
|
|
57
|
-
"vinext": "1.0.0-beta.
|
|
57
|
+
"vinext": "1.0.0-beta.3"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
|
-
"vinext": "
|
|
60
|
+
"vinext": "^1.0.0-beta.3"
|
|
61
61
|
},
|
|
62
62
|
"engines": {
|
|
63
63
|
"node": ">=22"
|