@warlock.js/cache 5.29.0 → 5.31.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 (115) hide show
  1. package/CHANGELOG.md +31 -7
  2. package/README.md +12 -21
  3. package/cjs/index.cjs +134 -71
  4. package/cjs/index.cjs.map +1 -1
  5. package/esm/cache-manager.d.mts +4 -2
  6. package/esm/cache-manager.d.mts.map +1 -1
  7. package/esm/cache-manager.mjs +3 -3
  8. package/esm/cache-manager.mjs.map +1 -1
  9. package/esm/cached/auto-key.d.mts +1 -1
  10. package/esm/cached/auto-key.d.mts.map +1 -1
  11. package/esm/cached/auto-key.mjs +1 -1
  12. package/esm/cached/auto-key.mjs.map +1 -1
  13. package/esm/cached/cached.d.mts +1 -1
  14. package/esm/cached/cached.d.mts.map +1 -1
  15. package/esm/cached/cached.mjs +1 -1
  16. package/esm/cached/cached.mjs.map +1 -1
  17. package/esm/cached/normalize-args.d.mts +1 -1
  18. package/esm/cached/normalize-args.d.mts.map +1 -1
  19. package/esm/cached/normalize-args.mjs +1 -1
  20. package/esm/cached/normalize-args.mjs.map +1 -1
  21. package/esm/drivers/base-cache-driver.d.mts +14 -6
  22. package/esm/drivers/base-cache-driver.d.mts.map +1 -1
  23. package/esm/drivers/base-cache-driver.mjs +32 -15
  24. package/esm/drivers/base-cache-driver.mjs.map +1 -1
  25. package/esm/drivers/file-cache-driver.d.mts +1 -1
  26. package/esm/drivers/file-cache-driver.d.mts.map +1 -1
  27. package/esm/drivers/file-cache-driver.mjs +2 -2
  28. package/esm/drivers/file-cache-driver.mjs.map +1 -1
  29. package/esm/drivers/in-memory-tag-index.d.mts +1 -1
  30. package/esm/drivers/in-memory-tag-index.d.mts.map +1 -1
  31. package/esm/drivers/in-memory-tag-index.mjs +1 -1
  32. package/esm/drivers/in-memory-tag-index.mjs.map +1 -1
  33. package/esm/drivers/index.d.mts +1 -1
  34. package/esm/drivers/index.mjs +1 -1
  35. package/esm/drivers/lru-memory-cache-driver.d.mts +4 -2
  36. package/esm/drivers/lru-memory-cache-driver.d.mts.map +1 -1
  37. package/esm/drivers/lru-memory-cache-driver.mjs +9 -6
  38. package/esm/drivers/lru-memory-cache-driver.mjs.map +1 -1
  39. package/esm/drivers/memory-cache-driver.d.mts +4 -2
  40. package/esm/drivers/memory-cache-driver.d.mts.map +1 -1
  41. package/esm/drivers/memory-cache-driver.mjs +12 -5
  42. package/esm/drivers/memory-cache-driver.mjs.map +1 -1
  43. package/esm/drivers/memory-extended-cache-driver.d.mts +1 -1
  44. package/esm/drivers/memory-extended-cache-driver.d.mts.map +1 -1
  45. package/esm/drivers/memory-extended-cache-driver.mjs +1 -1
  46. package/esm/drivers/memory-extended-cache-driver.mjs.map +1 -1
  47. package/esm/drivers/mock-cache-driver.d.mts +4 -2
  48. package/esm/drivers/mock-cache-driver.d.mts.map +1 -1
  49. package/esm/drivers/mock-cache-driver.mjs +14 -6
  50. package/esm/drivers/mock-cache-driver.mjs.map +1 -1
  51. package/esm/drivers/null-cache-driver.d.mts +5 -1
  52. package/esm/drivers/null-cache-driver.d.mts.map +1 -1
  53. package/esm/drivers/null-cache-driver.mjs +17 -10
  54. package/esm/drivers/null-cache-driver.mjs.map +1 -1
  55. package/esm/drivers/pg-cache-driver.d.mts +4 -2
  56. package/esm/drivers/pg-cache-driver.d.mts.map +1 -1
  57. package/esm/drivers/pg-cache-driver.mjs +9 -5
  58. package/esm/drivers/pg-cache-driver.mjs.map +1 -1
  59. package/esm/drivers/redis-cache-driver.d.mts +17 -3
  60. package/esm/drivers/redis-cache-driver.d.mts.map +1 -1
  61. package/esm/drivers/redis-cache-driver.mjs +26 -5
  62. package/esm/drivers/redis-cache-driver.mjs.map +1 -1
  63. package/esm/index.d.mts +2 -2
  64. package/esm/index.mjs +2 -2
  65. package/esm/list/memory-cache-list.d.mts +1 -1
  66. package/esm/list/memory-cache-list.d.mts.map +1 -1
  67. package/esm/list/memory-cache-list.mjs +1 -1
  68. package/esm/list/memory-cache-list.mjs.map +1 -1
  69. package/esm/metrics.d.mts +1 -1
  70. package/esm/metrics.d.mts.map +1 -1
  71. package/esm/metrics.mjs +1 -1
  72. package/esm/metrics.mjs.map +1 -1
  73. package/esm/scoped-cache.d.mts +4 -2
  74. package/esm/scoped-cache.d.mts.map +1 -1
  75. package/esm/scoped-cache.mjs +3 -3
  76. package/esm/scoped-cache.mjs.map +1 -1
  77. package/esm/tagged-cache.d.mts +4 -2
  78. package/esm/tagged-cache.d.mts.map +1 -1
  79. package/esm/tagged-cache.mjs +3 -3
  80. package/esm/tagged-cache.mjs.map +1 -1
  81. package/esm/tagged-scoped-cache.d.mts +4 -2
  82. package/esm/tagged-scoped-cache.d.mts.map +1 -1
  83. package/esm/tagged-scoped-cache.mjs +3 -3
  84. package/esm/tagged-scoped-cache.mjs.map +1 -1
  85. package/esm/types.d.mts +17 -6
  86. package/esm/types.d.mts.map +1 -1
  87. package/esm/types.mjs +1 -1
  88. package/esm/types.mjs.map +1 -1
  89. package/esm/utils.d.mts +1 -1
  90. package/esm/utils.d.mts.map +1 -1
  91. package/esm/utils.mjs +1 -1
  92. package/esm/utils.mjs.map +1 -1
  93. package/llms-full.txt +231 -224
  94. package/llms.txt +1 -1
  95. package/package.json +5 -4
  96. package/skills/apply-cache-patterns/SKILL.md +3 -2
  97. package/skills/cache-basics/SKILL.md +26 -24
  98. package/skills/configure-pg-cache/SKILL.md +2 -2
  99. package/skills/configure-set-options/SKILL.md +14 -14
  100. package/skills/handle-cache-errors/SKILL.md +8 -8
  101. package/skills/index.md +4 -1
  102. package/skills/observe-cache/SKILL.md +8 -8
  103. package/skills/pick-cache-driver/SKILL.md +24 -23
  104. package/skills/test-cache-code/SKILL.md +15 -10
  105. package/skills/use-cache-atomic/SKILL.md +17 -11
  106. package/skills/use-cache-bulk/SKILL.md +8 -5
  107. package/skills/use-cache-list/SKILL.md +13 -13
  108. package/skills/use-cache-lock/SKILL.md +11 -17
  109. package/skills/use-cache-namespace/SKILL.md +11 -13
  110. package/skills/use-cache-similarity/SKILL.md +9 -9
  111. package/skills/use-cache-tags/SKILL.md +13 -13
  112. package/skills/use-cache-update-merge/SKILL.md +10 -10
  113. package/skills/use-cache-utils/SKILL.md +17 -17
  114. package/skills/use-cached-hof/SKILL.md +8 -7
  115. package/skills/use-swr/SKILL.md +14 -18
package/llms.txt CHANGED
@@ -15,7 +15,7 @@
15
15
  - [overview](@warlock.js/cache/overview/SKILL.md): @warlock.js/cache overview for orientation and choosing the focused skill for the task.
16
16
  - [pick-cache-driver](@warlock.js/cache/pick-cache-driver/SKILL.md): Pick a cache driver — null / memory / memoryExtended / lru / file / redis / pg / mock — and configure it. Triggers: `cache.setCacheConfigurations`, `BaseCacheDriver`, `cache.use`, `cache.load`, `cache.driver`, `globalPrefix`; "which cache driver should I use", "configure redis driver", "register custom cache driver", "multi-tenant scoping"; typical import `import { cache, BaseCacheDriver } from "@warlock.js/cache"`. Skip: cache CRUD — the `cache-basics` topic; pg setup — the `configure-pg-cache` topic; competing libs `lru-cache`, `node-cache`, `keyv`, `ioredis`; native `Map`.
17
17
  - [test-cache-code](@warlock.js/cache/test-cache-code/SKILL.md): Test code that touches cache — MockCacheDriver (behavioral assertions with wasCalled / callLog), MemoryCacheDriver (full-stack), NullCacheDriver (graceful-degradation). Triggers: `MockCacheDriver`, `MemoryCacheDriver`, `NullCacheDriver`, `wasCalled`, `callLog`, `getStored`, `reset`, `cache.on`; "assert cache was invalidated", "test code that uses cache.set", "mock cache in vitest", "test similarity without a real embedder", "stub the pg cache driver"; typical import `import { cache, MockCacheDriver, MemoryCacheDriver } from "@warlock.js/cache"`. Skip: real driver picks — the `pick-cache-driver` topic; competing libs `jest-mock`, `sinon`, `redis-mock`; native `vi.fn`.
18
- - [use-cache-atomic](@warlock.js/cache/use-cache-atomic/SKILL.md): Atomic counters via cache.increment(key, by=1) / cache.decrement(key, by=1) — returns the new number, throws on non-numeric values. Triggers: `cache.increment`, `cache.decrement`, "view counter", "page views", "atomic counter", "decrement stock", "rate-limit counter", "INCRBY"; typical import `import { cache } from "@warlock.js/cache"`. Skip: read-modify-write of objects — the `use-cache-update-merge` topic; named-lock coordination — the `use-cache-lock` topic; competing libs `ioredis` `INCR`, native counters in a `Map`.
18
+ - [use-cache-atomic](@warlock.js/cache/use-cache-atomic/SKILL.md): Atomic counters via cache.increment(key, by=1, { ttl }?) / cache.decrement(key, by=1) — returns the new number, throws on non-numeric values. Triggers: `cache.increment`, `cache.decrement`, "view counter", "page views", "atomic counter", "decrement stock", "rate-limit counter", "INCRBY"; typical import `import { cache } from "@warlock.js/cache"`. Skip: read-modify-write of objects — the `use-cache-update-merge` topic; named-lock coordination — the `use-cache-lock` topic; competing libs `ioredis` `INCR`, native counters in a `Map`.
19
19
  - [use-cache-bulk](@warlock.js/cache/use-cache-bulk/SKILL.md): Bulk reads/writes via cache.many(keys[]) → values[] (nulls for misses, order preserved) and cache.setMany(record, ttl?) → void. Triggers: `cache.many`, `cache.setMany`, "get multiple keys at once", "batch read cache", "warm the cache", "preload many keys", "mget", "mset"; typical import `import { cache } from "@warlock.js/cache"`. Skip: tag-based bulk invalidation — the `use-cache-tags` topic; single-key ops — the `cache-basics` topic; competing libs `ioredis` `MGET`/`MSET`.
20
20
  - [use-cache-list](@warlock.js/cache/use-cache-list/SKILL.md): Ordered collections via cache.list<T>(key) — push / unshift / pop / shift / slice / all / length / trim / clear. Triggers: `cache.list`, `push`, `unshift`, `pop`, `shift`, `slice`, `trim`, `clear`; "job queue in cache", "keep most recent N events", "audit log buffer", "FIFO queue"; typical import `import { cache } from "@warlock.js/cache"`. Skip: locking around list writes — the `use-cache-lock` topic; competing libs `bullmq`, `bee-queue`, `bull`, `ioredis` `LPUSH`; native `Array.push`.
21
21
  - [use-cache-lock](@warlock.js/cache/use-cache-lock/SKILL.md): Distributed lock via cache.lock(key, ttl, fn) — acquire, run fn, auto-release. Returns {acquired: true, value} or {acquired: false}. Triggers: `cache.lock`, `LockOutcome`, `acquired`, `owner`; "run cron on only one server", "idempotent webhook handler", "dedup payment processing", "lock a task across nodes"; typical import `import { cache } from "@warlock.js/cache"`. Skip: raw `onConflict: "create"` recipe — the `apply-cache-patterns` topic; memoization — the `use-cached-hof` topic; competing libs `redlock`, `async-mutex`, `proper-lockfile`.
package/package.json CHANGED
@@ -5,9 +5,9 @@
5
5
  "environment": "server"
6
6
  },
7
7
  "dependencies": {
8
- "@warlock.js/fs": "5.29.0",
8
+ "@warlock.js/fs": "5.31.0",
9
9
  "@mongez/reinforcements": "^4.0.2",
10
- "@warlock.js/logger": "5.29.0",
10
+ "@warlock.js/logger": "5.31.0",
11
11
  "ms": "^2.1.3"
12
12
  },
13
13
  "peerDependencies": {
@@ -24,7 +24,8 @@
24
24
  },
25
25
  "repository": {
26
26
  "type": "git",
27
- "url": "https://github.com/warlockjs/cache"
27
+ "url": "git+https://github.com/warlockjs/framework.git",
28
+ "directory": "packages/cache"
28
29
  },
29
30
  "keywords": [
30
31
  "nodejs",
@@ -46,7 +47,7 @@
46
47
  ],
47
48
  "author": "hassanzohdy",
48
49
  "license": "MIT",
49
- "version": "5.29.0",
50
+ "version": "5.31.0",
50
51
  "main": "./cjs/index.cjs",
51
52
  "module": "./esm/index.mjs",
52
53
  "types": "./esm/index.d.mts",
@@ -11,7 +11,7 @@ Common shapes — the "general patterns" file. Specialized shapes live in their
11
11
 
12
12
  ```ts
13
13
  const user = await cache.remember(`user:${id}`, "1h", async () => {
14
- return db.users.find(id); // runs only on cache miss
14
+ return db.users.find(id); // runs only on cache miss
15
15
  });
16
16
  ```
17
17
 
@@ -81,8 +81,9 @@ await cache.set("user:1", user, "1h");
81
81
  ```
82
82
 
83
83
  Clear a tenant out:
84
+
84
85
  ```ts
85
- await cache.removeNamespace(""); // when globalPrefix is set, flush scopes to it
86
+ await cache.removeNamespace(""); // when globalPrefix is set, flush scopes to it
86
87
  // or
87
88
  await cache.tags([`tenant-${tenantId}`]).invalidate();
88
89
  ```
@@ -61,20 +61,22 @@ cache.setCacheConfigurations(config);
61
61
  await cache.init();
62
62
  ```
63
63
 
64
+ `logging` turns the per-operation key log (`<key> fetched`, `<key> not found`, ...) on or off. Email addresses inside logged keys are masked (`user.member@example.com` logs as `u***@example.com`), in operation lines and in error lines (which are written even with logging off); other personal data in a key is logged as is, so keep it out of keys or turn logging off in production.
65
+
64
66
  ## Primary ops
65
67
 
66
68
  ```ts
67
69
  // Set + get
68
70
  await cache.set("user.1", user, "1h");
69
- const cached = await cache.get<User>("user.1"); // User | null
71
+ const cached = await cache.get<User>("user.1"); // User | null
70
72
 
71
73
  // Presence + read-and-delete
72
- const exists = await cache.has("user.1"); // boolean
73
- const taken = await cache.pull<User>("user.1"); // returns then removes
74
+ const exists = await cache.has("user.1"); // boolean
75
+ const taken = await cache.pull<User>("user.1"); // returns then removes
74
76
 
75
77
  // Remove + flush
76
78
  await cache.remove("user.1");
77
- await cache.flush(); // wipe everything (current driver)
79
+ await cache.flush(); // wipe everything (current driver)
78
80
 
79
81
  // Many at once — array positionally aligned with the keys (null for misses)
80
82
  const [u1, u2, u3] = await cache.many(["user.1", "user.2", "user.3"]);
@@ -84,9 +86,9 @@ const [u1, u2, u3] = await cache.many(["user.1", "user.2", "user.3"]);
84
86
  await cache.forever("config.version", "1.2.3");
85
87
 
86
88
  // Counters
87
- await cache.increment("post.42.views"); // +1, returns new value
88
- await cache.increment("post.42.views", 10); // +10
89
- await cache.decrement("inventory.sku-x"); // -1
89
+ await cache.increment("post.42.views"); // +1, returns new value
90
+ await cache.increment("post.42.views", 10); // +10
91
+ await cache.decrement("inventory.sku-x"); // -1
90
92
 
91
93
  // Memoize an expensive function
92
94
  const user = await cache.remember("user.1", "1h", async () => db.users.find(1));
@@ -94,23 +96,23 @@ const user = await cache.remember("user.1", "1h", async () => db.users.find(1));
94
96
 
95
97
  ## Pick a skill
96
98
 
97
- | If the task is about… | Load |
98
- | --- | --- |
99
- | Choosing a driver, configuring it, or understanding what each one does best | The `pick-cache-driver` topic |
100
- | The `set` options object (`ttl`, `expiresAt`, `tags`, `onConflict`, `driver`, `vector`) | The `configure-set-options` topic |
101
- | Memoization with `remember()`, counters, negative caching, per-tenant scoping, TTL constants | The `apply-cache-patterns` topic |
102
- | Scoped handles via `cache.namespace(prefix, options?)` | The `use-cache-namespace` topic |
103
- | Tag-based invalidation — `cache.tags([...]).invalidate()` | The `use-cache-tags` topic |
104
- | Stale-while-revalidate — `cache.swr(...)` | The `use-swr` topic |
105
- | Wrapping a function with `cached()` — HOF memoization with `.invalidate()` | The `use-cached-hof` topic |
106
- | Distributed locks — `cache.lock(key, ttl, fn)` with auto-release | The `use-cache-lock` topic |
107
- | Queues, recent-N buffers, `push`/`shift`/`trim` — the list sub-API | The `use-cache-list` topic |
108
- | Atomic read-modify-write via `update()` and `merge()` | The `use-cache-update-merge` topic |
109
- | Similarity retrieval — `set({ vector })` + `cache.similar(...)` | The `use-cache-similarity` topic |
110
- | Postgres driver setup (KV-only or with pgvector) | The `configure-pg-cache` topic |
111
- | `cache.metrics()` aggregate snapshot + event bus for per-event reactions | The `observe-cache` topic |
112
- | Error classes (`CacheConfigurationError`, `CacheUnsupportedError`, etc.) | The `handle-cache-errors` topic |
113
- | Tests that touch cache code paths — `MockCacheDriver`, `MemoryCacheDriver` | The `test-cache-code` topic |
99
+ | If the task is about… | Load |
100
+ | -------------------------------------------------------------------------------------------- | ---------------------------------- |
101
+ | Choosing a driver, configuring it, or understanding what each one does best | The `pick-cache-driver` topic |
102
+ | The `set` options object (`ttl`, `expiresAt`, `tags`, `onConflict`, `driver`, `vector`) | The `configure-set-options` topic |
103
+ | Memoization with `remember()`, counters, negative caching, per-tenant scoping, TTL constants | The `apply-cache-patterns` topic |
104
+ | Scoped handles via `cache.namespace(prefix, options?)` | The `use-cache-namespace` topic |
105
+ | Tag-based invalidation — `cache.tags([...]).invalidate()` | The `use-cache-tags` topic |
106
+ | Stale-while-revalidate — `cache.swr(...)` | The `use-swr` topic |
107
+ | Wrapping a function with `cached()` — HOF memoization with `.invalidate()` | The `use-cached-hof` topic |
108
+ | Distributed locks — `cache.lock(key, ttl, fn)` with auto-release | The `use-cache-lock` topic |
109
+ | Queues, recent-N buffers, `push`/`shift`/`trim` — the list sub-API | The `use-cache-list` topic |
110
+ | Atomic read-modify-write via `update()` and `merge()` | The `use-cache-update-merge` topic |
111
+ | Similarity retrieval — `set({ vector })` + `cache.similar(...)` | The `use-cache-similarity` topic |
112
+ | Postgres driver setup (KV-only or with pgvector) | The `configure-pg-cache` topic |
113
+ | `cache.metrics()` aggregate snapshot + event bus for per-event reactions | The `observe-cache` topic |
114
+ | Error classes (`CacheConfigurationError`, `CacheUnsupportedError`, etc.) | The `handle-cache-errors` topic |
115
+ | Tests that touch cache code paths — `MockCacheDriver`, `MemoryCacheDriver` | The `test-cache-code` topic |
114
116
 
115
117
  ## Things NOT to do
116
118
 
@@ -39,8 +39,8 @@ cache.setCacheConfigurations({
39
39
  options: {
40
40
  pg: {
41
41
  client: pool,
42
- table: "warlock_cache", // optional, default
43
- ttl: "1h", // optional default
42
+ table: "warlock_cache", // optional, default
43
+ ttl: "1h", // optional default
44
44
  globalPrefix: "prod-app",
45
45
  },
46
46
  },
@@ -14,7 +14,7 @@ description: 'Configure cache.set''s third argument — ttl, expiresAt, tags, on
14
14
  await cache.set("name", "Jane", 600);
15
15
 
16
16
  // 2. String — human-readable duration, parsed via `ms`
17
- await cache.set("name", "Jane", "10m"); // "1s", "30m", "1h", "7d", "2 weeks"
17
+ await cache.set("name", "Jane", "10m"); // "1s", "30m", "1h", "7d", "2 weeks"
18
18
 
19
19
  // 3. Options object
20
20
  await cache.set("user:1", user, {
@@ -28,14 +28,14 @@ await cache.set("user:1", user, {
28
28
 
29
29
  ## Option keys
30
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 the `use-cache-tags` topic. |
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()` (see the `use-cache-similarity` topic). Drivers without similarity support throw `CacheUnsupportedError`. |
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 the `use-cache-tags` topic. |
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()` (see the `use-cache-similarity` topic). Drivers without similarity support throw `CacheUnsupportedError`. |
39
39
 
40
40
  ## TTL parsing
41
41
 
@@ -71,8 +71,8 @@ Conditional writes (`"create"` / `"update"`) return a `CacheSetResult`; uncondit
71
71
  Both of these throw `CacheConfigurationError`:
72
72
 
73
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
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
76
  ```
77
77
 
78
78
  ## Inline tags vs `cache.tags([...]).set(...)`
@@ -86,7 +86,7 @@ await cache.set("user:1", user, { tags: ["users", "tenant-42"] });
86
86
  // Fluent (useful when you already have a tagged instance)
87
87
  const users = cache.tags(["users"]);
88
88
  await users.set("user:1", user);
89
- await users.invalidate(); // drops every key tagged "users"
89
+ await users.invalidate(); // drops every key tagged "users"
90
90
  ```
91
91
 
92
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()`.
@@ -96,7 +96,7 @@ Inline tag semantics are **additive**, never replace. A subsequent `set("user:1"
96
96
  Every call site using the old positional-TTL shape keeps working:
97
97
 
98
98
  ```ts
99
- await cache.set("k", v); // no TTL — driver default
100
- await cache.set("k", v, 3600); // seconds
99
+ await cache.set("k", v); // no TTL — driver default
100
+ await cache.set("k", v, 3600); // seconds
101
101
  await cache.set("k", v, undefined); // same as no TTL
102
102
  ```
@@ -18,14 +18,14 @@ import {
18
18
  } from "@warlock.js/cache";
19
19
  ```
20
20
 
21
- | Class | When it's thrown | How to react |
22
- | --- | --- | --- |
23
- | `CacheError` | Abstract base — don't throw directly, match against it to catch any cache error | `catch (e) { if (e instanceof CacheError) … }` |
24
- | `CacheConfigurationError` | Bad TTL string, `ttl` + `expiresAt` together, `expiresAt` in the past, missing required driver option (e.g. redis without url/host, file without directory), attempting to use an unregistered driver name | Fix the config / call site. Never catch at runtime — this is a programmer error, not a user-facing one. |
25
- | `CacheConnectionError` | Declared for driver connection failures. Not thrown by any built-in driver today (Redis currently logs the error and emits an `"error"` event instead of throwing on failed connect). | Reserved for future use. |
26
- | `CacheDriverNotInitializedError` | Any data op called before `cache.init()` / `cache.use()` | Call `cache.init()` at app startup. Tests often forget this — add a `beforeEach`. |
27
- | `CacheUnsupportedError` | Driver doesn't implement the requested op. Today: `update` / `merge` on the file driver; `set({ vector })` and `similar()` on file / redis / pg-without-`vector`-config. | Switch driver (memory family for dev similarity, `pg` with `vector` config for production), or queue the op. |
28
- | `CacheConcurrencyError` | Declared for future optimistic-concurrency exhaustion on Redis `update()` | Not thrown today. Reserved for the v2.1 `WATCH`/`MULTI` implementation. |
21
+ | Class | When it's thrown | How to react |
22
+ | -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
23
+ | `CacheError` | Abstract base — don't throw directly, match against it to catch any cache error | `catch (e) { if (e instanceof CacheError) … }` |
24
+ | `CacheConfigurationError` | Bad TTL string, `ttl` + `expiresAt` together, `expiresAt` in the past, missing required driver option (e.g. redis without url/host, file without directory), attempting to use an unregistered driver name | Fix the config / call site. Never catch at runtime — this is a programmer error, not a user-facing one. |
25
+ | `CacheConnectionError` | Declared for driver connection failures. Not thrown by any built-in driver today (Redis currently logs the error and emits an `"error"` event instead of throwing on failed connect). | Reserved for future use. |
26
+ | `CacheDriverNotInitializedError` | Any data op called before `cache.init()` / `cache.use()` | Call `cache.init()` at app startup. Tests often forget this — add a `beforeEach`. |
27
+ | `CacheUnsupportedError` | Driver doesn't implement the requested op. Today: `update` / `merge` on the file driver; `set({ vector })` and `similar()` on file / redis / pg-without-`vector`-config. | Switch driver (memory family for dev similarity, `pg` with `vector` config for production), or queue the op. |
28
+ | `CacheConcurrencyError` | Declared for future optimistic-concurrency exhaustion on Redis `update()` | Not thrown today. Reserved for the v2.1 `WATCH`/`MULTI` implementation. |
29
29
  | `CacheError` (file driver, path containment) | A key/namespace, after percent-encoding, still resolves outside the cache root — `"Cache key resolves outside the cache directory: ..."`. Last line of defense against path traversal on top of the encoding step; should not happen in normal use. | Treat as a programmer/attacker-input error — don't retry; investigate the key source. See the `pick-cache-driver` topic for the encoding contract. |
30
30
 
31
31
  ## Special case — `setNX` unsupported
package/skills/index.md CHANGED
@@ -1,11 +1,13 @@
1
1
  ---
2
- description: "Cache layer for Warlock apps with pluggable drivers (memory, LRU, file, Redis, Postgres). Exports `cache`, `CacheManager`, `cached`, `TaggedCache`, `ScopedCache`, `RedisCacheDriver`, `MemoryCacheDriver`, `MockCacheDriver`, `PgCacheDriver`. Use for: \"cache this query for 5 minutes\", \"invalidate every key for tenant 42\", \"stale-while-revalidate\", \"take a lock so only one worker runs this\", \"tag keys and flush the tag\", \"increment a counter in cache\". Not this package: HTTP response/page caching belongs to @warlock.js/core or web; persistent data is a @warlock.js/cascade model."
2
+ description: 'Cache layer for Warlock apps with pluggable drivers (memory, LRU, file, Redis, Postgres). Exports `cache`, `CacheManager`, `cached`, `TaggedCache`, `ScopedCache`, `RedisCacheDriver`, `MemoryCacheDriver`, `MockCacheDriver`, `PgCacheDriver`. Use for: "cache this query for 5 minutes", "invalidate every key for tenant 42", "stale-while-revalidate", "take a lock so only one worker runs this", "tag keys and flush the tag", "increment a counter in cache". Not this package: HTTP response/page caching belongs to @warlock.js/core or web; persistent data is a @warlock.js/cascade model.'
3
3
  ---
4
+
4
5
  # @warlock.js/cache
5
6
 
6
7
  The `cache` singleton (a `CacheManager`) fronts one active driver. Everything, from `get`/`set` through `remember`, `swr`, `tags`, `namespace`, `lock` and `list`, works the same on any driver, so pick a driver once and write app code against `cache`.
7
8
 
8
9
  ## The 80% path
10
+
9
11
  1. Orient: `overview.md`, `cache-basics.md`.
10
12
  2. Choose and configure a driver: `pick-cache-driver.md` (Postgres: `configure-pg-cache.md`; TTL and options: `configure-set-options.md`).
11
13
  3. Cache expensive reads with `remember` or the `cached` wrapper: `use-cached-hof.md`, `apply-cache-patterns.md`.
@@ -14,6 +16,7 @@ The `cache` singleton (a `CacheManager`) fronts one active driver. Everything, f
14
16
  6. Test with the mock driver: `test-cache-code.md`; errors and metrics: `handle-cache-errors.md`, `observe-cache.md`.
15
17
 
16
18
  ## Conventions and pitfalls
19
+
17
20
  - Tags versus namespaces: tags are cross-cutting groups you flush together; a namespace is a key prefix you remove wholesale. Choose per invalidation need.
18
21
  - Check `configure-set-options.md` for TTL units, and avoid `forever` for data that can go stale.
19
22
  - Atomic counters, bulk ops, lists, merge and similarity have their own topics (`use-cache-atomic.md`, `use-cache-bulk.md`, `use-cache-list.md`, `use-cache-update-merge.md`, `use-cache-similarity.md`); do not hand-roll read-modify-write.
@@ -50,15 +50,15 @@ Listeners attached via `cache.on(...)` survive driver switches the same way the
50
50
 
51
51
  ## Which one to reach for
52
52
 
53
- | Goal | Use |
54
- |---|---|
55
- | Show hit rate / latency in a dashboard | `cache.metrics()` |
56
- | Page on cache errors | `cache.on("error", ...)` |
57
- | Periodic export to Prometheus / StatsD | `cache.metrics()` + `setInterval` + `resetMetrics()` |
58
- | Audit log of every removal | `cache.on("removed", ...)` |
53
+ | Goal | Use |
54
+ | ------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
55
+ | Show hit rate / latency in a dashboard | `cache.metrics()` |
56
+ | Page on cache errors | `cache.on("error", ...)` |
57
+ | Periodic export to Prometheus / StatsD | `cache.metrics()` + `setInterval` + `resetMetrics()` |
58
+ | Audit log of every removal | `cache.on("removed", ...)` |
59
59
  | See which tag invalidations ran, what they removed and how long they took | `cache.on("invalidated", ({ tags, keys, durationMs }) => ...)` — once per `tags([...]).invalidate()` |
60
- | Detect a specific anti-pattern (e.g. always-miss key) | `cache.on("miss", ...)` |
61
- | Debug "is the cache being hit at all?" in dev | `cache.metrics()` once at the end of a flow |
60
+ | Detect a specific anti-pattern (e.g. always-miss key) | `cache.on("miss", ...)` |
61
+ | Debug "is the cache being hit at all?" in dev | `cache.metrics()` once at the end of a flow |
62
62
 
63
63
  Both layers can coexist — events fire whether the metrics collector is attached or not.
64
64
 
@@ -7,30 +7,30 @@ description: 'Pick a cache driver — null / memory / memoryExtended / lru / fil
7
7
 
8
8
  Seven production drivers + a mock driver ship in-box. Pick by durability, scope, and workload.
9
9
 
10
- | Driver | Process scope | Persists on restart | Good for | Avoid when |
11
- | --- | --- | --- | --- | --- |
12
- | `null` | — | — | Disabling cache in tests; feature-flagging off | You actually want caching |
13
- | `memory` | Single process | No | Hot in-process data with default TTL; smallest latency | Multi-process / multi-node |
14
- | `memoryExtended` | Single process | No | Sliding-window TTL (TTL resets on every read) | Any multi-process deploy |
15
- | `lru` | Single process | No | Bounded in-memory caches (capacity-based eviction) | Need cross-process sharing |
16
- | `file` | Single host | Yes | Build artefacts, local dev persistence across restarts | Concurrency (no locks); multi-host |
17
- | `redis` | Shared | Yes (Redis-managed) | Anything shared across processes / nodes | Single-process-only workload — overkill |
18
- | `pg` | Shared | Yes (Postgres-managed) | You already run Postgres; semantic caching / RAG via pgvector | High-throughput hot reads (Redis is faster) |
10
+ | Driver | Process scope | Persists on restart | Good for | Avoid when |
11
+ | ---------------- | -------------- | ---------------------- | ------------------------------------------------------------- | ------------------------------------------- |
12
+ | `null` | — | — | Disabling cache in tests; feature-flagging off | You actually want caching |
13
+ | `memory` | Single process | No | Hot in-process data with default TTL; smallest latency | Multi-process / multi-node |
14
+ | `memoryExtended` | Single process | No | Sliding-window TTL (TTL resets on every read) | Any multi-process deploy |
15
+ | `lru` | Single process | No | Bounded in-memory caches (capacity-based eviction) | Need cross-process sharing |
16
+ | `file` | Single host | Yes | Build artefacts, local dev persistence across restarts | Concurrency (no locks); multi-host |
17
+ | `redis` | Shared | Yes (Redis-managed) | Anything shared across processes / nodes | Single-process-only workload — overkill |
18
+ | `pg` | Shared | Yes (Postgres-managed) | You already run Postgres; semantic caching / RAG via pgvector | High-throughput hot reads (Redis is faster) |
19
19
 
20
20
  ## Capability matrix
21
21
 
22
- | Capability | null | memory | memoryExt | lru | file | redis | pg |
23
- | --- | :-: | :-: | :-: | :-: | :-: | :-: | :-: |
24
- | `set` / `get` / `remove` / `flush` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
25
- | TTL (number or string; numeric strings = seconds) | — | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
26
- | Sliding TTL on read | — | — | ✓ | — | — | — | — |
27
- | `removeNamespace` | noop | ✓ | ✓ | ✓ (prefix-scan) | ✓ | ✓ | ✓ (LIKE prefix) |
28
- | `onConflict: "create"` / `"update"` | noop | emulated | emulated | emulated | emulated | native `NX`/`XX` | native (INSERT ON CONFLICT) |
29
- | `increment` / `decrement` / `pull` | — | one process | one process | one process | one process | **cross-server** | **cross-server** |
30
- | `update()` / `merge()` | ✓ | one process | one process | one process | ✗ throws | **cross-server** (compare-and-set) | **cross-server** (compare-and-set) |
31
- | List sub-API | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ (O(n) JSON blob today; native LPUSH/LRANGE in v2.1) | ✓ |
32
- | Tagged invalidation | noop | ✓ | ✓ | ✓ | ✓ (serialized index) | ✓ (native SET) | ✓ (JSON-array merge) |
33
- | `similar()` / `set({ vector })` | returns `[]` / noop | ✓ brute force | ✓ brute force | ✓ brute force | ✗ throws | ✗ throws (Phase 2 backlog) | ✓ (with `vector` config — pgvector) |
22
+ | Capability | null | memory | memoryExt | lru | file | redis | pg |
23
+ | ------------------------------------------------- | :-----------------: | :-----------: | :-----------: | :-------------: | :------------------: | :---------------------------------------------------: | :---------------------------------: |
24
+ | `set` / `get` / `remove` / `flush` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
25
+ | TTL (number or string; numeric strings = seconds) | — | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
26
+ | Sliding TTL on read | — | — | ✓ | — | — | — | — |
27
+ | `removeNamespace` | noop | ✓ | ✓ | ✓ (prefix-scan) | ✓ | ✓ | ✓ (LIKE prefix) |
28
+ | `onConflict: "create"` / `"update"` | noop | emulated | emulated | emulated | emulated | native `NX`/`XX` | native (INSERT ON CONFLICT) |
29
+ | `increment` / `decrement` / `pull` | — | one process | one process | one process | one process | **cross-server** | **cross-server** |
30
+ | `update()` / `merge()` | ✓ | one process | one process | one process | ✗ throws | **cross-server** (compare-and-set) | **cross-server** (compare-and-set) |
31
+ | List sub-API | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ (O(n) JSON blob today; native LPUSH/LRANGE in v2.1) | ✓ |
32
+ | Tagged invalidation | noop | ✓ | ✓ | ✓ | ✓ (serialized index) | ✓ (native SET) | ✓ (JSON-array merge) |
33
+ | `similar()` / `set({ vector })` | returns `[]` / noop | ✓ brute force | ✓ brute force | ✓ brute force | ✗ throws | ✗ throws (Phase 2 backlog) | ✓ (with `vector` config — pgvector) |
34
34
 
35
35
  ## Multi-server: use redis or pg
36
36
 
@@ -102,13 +102,14 @@ const pool = new Pool({ connectionString: process.env.DATABASE_URL });
102
102
  cache.setCacheConfigurations({
103
103
  default: "pg",
104
104
  drivers: { pg: PgCacheDriver },
105
- options: { pg: { table: "cache" } }, // static
105
+ options: { pg: { table: "cache" } }, // static
106
106
  });
107
107
 
108
- await cache.use("pg", { client: pool }); // runtime — skip init() in this case
108
+ await cache.use("pg", { client: pool }); // runtime — skip init() in this case
109
109
  ```
110
110
 
111
111
  Constraints:
112
+
112
113
  - The driver name must be registered in `setCacheConfigurations({ drivers })` — runtime options don't bypass registration.
113
114
  - Once a driver is loaded, calling `use`/`load`/`driver` again with **non-empty** new options throws `CacheConfigurationError`. Register a second driver name if you need a different config.
114
115
  - Calling without options (or with `{}`) on an already-loaded driver returns the cached instance silently.
@@ -63,7 +63,7 @@ When you want the real read/write semantics (eviction, TTL expiry, similarity sc
63
63
 
64
64
  `MockCacheDriver` does NOT implement `similar()` — vector writes are recorded into the call log but nearest-neighbor scoring is not available. Tests that call `cache.similar(...)` should use the memory driver.
65
65
 
66
- ## Strategy 3 — `NullCacheDriver` when you need cache *off*
66
+ ## Strategy 3 — `NullCacheDriver` when you need cache _off_
67
67
 
68
68
  Use `NullCacheDriver` to disable caching entirely for code paths that should still work without a cache (graceful-degradation tests):
69
69
 
@@ -88,7 +88,9 @@ import { vi } from "vitest";
88
88
  class FakeRedisClient {
89
89
  public store = new Map<string, string>();
90
90
  private expires = new Map<string, number>();
91
- public on() { return this; }
91
+ public on() {
92
+ return this;
93
+ }
92
94
  public async connect() {}
93
95
  public async quit() {}
94
96
  public async set(key: string, value: string, opts?: { EX?: number; NX?: boolean; XX?: boolean }) {
@@ -116,13 +118,18 @@ class FakeRedisClient {
116
118
  const regex = new RegExp("^" + pattern.replace(/\*/g, ".*") + "$");
117
119
  return [...this.store.keys()].filter((k) => regex.test(k));
118
120
  }
119
- public async flushAll() { this.store.clear(); this.expires.clear(); }
121
+ public async flushAll() {
122
+ this.store.clear();
123
+ this.expires.clear();
124
+ }
120
125
  public async incrBy(key: string, n: number) {
121
126
  const next = Number(this.store.get(key) ?? 0) + n;
122
127
  this.store.set(key, String(next));
123
128
  return next;
124
129
  }
125
- public async decrBy(key: string, n: number) { return this.incrBy(key, -n); }
130
+ public async decrBy(key: string, n: number) {
131
+ return this.incrBy(key, -n);
132
+ }
126
133
  }
127
134
 
128
135
  const fakeClient = new FakeRedisClient();
@@ -146,7 +153,7 @@ The in-package vitest config runs `silent: true`, but if you're outside the pack
146
153
  ```ts
147
154
  cache.setCacheConfigurations({
148
155
  default: "memory",
149
- logging: false, // <-- here
156
+ logging: false, // <-- here
150
157
  drivers: { memory: MemoryCacheDriver },
151
158
  options: { memory: {} },
152
159
  });
@@ -165,7 +172,7 @@ cache.on("hit", hits);
165
172
  await service.getProfile("1");
166
173
  await service.getProfile("1");
167
174
 
168
- expect(hits).toHaveBeenCalledTimes(1); // second call was a hit
175
+ expect(hits).toHaveBeenCalledTimes(1); // second call was a hit
169
176
  ```
170
177
 
171
178
  Listeners registered via `cache.on(...)` automatically attach to any driver loaded later, so order of `on()` vs `init()` doesn't matter.
@@ -178,9 +185,7 @@ The chain-serialization guarantee is worth testing when your code fans out concu
178
185
  await cache.set("counter", 0);
179
186
 
180
187
  await Promise.all(
181
- Array.from({ length: 10 }, () =>
182
- cache.update<number>("counter", (c) => (c ?? 0) + 1),
183
- ),
188
+ Array.from({ length: 10 }, () => cache.update<number>("counter", (c) => (c ?? 0) + 1)),
184
189
  );
185
190
 
186
191
  await expect(cache.get("counter")).resolves.toBe(10);
@@ -205,7 +210,7 @@ beforeEach(async () => {
205
210
 
206
211
  it("returns the most similar doc above threshold", async () => {
207
212
  await cache.set("a", { text: "alpha" }, { vector: [1, 0, 0] });
208
- await cache.set("b", { text: "beta" }, { vector: [0, 1, 0] });
213
+ await cache.set("b", { text: "beta" }, { vector: [0, 1, 0] });
209
214
 
210
215
  const hits = await cache.similar([1, 0, 0], { topK: 1, threshold: 0.5 });
211
216
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: use-cache-atomic
3
- description: 'Atomic counters via cache.increment(key, by=1) / cache.decrement(key, by=1) — returns the new number, throws on non-numeric values. Triggers: `cache.increment`, `cache.decrement`, "view counter", "page views", "atomic counter", "decrement stock", "rate-limit counter", "INCRBY"; typical import `import { cache } from "@warlock.js/cache"`. Skip: read-modify-write of objects — the `use-cache-update-merge` topic; named-lock coordination — the `use-cache-lock` topic; competing libs `ioredis` `INCR`, native counters in a `Map`.'
3
+ description: 'Atomic counters via cache.increment(key, by=1, { ttl }?) / cache.decrement(key, by=1) — returns the new number, throws on non-numeric values. Triggers: `cache.increment`, `cache.decrement`, "view counter", "page views", "atomic counter", "decrement stock", "rate-limit counter", "INCRBY"; typical import `import { cache } from "@warlock.js/cache"`. Skip: read-modify-write of objects — the `use-cache-update-merge` topic; named-lock coordination — the `use-cache-lock` topic; competing libs `ioredis` `INCR`, native counters in a `Map`.'
4
4
  ---
5
5
 
6
6
  # Atomic counters — `cache.increment` / `cache.decrement`
@@ -11,9 +11,9 @@ code. Both return the **new** value after the operation.
11
11
  ```ts
12
12
  import { cache } from "@warlock.js/cache";
13
13
 
14
- const views = await cache.increment(`post.${id}.views`); // +1 → 1, 2, 3…
15
- const bulk = await cache.increment(`post.${id}.views`, 10); // +10
16
- const left = await cache.decrement(`stock.${sku}`, 3); // -3
14
+ const views = await cache.increment(`post.${id}.views`); // +1 → 1, 2, 3…
15
+ const bulk = await cache.increment(`post.${id}.views`, 10); // +10
16
+ const left = await cache.decrement(`stock.${sku}`, 3); // -3
17
17
  ```
18
18
 
19
19
  - A missing key is treated as `0`, so the first `increment` returns `by` (default `1`).
@@ -25,18 +25,24 @@ const left = await cache.decrement(`stock.${sku}`, 3); // -3
25
25
 
26
26
  `increment`, `decrement`, `pull` and `update` are atomic, but only some drivers are atomic **across servers**:
27
27
 
28
- | Driver | `increment` / `decrement` | `pull` | `update` / `merge` |
29
- |---|---|---|---|
30
- | `redis` | cross-server (`INCRBY`) | cross-server (`GETDEL`) | cross-server (Lua compare-and-set, retries) |
31
- | `pg` | cross-server (one upsert) | cross-server (`DELETE … RETURNING`) | cross-server (compare-and-set, retries) |
32
- | `memory` / `memoryExtended` / `lru` / `mock` | one process | one process | one process |
33
- | `file` | one process | one process | not supported (throws) |
28
+ | Driver | `increment` / `decrement` | `pull` | `update` / `merge` |
29
+ | -------------------------------------------- | ------------------------- | ----------------------------------- | ------------------------------------------- |
30
+ | `redis` | cross-server (`INCRBY`) | cross-server (`GETDEL`) | cross-server (Lua compare-and-set, retries) |
31
+ | `pg` | cross-server (one upsert) | cross-server (`DELETE … RETURNING`) | cross-server (compare-and-set, retries) |
32
+ | `memory` / `memoryExtended` / `lru` / `mock` | one process | one process | one process |
33
+ | `file` | one process | one process | not supported (throws) |
34
34
 
35
35
  Use `redis` or `pg` for anything several servers touch (rate limits, login throttles, one-time tokens); memory is per-process. On redis/pg the `update` callback may run more than once under contention: keep it side-effect free.
36
36
 
37
37
  ## TTL is kept
38
38
 
39
- Every driver keeps the key's **remaining TTL** across `increment`/`decrement`, so a fixed-window counter can't turn permanent. Set the TTL when you create the counter (`cache.set(key, 0, "1m")`, then `increment`). Use `cache.update` (see the `use-cache-update-merge` topic) for objects.
39
+ Pass the TTL on **every increment** of an expiring counter:
40
+
41
+ ```ts
42
+ await cache.increment("login.failures", 1, { ttl: "1m" });
43
+ ```
44
+
45
+ A missing or expired key starts at the amount with that TTL (seconds or duration string); an existing deadline is preserved. An explicit TTL also adds expiry to a live permanent counter, repairing counters left by an earlier seed/increment race. Without `options.ttl`, a fresh key uses the driver's default TTL. Redis applies the count and expiry in one Lua call; Postgres uses one upsert. This closes the race where a separate `set(key, 0, { ttl, onConflict: "create" })` seed expires just before the increment recreates the key. Tagged calls forward the option; namespaces use their default TTL unless overridden per call. Decrements keep existing TTLs too. Use `cache.update` (see the `use-cache-update-merge` topic) for objects.
40
46
 
41
47
  ## Common shapes
42
48
 
@@ -30,11 +30,14 @@ const missingIds = ids.filter((_, index) => users[index] === null);
30
30
  ## Write many — `setMany(items, ttl?)`
31
31
 
32
32
  ```ts
33
- await cache.setMany({
34
- "user.1": alice,
35
- "user.2": bob,
36
- "user.3": carol,
37
- }, 3600); // optional TTL (seconds) applied to every entry
33
+ await cache.setMany(
34
+ {
35
+ "user.1": alice,
36
+ "user.2": bob,
37
+ "user.3": carol,
38
+ },
39
+ 3600,
40
+ ); // optional TTL (seconds) applied to every entry
38
41
  ```
39
42
 
40
43
  - Keys are the object keys; values are the object values.
@@ -12,14 +12,14 @@ Dedicated accessor for ordered collections — queues, recent-N buffers, sliding
12
12
  ```ts
13
13
  const recent = cache.list<Event>("recent-events");
14
14
 
15
- await recent.push(event); // append to tail — returns new length
16
- await recent.unshift(priorityEvent); // prepend to head — returns new length
17
- const tail = await recent.pop(); // remove + return tail
18
- const head = await recent.shift(); // remove + return head
19
- const first10 = await recent.slice(0, 10); // view — does not mutate
15
+ await recent.push(event); // append to tail — returns new length
16
+ await recent.unshift(priorityEvent); // prepend to head — returns new length
17
+ const tail = await recent.pop(); // remove + return tail
18
+ const head = await recent.shift(); // remove + return head
19
+ const first10 = await recent.slice(0, 10); // view — does not mutate
20
20
  const all = await recent.all();
21
21
  const count = await recent.length();
22
- await recent.trim(0, 99); // keep only indices 0..99 inclusive
22
+ await recent.trim(0, 99); // keep only indices 0..99 inclusive
23
23
  await recent.clear();
24
24
  ```
25
25
 
@@ -31,8 +31,8 @@ The generic flows through every method. Pass the element type at the accessor ca
31
31
  type Event = { type: string; at: number };
32
32
  const queue = cache.list<Event>("jobs:queue");
33
33
 
34
- await queue.push({ type: "import", at: Date.now() }); // ✓
35
- await queue.push("not an event" as never); // ✗ at the caller
34
+ await queue.push({ type: "import", at: Date.now() }); // ✓
35
+ await queue.push("not an event" as never); // ✗ at the caller
36
36
  ```
37
37
 
38
38
  ## Current performance characteristics
@@ -54,7 +54,7 @@ When a list becomes empty (e.g. after successive `pop()` / `shift()` / `trim(0,
54
54
  ```ts
55
55
  await recent.push("a");
56
56
  await recent.pop();
57
- await cache.get("recent-events"); // null, not []
57
+ await cache.get("recent-events"); // null, not []
58
58
  ```
59
59
 
60
60
  ## Typical recipes
@@ -62,18 +62,18 @@ await cache.get("recent-events"); // null, not []
62
62
  ```ts
63
63
  // Recent-N audit log
64
64
  const audit = cache.list<AuditEntry>("audit:recent");
65
- await audit.unshift(entry); // newest at head
66
- await audit.trim(0, 999); // keep most-recent 1000
65
+ await audit.unshift(entry); // newest at head
66
+ await audit.trim(0, 999); // keep most-recent 1000
67
67
 
68
68
  // Lightweight job queue (single-node)
69
69
  const queue = cache.list<Job>("jobs:pending");
70
70
  await queue.push(job);
71
- const next = await queue.shift(); // FIFO
71
+ const next = await queue.shift(); // FIFO
72
72
 
73
73
  // Stack
74
74
  const stack = cache.list<Frame>("stack");
75
75
  await stack.push(frame);
76
- const top = await stack.pop(); // LIFO
76
+ const top = await stack.pop(); // LIFO
77
77
  ```
78
78
 
79
79
  ## What lists are NOT for