@warlock.js/cache 5.19.0 → 5.20.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/CHANGELOG.md +55 -0
- package/cjs/index.cjs +1439 -331
- package/cjs/index.cjs.map +1 -1
- package/esm/cache-manager.d.mts +4 -0
- package/esm/cache-manager.d.mts.map +1 -1
- package/esm/cache-manager.mjs +23 -5
- package/esm/cache-manager.mjs.map +1 -1
- package/esm/cached/auto-key.d.mts +14 -14
- package/esm/cached/auto-key.d.mts.map +1 -1
- package/esm/cached/auto-key.mjs +50 -34
- package/esm/cached/auto-key.mjs.map +1 -1
- package/esm/cached/cached.mjs +4 -0
- package/esm/cached/cached.mjs.map +1 -1
- package/esm/drivers/base-cache-driver.d.mts +84 -8
- package/esm/drivers/base-cache-driver.d.mts.map +1 -1
- package/esm/drivers/base-cache-driver.mjs +152 -42
- package/esm/drivers/base-cache-driver.mjs.map +1 -1
- package/esm/drivers/file-cache-driver.d.mts +15 -0
- package/esm/drivers/file-cache-driver.d.mts.map +1 -1
- package/esm/drivers/file-cache-driver.mjs +68 -18
- package/esm/drivers/file-cache-driver.mjs.map +1 -1
- package/esm/drivers/in-memory-tag-index.d.mts +44 -0
- package/esm/drivers/in-memory-tag-index.d.mts.map +1 -0
- package/esm/drivers/in-memory-tag-index.mjs +73 -0
- package/esm/drivers/in-memory-tag-index.mjs.map +1 -0
- package/esm/drivers/index.d.mts +1 -1
- package/esm/drivers/index.mjs +1 -1
- package/esm/drivers/lru-memory-cache-driver.d.mts +43 -0
- package/esm/drivers/lru-memory-cache-driver.d.mts.map +1 -1
- package/esm/drivers/lru-memory-cache-driver.mjs +96 -0
- package/esm/drivers/lru-memory-cache-driver.mjs.map +1 -1
- package/esm/drivers/memory-cache-driver.d.mts +87 -30
- package/esm/drivers/memory-cache-driver.d.mts.map +1 -1
- package/esm/drivers/memory-cache-driver.mjs +214 -96
- package/esm/drivers/memory-cache-driver.mjs.map +1 -1
- package/esm/drivers/memory-extended-cache-driver.d.mts +9 -5
- package/esm/drivers/memory-extended-cache-driver.d.mts.map +1 -1
- package/esm/drivers/memory-extended-cache-driver.mjs +10 -23
- package/esm/drivers/memory-extended-cache-driver.mjs.map +1 -1
- package/esm/drivers/mock-cache-driver.d.mts +41 -0
- package/esm/drivers/mock-cache-driver.d.mts.map +1 -1
- package/esm/drivers/mock-cache-driver.mjs +105 -8
- package/esm/drivers/mock-cache-driver.mjs.map +1 -1
- package/esm/drivers/null-cache-driver.d.mts +8 -2
- package/esm/drivers/null-cache-driver.d.mts.map +1 -1
- package/esm/drivers/null-cache-driver.mjs +10 -2
- package/esm/drivers/null-cache-driver.mjs.map +1 -1
- package/esm/drivers/pg-cache-driver.d.mts +77 -3
- package/esm/drivers/pg-cache-driver.d.mts.map +1 -1
- package/esm/drivers/pg-cache-driver.mjs +229 -7
- package/esm/drivers/pg-cache-driver.mjs.map +1 -1
- package/esm/drivers/redis-cache-driver.d.mts +86 -1
- package/esm/drivers/redis-cache-driver.d.mts.map +1 -1
- package/esm/drivers/redis-cache-driver.mjs +252 -16
- package/esm/drivers/redis-cache-driver.mjs.map +1 -1
- package/esm/index.d.mts +2 -2
- package/esm/index.mjs +2 -2
- package/esm/list/memory-cache-list.d.mts +11 -8
- package/esm/list/memory-cache-list.d.mts.map +1 -1
- package/esm/list/memory-cache-list.mjs +39 -37
- package/esm/list/memory-cache-list.mjs.map +1 -1
- package/esm/scoped-cache.d.mts +9 -3
- package/esm/scoped-cache.d.mts.map +1 -1
- package/esm/scoped-cache.mjs +19 -7
- package/esm/scoped-cache.mjs.map +1 -1
- package/esm/tagged-cache.d.mts +20 -0
- package/esm/tagged-cache.d.mts.map +1 -1
- package/esm/tagged-cache.mjs +62 -24
- package/esm/tagged-cache.mjs.map +1 -1
- package/esm/tagged-scoped-cache.d.mts +7 -3
- package/esm/tagged-scoped-cache.d.mts.map +1 -1
- package/esm/tagged-scoped-cache.mjs +19 -7
- package/esm/tagged-scoped-cache.mjs.map +1 -1
- package/esm/types.d.mts +41 -3
- package/esm/types.d.mts.map +1 -1
- package/esm/types.mjs.map +1 -1
- package/esm/utils.d.mts +9 -3
- package/esm/utils.d.mts.map +1 -1
- package/esm/utils.mjs +16 -6
- package/esm/utils.mjs.map +1 -1
- package/llms-full.txt +1685 -1666
- package/llms.txt +18 -18
- package/package.json +4 -4
- package/skills/apply-cache-patterns/SKILL.md +98 -97
- package/skills/configure-pg-cache/SKILL.md +6 -5
- package/skills/configure-set-options/SKILL.md +102 -96
- package/skills/handle-cache-errors/SKILL.md +92 -92
- package/skills/index.md +20 -0
- package/skills/observe-cache/SKILL.md +103 -103
- package/skills/pick-cache-driver/SKILL.md +129 -123
- package/skills/test-cache-code/SKILL.md +219 -219
- package/skills/use-cache-atomic/SKILL.md +57 -65
- package/skills/use-cache-bulk/SKILL.md +57 -57
- package/skills/use-cache-list/SKILL.md +85 -85
- package/skills/use-cache-lock/SKILL.md +113 -104
- package/skills/use-cache-namespace/SKILL.md +88 -88
- package/skills/use-cache-similarity/SKILL.md +94 -94
- package/skills/use-cache-tags/SKILL.md +7 -4
- package/skills/use-cache-update-merge/SKILL.md +84 -84
- package/skills/use-cache-utils/SKILL.md +89 -89
- package/skills/use-cached-hof/SKILL.md +100 -99
- package/skills/use-swr/SKILL.md +104 -104
package/llms.txt
CHANGED
|
@@ -6,23 +6,23 @@
|
|
|
6
6
|
|
|
7
7
|
## Skills
|
|
8
8
|
|
|
9
|
-
- [apply-cache-patterns](@warlock.js/cache/apply-cache-patterns/SKILL.md):
|
|
9
|
+
- [apply-cache-patterns](@warlock.js/cache/apply-cache-patterns/SKILL.md): (no description)
|
|
10
10
|
- [cache-basics](@warlock.js/cache/cache-basics/SKILL.md): Start here for @warlock.js/cache drivers, key-value operations, and application cache configuration.
|
|
11
|
-
- [configure-pg-cache](@warlock.js/cache/configure-pg-cache/SKILL.md):
|
|
12
|
-
- [configure-set-options](@warlock.js/cache/configure-set-options/SKILL.md):
|
|
13
|
-
- [handle-cache-errors](@warlock.js/cache/handle-cache-errors/SKILL.md):
|
|
14
|
-
- [observe-cache](@warlock.js/cache/observe-cache/SKILL.md):
|
|
11
|
+
- [configure-pg-cache](@warlock.js/cache/configure-pg-cache/SKILL.md): Postgres cache driver setup — KV-only mode (default) or pgvector mode (opt in via options.pg.vector). Caller owns the pg.Pool, driver exposes driver.schema() for one-time DDL. Triggers: `PgCacheDriver`, `driver.schema`, `options.pg.vector`, `pg.Pool`, `hnsw`, `ivfflat`; "use Postgres as cache backend", "set up pgvector semantic cache", "DDL for warlock cache table"; typical import `import { cache, PgCacheDriver } from "@warlock.js/cache"`. Skip: cross-driver similarity API — `@warlock.js/cache/use-cache-similarity/SKILL.md`; driver picker — `@warlock.js/cache/pick-cache-driver/SKILL.md`; competing libs `pg-mem`; raw `pg` / `node-postgres`.
|
|
12
|
+
- [configure-set-options](@warlock.js/cache/configure-set-options/SKILL.md): (no description)
|
|
13
|
+
- [handle-cache-errors](@warlock.js/cache/handle-cache-errors/SKILL.md): (no description)
|
|
14
|
+
- [observe-cache](@warlock.js/cache/observe-cache/SKILL.md): (no description)
|
|
15
15
|
- [overview](@warlock.js/cache/overview/SKILL.md): @warlock.js/cache overview for orientation and choosing the focused skill for the task.
|
|
16
|
-
- [pick-cache-driver](@warlock.js/cache/pick-cache-driver/SKILL.md):
|
|
17
|
-
- [test-cache-code](@warlock.js/cache/test-cache-code/SKILL.md):
|
|
18
|
-
- [use-cache-atomic](@warlock.js/cache/use-cache-atomic/SKILL.md):
|
|
19
|
-
- [use-cache-bulk](@warlock.js/cache/use-cache-bulk/SKILL.md):
|
|
20
|
-
- [use-cache-list](@warlock.js/cache/use-cache-list/SKILL.md):
|
|
21
|
-
- [use-cache-lock](@warlock.js/cache/use-cache-lock/SKILL.md):
|
|
22
|
-
- [use-cache-namespace](@warlock.js/cache/use-cache-namespace/SKILL.md):
|
|
23
|
-
- [use-cache-similarity](@warlock.js/cache/use-cache-similarity/SKILL.md):
|
|
24
|
-
- [use-cache-tags](@warlock.js/cache/use-cache-tags/SKILL.md): Tag-based invalidation
|
|
25
|
-
- [use-cache-update-merge](@warlock.js/cache/use-cache-update-merge/SKILL.md):
|
|
26
|
-
- [use-cache-utils](@warlock.js/cache/use-cache-utils/SKILL.md):
|
|
27
|
-
- [use-cached-hof](@warlock.js/cache/use-cached-hof/SKILL.md):
|
|
28
|
-
- [use-swr](@warlock.js/cache/use-swr/SKILL.md):
|
|
16
|
+
- [pick-cache-driver](@warlock.js/cache/pick-cache-driver/SKILL.md): (no description)
|
|
17
|
+
- [test-cache-code](@warlock.js/cache/test-cache-code/SKILL.md): (no description)
|
|
18
|
+
- [use-cache-atomic](@warlock.js/cache/use-cache-atomic/SKILL.md): (no description)
|
|
19
|
+
- [use-cache-bulk](@warlock.js/cache/use-cache-bulk/SKILL.md): (no description)
|
|
20
|
+
- [use-cache-list](@warlock.js/cache/use-cache-list/SKILL.md): (no description)
|
|
21
|
+
- [use-cache-lock](@warlock.js/cache/use-cache-lock/SKILL.md): (no description)
|
|
22
|
+
- [use-cache-namespace](@warlock.js/cache/use-cache-namespace/SKILL.md): (no description)
|
|
23
|
+
- [use-cache-similarity](@warlock.js/cache/use-cache-similarity/SKILL.md): (no description)
|
|
24
|
+
- [use-cache-tags](@warlock.js/cache/use-cache-tags/SKILL.md): Tag-based invalidation — attach tags on write, then cache.tags([...]).invalidate() drops every key bound to any of those tags. Triggers: `cache.tags`, `invalidate`, `cache.set` with `tags`; "invalidate every key tagged users", "drop everything for tenant 42", "bulk cache invalidation without knowing keys", "tag a cached value"; typical import `import { cache } from "@warlock.js/cache"`. Skip: prefix-based drop — `@warlock.js/cache/use-cache-namespace/SKILL.md`; HOF memoization with tags — `@warlock.js/cache/use-cached-hof/SKILL.md`; SWR — `@warlock.js/cache/use-swr/SKILL.md`; competing libs `cache-manager` tags, Next.js `revalidateTag`.
|
|
25
|
+
- [use-cache-update-merge](@warlock.js/cache/use-cache-update-merge/SKILL.md): (no description)
|
|
26
|
+
- [use-cache-utils](@warlock.js/cache/use-cache-utils/SKILL.md): (no description)
|
|
27
|
+
- [use-cached-hof](@warlock.js/cache/use-cached-hof/SKILL.md): (no description)
|
|
28
|
+
- [use-swr](@warlock.js/cache/use-swr/SKILL.md): (no description)
|
package/package.json
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
"environment": "server"
|
|
6
6
|
},
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"@warlock.js/fs": "5.
|
|
9
|
-
"@mongez/reinforcements": "^4.0.
|
|
10
|
-
"@warlock.js/logger": "5.
|
|
8
|
+
"@warlock.js/fs": "5.20.0",
|
|
9
|
+
"@mongez/reinforcements": "^4.0.2",
|
|
10
|
+
"@warlock.js/logger": "5.20.0",
|
|
11
11
|
"ms": "^2.1.3"
|
|
12
12
|
},
|
|
13
13
|
"peerDependencies": {
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
],
|
|
47
47
|
"author": "hassanzohdy",
|
|
48
48
|
"license": "MIT",
|
|
49
|
-
"version": "5.
|
|
49
|
+
"version": "5.20.0",
|
|
50
50
|
"main": "./cjs/index.cjs",
|
|
51
51
|
"module": "./esm/index.mjs",
|
|
52
52
|
"types": "./esm/index.d.mts",
|
|
@@ -1,97 +1,98 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: apply-cache-patterns
|
|
3
|
-
description:
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Real-world caching patterns
|
|
7
|
-
|
|
8
|
-
Common shapes — the "general patterns" file. Specialized topics have dedicated skills: [`use-cache-tags`](@warlock.js/cache/use-cache-tags/SKILL.md), [`use-cache-namespace`](@warlock.js/cache/use-cache-namespace/SKILL.md), [`use-swr`](@warlock.js/cache/use-swr/SKILL.md), [`use-cache-lock`](@warlock.js/cache/use-cache-lock/SKILL.md), [`use-cache-list`](@warlock.js/cache/use-cache-list/SKILL.md).
|
|
9
|
-
|
|
10
|
-
## Memoize an expensive function — `remember`
|
|
11
|
-
|
|
12
|
-
```ts
|
|
13
|
-
const user = await cache.remember(`user:${id}`, "1h", async () => {
|
|
14
|
-
return db.users.find(id); // runs only on cache miss
|
|
15
|
-
});
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
- The callback runs once per miss.
|
|
19
|
-
- Concurrent callers for the same key share the in-flight promise (stampede protection) — within one Node process.
|
|
20
|
-
- `null` is the
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
await
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
- [`@warlock.js/cache/use-cache-
|
|
93
|
-
- [`@warlock.js/cache/use-cache-
|
|
94
|
-
- [`@warlock.js/cache/use-cache-
|
|
95
|
-
- [`@warlock.js/cache/use-cache-
|
|
96
|
-
- [`@warlock.js/cache/use-
|
|
97
|
-
- [`@warlock.js/cache/use-
|
|
1
|
+
---
|
|
2
|
+
name: apply-cache-patterns
|
|
3
|
+
description: 'Compose cache primitives into real-world patterns — remember() memoization, cross-node stampede protection via a distributed lock (onConflict: ''create''), negative caching, and per-tenant scoping. Triggers: `cache.remember`, `cache.set` with `onConflict: "create"`, `globalPrefix`; "memoize this function", "prevent cache stampede across nodes", "cache not-found results", "per-tenant cache scoping"; typical import `import { cache } from "@warlock.js/cache"`. Skip: counters — `@warlock.js/cache/use-cache-atomic/SKILL.md`; bulk get/set — `@warlock.js/cache/use-cache-bulk/SKILL.md`; TTL constants/utilities — `@warlock.js/cache/use-cache-utils/SKILL.md`; named lock wrapper — `@warlock.js/cache/use-cache-lock/SKILL.md`; SWR — `@warlock.js/cache/use-swr/SKILL.md`; competing libs `lru-cache`, `node-cache`, `keyv`.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Real-world caching patterns
|
|
7
|
+
|
|
8
|
+
Common shapes — the "general patterns" file. Specialized topics have dedicated skills: [`use-cache-tags`](@warlock.js/cache/use-cache-tags/SKILL.md), [`use-cache-namespace`](@warlock.js/cache/use-cache-namespace/SKILL.md), [`use-swr`](@warlock.js/cache/use-swr/SKILL.md), [`use-cache-lock`](@warlock.js/cache/use-cache-lock/SKILL.md), [`use-cache-list`](@warlock.js/cache/use-cache-list/SKILL.md).
|
|
9
|
+
|
|
10
|
+
## Memoize an expensive function — `remember`
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
const user = await cache.remember(`user:${id}`, "1h", async () => {
|
|
14
|
+
return db.users.find(id); // runs only on cache miss
|
|
15
|
+
});
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
- The callback runs once per miss.
|
|
19
|
+
- Concurrent callers for the same key share the in-flight promise (stampede protection) — within one Node process.
|
|
20
|
+
- `null` is the only miss. `0`, `false` and `""` are cached like any other value, so `remember` doesn't recompute them. A callback returning `null` is not cached, so the callback re-runs on the next call: to cache a "not found," store a sentinel (see negative caching below).
|
|
21
|
+
- Stampede protection is per process, even on redis. To run something once across servers, wrap it in `cache.lock()` on `redis` or `pg`.
|
|
22
|
+
|
|
23
|
+
## Cross-process stampede protection — distributed lock via `onConflict`
|
|
24
|
+
|
|
25
|
+
`remember`'s lock is per-process. For cross-node safety, acquire a short-lived distributed lock before doing expensive work:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
const lockKey = `lock:build-report:${reportId}`;
|
|
29
|
+
const acquired = await cache.set(lockKey, process.pid, {
|
|
30
|
+
onConflict: "create",
|
|
31
|
+
ttl: "2m",
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
if (!acquired.wasSet) {
|
|
35
|
+
// another node is already building — wait or skip
|
|
36
|
+
return cache.get(`report:${reportId}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const report = await buildExpensiveReport(reportId);
|
|
41
|
+
await cache.set(`report:${reportId}`, report, "1h");
|
|
42
|
+
return report;
|
|
43
|
+
} finally {
|
|
44
|
+
await cache.remove(lockKey);
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
This requires a driver with atomic `SET NX` — Redis is native, memory/LRU/file emulate (single-process only). For a higher-level wrapper that does the lock-and-release for you, see [`@warlock.js/cache/use-cache-lock/SKILL.md`](@warlock.js/cache/use-cache-lock/SKILL.md).
|
|
49
|
+
|
|
50
|
+
## Negative caching
|
|
51
|
+
|
|
52
|
+
Cache "not found" results with a shorter TTL to avoid hammering the origin:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
const user = await cache.remember(`user:${id}`, "5m", async () => {
|
|
56
|
+
const found = await db.users.find(id);
|
|
57
|
+
return found ?? { __miss: true };
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (user?.__miss) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Don't return raw `null` inside `remember` to "cache the miss": `null` is the miss sentinel, so the callback re-runs every time and the origin still gets hammered. The `{ __miss: true }` sentinel is what skips the next call.
|
|
66
|
+
|
|
67
|
+
## Per-tenant caching
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
// In cache config:
|
|
71
|
+
options: {
|
|
72
|
+
redis: {
|
|
73
|
+
url: "...",
|
|
74
|
+
globalPrefix: () => `tenant-${currentContext.tenantId}`,
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// At the call site — no tenancy awareness needed:
|
|
79
|
+
await cache.set("user:1", user, "1h");
|
|
80
|
+
// Actual key: "tenant-42.user.1"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Clear a tenant out:
|
|
84
|
+
```ts
|
|
85
|
+
await cache.removeNamespace(""); // when globalPrefix is set, flush scopes to it
|
|
86
|
+
// or
|
|
87
|
+
await cache.tags([`tenant-${tenantId}`]).invalidate();
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## See also
|
|
91
|
+
|
|
92
|
+
- [`@warlock.js/cache/use-cache-atomic/SKILL.md`](@warlock.js/cache/use-cache-atomic/SKILL.md) — `increment` / `decrement` counters
|
|
93
|
+
- [`@warlock.js/cache/use-cache-bulk/SKILL.md`](@warlock.js/cache/use-cache-bulk/SKILL.md) — `many` / `setMany`
|
|
94
|
+
- [`@warlock.js/cache/use-cache-utils/SKILL.md`](@warlock.js/cache/use-cache-utils/SKILL.md) — `CACHE_FOR` constants and TTL/key helpers
|
|
95
|
+
- [`@warlock.js/cache/use-cache-tags/SKILL.md`](@warlock.js/cache/use-cache-tags/SKILL.md) — tag-based invalidation
|
|
96
|
+
- [`@warlock.js/cache/use-cache-namespace/SKILL.md`](@warlock.js/cache/use-cache-namespace/SKILL.md) — scoped handles and `removeNamespace`
|
|
97
|
+
- [`@warlock.js/cache/use-swr/SKILL.md`](@warlock.js/cache/use-swr/SKILL.md) — stale-while-revalidate for slow upstreams
|
|
98
|
+
- [`@warlock.js/cache/use-cached-hof/SKILL.md`](@warlock.js/cache/use-cached-hof/SKILL.md) — `cached()` HOF for declarative memoization
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: configure-pg-cache
|
|
3
|
-
description:
|
|
3
|
+
description: 'Postgres cache driver setup — KV-only mode (default) or pgvector mode (opt in via options.pg.vector). Caller owns the pg.Pool, driver exposes driver.schema() for one-time DDL. Triggers: `PgCacheDriver`, `driver.schema`, `options.pg.vector`, `pg.Pool`, `hnsw`, `ivfflat`; "use Postgres as cache backend", "set up pgvector semantic cache", "DDL for warlock cache table"; typical import `import { cache, PgCacheDriver } from "@warlock.js/cache"`. Skip: cross-driver similarity API — `@warlock.js/cache/use-cache-similarity/SKILL.md`; driver picker — `@warlock.js/cache/pick-cache-driver/SKILL.md`; competing libs `pg-mem`; raw `pg` / `node-postgres`.'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# `pg` cache driver — Postgres setup
|
|
@@ -13,14 +13,15 @@ Persistent cache backed by your existing Postgres pool. Two modes: **KV-only** (
|
|
|
13
13
|
2. **`pg` is an optional peer dep.** Lazy-loaded; install only if you use this driver.
|
|
14
14
|
3. **No auto-migration.** Driver exposes `driver.schema()` returning a DDL string — caller runs it via their own migration tool.
|
|
15
15
|
4. **Table name is regex-validated** (`[A-Za-z_][A-Za-z0-9_]*`) before DDL interpolation. No SQL injection via misconfiguration.
|
|
16
|
-
5. **TTL is lazy on read.** `SELECT ... WHERE expires_at IS NULL OR expires_at > now()`.
|
|
16
|
+
5. **TTL is lazy on read.** `SELECT ... WHERE expires_at IS NULL OR expires_at > now()`. Expiry is computed on the DB clock. `driver.prune(limit = 1000)` deletes expired rows in batches and returns the count; `set()` also runs it on about 1 in 200 writes. Call it from a scheduled job for unique-key workloads.
|
|
17
17
|
6. **`onConflict` is race-safe at the SQL layer:**
|
|
18
18
|
- `create` → `INSERT ... ON CONFLICT DO UPDATE WHERE expires_at < now() RETURNING value` (reclaims expired rows; blocks live ones).
|
|
19
19
|
- `update` → `UPDATE ... WHERE expires_at IS NULL OR expires_at > now() RETURNING value`.
|
|
20
20
|
- `upsert` → unconditional `INSERT ... ON CONFLICT DO UPDATE`.
|
|
21
|
-
7.
|
|
22
|
-
8. **
|
|
23
|
-
9. **
|
|
21
|
+
7. **Atomic ops are cross-server:** `increment` is one upsert that keeps `expires_at`, `pull` is `DELETE … RETURNING`, `update` is a compare-and-set with bounded retries (callback may run more than once). Tags use a one-statement JSON-array merge.
|
|
22
|
+
8. **`stale_at TIMESTAMPTZ` column** powers [stale-while-revalidate](@warlock.js/cache/use-swr/SKILL.md) — `cache.swr(...)` populates it on writes, plain `set()` leaves it null (always-fresh). Provision via `driver.schema()` like any other column.
|
|
23
|
+
9. **pgvector requires `CREATE EXTENSION vector;` once on the database.** Lazy probe on first vector op throws `CacheConfigurationError` if missing; result is cached.
|
|
24
|
+
10. **Vectors are passed as text literals** (`'[1,2,3]'::vector`). No binary protocol dependency — works against any pg client.
|
|
24
25
|
|
|
25
26
|
## Configuration
|
|
26
27
|
|
|
@@ -1,96 +1,102 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: configure-set-options
|
|
3
|
-
description: "
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# The `set` options object
|
|
7
|
-
|
|
8
|
-
`cache.set(key, value, ttlOrOptions?)` — the 3rd argument accepts three shapes.
|
|
9
|
-
|
|
10
|
-
## The three shapes
|
|
11
|
-
|
|
12
|
-
```ts
|
|
13
|
-
// 1. Number — seconds
|
|
14
|
-
await cache.set("name", "Jane", 600);
|
|
15
|
-
|
|
16
|
-
// 2. String — human-readable duration, parsed via `ms`
|
|
17
|
-
await cache.set("name", "Jane", "10m"); // "1s", "30m", "1h", "7d", "2 weeks"
|
|
18
|
-
|
|
19
|
-
// 3. Options object
|
|
20
|
-
await cache.set("user:1", user, {
|
|
21
|
-
ttl: "1h",
|
|
22
|
-
expiresAt: new Date("2026-12-31"),
|
|
23
|
-
tags: ["users"],
|
|
24
|
-
onConflict: "create",
|
|
25
|
-
driver: "redis",
|
|
26
|
-
});
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
## Option keys
|
|
30
|
-
|
|
31
|
-
| Key | Type | Notes |
|
|
32
|
-
| --- | --- | --- |
|
|
33
|
-
| `ttl` | `number \| string` | Relative expiry. Mutually exclusive with `expiresAt`. |
|
|
34
|
-
| `expiresAt` | `number \| Date` | Absolute deadline (epoch ms or Date). Must be in the future. Mutually exclusive with `ttl`. |
|
|
35
|
-
| `tags` | `string[]` | Inline equivalent of `cache.tags([...]).set(...)`. See [`@warlock.js/cache/use-cache-tags/SKILL.md`](@warlock.js/cache/use-cache-tags/SKILL.md). |
|
|
36
|
-
| `onConflict` | `"create" \| "update" \| "upsert"` | See below. Default `"upsert"`. |
|
|
37
|
-
| `driver` | `string` | Per-call driver override by registered name. |
|
|
38
|
-
| `vector` | `number[]` | Embedding indexed alongside the entry for [`cache.similar()`](@warlock.js/cache/use-cache-similarity/SKILL.md). Drivers without similarity support throw `CacheUnsupportedError`. |
|
|
39
|
-
|
|
40
|
-
##
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
1
|
+
---
|
|
2
|
+
name: configure-set-options
|
|
3
|
+
description: 'Configure cache.set''s third argument — ttl, expiresAt, tags, onConflict (create / update / upsert), driver, vector. Triggers: `cache.set`, `ttl`, `expiresAt`, `tags`, `onConflict`, `driver`, `vector`, `CacheSetResult`, `wasSet`; "set a key only if missing", "set with absolute deadline", "attach tags inline", "route one cache call to redis"; typical import `import { cache } from "@warlock.js/cache"`. Skip: tag fluent API — `@warlock.js/cache/use-cache-tags/SKILL.md`; vector queries — `@warlock.js/cache/use-cache-similarity/SKILL.md`; competing libs `keyv`, `ioredis`.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# The `set` options object
|
|
7
|
+
|
|
8
|
+
`cache.set(key, value, ttlOrOptions?)` — the 3rd argument accepts three shapes.
|
|
9
|
+
|
|
10
|
+
## The three shapes
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
// 1. Number — seconds
|
|
14
|
+
await cache.set("name", "Jane", 600);
|
|
15
|
+
|
|
16
|
+
// 2. String — human-readable duration, parsed via `ms`
|
|
17
|
+
await cache.set("name", "Jane", "10m"); // "1s", "30m", "1h", "7d", "2 weeks"
|
|
18
|
+
|
|
19
|
+
// 3. Options object
|
|
20
|
+
await cache.set("user:1", user, {
|
|
21
|
+
ttl: "1h",
|
|
22
|
+
expiresAt: new Date("2026-12-31"),
|
|
23
|
+
tags: ["users"],
|
|
24
|
+
onConflict: "create",
|
|
25
|
+
driver: "redis",
|
|
26
|
+
});
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Option keys
|
|
30
|
+
|
|
31
|
+
| Key | Type | Notes |
|
|
32
|
+
| --- | --- | --- |
|
|
33
|
+
| `ttl` | `number \| string` | Relative expiry. Mutually exclusive with `expiresAt`. |
|
|
34
|
+
| `expiresAt` | `number \| Date` | Absolute deadline (epoch ms or Date). Must be in the future. Mutually exclusive with `ttl`. |
|
|
35
|
+
| `tags` | `string[]` | Inline equivalent of `cache.tags([...]).set(...)`. See [`@warlock.js/cache/use-cache-tags/SKILL.md`](@warlock.js/cache/use-cache-tags/SKILL.md). |
|
|
36
|
+
| `onConflict` | `"create" \| "update" \| "upsert"` | See below. Default `"upsert"`. |
|
|
37
|
+
| `driver` | `string` | Per-call driver override by registered name. |
|
|
38
|
+
| `vector` | `number[]` | Embedding indexed alongside the entry for [`cache.similar()`](@warlock.js/cache/use-cache-similarity/SKILL.md). Drivers without similarity support throw `CacheUnsupportedError`. |
|
|
39
|
+
|
|
40
|
+
## TTL parsing
|
|
41
|
+
|
|
42
|
+
- Number or bare numeric string (`"3600"`) = **seconds**. Fractions and sub-second values (`0.5`, `"500ms"`) round **up** to whole seconds, never 0.
|
|
43
|
+
- `0` / `Infinity` = no expiry. `NaN`, negative and unparseable values throw `CacheConfigurationError`.
|
|
44
|
+
- `cache.lock()` refuses any TTL that never expires.
|
|
45
|
+
|
|
46
|
+
## `onConflict` policies
|
|
47
|
+
|
|
48
|
+
Self-documenting enum; Redis maps these to `NX` / `XX` natively, others emulate.
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
// create — set only if key is missing
|
|
52
|
+
const result = await cache.set("lock:jobs:import", workerId, {
|
|
53
|
+
onConflict: "create",
|
|
54
|
+
ttl: "5m",
|
|
55
|
+
});
|
|
56
|
+
// result: { wasSet: true, existing: null } on acquire
|
|
57
|
+
// { wasSet: false, existing: <prior workerId> } on conflict — someone else holds the lock
|
|
58
|
+
|
|
59
|
+
if (!result.wasSet) {
|
|
60
|
+
// another worker is already running; abort.
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// update — set only if key exists (don't resurrect expired sessions)
|
|
64
|
+
await cache.set("session:abc", session, { onConflict: "update" });
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Conditional writes (`"create"` / `"update"`) return a `CacheSetResult`; unconditional `"upsert"` returns the value or driver instance as before.
|
|
68
|
+
|
|
69
|
+
## Mutually-exclusive validations
|
|
70
|
+
|
|
71
|
+
Both of these throw `CacheConfigurationError`:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
await cache.set("k", v, { ttl: "1h", expiresAt: Date.now() + 1000 }); // both set
|
|
75
|
+
await cache.set("k", v, { expiresAt: Date.now() - 1000 }); // past deadline
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Inline tags vs `cache.tags([...]).set(...)`
|
|
79
|
+
|
|
80
|
+
Both work; inline is terser when you're writing one value under known tags:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
// Inline
|
|
84
|
+
await cache.set("user:1", user, { tags: ["users", "tenant-42"] });
|
|
85
|
+
|
|
86
|
+
// Fluent (useful when you already have a tagged instance)
|
|
87
|
+
const users = cache.tags(["users"]);
|
|
88
|
+
await users.set("user:1", user);
|
|
89
|
+
await users.invalidate(); // drops every key tagged "users"
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Inline tag semantics are **additive**, never replace. A subsequent `set("user:1", ...)` with no `tags` leaves previous associations intact (the tag index still points to the key), and a `set(..., { tags: [...] })` only appends the key to those tags' index entries — it never removes the key from tags it was bound to earlier. To drop stale bindings, invalidate the old tag explicitly via `cache.tags([...]).invalidate()`.
|
|
93
|
+
|
|
94
|
+
## Back-compat note
|
|
95
|
+
|
|
96
|
+
Every call site using the old positional-TTL shape keeps working:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
await cache.set("k", v); // no TTL — driver default
|
|
100
|
+
await cache.set("k", v, 3600); // seconds
|
|
101
|
+
await cache.set("k", v, undefined); // same as no TTL
|
|
102
|
+
```
|