@warlock.js/cache 5.19.1 → 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.
Files changed (102) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/cjs/index.cjs +1439 -331
  3. package/cjs/index.cjs.map +1 -1
  4. package/esm/cache-manager.d.mts +4 -0
  5. package/esm/cache-manager.d.mts.map +1 -1
  6. package/esm/cache-manager.mjs +23 -5
  7. package/esm/cache-manager.mjs.map +1 -1
  8. package/esm/cached/auto-key.d.mts +14 -14
  9. package/esm/cached/auto-key.d.mts.map +1 -1
  10. package/esm/cached/auto-key.mjs +50 -34
  11. package/esm/cached/auto-key.mjs.map +1 -1
  12. package/esm/cached/cached.mjs +4 -0
  13. package/esm/cached/cached.mjs.map +1 -1
  14. package/esm/drivers/base-cache-driver.d.mts +84 -8
  15. package/esm/drivers/base-cache-driver.d.mts.map +1 -1
  16. package/esm/drivers/base-cache-driver.mjs +152 -42
  17. package/esm/drivers/base-cache-driver.mjs.map +1 -1
  18. package/esm/drivers/file-cache-driver.d.mts +15 -0
  19. package/esm/drivers/file-cache-driver.d.mts.map +1 -1
  20. package/esm/drivers/file-cache-driver.mjs +68 -18
  21. package/esm/drivers/file-cache-driver.mjs.map +1 -1
  22. package/esm/drivers/in-memory-tag-index.d.mts +44 -0
  23. package/esm/drivers/in-memory-tag-index.d.mts.map +1 -0
  24. package/esm/drivers/in-memory-tag-index.mjs +73 -0
  25. package/esm/drivers/in-memory-tag-index.mjs.map +1 -0
  26. package/esm/drivers/index.d.mts +1 -1
  27. package/esm/drivers/index.mjs +1 -1
  28. package/esm/drivers/lru-memory-cache-driver.d.mts +43 -0
  29. package/esm/drivers/lru-memory-cache-driver.d.mts.map +1 -1
  30. package/esm/drivers/lru-memory-cache-driver.mjs +96 -0
  31. package/esm/drivers/lru-memory-cache-driver.mjs.map +1 -1
  32. package/esm/drivers/memory-cache-driver.d.mts +87 -30
  33. package/esm/drivers/memory-cache-driver.d.mts.map +1 -1
  34. package/esm/drivers/memory-cache-driver.mjs +214 -96
  35. package/esm/drivers/memory-cache-driver.mjs.map +1 -1
  36. package/esm/drivers/memory-extended-cache-driver.d.mts +9 -5
  37. package/esm/drivers/memory-extended-cache-driver.d.mts.map +1 -1
  38. package/esm/drivers/memory-extended-cache-driver.mjs +10 -23
  39. package/esm/drivers/memory-extended-cache-driver.mjs.map +1 -1
  40. package/esm/drivers/mock-cache-driver.d.mts +41 -0
  41. package/esm/drivers/mock-cache-driver.d.mts.map +1 -1
  42. package/esm/drivers/mock-cache-driver.mjs +105 -8
  43. package/esm/drivers/mock-cache-driver.mjs.map +1 -1
  44. package/esm/drivers/null-cache-driver.d.mts +8 -2
  45. package/esm/drivers/null-cache-driver.d.mts.map +1 -1
  46. package/esm/drivers/null-cache-driver.mjs +10 -2
  47. package/esm/drivers/null-cache-driver.mjs.map +1 -1
  48. package/esm/drivers/pg-cache-driver.d.mts +77 -3
  49. package/esm/drivers/pg-cache-driver.d.mts.map +1 -1
  50. package/esm/drivers/pg-cache-driver.mjs +229 -7
  51. package/esm/drivers/pg-cache-driver.mjs.map +1 -1
  52. package/esm/drivers/redis-cache-driver.d.mts +86 -1
  53. package/esm/drivers/redis-cache-driver.d.mts.map +1 -1
  54. package/esm/drivers/redis-cache-driver.mjs +252 -16
  55. package/esm/drivers/redis-cache-driver.mjs.map +1 -1
  56. package/esm/index.d.mts +2 -2
  57. package/esm/index.mjs +2 -2
  58. package/esm/list/memory-cache-list.d.mts +11 -8
  59. package/esm/list/memory-cache-list.d.mts.map +1 -1
  60. package/esm/list/memory-cache-list.mjs +39 -37
  61. package/esm/list/memory-cache-list.mjs.map +1 -1
  62. package/esm/scoped-cache.d.mts +9 -3
  63. package/esm/scoped-cache.d.mts.map +1 -1
  64. package/esm/scoped-cache.mjs +19 -7
  65. package/esm/scoped-cache.mjs.map +1 -1
  66. package/esm/tagged-cache.d.mts +20 -0
  67. package/esm/tagged-cache.d.mts.map +1 -1
  68. package/esm/tagged-cache.mjs +62 -24
  69. package/esm/tagged-cache.mjs.map +1 -1
  70. package/esm/tagged-scoped-cache.d.mts +7 -3
  71. package/esm/tagged-scoped-cache.d.mts.map +1 -1
  72. package/esm/tagged-scoped-cache.mjs +19 -7
  73. package/esm/tagged-scoped-cache.mjs.map +1 -1
  74. package/esm/types.d.mts +41 -3
  75. package/esm/types.d.mts.map +1 -1
  76. package/esm/types.mjs.map +1 -1
  77. package/esm/utils.d.mts +9 -3
  78. package/esm/utils.d.mts.map +1 -1
  79. package/esm/utils.mjs +16 -6
  80. package/esm/utils.mjs.map +1 -1
  81. package/llms-full.txt +1685 -1666
  82. package/llms.txt +18 -18
  83. package/package.json +4 -4
  84. package/skills/apply-cache-patterns/SKILL.md +98 -97
  85. package/skills/configure-pg-cache/SKILL.md +6 -5
  86. package/skills/configure-set-options/SKILL.md +102 -96
  87. package/skills/handle-cache-errors/SKILL.md +92 -92
  88. package/skills/index.md +20 -0
  89. package/skills/observe-cache/SKILL.md +103 -103
  90. package/skills/pick-cache-driver/SKILL.md +129 -123
  91. package/skills/test-cache-code/SKILL.md +219 -219
  92. package/skills/use-cache-atomic/SKILL.md +57 -65
  93. package/skills/use-cache-bulk/SKILL.md +57 -57
  94. package/skills/use-cache-list/SKILL.md +85 -85
  95. package/skills/use-cache-lock/SKILL.md +113 -104
  96. package/skills/use-cache-namespace/SKILL.md +88 -88
  97. package/skills/use-cache-similarity/SKILL.md +94 -94
  98. package/skills/use-cache-tags/SKILL.md +7 -4
  99. package/skills/use-cache-update-merge/SKILL.md +84 -84
  100. package/skills/use-cache-utils/SKILL.md +89 -89
  101. package/skills/use-cached-hof/SKILL.md +100 -99
  102. 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): Real-world caching patterns in @warlock.js/cache; use when you need to apply cache patterns.
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): pg cache driver — Postgres setup in @warlock.js/cache; use when you need to configure pg cache.
12
- - [configure-set-options](@warlock.js/cache/configure-set-options/SKILL.md): The set options object in @warlock.js/cache; use when you need to configure set options.
13
- - [handle-cache-errors](@warlock.js/cache/handle-cache-errors/SKILL.md): Error classes in @warlock.js/cache; use when you need to handle cache errors.
14
- - [observe-cache](@warlock.js/cache/observe-cache/SKILL.md): Cache observability — cache.metrics() and the event bus in @warlock.js/cache; use when you need to observe cache.
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): Cache drivers — pick the right one in @warlock.js/cache; use when you need to choose cache driver.
17
- - [test-cache-code](@warlock.js/cache/test-cache-code/SKILL.md): Testing code that touches cache in @warlock.js/cache; use when you need to test cache code.
18
- - [use-cache-atomic](@warlock.js/cache/use-cache-atomic/SKILL.md): Atomic counters — cache.increment / cache.decrement in @warlock.js/cache; use when you need to use cache atomic.
19
- - [use-cache-bulk](@warlock.js/cache/use-cache-bulk/SKILL.md): Bulk operations — cache.many / cache.setMany in @warlock.js/cache; use when you need to use cache bulk.
20
- - [use-cache-list](@warlock.js/cache/use-cache-list/SKILL.md): Lists — the cache.list<T>(key) sub-API in @warlock.js/cache; use when you need to use cache list.
21
- - [use-cache-lock](@warlock.js/cache/use-cache-lock/SKILL.md): cache.lock() — distributed locks with auto-release in @warlock.js/cache; use when you need to use cache lock.
22
- - [use-cache-namespace](@warlock.js/cache/use-cache-namespace/SKILL.md): Scoped caches — cache.namespace(prefix, options?) in @warlock.js/cache; use when you need to use cache namespace.
23
- - [use-cache-similarity](@warlock.js/cache/use-cache-similarity/SKILL.md): cache.similar() — vector-based retrieval in @warlock.js/cache; use when you need to use cache similarity.
24
- - [use-cache-tags](@warlock.js/cache/use-cache-tags/SKILL.md): Tag-based invalidation in @warlock.js/cache; use when you need to use cache tags.
25
- - [use-cache-update-merge](@warlock.js/cache/use-cache-update-merge/SKILL.md): update and merge — atomic read-modify-write in @warlock.js/cache; use when you need to use cache update merge.
26
- - [use-cache-utils](@warlock.js/cache/use-cache-utils/SKILL.md): Cache utilities in @warlock.js/cache; use when you need to use cache utils.
27
- - [use-cached-hof](@warlock.js/cache/use-cached-hof/SKILL.md): cached() — function-memoization wrapper in @warlock.js/cache; use when you need to use cached hof.
28
- - [use-swr](@warlock.js/cache/use-swr/SKILL.md): Stale-while-revalidate — cache.swr(key, options, fn) in @warlock.js/cache; use when you need to use swr.
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.19.1",
9
- "@mongez/reinforcements": "^4.0.1",
10
- "@warlock.js/logger": "5.19.1",
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.19.1",
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: "Real-world caching patterns in @warlock.js/cache; use when you need to apply cache patterns."
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 universal miss sentinel. `remember` short-circuits on a **truthy** cached value (`if (cachedValue) return cachedValue;`), so a stored `null` reads back as a miss and the callback **re-runs on every call** — you get no caching at all, plus a wasted write each time. To actually cache a "not found," store a truthy sentinel instead (see negative caching below).
21
-
22
- ## Cross-process stampede protection — distributed lock via `onConflict`
23
-
24
- `remember`'s lock is per-process. For cross-node safety, acquire a short-lived distributed lock before doing expensive work:
25
-
26
- ```ts
27
- const lockKey = `lock:build-report:${reportId}`;
28
- const acquired = await cache.set(lockKey, process.pid, {
29
- onConflict: "create",
30
- ttl: "2m",
31
- });
32
-
33
- if (!acquired.wasSet) {
34
- // another node is already building — wait or skip
35
- return cache.get(`report:${reportId}`);
36
- }
37
-
38
- try {
39
- const report = await buildExpensiveReport(reportId);
40
- await cache.set(`report:${reportId}`, report, "1h");
41
- return report;
42
- } finally {
43
- await cache.remove(lockKey);
44
- }
45
- ```
46
-
47
- 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).
48
-
49
- ## Negative caching
50
-
51
- Cache "not found" results with a shorter TTL to avoid hammering the origin:
52
-
53
- ```ts
54
- const user = await cache.remember(`user:${id}`, "5m", async () => {
55
- const found = await db.users.find(id);
56
- return found ?? { __miss: true };
57
- });
58
-
59
- if (user?.__miss) {
60
- return null;
61
- }
62
- ```
63
-
64
- Don't return raw `null` inside `remember` to "cache the miss" — it won't. `remember`'s truthy guard treats a stored `null` as a miss, so the callback re-runs every time and the origin still gets hammered. The truthy `{ __miss: true }` sentinel is what actually skips the next call.
65
-
66
- ## Per-tenant caching
67
-
68
- ```ts
69
- // In cache config:
70
- options: {
71
- redis: {
72
- url: "...",
73
- globalPrefix: () => `tenant-${currentContext.tenantId}`,
74
- },
75
- }
76
-
77
- // At the call site — no tenancy awareness needed:
78
- await cache.set("user:1", user, "1h");
79
- // Actual key: "tenant-42.user.1"
80
- ```
81
-
82
- Clear a tenant out:
83
- ```ts
84
- await cache.removeNamespace(""); // when globalPrefix is set, flush scopes to it
85
- // or
86
- await cache.tags([`tenant-${tenantId}`]).invalidate();
87
- ```
88
-
89
- ## See also
90
-
91
- - [`@warlock.js/cache/use-cache-atomic/SKILL.md`](@warlock.js/cache/use-cache-atomic/SKILL.md) — `increment` / `decrement` counters
92
- - [`@warlock.js/cache/use-cache-bulk/SKILL.md`](@warlock.js/cache/use-cache-bulk/SKILL.md) — `many` / `setMany`
93
- - [`@warlock.js/cache/use-cache-utils/SKILL.md`](@warlock.js/cache/use-cache-utils/SKILL.md) — `CACHE_FOR` constants and TTL/key helpers
94
- - [`@warlock.js/cache/use-cache-tags/SKILL.md`](@warlock.js/cache/use-cache-tags/SKILL.md) — tag-based invalidation
95
- - [`@warlock.js/cache/use-cache-namespace/SKILL.md`](@warlock.js/cache/use-cache-namespace/SKILL.md) — scoped handles and `removeNamespace`
96
- - [`@warlock.js/cache/use-swr/SKILL.md`](@warlock.js/cache/use-swr/SKILL.md) — stale-while-revalidate for slow upstreams
97
- - [`@warlock.js/cache/use-cached-hof/SKILL.md`](@warlock.js/cache/use-cached-hof/SKILL.md) — `cached()` HOF for declarative memoization
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: "pg cache driver — Postgres setup in @warlock.js/cache; use when you need to configure pg cache."
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()`. Expired rows aren't auto-deleted unless you GC them yourself.
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. **`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.
22
- 8. **pgvector requires `CREATE EXTENSION vector;` once on the database.** Lazy probe on first vector op throws `CacheConfigurationError` if missing; result is cached.
23
- 9. **Vectors are passed as text literals** (`'[1,2,3]'::vector`). No binary protocol dependency — works against any pg client.
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: "The set options object in @warlock.js/cache; use when you need to configure set options."
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
- ## `onConflict` policies
41
-
42
- Self-documenting enum; Redis maps these to `NX` / `XX` natively, others emulate.
43
-
44
- ```ts
45
- // create — set only if key is missing
46
- const result = await cache.set("lock:jobs:import", workerId, {
47
- onConflict: "create",
48
- ttl: "5m",
49
- });
50
- // result: { wasSet: true, existing: null } on acquire
51
- // { wasSet: false, existing: <prior workerId> } on conflict — someone else holds the lock
52
-
53
- if (!result.wasSet) {
54
- // another worker is already running; abort.
55
- }
56
-
57
- // update — set only if key exists (don't resurrect expired sessions)
58
- await cache.set("session:abc", session, { onConflict: "update" });
59
- ```
60
-
61
- Conditional writes (`"create"` / `"update"`) return a `CacheSetResult`; unconditional `"upsert"` returns the value or driver instance as before.
62
-
63
- ## Mutually-exclusive validations
64
-
65
- Both of these throw `CacheConfigurationError`:
66
-
67
- ```ts
68
- await cache.set("k", v, { ttl: "1h", expiresAt: Date.now() + 1000 }); // both set
69
- await cache.set("k", v, { expiresAt: Date.now() - 1000 }); // past deadline
70
- ```
71
-
72
- ## Inline tags vs `cache.tags([...]).set(...)`
73
-
74
- Both work; inline is terser when you're writing one value under known tags:
75
-
76
- ```ts
77
- // Inline
78
- await cache.set("user:1", user, { tags: ["users", "tenant-42"] });
79
-
80
- // Fluent (useful when you already have a tagged instance)
81
- const users = cache.tags(["users"]);
82
- await users.set("user:1", user);
83
- await users.invalidate(); // drops every key tagged "users"
84
- ```
85
-
86
- 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()`.
87
-
88
- ## Back-compat note
89
-
90
- Every call site using the old positional-TTL shape keeps working:
91
-
92
- ```ts
93
- await cache.set("k", v); // no TTL — driver default
94
- await cache.set("k", v, 3600); // seconds
95
- await cache.set("k", v, undefined); // same as no TTL
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
+ ```