@warlock.js/cache 5.23.2 → 5.25.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 +12 -0
- package/README.md +616 -616
- package/cjs/index.cjs +21 -1
- package/cjs/index.cjs.map +1 -1
- package/esm/drivers/base-cache-driver.d.mts +12 -0
- package/esm/drivers/base-cache-driver.d.mts.map +1 -1
- package/esm/drivers/base-cache-driver.mjs +10 -0
- package/esm/drivers/base-cache-driver.mjs.map +1 -1
- package/esm/drivers/null-cache-driver.mjs.map +1 -1
- package/esm/tagged-cache.d.mts.map +1 -1
- package/esm/tagged-cache.mjs +11 -1
- package/esm/tagged-cache.mjs.map +1 -1
- package/esm/types.d.mts +5 -2
- package/esm/types.d.mts.map +1 -1
- package/esm/types.mjs.map +1 -1
- package/esm/utils.mjs.map +1 -1
- package/llms-full.txt +1632 -1631
- package/llms.txt +1 -1
- package/package.json +3 -3
- package/skills/apply-cache-patterns/SKILL.md +95 -95
- package/skills/configure-set-options/SKILL.md +96 -96
- package/skills/handle-cache-errors/SKILL.md +92 -92
- package/skills/observe-cache/SKILL.md +104 -103
- package/skills/pick-cache-driver/SKILL.md +121 -121
- package/skills/test-cache-code/SKILL.md +219 -219
- package/skills/use-cache-atomic/SKILL.md +42 -42
- package/skills/use-cache-bulk/SKILL.md +57 -57
- package/skills/use-cache-list/SKILL.md +85 -85
- package/skills/use-cache-lock/SKILL.md +101 -101
- package/skills/use-cache-namespace/SKILL.md +88 -88
- package/skills/use-cache-similarity/SKILL.md +94 -94
- package/skills/use-cache-update-merge/SKILL.md +84 -84
- package/skills/use-cache-utils/SKILL.md +89 -89
- package/skills/use-cached-hof/SKILL.md +93 -93
- package/skills/use-swr/SKILL.md +104 -104
|
@@ -1,219 +1,219 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: test-cache-code
|
|
3
|
-
description: '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 — `@warlock.js/cache/pick-cache-driver/SKILL.md`; competing libs `jest-mock`, `sinon`, `redis-mock`; native `vi.fn`.'
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Testing code that touches cache
|
|
7
|
-
|
|
8
|
-
Three good strategies — pick based on what you're testing.
|
|
9
|
-
|
|
10
|
-
## Strategy 1 — `MockCacheDriver` for behavioral assertions (preferred)
|
|
11
|
-
|
|
12
|
-
When you want to assert "did my service actually invalidate the cache after the update?" or "was `set` called with the right TTL?", reach for `MockCacheDriver`. It implements the full driver contract on a `Map` and adds three introspection helpers:
|
|
13
|
-
|
|
14
|
-
- `wasCalled(operation, key?)` — was a given op invoked? Optional key matched post-`parseKey`.
|
|
15
|
-
- `getStored(key)` — raw stored value, bypassing TTL handling and clone protection.
|
|
16
|
-
- `reset()` — wipe storage, tag index, and call log in one call.
|
|
17
|
-
- `callLog: CacheCall[]` — ordered record of every op (operation, parsed key, raw args, timestamp).
|
|
18
|
-
|
|
19
|
-
```ts
|
|
20
|
-
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
21
|
-
import { cache, MockCacheDriver } from "@warlock.js/cache";
|
|
22
|
-
|
|
23
|
-
describe("UserService.update", () => {
|
|
24
|
-
let driver: MockCacheDriver;
|
|
25
|
-
|
|
26
|
-
beforeEach(async () => {
|
|
27
|
-
cache.setCacheConfigurations({
|
|
28
|
-
default: "mock",
|
|
29
|
-
logging: false,
|
|
30
|
-
drivers: { mock: MockCacheDriver },
|
|
31
|
-
options: { mock: {} },
|
|
32
|
-
});
|
|
33
|
-
await cache.init();
|
|
34
|
-
|
|
35
|
-
driver = cache.currentDriver as MockCacheDriver;
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
afterEach(async () => {
|
|
39
|
-
driver.reset();
|
|
40
|
-
await cache.disconnect();
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("invalidates the user cache after update", async () => {
|
|
44
|
-
await new UserService().update(42, { name: "Jane" });
|
|
45
|
-
|
|
46
|
-
expect(driver.wasCalled("remove", "users.42")).toBe(true);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("caches with the right TTL on read-through", async () => {
|
|
50
|
-
await new UserService().getProfile(1);
|
|
51
|
-
|
|
52
|
-
const setCall = driver.callLog.find((call) => call.operation === "set");
|
|
53
|
-
expect(setCall?.args[1]).toBe("1h");
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
`wasCalled` normalizes object keys, so `wasCalled("set", { id: 1 })` and `wasCalled("set", "id.1")` match the same call.
|
|
59
|
-
|
|
60
|
-
## Strategy 2 — `MemoryCacheDriver` for full-stack integration tests
|
|
61
|
-
|
|
62
|
-
When you want the real read/write semantics (eviction, TTL expiry, similarity scoring) without the introspection ceremony, `MemoryCacheDriver` is the right pick. Same setup pattern as the mock — swap `MockCacheDriver` for `MemoryCacheDriver`.
|
|
63
|
-
|
|
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
|
-
|
|
66
|
-
## Strategy 3 — `NullCacheDriver` when you need cache *off*
|
|
67
|
-
|
|
68
|
-
Use `NullCacheDriver` to disable caching entirely for code paths that should still work without a cache (graceful-degradation tests):
|
|
69
|
-
|
|
70
|
-
```ts
|
|
71
|
-
cache.setCacheConfigurations({
|
|
72
|
-
default: "null",
|
|
73
|
-
drivers: { null: NullCacheDriver },
|
|
74
|
-
options: { null: {} },
|
|
75
|
-
});
|
|
76
|
-
await cache.init();
|
|
77
|
-
|
|
78
|
-
// All cache ops no-op; get() always returns null; set() silently discards.
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
## Mocking Redis (for driver-level tests, not app code)
|
|
82
|
-
|
|
83
|
-
For tests that specifically exercise `RedisCacheDriver`, use `vi.mock("redis")` with an in-memory fake. Example (condensed from `redis-cache-driver.spec.ts`):
|
|
84
|
-
|
|
85
|
-
```ts
|
|
86
|
-
import { vi } from "vitest";
|
|
87
|
-
|
|
88
|
-
class FakeRedisClient {
|
|
89
|
-
public store = new Map<string, string>();
|
|
90
|
-
private expires = new Map<string, number>();
|
|
91
|
-
public on() { return this; }
|
|
92
|
-
public async connect() {}
|
|
93
|
-
public async quit() {}
|
|
94
|
-
public async set(key: string, value: string, opts?: { EX?: number; NX?: boolean; XX?: boolean }) {
|
|
95
|
-
if (opts?.NX && this.store.has(key)) return null;
|
|
96
|
-
if (opts?.XX && !this.store.has(key)) return null;
|
|
97
|
-
this.store.set(key, value);
|
|
98
|
-
if (opts?.EX) this.expires.set(key, Date.now() + opts.EX * 1000);
|
|
99
|
-
return "OK";
|
|
100
|
-
}
|
|
101
|
-
public async get(key: string) {
|
|
102
|
-
const ttl = this.expires.get(key);
|
|
103
|
-
if (ttl && ttl < Date.now()) {
|
|
104
|
-
this.store.delete(key);
|
|
105
|
-
return null;
|
|
106
|
-
}
|
|
107
|
-
return this.store.get(key) ?? null;
|
|
108
|
-
}
|
|
109
|
-
public async del(keys: string | string[]) {
|
|
110
|
-
const arr = Array.isArray(keys) ? keys : [keys];
|
|
111
|
-
let count = 0;
|
|
112
|
-
for (const k of arr) if (this.store.delete(k)) count++;
|
|
113
|
-
return count;
|
|
114
|
-
}
|
|
115
|
-
public async keys(pattern: string) {
|
|
116
|
-
const regex = new RegExp("^" + pattern.replace(/\*/g, ".*") + "$");
|
|
117
|
-
return [...this.store.keys()].filter((k) => regex.test(k));
|
|
118
|
-
}
|
|
119
|
-
public async flushAll() { this.store.clear(); this.expires.clear(); }
|
|
120
|
-
public async incrBy(key: string, n: number) {
|
|
121
|
-
const next = Number(this.store.get(key) ?? 0) + n;
|
|
122
|
-
this.store.set(key, String(next));
|
|
123
|
-
return next;
|
|
124
|
-
}
|
|
125
|
-
public async decrBy(key: string, n: number) { return this.incrBy(key, -n); }
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const fakeClient = new FakeRedisClient();
|
|
129
|
-
vi.mock("redis", () => ({ createClient: vi.fn(() => fakeClient) }));
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
**One-time gotcha:** `RedisCacheDriver` kicks off an async `loadRedis()` at module import that resolves its internal "redis is available" flag. Before running any test that calls `connect()`, wait a short tick after the first dynamic import so the flag flips. The in-tree spec uses:
|
|
133
|
-
|
|
134
|
-
```ts
|
|
135
|
-
async function importDriver() {
|
|
136
|
-
const mod = await import("./redis-cache-driver");
|
|
137
|
-
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
138
|
-
return mod.RedisCacheDriver;
|
|
139
|
-
}
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
## Silencing cache logs in tests
|
|
143
|
-
|
|
144
|
-
The in-package vitest config runs `silent: true`, but if you're outside the package, explicitly disable logging:
|
|
145
|
-
|
|
146
|
-
```ts
|
|
147
|
-
cache.setCacheConfigurations({
|
|
148
|
-
default: "memory",
|
|
149
|
-
logging: false, // <-- here
|
|
150
|
-
drivers: { memory: MemoryCacheDriver },
|
|
151
|
-
options: { memory: {} },
|
|
152
|
-
});
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
Or per-driver: `driver.setLoggingState(false)`.
|
|
156
|
-
|
|
157
|
-
## Spying on events
|
|
158
|
-
|
|
159
|
-
Every driver emits `hit`, `miss`, `set`, `removed`, `flushed`, `expired`. Attach listeners to assert cache behavior without inspecting internal state:
|
|
160
|
-
|
|
161
|
-
```ts
|
|
162
|
-
const hits = vi.fn();
|
|
163
|
-
cache.on("hit", hits);
|
|
164
|
-
|
|
165
|
-
await service.getProfile("1");
|
|
166
|
-
await service.getProfile("1");
|
|
167
|
-
|
|
168
|
-
expect(hits).toHaveBeenCalledTimes(1); // second call was a hit
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
Listeners registered via `cache.on(...)` automatically attach to any driver loaded later, so order of `on()` vs `init()` doesn't matter.
|
|
172
|
-
|
|
173
|
-
## Tests for `update` / `merge` concurrency
|
|
174
|
-
|
|
175
|
-
The chain-serialization guarantee is worth testing when your code fans out concurrent updates:
|
|
176
|
-
|
|
177
|
-
```ts
|
|
178
|
-
await cache.set("counter", 0);
|
|
179
|
-
|
|
180
|
-
await Promise.all(
|
|
181
|
-
Array.from({ length: 10 }, () =>
|
|
182
|
-
cache.update<number>("counter", (c) => (c ?? 0) + 1),
|
|
183
|
-
),
|
|
184
|
-
);
|
|
185
|
-
|
|
186
|
-
await expect(cache.get("counter")).resolves.toBe(10);
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
Runs in-process only — for cross-process safety see [`@warlock.js/cache/use-cache-lock/SKILL.md`](@warlock.js/cache/use-cache-lock/SKILL.md).
|
|
190
|
-
|
|
191
|
-
## Testing similarity code paths
|
|
192
|
-
|
|
193
|
-
`MemoryCacheDriver` runs `similar()` brute-force in-process — perfect for tests of code that calls `cache.similar(...)`. Use stable, hand-written vectors (don't call a real embedder in tests):
|
|
194
|
-
|
|
195
|
-
```ts
|
|
196
|
-
beforeEach(async () => {
|
|
197
|
-
cache.setCacheConfigurations({
|
|
198
|
-
default: "memory",
|
|
199
|
-
logging: false,
|
|
200
|
-
drivers: { memory: MemoryCacheDriver },
|
|
201
|
-
options: { memory: {} },
|
|
202
|
-
});
|
|
203
|
-
await cache.init();
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it("returns the most similar doc above threshold", async () => {
|
|
207
|
-
await cache.set("a", { text: "alpha" }, { vector: [1, 0, 0] });
|
|
208
|
-
await cache.set("b", { text: "beta" }, { vector: [0, 1, 0] });
|
|
209
|
-
|
|
210
|
-
const hits = await cache.similar([1, 0, 0], { topK: 1, threshold: 0.5 });
|
|
211
|
-
|
|
212
|
-
expect(hits).toHaveLength(1);
|
|
213
|
-
expect(hits[0].key).toBe("a");
|
|
214
|
-
});
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
## Testing the `pg` driver without a real Postgres
|
|
218
|
-
|
|
219
|
-
The `pg` driver accepts any object satisfying `PgClientLike` (`{ query(text, values) }`). For unit tests, hand-roll a minimal Map-backed fake — see `src/drivers/pg-cache-driver.spec.ts` for a 60-line `FakePool` that recognizes the exact SQL shapes the driver issues. For integration coverage, gate a real-PG suite on a `POSTGRES_URL` env var and skip cleanly when unset.
|
|
1
|
+
---
|
|
2
|
+
name: test-cache-code
|
|
3
|
+
description: '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 — `@warlock.js/cache/pick-cache-driver/SKILL.md`; competing libs `jest-mock`, `sinon`, `redis-mock`; native `vi.fn`.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Testing code that touches cache
|
|
7
|
+
|
|
8
|
+
Three good strategies — pick based on what you're testing.
|
|
9
|
+
|
|
10
|
+
## Strategy 1 — `MockCacheDriver` for behavioral assertions (preferred)
|
|
11
|
+
|
|
12
|
+
When you want to assert "did my service actually invalidate the cache after the update?" or "was `set` called with the right TTL?", reach for `MockCacheDriver`. It implements the full driver contract on a `Map` and adds three introspection helpers:
|
|
13
|
+
|
|
14
|
+
- `wasCalled(operation, key?)` — was a given op invoked? Optional key matched post-`parseKey`.
|
|
15
|
+
- `getStored(key)` — raw stored value, bypassing TTL handling and clone protection.
|
|
16
|
+
- `reset()` — wipe storage, tag index, and call log in one call.
|
|
17
|
+
- `callLog: CacheCall[]` — ordered record of every op (operation, parsed key, raw args, timestamp).
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
21
|
+
import { cache, MockCacheDriver } from "@warlock.js/cache";
|
|
22
|
+
|
|
23
|
+
describe("UserService.update", () => {
|
|
24
|
+
let driver: MockCacheDriver;
|
|
25
|
+
|
|
26
|
+
beforeEach(async () => {
|
|
27
|
+
cache.setCacheConfigurations({
|
|
28
|
+
default: "mock",
|
|
29
|
+
logging: false,
|
|
30
|
+
drivers: { mock: MockCacheDriver },
|
|
31
|
+
options: { mock: {} },
|
|
32
|
+
});
|
|
33
|
+
await cache.init();
|
|
34
|
+
|
|
35
|
+
driver = cache.currentDriver as MockCacheDriver;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
afterEach(async () => {
|
|
39
|
+
driver.reset();
|
|
40
|
+
await cache.disconnect();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("invalidates the user cache after update", async () => {
|
|
44
|
+
await new UserService().update(42, { name: "Jane" });
|
|
45
|
+
|
|
46
|
+
expect(driver.wasCalled("remove", "users.42")).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("caches with the right TTL on read-through", async () => {
|
|
50
|
+
await new UserService().getProfile(1);
|
|
51
|
+
|
|
52
|
+
const setCall = driver.callLog.find((call) => call.operation === "set");
|
|
53
|
+
expect(setCall?.args[1]).toBe("1h");
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`wasCalled` normalizes object keys, so `wasCalled("set", { id: 1 })` and `wasCalled("set", "id.1")` match the same call.
|
|
59
|
+
|
|
60
|
+
## Strategy 2 — `MemoryCacheDriver` for full-stack integration tests
|
|
61
|
+
|
|
62
|
+
When you want the real read/write semantics (eviction, TTL expiry, similarity scoring) without the introspection ceremony, `MemoryCacheDriver` is the right pick. Same setup pattern as the mock — swap `MockCacheDriver` for `MemoryCacheDriver`.
|
|
63
|
+
|
|
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
|
+
|
|
66
|
+
## Strategy 3 — `NullCacheDriver` when you need cache *off*
|
|
67
|
+
|
|
68
|
+
Use `NullCacheDriver` to disable caching entirely for code paths that should still work without a cache (graceful-degradation tests):
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
cache.setCacheConfigurations({
|
|
72
|
+
default: "null",
|
|
73
|
+
drivers: { null: NullCacheDriver },
|
|
74
|
+
options: { null: {} },
|
|
75
|
+
});
|
|
76
|
+
await cache.init();
|
|
77
|
+
|
|
78
|
+
// All cache ops no-op; get() always returns null; set() silently discards.
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Mocking Redis (for driver-level tests, not app code)
|
|
82
|
+
|
|
83
|
+
For tests that specifically exercise `RedisCacheDriver`, use `vi.mock("redis")` with an in-memory fake. Example (condensed from `redis-cache-driver.spec.ts`):
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import { vi } from "vitest";
|
|
87
|
+
|
|
88
|
+
class FakeRedisClient {
|
|
89
|
+
public store = new Map<string, string>();
|
|
90
|
+
private expires = new Map<string, number>();
|
|
91
|
+
public on() { return this; }
|
|
92
|
+
public async connect() {}
|
|
93
|
+
public async quit() {}
|
|
94
|
+
public async set(key: string, value: string, opts?: { EX?: number; NX?: boolean; XX?: boolean }) {
|
|
95
|
+
if (opts?.NX && this.store.has(key)) return null;
|
|
96
|
+
if (opts?.XX && !this.store.has(key)) return null;
|
|
97
|
+
this.store.set(key, value);
|
|
98
|
+
if (opts?.EX) this.expires.set(key, Date.now() + opts.EX * 1000);
|
|
99
|
+
return "OK";
|
|
100
|
+
}
|
|
101
|
+
public async get(key: string) {
|
|
102
|
+
const ttl = this.expires.get(key);
|
|
103
|
+
if (ttl && ttl < Date.now()) {
|
|
104
|
+
this.store.delete(key);
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
return this.store.get(key) ?? null;
|
|
108
|
+
}
|
|
109
|
+
public async del(keys: string | string[]) {
|
|
110
|
+
const arr = Array.isArray(keys) ? keys : [keys];
|
|
111
|
+
let count = 0;
|
|
112
|
+
for (const k of arr) if (this.store.delete(k)) count++;
|
|
113
|
+
return count;
|
|
114
|
+
}
|
|
115
|
+
public async keys(pattern: string) {
|
|
116
|
+
const regex = new RegExp("^" + pattern.replace(/\*/g, ".*") + "$");
|
|
117
|
+
return [...this.store.keys()].filter((k) => regex.test(k));
|
|
118
|
+
}
|
|
119
|
+
public async flushAll() { this.store.clear(); this.expires.clear(); }
|
|
120
|
+
public async incrBy(key: string, n: number) {
|
|
121
|
+
const next = Number(this.store.get(key) ?? 0) + n;
|
|
122
|
+
this.store.set(key, String(next));
|
|
123
|
+
return next;
|
|
124
|
+
}
|
|
125
|
+
public async decrBy(key: string, n: number) { return this.incrBy(key, -n); }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const fakeClient = new FakeRedisClient();
|
|
129
|
+
vi.mock("redis", () => ({ createClient: vi.fn(() => fakeClient) }));
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**One-time gotcha:** `RedisCacheDriver` kicks off an async `loadRedis()` at module import that resolves its internal "redis is available" flag. Before running any test that calls `connect()`, wait a short tick after the first dynamic import so the flag flips. The in-tree spec uses:
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
async function importDriver() {
|
|
136
|
+
const mod = await import("./redis-cache-driver");
|
|
137
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
138
|
+
return mod.RedisCacheDriver;
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Silencing cache logs in tests
|
|
143
|
+
|
|
144
|
+
The in-package vitest config runs `silent: true`, but if you're outside the package, explicitly disable logging:
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
cache.setCacheConfigurations({
|
|
148
|
+
default: "memory",
|
|
149
|
+
logging: false, // <-- here
|
|
150
|
+
drivers: { memory: MemoryCacheDriver },
|
|
151
|
+
options: { memory: {} },
|
|
152
|
+
});
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Or per-driver: `driver.setLoggingState(false)`.
|
|
156
|
+
|
|
157
|
+
## Spying on events
|
|
158
|
+
|
|
159
|
+
Every driver emits `hit`, `miss`, `set`, `removed`, `flushed`, `expired`. Attach listeners to assert cache behavior without inspecting internal state:
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
const hits = vi.fn();
|
|
163
|
+
cache.on("hit", hits);
|
|
164
|
+
|
|
165
|
+
await service.getProfile("1");
|
|
166
|
+
await service.getProfile("1");
|
|
167
|
+
|
|
168
|
+
expect(hits).toHaveBeenCalledTimes(1); // second call was a hit
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Listeners registered via `cache.on(...)` automatically attach to any driver loaded later, so order of `on()` vs `init()` doesn't matter.
|
|
172
|
+
|
|
173
|
+
## Tests for `update` / `merge` concurrency
|
|
174
|
+
|
|
175
|
+
The chain-serialization guarantee is worth testing when your code fans out concurrent updates:
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
await cache.set("counter", 0);
|
|
179
|
+
|
|
180
|
+
await Promise.all(
|
|
181
|
+
Array.from({ length: 10 }, () =>
|
|
182
|
+
cache.update<number>("counter", (c) => (c ?? 0) + 1),
|
|
183
|
+
),
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
await expect(cache.get("counter")).resolves.toBe(10);
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Runs in-process only — for cross-process safety see [`@warlock.js/cache/use-cache-lock/SKILL.md`](@warlock.js/cache/use-cache-lock/SKILL.md).
|
|
190
|
+
|
|
191
|
+
## Testing similarity code paths
|
|
192
|
+
|
|
193
|
+
`MemoryCacheDriver` runs `similar()` brute-force in-process — perfect for tests of code that calls `cache.similar(...)`. Use stable, hand-written vectors (don't call a real embedder in tests):
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
beforeEach(async () => {
|
|
197
|
+
cache.setCacheConfigurations({
|
|
198
|
+
default: "memory",
|
|
199
|
+
logging: false,
|
|
200
|
+
drivers: { memory: MemoryCacheDriver },
|
|
201
|
+
options: { memory: {} },
|
|
202
|
+
});
|
|
203
|
+
await cache.init();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("returns the most similar doc above threshold", async () => {
|
|
207
|
+
await cache.set("a", { text: "alpha" }, { vector: [1, 0, 0] });
|
|
208
|
+
await cache.set("b", { text: "beta" }, { vector: [0, 1, 0] });
|
|
209
|
+
|
|
210
|
+
const hits = await cache.similar([1, 0, 0], { topK: 1, threshold: 0.5 });
|
|
211
|
+
|
|
212
|
+
expect(hits).toHaveLength(1);
|
|
213
|
+
expect(hits[0].key).toBe("a");
|
|
214
|
+
});
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Testing the `pg` driver without a real Postgres
|
|
218
|
+
|
|
219
|
+
The `pg` driver accepts any object satisfying `PgClientLike` (`{ query(text, values) }`). For unit tests, hand-roll a minimal Map-backed fake — see `src/drivers/pg-cache-driver.spec.ts` for a 60-line `FakePool` that recognizes the exact SQL shapes the driver issues. For integration coverage, gate a real-PG suite on a `POSTGRES_URL` env var and skip cleanly when unset.
|
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
---
|
|
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 — `@warlock.js/cache/use-cache-update-merge/SKILL.md`; named-lock coordination — `@warlock.js/cache/use-cache-lock/SKILL.md`; competing libs `ioredis` `INCR`, native counters in a `Map`.'
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Atomic counters — `cache.increment` / `cache.decrement`
|
|
7
|
-
|
|
8
|
-
Numeric counters that go up and down without a read-then-write race in your own
|
|
9
|
-
code. Both return the **new** value after the operation.
|
|
10
|
-
|
|
11
|
-
```ts
|
|
12
|
-
import { cache } from "@warlock.js/cache";
|
|
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
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
- A missing key is treated as `0`, so the first `increment` returns `by` (default `1`).
|
|
20
|
-
- `decrement(key, n)` is exactly `increment(key, -n)`.
|
|
21
|
-
- The stored value must be numeric — incrementing a string/object throws:
|
|
22
|
-
`Error: Cannot increment non-numeric value for key: <key>`.
|
|
23
|
-
|
|
1
|
+
---
|
|
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 — `@warlock.js/cache/use-cache-update-merge/SKILL.md`; named-lock coordination — `@warlock.js/cache/use-cache-lock/SKILL.md`; competing libs `ioredis` `INCR`, native counters in a `Map`.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Atomic counters — `cache.increment` / `cache.decrement`
|
|
7
|
+
|
|
8
|
+
Numeric counters that go up and down without a read-then-write race in your own
|
|
9
|
+
code. Both return the **new** value after the operation.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { cache } from "@warlock.js/cache";
|
|
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
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
- A missing key is treated as `0`, so the first `increment` returns `by` (default `1`).
|
|
20
|
+
- `decrement(key, n)` is exactly `increment(key, -n)`.
|
|
21
|
+
- The stored value must be numeric — incrementing a string/object throws:
|
|
22
|
+
`Error: Cannot increment non-numeric value for key: <key>`.
|
|
23
|
+
|
|
24
24
|
## Atomicity is per-driver
|
|
25
25
|
|
|
26
26
|
`increment`, `decrement`, `pull` and `update` are atomic, but only some drivers are atomic **across servers**:
|
|
@@ -38,22 +38,22 @@ Use `redis` or `pg` for anything several servers touch (rate limits, login throt
|
|
|
38
38
|
|
|
39
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`](@warlock.js/cache/use-cache-update-merge/SKILL.md) for objects.
|
|
40
40
|
|
|
41
|
-
## Common shapes
|
|
42
|
-
|
|
43
|
-
```ts
|
|
44
|
-
// View counter
|
|
45
|
-
await cache.increment(`post.${id}.views`);
|
|
46
|
-
|
|
47
|
-
// Decrement stock, guard against oversell
|
|
48
|
-
const remaining = await cache.decrement(`stock.${sku}`, qty);
|
|
49
|
-
if (remaining < 0) {
|
|
50
|
-
await cache.increment(`stock.${sku}`, qty); // roll back
|
|
51
|
-
throw new Error("Out of stock");
|
|
52
|
-
}
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
## See also
|
|
56
|
-
|
|
57
|
-
- [`@warlock.js/cache/use-cache-update-merge/SKILL.md`](@warlock.js/cache/use-cache-update-merge/SKILL.md) — atomic read-modify-write for objects, TTL-preserving
|
|
58
|
-
- [`@warlock.js/cache/use-cache-lock/SKILL.md`](@warlock.js/cache/use-cache-lock/SKILL.md) — coordinate multi-step critical sections
|
|
59
|
-
- [`@warlock.js/cache/pick-cache-driver/SKILL.md`](@warlock.js/cache/pick-cache-driver/SKILL.md) — when you need cross-node atomicity
|
|
41
|
+
## Common shapes
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
// View counter
|
|
45
|
+
await cache.increment(`post.${id}.views`);
|
|
46
|
+
|
|
47
|
+
// Decrement stock, guard against oversell
|
|
48
|
+
const remaining = await cache.decrement(`stock.${sku}`, qty);
|
|
49
|
+
if (remaining < 0) {
|
|
50
|
+
await cache.increment(`stock.${sku}`, qty); // roll back
|
|
51
|
+
throw new Error("Out of stock");
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## See also
|
|
56
|
+
|
|
57
|
+
- [`@warlock.js/cache/use-cache-update-merge/SKILL.md`](@warlock.js/cache/use-cache-update-merge/SKILL.md) — atomic read-modify-write for objects, TTL-preserving
|
|
58
|
+
- [`@warlock.js/cache/use-cache-lock/SKILL.md`](@warlock.js/cache/use-cache-lock/SKILL.md) — coordinate multi-step critical sections
|
|
59
|
+
- [`@warlock.js/cache/pick-cache-driver/SKILL.md`](@warlock.js/cache/pick-cache-driver/SKILL.md) — when you need cross-node atomicity
|
|
@@ -1,57 +1,57 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: use-cache-bulk
|
|
3
|
-
description: '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 — `@warlock.js/cache/use-cache-tags/SKILL.md`; single-key ops — `@warlock.js/cache/cache-basics/SKILL.md`; competing libs `ioredis` `MGET`/`MSET`.'
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Bulk operations — `cache.many` / `cache.setMany`
|
|
7
|
-
|
|
8
|
-
Read or write a batch of keys in one call instead of awaiting them one at a time.
|
|
9
|
-
|
|
10
|
-
## Read many — `many(keys)`
|
|
11
|
-
|
|
12
|
-
```ts
|
|
13
|
-
import { cache } from "@warlock.js/cache";
|
|
14
|
-
|
|
15
|
-
const [alice, bob, carol] = await cache.many(["user.1", "user.2", "user.3"]);
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
- Returns an array **positionally aligned** with `keys`.
|
|
19
|
-
- Missing keys come back as `null` (same as `get`), so the result length always
|
|
20
|
-
equals the input length — zip them back together by index.
|
|
21
|
-
|
|
22
|
-
```ts
|
|
23
|
-
const ids = [1, 2, 3];
|
|
24
|
-
const users = await cache.many(ids.map((id) => `user.${id}`));
|
|
25
|
-
|
|
26
|
-
const missingIds = ids.filter((_, index) => users[index] === null);
|
|
27
|
-
// fetch only the misses from the origin…
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
## Write many — `setMany(items, ttl?)`
|
|
31
|
-
|
|
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
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
- Keys are the object keys; values are the object values.
|
|
41
|
-
- The optional second arg is a single TTL (seconds) applied to **all** entries —
|
|
42
|
-
there's no per-entry TTL or `tags` knob here. When you need tags or mixed TTLs,
|
|
43
|
-
loop with [`cache.set`](@warlock.js/cache/configure-set-options/SKILL.md) and
|
|
44
|
-
the rich options object instead.
|
|
45
|
-
|
|
46
|
-
## Performance note
|
|
47
|
-
|
|
48
|
-
On every driver these run their underlying `get`/`set` calls **concurrently**
|
|
49
|
-
(`Promise.all`) on the shared connection; on the memory family it's effectively
|
|
50
|
-
instant. There's no partial-failure handling — if one write rejects, the
|
|
51
|
-
returned promise rejects.
|
|
52
|
-
|
|
53
|
-
## See also
|
|
54
|
-
|
|
55
|
-
- [`@warlock.js/cache/cache-basics/SKILL.md`](@warlock.js/cache/cache-basics/SKILL.md) — single-key `get` / `set` / `remember`
|
|
56
|
-
- [`@warlock.js/cache/configure-set-options/SKILL.md`](@warlock.js/cache/configure-set-options/SKILL.md) — per-entry TTL, tags, conflict policy
|
|
57
|
-
- [`@warlock.js/cache/use-cache-tags/SKILL.md`](@warlock.js/cache/use-cache-tags/SKILL.md) — invalidate a batch by tag
|
|
1
|
+
---
|
|
2
|
+
name: use-cache-bulk
|
|
3
|
+
description: '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 — `@warlock.js/cache/use-cache-tags/SKILL.md`; single-key ops — `@warlock.js/cache/cache-basics/SKILL.md`; competing libs `ioredis` `MGET`/`MSET`.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Bulk operations — `cache.many` / `cache.setMany`
|
|
7
|
+
|
|
8
|
+
Read or write a batch of keys in one call instead of awaiting them one at a time.
|
|
9
|
+
|
|
10
|
+
## Read many — `many(keys)`
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { cache } from "@warlock.js/cache";
|
|
14
|
+
|
|
15
|
+
const [alice, bob, carol] = await cache.many(["user.1", "user.2", "user.3"]);
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
- Returns an array **positionally aligned** with `keys`.
|
|
19
|
+
- Missing keys come back as `null` (same as `get`), so the result length always
|
|
20
|
+
equals the input length — zip them back together by index.
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
const ids = [1, 2, 3];
|
|
24
|
+
const users = await cache.many(ids.map((id) => `user.${id}`));
|
|
25
|
+
|
|
26
|
+
const missingIds = ids.filter((_, index) => users[index] === null);
|
|
27
|
+
// fetch only the misses from the origin…
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Write many — `setMany(items, ttl?)`
|
|
31
|
+
|
|
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
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
- Keys are the object keys; values are the object values.
|
|
41
|
+
- The optional second arg is a single TTL (seconds) applied to **all** entries —
|
|
42
|
+
there's no per-entry TTL or `tags` knob here. When you need tags or mixed TTLs,
|
|
43
|
+
loop with [`cache.set`](@warlock.js/cache/configure-set-options/SKILL.md) and
|
|
44
|
+
the rich options object instead.
|
|
45
|
+
|
|
46
|
+
## Performance note
|
|
47
|
+
|
|
48
|
+
On every driver these run their underlying `get`/`set` calls **concurrently**
|
|
49
|
+
(`Promise.all`) on the shared connection; on the memory family it's effectively
|
|
50
|
+
instant. There's no partial-failure handling — if one write rejects, the
|
|
51
|
+
returned promise rejects.
|
|
52
|
+
|
|
53
|
+
## See also
|
|
54
|
+
|
|
55
|
+
- [`@warlock.js/cache/cache-basics/SKILL.md`](@warlock.js/cache/cache-basics/SKILL.md) — single-key `get` / `set` / `remember`
|
|
56
|
+
- [`@warlock.js/cache/configure-set-options/SKILL.md`](@warlock.js/cache/configure-set-options/SKILL.md) — per-entry TTL, tags, conflict policy
|
|
57
|
+
- [`@warlock.js/cache/use-cache-tags/SKILL.md`](@warlock.js/cache/use-cache-tags/SKILL.md) — invalidate a batch by tag
|