@sdxc/cache 2026.10.12 → 2026.10.16
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/README.md +209 -145
- package/dist/adapters/worker-kv.d.ts +12 -1
- package/dist/testing/conformance.js +2 -1
- package/package.json +8 -7
package/README.md
CHANGED
|
@@ -2,241 +2,305 @@
|
|
|
2
2
|
|
|
3
3
|
Cache contract with adapters for memory and Cloudflare KV.
|
|
4
4
|
|
|
5
|
-
One interface covers read, write, fetch and delete, and an adapter binds it to a store
|
|
6
|
-
in-process map for tests, Workers KV in production.
|
|
7
|
-
|
|
8
|
-
namespace whatever mix of types goes into it.
|
|
5
|
+
One `Cache` interface covers read, write, fetch and delete, and an adapter binds it to a store:
|
|
6
|
+
an in-process map for tests, Workers KV in production. Every call answers with a `Result`, and
|
|
7
|
+
each method takes the value's type on its own, so one instance serves a whole namespace.
|
|
9
8
|
|
|
10
9
|
## Installation
|
|
11
10
|
|
|
12
|
-
```
|
|
11
|
+
```sh
|
|
13
12
|
npm add @sdxc/cache
|
|
14
13
|
```
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
`isSuccess` come from. It installs alongside this package.
|
|
15
|
+
Calls answer with a `Result` from [`@sdxc/result`](https://www.npmjs.com/package/@sdxc/result),
|
|
16
|
+
where `isFailure` and `isSuccess` come from; it installs alongside this package.
|
|
19
17
|
|
|
20
18
|
## Usage
|
|
21
19
|
|
|
22
|
-
|
|
23
|
-
cache serves a whole namespace whatever mix of types goes into it. Nothing throws — every
|
|
24
|
-
call answers with a `Result`:
|
|
20
|
+
### Compute once, read many times
|
|
25
21
|
|
|
26
22
|
```typescript
|
|
27
|
-
import {
|
|
23
|
+
import { MemoryCache } from "@sdxc/cache/memory";
|
|
28
24
|
import { isFailure } from "@sdxc/result";
|
|
29
25
|
|
|
30
|
-
let cache = new
|
|
31
|
-
|
|
32
|
-
let post = await cache.fetch(
|
|
33
|
-
`post:${id}`,
|
|
34
|
-
async () => db.query.posts.findFirst({ where: eq(posts.id, id) }),
|
|
35
|
-
{ ttl: "5 minutes" },
|
|
36
|
-
);
|
|
26
|
+
let cache = new MemoryCache();
|
|
37
27
|
|
|
38
|
-
|
|
39
|
-
|
|
28
|
+
let post = await cache.fetch(`post:${id}`, async () => loadPost(id), { ttl: "5 minutes" });
|
|
29
|
+
if (isFailure(post)) return new Response(null, { status: 500 });
|
|
30
|
+
return Response.json(post.data);
|
|
40
31
|
```
|
|
41
32
|
|
|
42
|
-
`fetch`
|
|
43
|
-
comes
|
|
33
|
+
`fetch` returns the stored entry, or runs the loader on a miss and stores what it returns. The
|
|
34
|
+
type comes from the loader, so nothing is annotated.
|
|
35
|
+
|
|
36
|
+
### Read, write and delete entries
|
|
44
37
|
|
|
45
38
|
```typescript
|
|
46
|
-
|
|
47
|
-
// Result<{ at: string }, CacheError> — a Date is written as the string it serializes to
|
|
48
|
-
```
|
|
39
|
+
import { isSuccess } from "@sdxc/result";
|
|
49
40
|
|
|
50
|
-
|
|
41
|
+
await cache.write("post:1", { id: 1, title: "Hello" }, { ttl: "1 hour" });
|
|
51
42
|
|
|
52
|
-
|
|
53
|
-
|
|
43
|
+
let hit = await cache.read<{ id: number; title: string }>("post:1");
|
|
44
|
+
if (isSuccess(hit) && hit.data !== null) console.info(hit.data.title);
|
|
54
45
|
|
|
55
|
-
|
|
46
|
+
await cache.delete("post:1");
|
|
56
47
|
```
|
|
57
48
|
|
|
58
|
-
|
|
49
|
+
`read` succeeds with `null` for a missing or expired entry. A `ttl` is whole seconds or a
|
|
50
|
+
duration string such as `"1 hour"`; without one the entry never expires.
|
|
59
51
|
|
|
60
|
-
|
|
61
|
-
[`@sdxc/result`](https://www.npmjs.com/package/@sdxc/result), and `CacheError.code` says
|
|
62
|
-
which of three things happened:
|
|
52
|
+
### Values come back as JSON
|
|
63
53
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
| `unavailable` | The store could not be reached, or refused the operation | `read`, `write`, `delete` |
|
|
67
|
-
| `invalid_value` | JSON could not write the value, or read the entry stored | every method |
|
|
68
|
-
| `load_failed` | The loader a `fetch` was given failed | `fetch` |
|
|
54
|
+
Entries are stored as JSON, and every method answers with the value as it reads back, so a hit
|
|
55
|
+
and a miss have the same type:
|
|
69
56
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
`load_failed` is not logged, because it is the caller's and travels in the `Result`.
|
|
57
|
+
```typescript
|
|
58
|
+
let session = await cache.fetch("session", async () => ({ at: new Date() }));
|
|
59
|
+
// Result<{ at: string }, CacheError>: the Date reads back as its ISO string
|
|
60
|
+
```
|
|
75
61
|
|
|
76
|
-
|
|
77
|
-
caller that would rather throw rethrows the cause and keeps its own error type:
|
|
62
|
+
### Store entries in Workers KV
|
|
78
63
|
|
|
79
64
|
```typescript
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
65
|
+
import { WorkerKVCache } from "@sdxc/cache/worker-kv";
|
|
66
|
+
|
|
67
|
+
let cache = new WorkerKVCache(env.CACHE, { waitUntil: ctx.waitUntil.bind(ctx) });
|
|
83
68
|
```
|
|
84
69
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
answer with the value. So a `fetch` failure means the value could not be produced at all.
|
|
70
|
+
With `waitUntil`, a write returns at once and the put finishes after the response; the instance
|
|
71
|
+
answers reads of that key from memory until KV holds it. Create one instance per request.
|
|
88
72
|
|
|
89
73
|
## API
|
|
90
74
|
|
|
91
75
|
### `Cache`
|
|
92
76
|
|
|
93
|
-
The contract
|
|
77
|
+
The contract every adapter implements, exported from `@sdxc/cache`.
|
|
94
78
|
|
|
95
|
-
#### `read<T>(key): Promise<Result<JSONSerialized<T> | null, CacheError>>`
|
|
79
|
+
#### `read<T = JSONValue>(key: string): Promise<Result<JSONSerialized<T> | null, CacheError>>`
|
|
96
80
|
|
|
97
|
-
Reads an entry, succeeding with `null` when it is missing or expired.
|
|
81
|
+
Reads an entry, succeeding with `null` when it is missing or expired. `T` is an assertion about
|
|
82
|
+
what the store holds. A stored `null` also reads as `null`; use `fetch` where a cached `null` must
|
|
83
|
+
count as a hit.
|
|
98
84
|
|
|
99
|
-
`T
|
|
100
|
-
signature can check it, so the parameter states a claim rather than verifying one. It
|
|
101
|
-
defaults to `JSONValue` for a caller who would rather narrow the result.
|
|
85
|
+
#### `write<T>(key: string, value: T, options?: CacheWriteOptions): Promise<Result<void, CacheError>>`
|
|
102
86
|
|
|
103
|
-
|
|
104
|
-
|
|
87
|
+
Stores an entry, replacing any current value for the key. The entry is readable once the call
|
|
88
|
+
succeeds.
|
|
105
89
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
90
|
+
#### `fetch<T>(key: string, load: () => Promise<T>, options?: CacheWriteOptions): Promise<Result<JSONSerialized<T>, CacheError>>`
|
|
91
|
+
|
|
92
|
+
Returns the stored entry, or computes, stores and returns it on a miss. A stored `null` is a
|
|
93
|
+
hit. Store problems (unreachable store, an entry that is not JSON, a refused write) still answer
|
|
94
|
+
with the computed value, so a failure means the value could not be produced: `load_failed` or
|
|
95
|
+
`invalid_value`.
|
|
110
96
|
|
|
111
|
-
#### `
|
|
97
|
+
#### `delete(key: string): Promise<Result<void, CacheError>>`
|
|
112
98
|
|
|
113
|
-
|
|
114
|
-
readable.
|
|
99
|
+
Removes an entry, succeeding whether or not it exists.
|
|
115
100
|
|
|
116
|
-
|
|
117
|
-
such as `"1 hour"`. Omitted, the entry never expires.
|
|
101
|
+
### `CacheError`
|
|
118
102
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
```
|
|
103
|
+
The error inside every failed `Result`: `code`, the `key` the call was made on, and the original
|
|
104
|
+
error as `cause`.
|
|
122
105
|
|
|
123
|
-
|
|
106
|
+
| `code` | Means | Reached by |
|
|
107
|
+
| --------------- | ----------------------------------------------------------- | ------------------------- |
|
|
108
|
+
| `unavailable` | The store could not be reached, or refused the operation | `read`, `write`, `delete` |
|
|
109
|
+
| `invalid_value` | JSON could not write the value, or could not read the entry | every method but `delete` |
|
|
110
|
+
| `load_failed` | The loader given to `fetch` threw or rejected | `fetch` |
|
|
124
111
|
|
|
125
|
-
|
|
126
|
-
|
|
112
|
+
An `unavailable` failure is also logged as a warning on the current invocation's log from
|
|
113
|
+
[`@sdxc/logger`](https://www.npmjs.com/package/@sdxc/logger), so a caller can treat it as a miss
|
|
114
|
+
and still leave a trace.
|
|
127
115
|
|
|
128
|
-
|
|
116
|
+
### `new MemoryCache(options?: MemoryCacheOptions)`
|
|
129
117
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
```
|
|
118
|
+
From `@sdxc/cache/memory`. Holds entries in a map for the life of the instance, serializing on
|
|
119
|
+
write and parsing on read exactly as a remote store does; a value read back is a copy.
|
|
133
120
|
|
|
134
|
-
|
|
121
|
+
- `options.now`: returns the current time in milliseconds, for expiring entries in a test.
|
|
122
|
+
Defaults to `Date.now`.
|
|
135
123
|
|
|
136
|
-
|
|
124
|
+
### `new WorkerKVCache(kv: KVNamespace, options?: WorkerKVCacheOptions)`
|
|
137
125
|
|
|
138
|
-
|
|
126
|
+
From `@sdxc/cache/worker-kv`. Stores entries in a Cloudflare KV namespace.
|
|
139
127
|
|
|
140
|
-
|
|
141
|
-
|
|
128
|
+
- `options.waitUntil`: extends the invocation past the response. Given one, writes are deferred
|
|
129
|
+
to it, the instance answers reads from those writes immediately, writes to one key land in
|
|
130
|
+
order, and a `delete` waits for a pending put.
|
|
142
131
|
|
|
143
|
-
|
|
132
|
+
KV rejects a TTL under 60 seconds: the entry is not written and the put reports `unavailable`.
|
|
144
133
|
|
|
145
|
-
|
|
134
|
+
### `conformance(options: ConformanceOptions): void`
|
|
146
135
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
suite every other adapter does. A value read back is a copy, so mutating it changes
|
|
150
|
-
nothing that is stored.
|
|
136
|
+
From `@sdxc/cache/conformance`. Registers Vitest tests asserting the `Cache` contract against an
|
|
137
|
+
adapter you construct.
|
|
151
138
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
139
|
+
- `options.name`: labels the registered suite.
|
|
140
|
+
- `options.create`: builds the cache under test; called once per test.
|
|
141
|
+
- `options.expire`: moves the adapter's clock forward by a number of seconds. Leave it out and
|
|
142
|
+
the expiry tests are skipped.
|
|
156
143
|
|
|
157
|
-
|
|
144
|
+
### Types
|
|
158
145
|
|
|
159
|
-
|
|
146
|
+
#### `CacheWriteOptions`
|
|
160
147
|
|
|
161
|
-
|
|
148
|
+
`{ ttl?: DurationInput }`, where `DurationInput` from
|
|
149
|
+
[`@sdxc/duration`](https://www.npmjs.com/package/@sdxc/duration) is whole seconds or a duration
|
|
150
|
+
string.
|
|
151
|
+
|
|
152
|
+
#### `CacheErrorCode`
|
|
153
|
+
|
|
154
|
+
`"unavailable" | "invalid_value" | "load_failed"`.
|
|
155
|
+
|
|
156
|
+
#### `CacheErrorOptions`
|
|
157
|
+
|
|
158
|
+
What `new CacheError(message, options)` takes: `code`, `key`, and the standard `cause`. An
|
|
159
|
+
adapter of your own builds its failures with it.
|
|
160
|
+
|
|
161
|
+
#### `MemoryCacheOptions`, `WorkerKVCacheOptions`, `ConformanceOptions`
|
|
162
|
+
|
|
163
|
+
The options objects described above.
|
|
164
|
+
|
|
165
|
+
## Pattern: Accept Any Cache, Test With Memory
|
|
166
|
+
|
|
167
|
+
Type a dependency as `Cache` so production passes the KV adapter and a test passes a memory one.
|
|
162
168
|
|
|
163
169
|
```typescript
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
170
|
+
import type { Cache } from "@sdxc/cache";
|
|
171
|
+
import { MemoryCache } from "@sdxc/cache/memory";
|
|
172
|
+
import { isFailure } from "@sdxc/result";
|
|
173
|
+
import { expect, test, vi } from "vitest";
|
|
174
|
+
|
|
175
|
+
interface Post {
|
|
176
|
+
id: string;
|
|
177
|
+
title: string;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function publishedPosts(cache: Cache, load: () => Promise<Post[]>) {
|
|
181
|
+
let posts = await cache.fetch("posts:published", load, { ttl: "5 minutes" });
|
|
182
|
+
return isFailure(posts) ? [] : posts.data;
|
|
183
|
+
}
|
|
167
184
|
|
|
168
|
-
|
|
169
|
-
|
|
185
|
+
test("loads the posts once", async () => {
|
|
186
|
+
let cache = new MemoryCache();
|
|
187
|
+
let load = vi.fn(async () => [{ id: "1", title: "Hello" }]);
|
|
170
188
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
which reading your own writes is both achievable and worth anything. Two writes to one key
|
|
174
|
-
land in the order they were made, and a `delete` waits for a put still in flight.
|
|
189
|
+
await publishedPosts(cache, load);
|
|
190
|
+
await publishedPosts(cache, load);
|
|
175
191
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
192
|
+
expect(load).toHaveBeenCalledTimes(1);
|
|
193
|
+
});
|
|
194
|
+
```
|
|
179
195
|
|
|
180
|
-
|
|
196
|
+
## Pattern: Expire Entries On A Test Clock
|
|
181
197
|
|
|
182
|
-
|
|
183
|
-
against whatever you construct. Run it against a new adapter and it is a cache, or it is
|
|
184
|
-
not.
|
|
198
|
+
Pass `now` to control time, and an entry expires without waiting for it.
|
|
185
199
|
|
|
186
200
|
```typescript
|
|
187
|
-
import {
|
|
201
|
+
import { MemoryCache } from "@sdxc/cache/memory";
|
|
202
|
+
import { unwrap } from "@sdxc/result";
|
|
203
|
+
import { expect, test } from "vitest";
|
|
188
204
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
205
|
+
test("an entry expires after its ttl", async () => {
|
|
206
|
+
let clock = 0;
|
|
207
|
+
let cache = new MemoryCache({ now: () => clock });
|
|
208
|
+
|
|
209
|
+
unwrap(await cache.write("token", "abc", { ttl: "1 minute" }));
|
|
210
|
+
clock += 59_000;
|
|
211
|
+
expect(unwrap(await cache.read("token"))).toBe("abc");
|
|
212
|
+
|
|
213
|
+
clock += 1_000;
|
|
214
|
+
expect(unwrap(await cache.read("token"))).toBeNull();
|
|
193
215
|
});
|
|
194
216
|
```
|
|
195
217
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
time leaves it out, and the expiry tests are registered only for the adapters that can.
|
|
218
|
+
## Pattern: Cache Per Request In A Worker
|
|
219
|
+
|
|
220
|
+
Build the KV adapter inside the handler so read-your-own-writes stays scoped to one request, and
|
|
221
|
+
key entries by what they hold so a shared namespace stays readable.
|
|
201
222
|
|
|
202
|
-
|
|
223
|
+
```typescript
|
|
224
|
+
import { WorkerKVCache } from "@sdxc/cache/worker-kv";
|
|
225
|
+
import { isFailure } from "@sdxc/result";
|
|
226
|
+
|
|
227
|
+
interface Env {
|
|
228
|
+
CACHE: KVNamespace;
|
|
229
|
+
}
|
|
203
230
|
|
|
204
|
-
|
|
205
|
-
|
|
231
|
+
export default {
|
|
232
|
+
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
|
|
233
|
+
let cache = new WorkerKVCache(env.CACHE, { waitUntil: ctx.waitUntil.bind(ctx) });
|
|
234
|
+
let locale = new URL(request.url).searchParams.get("locale") ?? "en";
|
|
235
|
+
|
|
236
|
+
let feed = await cache.fetch(`feed:${locale}`, async () => buildFeed(locale), {
|
|
237
|
+
ttl: "10 minutes",
|
|
238
|
+
});
|
|
239
|
+
if (isFailure(feed)) return new Response("Feed unavailable", { status: 503 });
|
|
240
|
+
|
|
241
|
+
return Response.json(feed.data);
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## Pattern: Invalidate After A Write
|
|
247
|
+
|
|
248
|
+
Delete the cached entry when the source changes, and the next `fetch` recomputes it.
|
|
206
249
|
|
|
207
250
|
```typescript
|
|
208
|
-
|
|
209
|
-
|
|
251
|
+
import type { Cache } from "@sdxc/cache";
|
|
252
|
+
|
|
253
|
+
async function renamePost(cache: Cache, id: string, title: string) {
|
|
254
|
+
await savePostTitle(id, title);
|
|
255
|
+
await cache.delete(`post:${id}`);
|
|
256
|
+
}
|
|
210
257
|
```
|
|
211
258
|
|
|
212
|
-
## Pattern:
|
|
259
|
+
## Pattern: Rethrow Failures
|
|
213
260
|
|
|
214
|
-
|
|
261
|
+
A caller that prefers exceptions rethrows the original error, keeping its own error type.
|
|
215
262
|
|
|
216
263
|
```typescript
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
ttl: "5 minutes",
|
|
220
|
-
});
|
|
264
|
+
import type { Cache } from "@sdxc/cache";
|
|
265
|
+
import { isFailure } from "@sdxc/result";
|
|
221
266
|
|
|
222
|
-
|
|
267
|
+
async function cachedReport(cache: Cache, month: string) {
|
|
268
|
+
let report = await cache.fetch(`report:${month}`, async () => buildReport(month), {
|
|
269
|
+
ttl: "1 day",
|
|
270
|
+
});
|
|
271
|
+
if (isFailure(report)) throw report.error.cause ?? report.error;
|
|
272
|
+
return report.data;
|
|
223
273
|
}
|
|
274
|
+
```
|
|
224
275
|
|
|
225
|
-
|
|
226
|
-
let cache = new MemoryCache();
|
|
276
|
+
## Pattern: Verify A Custom Adapter
|
|
227
277
|
|
|
228
|
-
|
|
229
|
-
|
|
278
|
+
Implement `Cache` over another store, answer its failures with a `CacheError`, and run the
|
|
279
|
+
conformance suite against it in a test file.
|
|
230
280
|
|
|
231
|
-
|
|
281
|
+
```typescript
|
|
282
|
+
import type { Cache } from "@sdxc/cache";
|
|
283
|
+
import { CacheError } from "@sdxc/cache";
|
|
284
|
+
import { conformance } from "@sdxc/cache/conformance";
|
|
285
|
+
import { failure } from "@sdxc/result";
|
|
286
|
+
|
|
287
|
+
export function unavailable(key: string, cause: unknown) {
|
|
288
|
+
return failure(
|
|
289
|
+
new CacheError(`The store could not reach ${key}`, { code: "unavailable", key, cause }),
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
conformance({
|
|
294
|
+
name: "RedisCache",
|
|
295
|
+
create: (): Cache => new RedisCache(redis),
|
|
232
296
|
});
|
|
233
297
|
```
|
|
234
298
|
|
|
235
299
|
## Versioning
|
|
236
300
|
|
|
237
|
-
Releases are dated rather than semantic. A version is the UTC date it was published,
|
|
238
|
-
|
|
239
|
-
|
|
301
|
+
Releases are dated rather than semantic. A version is the UTC date it was published, written
|
|
302
|
+
`YYYY.M.D`, so `2026.9.4` is the release from 4 September 2026. At most one release goes out per
|
|
303
|
+
day.
|
|
240
304
|
|
|
241
305
|
Those numbers say when, not what: a later date means a later release and carries no
|
|
242
306
|
compatibility promise. Any release may change or remove an export.
|
|
@@ -251,8 +315,8 @@ Depend on one exact date, and move it when you are ready to take the change:
|
|
|
251
315
|
}
|
|
252
316
|
```
|
|
253
317
|
|
|
254
|
-
A caret or tilde range reads the date as major, minor and patch, so it accepts every
|
|
255
|
-
|
|
318
|
+
A caret or tilde range reads the date as major, minor and patch, so it accepts every later
|
|
319
|
+
release in the same year. An exact version keeps the upgrade yours to schedule.
|
|
256
320
|
|
|
257
321
|
## License
|
|
258
322
|
|
|
@@ -9,6 +9,17 @@
|
|
|
9
9
|
import type { Result } from "@sdxc/result";
|
|
10
10
|
import type { JSONSerialized, JSONValue } from "@sdxc/types";
|
|
11
11
|
import type { Cache, CacheError, CacheWriteOptions } from "../index.js";
|
|
12
|
+
/**
|
|
13
|
+
* The calls this cache makes on a Workers KV binding. Declared here so a binding typed by any
|
|
14
|
+
* source fits it: a Worker's generated types, `@cloudflare/workers-types`, or a test's own.
|
|
15
|
+
*/
|
|
16
|
+
export interface WorkerKVNamespace {
|
|
17
|
+
get(key: string, type: "text"): Promise<string | null>;
|
|
18
|
+
put(key: string, value: string, options?: {
|
|
19
|
+
expirationTtl?: number;
|
|
20
|
+
}): Promise<void>;
|
|
21
|
+
delete(key: string): Promise<void>;
|
|
22
|
+
}
|
|
12
23
|
/** How a Worker KV cache defers its writes. */
|
|
13
24
|
export interface WorkerKVCacheOptions {
|
|
14
25
|
/**
|
|
@@ -30,7 +41,7 @@ export declare class WorkerKVCache implements Cache {
|
|
|
30
41
|
* @param kv The namespace entries are stored in.
|
|
31
42
|
* @param options How writes are deferred.
|
|
32
43
|
*/
|
|
33
|
-
constructor(kv:
|
|
44
|
+
constructor(kv: WorkerKVNamespace, { waitUntil }?: WorkerKVCacheOptions);
|
|
34
45
|
read<T = JSONValue>(key: string): Promise<Result<JSONSerialized<T> | null, CacheError>>;
|
|
35
46
|
write<T>(key: string, value: T, options?: CacheWriteOptions): Promise<Result<void, CacheError>>;
|
|
36
47
|
fetch<T>(key: string, load: () => Promise<T>, options?: CacheWriteOptions): Promise<Result<JSONSerialized<T>, CacheError>>;
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* @copyright Sergio Xalambrí 2026
|
|
8
8
|
*/
|
|
9
9
|
import { isSuccess, unwrap } from "@sdxc/result";
|
|
10
|
+
import { generateUUID } from "@sdxc/uuid/v4";
|
|
10
11
|
import { describe, expect, test, vi } from "vitest";
|
|
11
12
|
/**
|
|
12
13
|
* The shortest TTL the suite asks for. KV refuses anything under 60 seconds, so
|
|
@@ -20,7 +21,7 @@ const MIN_TTL_SECONDS = 60;
|
|
|
20
21
|
*/
|
|
21
22
|
export function conformance({ name, create, expire }) {
|
|
22
23
|
/** A key no other test has used, so adapters sharing a store cannot collide. */
|
|
23
|
-
let key = () => `conformance:${
|
|
24
|
+
let key = () => `conformance:${generateUUID()}`;
|
|
24
25
|
/** The error a call answered with, asserting that it answered with one. */
|
|
25
26
|
let errorOf = (result) => {
|
|
26
27
|
if (isSuccess(result))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdxc/cache",
|
|
3
|
-
"version": "2026.10.
|
|
3
|
+
"version": "2026.10.16",
|
|
4
4
|
"description": "Cache contract with adapters for memory and Cloudflare KV",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -11,12 +11,13 @@
|
|
|
11
11
|
"./worker-kv": "./dist/adapters/worker-kv.js"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@sdxc/duration": "2026.10.
|
|
15
|
-
"@sdxc/logger": "2026.10.
|
|
16
|
-
"@sdxc/result": "2026.10.
|
|
17
|
-
"@sdxc/types": "2026.10.
|
|
14
|
+
"@sdxc/duration": "2026.10.13",
|
|
15
|
+
"@sdxc/logger": "2026.10.16",
|
|
16
|
+
"@sdxc/result": "2026.10.13",
|
|
17
|
+
"@sdxc/types": "2026.10.13",
|
|
18
|
+
"@sdxc/uuid": "2026.10.13"
|
|
18
19
|
},
|
|
19
|
-
"gitHead": "
|
|
20
|
+
"gitHead": "1e547d8431568f5ed9bdb079ca3046c936240fde",
|
|
20
21
|
"publishConfig": {
|
|
21
22
|
"access": "public"
|
|
22
23
|
},
|
|
@@ -25,7 +26,7 @@
|
|
|
25
26
|
"url": "git+https://github.com/sergiodxa/monorepo.git",
|
|
26
27
|
"directory": "packages/cache"
|
|
27
28
|
},
|
|
28
|
-
"homepage": "https://sdxc.
|
|
29
|
+
"homepage": "https://sdxc.dev/api/cache",
|
|
29
30
|
"author": {
|
|
30
31
|
"name": "Sergio Xalambrí",
|
|
31
32
|
"url": "https://sergiodxa.com"
|