@dotpress/cache 2.0.0-alpha.1 → 2.0.0-alpha.3
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 +15 -280
- package/dist/index.d.ts +4 -1
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -2,304 +2,39 @@
|
|
|
2
2
|
|
|
3
3
|
Caching for [Dotpress](https://dotpress.dev), at two levels:
|
|
4
4
|
|
|
5
|
-
- **memo**: values loaded once per
|
|
6
|
-
- **cache**: values kept across requests and instances,
|
|
5
|
+
- **memo**: values loaded once per request, event message or job, with single flight and batching (no N+1 queries);
|
|
6
|
+
- **cache**: values kept across requests and instances, with tags, a grace period when the source fails, and stores for Redis, Valkey, Upstash and Cloudflare Workers KV. It fails open when the store is down.
|
|
7
7
|
|
|
8
8
|
Works without a Dotpress app: in a script, a worker or a Lambda function.
|
|
9
9
|
|
|
10
10
|
> 🚧 Dotpress V2 is in alpha (`2.0.0-alpha`): the API may still change before 2.0.0.
|
|
11
11
|
|
|
12
12
|
```sh
|
|
13
|
-
|
|
14
|
-
pnpm add @dotpress/core # for cachePlugin
|
|
13
|
+
npm install @dotpress/cache
|
|
15
14
|
```
|
|
16
15
|
|
|
17
|
-
## Memo
|
|
18
|
-
|
|
19
|
-
A middleware, a route handler and a service all need the current user: with a memo, it is loaded once per request.
|
|
20
|
-
|
|
21
16
|
```ts
|
|
22
|
-
import { createMemo, defineMemo } from '@dotpress/cache'
|
|
17
|
+
import { createCache, createMemo, createMemoryStore, defineCacheEntry, defineMemo } from '@dotpress/cache'
|
|
23
18
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
load: (id: string) => db.users.findById(id),
|
|
27
|
-
})
|
|
19
|
+
// Memo: loaded once per unit of work
|
|
20
|
+
export const loadUser = defineMemo({ name: 'user', load: (id: string) => db.users.findById(id) })
|
|
28
21
|
|
|
29
22
|
const memo = createMemo() // one per request, message or job
|
|
30
|
-
const user = await memo(loadUser,
|
|
31
|
-
const same = await memo(loadUser, id) // memoized
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
- **Single flight**: calls made while a value loads share the same load. Three parallel calls with the same id make one query.
|
|
35
|
-
- **Failures are shared, not kept**: the calls in progress fail together, and the next call loads again.
|
|
36
|
-
- **Keys**: the arguments are compared by value (strings, numbers, booleans, `null`, `undefined`, bigints, dates, arrays, plain objects; the order of object properties does not matter). For other arguments, add a `key` option that returns a string: `key: (user) => user.id`.
|
|
37
|
-
- **Updates**: `memo.set(loadUser, id, user)` after an update, `memo.delete(loadUser, id)` to load again, `memo.clear(loadUser)` or `memo.clear()`.
|
|
38
|
-
|
|
39
|
-
A memo lives as long as its unit of work: create one per request, per event message, per job, never one per process (it would grow forever and serve old values). Values shared across requests belong in the cache.
|
|
40
|
-
|
|
41
|
-
Values are the objects returned by `load`, not copies: a caller that mutates one changes it for the others.
|
|
42
|
-
|
|
43
|
-
### Batching
|
|
44
|
-
|
|
45
|
-
With `batch` instead of `load`, the keys requested in the same tick are loaded in one call: no N+1 queries when a list renders the author of each item.
|
|
46
|
-
|
|
47
|
-
```ts
|
|
48
|
-
export const loadAuthor = defineMemo({
|
|
49
|
-
name: 'author',
|
|
50
|
-
// A Map by id: missing ids give undefined.
|
|
51
|
-
batch: async (ids: readonly string[]) => {
|
|
52
|
-
const users = await db.users.findManyById(ids)
|
|
53
|
-
return new Map(users.map((user) => [user.id, user]))
|
|
54
|
-
},
|
|
55
|
-
maxBatchSize: 100, // optional: keys per call
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
const authors = await Promise.all(posts.map((post) => memo(loadAuthor, post.authorId))) // one query
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
`batch` receives the keys without duplicates, and returns either a `Map` by key, or an array of values in the order of the keys, where an `Error` fails its key only. An array of the wrong length fails every key with a `MemoBatchError`.
|
|
62
|
-
|
|
63
|
-
Keys requested before the batch runs join it: it runs on a timer (`setTimeout(run, 0)`), after the code that requested them, awaits included. Replace it with `schedule: (run) => …`. Keys requested once it has started go to the next batch.
|
|
64
|
-
|
|
65
|
-
## Cache
|
|
66
|
-
|
|
67
|
-
```ts
|
|
68
|
-
import { createCache, createMemoryStore, defineCacheEntry } from '@dotpress/cache'
|
|
69
|
-
import { z } from 'zod'
|
|
70
|
-
|
|
71
|
-
export const userCache = defineCacheEntry({
|
|
72
|
-
name: 'user',
|
|
73
|
-
key: (id: string) => id,
|
|
74
|
-
schema: z.object({ id: z.string(), name: z.string(), createdAt: z.coerce.date() }), // optional
|
|
75
|
-
ttl: '1h',
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
const cache = createCache({ store: createMemoryStore(), prefix: 'app', ttl: '10m', logger })
|
|
79
|
-
|
|
80
|
-
const user = await cache.getOrSet(userCache, id, () => db.users.findById(id))
|
|
81
|
-
await cache.get(userCache, id) // undefined when missing or expired
|
|
82
|
-
await cache.set(userCache, id, user)
|
|
83
|
-
await cache.delete(userCache, id) // after an update
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
- **`getOrSet`** reads the value, or loads it with the last argument and caches it. Calls made while it loads share the load (single flight, per cache). A failed load is not cached: the error reaches the caller, unless the grace period serves a stale value (see [Resilience](#resilience)).
|
|
87
|
-
- **`get`** returns fresh values only: undefined when missing, expired or invalidated by a tag.
|
|
88
|
-
- **Keys**: `prefix:name:vVersion:key`, like `app:user:v1:42`. `key` returns a string, a number, or a value compared like the arguments of a memo; without `key`, the argument (or the arguments) is the key. Keys longer than the store accepts are hashed (SHA-256).
|
|
89
|
-
- **`version`** (1 by default) is part of the key: increase it when the shape of the values changes, and the values written by the previous version are ignored.
|
|
90
|
-
- **Durations**: a number of seconds, or a string: `'500ms'`, `'30s'`, `'10m'`, `'1h'`, `'7d'`. The `ttl` of an entry overrides the `ttl` of the cache (1 hour by default).
|
|
91
|
-
- **Types**: with a `schema`, the value type comes from it; without, `getOrSet` returns the type of the loader, and `get` returns `unknown`. `defineCacheEntry<[id: string], User>({ … })` declares it.
|
|
92
|
-
- **`withLogger(logger)`** returns the same cache with another logger (the logger of a request).
|
|
93
|
-
|
|
94
|
-
### Values
|
|
95
|
-
|
|
96
|
-
Values are serialized with JSON (or `serializer: { serialize, deserialize }`, like superjson). `undefined` is not cached; `null` is.
|
|
23
|
+
const user = await memo(loadUser, '42') // loads; the next calls are memoized
|
|
97
24
|
|
|
98
|
-
|
|
25
|
+
// Cache: kept across requests
|
|
26
|
+
export const planCache = defineCacheEntry({ name: 'plan', key: (id: string) => id, ttl: '1h' })
|
|
99
27
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
### Resilience
|
|
103
|
-
|
|
104
|
-
Some data is better a little outdated than missing: a price list, a catalog, a configuration. An entry (or the cache, for every entry) sets how stale values are served:
|
|
105
|
-
|
|
106
|
-
```ts
|
|
107
|
-
export const catalogCache = defineCacheEntry({
|
|
108
|
-
name: 'catalog',
|
|
109
|
-
ttl: '5m',
|
|
110
|
-
grace: '1d', // stale-if-error: serve the expired value when the loader fails
|
|
111
|
-
graceBackoff: '30s', // then do not call the failing loader again for 30 seconds
|
|
112
|
-
staleWhileRevalidate: '1m', // serve the expired value and refresh it in the background
|
|
113
|
-
timeout: '300ms', // a slower loader serves the stale value, and goes on in the background
|
|
114
|
-
hardTimeout: '5s', // a slower loader fails (or serves the stale value)
|
|
115
|
-
})
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
| Option | Default | Effect, once the `ttl` is over |
|
|
119
|
-
| --- | --- | --- |
|
|
120
|
-
| `grace` | 0 (disabled) | During this time, when the loader fails or times out, `getOrSet` returns the expired value instead of the error, with a `warn` log. |
|
|
121
|
-
| `graceBackoff` | `'10s'` | After such a failure, the loader is not called again during this delay: the expired value is served directly. A successful load ends it. |
|
|
122
|
-
| `staleWhileRevalidate` | 0 (disabled) | During this time, `getOrSet` returns the expired value at once and reloads it in the background (once per key). Failed refreshes are logged. |
|
|
123
|
-
| `timeout` | none | With an expired value in its grace period, a load slower than this returns that value (`warn` log); the load goes on and caches its result. |
|
|
124
|
-
| `hardTimeout` | none | A load slower than this fails with a `CacheTimeoutError`, or returns the expired value of the grace period. The load goes on and caches its result. |
|
|
125
|
-
| `lock` | none | Distributed single flight (stores with locks, like Redis): before loading a missing value, the instance takes a lock for at most this duration; the other instances wait for the value meanwhile (reading it every 50 ms), then load it themselves if the lock was not released. Set it above the duration of the loader. |
|
|
126
|
-
|
|
127
|
-
Values stay in the store for `ttl` plus the longer of `grace` and `staleWhileRevalidate`. Without a grace period, a failing loader fails the call, as without cache.
|
|
128
|
-
|
|
129
|
-
### Tags
|
|
130
|
-
|
|
131
|
-
Tags expire related values at once, on every store:
|
|
132
|
-
|
|
133
|
-
```ts
|
|
134
|
-
export const productCache = defineCacheEntry({
|
|
135
|
-
name: 'product',
|
|
136
|
-
key: (id: string) => id,
|
|
137
|
-
tags: (id: string) => [`product:${id}`, 'catalog'],
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
await cache.invalidateTags(['catalog']) // after an import
|
|
28
|
+
const cache = createCache({ store: createMemoryStore(), prefix: 'app' })
|
|
29
|
+
const plan = await cache.getOrSet(planCache, user.planId, () => billing.getPlan(user.planId))
|
|
141
30
|
```
|
|
142
31
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
### Events
|
|
146
|
-
|
|
147
|
-
`onEvent(event)` is called for each result of `get` and `getOrSet`, for metrics: `{ type: 'hit' | 'miss' | 'stale', reason?: 'revalidate' | 'error' | 'timeout' | 'backoff', entry, key }`. The same events are logged at `debug` level.
|
|
148
|
-
|
|
149
|
-
### Store failures
|
|
150
|
-
|
|
151
|
-
The cache is an optimization: by default it **fails open**. When the store fails, a read is a miss (the loader runs), a write is skipped, and the app keeps working. Every failure is reported:
|
|
152
|
-
|
|
153
|
-
- a log: `warn` for reads and writes, `error` for deletions and invalidations (they may leave outdated values), with the store, the operation, the key and the entry;
|
|
154
|
-
- `onError(error, { operation, key, entry })`, for metrics or alerts (its own errors are logged and ignored); `operation` is `get`, `set`, `delete` or `invalidate`.
|
|
155
|
-
|
|
156
|
-
```ts
|
|
157
|
-
const cache = createCache({
|
|
158
|
-
store,
|
|
159
|
-
logger,
|
|
160
|
-
onError: (error, { operation }) => metrics.increment('cache.store_error', { operation }),
|
|
161
|
-
storeTimeout: '200ms', // a slower store counts as a failure
|
|
162
|
-
failOpen: true, // false: throw a CacheStoreError instead
|
|
163
|
-
})
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
## Stores
|
|
167
|
-
|
|
168
|
-
| Store | Import | Use |
|
|
169
|
-
| --- | --- | --- |
|
|
170
|
-
| `createMemoryStore({ maxEntries })` | `@dotpress/cache` | memory of the process: one instance, development, tests. Least recently used entries are removed beyond `maxEntries` (10,000 by default) |
|
|
171
|
-
| `createRedisStore({ client })` | `@dotpress/cache/redis` | Redis or Valkey, with a node-redis or ioredis client |
|
|
172
|
-
| `createUpstashStore({ url, token })` | `@dotpress/cache/upstash` | Upstash Redis over its REST API: every runtime, edge included |
|
|
173
|
-
| `createCloudflareKvStore({ kv })` | `@dotpress/cache/cloudflare-kv` | Workers KV, through the binding of a Worker: values read often and changed rarely |
|
|
174
|
-
|
|
175
|
-
A store implements `CacheStore`: `name`, `get(key)`, `set(key, value, ttl)` (strings, `ttl` in milliseconds), `delete(key)`, and optionally `getMany(keys)` (one round trip for the tokens of tags), `lock(key, ttl)` / `unlock(key, token)` (the `lock` option) and `maxKeyLength` (bytes).
|
|
176
|
-
|
|
177
|
-
### Redis and Valkey
|
|
178
|
-
|
|
179
|
-
```ts
|
|
180
|
-
import { createClient } from 'redis' // or: import { Redis } from 'ioredis'
|
|
181
|
-
import { createRedisStore } from '@dotpress/cache/redis'
|
|
182
|
-
|
|
183
|
-
const client = createClient({ url: process.env.REDIS_URL })
|
|
184
|
-
client.on('error', (error) => logger.error({ error }, 'Redis error'))
|
|
185
|
-
await client.connect()
|
|
186
|
-
|
|
187
|
-
const cache = createCache({ store: createRedisStore({ client }), prefix: 'app', storeTimeout: '200ms' })
|
|
188
|
-
// On shutdown: await client.close()
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
The store uses the client of the app, which connects and closes it: [node-redis](https://github.com/redis/node-redis) (`redis`, v4 or later; the client recommended by Redis), or [ioredis](https://github.com/redis/ioredis) (`new Redis(url)`; also used by BullMQ). It works with Redis and [Valkey](https://valkey.io), and any server of the same protocol (Dragonfly, KeyDB…). Commands: `GET`, `MGET` (tags), `SET … PX`, `DEL`, and for the `lock` option `SET … NX PX` and a Lua script that releases a lock only with its token (no `DELEX`, which needs Redis 8.4).
|
|
192
|
-
|
|
193
|
-
- Cluster clients (`createCluster()`, `new Cluster()`) are refused for now: use a standalone server, or the primary of a replicated one.
|
|
194
|
-
- Set `storeTimeout`: while the client reconnects, its commands wait, and the cache would wait with them.
|
|
195
|
-
- Valkey on a managed service (ElastiCache, Memorystore, Upstash with its Redis protocol endpoint) works the same way.
|
|
196
|
-
|
|
197
|
-
### Upstash
|
|
198
|
-
|
|
199
|
-
```ts
|
|
200
|
-
import { createUpstashStore } from '@dotpress/cache/upstash'
|
|
201
|
-
|
|
202
|
-
const store = createUpstashStore({
|
|
203
|
-
url: env.UPSTASH_REDIS_REST_URL,
|
|
204
|
-
token: env.UPSTASH_REDIS_REST_TOKEN, // the standard token: the read-only one cannot write
|
|
205
|
-
})
|
|
206
|
-
const cache = createCache({ store, prefix: 'app', storeTimeout: '500ms' })
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
The store calls the [REST API](https://upstash.com/docs/redis/features/restapi) of [Upstash Redis](https://upstash.com/docs/redis) with `fetch`: no dependency, no connection to open or close, and it runs on Cloudflare Workers, Vercel, Deno, Bun and Node.js. Each operation is one HTTPS request (`GET`, `MGET` for tags, `SET … PX`, `DEL`, and `SET … NX PX` plus a Lua script for the `lock` option). Errors of the API (`401`, `400 ERR …`) are store failures: logged, and the cache fails open. A request is limited to 10 MB on the free and pay-as-you-go plans. The `fetch` option replaces the global `fetch`.
|
|
210
|
-
|
|
211
|
-
The `@upstash/redis` SDK is not needed: by default it applies `JSON.parse` to every reply, which would change the values of the cache.
|
|
212
|
-
|
|
213
|
-
### Cloudflare Workers KV
|
|
214
|
-
|
|
215
|
-
```jsonc
|
|
216
|
-
// wrangler.jsonc
|
|
217
|
-
{ "kv_namespaces": [{ "binding": "CACHE", "id": "<namespace id>" }] }
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
```ts
|
|
221
|
-
import { env } from 'cloudflare:workers'
|
|
222
|
-
import { createCloudflareKvStore } from '@dotpress/cache/cloudflare-kv'
|
|
223
|
-
|
|
224
|
-
const cache = createCache({ store: createCloudflareKvStore({ kv: () => env.CACHE }), prefix: 'app' })
|
|
225
|
-
```
|
|
226
|
-
|
|
227
|
-
`kv` is the binding, or a function that returns it (resolved on use). [Workers KV](https://developers.cloudflare.com/kv) suits values read often and changed rarely (configuration, catalogs, rendered pages); keep in mind its limits:
|
|
228
|
-
|
|
229
|
-
| Limit | Effect |
|
|
230
|
-
| --- | --- |
|
|
231
|
-
| Eventual consistency | A write, deletion or invalidation can take 60 seconds or more to reach other locations: other locations serve the previous value meanwhile. |
|
|
232
|
-
| Expiration of 60 seconds at least | Shorter `ttl` are kept in the value: the cache expires them on time, KV removes them after 60 seconds. |
|
|
233
|
-
| One write per second per key | Faster writes fail with a 429: a store failure (logged, fails open). Tags write a token per tag on `invalidateTags()`. |
|
|
234
|
-
| Keys of 512 bytes | Longer keys are hashed. |
|
|
235
|
-
| No locks | The `lock` option is ignored: single flight stays per instance. |
|
|
236
|
-
| Free plan: 1,000 writes per day | Cache misses write: count them before choosing KV. |
|
|
237
|
-
|
|
238
|
-
`cacheTtl` (30 seconds at least) keeps read values in the cache of the location for longer: faster reads, but changes are seen even later. Tokens of tags are read in bulk (100 keys per read).
|
|
239
|
-
|
|
240
|
-
## Dotpress plugin
|
|
241
|
-
|
|
242
|
-
```ts
|
|
243
|
-
import { cachePlugin } from '@dotpress/cache/plugin'
|
|
244
|
-
|
|
245
|
-
await createApp({ adapter, plugins: [cachePlugin({ cache })] })
|
|
246
|
-
|
|
247
|
-
const authenticate = defineMiddleware(async (ctx) => {
|
|
248
|
-
ctx.session = await ctx.memo(loadSession, ctx.req.headers.authorization)
|
|
249
|
-
})
|
|
250
|
-
|
|
251
|
-
defineRoute({
|
|
252
|
-
path: '/me',
|
|
253
|
-
middlewares: [authenticate],
|
|
254
|
-
handler: async ({ memo, cache, session }) => {
|
|
255
|
-
const user = await memo(loadUser, session.userId) // loaded once per request
|
|
256
|
-
const plan = await cache.getOrSet(planCache, user.planId, () => billing.getPlan(user.planId))
|
|
257
|
-
return { user, plan }
|
|
258
|
-
},
|
|
259
|
-
})
|
|
260
|
-
```
|
|
261
|
-
|
|
262
|
-
- `ctx.memo`: a new memo for each request, created on first use.
|
|
263
|
-
- `ctx.cache` (with `cache`): the cache, logging with the request logger (request ID).
|
|
264
|
-
|
|
265
|
-
### Memo without passing the context
|
|
266
|
-
|
|
267
|
-
Services deep in the code do not always receive `ctx`. With `memoScope: asyncLocalMemo`, each request runs in the scope of its memo ([`AsyncLocalStorage`](https://nodejs.org/api/async_context.html): Node.js, Bun, Deno, and Cloudflare Workers with the `nodejs_compat` flag), and a memoized loader works anywhere during the request:
|
|
268
|
-
|
|
269
|
-
```ts
|
|
270
|
-
import { asyncLocalMemo, memoized } from '@dotpress/cache/als'
|
|
271
|
-
|
|
272
|
-
await createApp({ adapter, plugins: [cachePlugin({ cache, memoScope: asyncLocalMemo })] })
|
|
273
|
-
|
|
274
|
-
export const getUser = memoized(loadUser)
|
|
275
|
-
|
|
276
|
-
// A service, called by a middleware, a handler or another service
|
|
277
|
-
export const canEdit = async (userId: string, doc: Doc) => (await getUser(userId)).role === 'admin'
|
|
278
|
-
```
|
|
279
|
-
|
|
280
|
-
Outside a request (an event listener, a job, a script), open a scope with `runWithMemo()`; `currentMemo()` returns the memo of the scope (for `set` and `delete`). Outside a scope, both throw an explicit error: there is no global memo.
|
|
281
|
-
|
|
282
|
-
```ts
|
|
283
|
-
import { runWithMemo } from '@dotpress/cache/als'
|
|
284
|
-
|
|
285
|
-
export default defineListener(orderPaid, {
|
|
286
|
-
name: 'send-invoice',
|
|
287
|
-
handle: ({ payload }) => runWithMemo(() => sendInvoice(payload.orderId)), // one memo per message
|
|
288
|
-
})
|
|
289
|
-
```
|
|
290
|
-
|
|
291
|
-
Without `/als`, create a memo per message: `const memo = createMemo()`.
|
|
292
|
-
|
|
293
|
-
## Errors
|
|
32
|
+
With `cachePlugin()` of `@dotpress/cache/plugin`, every request gets `ctx.memo` and `ctx.cache`.
|
|
294
33
|
|
|
295
|
-
|
|
296
|
-
| --- | --- |
|
|
297
|
-
| `CacheError` | Base class of the errors of the package |
|
|
298
|
-
| `MemoBatchError` | `batch` returned values that do not match the keys (`memo`: name of the memo) |
|
|
299
|
-
| `CacheTimeoutError` | A load exceeded `hardTimeout`, without stale value to serve (`entry`, `key`, `timeout`) |
|
|
300
|
-
| `CacheStoreError` | The store failed or timed out, with `failOpen: false` (`store`, `operation`, `key`, `cause`); also passed to `onError` |
|
|
34
|
+
## Documentation
|
|
301
35
|
|
|
302
|
-
|
|
36
|
+
- [Cache and memoization](https://dotpress.dev/services/cache): memo and batching, cache, invalidation and tags, resilience, store failures, stores
|
|
37
|
+
- [Cache reference](https://dotpress.dev/reference/cache): every option, the stores and their limits, errors
|
|
303
38
|
|
|
304
39
|
## License
|
|
305
40
|
|
package/dist/index.d.ts
CHANGED
|
@@ -357,9 +357,12 @@ interface Cache {
|
|
|
357
357
|
* The value of the arguments, loaded by the last argument when missing
|
|
358
358
|
* and cached. Calls made while it loads share the load. Applies the
|
|
359
359
|
* grace period, stale-while-revalidate and timeouts of the entry.
|
|
360
|
+
*
|
|
361
|
+
* A loader may return `undefined` (not found): it is not cached, and
|
|
362
|
+
* `getOrSet` returns it.
|
|
360
363
|
*/
|
|
361
364
|
getOrSet<A extends readonly unknown[], R>(entry: CacheEntry<A, Untyped>, ...argsAndLoader: [...A, () => R | Promise<R>]): Promise<R>;
|
|
362
|
-
getOrSet<A extends readonly unknown[], T>(entry: CacheEntry<A, T>, ...argsAndLoader: [...A, () =>
|
|
365
|
+
getOrSet<A extends readonly unknown[], T, R extends T | undefined = T>(entry: CacheEntry<A, T>, ...argsAndLoader: [...A, () => R | Promise<R>]): Promise<[R] extends [T] ? T : T | undefined>;
|
|
363
366
|
/**
|
|
364
367
|
* Expires every value with one of the tags (they can still be served by
|
|
365
368
|
* the grace period when the loader fails).
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/logger.ts","../src/store.ts","../src/cache.ts"],"sourcesContent":["/**\n * Logger of the cache. A Dotpress or Pino logger fits; `cachePlugin` uses\n * the logger of the request.\n */\nexport interface CacheLogger {\n debug?(data: object, message: string): void\n info(data: object, message: string): void\n warn(data: object, message: string): void\n error(data: object, message: string): void\n}\n\nconst noop = () => undefined\n\n/** Logger that discards everything. */\nexport const silentLogger: CacheLogger = {\n debug: noop,\n info: noop,\n warn: noop,\n error: noop,\n}\n\n/** Checks a logger option. */\nexport const checkLogger = (label: string, logger: unknown) => {\n for (const level of ['info', 'warn', 'error'] as const) {\n if (\n typeof (logger as Record<string, unknown> | null)?.[level] !== 'function'\n ) {\n throw new TypeError(\n `${label}: logger must have info(), warn() and error() methods.`\n )\n }\n }\n return logger as CacheLogger\n}\n","/**\n * Where the cache keeps its entries: Redis, Upstash, Cloudflare KV,\n * memory… Values are strings, serialized by the cache.\n */\nexport interface CacheStore {\n /** Name of the store, in logs and errors. */\n readonly name: string\n /**\n * Longest key accepted, in bytes (UTF-8). Longer keys are hashed by the\n * cache. Undefined without limit.\n */\n readonly maxKeyLength?: number | undefined\n /** Value of a key, or undefined when it is missing or expired. */\n get(key: string): Promise<string | undefined>\n /**\n * Values of several keys, in their order: one round trip for the tags of\n * an entry. Optional: the cache calls `get` for each key without it.\n */\n getMany?(keys: readonly string[]): Promise<(string | undefined)[]>\n /** Sets a value that expires after `ttl` milliseconds (at least 1). */\n set(key: string, value: string, ttl: number): Promise<void>\n /** Removes a key; missing keys are ignored. */\n delete(key: string): Promise<void>\n /**\n * Takes a lock for `ttl` milliseconds: returns its token, or undefined\n * when another holder has it. Optional: the `lock` option of the cache\n * needs it.\n */\n lock?(key: string, ttl: number): Promise<string | undefined>\n /** Releases a lock, if the token still holds it. */\n unlock?(key: string, token: string): Promise<void>\n}\n\n/** Checks a store option. */\nexport const isStore = (value: unknown): value is CacheStore => {\n const store = value as Partial<CacheStore> | null\n return (\n typeof store?.name === 'string' &&\n typeof store.get === 'function' &&\n typeof store.set === 'function' &&\n typeof store.delete === 'function'\n )\n}\n\n/** Options of `createMemoryStore()`. */\nexport interface MemoryStoreOptions {\n /**\n * Entries kept at most: the least recently used ones are removed\n * first. Defaults to 10,000.\n */\n maxEntries?: number | undefined\n}\n\n/** Store in the memory of the process: for one instance, and for tests. */\nexport interface MemoryStore extends CacheStore {\n /** Number of entries, expired ones included until they are read. */\n readonly size: number\n /** Removes every entry. */\n clear(): void\n}\n\n/**\n * Creates a store in the memory of the process, least recently used\n * entries removed first. Each instance has its own: use a shared store\n * (Redis…) for several instances.\n */\nexport const createMemoryStore = (\n options: MemoryStoreOptions = {}\n): MemoryStore => {\n const label = 'createMemoryStore()'\n if (typeof options !== 'object' || options === null) {\n throw new TypeError(`${label}: expected an object of options.`)\n }\n for (const key of Object.keys(options)) {\n if (key !== 'maxEntries') {\n throw new TypeError(`${label}: unknown option \"${key}\".`)\n }\n }\n const { maxEntries = 10_000 } = options\n if (!Number.isInteger(maxEntries) || maxEntries < 1) {\n throw new TypeError(`${label}: maxEntries must be a positive integer.`)\n }\n\n // Insertion order of a Map: the first entry is the least recently used.\n const entries = new Map<string, { value: string; expiresAt: number }>()\n const locks = new Map<string, { token: string; expiresAt: number }>()\n\n const read = (key: string) => {\n const entry = entries.get(key)\n if (!entry) return undefined\n entries.delete(key)\n if (entry.expiresAt <= Date.now()) return undefined\n entries.set(key, entry)\n return entry.value\n }\n\n return Object.freeze({\n name: 'memory',\n get size() {\n return entries.size\n },\n get: async (key: string) => read(key),\n getMany: async (keys: readonly string[]) => keys.map(read),\n set: async (key: string, value: string, ttl: number) => {\n entries.delete(key)\n entries.set(key, { value, expiresAt: Date.now() + ttl })\n for (const oldest of entries.keys()) {\n if (entries.size <= maxEntries) break\n entries.delete(oldest)\n }\n },\n delete: async (key: string) => {\n entries.delete(key)\n },\n lock: async (key: string, ttl: number) => {\n const held = locks.get(key)\n if (held && held.expiresAt > Date.now()) return undefined\n const token = crypto.randomUUID()\n locks.set(key, { token, expiresAt: Date.now() + ttl })\n return token\n },\n unlock: async (key: string, token: string) => {\n if (locks.get(key)?.token === token) locks.delete(key)\n },\n clear: () => {\n entries.clear()\n locks.clear()\n },\n })\n}\n","import { toMilliseconds, type Duration } from './duration.js'\nimport {\n CacheStoreError,\n CacheTimeoutError,\n type StoreOperation,\n} from './errors.js'\nimport { keyOf } from './keys.js'\nimport { checkLogger, silentLogger, type CacheLogger } from './logger.js'\nimport { isStore, type CacheStore } from './store.js'\n\n/**\n * A schema that validates cached values when they are read: a Zod schema,\n * or anything with the same `safeParseAsync()`.\n */\nexport interface CacheSchema<T> {\n safeParseAsync(\n value: unknown\n ): Promise<{ success: true; data: T } | { success: false }>\n}\n\ndeclare const untyped: unique symbol\n/** Value type of an entry without schema: given by the loader. */\nexport interface Untyped {\n readonly [untyped]: true\n}\n\ndeclare const entryTypes: unique symbol\n\n/** A kind of cached value, created by `defineCacheEntry()`. */\nexport interface CacheEntry<A extends readonly unknown[], T> {\n readonly name: string\n readonly version: number\n /** Types only. */\n readonly [entryTypes]?: { args: A; value: (value: T) => T }\n}\n\n/** Value of an entry: `unknown` without schema. */\nexport type CacheValue<T> = T extends Untyped ? unknown : T\n\n/**\n * Lifetime and resilience of values: options of an entry, or defaults of\n * the cache.\n */\nexport interface CachePolicy {\n /** Lifetime of the values. Defaults to 1 hour. */\n ttl?: Duration | undefined\n /**\n * Stale-if-error: how long after expiry a value can still be served when\n * the loader fails (or times out). Defaults to 0 (disabled).\n */\n grace?: Duration | undefined\n /**\n * After a failed load served from the grace period, the loader is not\n * called again before this delay: the stale value is served. Defaults to\n * 10 seconds.\n */\n graceBackoff?: Duration | undefined\n /**\n * How long after expiry a value is still served while a background load\n * refreshes it. Defaults to 0 (disabled).\n */\n staleWhileRevalidate?: Duration | undefined\n /**\n * Soft timeout: a load slower than this serves the stale value of the\n * grace period, if any, and goes on in the background.\n */\n timeout?: Duration | undefined\n /**\n * Hard timeout: a load slower than this fails (`CacheTimeoutError`), or\n * serves the stale value of the grace period, if any. It goes on in the\n * background.\n */\n hardTimeout?: Duration | undefined\n /**\n * Distributed single flight: before loading a missing value, takes a\n * lock in the store (Redis…) for at most this duration; the other\n * instances wait for the value meanwhile, then load it themselves.\n * Ignored by stores without locks. Disabled by default.\n */\n lock?: Duration | undefined\n}\n\n/** Options of `defineCacheEntry()`. */\nexport interface CacheEntryOptions<\n A extends readonly unknown[],\n T,\n> extends CachePolicy {\n /**\n * Name of the entry, in keys and logs: letters, digits, `.`, `_` and\n * `-`.\n */\n name: string\n /**\n * Key of the arguments: a string, a number, or a value compared like the\n * arguments of a memo. Defaults to the argument (or the arguments).\n */\n key?: ((...args: A) => unknown) | undefined\n /**\n * Validates cached values when they are read (entries from an older\n * version of the app) and written. JSON turns dates into strings: use\n * `z.coerce.date()`.\n */\n schema?: CacheSchema<T> | undefined\n /**\n * Version of the values, in the key: increase it when their shape\n * changes, to ignore the values written before. Defaults to 1.\n */\n version?: number | undefined\n /**\n * Tags of the arguments: `invalidateTags()` expires every value with one\n * of them.\n */\n tags?: ((...args: A) => readonly string[]) | undefined\n}\n\n/** A policy in milliseconds; undefined when not set. */\ntype Policy = { [K in keyof CachePolicy]: number | undefined }\n\ninterface EntryConfig {\n name: string\n version: number\n key: ((...args: readonly unknown[]) => unknown) | undefined\n schema: CacheSchema<unknown> | undefined\n tags: ((...args: readonly unknown[]) => readonly string[]) | undefined\n policy: Policy\n}\n\nconst entries = new WeakMap<object, EntryConfig>()\n\nconst NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/\nconst POLICY = [\n 'ttl',\n 'grace',\n 'graceBackoff',\n 'staleWhileRevalidate',\n 'timeout',\n 'hardTimeout',\n 'lock',\n] as const\nconst ENTRY_OPTIONS = new Set([\n 'name',\n 'key',\n 'schema',\n 'version',\n 'tags',\n ...POLICY,\n])\n\n/** Reads the durations of a policy (`ttl` and timeouts at least 1 ms). */\nconst policyOf = (label: string, values: Record<string, unknown>): Policy => {\n const policy: Policy = {}\n for (const option of POLICY) {\n const value = values[option]\n if (value === undefined) continue\n policy[option] =\n option === 'ttl' ||\n option === 'timeout' ||\n option === 'hardTimeout' ||\n option === 'lock'\n ? positive(`${label}: ${option}`, value)\n : toMilliseconds(`${label}: ${option}`, value)\n }\n return policy\n}\n\n/**\n * Defines a kind of cached value: its key, its lifetime, and optionally\n * its schema.\n *\n * ```ts\n * export const userCache = defineCacheEntry({\n * name: 'user',\n * key: (id: string) => id,\n * schema: userSchema,\n * ttl: '1h',\n * })\n * ```\n */\nexport function defineCacheEntry<A extends readonly unknown[], T>(\n options: CacheEntryOptions<A, T> & { schema: CacheSchema<T> }\n): CacheEntry<A, T>\nexport function defineCacheEntry<A extends readonly unknown[]>(\n options: CacheEntryOptions<A, never> & { schema?: undefined }\n): CacheEntry<A, Untyped>\nexport function defineCacheEntry<A extends readonly unknown[], T>(\n options: CacheEntryOptions<A, T>\n): CacheEntry<A, T>\nexport function defineCacheEntry(options: unknown): {\n readonly name: string\n readonly version: number\n} {\n const label = 'defineCacheEntry()'\n if (typeof options !== 'object' || options === null) {\n throw new TypeError(`${label}: expected an object of options.`)\n }\n const values = options as Record<string, unknown>\n const { name, key, schema, tags, version = 1 } = values\n if (typeof name !== 'string' || !NAME.test(name)) {\n throw new TypeError(\n `${label}: name must have 1 to 128 letters, digits, \".\", \"_\" or \"-\", starting with a letter or a digit.`\n )\n }\n const named = `defineCacheEntry(\"${name}\")`\n for (const option of Object.keys(values)) {\n if (!ENTRY_OPTIONS.has(option)) {\n throw new TypeError(`${named}: unknown option \"${option}\".`)\n }\n }\n if (key !== undefined && typeof key !== 'function') {\n throw new TypeError(`${named}: key must be a function.`)\n }\n if (\n schema !== undefined &&\n typeof (schema as Partial<CacheSchema<unknown>> | null)?.safeParseAsync !==\n 'function'\n ) {\n throw new TypeError(\n `${named}: schema must have a safeParseAsync() method, like a Zod schema.`\n )\n }\n if (!Number.isInteger(version) || (version as number) < 0) {\n throw new TypeError(`${named}: version must be a non-negative integer.`)\n }\n if (tags !== undefined && typeof tags !== 'function') {\n throw new TypeError(`${named}: tags must be a function.`)\n }\n\n const entry = Object.freeze({ name, version: version as number })\n entries.set(entry, {\n name,\n version: version as number,\n key: key as EntryConfig['key'],\n schema: schema as CacheSchema<unknown> | undefined,\n tags: tags as EntryConfig['tags'],\n policy: policyOf(named, values),\n })\n return entry\n}\n\nconst positive = (label: string, value: unknown) => {\n const ms = toMilliseconds(label, value)\n if (ms < 1) throw new TypeError(`${label} must be at least 1 millisecond.`)\n return ms\n}\n\n/** Turns values into strings for the store, and back. */\nexport interface CacheSerializer {\n serialize(value: unknown): string\n deserialize(text: string): unknown\n}\n\n/** Details of a store failure, for `onError`. */\nexport interface CacheErrorInfo {\n readonly operation: StoreOperation\n /** Key of the store. */\n readonly key: string\n /** Name of the entry. */\n readonly entry: string\n}\n\n/** Something that happened to a value, for `onEvent`. */\nexport interface CacheEvent {\n /**\n * `hit`: fresh value; `miss`: loaded; `stale`: a value past its `ttl`\n * served, for the `reason`.\n */\n readonly type: 'hit' | 'miss' | 'stale'\n readonly reason?: 'revalidate' | 'error' | 'timeout' | 'backoff'\n /** Name of the entry. */\n readonly entry: string\n /** Key of the store. */\n readonly key: string\n}\n\n/** Options of `createCache()`: the policy is the default of the entries. */\nexport interface CacheOptions extends CachePolicy {\n store: CacheStore\n /** Prefix of every key, like the name of the app. */\n prefix?: string | undefined\n /** Defaults to JSON. */\n serializer?: CacheSerializer | undefined\n logger?: CacheLogger | undefined\n /**\n * Called for each store failure, besides the log: metrics, alerts. Its\n * own errors are logged and ignored.\n */\n onError?:\n | ((error: CacheStoreError, info: CacheErrorInfo) => void | Promise<void>)\n | undefined\n /**\n * When the store fails, behaves as a cache miss (reads) or ignores the\n * write, after logging it. Set false to throw a `CacheStoreError`.\n * Defaults to true.\n */\n failOpen?: boolean | undefined\n /** A store operation slower than this counts as a failure. */\n storeTimeout?: Duration | undefined\n /**\n * Called for each hit, miss and stale value served: metrics. Its errors\n * are logged and ignored.\n */\n onEvent?: ((event: CacheEvent) => void) | undefined\n}\n\n/** A cache, created by `createCache()`. */\nexport interface Cache {\n /**\n * The fresh value of the arguments, or undefined when missing, expired\n * or invalidated.\n */\n get<A extends readonly unknown[], T>(\n entry: CacheEntry<A, T>,\n ...args: A\n ): Promise<CacheValue<T> | undefined>\n /** Sets the value of the arguments (the last argument). */\n set<A extends readonly unknown[], T>(\n entry: CacheEntry<A, T>,\n ...argsAndValue: [...A, CacheValue<T>]\n ): Promise<void>\n /** Removes the value of the arguments. */\n delete<A extends readonly unknown[], T>(\n entry: CacheEntry<A, T>,\n ...args: A\n ): Promise<void>\n /**\n * The value of the arguments, loaded by the last argument when missing\n * and cached. Calls made while it loads share the load. Applies the\n * grace period, stale-while-revalidate and timeouts of the entry.\n */\n getOrSet<A extends readonly unknown[], R>(\n entry: CacheEntry<A, Untyped>,\n ...argsAndLoader: [...A, () => R | Promise<R>]\n ): Promise<R>\n getOrSet<A extends readonly unknown[], T>(\n entry: CacheEntry<A, T>,\n ...argsAndLoader: [...A, () => T | Promise<T>]\n ): Promise<T>\n /**\n * Expires every value with one of the tags (they can still be served by\n * the grace period when the loader fails).\n */\n invalidateTags(tags: readonly string[]): Promise<void>\n /** The same cache, logging with another logger. */\n withLogger(logger: CacheLogger): Cache\n}\n\nconst CACHE_OPTIONS = new Set([\n 'store',\n 'prefix',\n 'ttl',\n 'serializer',\n 'logger',\n 'onError',\n 'failOpen',\n 'storeTimeout',\n 'onEvent',\n ...POLICY,\n])\n\nconst jsonSerializer: CacheSerializer = {\n serialize: (value) => JSON.stringify(value),\n deserialize: (text) => JSON.parse(text) as unknown,\n}\n\n/**\n * Stored form of a value: `{ v, c (created), e (expires), d (data),\n * t (token of each tag) }`.\n */\ninterface StoredEntry {\n v: 1\n c: number\n e: number\n d: string\n t?: Record<string, string>\n}\n\nconst parseStored = (text: string): StoredEntry | undefined => {\n try {\n const stored = JSON.parse(text) as Partial<StoredEntry> | null\n if (\n stored?.v === 1 &&\n typeof stored.c === 'number' &&\n typeof stored.e === 'number' &&\n typeof stored.d === 'string' &&\n (stored.t === undefined ||\n (typeof stored.t === 'object' && stored.t !== null))\n ) {\n return stored as StoredEntry\n }\n } catch {\n // Not an entry of this cache.\n }\n return undefined\n}\n\nconst encoder = new TextEncoder()\n\n// Tokens of tags outlive the values that use them: 30 days at least.\nconst TAG_LIFETIME = 30 * 86_400_000\n// While another instance holds the lock, the value is read again so often.\nconst LOCK_POLL = 50\n\n/** SHA-256 of a text, in hexadecimal. */\nconst sha256 = async (text: string) => {\n const digest = await crypto.subtle.digest('SHA-256', encoder.encode(text))\n return [...new Uint8Array(digest)]\n .map((byte) => byte.toString(16).padStart(2, '0'))\n .join('')\n}\n\n/**\n * Creates a cache on a store. Reads and writes fail open by default: a\n * store failure is logged (and passed to `onError`), and the app keeps\n * working without cache.\n *\n * ```ts\n * const cache = createCache({ store: createMemoryStore(), prefix: 'app', ttl: '10m' })\n * const user = await cache.getOrSet(userCache, id, () => db.users.findById(id))\n * ```\n */\nexport const createCache = (options: CacheOptions): Cache => {\n const label = 'createCache()'\n if (typeof options !== 'object' || options === null) {\n throw new TypeError(`${label}: expected an object of options.`)\n }\n for (const key of Object.keys(options)) {\n if (!CACHE_OPTIONS.has(key)) {\n throw new TypeError(`${label}: unknown option \"${key}\".`)\n }\n }\n const {\n store,\n prefix,\n serializer = jsonSerializer,\n onError,\n onEvent,\n failOpen = true,\n } = options\n if (!isStore(store)) {\n throw new TypeError(\n `${label}: store must be a cache store (createMemoryStore(), createRedisStore()…).`\n )\n }\n if (prefix !== undefined && (typeof prefix !== 'string' || prefix === '')) {\n throw new TypeError(`${label}: prefix must be a non-empty string.`)\n }\n const defaults = policyOf(\n label,\n options as unknown as Record<string, unknown>\n )\n if (\n typeof serializer?.serialize !== 'function' ||\n typeof serializer.deserialize !== 'function'\n ) {\n throw new TypeError(\n `${label}: serializer must have serialize() and deserialize() methods.`\n )\n }\n if (onError !== undefined && typeof onError !== 'function') {\n throw new TypeError(`${label}: onError must be a function.`)\n }\n if (onEvent !== undefined && typeof onEvent !== 'function') {\n throw new TypeError(`${label}: onEvent must be a function.`)\n }\n if (typeof failOpen !== 'boolean') {\n throw new TypeError(`${label}: failOpen must be a boolean.`)\n }\n const storeTimeout =\n options.storeTimeout === undefined\n ? undefined\n : positive(`${label}: storeTimeout`, options.storeTimeout)\n const rootLogger =\n options.logger === undefined\n ? silentLogger\n : checkLogger(label, options.logger)\n\n // Shared by every logger of the cache: loads in progress, and the end of\n // the backoff of the keys whose loader failed.\n const loading = new Map<string, Promise<unknown>>()\n const backoffs = new Map<string, number>()\n\n const configOf = (method: string, entry: unknown) => {\n const config =\n typeof entry === 'object' && entry !== null\n ? entries.get(entry)\n : undefined\n if (!config) {\n throw new TypeError(\n `cache.${method}(): expected an entry created by defineCacheEntry().`\n )\n }\n return config\n }\n\n /** Policy of an entry, with the defaults of the cache. */\n const policyFor = (config: EntryConfig) => {\n const value = (option: keyof CachePolicy) =>\n config.policy[option] ?? defaults[option]\n return {\n ttl: value('ttl') ?? 3_600_000,\n grace: value('grace') ?? 0,\n graceBackoff: value('graceBackoff') ?? 10_000,\n staleWhileRevalidate: value('staleWhileRevalidate') ?? 0,\n timeout: value('timeout'),\n hardTimeout: value('hardTimeout'),\n lock: value('lock'),\n }\n }\n\n /** A key of the store, hashed when longer than the store accepts. */\n const fit = async (base: string, part: string) => {\n const key = `${base}:${part}`\n return encoder.encode(key).length > (store.maxKeyLength ?? Infinity)\n ? `${base}:#${await sha256(part)}`\n : key\n }\n\n /** Key of the store for the arguments of an entry. */\n const keyFor = async (config: EntryConfig, args: readonly unknown[]) => {\n const raw = config.key\n ? config.key(...args)\n : args.length === 1\n ? args[0]\n : args\n const base = [prefix, config.name, `v${config.version}`]\n .filter((item) => item !== undefined)\n .join(':')\n if (!config.key && args.length === 0) return base\n return fit(\n base,\n typeof raw === 'string' ? raw : keyOf(`Cache entry \"${config.name}\"`, raw)\n )\n }\n\n /** Checks the tags returned by an entry, or given to invalidate. */\n const checkTags = (label: string, tags: unknown) => {\n if (\n !Array.isArray(tags) ||\n !tags.every((tag) => typeof tag === 'string' && tag !== '')\n ) {\n throw new TypeError(\n `${label}: tags must be an array of non-empty strings.`\n )\n }\n return [...new Set(tags as string[])]\n }\n\n /** Keys of the tokens of tags (`_` cannot start the name of an entry). */\n const tagKeysOf = (tags: readonly string[]) => {\n const base = prefix === undefined ? '_tag' : `${prefix}:_tag`\n return Promise.all(\n tags.map(async (tag) => ({ tag, key: await fit(base, tag) }))\n )\n }\n\n const create = (logger: CacheLogger): Cache => {\n const describe = (error: unknown) =>\n error instanceof Error ? error.message : String(error)\n\n /** Runs a store operation: failures are logged, and fail open. */\n const guard = async <R>(\n operation: StoreOperation,\n key: string,\n entry: string,\n run: () => Promise<R>,\n fallback: R\n ): Promise<R> => {\n let timer: ReturnType<typeof setTimeout> | undefined\n try {\n const running = run()\n return storeTimeout === undefined\n ? await running\n : await Promise.race([\n running,\n new Promise<never>((_, reject) => {\n timer = setTimeout(\n () =>\n reject(\n new Error(`no answer within ${storeTimeout} milliseconds`)\n ),\n storeTimeout\n )\n }),\n ])\n } catch (cause) {\n const error = new CacheStoreError(store.name, operation, key, {\n cause,\n })\n const data = {\n store: store.name,\n operation,\n key,\n entry,\n error: error.message,\n }\n // A failed deletion or invalidation may leave outdated values.\n if (operation === 'delete' || operation === 'invalidate') {\n logger.error(data, 'Cache store failed')\n } else {\n logger.warn(data, 'Cache store failed')\n }\n if (onError) {\n try {\n await onError(error, { operation, key, entry })\n } catch (hookError) {\n logger.error(\n { error: describe(hookError) },\n 'Cache onError hook failed'\n )\n }\n }\n if (!failOpen) throw error\n return fallback\n } finally {\n clearTimeout(timer)\n }\n }\n\n /** Reports an event to the log (debug) and `onEvent`. */\n const report = (event: CacheEvent) => {\n const { type, reason, entry, key } = event\n logger.debug?.(\n { entry, key, ...(reason ? { reason } : {}) },\n type === 'hit'\n ? 'Cache hit'\n : type === 'miss'\n ? 'Cache miss'\n : 'Cache served a stale value'\n )\n if (!onEvent) return\n try {\n onEvent(event)\n } catch (error) {\n logger.error({ error: describe(error) }, 'Cache onEvent hook failed')\n }\n }\n\n /**\n * Current tokens of tags (undefined for a missing tag), or undefined\n * when the store cannot read them.\n */\n const tokensOf = async (\n entry: string,\n tagKeys: readonly { tag: string; key: string }[]\n ) => {\n const keys = tagKeys.map(({ key }) => key)\n const values = await guard<(string | undefined)[] | undefined>(\n 'get',\n keys.join(' '),\n entry,\n () =>\n store.getMany\n ? store.getMany(keys)\n : Promise.all(keys.map((key) => store.get(key))),\n undefined\n )\n return values?.map((token, index) => ({\n tag: (tagKeys[index] as { tag: string }).tag,\n token,\n }))\n }\n\n /** Deserializes and validates a value: `{ value }`, or undefined. */\n const decode = async (config: EntryConfig, key: string, data: string) => {\n let value: unknown\n try {\n value = serializer.deserialize(data)\n } catch (error) {\n logger.warn(\n { entry: config.name, key, error: describe(error) },\n 'Cache value cannot be deserialized'\n )\n return undefined\n }\n if (!config.schema) return { value }\n const result = await config.schema.safeParseAsync(value)\n return result.success ? { value: result.data } : undefined\n }\n\n /**\n * Reads a stored value: its value, expiry, and whether its tags are\n * still valid. Undefined when missing, invalid, or past every stale\n * window.\n */\n const lookup = async (\n config: EntryConfig,\n policy: ReturnType<typeof policyFor>,\n key: string\n ) => {\n const text = await guard(\n 'get',\n key,\n config.name,\n () => store.get(key),\n undefined\n )\n if (text === undefined) return undefined\n const stored = parseStored(text)\n const now = Date.now()\n if (\n !stored ||\n stored.e + Math.max(policy.grace, policy.staleWhileRevalidate) <= now\n ) {\n return undefined\n }\n const decoded = await decode(config, key, stored.d)\n if (!decoded) return undefined\n const tags = Object.entries(stored.t ?? {})\n let tagsValid = true\n if (tags.length > 0) {\n const current = await tokensOf(\n config.name,\n await tagKeysOf(tags.map(([tag]) => tag))\n )\n // Unreadable tokens: the value may have been invalidated.\n tagsValid =\n current?.every(({ tag, token }) => token === stored.t?.[tag]) ?? false\n }\n return {\n value: decoded.value,\n expiresAt: stored.e,\n fresh: stored.e > now && tagsValid,\n tagsValid,\n }\n }\n\n /**\n * Writes a value, after checking that it reads back: returns the value\n * as read later, or undefined when it cannot be cached.\n */\n const write = async (\n config: EntryConfig,\n key: string,\n args: readonly unknown[],\n value: unknown\n ): Promise<{ value: unknown } | undefined> => {\n if (value === undefined) return undefined\n let data: string\n try {\n data = serializer.serialize(value)\n if (typeof data !== 'string') throw new TypeError('not a string')\n } catch (error) {\n logger.warn(\n { entry: config.name, key, error: describe(error) },\n 'Cache value cannot be serialized'\n )\n return undefined\n }\n const checked = config.schema\n ? await decode(config, key, data)\n : { value }\n if (!checked) {\n logger.warn(\n { entry: config.name, key },\n 'Cache value does not match the schema of the entry: not cached'\n )\n return undefined\n }\n const policy = policyFor(config)\n const lifetime =\n policy.ttl + Math.max(policy.grace, policy.staleWhileRevalidate)\n const now = Date.now()\n const stored: StoredEntry = {\n v: 1,\n c: now,\n e: now + policy.ttl,\n d: data,\n }\n if (config.tags) {\n const tags = checkTags(\n `defineCacheEntry(\"${config.name}\")`,\n config.tags(...args)\n )\n if (tags.length > 0) {\n const tagKeys = await tagKeysOf(tags)\n const current = await tokensOf(config.name, tagKeys)\n // Unreadable tokens: creating new ones would invalidate every\n // value of these tags, so the value is not cached.\n if (!current) return checked\n stored.t = {}\n for (const [index, { tag, token }] of current.entries()) {\n if (token !== undefined) {\n stored.t[tag] = token\n continue\n }\n // First value with this tag: its token is created.\n const created = crypto.randomUUID()\n const tagKey = (tagKeys[index] as { key: string }).key\n await guard(\n 'set',\n tagKey,\n config.name,\n () =>\n store.set(tagKey, created, Math.max(lifetime, TAG_LIFETIME)),\n undefined\n )\n stored.t[tag] = created\n }\n }\n }\n await guard(\n 'set',\n key,\n config.name,\n () => store.set(key, JSON.stringify(stored), lifetime),\n undefined\n )\n return checked\n }\n\n /**\n * Loads and writes a value, once per key at a time, and once for every\n * instance with the `lock` option.\n */\n const load = (\n config: EntryConfig,\n key: string,\n args: readonly unknown[],\n loader: () => unknown\n ) => {\n const running = loading.get(key)\n if (running) return running\n const loaded = (async () => {\n const policy = policyFor(config)\n const lockKey = `${key}:lock`\n let token: string | undefined\n if (policy.lock !== undefined && store.lock) {\n const lock = store.lock.bind(store)\n const ttl = policy.lock\n // null: the store failed, load without lock.\n const taken = await guard<string | undefined | null>(\n 'lock',\n lockKey,\n config.name,\n () => lock(lockKey, ttl),\n null\n )\n if (taken === undefined) {\n // Another instance loads it: wait for its value.\n const until = Date.now() + ttl\n while (Date.now() < until) {\n await new Promise((resolve) => setTimeout(resolve, LOCK_POLL))\n const found = await lookup(config, policy, key)\n if (found?.fresh) return found.value\n }\n logger.warn(\n { entry: config.name, key, lock: ttl },\n 'Cache lock not released in time: loading anyway'\n )\n } else if (taken !== null) {\n token = taken\n }\n }\n try {\n const value: unknown = await loader()\n const written = await write(config, key, args, value)\n backoffs.delete(key)\n return written ? written.value : value\n } finally {\n if (token !== undefined && store.unlock) {\n const unlock = store.unlock.bind(store)\n const held = token\n await guard(\n 'unlock',\n lockKey,\n config.name,\n () => unlock(lockKey, held),\n undefined\n )\n }\n }\n })()\n loading.set(key, loaded)\n void loaded.then(\n () => loading.delete(key),\n () => loading.delete(key)\n )\n return loaded\n }\n\n /** Logs the failure of a load that nobody waits for anymore. */\n const inBackground = (\n config: EntryConfig,\n key: string,\n loaded: Promise<unknown>\n ) => {\n loaded.catch((error: unknown) => {\n logger.warn(\n { entry: config.name, key, error: describe(error) },\n 'Cache refresh failed'\n )\n })\n }\n\n /** Rejects after `ms` (never when undefined), until cleared. */\n const deadline = (ms: number | undefined, error: () => unknown) => {\n let timer: ReturnType<typeof setTimeout> | undefined\n const promise = new Promise<never>((_, reject) => {\n if (ms !== undefined) timer = setTimeout(() => reject(error()), ms)\n })\n return { promise, clear: () => clearTimeout(timer) }\n }\n\n const cache: Cache = {\n get: (async (entry: unknown, ...args: unknown[]) => {\n const config = configOf('get', entry)\n const key = await keyFor(config, args)\n const found = await lookup(config, policyFor(config), key)\n report({ type: found?.fresh ? 'hit' : 'miss', entry: config.name, key })\n return found?.fresh ? found.value : undefined\n }) as Cache['get'],\n\n set: (async (entry: unknown, ...argsAndValue: unknown[]) => {\n const config = configOf('set', entry)\n const args = argsAndValue.slice(0, -1)\n const key = await keyFor(config, args)\n await write(config, key, args, argsAndValue.at(-1))\n }) as Cache['set'],\n\n delete: (async (entry: unknown, ...args: unknown[]) => {\n const config = configOf('delete', entry)\n const key = await keyFor(config, args)\n await guard(\n 'delete',\n key,\n config.name,\n () => store.delete(key),\n undefined\n )\n }) as Cache['delete'],\n\n getOrSet: (async (entry: unknown, ...argsAndLoader: unknown[]) => {\n const config = configOf('getOrSet', entry)\n const loader = argsAndLoader.at(-1)\n if (typeof loader !== 'function') {\n throw new TypeError(\n 'cache.getOrSet(): the last argument must be the function that loads the value.'\n )\n }\n const args = argsAndLoader.slice(0, -1)\n const key = await keyFor(config, args)\n const policy = policyFor(config)\n const found = await lookup(config, policy, key)\n const event = { entry: config.name, key }\n if (found?.fresh) {\n report({ type: 'hit', ...event })\n return found.value\n }\n const now = Date.now()\n\n // Stale-while-revalidate: serve, and refresh in the background.\n if (\n found?.tagsValid &&\n found.expiresAt + policy.staleWhileRevalidate > now\n ) {\n report({ type: 'stale', reason: 'revalidate', ...event })\n inBackground(\n config,\n key,\n load(config, key, args, loader as () => unknown)\n )\n return found.value\n }\n\n // Grace period: stale value served if the loader fails. Without it,\n // an invalidated value is never served.\n const stale =\n found && policy.grace > 0 && found.expiresAt + policy.grace > now\n ? found\n : undefined\n if (stale && (backoffs.get(key) ?? 0) > now) {\n report({ type: 'stale', reason: 'backoff', ...event })\n return stale.value\n }\n\n report({ type: 'miss', ...event })\n const loaded = load(config, key, args, loader as () => unknown)\n const slow = Symbol('slow')\n const soft = deadline(stale ? policy.timeout : undefined, () => slow)\n const hard = deadline(\n policy.hardTimeout,\n () =>\n new CacheTimeoutError(\n config.name,\n key,\n policy.hardTimeout as number\n )\n )\n try {\n return await Promise.race([loaded, soft.promise, hard.promise])\n } catch (error) {\n // The load goes on, and caches its value.\n if (error === slow || error instanceof CacheTimeoutError) {\n inBackground(config, key, loaded)\n }\n if (!stale) throw error\n if (error === slow) {\n logger.warn(\n { ...event, timeout: policy.timeout },\n 'Cache served a stale value: the loader is slow'\n )\n report({ type: 'stale', reason: 'timeout', ...event })\n return stale.value\n }\n backoffs.set(key, Date.now() + policy.graceBackoff)\n logger.warn(\n { ...event, error: describe(error) },\n 'Cache served a stale value: the loader failed'\n )\n report({ type: 'stale', reason: 'error', ...event })\n return stale.value\n } finally {\n soft.clear()\n hard.clear()\n }\n }) as Cache['getOrSet'],\n\n invalidateTags: async (tags) => {\n const tagKeys = await tagKeysOf(\n checkTags('cache.invalidateTags()', tags)\n )\n await Promise.all(\n tagKeys.map(({ tag, key }) =>\n guard(\n 'invalidate',\n key,\n `tag:${tag}`,\n () => store.set(key, crypto.randomUUID(), TAG_LIFETIME),\n undefined\n )\n )\n )\n logger.debug?.({ tags }, 'Cache tags invalidated')\n },\n\n withLogger: (other) => create(checkLogger('withLogger()', other)),\n }\n return Object.freeze(cache)\n }\n\n return create(rootLogger)\n}\n"],"mappings":"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","names":["entries","label"]}
|
|
1
|
+
{"version":3,"sources":["../src/logger.ts","../src/store.ts","../src/cache.ts"],"sourcesContent":["/**\n * Logger of the cache. A Dotpress or Pino logger fits; `cachePlugin` uses\n * the logger of the request.\n */\nexport interface CacheLogger {\n debug?(data: object, message: string): void\n info(data: object, message: string): void\n warn(data: object, message: string): void\n error(data: object, message: string): void\n}\n\nconst noop = () => undefined\n\n/** Logger that discards everything. */\nexport const silentLogger: CacheLogger = {\n debug: noop,\n info: noop,\n warn: noop,\n error: noop,\n}\n\n/** Checks a logger option. */\nexport const checkLogger = (label: string, logger: unknown) => {\n for (const level of ['info', 'warn', 'error'] as const) {\n if (\n typeof (logger as Record<string, unknown> | null)?.[level] !== 'function'\n ) {\n throw new TypeError(\n `${label}: logger must have info(), warn() and error() methods.`\n )\n }\n }\n return logger as CacheLogger\n}\n","/**\n * Where the cache keeps its entries: Redis, Upstash, Cloudflare KV,\n * memory… Values are strings, serialized by the cache.\n */\nexport interface CacheStore {\n /** Name of the store, in logs and errors. */\n readonly name: string\n /**\n * Longest key accepted, in bytes (UTF-8). Longer keys are hashed by the\n * cache. Undefined without limit.\n */\n readonly maxKeyLength?: number | undefined\n /** Value of a key, or undefined when it is missing or expired. */\n get(key: string): Promise<string | undefined>\n /**\n * Values of several keys, in their order: one round trip for the tags of\n * an entry. Optional: the cache calls `get` for each key without it.\n */\n getMany?(keys: readonly string[]): Promise<(string | undefined)[]>\n /** Sets a value that expires after `ttl` milliseconds (at least 1). */\n set(key: string, value: string, ttl: number): Promise<void>\n /** Removes a key; missing keys are ignored. */\n delete(key: string): Promise<void>\n /**\n * Takes a lock for `ttl` milliseconds: returns its token, or undefined\n * when another holder has it. Optional: the `lock` option of the cache\n * needs it.\n */\n lock?(key: string, ttl: number): Promise<string | undefined>\n /** Releases a lock, if the token still holds it. */\n unlock?(key: string, token: string): Promise<void>\n}\n\n/** Checks a store option. */\nexport const isStore = (value: unknown): value is CacheStore => {\n const store = value as Partial<CacheStore> | null\n return (\n typeof store?.name === 'string' &&\n typeof store.get === 'function' &&\n typeof store.set === 'function' &&\n typeof store.delete === 'function'\n )\n}\n\n/** Options of `createMemoryStore()`. */\nexport interface MemoryStoreOptions {\n /**\n * Entries kept at most: the least recently used ones are removed\n * first. Defaults to 10,000.\n */\n maxEntries?: number | undefined\n}\n\n/** Store in the memory of the process: for one instance, and for tests. */\nexport interface MemoryStore extends CacheStore {\n /** Number of entries, expired ones included until they are read. */\n readonly size: number\n /** Removes every entry. */\n clear(): void\n}\n\n/**\n * Creates a store in the memory of the process, least recently used\n * entries removed first. Each instance has its own: use a shared store\n * (Redis…) for several instances.\n */\nexport const createMemoryStore = (\n options: MemoryStoreOptions = {}\n): MemoryStore => {\n const label = 'createMemoryStore()'\n if (typeof options !== 'object' || options === null) {\n throw new TypeError(`${label}: expected an object of options.`)\n }\n for (const key of Object.keys(options)) {\n if (key !== 'maxEntries') {\n throw new TypeError(`${label}: unknown option \"${key}\".`)\n }\n }\n const { maxEntries = 10_000 } = options\n if (!Number.isInteger(maxEntries) || maxEntries < 1) {\n throw new TypeError(`${label}: maxEntries must be a positive integer.`)\n }\n\n // Insertion order of a Map: the first entry is the least recently used.\n const entries = new Map<string, { value: string; expiresAt: number }>()\n const locks = new Map<string, { token: string; expiresAt: number }>()\n\n const read = (key: string) => {\n const entry = entries.get(key)\n if (!entry) return undefined\n entries.delete(key)\n if (entry.expiresAt <= Date.now()) return undefined\n entries.set(key, entry)\n return entry.value\n }\n\n return Object.freeze({\n name: 'memory',\n get size() {\n return entries.size\n },\n get: async (key: string) => read(key),\n getMany: async (keys: readonly string[]) => keys.map(read),\n set: async (key: string, value: string, ttl: number) => {\n entries.delete(key)\n entries.set(key, { value, expiresAt: Date.now() + ttl })\n for (const oldest of entries.keys()) {\n if (entries.size <= maxEntries) break\n entries.delete(oldest)\n }\n },\n delete: async (key: string) => {\n entries.delete(key)\n },\n lock: async (key: string, ttl: number) => {\n const held = locks.get(key)\n if (held && held.expiresAt > Date.now()) return undefined\n const token = crypto.randomUUID()\n locks.set(key, { token, expiresAt: Date.now() + ttl })\n return token\n },\n unlock: async (key: string, token: string) => {\n if (locks.get(key)?.token === token) locks.delete(key)\n },\n clear: () => {\n entries.clear()\n locks.clear()\n },\n })\n}\n","import { toMilliseconds, type Duration } from './duration.js'\nimport {\n CacheStoreError,\n CacheTimeoutError,\n type StoreOperation,\n} from './errors.js'\nimport { keyOf } from './keys.js'\nimport { checkLogger, silentLogger, type CacheLogger } from './logger.js'\nimport { isStore, type CacheStore } from './store.js'\n\n/**\n * A schema that validates cached values when they are read: a Zod schema,\n * or anything with the same `safeParseAsync()`.\n */\nexport interface CacheSchema<T> {\n safeParseAsync(\n value: unknown\n ): Promise<{ success: true; data: T } | { success: false }>\n}\n\ndeclare const untyped: unique symbol\n/** Value type of an entry without schema: given by the loader. */\nexport interface Untyped {\n readonly [untyped]: true\n}\n\ndeclare const entryTypes: unique symbol\n\n/** A kind of cached value, created by `defineCacheEntry()`. */\nexport interface CacheEntry<A extends readonly unknown[], T> {\n readonly name: string\n readonly version: number\n /** Types only. */\n readonly [entryTypes]?: { args: A; value: (value: T) => T }\n}\n\n/** Value of an entry: `unknown` without schema. */\nexport type CacheValue<T> = T extends Untyped ? unknown : T\n\n/**\n * Lifetime and resilience of values: options of an entry, or defaults of\n * the cache.\n */\nexport interface CachePolicy {\n /** Lifetime of the values. Defaults to 1 hour. */\n ttl?: Duration | undefined\n /**\n * Stale-if-error: how long after expiry a value can still be served when\n * the loader fails (or times out). Defaults to 0 (disabled).\n */\n grace?: Duration | undefined\n /**\n * After a failed load served from the grace period, the loader is not\n * called again before this delay: the stale value is served. Defaults to\n * 10 seconds.\n */\n graceBackoff?: Duration | undefined\n /**\n * How long after expiry a value is still served while a background load\n * refreshes it. Defaults to 0 (disabled).\n */\n staleWhileRevalidate?: Duration | undefined\n /**\n * Soft timeout: a load slower than this serves the stale value of the\n * grace period, if any, and goes on in the background.\n */\n timeout?: Duration | undefined\n /**\n * Hard timeout: a load slower than this fails (`CacheTimeoutError`), or\n * serves the stale value of the grace period, if any. It goes on in the\n * background.\n */\n hardTimeout?: Duration | undefined\n /**\n * Distributed single flight: before loading a missing value, takes a\n * lock in the store (Redis…) for at most this duration; the other\n * instances wait for the value meanwhile, then load it themselves.\n * Ignored by stores without locks. Disabled by default.\n */\n lock?: Duration | undefined\n}\n\n/** Options of `defineCacheEntry()`. */\nexport interface CacheEntryOptions<\n A extends readonly unknown[],\n T,\n> extends CachePolicy {\n /**\n * Name of the entry, in keys and logs: letters, digits, `.`, `_` and\n * `-`.\n */\n name: string\n /**\n * Key of the arguments: a string, a number, or a value compared like the\n * arguments of a memo. Defaults to the argument (or the arguments).\n */\n key?: ((...args: A) => unknown) | undefined\n /**\n * Validates cached values when they are read (entries from an older\n * version of the app) and written. JSON turns dates into strings: use\n * `z.coerce.date()`.\n */\n schema?: CacheSchema<T> | undefined\n /**\n * Version of the values, in the key: increase it when their shape\n * changes, to ignore the values written before. Defaults to 1.\n */\n version?: number | undefined\n /**\n * Tags of the arguments: `invalidateTags()` expires every value with one\n * of them.\n */\n tags?: ((...args: A) => readonly string[]) | undefined\n}\n\n/** A policy in milliseconds; undefined when not set. */\ntype Policy = { [K in keyof CachePolicy]: number | undefined }\n\ninterface EntryConfig {\n name: string\n version: number\n key: ((...args: readonly unknown[]) => unknown) | undefined\n schema: CacheSchema<unknown> | undefined\n tags: ((...args: readonly unknown[]) => readonly string[]) | undefined\n policy: Policy\n}\n\nconst entries = new WeakMap<object, EntryConfig>()\n\nconst NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/\nconst POLICY = [\n 'ttl',\n 'grace',\n 'graceBackoff',\n 'staleWhileRevalidate',\n 'timeout',\n 'hardTimeout',\n 'lock',\n] as const\nconst ENTRY_OPTIONS = new Set([\n 'name',\n 'key',\n 'schema',\n 'version',\n 'tags',\n ...POLICY,\n])\n\n/** Reads the durations of a policy (`ttl` and timeouts at least 1 ms). */\nconst policyOf = (label: string, values: Record<string, unknown>): Policy => {\n const policy: Policy = {}\n for (const option of POLICY) {\n const value = values[option]\n if (value === undefined) continue\n policy[option] =\n option === 'ttl' ||\n option === 'timeout' ||\n option === 'hardTimeout' ||\n option === 'lock'\n ? positive(`${label}: ${option}`, value)\n : toMilliseconds(`${label}: ${option}`, value)\n }\n return policy\n}\n\n/**\n * Defines a kind of cached value: its key, its lifetime, and optionally\n * its schema.\n *\n * ```ts\n * export const userCache = defineCacheEntry({\n * name: 'user',\n * key: (id: string) => id,\n * schema: userSchema,\n * ttl: '1h',\n * })\n * ```\n */\nexport function defineCacheEntry<A extends readonly unknown[], T>(\n options: CacheEntryOptions<A, T> & { schema: CacheSchema<T> }\n): CacheEntry<A, T>\nexport function defineCacheEntry<A extends readonly unknown[]>(\n options: CacheEntryOptions<A, never> & { schema?: undefined }\n): CacheEntry<A, Untyped>\nexport function defineCacheEntry<A extends readonly unknown[], T>(\n options: CacheEntryOptions<A, T>\n): CacheEntry<A, T>\nexport function defineCacheEntry(options: unknown): {\n readonly name: string\n readonly version: number\n} {\n const label = 'defineCacheEntry()'\n if (typeof options !== 'object' || options === null) {\n throw new TypeError(`${label}: expected an object of options.`)\n }\n const values = options as Record<string, unknown>\n const { name, key, schema, tags, version = 1 } = values\n if (typeof name !== 'string' || !NAME.test(name)) {\n throw new TypeError(\n `${label}: name must have 1 to 128 letters, digits, \".\", \"_\" or \"-\", starting with a letter or a digit.`\n )\n }\n const named = `defineCacheEntry(\"${name}\")`\n for (const option of Object.keys(values)) {\n if (!ENTRY_OPTIONS.has(option)) {\n throw new TypeError(`${named}: unknown option \"${option}\".`)\n }\n }\n if (key !== undefined && typeof key !== 'function') {\n throw new TypeError(`${named}: key must be a function.`)\n }\n if (\n schema !== undefined &&\n typeof (schema as Partial<CacheSchema<unknown>> | null)?.safeParseAsync !==\n 'function'\n ) {\n throw new TypeError(\n `${named}: schema must have a safeParseAsync() method, like a Zod schema.`\n )\n }\n if (!Number.isInteger(version) || (version as number) < 0) {\n throw new TypeError(`${named}: version must be a non-negative integer.`)\n }\n if (tags !== undefined && typeof tags !== 'function') {\n throw new TypeError(`${named}: tags must be a function.`)\n }\n\n const entry = Object.freeze({ name, version: version as number })\n entries.set(entry, {\n name,\n version: version as number,\n key: key as EntryConfig['key'],\n schema: schema as CacheSchema<unknown> | undefined,\n tags: tags as EntryConfig['tags'],\n policy: policyOf(named, values),\n })\n return entry\n}\n\nconst positive = (label: string, value: unknown) => {\n const ms = toMilliseconds(label, value)\n if (ms < 1) throw new TypeError(`${label} must be at least 1 millisecond.`)\n return ms\n}\n\n/** Turns values into strings for the store, and back. */\nexport interface CacheSerializer {\n serialize(value: unknown): string\n deserialize(text: string): unknown\n}\n\n/** Details of a store failure, for `onError`. */\nexport interface CacheErrorInfo {\n readonly operation: StoreOperation\n /** Key of the store. */\n readonly key: string\n /** Name of the entry. */\n readonly entry: string\n}\n\n/** Something that happened to a value, for `onEvent`. */\nexport interface CacheEvent {\n /**\n * `hit`: fresh value; `miss`: loaded; `stale`: a value past its `ttl`\n * served, for the `reason`.\n */\n readonly type: 'hit' | 'miss' | 'stale'\n readonly reason?: 'revalidate' | 'error' | 'timeout' | 'backoff'\n /** Name of the entry. */\n readonly entry: string\n /** Key of the store. */\n readonly key: string\n}\n\n/** Options of `createCache()`: the policy is the default of the entries. */\nexport interface CacheOptions extends CachePolicy {\n store: CacheStore\n /** Prefix of every key, like the name of the app. */\n prefix?: string | undefined\n /** Defaults to JSON. */\n serializer?: CacheSerializer | undefined\n logger?: CacheLogger | undefined\n /**\n * Called for each store failure, besides the log: metrics, alerts. Its\n * own errors are logged and ignored.\n */\n onError?:\n | ((error: CacheStoreError, info: CacheErrorInfo) => void | Promise<void>)\n | undefined\n /**\n * When the store fails, behaves as a cache miss (reads) or ignores the\n * write, after logging it. Set false to throw a `CacheStoreError`.\n * Defaults to true.\n */\n failOpen?: boolean | undefined\n /** A store operation slower than this counts as a failure. */\n storeTimeout?: Duration | undefined\n /**\n * Called for each hit, miss and stale value served: metrics. Its errors\n * are logged and ignored.\n */\n onEvent?: ((event: CacheEvent) => void) | undefined\n}\n\n/** A cache, created by `createCache()`. */\nexport interface Cache {\n /**\n * The fresh value of the arguments, or undefined when missing, expired\n * or invalidated.\n */\n get<A extends readonly unknown[], T>(\n entry: CacheEntry<A, T>,\n ...args: A\n ): Promise<CacheValue<T> | undefined>\n /** Sets the value of the arguments (the last argument). */\n set<A extends readonly unknown[], T>(\n entry: CacheEntry<A, T>,\n ...argsAndValue: [...A, CacheValue<T>]\n ): Promise<void>\n /** Removes the value of the arguments. */\n delete<A extends readonly unknown[], T>(\n entry: CacheEntry<A, T>,\n ...args: A\n ): Promise<void>\n /**\n * The value of the arguments, loaded by the last argument when missing\n * and cached. Calls made while it loads share the load. Applies the\n * grace period, stale-while-revalidate and timeouts of the entry.\n *\n * A loader may return `undefined` (not found): it is not cached, and\n * `getOrSet` returns it.\n */\n getOrSet<A extends readonly unknown[], R>(\n entry: CacheEntry<A, Untyped>,\n ...argsAndLoader: [...A, () => R | Promise<R>]\n ): Promise<R>\n getOrSet<A extends readonly unknown[], T, R extends T | undefined = T>(\n entry: CacheEntry<A, T>,\n ...argsAndLoader: [...A, () => R | Promise<R>]\n ): Promise<[R] extends [T] ? T : T | undefined>\n /**\n * Expires every value with one of the tags (they can still be served by\n * the grace period when the loader fails).\n */\n invalidateTags(tags: readonly string[]): Promise<void>\n /** The same cache, logging with another logger. */\n withLogger(logger: CacheLogger): Cache\n}\n\nconst CACHE_OPTIONS = new Set([\n 'store',\n 'prefix',\n 'ttl',\n 'serializer',\n 'logger',\n 'onError',\n 'failOpen',\n 'storeTimeout',\n 'onEvent',\n ...POLICY,\n])\n\nconst jsonSerializer: CacheSerializer = {\n serialize: (value) => JSON.stringify(value),\n deserialize: (text) => JSON.parse(text) as unknown,\n}\n\n/**\n * Stored form of a value: `{ v, c (created), e (expires), d (data),\n * t (token of each tag) }`.\n */\ninterface StoredEntry {\n v: 1\n c: number\n e: number\n d: string\n t?: Record<string, string>\n}\n\nconst parseStored = (text: string): StoredEntry | undefined => {\n try {\n const stored = JSON.parse(text) as Partial<StoredEntry> | null\n if (\n stored?.v === 1 &&\n typeof stored.c === 'number' &&\n typeof stored.e === 'number' &&\n typeof stored.d === 'string' &&\n (stored.t === undefined ||\n (typeof stored.t === 'object' && stored.t !== null))\n ) {\n return stored as StoredEntry\n }\n } catch {\n // Not an entry of this cache.\n }\n return undefined\n}\n\nconst encoder = new TextEncoder()\n\n// Tokens of tags outlive the values that use them: 30 days at least.\nconst TAG_LIFETIME = 30 * 86_400_000\n// While another instance holds the lock, the value is read again so often.\nconst LOCK_POLL = 50\n\n/** SHA-256 of a text, in hexadecimal. */\nconst sha256 = async (text: string) => {\n const digest = await crypto.subtle.digest('SHA-256', encoder.encode(text))\n return [...new Uint8Array(digest)]\n .map((byte) => byte.toString(16).padStart(2, '0'))\n .join('')\n}\n\n/**\n * Creates a cache on a store. Reads and writes fail open by default: a\n * store failure is logged (and passed to `onError`), and the app keeps\n * working without cache.\n *\n * ```ts\n * const cache = createCache({ store: createMemoryStore(), prefix: 'app', ttl: '10m' })\n * const user = await cache.getOrSet(userCache, id, () => db.users.findById(id))\n * ```\n */\nexport const createCache = (options: CacheOptions): Cache => {\n const label = 'createCache()'\n if (typeof options !== 'object' || options === null) {\n throw new TypeError(`${label}: expected an object of options.`)\n }\n for (const key of Object.keys(options)) {\n if (!CACHE_OPTIONS.has(key)) {\n throw new TypeError(`${label}: unknown option \"${key}\".`)\n }\n }\n const {\n store,\n prefix,\n serializer = jsonSerializer,\n onError,\n onEvent,\n failOpen = true,\n } = options\n if (!isStore(store)) {\n throw new TypeError(\n `${label}: store must be a cache store (createMemoryStore(), createRedisStore()…).`\n )\n }\n if (prefix !== undefined && (typeof prefix !== 'string' || prefix === '')) {\n throw new TypeError(`${label}: prefix must be a non-empty string.`)\n }\n const defaults = policyOf(\n label,\n options as unknown as Record<string, unknown>\n )\n if (\n typeof serializer?.serialize !== 'function' ||\n typeof serializer.deserialize !== 'function'\n ) {\n throw new TypeError(\n `${label}: serializer must have serialize() and deserialize() methods.`\n )\n }\n if (onError !== undefined && typeof onError !== 'function') {\n throw new TypeError(`${label}: onError must be a function.`)\n }\n if (onEvent !== undefined && typeof onEvent !== 'function') {\n throw new TypeError(`${label}: onEvent must be a function.`)\n }\n if (typeof failOpen !== 'boolean') {\n throw new TypeError(`${label}: failOpen must be a boolean.`)\n }\n const storeTimeout =\n options.storeTimeout === undefined\n ? undefined\n : positive(`${label}: storeTimeout`, options.storeTimeout)\n const rootLogger =\n options.logger === undefined\n ? silentLogger\n : checkLogger(label, options.logger)\n\n // Shared by every logger of the cache: loads in progress, and the end of\n // the backoff of the keys whose loader failed.\n const loading = new Map<string, Promise<unknown>>()\n const backoffs = new Map<string, number>()\n\n const configOf = (method: string, entry: unknown) => {\n const config =\n typeof entry === 'object' && entry !== null\n ? entries.get(entry)\n : undefined\n if (!config) {\n throw new TypeError(\n `cache.${method}(): expected an entry created by defineCacheEntry().`\n )\n }\n return config\n }\n\n /** Policy of an entry, with the defaults of the cache. */\n const policyFor = (config: EntryConfig) => {\n const value = (option: keyof CachePolicy) =>\n config.policy[option] ?? defaults[option]\n return {\n ttl: value('ttl') ?? 3_600_000,\n grace: value('grace') ?? 0,\n graceBackoff: value('graceBackoff') ?? 10_000,\n staleWhileRevalidate: value('staleWhileRevalidate') ?? 0,\n timeout: value('timeout'),\n hardTimeout: value('hardTimeout'),\n lock: value('lock'),\n }\n }\n\n /** A key of the store, hashed when longer than the store accepts. */\n const fit = async (base: string, part: string) => {\n const key = `${base}:${part}`\n return encoder.encode(key).length > (store.maxKeyLength ?? Infinity)\n ? `${base}:#${await sha256(part)}`\n : key\n }\n\n /** Key of the store for the arguments of an entry. */\n const keyFor = async (config: EntryConfig, args: readonly unknown[]) => {\n const raw = config.key\n ? config.key(...args)\n : args.length === 1\n ? args[0]\n : args\n const base = [prefix, config.name, `v${config.version}`]\n .filter((item) => item !== undefined)\n .join(':')\n if (!config.key && args.length === 0) return base\n return fit(\n base,\n typeof raw === 'string' ? raw : keyOf(`Cache entry \"${config.name}\"`, raw)\n )\n }\n\n /** Checks the tags returned by an entry, or given to invalidate. */\n const checkTags = (label: string, tags: unknown) => {\n if (\n !Array.isArray(tags) ||\n !tags.every((tag) => typeof tag === 'string' && tag !== '')\n ) {\n throw new TypeError(\n `${label}: tags must be an array of non-empty strings.`\n )\n }\n return [...new Set(tags as string[])]\n }\n\n /** Keys of the tokens of tags (`_` cannot start the name of an entry). */\n const tagKeysOf = (tags: readonly string[]) => {\n const base = prefix === undefined ? '_tag' : `${prefix}:_tag`\n return Promise.all(\n tags.map(async (tag) => ({ tag, key: await fit(base, tag) }))\n )\n }\n\n const create = (logger: CacheLogger): Cache => {\n const describe = (error: unknown) =>\n error instanceof Error ? error.message : String(error)\n\n /** Runs a store operation: failures are logged, and fail open. */\n const guard = async <R>(\n operation: StoreOperation,\n key: string,\n entry: string,\n run: () => Promise<R>,\n fallback: R\n ): Promise<R> => {\n let timer: ReturnType<typeof setTimeout> | undefined\n try {\n const running = run()\n return storeTimeout === undefined\n ? await running\n : await Promise.race([\n running,\n new Promise<never>((_, reject) => {\n timer = setTimeout(\n () =>\n reject(\n new Error(`no answer within ${storeTimeout} milliseconds`)\n ),\n storeTimeout\n )\n }),\n ])\n } catch (cause) {\n const error = new CacheStoreError(store.name, operation, key, {\n cause,\n })\n const data = {\n store: store.name,\n operation,\n key,\n entry,\n error: error.message,\n }\n // A failed deletion or invalidation may leave outdated values.\n if (operation === 'delete' || operation === 'invalidate') {\n logger.error(data, 'Cache store failed')\n } else {\n logger.warn(data, 'Cache store failed')\n }\n if (onError) {\n try {\n await onError(error, { operation, key, entry })\n } catch (hookError) {\n logger.error(\n { error: describe(hookError) },\n 'Cache onError hook failed'\n )\n }\n }\n if (!failOpen) throw error\n return fallback\n } finally {\n clearTimeout(timer)\n }\n }\n\n /** Reports an event to the log (debug) and `onEvent`. */\n const report = (event: CacheEvent) => {\n const { type, reason, entry, key } = event\n logger.debug?.(\n { entry, key, ...(reason ? { reason } : {}) },\n type === 'hit'\n ? 'Cache hit'\n : type === 'miss'\n ? 'Cache miss'\n : 'Cache served a stale value'\n )\n if (!onEvent) return\n try {\n onEvent(event)\n } catch (error) {\n logger.error({ error: describe(error) }, 'Cache onEvent hook failed')\n }\n }\n\n /**\n * Current tokens of tags (undefined for a missing tag), or undefined\n * when the store cannot read them.\n */\n const tokensOf = async (\n entry: string,\n tagKeys: readonly { tag: string; key: string }[]\n ) => {\n const keys = tagKeys.map(({ key }) => key)\n const values = await guard<(string | undefined)[] | undefined>(\n 'get',\n keys.join(' '),\n entry,\n () =>\n store.getMany\n ? store.getMany(keys)\n : Promise.all(keys.map((key) => store.get(key))),\n undefined\n )\n return values?.map((token, index) => ({\n tag: (tagKeys[index] as { tag: string }).tag,\n token,\n }))\n }\n\n /** Deserializes and validates a value: `{ value }`, or undefined. */\n const decode = async (config: EntryConfig, key: string, data: string) => {\n let value: unknown\n try {\n value = serializer.deserialize(data)\n } catch (error) {\n logger.warn(\n { entry: config.name, key, error: describe(error) },\n 'Cache value cannot be deserialized'\n )\n return undefined\n }\n if (!config.schema) return { value }\n const result = await config.schema.safeParseAsync(value)\n return result.success ? { value: result.data } : undefined\n }\n\n /**\n * Reads a stored value: its value, expiry, and whether its tags are\n * still valid. Undefined when missing, invalid, or past every stale\n * window.\n */\n const lookup = async (\n config: EntryConfig,\n policy: ReturnType<typeof policyFor>,\n key: string\n ) => {\n const text = await guard(\n 'get',\n key,\n config.name,\n () => store.get(key),\n undefined\n )\n if (text === undefined) return undefined\n const stored = parseStored(text)\n const now = Date.now()\n if (\n !stored ||\n stored.e + Math.max(policy.grace, policy.staleWhileRevalidate) <= now\n ) {\n return undefined\n }\n const decoded = await decode(config, key, stored.d)\n if (!decoded) return undefined\n const tags = Object.entries(stored.t ?? {})\n let tagsValid = true\n if (tags.length > 0) {\n const current = await tokensOf(\n config.name,\n await tagKeysOf(tags.map(([tag]) => tag))\n )\n // Unreadable tokens: the value may have been invalidated.\n tagsValid =\n current?.every(({ tag, token }) => token === stored.t?.[tag]) ?? false\n }\n return {\n value: decoded.value,\n expiresAt: stored.e,\n fresh: stored.e > now && tagsValid,\n tagsValid,\n }\n }\n\n /**\n * Writes a value, after checking that it reads back: returns the value\n * as read later, or undefined when it cannot be cached.\n */\n const write = async (\n config: EntryConfig,\n key: string,\n args: readonly unknown[],\n value: unknown\n ): Promise<{ value: unknown } | undefined> => {\n if (value === undefined) return undefined\n let data: string\n try {\n data = serializer.serialize(value)\n if (typeof data !== 'string') throw new TypeError('not a string')\n } catch (error) {\n logger.warn(\n { entry: config.name, key, error: describe(error) },\n 'Cache value cannot be serialized'\n )\n return undefined\n }\n const checked = config.schema\n ? await decode(config, key, data)\n : { value }\n if (!checked) {\n logger.warn(\n { entry: config.name, key },\n 'Cache value does not match the schema of the entry: not cached'\n )\n return undefined\n }\n const policy = policyFor(config)\n const lifetime =\n policy.ttl + Math.max(policy.grace, policy.staleWhileRevalidate)\n const now = Date.now()\n const stored: StoredEntry = {\n v: 1,\n c: now,\n e: now + policy.ttl,\n d: data,\n }\n if (config.tags) {\n const tags = checkTags(\n `defineCacheEntry(\"${config.name}\")`,\n config.tags(...args)\n )\n if (tags.length > 0) {\n const tagKeys = await tagKeysOf(tags)\n const current = await tokensOf(config.name, tagKeys)\n // Unreadable tokens: creating new ones would invalidate every\n // value of these tags, so the value is not cached.\n if (!current) return checked\n stored.t = {}\n for (const [index, { tag, token }] of current.entries()) {\n if (token !== undefined) {\n stored.t[tag] = token\n continue\n }\n // First value with this tag: its token is created.\n const created = crypto.randomUUID()\n const tagKey = (tagKeys[index] as { key: string }).key\n await guard(\n 'set',\n tagKey,\n config.name,\n () =>\n store.set(tagKey, created, Math.max(lifetime, TAG_LIFETIME)),\n undefined\n )\n stored.t[tag] = created\n }\n }\n }\n await guard(\n 'set',\n key,\n config.name,\n () => store.set(key, JSON.stringify(stored), lifetime),\n undefined\n )\n return checked\n }\n\n /**\n * Loads and writes a value, once per key at a time, and once for every\n * instance with the `lock` option.\n */\n const load = (\n config: EntryConfig,\n key: string,\n args: readonly unknown[],\n loader: () => unknown\n ) => {\n const running = loading.get(key)\n if (running) return running\n const loaded = (async () => {\n const policy = policyFor(config)\n const lockKey = `${key}:lock`\n let token: string | undefined\n if (policy.lock !== undefined && store.lock) {\n const lock = store.lock.bind(store)\n const ttl = policy.lock\n // null: the store failed, load without lock.\n const taken = await guard<string | undefined | null>(\n 'lock',\n lockKey,\n config.name,\n () => lock(lockKey, ttl),\n null\n )\n if (taken === undefined) {\n // Another instance loads it: wait for its value.\n const until = Date.now() + ttl\n while (Date.now() < until) {\n await new Promise((resolve) => setTimeout(resolve, LOCK_POLL))\n const found = await lookup(config, policy, key)\n if (found?.fresh) return found.value\n }\n logger.warn(\n { entry: config.name, key, lock: ttl },\n 'Cache lock not released in time: loading anyway'\n )\n } else if (taken !== null) {\n token = taken\n }\n }\n try {\n const value: unknown = await loader()\n const written = await write(config, key, args, value)\n backoffs.delete(key)\n return written ? written.value : value\n } finally {\n if (token !== undefined && store.unlock) {\n const unlock = store.unlock.bind(store)\n const held = token\n await guard(\n 'unlock',\n lockKey,\n config.name,\n () => unlock(lockKey, held),\n undefined\n )\n }\n }\n })()\n loading.set(key, loaded)\n void loaded.then(\n () => loading.delete(key),\n () => loading.delete(key)\n )\n return loaded\n }\n\n /** Logs the failure of a load that nobody waits for anymore. */\n const inBackground = (\n config: EntryConfig,\n key: string,\n loaded: Promise<unknown>\n ) => {\n loaded.catch((error: unknown) => {\n logger.warn(\n { entry: config.name, key, error: describe(error) },\n 'Cache refresh failed'\n )\n })\n }\n\n /** Rejects after `ms` (never when undefined), until cleared. */\n const deadline = (ms: number | undefined, error: () => unknown) => {\n let timer: ReturnType<typeof setTimeout> | undefined\n const promise = new Promise<never>((_, reject) => {\n if (ms !== undefined) timer = setTimeout(() => reject(error()), ms)\n })\n return { promise, clear: () => clearTimeout(timer) }\n }\n\n const cache: Cache = {\n get: (async (entry: unknown, ...args: unknown[]) => {\n const config = configOf('get', entry)\n const key = await keyFor(config, args)\n const found = await lookup(config, policyFor(config), key)\n report({ type: found?.fresh ? 'hit' : 'miss', entry: config.name, key })\n return found?.fresh ? found.value : undefined\n }) as Cache['get'],\n\n set: (async (entry: unknown, ...argsAndValue: unknown[]) => {\n const config = configOf('set', entry)\n const args = argsAndValue.slice(0, -1)\n const key = await keyFor(config, args)\n await write(config, key, args, argsAndValue.at(-1))\n }) as Cache['set'],\n\n delete: (async (entry: unknown, ...args: unknown[]) => {\n const config = configOf('delete', entry)\n const key = await keyFor(config, args)\n await guard(\n 'delete',\n key,\n config.name,\n () => store.delete(key),\n undefined\n )\n }) as Cache['delete'],\n\n getOrSet: (async (entry: unknown, ...argsAndLoader: unknown[]) => {\n const config = configOf('getOrSet', entry)\n const loader = argsAndLoader.at(-1)\n if (typeof loader !== 'function') {\n throw new TypeError(\n 'cache.getOrSet(): the last argument must be the function that loads the value.'\n )\n }\n const args = argsAndLoader.slice(0, -1)\n const key = await keyFor(config, args)\n const policy = policyFor(config)\n const found = await lookup(config, policy, key)\n const event = { entry: config.name, key }\n if (found?.fresh) {\n report({ type: 'hit', ...event })\n return found.value\n }\n const now = Date.now()\n\n // Stale-while-revalidate: serve, and refresh in the background.\n if (\n found?.tagsValid &&\n found.expiresAt + policy.staleWhileRevalidate > now\n ) {\n report({ type: 'stale', reason: 'revalidate', ...event })\n inBackground(\n config,\n key,\n load(config, key, args, loader as () => unknown)\n )\n return found.value\n }\n\n // Grace period: stale value served if the loader fails. Without it,\n // an invalidated value is never served.\n const stale =\n found && policy.grace > 0 && found.expiresAt + policy.grace > now\n ? found\n : undefined\n if (stale && (backoffs.get(key) ?? 0) > now) {\n report({ type: 'stale', reason: 'backoff', ...event })\n return stale.value\n }\n\n report({ type: 'miss', ...event })\n const loaded = load(config, key, args, loader as () => unknown)\n const slow = Symbol('slow')\n const soft = deadline(stale ? policy.timeout : undefined, () => slow)\n const hard = deadline(\n policy.hardTimeout,\n () =>\n new CacheTimeoutError(\n config.name,\n key,\n policy.hardTimeout as number\n )\n )\n try {\n return await Promise.race([loaded, soft.promise, hard.promise])\n } catch (error) {\n // The load goes on, and caches its value.\n if (error === slow || error instanceof CacheTimeoutError) {\n inBackground(config, key, loaded)\n }\n if (!stale) throw error\n if (error === slow) {\n logger.warn(\n { ...event, timeout: policy.timeout },\n 'Cache served a stale value: the loader is slow'\n )\n report({ type: 'stale', reason: 'timeout', ...event })\n return stale.value\n }\n backoffs.set(key, Date.now() + policy.graceBackoff)\n logger.warn(\n { ...event, error: describe(error) },\n 'Cache served a stale value: the loader failed'\n )\n report({ type: 'stale', reason: 'error', ...event })\n return stale.value\n } finally {\n soft.clear()\n hard.clear()\n }\n }) as Cache['getOrSet'],\n\n invalidateTags: async (tags) => {\n const tagKeys = await tagKeysOf(\n checkTags('cache.invalidateTags()', tags)\n )\n await Promise.all(\n tagKeys.map(({ tag, key }) =>\n guard(\n 'invalidate',\n key,\n `tag:${tag}`,\n () => store.set(key, crypto.randomUUID(), TAG_LIFETIME),\n undefined\n )\n )\n )\n logger.debug?.({ tags }, 'Cache tags invalidated')\n },\n\n withLogger: (other) => create(checkLogger('withLogger()', other)),\n }\n return Object.freeze(cache)\n }\n\n return create(rootLogger)\n}\n"],"mappings":"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","names":["entries","label"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotpress/cache",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.3",
|
|
4
4
|
"description": "Request memoization with batching, and a resilient cache with interchangeable stores",
|
|
5
5
|
"author": "Renaud Guyon",
|
|
6
6
|
"license": "MIT",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"node": ">=22.12"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
|
53
|
-
"@dotpress/core": "^2.0.0-alpha.
|
|
53
|
+
"@dotpress/core": "^2.0.0-alpha.3"
|
|
54
54
|
},
|
|
55
55
|
"peerDependenciesMeta": {
|
|
56
56
|
"@dotpress/core": {
|
|
@@ -58,8 +58,8 @@
|
|
|
58
58
|
}
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
|
-
"@dotpress/core": "2.0.0-alpha.
|
|
62
|
-
"@dotpress/testing": "2.0.0-alpha.
|
|
61
|
+
"@dotpress/core": "2.0.0-alpha.3",
|
|
62
|
+
"@dotpress/testing": "2.0.0-alpha.3",
|
|
63
63
|
"ioredis": "~6.0.0",
|
|
64
64
|
"miniflare": "~4.20260730.0",
|
|
65
65
|
"redis": "~6.3.1",
|