@warlock.js/cache 4.0.171 → 4.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +85 -0
- package/cjs/index.cjs +4088 -0
- package/cjs/index.cjs.map +1 -0
- package/esm/cache-manager.d.mts +314 -0
- package/esm/cache-manager.d.mts.map +1 -0
- package/esm/cache-manager.mjs +486 -0
- package/esm/cache-manager.mjs.map +1 -0
- package/esm/cached/auto-key.d.mts +25 -0
- package/esm/cached/auto-key.d.mts.map +1 -0
- package/esm/cached/auto-key.mjs +55 -0
- package/esm/cached/auto-key.mjs.map +1 -0
- package/esm/cached/cached.d.mts +54 -0
- package/esm/cached/cached.d.mts.map +1 -0
- package/esm/cached/cached.mjs +25 -0
- package/esm/cached/cached.mjs.map +1 -0
- package/esm/cached/index.d.mts +3 -0
- package/esm/cached/index.mjs +5 -0
- package/esm/cached/normalize-args.d.mts +51 -0
- package/esm/cached/normalize-args.d.mts.map +1 -0
- package/esm/cached/normalize-args.mjs +26 -0
- package/esm/cached/normalize-args.mjs.map +1 -0
- package/esm/drivers/base-cache-driver.d.mts +322 -0
- package/esm/drivers/base-cache-driver.d.mts.map +1 -0
- package/esm/drivers/base-cache-driver.mjs +522 -0
- package/esm/drivers/base-cache-driver.mjs.map +1 -0
- package/esm/drivers/file-cache-driver.d.mts +68 -0
- package/esm/drivers/file-cache-driver.d.mts.map +1 -0
- package/esm/drivers/file-cache-driver.mjs +174 -0
- package/esm/drivers/file-cache-driver.mjs.map +1 -0
- package/esm/drivers/index.d.mts +9 -0
- package/esm/drivers/index.mjs +11 -0
- package/esm/drivers/lru-memory-cache-driver.d.mts +136 -0
- package/esm/drivers/lru-memory-cache-driver.d.mts.map +1 -0
- package/esm/drivers/lru-memory-cache-driver.mjs +317 -0
- package/esm/drivers/lru-memory-cache-driver.mjs.map +1 -0
- package/esm/drivers/memory-cache-driver.d.mts +112 -0
- package/esm/drivers/memory-cache-driver.d.mts.map +1 -0
- package/esm/drivers/memory-cache-driver.mjs +241 -0
- package/esm/drivers/memory-cache-driver.mjs.map +1 -0
- package/esm/drivers/memory-extended-cache-driver.d.mts +17 -0
- package/esm/drivers/memory-extended-cache-driver.d.mts.map +1 -0
- package/esm/drivers/memory-extended-cache-driver.mjs +34 -0
- package/esm/drivers/memory-extended-cache-driver.mjs.map +1 -0
- package/esm/drivers/mock-cache-driver.d.mts +137 -0
- package/esm/drivers/mock-cache-driver.d.mts.map +1 -0
- package/esm/drivers/mock-cache-driver.mjs +226 -0
- package/esm/drivers/mock-cache-driver.mjs.map +1 -0
- package/esm/drivers/null-cache-driver.d.mts +69 -0
- package/esm/drivers/null-cache-driver.d.mts.map +1 -0
- package/esm/drivers/null-cache-driver.mjs +92 -0
- package/esm/drivers/null-cache-driver.mjs.map +1 -0
- package/esm/drivers/pg-cache-driver.d.mts +148 -0
- package/esm/drivers/pg-cache-driver.d.mts.map +1 -0
- package/esm/drivers/pg-cache-driver.mjs +437 -0
- package/esm/drivers/pg-cache-driver.mjs.map +1 -0
- package/esm/drivers/redis-cache-driver.d.mts +86 -0
- package/esm/drivers/redis-cache-driver.d.mts.map +1 -0
- package/esm/drivers/redis-cache-driver.mjs +312 -0
- package/esm/drivers/redis-cache-driver.mjs.map +1 -0
- package/esm/index.d.mts +21 -0
- package/esm/index.mjs +24 -0
- package/esm/list/index.d.mts +1 -0
- package/esm/list/memory-cache-list.d.mts +77 -0
- package/esm/list/memory-cache-list.d.mts.map +1 -0
- package/esm/list/memory-cache-list.mjs +119 -0
- package/esm/list/memory-cache-list.mjs.map +1 -0
- package/esm/metrics.d.mts +118 -0
- package/esm/metrics.d.mts.map +1 -0
- package/esm/metrics.mjs +197 -0
- package/esm/metrics.mjs.map +1 -0
- package/esm/scoped-cache.d.mts +205 -0
- package/esm/scoped-cache.d.mts.map +1 -0
- package/esm/scoped-cache.mjs +274 -0
- package/esm/scoped-cache.mjs.map +1 -0
- package/esm/tagged-cache.d.mts +89 -0
- package/esm/tagged-cache.d.mts.map +1 -0
- package/esm/tagged-cache.mjs +147 -0
- package/esm/tagged-cache.mjs.map +1 -0
- package/esm/tagged-scoped-cache.d.mts +111 -0
- package/esm/tagged-scoped-cache.d.mts.map +1 -0
- package/esm/tagged-scoped-cache.mjs +142 -0
- package/esm/tagged-scoped-cache.mjs.map +1 -0
- package/esm/types.d.mts +1067 -0
- package/esm/types.d.mts.map +1 -0
- package/esm/types.mjs +62 -0
- package/esm/types.mjs.map +1 -0
- package/esm/utils.d.mts +161 -0
- package/esm/utils.d.mts.map +1 -0
- package/esm/utils.mjs +222 -0
- package/esm/utils.mjs.map +1 -0
- package/llms-full.txt +2071 -0
- package/llms.txt +28 -0
- package/package.json +53 -39
- package/skills/apply-cache-patterns/SKILL.md +97 -0
- package/skills/cache-basics/SKILL.md +121 -0
- package/skills/configure-pg-cache/SKILL.md +115 -0
- package/skills/configure-set-options/SKILL.md +96 -0
- package/skills/handle-cache-errors/SKILL.md +91 -0
- package/skills/observe-cache/SKILL.md +103 -0
- package/skills/overview/SKILL.md +69 -0
- package/skills/pick-cache-driver/SKILL.md +115 -0
- package/skills/test-cache-code/SKILL.md +219 -0
- package/skills/use-cache-atomic/SKILL.md +67 -0
- package/skills/use-cache-bulk/SKILL.md +57 -0
- package/skills/use-cache-list/SKILL.md +85 -0
- package/skills/use-cache-lock/SKILL.md +104 -0
- package/skills/use-cache-namespace/SKILL.md +88 -0
- package/skills/use-cache-similarity/SKILL.md +94 -0
- package/skills/use-cache-tags/SKILL.md +85 -0
- package/skills/use-cache-update-merge/SKILL.md +84 -0
- package/skills/use-cache-utils/SKILL.md +89 -0
- package/skills/use-cached-hof/SKILL.md +102 -0
- package/skills/use-swr/SKILL.md +104 -0
- package/cjs/cache-manager.d.ts +0 -163
- package/cjs/cache-manager.d.ts.map +0 -1
- package/cjs/cache-manager.js +0 -322
- package/cjs/cache-manager.js.map +0 -1
- package/cjs/drivers/base-cache-driver.d.ts +0 -152
- package/cjs/drivers/base-cache-driver.d.ts.map +0 -1
- package/cjs/drivers/base-cache-driver.js +0 -321
- package/cjs/drivers/base-cache-driver.js.map +0 -1
- package/cjs/drivers/file-cache-driver.d.ts +0 -45
- package/cjs/drivers/file-cache-driver.d.ts.map +0 -1
- package/cjs/drivers/file-cache-driver.js +0 -133
- package/cjs/drivers/file-cache-driver.js.map +0 -1
- package/cjs/drivers/index.d.ts +0 -8
- package/cjs/drivers/index.d.ts.map +0 -1
- package/cjs/drivers/lru-memory-cache-driver.d.ts +0 -98
- package/cjs/drivers/lru-memory-cache-driver.d.ts.map +0 -1
- package/cjs/drivers/lru-memory-cache-driver.js +0 -252
- package/cjs/drivers/lru-memory-cache-driver.js.map +0 -1
- package/cjs/drivers/memory-cache-driver.d.ts +0 -82
- package/cjs/drivers/memory-cache-driver.d.ts.map +0 -1
- package/cjs/drivers/memory-cache-driver.js +0 -218
- package/cjs/drivers/memory-cache-driver.js.map +0 -1
- package/cjs/drivers/memory-extended-cache-driver.d.ts +0 -13
- package/cjs/drivers/memory-extended-cache-driver.d.ts.map +0 -1
- package/cjs/drivers/memory-extended-cache-driver.js +0 -25
- package/cjs/drivers/memory-extended-cache-driver.js.map +0 -1
- package/cjs/drivers/null-cache-driver.d.ts +0 -58
- package/cjs/drivers/null-cache-driver.d.ts.map +0 -1
- package/cjs/drivers/null-cache-driver.js +0 -84
- package/cjs/drivers/null-cache-driver.js.map +0 -1
- package/cjs/drivers/redis-cache-driver.d.ts +0 -57
- package/cjs/drivers/redis-cache-driver.d.ts.map +0 -1
- package/cjs/drivers/redis-cache-driver.js +0 -263
- package/cjs/drivers/redis-cache-driver.js.map +0 -1
- package/cjs/index.d.ts +0 -6
- package/cjs/index.d.ts.map +0 -1
- package/cjs/index.js +0 -1
- package/cjs/index.js.map +0 -1
- package/cjs/tagged-cache.d.ts +0 -77
- package/cjs/tagged-cache.d.ts.map +0 -1
- package/cjs/tagged-cache.js +0 -160
- package/cjs/tagged-cache.js.map +0 -1
- package/cjs/types.d.ts +0 -391
- package/cjs/types.d.ts.map +0 -1
- package/cjs/types.js +0 -36
- package/cjs/types.js.map +0 -1
- package/cjs/utils.d.ts +0 -50
- package/cjs/utils.d.ts.map +0 -1
- package/cjs/utils.js +0 -55
- package/cjs/utils.js.map +0 -1
- package/esm/cache-manager.d.ts +0 -163
- package/esm/cache-manager.d.ts.map +0 -1
- package/esm/cache-manager.js +0 -322
- package/esm/cache-manager.js.map +0 -1
- package/esm/drivers/base-cache-driver.d.ts +0 -152
- package/esm/drivers/base-cache-driver.d.ts.map +0 -1
- package/esm/drivers/base-cache-driver.js +0 -321
- package/esm/drivers/base-cache-driver.js.map +0 -1
- package/esm/drivers/file-cache-driver.d.ts +0 -45
- package/esm/drivers/file-cache-driver.d.ts.map +0 -1
- package/esm/drivers/file-cache-driver.js +0 -133
- package/esm/drivers/file-cache-driver.js.map +0 -1
- package/esm/drivers/index.d.ts +0 -8
- package/esm/drivers/index.d.ts.map +0 -1
- package/esm/drivers/lru-memory-cache-driver.d.ts +0 -98
- package/esm/drivers/lru-memory-cache-driver.d.ts.map +0 -1
- package/esm/drivers/lru-memory-cache-driver.js +0 -252
- package/esm/drivers/lru-memory-cache-driver.js.map +0 -1
- package/esm/drivers/memory-cache-driver.d.ts +0 -82
- package/esm/drivers/memory-cache-driver.d.ts.map +0 -1
- package/esm/drivers/memory-cache-driver.js +0 -218
- package/esm/drivers/memory-cache-driver.js.map +0 -1
- package/esm/drivers/memory-extended-cache-driver.d.ts +0 -13
- package/esm/drivers/memory-extended-cache-driver.d.ts.map +0 -1
- package/esm/drivers/memory-extended-cache-driver.js +0 -25
- package/esm/drivers/memory-extended-cache-driver.js.map +0 -1
- package/esm/drivers/null-cache-driver.d.ts +0 -58
- package/esm/drivers/null-cache-driver.d.ts.map +0 -1
- package/esm/drivers/null-cache-driver.js +0 -84
- package/esm/drivers/null-cache-driver.js.map +0 -1
- package/esm/drivers/redis-cache-driver.d.ts +0 -57
- package/esm/drivers/redis-cache-driver.d.ts.map +0 -1
- package/esm/drivers/redis-cache-driver.js +0 -263
- package/esm/drivers/redis-cache-driver.js.map +0 -1
- package/esm/index.d.ts +0 -6
- package/esm/index.d.ts.map +0 -1
- package/esm/index.js +0 -1
- package/esm/index.js.map +0 -1
- package/esm/tagged-cache.d.ts +0 -77
- package/esm/tagged-cache.d.ts.map +0 -1
- package/esm/tagged-cache.js +0 -160
- package/esm/tagged-cache.js.map +0 -1
- package/esm/types.d.ts +0 -391
- package/esm/types.d.ts.map +0 -1
- package/esm/types.js +0 -36
- package/esm/types.js.map +0 -1
- package/esm/utils.d.ts +0 -50
- package/esm/utils.d.ts.map +0 -1
- package/esm/utils.js +0 -55
- package/esm/utils.js.map +0 -1
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
import { CacheConfigurationError, CacheUnsupportedError } from "../types.mjs";
|
|
2
|
+
import { BaseCacheDriver } from "./base-cache-driver.mjs";
|
|
3
|
+
|
|
4
|
+
//#region ../../@warlock.js/cache/src/drivers/pg-cache-driver.ts
|
|
5
|
+
/**
|
|
6
|
+
* Allowed characters in a Postgres identifier (table name). We accept the
|
|
7
|
+
* conservative ASCII subset and reject anything else — interpolating an
|
|
8
|
+
* arbitrary string into DDL would be a SQL-injection footgun.
|
|
9
|
+
*/
|
|
10
|
+
const SAFE_IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
11
|
+
/**
|
|
12
|
+
* Postgres cache driver with optional pgvector similarity support.
|
|
13
|
+
*
|
|
14
|
+
* Connection lifecycle is the caller's responsibility — pass an already-built
|
|
15
|
+
* `pg.Pool` or `pg.Client` via the `client` option. The driver never closes it
|
|
16
|
+
* on `cache.disconnect()`, so the same pool can serve queries elsewhere in
|
|
17
|
+
* the app.
|
|
18
|
+
*
|
|
19
|
+
* Schema is not auto-migrated. Call `driver.schema()` to get the DDL string
|
|
20
|
+
* and run it through whichever migration tool you use.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* import { Pool } from "pg";
|
|
24
|
+
* import { cache, PgCacheDriver } from "@warlock.js/cache";
|
|
25
|
+
*
|
|
26
|
+
* const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
27
|
+
*
|
|
28
|
+
* cache.setCacheConfigurations({
|
|
29
|
+
* default: "pg",
|
|
30
|
+
* drivers: { pg: PgCacheDriver },
|
|
31
|
+
* options: { pg: { client: pool, table: "warlock_cache", ttl: "1h" } },
|
|
32
|
+
* });
|
|
33
|
+
*
|
|
34
|
+
* await cache.init();
|
|
35
|
+
*
|
|
36
|
+
* // Run once, via your own migration tooling:
|
|
37
|
+
* // await pool.query(driver.schema());
|
|
38
|
+
*/
|
|
39
|
+
var PgCacheDriver = class extends BaseCacheDriver {
|
|
40
|
+
constructor(..._args) {
|
|
41
|
+
super(..._args);
|
|
42
|
+
this.name = "pg";
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* {@inheritdoc}
|
|
46
|
+
*
|
|
47
|
+
* Validates `client` presence and the table name before storing options.
|
|
48
|
+
*/
|
|
49
|
+
setOptions(options) {
|
|
50
|
+
if (!options || !options.client || typeof options.client.query !== "function") throw new CacheConfigurationError("Pg cache driver requires a 'client' option implementing { query(text, values) } — pass a pg.Pool or pg.Client.");
|
|
51
|
+
const table = options.table ?? "warlock_cache";
|
|
52
|
+
if (!SAFE_IDENT.test(table)) throw new CacheConfigurationError(`Pg cache driver: invalid table name '${table}'. Allowed: [A-Za-z_][A-Za-z0-9_]*.`);
|
|
53
|
+
if (options.vector) {
|
|
54
|
+
const dim = options.vector.dimensions;
|
|
55
|
+
if (!Number.isInteger(dim) || dim <= 0) throw new CacheConfigurationError(`Pg cache driver: vector.dimensions must be a positive integer; got ${dim}.`);
|
|
56
|
+
const idx = options.vector.index ?? "hnsw";
|
|
57
|
+
if (idx !== "hnsw" && idx !== "ivfflat") throw new CacheConfigurationError(`Pg cache driver: vector.index must be 'hnsw' or 'ivfflat'; got '${idx}'.`);
|
|
58
|
+
}
|
|
59
|
+
return super.setOptions({
|
|
60
|
+
...options,
|
|
61
|
+
table
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Lazy pgvector availability check. Runs once on the first vector op and
|
|
66
|
+
* caches the result — subsequent ops are zero-overhead. Throws
|
|
67
|
+
* {@link CacheConfigurationError} if the extension isn't installed; throws
|
|
68
|
+
* {@link CacheUnsupportedError} if `vector` config wasn't provided at all.
|
|
69
|
+
*/
|
|
70
|
+
async ensureVectorReady() {
|
|
71
|
+
if (!this.options.vector) throw new CacheUnsupportedError("'pg' driver: similarity retrieval requires the 'vector' config block. Set options.vector.dimensions and reconnect.");
|
|
72
|
+
if (this.vectorReady === true) return;
|
|
73
|
+
const { rows } = await this.pgClient.query(`SELECT 1 FROM pg_extension WHERE extname = 'vector'`);
|
|
74
|
+
if (rows.length === 0) throw new CacheConfigurationError("'pg' driver: pgvector extension not installed. Run 'CREATE EXTENSION vector;' or remove the 'vector' config option.");
|
|
75
|
+
this.vectorReady = true;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Format a numeric vector for pgvector ingestion. The `vector` type accepts
|
|
79
|
+
* a string literal `'[1,2,3]'` cast via `::vector` — this avoids depending
|
|
80
|
+
* on the binary protocol and works against any pg client.
|
|
81
|
+
*/
|
|
82
|
+
formatVector(vector) {
|
|
83
|
+
return `[${vector.join(",")}]`;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Resolved table name. Always defined post-`setOptions` — the validator
|
|
87
|
+
* fills in the default.
|
|
88
|
+
*/
|
|
89
|
+
get table() {
|
|
90
|
+
return this.options.table ?? "warlock_cache";
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The user-supplied `pg.Pool` / `pg.Client`. Use this rather than `this.client`
|
|
94
|
+
* (which has a generic fallback to `this`) for actual queries.
|
|
95
|
+
*/
|
|
96
|
+
get pgClient() {
|
|
97
|
+
return this.options.client;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Compute an absolute `expires_at` Date for the given relative TTL in seconds,
|
|
101
|
+
* or `null` when the entry should not expire (`Infinity` / 0 / undefined).
|
|
102
|
+
*/
|
|
103
|
+
ttlToExpiresAt(ttl) {
|
|
104
|
+
if (!ttl || ttl === Infinity) return null;
|
|
105
|
+
return new Date(Date.now() + ttl * 1e3);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Return the SQL needed to provision the cache table + index. Run once via
|
|
109
|
+
* the caller's migration tooling — the driver never auto-migrates.
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* await pool.query(driver.schema());
|
|
113
|
+
*/
|
|
114
|
+
schema() {
|
|
115
|
+
const t = this.table;
|
|
116
|
+
const vec = this.options.vector;
|
|
117
|
+
const columns = [
|
|
118
|
+
` key TEXT PRIMARY KEY,`,
|
|
119
|
+
` value JSONB NOT NULL,`,
|
|
120
|
+
` expires_at TIMESTAMPTZ,`,
|
|
121
|
+
` stale_at TIMESTAMPTZ,`,
|
|
122
|
+
` tags TEXT[] NOT NULL DEFAULT '{}'::TEXT[]`
|
|
123
|
+
];
|
|
124
|
+
if (vec) {
|
|
125
|
+
columns[columns.length - 1] = columns[columns.length - 1] + ",";
|
|
126
|
+
columns.push(` embedding VECTOR(${vec.dimensions})`);
|
|
127
|
+
}
|
|
128
|
+
const lines = [
|
|
129
|
+
`CREATE TABLE IF NOT EXISTS ${t} (`,
|
|
130
|
+
...columns,
|
|
131
|
+
`);`,
|
|
132
|
+
`CREATE INDEX IF NOT EXISTS idx_${t}_expires_at ON ${t} (expires_at);`,
|
|
133
|
+
`CREATE INDEX IF NOT EXISTS idx_${t}_tags ON ${t} USING GIN (tags);`
|
|
134
|
+
];
|
|
135
|
+
if (vec) {
|
|
136
|
+
const idx = vec.index ?? "hnsw";
|
|
137
|
+
lines.push(`CREATE INDEX IF NOT EXISTS idx_${t}_embedding ON ${t} USING ${idx} (embedding vector_cosine_ops);`);
|
|
138
|
+
}
|
|
139
|
+
return lines.join("\n");
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* {@inheritdoc}
|
|
143
|
+
*/
|
|
144
|
+
async connect() {
|
|
145
|
+
this.log("connecting");
|
|
146
|
+
this.log("connected");
|
|
147
|
+
await this.emit("connected");
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* {@inheritdoc}
|
|
151
|
+
*
|
|
152
|
+
* Does NOT close the user-supplied client — lifecycle stays with the caller.
|
|
153
|
+
*/
|
|
154
|
+
async disconnect() {
|
|
155
|
+
this.log("disconnected");
|
|
156
|
+
await this.emit("disconnected");
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* {@inheritdoc}
|
|
160
|
+
*/
|
|
161
|
+
async set(key, value, ttlOrOptions) {
|
|
162
|
+
const parsedKey = this.parseKey(key);
|
|
163
|
+
const { ttl, tags, onConflict, vector, staleAt } = this.resolveSetOptions(ttlOrOptions);
|
|
164
|
+
if (vector) {
|
|
165
|
+
if (!this.options.vector) throw new CacheUnsupportedError("'pg' driver: cannot index a vector without options.vector configuration — set { dimensions } and recreate the table via driver.schema().");
|
|
166
|
+
const expected = this.options.vector.dimensions;
|
|
167
|
+
if (vector.length !== expected) throw new CacheConfigurationError(`Pg cache driver: vector dimension mismatch — expected ${expected}, got ${vector.length}.`);
|
|
168
|
+
await this.ensureVectorReady();
|
|
169
|
+
}
|
|
170
|
+
this.log("caching", parsedKey);
|
|
171
|
+
const expiresAt = this.ttlToExpiresAt(ttl);
|
|
172
|
+
const staleAtDate = staleAt !== void 0 ? new Date(staleAt) : null;
|
|
173
|
+
const tagsArr = tags ?? [];
|
|
174
|
+
const serialized = JSON.stringify(value);
|
|
175
|
+
const vecLiteral = vector ? this.formatVector(vector) : null;
|
|
176
|
+
const t = this.table;
|
|
177
|
+
const cols = [
|
|
178
|
+
"key",
|
|
179
|
+
"value",
|
|
180
|
+
"expires_at",
|
|
181
|
+
"stale_at",
|
|
182
|
+
"tags"
|
|
183
|
+
];
|
|
184
|
+
const placeholders = [
|
|
185
|
+
"$1",
|
|
186
|
+
"$2::jsonb",
|
|
187
|
+
"$3",
|
|
188
|
+
"$4",
|
|
189
|
+
"$5"
|
|
190
|
+
];
|
|
191
|
+
const params = [
|
|
192
|
+
parsedKey,
|
|
193
|
+
serialized,
|
|
194
|
+
expiresAt,
|
|
195
|
+
staleAtDate,
|
|
196
|
+
tagsArr
|
|
197
|
+
];
|
|
198
|
+
if (vecLiteral !== null) {
|
|
199
|
+
cols.push("embedding");
|
|
200
|
+
placeholders.push(`$${params.length + 1}::vector`);
|
|
201
|
+
params.push(vecLiteral);
|
|
202
|
+
}
|
|
203
|
+
const colList = cols.join(", ");
|
|
204
|
+
const valList = placeholders.join(", ");
|
|
205
|
+
const setClause = cols.slice(1).map((c) => `${c} = EXCLUDED.${c}`).join(", ");
|
|
206
|
+
const updateSetClause = cols.slice(1).map((c, i) => `${c} = ${placeholders[i + 1]}`).join(", ");
|
|
207
|
+
if (onConflict === "create") {
|
|
208
|
+
const { rows } = await this.pgClient.query(`INSERT INTO ${t}(${colList})
|
|
209
|
+
VALUES (${valList})
|
|
210
|
+
ON CONFLICT (key) DO UPDATE
|
|
211
|
+
SET ${setClause}
|
|
212
|
+
WHERE ${t}.expires_at IS NOT NULL AND ${t}.expires_at < now()
|
|
213
|
+
RETURNING value`, params);
|
|
214
|
+
if (rows.length === 0) return {
|
|
215
|
+
wasSet: false,
|
|
216
|
+
existing: await this.get(key)
|
|
217
|
+
};
|
|
218
|
+
if (tags && tags.length > 0) await this.applyTags(parsedKey, tags);
|
|
219
|
+
this.log("cached", parsedKey);
|
|
220
|
+
await this.emit("set", {
|
|
221
|
+
key: parsedKey,
|
|
222
|
+
value,
|
|
223
|
+
ttl
|
|
224
|
+
});
|
|
225
|
+
return {
|
|
226
|
+
wasSet: true,
|
|
227
|
+
existing: null
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
if (onConflict === "update") {
|
|
231
|
+
const { rows } = await this.pgClient.query(`UPDATE ${t}
|
|
232
|
+
SET ${updateSetClause}
|
|
233
|
+
WHERE key = $1 AND (expires_at IS NULL OR expires_at > now())
|
|
234
|
+
RETURNING value`, params);
|
|
235
|
+
if (rows.length === 0) return {
|
|
236
|
+
wasSet: false,
|
|
237
|
+
existing: null
|
|
238
|
+
};
|
|
239
|
+
if (tags && tags.length > 0) await this.applyTags(parsedKey, tags);
|
|
240
|
+
this.log("cached", parsedKey);
|
|
241
|
+
await this.emit("set", {
|
|
242
|
+
key: parsedKey,
|
|
243
|
+
value,
|
|
244
|
+
ttl
|
|
245
|
+
});
|
|
246
|
+
return {
|
|
247
|
+
wasSet: true,
|
|
248
|
+
existing: null
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
await this.pgClient.query(`INSERT INTO ${t}(${colList})
|
|
252
|
+
VALUES (${valList})
|
|
253
|
+
ON CONFLICT (key) DO UPDATE
|
|
254
|
+
SET ${setClause}`, params);
|
|
255
|
+
if (tags && tags.length > 0) await this.applyTags(parsedKey, tags);
|
|
256
|
+
this.log("cached", parsedKey);
|
|
257
|
+
await this.emit("set", {
|
|
258
|
+
key: parsedKey,
|
|
259
|
+
value,
|
|
260
|
+
ttl
|
|
261
|
+
});
|
|
262
|
+
return value;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* {@inheritdoc}
|
|
266
|
+
*/
|
|
267
|
+
async get(key) {
|
|
268
|
+
const parsedKey = this.parseKey(key);
|
|
269
|
+
this.log("fetching", parsedKey);
|
|
270
|
+
const t = this.table;
|
|
271
|
+
const { rows } = await this.pgClient.query(`SELECT value FROM ${t}
|
|
272
|
+
WHERE key = $1 AND (expires_at IS NULL OR expires_at > now())`, [parsedKey]);
|
|
273
|
+
if (rows.length === 0) {
|
|
274
|
+
this.log("notFound", parsedKey);
|
|
275
|
+
await this.emit("miss", { key: parsedKey });
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
this.log("fetched", parsedKey);
|
|
279
|
+
let value = rows[0].value;
|
|
280
|
+
if (typeof value === "string") try {
|
|
281
|
+
value = JSON.parse(value);
|
|
282
|
+
} catch {}
|
|
283
|
+
if (value === null || value === void 0) {
|
|
284
|
+
await this.emit("hit", {
|
|
285
|
+
key: parsedKey,
|
|
286
|
+
value
|
|
287
|
+
});
|
|
288
|
+
return value;
|
|
289
|
+
}
|
|
290
|
+
const type = typeof value;
|
|
291
|
+
if (type === "string" || type === "number" || type === "boolean") {
|
|
292
|
+
await this.emit("hit", {
|
|
293
|
+
key: parsedKey,
|
|
294
|
+
value
|
|
295
|
+
});
|
|
296
|
+
return value;
|
|
297
|
+
}
|
|
298
|
+
try {
|
|
299
|
+
const cloned = structuredClone(value);
|
|
300
|
+
await this.emit("hit", {
|
|
301
|
+
key: parsedKey,
|
|
302
|
+
value: cloned
|
|
303
|
+
});
|
|
304
|
+
return cloned;
|
|
305
|
+
} catch (error) {
|
|
306
|
+
this.logError(`Failed to clone cached value for ${parsedKey}`, error);
|
|
307
|
+
throw error;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Read the raw {@link CacheData} wrapper, including `staleAt` metadata.
|
|
312
|
+
* Returns `null` for missing or expired rows — `swr()` consumes this to
|
|
313
|
+
* branch on freshness without going through `get()`'s clone-and-emit path.
|
|
314
|
+
*/
|
|
315
|
+
async getEntry(key) {
|
|
316
|
+
const parsedKey = this.parseKey(key);
|
|
317
|
+
const t = this.table;
|
|
318
|
+
const { rows } = await this.pgClient.query(`SELECT value, expires_at, stale_at FROM ${t}
|
|
319
|
+
WHERE key = $1 AND (expires_at IS NULL OR expires_at > now())`, [parsedKey]);
|
|
320
|
+
if (rows.length === 0) return null;
|
|
321
|
+
let data = rows[0].value;
|
|
322
|
+
if (typeof data === "string") try {
|
|
323
|
+
data = JSON.parse(data);
|
|
324
|
+
} catch {}
|
|
325
|
+
const entry = { data };
|
|
326
|
+
const expiresAtRaw = rows[0].expires_at;
|
|
327
|
+
if (expiresAtRaw) entry.expiresAt = (expiresAtRaw instanceof Date ? expiresAtRaw : new Date(expiresAtRaw)).getTime();
|
|
328
|
+
const staleAtRaw = rows[0].stale_at;
|
|
329
|
+
if (staleAtRaw) entry.staleAt = (staleAtRaw instanceof Date ? staleAtRaw : new Date(staleAtRaw)).getTime();
|
|
330
|
+
return entry;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* {@inheritdoc}
|
|
334
|
+
*/
|
|
335
|
+
async remove(key) {
|
|
336
|
+
const parsedKey = this.parseKey(key);
|
|
337
|
+
this.log("removing", parsedKey);
|
|
338
|
+
const t = this.table;
|
|
339
|
+
await this.pgClient.query(`DELETE FROM ${t} WHERE key = $1`, [parsedKey]);
|
|
340
|
+
this.log("removed", parsedKey);
|
|
341
|
+
await this.emit("removed", { key: parsedKey });
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* {@inheritdoc}
|
|
345
|
+
*
|
|
346
|
+
* Deletes all rows whose key equals the namespace exactly or starts with
|
|
347
|
+
* `<namespace>.` — same boundary semantics as the other drivers.
|
|
348
|
+
*/
|
|
349
|
+
async removeNamespace(namespace) {
|
|
350
|
+
const parsed = this.parseKey(namespace);
|
|
351
|
+
this.log("clearing", parsed || "(all)");
|
|
352
|
+
const t = this.table;
|
|
353
|
+
if (parsed === "") await this.pgClient.query(`DELETE FROM ${t}`);
|
|
354
|
+
else {
|
|
355
|
+
const escaped = parsed.replace(/\\/g, "\\\\").replace(/_/g, "\\_").replace(/%/g, "\\%");
|
|
356
|
+
await this.pgClient.query(`DELETE FROM ${t} WHERE key = $1 OR key LIKE $2 ESCAPE '\\'`, [parsed, `${escaped}.%`]);
|
|
357
|
+
}
|
|
358
|
+
this.log("cleared", parsed || "(all)");
|
|
359
|
+
return this;
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* {@inheritdoc}
|
|
363
|
+
*
|
|
364
|
+
* Honors `globalPrefix` — when configured, scopes the flush to entries
|
|
365
|
+
* under the prefix rather than truncating the entire table (which could
|
|
366
|
+
* wipe sibling tenants sharing the same Postgres database).
|
|
367
|
+
*/
|
|
368
|
+
async flush() {
|
|
369
|
+
this.log("flushing");
|
|
370
|
+
if (this.options.globalPrefix) await this.removeNamespace("");
|
|
371
|
+
else await this.pgClient.query(`DELETE FROM ${this.table}`);
|
|
372
|
+
this.log("flushed");
|
|
373
|
+
await this.emit("flushed");
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* {@inheritdoc}
|
|
377
|
+
*
|
|
378
|
+
* pgvector-backed similarity. Uses the `<=>` cosine-distance operator
|
|
379
|
+
* (lower distance = higher similarity) and converts to cosine similarity
|
|
380
|
+
* as `1 - distance` so the returned `score` matches the rest of the
|
|
381
|
+
* package (`[0, 1]`, higher is more similar).
|
|
382
|
+
*
|
|
383
|
+
* Honors `topK`, `threshold`, and an optional `tags` filter (native
|
|
384
|
+
* `tags && $tags` overlap query — much faster than the meta-key path).
|
|
385
|
+
*
|
|
386
|
+
* Throws {@link CacheUnsupportedError} when `options.vector` was not
|
|
387
|
+
* configured at driver setup; throws {@link CacheConfigurationError} when
|
|
388
|
+
* the pgvector extension is missing or the query vector's dimension count
|
|
389
|
+
* doesn't match the configured one.
|
|
390
|
+
*/
|
|
391
|
+
async similar(vector, options) {
|
|
392
|
+
if (!this.options.vector) throw new CacheUnsupportedError("'pg' driver: similarity retrieval requires the 'vector' config block. Set options.vector.dimensions and reconnect.");
|
|
393
|
+
const expected = this.options.vector.dimensions;
|
|
394
|
+
if (vector.length !== expected) throw new CacheConfigurationError(`Pg cache driver: vector dimension mismatch — expected ${expected}, got ${vector.length}.`);
|
|
395
|
+
if (!Number.isInteger(options.topK) || options.topK <= 0) throw new CacheConfigurationError(`Pg cache driver: similar.topK must be a positive integer; got ${options.topK}.`);
|
|
396
|
+
await this.ensureVectorReady();
|
|
397
|
+
const t = this.table;
|
|
398
|
+
const params = [this.formatVector(vector)];
|
|
399
|
+
let tagFilter = "";
|
|
400
|
+
if (options.tags && options.tags.length > 0) {
|
|
401
|
+
params.push(options.tags);
|
|
402
|
+
tagFilter = `AND tags && $${params.length}`;
|
|
403
|
+
}
|
|
404
|
+
params.push(options.topK);
|
|
405
|
+
const topKParam = `$${params.length}`;
|
|
406
|
+
const { rows } = await this.pgClient.query(`SELECT key, value, 1 - (embedding <=> $1::vector) AS score
|
|
407
|
+
FROM ${t}
|
|
408
|
+
WHERE embedding IS NOT NULL
|
|
409
|
+
AND (expires_at IS NULL OR expires_at > now())
|
|
410
|
+
${tagFilter}
|
|
411
|
+
ORDER BY embedding <=> $1::vector
|
|
412
|
+
LIMIT ${topKParam}`, params);
|
|
413
|
+
const hits = [];
|
|
414
|
+
for (const row of rows) {
|
|
415
|
+
const score = Number(row.score);
|
|
416
|
+
if (options.threshold !== void 0 && score < options.threshold) continue;
|
|
417
|
+
let value = row.value;
|
|
418
|
+
if (typeof value === "string") try {
|
|
419
|
+
value = JSON.parse(value);
|
|
420
|
+
} catch {}
|
|
421
|
+
if (value !== null && value !== void 0) {
|
|
422
|
+
const ty = typeof value;
|
|
423
|
+
if (ty !== "string" && ty !== "number" && ty !== "boolean") value = structuredClone(value);
|
|
424
|
+
}
|
|
425
|
+
hits.push({
|
|
426
|
+
key: row.key,
|
|
427
|
+
value,
|
|
428
|
+
score
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
return hits;
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
//#endregion
|
|
436
|
+
export { PgCacheDriver };
|
|
437
|
+
//# sourceMappingURL=pg-cache-driver.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pg-cache-driver.mjs","names":[],"sources":["../../../../../../@warlock.js/cache/src/drivers/pg-cache-driver.ts"],"sourcesContent":["import type {\n CacheData,\n CacheDriver,\n CacheKey,\n CacheSetOptions,\n CacheSetResult,\n CacheSimilarHit,\n CacheSimilarOptions,\n CacheTtl,\n PgCacheOptions,\n PgClientLike,\n} from \"../types\";\nimport { CacheConfigurationError, CacheUnsupportedError } from \"../types\";\nimport { BaseCacheDriver } from \"./base-cache-driver\";\n\n/**\n * Allowed characters in a Postgres identifier (table name). We accept the\n * conservative ASCII subset and reject anything else — interpolating an\n * arbitrary string into DDL would be a SQL-injection footgun.\n */\nconst SAFE_IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;\n\n/**\n * Postgres cache driver with optional pgvector similarity support.\n *\n * Connection lifecycle is the caller's responsibility — pass an already-built\n * `pg.Pool` or `pg.Client` via the `client` option. The driver never closes it\n * on `cache.disconnect()`, so the same pool can serve queries elsewhere in\n * the app.\n *\n * Schema is not auto-migrated. Call `driver.schema()` to get the DDL string\n * and run it through whichever migration tool you use.\n *\n * @example\n * import { Pool } from \"pg\";\n * import { cache, PgCacheDriver } from \"@warlock.js/cache\";\n *\n * const pool = new Pool({ connectionString: process.env.DATABASE_URL });\n *\n * cache.setCacheConfigurations({\n * default: \"pg\",\n * drivers: { pg: PgCacheDriver },\n * options: { pg: { client: pool, table: \"warlock_cache\", ttl: \"1h\" } },\n * });\n *\n * await cache.init();\n *\n * // Run once, via your own migration tooling:\n * // await pool.query(driver.schema());\n */\nexport class PgCacheDriver\n extends BaseCacheDriver<PgClientLike, PgCacheOptions>\n implements CacheDriver<PgClientLike, PgCacheOptions>\n{\n /**\n * {@inheritdoc}\n */\n public name = \"pg\";\n\n /**\n * Cached result of the pgvector extension check. Populated lazily on first\n * vector op so the driver doesn't probe the database at construction time.\n */\n protected vectorReady?: boolean;\n\n /**\n * {@inheritdoc}\n *\n * Validates `client` presence and the table name before storing options.\n */\n public setOptions(options: PgCacheOptions) {\n if (!options || !options.client || typeof options.client.query !== \"function\") {\n throw new CacheConfigurationError(\n \"Pg cache driver requires a 'client' option implementing { query(text, values) } — pass a pg.Pool or pg.Client.\",\n );\n }\n\n const table = options.table ?? \"warlock_cache\";\n if (!SAFE_IDENT.test(table)) {\n throw new CacheConfigurationError(\n `Pg cache driver: invalid table name '${table}'. Allowed: [A-Za-z_][A-Za-z0-9_]*.`,\n );\n }\n\n if (options.vector) {\n const dim = options.vector.dimensions;\n if (!Number.isInteger(dim) || dim <= 0) {\n throw new CacheConfigurationError(\n `Pg cache driver: vector.dimensions must be a positive integer; got ${dim}.`,\n );\n }\n const idx = options.vector.index ?? \"hnsw\";\n if (idx !== \"hnsw\" && idx !== \"ivfflat\") {\n throw new CacheConfigurationError(\n `Pg cache driver: vector.index must be 'hnsw' or 'ivfflat'; got '${idx}'.`,\n );\n }\n }\n\n return super.setOptions({ ...options, table });\n }\n\n /**\n * Lazy pgvector availability check. Runs once on the first vector op and\n * caches the result — subsequent ops are zero-overhead. Throws\n * {@link CacheConfigurationError} if the extension isn't installed; throws\n * {@link CacheUnsupportedError} if `vector` config wasn't provided at all.\n */\n protected async ensureVectorReady(): Promise<void> {\n if (!this.options.vector) {\n throw new CacheUnsupportedError(\n \"'pg' driver: similarity retrieval requires the 'vector' config block. Set options.vector.dimensions and reconnect.\",\n );\n }\n\n if (this.vectorReady === true) {\n return;\n }\n\n const { rows } = await this.pgClient.query(\n `SELECT 1 FROM pg_extension WHERE extname = 'vector'`,\n );\n\n if (rows.length === 0) {\n throw new CacheConfigurationError(\n \"'pg' driver: pgvector extension not installed. Run 'CREATE EXTENSION vector;' or remove the 'vector' config option.\",\n );\n }\n\n this.vectorReady = true;\n }\n\n /**\n * Format a numeric vector for pgvector ingestion. The `vector` type accepts\n * a string literal `'[1,2,3]'` cast via `::vector` — this avoids depending\n * on the binary protocol and works against any pg client.\n */\n protected formatVector(vector: number[]): string {\n return `[${vector.join(\",\")}]`;\n }\n\n /**\n * Resolved table name. Always defined post-`setOptions` — the validator\n * fills in the default.\n */\n protected get table(): string {\n return this.options.table ?? \"warlock_cache\";\n }\n\n /**\n * The user-supplied `pg.Pool` / `pg.Client`. Use this rather than `this.client`\n * (which has a generic fallback to `this`) for actual queries.\n */\n protected get pgClient(): PgClientLike {\n return this.options.client;\n }\n\n /**\n * Compute an absolute `expires_at` Date for the given relative TTL in seconds,\n * or `null` when the entry should not expire (`Infinity` / 0 / undefined).\n */\n protected ttlToExpiresAt(ttl?: number): Date | null {\n if (!ttl || ttl === Infinity) {\n return null;\n }\n\n return new Date(Date.now() + ttl * 1000);\n }\n\n /**\n * Return the SQL needed to provision the cache table + index. Run once via\n * the caller's migration tooling — the driver never auto-migrates.\n *\n * @example\n * await pool.query(driver.schema());\n */\n public schema(): string {\n const t = this.table;\n const vec = this.options.vector;\n\n const columns = [\n ` key TEXT PRIMARY KEY,`,\n ` value JSONB NOT NULL,`,\n ` expires_at TIMESTAMPTZ,`,\n ` stale_at TIMESTAMPTZ,`,\n ` tags TEXT[] NOT NULL DEFAULT '{}'::TEXT[]`,\n ];\n\n if (vec) {\n // Trailing comma on the previous line to make this a valid column list.\n columns[columns.length - 1] = columns[columns.length - 1] + \",\";\n columns.push(` embedding VECTOR(${vec.dimensions})`);\n }\n\n const lines = [\n `CREATE TABLE IF NOT EXISTS ${t} (`,\n ...columns,\n `);`,\n `CREATE INDEX IF NOT EXISTS idx_${t}_expires_at ON ${t} (expires_at);`,\n `CREATE INDEX IF NOT EXISTS idx_${t}_tags ON ${t} USING GIN (tags);`,\n ];\n\n if (vec) {\n const idx = vec.index ?? \"hnsw\";\n lines.push(\n `CREATE INDEX IF NOT EXISTS idx_${t}_embedding ON ${t} USING ${idx} (embedding vector_cosine_ops);`,\n );\n }\n\n return lines.join(\"\\n\");\n }\n\n /**\n * {@inheritdoc}\n */\n public async connect() {\n // No-op — caller owns the connection. We emit `connected` for symmetry\n // with the other drivers' lifecycle events.\n this.log(\"connecting\");\n this.log(\"connected\");\n await this.emit(\"connected\");\n }\n\n /**\n * {@inheritdoc}\n *\n * Does NOT close the user-supplied client — lifecycle stays with the caller.\n */\n public async disconnect() {\n this.log(\"disconnected\");\n await this.emit(\"disconnected\");\n }\n\n /**\n * {@inheritdoc}\n */\n public async set(\n key: CacheKey,\n value: any,\n ttlOrOptions?: CacheTtl | CacheSetOptions,\n ): Promise<any> {\n const parsedKey = this.parseKey(key);\n const { ttl, tags, onConflict, vector, staleAt } = this.resolveSetOptions(ttlOrOptions);\n\n if (vector) {\n if (!this.options.vector) {\n throw new CacheUnsupportedError(\n \"'pg' driver: cannot index a vector without options.vector configuration — set { dimensions } and recreate the table via driver.schema().\",\n );\n }\n\n const expected = this.options.vector.dimensions;\n if (vector.length !== expected) {\n throw new CacheConfigurationError(\n `Pg cache driver: vector dimension mismatch — expected ${expected}, got ${vector.length}.`,\n );\n }\n\n await this.ensureVectorReady();\n }\n\n this.log(\"caching\", parsedKey);\n\n const expiresAt = this.ttlToExpiresAt(ttl);\n const staleAtDate = staleAt !== undefined ? new Date(staleAt) : null;\n const tagsArr = tags ?? [];\n const serialized = JSON.stringify(value);\n const vecLiteral = vector ? this.formatVector(vector) : null;\n\n const t = this.table;\n\n // Build column / placeholder / param triplets dynamically so the same code\n // path serves both KV-only and vector-aware writes. Param order is fixed:\n // $1 = key, $2 = value (jsonb), $3 = expires_at, $4 = stale_at,\n // $5 = tags, $6 = embedding::vector (only present when vecLiteral !== null).\n const cols = [\"key\", \"value\", \"expires_at\", \"stale_at\", \"tags\"];\n const placeholders = [\"$1\", \"$2::jsonb\", \"$3\", \"$4\", \"$5\"];\n const params: unknown[] = [parsedKey, serialized, expiresAt, staleAtDate, tagsArr];\n if (vecLiteral !== null) {\n cols.push(\"embedding\");\n placeholders.push(`$${params.length + 1}::vector`);\n params.push(vecLiteral);\n }\n const colList = cols.join(\", \");\n const valList = placeholders.join(\", \");\n const setClause = cols\n .slice(1)\n .map((c) => `${c} = EXCLUDED.${c}`)\n .join(\", \");\n const updateSetClause = cols\n .slice(1)\n .map((c, i) => `${c} = ${placeholders[i + 1]}`)\n .join(\", \");\n\n if (onConflict === \"create\") {\n // Race-safe insert: if another worker already holds the key (and the row\n // hasn't expired), DO NOTHING; we then SELECT to surface the existing value.\n const { rows } = await this.pgClient.query(\n `INSERT INTO ${t}(${colList})\n VALUES (${valList})\n ON CONFLICT (key) DO UPDATE\n SET ${setClause}\n WHERE ${t}.expires_at IS NOT NULL AND ${t}.expires_at < now()\n RETURNING value`,\n params,\n );\n\n if (rows.length === 0) {\n // Conflict + existing row not expired → fetch existing for the result.\n const existing = await this.get(key);\n return { wasSet: false, existing } satisfies CacheSetResult;\n }\n\n if (tags && tags.length > 0) {\n await this.applyTags(parsedKey, tags);\n }\n\n this.log(\"cached\", parsedKey);\n await this.emit(\"set\", { key: parsedKey, value, ttl });\n return { wasSet: true, existing: null } satisfies CacheSetResult;\n }\n\n if (onConflict === \"update\") {\n // Update only when the key exists AND hasn't expired.\n const { rows } = await this.pgClient.query(\n `UPDATE ${t}\n SET ${updateSetClause}\n WHERE key = $1 AND (expires_at IS NULL OR expires_at > now())\n RETURNING value`,\n params,\n );\n\n if (rows.length === 0) {\n return { wasSet: false, existing: null } satisfies CacheSetResult;\n }\n\n if (tags && tags.length > 0) {\n await this.applyTags(parsedKey, tags);\n }\n\n this.log(\"cached\", parsedKey);\n await this.emit(\"set\", { key: parsedKey, value, ttl });\n return { wasSet: true, existing: null } satisfies CacheSetResult;\n }\n\n // upsert (default)\n await this.pgClient.query(\n `INSERT INTO ${t}(${colList})\n VALUES (${valList})\n ON CONFLICT (key) DO UPDATE\n SET ${setClause}`,\n params,\n );\n\n if (tags && tags.length > 0) {\n await this.applyTags(parsedKey, tags);\n }\n\n this.log(\"cached\", parsedKey);\n await this.emit(\"set\", { key: parsedKey, value, ttl });\n return value;\n }\n\n /**\n * {@inheritdoc}\n */\n public async get(key: CacheKey) {\n const parsedKey = this.parseKey(key);\n this.log(\"fetching\", parsedKey);\n\n const t = this.table;\n const { rows } = await this.pgClient.query(\n `SELECT value FROM ${t}\n WHERE key = $1 AND (expires_at IS NULL OR expires_at > now())`,\n [parsedKey],\n );\n\n if (rows.length === 0) {\n this.log(\"notFound\", parsedKey);\n await this.emit(\"miss\", { key: parsedKey });\n return null;\n }\n\n this.log(\"fetched\", parsedKey);\n\n // pg's JSONB type round-trips through node-postgres as a parsed JS value\n // already — but some pool implementations may hand back a string. Be defensive.\n let value = rows[0].value;\n if (typeof value === \"string\") {\n try {\n value = JSON.parse(value);\n } catch {\n // Leave it as-is; cloning below will fail loud if it's truly broken.\n }\n }\n\n if (value === null || value === undefined) {\n await this.emit(\"hit\", { key: parsedKey, value });\n return value;\n }\n\n const type = typeof value;\n if (type === \"string\" || type === \"number\" || type === \"boolean\") {\n await this.emit(\"hit\", { key: parsedKey, value });\n return value;\n }\n\n try {\n const cloned = structuredClone(value);\n await this.emit(\"hit\", { key: parsedKey, value: cloned });\n return cloned;\n } catch (error) {\n this.logError(`Failed to clone cached value for ${parsedKey}`, error);\n throw error;\n }\n }\n\n /**\n * Read the raw {@link CacheData} wrapper, including `staleAt` metadata.\n * Returns `null` for missing or expired rows — `swr()` consumes this to\n * branch on freshness without going through `get()`'s clone-and-emit path.\n */\n protected async getEntry(key: CacheKey): Promise<CacheData | null> {\n const parsedKey = this.parseKey(key);\n const t = this.table;\n\n const { rows } = await this.pgClient.query(\n `SELECT value, expires_at, stale_at FROM ${t}\n WHERE key = $1 AND (expires_at IS NULL OR expires_at > now())`,\n [parsedKey],\n );\n\n if (rows.length === 0) {\n return null;\n }\n\n let data = rows[0].value;\n if (typeof data === \"string\") {\n try {\n data = JSON.parse(data);\n } catch {\n // Leave as-is — getEntry is metadata-shaped, the caller will hit the\n // same parse path on the public `get()` if they care.\n }\n }\n\n const entry: CacheData = { data };\n\n const expiresAtRaw = rows[0].expires_at as Date | string | null;\n if (expiresAtRaw) {\n const expiresAt = expiresAtRaw instanceof Date ? expiresAtRaw : new Date(expiresAtRaw);\n entry.expiresAt = expiresAt.getTime();\n }\n\n const staleAtRaw = rows[0].stale_at as Date | string | null;\n if (staleAtRaw) {\n const staleAt = staleAtRaw instanceof Date ? staleAtRaw : new Date(staleAtRaw);\n entry.staleAt = staleAt.getTime();\n }\n\n return entry;\n }\n\n /**\n * {@inheritdoc}\n */\n public async remove(key: CacheKey) {\n const parsedKey = this.parseKey(key);\n this.log(\"removing\", parsedKey);\n\n const t = this.table;\n await this.pgClient.query(`DELETE FROM ${t} WHERE key = $1`, [parsedKey]);\n\n this.log(\"removed\", parsedKey);\n await this.emit(\"removed\", { key: parsedKey });\n }\n\n /**\n * {@inheritdoc}\n *\n * Deletes all rows whose key equals the namespace exactly or starts with\n * `<namespace>.` — same boundary semantics as the other drivers.\n */\n public async removeNamespace(namespace: string) {\n const parsed = this.parseKey(namespace);\n this.log(\"clearing\", parsed || \"(all)\");\n\n const t = this.table;\n\n if (parsed === \"\") {\n await this.pgClient.query(`DELETE FROM ${t}`);\n } else {\n // Escape `_` and `%` in the prefix so they aren't treated as LIKE wildcards.\n const escaped = parsed.replace(/\\\\/g, \"\\\\\\\\\").replace(/_/g, \"\\\\_\").replace(/%/g, \"\\\\%\");\n await this.pgClient.query(`DELETE FROM ${t} WHERE key = $1 OR key LIKE $2 ESCAPE '\\\\'`, [\n parsed,\n `${escaped}.%`,\n ]);\n }\n\n this.log(\"cleared\", parsed || \"(all)\");\n return this;\n }\n\n /**\n * {@inheritdoc}\n *\n * Honors `globalPrefix` — when configured, scopes the flush to entries\n * under the prefix rather than truncating the entire table (which could\n * wipe sibling tenants sharing the same Postgres database).\n */\n public async flush() {\n this.log(\"flushing\");\n\n if (this.options.globalPrefix) {\n await this.removeNamespace(\"\");\n } else {\n await this.pgClient.query(`DELETE FROM ${this.table}`);\n }\n\n this.log(\"flushed\");\n await this.emit(\"flushed\");\n }\n\n /**\n * {@inheritdoc}\n *\n * pgvector-backed similarity. Uses the `<=>` cosine-distance operator\n * (lower distance = higher similarity) and converts to cosine similarity\n * as `1 - distance` so the returned `score` matches the rest of the\n * package (`[0, 1]`, higher is more similar).\n *\n * Honors `topK`, `threshold`, and an optional `tags` filter (native\n * `tags && $tags` overlap query — much faster than the meta-key path).\n *\n * Throws {@link CacheUnsupportedError} when `options.vector` was not\n * configured at driver setup; throws {@link CacheConfigurationError} when\n * the pgvector extension is missing or the query vector's dimension count\n * doesn't match the configured one.\n */\n public async similar<T = any>(\n vector: number[],\n options: CacheSimilarOptions,\n ): Promise<CacheSimilarHit<T>[]> {\n if (!this.options.vector) {\n throw new CacheUnsupportedError(\n \"'pg' driver: similarity retrieval requires the 'vector' config block. Set options.vector.dimensions and reconnect.\",\n );\n }\n\n const expected = this.options.vector.dimensions;\n if (vector.length !== expected) {\n throw new CacheConfigurationError(\n `Pg cache driver: vector dimension mismatch — expected ${expected}, got ${vector.length}.`,\n );\n }\n\n if (!Number.isInteger(options.topK) || options.topK <= 0) {\n throw new CacheConfigurationError(\n `Pg cache driver: similar.topK must be a positive integer; got ${options.topK}.`,\n );\n }\n\n await this.ensureVectorReady();\n\n const t = this.table;\n const vecLiteral = this.formatVector(vector);\n const params: unknown[] = [vecLiteral];\n let tagFilter = \"\";\n\n if (options.tags && options.tags.length > 0) {\n params.push(options.tags);\n tagFilter = `AND tags && $${params.length}`;\n }\n\n params.push(options.topK);\n const topKParam = `$${params.length}`;\n\n const { rows } = await this.pgClient.query(\n `SELECT key, value, 1 - (embedding <=> $1::vector) AS score\n FROM ${t}\n WHERE embedding IS NOT NULL\n AND (expires_at IS NULL OR expires_at > now())\n ${tagFilter}\n ORDER BY embedding <=> $1::vector\n LIMIT ${topKParam}`,\n params,\n );\n\n const hits: CacheSimilarHit<T>[] = [];\n for (const row of rows) {\n const score = Number(row.score);\n if (options.threshold !== undefined && score < options.threshold) {\n continue;\n }\n\n let value = row.value;\n if (typeof value === \"string\") {\n try {\n value = JSON.parse(value);\n } catch {\n // Surface as-is; if non-JSON crept in, the consumer will notice.\n }\n }\n\n // Match get() cloning semantics so consumers can't mutate cached state.\n if (value !== null && value !== undefined) {\n const ty = typeof value;\n if (ty !== \"string\" && ty !== \"number\" && ty !== \"boolean\") {\n value = structuredClone(value);\n }\n }\n\n hits.push({ key: row.key, value, score });\n }\n\n return hits;\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { BaseCacheDriver } from "./base-cache-driver.mjs";
|
|
2
|
+
import { CacheData, CacheDriver, CacheKey, CacheSetOptions, CacheTtl, RedisOptions } from "../types.mjs";
|
|
3
|
+
import { createClient } from "redis";
|
|
4
|
+
|
|
5
|
+
//#region ../../@warlock.js/cache/src/drivers/redis-cache-driver.d.ts
|
|
6
|
+
declare class RedisCacheDriver extends BaseCacheDriver<ReturnType<typeof createClient>, RedisOptions> implements CacheDriver<ReturnType<typeof createClient>, RedisOptions> {
|
|
7
|
+
/**
|
|
8
|
+
* Cache driver name
|
|
9
|
+
*/
|
|
10
|
+
name: string;
|
|
11
|
+
/**
|
|
12
|
+
* {@inheritdoc}
|
|
13
|
+
*/
|
|
14
|
+
setOptions(options: RedisOptions): this;
|
|
15
|
+
/**
|
|
16
|
+
* {@inheritDoc}
|
|
17
|
+
*/
|
|
18
|
+
removeNamespace(namespace: string): Promise<string[] | undefined>;
|
|
19
|
+
/**
|
|
20
|
+
* {@inheritDoc}
|
|
21
|
+
*/
|
|
22
|
+
set(key: CacheKey, value: any, ttlOrOptions?: CacheTtl | CacheSetOptions): Promise<any>;
|
|
23
|
+
/**
|
|
24
|
+
* Build the sidecar key Redis uses to track SWR freshness without
|
|
25
|
+
* wrapping the main value JSON.
|
|
26
|
+
*/
|
|
27
|
+
protected swrMetaKey(parsedKey: string): string;
|
|
28
|
+
/**
|
|
29
|
+
* Read the raw {@link CacheData} wrapper, fetching the value and the
|
|
30
|
+
* SWR sidecar in parallel. Returns `null` when the main key is missing
|
|
31
|
+
* or expired (Redis handles expiry natively, so the absence of the
|
|
32
|
+
* value alone tells us).
|
|
33
|
+
*/
|
|
34
|
+
protected getEntry(key: CacheKey): Promise<CacheData | null>;
|
|
35
|
+
/**
|
|
36
|
+
* {@inheritdoc}
|
|
37
|
+
*
|
|
38
|
+
* Redis tracks expiry natively (the payload carries no `expiresAt`), so read
|
|
39
|
+
* the remaining lifetime with the `TTL` command. Redis returns `-2` for a
|
|
40
|
+
* missing key and `-1` for a key with no expiry.
|
|
41
|
+
*/
|
|
42
|
+
protected getRemainingTtl(key: CacheKey): Promise<number | undefined>;
|
|
43
|
+
/**
|
|
44
|
+
* {@inheritDoc}
|
|
45
|
+
*/
|
|
46
|
+
get(key: CacheKey): Promise<any>;
|
|
47
|
+
/**
|
|
48
|
+
* {@inheritDoc}
|
|
49
|
+
*/
|
|
50
|
+
remove(key: CacheKey): Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* {@inheritDoc}
|
|
53
|
+
*/
|
|
54
|
+
flush(): Promise<void>;
|
|
55
|
+
/**
|
|
56
|
+
* {@inheritDoc}
|
|
57
|
+
*/
|
|
58
|
+
connect(): Promise<void>;
|
|
59
|
+
/**
|
|
60
|
+
* {@inheritDoc}
|
|
61
|
+
*
|
|
62
|
+
* Guards against disconnecting when the client was never created. The base
|
|
63
|
+
* `client` getter falls back to `this` when no client is set, so we check
|
|
64
|
+
* the backing `clientDriver` directly — using `this.client` for this guard
|
|
65
|
+
* would always be truthy and crash with "this.quit is not a function".
|
|
66
|
+
*/
|
|
67
|
+
disconnect(): Promise<void>;
|
|
68
|
+
/**
|
|
69
|
+
* Atomic increment using Redis native INCRBY command
|
|
70
|
+
* {@inheritdoc}
|
|
71
|
+
*/
|
|
72
|
+
increment(key: CacheKey, value?: number): Promise<number>;
|
|
73
|
+
/**
|
|
74
|
+
* Atomic decrement using Redis native DECRBY command
|
|
75
|
+
* {@inheritdoc}
|
|
76
|
+
*/
|
|
77
|
+
decrement(key: CacheKey, value?: number): Promise<number>;
|
|
78
|
+
/**
|
|
79
|
+
* Set if not exists (atomic operation)
|
|
80
|
+
* Returns true if key was set, false if key already existed
|
|
81
|
+
*/
|
|
82
|
+
setNX(key: CacheKey, value: any, ttl?: number): Promise<boolean>;
|
|
83
|
+
}
|
|
84
|
+
//#endregion
|
|
85
|
+
export { RedisCacheDriver };
|
|
86
|
+
//# sourceMappingURL=redis-cache-driver.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redis-cache-driver.d.mts","names":[],"sources":["../../../../../../@warlock.js/cache/src/drivers/redis-cache-driver.ts"],"mappings":";;;;;cA0Da,gBAAA,SACH,eAAA,CAAgB,UAAA,QAAkB,YAAA,GAAe,YAAA,aAC9C,WAAA,CAAY,UAAA,QAAkB,YAAA,GAAe,YAAA;;AAF1D;;EAOS,IAAA;EANmC;;;EAWnC,UAAA,CAAW,OAAA,EAAS,YAAA;EAVJ;;;EAuBV,eAAA,CAAgB,SAAA,WAAiB,OAAA;EAuBvC;;;EADM,GAAA,CACX,GAAA,EAAK,QAAA,EACL,KAAA,OACA,YAAA,GAAe,QAAA,GAAW,eAAA,GACzB,OAAA;EAmF2B;;;;EAAA,UAVpB,UAAA,CAAW,SAAA;EAqDC;;;;;;EAAA,UA3CN,QAAA,CAAS,GAAA,EAAK,QAAA,GAAW,OAAA,CAAQ,SAAA;EAkMrB;;;;;;;EAAA,UAzKZ,eAAA,CAAgB,GAAA,EAAK,QAAA,GAAW,OAAA;EA7J1B;;;EA+KT,GAAA,CAAI,GAAA,EAAK,QAAA,GAAQ,OAAA;EAhLY;;;EA+N7B,MAAA,CAAO,GAAA,EAAK,QAAA,GAAQ,OAAA;EA9NQ;;;EA+O5B,KAAA,CAAA,GAAK,OAAA;EArOS;;;EAuPd,OAAA,CAAA,GAAO,OAAA;EA1O0B;;;;;;;;EA8RjC,UAAA,CAAA,GAAU,OAAA;EA3Lb;;;;EA4MG,SAAA,CAAU,GAAA,EAAK,QAAA,EAAU,KAAA,YAAoB,OAAA;EAlMjB;;;;EAqN5B,SAAA,CAAU,GAAA,EAAK,QAAA,EAAU,KAAA,YAAoB,OAAA;EA5LV;;;;EA+MnC,KAAA,CAAM,GAAA,EAAK,QAAA,EAAU,KAAA,OAAY,GAAA,YAAe,OAAA;AAAA"}
|