@warlock.js/cache 5.30.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.
- package/CHANGELOG.md +20 -8
- package/README.md +12 -21
- package/cjs/index.cjs +46 -27
- package/cjs/index.cjs.map +1 -1
- package/esm/cache-manager.d.mts +3 -1
- package/esm/cache-manager.d.mts.map +1 -1
- package/esm/cache-manager.mjs +2 -2
- package/esm/cache-manager.mjs.map +1 -1
- package/esm/drivers/base-cache-driver.d.mts +3 -1
- package/esm/drivers/base-cache-driver.d.mts.map +1 -1
- package/esm/drivers/base-cache-driver.mjs +4 -3
- package/esm/drivers/base-cache-driver.mjs.map +1 -1
- package/esm/drivers/file-cache-driver.d.mts.map +1 -1
- package/esm/drivers/file-cache-driver.mjs.map +1 -1
- package/esm/drivers/lru-memory-cache-driver.d.mts +3 -1
- package/esm/drivers/lru-memory-cache-driver.d.mts.map +1 -1
- package/esm/drivers/lru-memory-cache-driver.mjs +6 -4
- package/esm/drivers/lru-memory-cache-driver.mjs.map +1 -1
- package/esm/drivers/memory-cache-driver.d.mts +3 -1
- package/esm/drivers/memory-cache-driver.d.mts.map +1 -1
- package/esm/drivers/memory-cache-driver.mjs +9 -3
- package/esm/drivers/memory-cache-driver.mjs.map +1 -1
- package/esm/drivers/mock-cache-driver.d.mts +3 -1
- package/esm/drivers/mock-cache-driver.d.mts.map +1 -1
- package/esm/drivers/mock-cache-driver.mjs +11 -4
- package/esm/drivers/mock-cache-driver.mjs.map +1 -1
- package/esm/drivers/pg-cache-driver.d.mts +3 -1
- package/esm/drivers/pg-cache-driver.d.mts.map +1 -1
- package/esm/drivers/pg-cache-driver.mjs +6 -3
- package/esm/drivers/pg-cache-driver.mjs.map +1 -1
- package/esm/drivers/redis-cache-driver.d.mts +4 -2
- package/esm/drivers/redis-cache-driver.d.mts.map +1 -1
- package/esm/drivers/redis-cache-driver.mjs +4 -3
- package/esm/drivers/redis-cache-driver.mjs.map +1 -1
- package/esm/list/memory-cache-list.mjs.map +1 -1
- package/esm/metrics.d.mts.map +1 -1
- package/esm/metrics.mjs.map +1 -1
- package/esm/scoped-cache.d.mts +3 -1
- package/esm/scoped-cache.d.mts.map +1 -1
- package/esm/scoped-cache.mjs +2 -2
- package/esm/scoped-cache.mjs.map +1 -1
- package/esm/tagged-cache.d.mts +3 -1
- package/esm/tagged-cache.d.mts.map +1 -1
- package/esm/tagged-cache.mjs +2 -2
- package/esm/tagged-cache.mjs.map +1 -1
- package/esm/tagged-scoped-cache.d.mts +3 -1
- package/esm/tagged-scoped-cache.d.mts.map +1 -1
- package/esm/tagged-scoped-cache.mjs +2 -2
- package/esm/tagged-scoped-cache.mjs.map +1 -1
- package/esm/types.d.mts +16 -5
- package/esm/types.d.mts.map +1 -1
- package/esm/types.mjs.map +1 -1
- package/esm/utils.d.mts.map +1 -1
- package/esm/utils.mjs.map +1 -1
- package/llms-full.txt +229 -224
- package/llms.txt +1 -1
- package/package.json +3 -3
- package/skills/apply-cache-patterns/SKILL.md +3 -2
- package/skills/cache-basics/SKILL.md +24 -24
- package/skills/configure-pg-cache/SKILL.md +2 -2
- package/skills/configure-set-options/SKILL.md +14 -14
- package/skills/handle-cache-errors/SKILL.md +8 -8
- package/skills/index.md +4 -1
- package/skills/observe-cache/SKILL.md +8 -8
- package/skills/pick-cache-driver/SKILL.md +24 -23
- package/skills/test-cache-code/SKILL.md +15 -10
- package/skills/use-cache-atomic/SKILL.md +17 -11
- package/skills/use-cache-bulk/SKILL.md +8 -5
- package/skills/use-cache-list/SKILL.md +13 -13
- package/skills/use-cache-lock/SKILL.md +11 -17
- package/skills/use-cache-namespace/SKILL.md +11 -13
- package/skills/use-cache-similarity/SKILL.md +9 -9
- package/skills/use-cache-tags/SKILL.md +13 -13
- package/skills/use-cache-update-merge/SKILL.md +10 -10
- package/skills/use-cache-utils/SKILL.md +17 -17
- package/skills/use-cached-hof/SKILL.md +8 -7
- 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.
|
|
8
|
+
"@warlock.js/fs": "5.31.0",
|
|
9
9
|
"@mongez/reinforcements": "^4.0.2",
|
|
10
|
-
"@warlock.js/logger": "5.
|
|
10
|
+
"@warlock.js/logger": "5.31.0",
|
|
11
11
|
"ms": "^2.1.3"
|
|
12
12
|
},
|
|
13
13
|
"peerDependencies": {
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
],
|
|
48
48
|
"author": "hassanzohdy",
|
|
49
49
|
"license": "MIT",
|
|
50
|
-
"version": "5.
|
|
50
|
+
"version": "5.31.0",
|
|
51
51
|
"main": "./cjs/index.cjs",
|
|
52
52
|
"module": "./esm/index.mjs",
|
|
53
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);
|
|
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("");
|
|
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
|
```
|
|
@@ -68,15 +68,15 @@ await cache.init();
|
|
|
68
68
|
```ts
|
|
69
69
|
// Set + get
|
|
70
70
|
await cache.set("user.1", user, "1h");
|
|
71
|
-
const cached = await cache.get<User>("user.1");
|
|
71
|
+
const cached = await cache.get<User>("user.1"); // User | null
|
|
72
72
|
|
|
73
73
|
// Presence + read-and-delete
|
|
74
|
-
const exists = await cache.has("user.1");
|
|
75
|
-
const taken = await cache.pull<User>("user.1");
|
|
74
|
+
const exists = await cache.has("user.1"); // boolean
|
|
75
|
+
const taken = await cache.pull<User>("user.1"); // returns then removes
|
|
76
76
|
|
|
77
77
|
// Remove + flush
|
|
78
78
|
await cache.remove("user.1");
|
|
79
|
-
await cache.flush();
|
|
79
|
+
await cache.flush(); // wipe everything (current driver)
|
|
80
80
|
|
|
81
81
|
// Many at once — array positionally aligned with the keys (null for misses)
|
|
82
82
|
const [u1, u2, u3] = await cache.many(["user.1", "user.2", "user.3"]);
|
|
@@ -86,9 +86,9 @@ const [u1, u2, u3] = await cache.many(["user.1", "user.2", "user.3"]);
|
|
|
86
86
|
await cache.forever("config.version", "1.2.3");
|
|
87
87
|
|
|
88
88
|
// Counters
|
|
89
|
-
await cache.increment("post.42.views");
|
|
90
|
-
await cache.increment("post.42.views", 10);
|
|
91
|
-
await cache.decrement("inventory.sku-x");
|
|
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
|
|
92
92
|
|
|
93
93
|
// Memoize an expensive function
|
|
94
94
|
const user = await cache.remember("user.1", "1h", async () => db.users.find(1));
|
|
@@ -96,23 +96,23 @@ const user = await cache.remember("user.1", "1h", async () => db.users.find(1));
|
|
|
96
96
|
|
|
97
97
|
## Pick a skill
|
|
98
98
|
|
|
99
|
-
| If the task is about…
|
|
100
|
-
|
|
|
101
|
-
| Choosing a driver, configuring it, or understanding what each one does best
|
|
102
|
-
| The `set` options object (`ttl`, `expiresAt`, `tags`, `onConflict`, `driver`, `vector`)
|
|
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?)`
|
|
105
|
-
| Tag-based invalidation — `cache.tags([...]).invalidate()`
|
|
106
|
-
| Stale-while-revalidate — `cache.swr(...)`
|
|
107
|
-
| Wrapping a function with `cached()` — HOF memoization with `.invalidate()`
|
|
108
|
-
| Distributed locks — `cache.lock(key, ttl, fn)` with auto-release
|
|
109
|
-
| Queues, recent-N buffers, `push`/`shift`/`trim` — the list sub-API
|
|
110
|
-
| Atomic read-modify-write via `update()` and `merge()`
|
|
111
|
-
| Similarity retrieval — `set({ vector })` + `cache.similar(...)`
|
|
112
|
-
| Postgres driver setup (KV-only or with pgvector)
|
|
113
|
-
| `cache.metrics()` aggregate snapshot + event bus for per-event reactions
|
|
114
|
-
| Error classes (`CacheConfigurationError`, `CacheUnsupportedError`, etc.)
|
|
115
|
-
| Tests that touch cache code paths — `MockCacheDriver`, `MemoryCacheDriver`
|
|
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 |
|
|
116
116
|
|
|
117
117
|
## Things NOT to do
|
|
118
118
|
|
|
@@ -39,8 +39,8 @@ cache.setCacheConfigurations({
|
|
|
39
39
|
options: {
|
|
40
40
|
pg: {
|
|
41
41
|
client: pool,
|
|
42
|
-
table: "warlock_cache",
|
|
43
|
-
ttl: "1h",
|
|
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");
|
|
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
|
|
32
|
-
|
|
|
33
|
-
| `ttl`
|
|
34
|
-
| `expiresAt`
|
|
35
|
-
| `tags`
|
|
36
|
-
| `onConflict` | `"create" \| "update" \| "upsert"` | See below. Default `"upsert"`.
|
|
37
|
-
| `driver`
|
|
38
|
-
| `vector`
|
|
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 });
|
|
75
|
-
await cache.set("k", v, { expiresAt: Date.now() - 1000 });
|
|
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();
|
|
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);
|
|
100
|
-
await cache.set("k", v, 3600);
|
|
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
|
|
22
|
-
|
|
|
23
|
-
| `CacheError`
|
|
24
|
-
| `CacheConfigurationError`
|
|
25
|
-
| `CacheConnectionError`
|
|
26
|
-
| `CacheDriverNotInitializedError`
|
|
27
|
-
| `CacheUnsupportedError`
|
|
28
|
-
| `CacheConcurrencyError`
|
|
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:
|
|
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
|
|
54
|
-
|
|
55
|
-
| Show hit rate / latency in a dashboard
|
|
56
|
-
| Page on cache errors
|
|
57
|
-
| Periodic export to Prometheus / StatsD
|
|
58
|
-
| Audit log of every removal
|
|
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)
|
|
61
|
-
| Debug "is the cache being hit at all?" in dev
|
|
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
|
|
11
|
-
|
|
|
12
|
-
| `null`
|
|
13
|
-
| `memory`
|
|
14
|
-
| `memoryExtended` | Single process | No
|
|
15
|
-
| `lru`
|
|
16
|
-
| `file`
|
|
17
|
-
| `redis`
|
|
18
|
-
| `pg`
|
|
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
|
|
23
|
-
|
|
|
24
|
-
| `set` / `get` / `remove` / `flush`
|
|
25
|
-
| TTL (number or string; numeric strings = seconds) |
|
|
26
|
-
| Sliding TTL on read
|
|
27
|
-
| `removeNamespace`
|
|
28
|
-
| `onConflict: "create"` / `"update"`
|
|
29
|
-
| `increment` / `decrement` / `pull`
|
|
30
|
-
| `update()` / `merge()`
|
|
31
|
-
| List sub-API
|
|
32
|
-
| Tagged invalidation
|
|
33
|
-
| `similar()` / `set({ vector })`
|
|
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" } },
|
|
105
|
+
options: { pg: { table: "cache" } }, // static
|
|
106
106
|
});
|
|
107
107
|
|
|
108
|
-
await cache.use("pg", { client: pool });
|
|
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
|
|
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() {
|
|
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() {
|
|
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) {
|
|
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,
|
|
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);
|
|
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" },
|
|
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`);
|
|
15
|
-
const bulk = await cache.increment(`post.${id}.views`, 10);
|
|
16
|
-
const left = await cache.decrement(`stock.${sku}`, 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
|
|
29
|
-
|
|
30
|
-
| `redis`
|
|
31
|
-
| `pg`
|
|
32
|
-
| `memory` / `memoryExtended` / `lru` / `mock` | one process
|
|
33
|
-
| `file`
|
|
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
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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);
|
|
16
|
-
await recent.unshift(priorityEvent);
|
|
17
|
-
const tail = await recent.pop();
|
|
18
|
-
const head = await recent.shift();
|
|
19
|
-
const first10 = await recent.slice(0, 10);
|
|
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);
|
|
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);
|
|
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");
|
|
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);
|
|
66
|
-
await audit.trim(0, 999);
|
|
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();
|
|
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();
|
|
76
|
+
const top = await stack.pop(); // LIFO
|
|
77
77
|
```
|
|
78
78
|
|
|
79
79
|
## What lists are NOT for
|