@saacms/plugin-cache-kv 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +92 -0
- package/dist/plugin.d.ts +67 -0
- package/dist/plugin.d.ts.map +1 -0
- package/package.json +35 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @saacms/plugin-cache-kv — public surface.
|
|
3
|
+
*
|
|
4
|
+
* Opt-in Cloudflare KV-backed L4 cache plugin per ADR 0023 amendment 2026-05-15
|
|
5
|
+
* (no-platform-dependency principle) and ADR 0024 §"Platform-specific
|
|
6
|
+
* enhancement layer". The framework's caching story works WITHOUT this plugin
|
|
7
|
+
* via per-isolate Map + D1 (the correctness layer); installing this plugin
|
|
8
|
+
* upgrades cache reads to KV (cross-isolate, edge-replicated) for hot paths
|
|
9
|
+
* — per-user OpenAPI snapshots, ETag projections, session shadows, etc.
|
|
10
|
+
*/
|
|
11
|
+
export { cacheKv, kvCacheAdapter } from "./plugin.ts";
|
|
12
|
+
export type { CacheKvOptions, KvCacheAdapter, KvCacheBinding, KvCacheEntry, KvCachePutOptions, } from "./plugin.ts";
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AACrD,YAAY,EACV,cAAc,EACd,cAAc,EACd,cAAc,EACd,YAAY,EACZ,iBAAiB,GAClB,MAAM,aAAa,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// src/plugin.ts
|
|
2
|
+
var TAG_RE = /^[a-zA-Z0-9_:./-]{1,128}$/;
|
|
3
|
+
var TAG_INDEX_PREFIX = "tag:";
|
|
4
|
+
var DEFAULT_TTL_SECONDS = 300;
|
|
5
|
+
var DEFAULT_MAX_ENTRY_BYTES = 1e6;
|
|
6
|
+
function assertSafeTag(tag) {
|
|
7
|
+
if (typeof tag !== "string" || !TAG_RE.test(tag)) {
|
|
8
|
+
throw new Error(`[saacms/cache-kv] invalid tag: ${JSON.stringify(tag)}`);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function tagIndexKey(tag, key) {
|
|
12
|
+
return `${TAG_INDEX_PREFIX}${tag}:${key}`;
|
|
13
|
+
}
|
|
14
|
+
function kvCacheAdapter(opts) {
|
|
15
|
+
const binding = opts.binding;
|
|
16
|
+
const defaultTtl = opts.defaultTtlSeconds ?? DEFAULT_TTL_SECONDS;
|
|
17
|
+
const maxBytes = opts.maxEntryBytes ?? DEFAULT_MAX_ENTRY_BYTES;
|
|
18
|
+
return {
|
|
19
|
+
async get(key) {
|
|
20
|
+
try {
|
|
21
|
+
const raw = await binding.get(key, "json");
|
|
22
|
+
if (raw === null || raw === undefined)
|
|
23
|
+
return null;
|
|
24
|
+
return raw;
|
|
25
|
+
} catch (err) {
|
|
26
|
+
console.warn(`[saacms/cache-kv] get(${JSON.stringify(key)}) failed:`, err);
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
async put(key, value, putOpts) {
|
|
31
|
+
const tags = putOpts?.tags ?? [];
|
|
32
|
+
for (const t of tags)
|
|
33
|
+
assertSafeTag(t);
|
|
34
|
+
const entry = {
|
|
35
|
+
value,
|
|
36
|
+
tags,
|
|
37
|
+
storedAt: new Date().toISOString()
|
|
38
|
+
};
|
|
39
|
+
const json = JSON.stringify(entry);
|
|
40
|
+
if (json.length > maxBytes) {
|
|
41
|
+
console.warn(`[saacms/cache-kv] put(${JSON.stringify(key)}) skipped: entry ${json.length}B exceeds maxEntryBytes ${maxBytes}`);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const ttl = putOpts?.expirationTtl ?? defaultTtl;
|
|
45
|
+
const writeOpts = { expirationTtl: ttl };
|
|
46
|
+
await binding.put(key, json, writeOpts);
|
|
47
|
+
for (const tag of tags) {
|
|
48
|
+
await binding.put(tagIndexKey(tag, key), "", writeOpts);
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
async delete(key) {
|
|
52
|
+
await binding.delete(key);
|
|
53
|
+
},
|
|
54
|
+
async purgeByTag(tag) {
|
|
55
|
+
assertSafeTag(tag);
|
|
56
|
+
const prefix = `${TAG_INDEX_PREFIX}${tag}:`;
|
|
57
|
+
let cursor;
|
|
58
|
+
let purged = 0;
|
|
59
|
+
do {
|
|
60
|
+
const page = await binding.list({ prefix, cursor });
|
|
61
|
+
for (const entry of page.keys) {
|
|
62
|
+
const cacheKey = entry.name.slice(prefix.length);
|
|
63
|
+
await binding.delete(cacheKey);
|
|
64
|
+
await binding.delete(entry.name);
|
|
65
|
+
purged += 1;
|
|
66
|
+
}
|
|
67
|
+
if (page.list_complete) {
|
|
68
|
+
cursor = undefined;
|
|
69
|
+
} else {
|
|
70
|
+
cursor = page.cursor;
|
|
71
|
+
if (!cursor)
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
} while (cursor);
|
|
75
|
+
return { purgedKeys: purged };
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function cacheKv(options) {
|
|
80
|
+
const adapter = kvCacheAdapter(options);
|
|
81
|
+
return {
|
|
82
|
+
name: "@saacms/plugin-cache-kv",
|
|
83
|
+
version: "0.1.0",
|
|
84
|
+
services: {
|
|
85
|
+
cache: adapter
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
export {
|
|
90
|
+
kvCacheAdapter,
|
|
91
|
+
cacheKv
|
|
92
|
+
};
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @saacms/plugin-cache-kv — Cloudflare KV-backed L4 cache.
|
|
3
|
+
*
|
|
4
|
+
* Per ADR 0021 (cache hierarchy) + ADR 0023 amendment 2026-05-15
|
|
5
|
+
* (no-platform-dependency principle): this plugin is the *efficiency* layer.
|
|
6
|
+
* The framework's caching story works without it via per-isolate Map + D1;
|
|
7
|
+
* installing it upgrades cache reads to KV (cross-isolate, edge-replicated)
|
|
8
|
+
* for hot paths — per-user OpenAPI snapshots, ETag projections, etc.
|
|
9
|
+
*
|
|
10
|
+
* The plugin contributes a `services.cache` slot containing a `KvCacheAdapter`
|
|
11
|
+
* bound to a host-supplied KV binding. Runtime cache-wiring (read-through
|
|
12
|
+
* fallthrough to Map → D1, event-driven invalidation) is a separate dispatch;
|
|
13
|
+
* this dispatch ships the adapter + factory.
|
|
14
|
+
*/
|
|
15
|
+
import type { PluginDef } from "@saacms/core";
|
|
16
|
+
export interface CacheKvOptions {
|
|
17
|
+
/** The Workers `KVNamespace` binding; supplied by the host. */
|
|
18
|
+
readonly binding: KvCacheBinding;
|
|
19
|
+
/** Documentational: the binding name the host wired (default `SAACMS_CACHE`). */
|
|
20
|
+
readonly bindingName?: string;
|
|
21
|
+
/** Default TTL applied to entries that don't specify one. Default: 300s. */
|
|
22
|
+
readonly defaultTtlSeconds?: number;
|
|
23
|
+
/** Max bytes per entry; KV's hard cap is 25MB but we default to 1MB. */
|
|
24
|
+
readonly maxEntryBytes?: number;
|
|
25
|
+
}
|
|
26
|
+
/** Subset of `KVNamespace` used by this plugin. Mirrors @cloudflare/workers-types. */
|
|
27
|
+
export interface KvCacheBinding {
|
|
28
|
+
get(key: string, type: "json"): Promise<unknown>;
|
|
29
|
+
get(key: string, type: "text"): Promise<string | null>;
|
|
30
|
+
put(key: string, value: string | ArrayBuffer | ArrayBufferView, opts?: {
|
|
31
|
+
readonly expirationTtl?: number;
|
|
32
|
+
readonly metadata?: unknown;
|
|
33
|
+
}): Promise<void>;
|
|
34
|
+
delete(key: string): Promise<void>;
|
|
35
|
+
list(opts?: {
|
|
36
|
+
readonly prefix?: string;
|
|
37
|
+
readonly cursor?: string;
|
|
38
|
+
}): Promise<{
|
|
39
|
+
readonly keys: ReadonlyArray<{
|
|
40
|
+
readonly name: string;
|
|
41
|
+
readonly metadata?: unknown;
|
|
42
|
+
}>;
|
|
43
|
+
readonly list_complete: boolean;
|
|
44
|
+
readonly cursor?: string;
|
|
45
|
+
}>;
|
|
46
|
+
}
|
|
47
|
+
/** What the plugin stores under each cache key. */
|
|
48
|
+
export interface KvCacheEntry<T = unknown> {
|
|
49
|
+
readonly value: T;
|
|
50
|
+
readonly tags: ReadonlyArray<string>;
|
|
51
|
+
readonly storedAt: string;
|
|
52
|
+
}
|
|
53
|
+
export interface KvCachePutOptions {
|
|
54
|
+
readonly tags?: ReadonlyArray<string>;
|
|
55
|
+
readonly expirationTtl?: number;
|
|
56
|
+
}
|
|
57
|
+
export interface KvCacheAdapter {
|
|
58
|
+
get<T = unknown>(key: string): Promise<KvCacheEntry<T> | null>;
|
|
59
|
+
put<T = unknown>(key: string, value: T, opts?: KvCachePutOptions): Promise<void>;
|
|
60
|
+
delete(key: string): Promise<void>;
|
|
61
|
+
purgeByTag(tag: string): Promise<{
|
|
62
|
+
purgedKeys: number;
|
|
63
|
+
}>;
|
|
64
|
+
}
|
|
65
|
+
export declare function kvCacheAdapter(opts: CacheKvOptions): KvCacheAdapter;
|
|
66
|
+
export declare function cacheKv(options: CacheKvOptions): PluginDef;
|
|
67
|
+
//# sourceMappingURL=plugin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAM7C,MAAM,WAAW,cAAc;IAC7B,+DAA+D;IAC/D,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAA;IAChC,iFAAiF;IACjF,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,4EAA4E;IAC5E,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAA;IACnC,wEAAwE;IACxE,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAA;CAChC;AAED,sFAAsF;AACtF,MAAM,WAAW,cAAc;IAC7B,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAChD,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IACtD,GAAG,CACD,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,GAAG,WAAW,GAAG,eAAe,EAC7C,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,GACtE,OAAO,CAAC,IAAI,CAAC,CAAA;IAChB,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAClC,IAAI,CAAC,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC;QAC3E,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;YAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;SAAE,CAAC,CAAA;QACpF,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAA;QAC/B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KACzB,CAAC,CAAA;CACH;AAED,mDAAmD;AACnD,MAAM,WAAW,YAAY,CAAC,CAAC,GAAG,OAAO;IACvC,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAA;IACjB,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IACpC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;CAC1B;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IACrC,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAA;CAChC;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAA;IAC9D,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAChF,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAClC,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CACzD;AAyBD,wBAAgB,cAAc,CAAC,IAAI,EAAE,cAAc,GAAG,cAAc,CA+EnE;AAMD,wBAAgB,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,SAAS,CAS1D"}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@saacms/plugin-cache-kv",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": {
|
|
7
|
+
"import": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"default": "./dist/index.js"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc --build",
|
|
18
|
+
"typecheck": "tsc --build --noEmit",
|
|
19
|
+
"prepack": "cp package.json package.json.pack-bak && bun run ../../scripts/prepack-pkg.ts",
|
|
20
|
+
"postpack": "mv package.json.pack-bak package.json"
|
|
21
|
+
},
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@saacms/core": "workspace:*",
|
|
27
|
+
"@cloudflare/workers-types": "^4.20240925.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/bun": "latest",
|
|
31
|
+
"typescript": "^5.7.0"
|
|
32
|
+
},
|
|
33
|
+
"main": "./dist/index.js",
|
|
34
|
+
"types": "./dist/index.d.ts"
|
|
35
|
+
}
|