@prisma/orm-extension-middleware-cache 8.0.0-rc.14-dev.8 → 8.0.0-rc.14-dev.9
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 +99 -3
- package/dist/index.d.mts +167 -163
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +149 -158
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -9,11 +9,107 @@ pnpm add @prisma/orm-extension-middleware-cache
|
|
|
9
9
|
The whole surface is the package root:
|
|
10
10
|
|
|
11
11
|
```ts
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
cacheAnnotation,
|
|
14
|
+
createCacheMiddleware,
|
|
15
|
+
createInMemoryCacheStore,
|
|
16
|
+
deriveKeyFromContentHash,
|
|
17
|
+
type CacheStore,
|
|
18
|
+
} from '@prisma/orm-extension-middleware-cache';
|
|
13
19
|
```
|
|
14
20
|
|
|
15
21
|
## Responsibilities
|
|
16
22
|
|
|
17
|
-
A
|
|
23
|
+
A read-through cache middleware: on a hit it returns cached rows and never invokes the driver; on a miss it buffers the driver's rows and stores them when the read completes. A read opts in with `cacheAnnotation({ key?, meta?, bypass? })`: `key` names the entry, otherwise the `deriveKey` option computes it (default: the family runtime's content hash, `deriveKeyFromContentHash`); `meta` is handed to the store with the entry; `bypass: true` skips the cache for that call. Connection- and transaction-scoped executions bypass the cache.
|
|
18
24
|
|
|
19
|
-
|
|
25
|
+
How long an entry lives is the store's policy. The default store, `createInMemoryCacheStore({ maxEntries?, ttlMs?, clock? })`, keeps up to 1000 values for 60 seconds each; `ttlMs: Infinity` never expires. A key's version is kept for `ttlMs` after the `unset` that moved it. Versions are not bounded by `maxEntries`. A forgotten version reads as 0, so with a finite `ttlMs` a read that takes longer than `ttlMs` and overlaps an `invalidate` can store stale rows. With `ttlMs: Infinity`, the versions of invalidated keys are never forgotten and grow with the number of distinct keys invalidated. A `maxEntries` that is not a positive integer or a `ttlMs` that is not positive throws `RUNTIME.ARGUMENT_INVALID`. Implement the `CacheStore` interface to use Redis, Memcached, or any other backend.
|
|
26
|
+
|
|
27
|
+
## The store
|
|
28
|
+
|
|
29
|
+
The store is a cache of values; this middleware stores `CachedRows`, the raw rows of one read.
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
interface CacheEntry<TMeta = unknown, TValue = CachedRows> {
|
|
33
|
+
readonly key: string;
|
|
34
|
+
readonly meta: TMeta | undefined;
|
|
35
|
+
readonly version: number;
|
|
36
|
+
readonly data: { readonly empty: true } | { readonly empty: false; readonly value: TValue };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface CacheStore<TMeta = unknown, TValue = CachedRows> {
|
|
40
|
+
get(target: {
|
|
41
|
+
readonly key: string;
|
|
42
|
+
readonly meta: TMeta | undefined;
|
|
43
|
+
}): Promise<CacheEntry<TMeta, TValue>>;
|
|
44
|
+
set(entry: CacheEntry<TMeta, TValue>, value: TValue): Promise<boolean>;
|
|
45
|
+
unset(target: {
|
|
46
|
+
readonly keys: readonly string[] | undefined;
|
|
47
|
+
readonly meta: TMeta | undefined;
|
|
48
|
+
}): Promise<void>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type CachedRows = readonly Record<string, unknown>[];
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The store keeps a version per key, even for a key that holds no value; a key never seen has version 0, and only `unset` changes it. `get({ key, meta })` always returns a `CacheEntry` with the key, the read annotation's `meta`, the current version and `data`, which is `{ empty: true }` on a miss. A store that matches `meta` folds the versions of whatever `meta` names into that version, and does the same when `set` compares, so an `invalidate({ meta })` refuses an overlapping read's `set` even for a key the store has never seen. `set(entry, value)` stores `value` if and only if the key's version still equals `entry.version`, and returns whether it stored; there is no unconditional write, so a prefill is `get` then `set`. `unset` removes every value named in `keys` (`undefined` or non-empty) and every value that matches `meta`, increments the version of every key it removes or would remove, and an `unset` by key also drops that key from any `meta` index. `set` must be atomic against `unset`: one synchronous step in memory, or one script on a server. Keep a moved version at least as long as a read can take; the default store keeps it for `ttlMs`. The store interprets `meta` in all three methods; the middleware never does. A store that cannot act on a `meta` given to `unset` must throw: the default store throws `RUNTIME.CACHE_STORE_META_UNSUPPORTED`. `unset` must not run queries through the runtime that uses the middleware. A store used with this middleware holds rows: `createCacheMiddleware` takes a `CacheStore<TMeta, CachedRows>`, and a store of any other value type, `unknown` included, is a type error. `TValue` defaults to `CachedRows`, so `CacheStore<TMeta>` is a store of rows; it exists so the same store type can serve other caches. The default store, `createInMemoryCacheStore()`, holds rows unless you give it another value type.
|
|
55
|
+
|
|
56
|
+
## Typed meta
|
|
57
|
+
|
|
58
|
+
`CacheStore<TMeta>`, `createCacheMiddleware` and `cacheAnnotation<TMeta>` take the shape of `meta` as a type parameter; it defaults to `unknown`. `createCacheMiddleware` infers it from the store, so `invalidate` accepts only that shape:
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
interface TagMeta {
|
|
62
|
+
tags: string[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
class TagStore implements CacheStore<TagMeta> {
|
|
66
|
+
// get({ key, meta }), set(entry, value) and unset({ keys, meta }), with meta: TagMeta | undefined
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const cache = createCacheMiddleware({ store: new TagStore() }); // CacheMiddleware<TagMeta>
|
|
70
|
+
await cache.invalidate({ meta: { tags: ['users'] } }); // compiles
|
|
71
|
+
await cache.invalidate({ meta: { tag: 'users' } }); // type error
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Nothing ties the annotation's `TMeta` to the store's at compile time, because the annotation is written where the query is and the store where the middleware is set up. A store package should therefore export a wrapper typed with its own meta, so the two agree:
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
export const cached = (o: CacheAnnotationOptions<TagMeta>) => cacheAnnotation<TagMeta>(o);
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Deriving keys
|
|
81
|
+
|
|
82
|
+
`createCacheMiddleware({ deriveKey })` computes the key of every cached read whose annotation has no `key`. Build on the default to add a prefix:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
const cache = createCacheMiddleware({
|
|
86
|
+
deriveKey: async (exec, ctx) => `users:${await deriveKeyFromContentHash(exec, ctx)}`,
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
An explicit annotation `key` is used literally and bypasses `deriveKey`, so a tenant or namespace prefix must be part of it too. A derivation that returns the same key for different plans serves one plan's rows to the other, and one that drops the content hash no longer changes on a schema migration. If `deriveKey` throws, the read fails.
|
|
91
|
+
|
|
92
|
+
## Invalidation
|
|
93
|
+
|
|
94
|
+
`invalidate({ keys?, meta? })` removes entries through one `store.unset({ keys, meta })` call, and does nothing when there are no keys and no `meta`.
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
const cache = createCacheMiddleware();
|
|
98
|
+
|
|
99
|
+
await db.orm.public.User.first({ id: 1 }, (meta) =>
|
|
100
|
+
meta.annotate(cacheAnnotation({ key: 'user-1' })),
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
await db.orm.public.User.where({ id: 1 }).update({ name: 'Alicia' });
|
|
104
|
+
await cache.invalidate({ keys: ['user-1'] });
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
A read that missed before an `invalidate` and finishes after it does not store its rows. Its `store.get` returned an empty entry with, say, version 3; the `unset` moves the key to version 4; `store.set(entry, rows)` then returns `false`. This works across processes that share a store, because the store owns the version. Call `invalidate` after the write has committed: inside a transaction, another request can put the old rows back in the cache before the commit. An error from the store propagates.
|
|
108
|
+
|
|
109
|
+
A tag scheme is a store policy: `cacheAnnotation({ meta: { tags: ['users'] } })` on the read, a store that indexes `meta.tags` in `set`, and `cache.invalidate({ meta: { tags: ['users'] } })` after the write.
|
|
110
|
+
|
|
111
|
+
## Scope
|
|
112
|
+
|
|
113
|
+
The middleware is a read-through cache with a control surface: keys and `deriveKey`, `meta`, `bypass`, `invalidate`, and the `CacheStore` interface. It carries data between the annotations and the store and never interprets it. Lifetime and the meaning of `meta` are the store's; deciding what to invalidate and when, request coalescing and routing between several stores belong in extensions built on these primitives.
|
|
114
|
+
|
|
115
|
+
Invalidating as part of a write is not supported yet: it must wait for the transaction to commit, including the transactions the ORM opens for its own `update()` and `delete()`, and the runtime has no post-commit hook. Until then, call `invalidate` after the write returns. Serve-stale strategies such as stale-while-revalidate need the hit-or-miss decision, which only the middleware makes.
|
package/dist/index.d.mts
CHANGED
|
@@ -1,219 +1,223 @@
|
|
|
1
|
-
import { CrossFamilyMiddleware } from "@prisma/orm-framework/components/runtime";
|
|
1
|
+
import { AnnotationHandle, AnnotationValue, CrossFamilyMiddleware, ExecutionPlan, RuntimeMiddlewareContext } from "@prisma/orm-framework/components/runtime";
|
|
2
2
|
//#region ../../../3-extensions/middleware-cache/dist/index.d.mts
|
|
3
3
|
//#region src/cache-annotation.d.ts
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* - `key` — Per-query override of the cache key. When supplied, replaces
|
|
16
|
-
* the default `RuntimeMiddlewareContext.contentHash(exec)` digest.
|
|
17
|
-
* The supplied string is stored as-is — the cache middleware does
|
|
18
|
-
* **not** rehash it, so the caller is responsible for ensuring the
|
|
19
|
-
* string is bounded in size and free of sensitive data they do not
|
|
20
|
-
* want flowing into logs / Redis `KEYS` / persistence dumps.
|
|
5
|
+
* Options for a read annotated with `cacheAnnotation`. A read with the annotation, in runtime scope
|
|
6
|
+
* and not bypassed, is cached; how long the entry lives is the store's policy.
|
|
7
|
+
*
|
|
8
|
+
* - `key` — the entry's key. When absent, the middleware's `deriveKey` computes it. The key is
|
|
9
|
+
* stored and logged as given, so keep it bounded and free of secrets.
|
|
10
|
+
* - `meta` — any value, passed by reference to the store's `set` with the entry. The middleware
|
|
11
|
+
* never reads it. Stores use it to group entries (for example by tag) or to set a lifetime.
|
|
12
|
+
* - `bypass` — when `true`, the read neither reads from nor writes to the cache.
|
|
13
|
+
*
|
|
14
|
+
* `TMeta` is the shape of `meta`; pass the store's `TMeta` so the two agree.
|
|
21
15
|
*/
|
|
22
|
-
interface
|
|
23
|
-
readonly ttl?: number;
|
|
24
|
-
readonly skip?: boolean;
|
|
16
|
+
interface CacheAnnotationOptions<TMeta = unknown> {
|
|
25
17
|
readonly key?: string;
|
|
18
|
+
readonly meta?: TMeta;
|
|
19
|
+
readonly bypass?: boolean;
|
|
26
20
|
}
|
|
27
21
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
22
|
+
* The type of `cacheAnnotation`: a call generic in `TMeta`, plus the read-only `namespace`,
|
|
23
|
+
* `applicableTo` and `read` of the underlying annotation handle.
|
|
24
|
+
*/
|
|
25
|
+
type CacheAnnotationHandle = (<TMeta = unknown>(options: CacheAnnotationOptions<TMeta>) => AnnotationValue<CacheAnnotationOptions, 'read'>) & {
|
|
26
|
+
readonly namespace: string;
|
|
27
|
+
readonly applicableTo: ReadonlySet<'read'>;
|
|
28
|
+
readonly read: AnnotationHandle<CacheAnnotationOptions, 'read'>['read'];
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Marks a read for the cache middleware. It applies to reads only: a write terminal refuses it at
|
|
32
|
+
* compile time and at run time. The middleware reads it from `plan.meta.annotations.cache`.
|
|
36
33
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
34
|
+
* `TMeta` types `meta` for this call only. Nothing ties it to the store's `TMeta`, so a store
|
|
35
|
+
* package should export a wrapper typed with its own meta, such as
|
|
36
|
+
* `(o: CacheAnnotationOptions<TagMeta>) => cacheAnnotation<TagMeta>(o)`.
|
|
39
37
|
*
|
|
40
38
|
* @example
|
|
41
39
|
* ```typescript
|
|
42
40
|
* import { cacheAnnotation } from '@internal/middleware-cache';
|
|
43
41
|
*
|
|
44
42
|
* // ORM read terminal — accepts the read-only annotation via the meta callback.
|
|
45
|
-
* const user = await db.User.first(
|
|
43
|
+
* const user = await db.orm.public.User.first(
|
|
46
44
|
* { id },
|
|
47
|
-
* (meta) => meta.annotate(cacheAnnotation({
|
|
45
|
+
* (meta) => meta.annotate(cacheAnnotation({ key: `user-${id}` })),
|
|
48
46
|
* );
|
|
49
47
|
*
|
|
50
48
|
* // SQL DSL select builder — chainable.
|
|
51
49
|
* const plan = db.sql
|
|
52
50
|
* .from(tables.user)
|
|
53
|
-
* .annotate(cacheAnnotation({
|
|
51
|
+
* .annotate(cacheAnnotation({}))
|
|
54
52
|
* .select({ id: tables.user.columns.id })
|
|
55
53
|
* .build();
|
|
56
54
|
* ```
|
|
57
55
|
*/
|
|
58
|
-
declare const cacheAnnotation:
|
|
56
|
+
declare const cacheAnnotation: CacheAnnotationHandle;
|
|
59
57
|
//#endregion
|
|
60
58
|
//#region src/cache-store.d.ts
|
|
61
59
|
/**
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* wraps the orchestrator output, so intercepted rows go through the
|
|
66
|
-
* same codec decoding as driver rows on the way to the consumer. The
|
|
67
|
-
* cache stores wire-format values; decoding happens once per consumer
|
|
68
|
-
* read regardless of where the rows came from.
|
|
69
|
-
* - `storedAt` is the clock value at the moment the entry was committed
|
|
70
|
-
* to the store. It is informational metadata for callers (debugging,
|
|
71
|
-
* telemetry) and is **not** used by the in-memory store itself for
|
|
72
|
-
* expiry — TTL is driven by the store's own clock plus the `ttlMs`
|
|
73
|
-
* passed to `set`. Custom stores may use it differently.
|
|
60
|
+
* The rows one read produced, stored raw (undecoded): the value type the cache middleware stores.
|
|
61
|
+
* The SQL runtime decodes intercepted rows the same way as driver rows, so a hit and a miss yield
|
|
62
|
+
* the same values to the consumer.
|
|
74
63
|
*/
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
64
|
+
type CachedRows = readonly Record<string, unknown>[];
|
|
65
|
+
/**
|
|
66
|
+
* One key of a `CacheStore`, as `get` saw it.
|
|
67
|
+
*
|
|
68
|
+
* - `key` and `meta` — what `get` was asked for. `meta` is the read annotation's `meta`, or
|
|
69
|
+
* `undefined`.
|
|
70
|
+
* - `version` — the key's version when `get` ran. A store that matches `meta` folds the versions
|
|
71
|
+
* of whatever `meta` names into it.
|
|
72
|
+
* - `data` — `{ empty: true }` when the store holds no live value for the key, otherwise that
|
|
73
|
+
* value.
|
|
74
|
+
*/
|
|
75
|
+
interface CacheEntry<TMeta = unknown, TValue = CachedRows> {
|
|
76
|
+
readonly key: string;
|
|
77
|
+
readonly meta: TMeta | undefined;
|
|
78
|
+
readonly version: number;
|
|
79
|
+
readonly data: {
|
|
80
|
+
readonly empty: true;
|
|
81
|
+
} | {
|
|
82
|
+
readonly empty: false;
|
|
83
|
+
readonly value: TValue;
|
|
84
|
+
};
|
|
78
85
|
}
|
|
79
86
|
/**
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
87
|
+
* A cache of values that the cache middleware reads from and writes to. The middleware stores
|
|
88
|
+
* `CachedRows`.
|
|
89
|
+
*
|
|
90
|
+
* The store keeps a version per key, an integer it keeps even for a key that holds no value. A key
|
|
91
|
+
* never seen, or forgotten, has version 0. Only `unset` changes a version.
|
|
92
|
+
*
|
|
93
|
+
* - `get` always returns an entry: the live value or `{ empty: true }`, and the current version.
|
|
94
|
+
* A store that matches `meta` folds the versions of whatever `meta` names into that version, and
|
|
95
|
+
* does the same when `set` compares, so an `unset` by `meta` refuses a read's `set` even for a
|
|
96
|
+
* key it has never seen.
|
|
97
|
+
* - `set(entry, value)` stores `value` under `entry.key` with `entry.meta`, if and only if the
|
|
98
|
+
* key's current version still equals `entry.version`, and returns whether it stored. `entry` is
|
|
99
|
+
* the one `get` returned, empty or not. There is no unconditional write: to prefill, `get` then
|
|
100
|
+
* `set`. The compare and the write must be atomic with respect to `unset`, for example one
|
|
101
|
+
* synchronous step in memory or one script on a server.
|
|
102
|
+
* - `unset` removes every value named in `keys` and every value that matches `meta`, and
|
|
103
|
+
* increments the version of every key it removes or would remove, including keys that hold no
|
|
104
|
+
* value. `keys` is `undefined` or non-empty. A store that cannot act on a `meta` it is given
|
|
105
|
+
* must throw rather than ignore it. An `unset` by key must also drop that key from any `meta`
|
|
106
|
+
* index the store keeps.
|
|
107
|
+
*
|
|
108
|
+
* Lifetime and eviction are the store's policy. A version that `unset` moved must be kept at least
|
|
109
|
+
* as long as a read can take, so that a read that started before the `unset` cannot store. `unset`
|
|
110
|
+
* must not run queries through the runtime that uses the middleware.
|
|
111
|
+
*
|
|
112
|
+
* `TMeta` is the shape of `meta` the store understands. The middleware does not check that a read
|
|
113
|
+
* annotation's `meta` has this shape; see `cacheAnnotation`.
|
|
102
114
|
*/
|
|
103
|
-
interface CacheStore {
|
|
104
|
-
get(
|
|
105
|
-
|
|
115
|
+
interface CacheStore<TMeta = unknown, TValue = CachedRows> {
|
|
116
|
+
get(target: {
|
|
117
|
+
readonly key: string;
|
|
118
|
+
readonly meta: TMeta | undefined;
|
|
119
|
+
}): Promise<CacheEntry<TMeta, TValue>>;
|
|
120
|
+
set(entry: CacheEntry<TMeta, TValue>, value: TValue): Promise<boolean>;
|
|
121
|
+
unset(target: {
|
|
122
|
+
readonly keys: readonly string[] | undefined;
|
|
123
|
+
readonly meta: TMeta | undefined;
|
|
124
|
+
}): Promise<void>;
|
|
106
125
|
}
|
|
107
126
|
/**
|
|
108
|
-
* Options
|
|
109
|
-
*
|
|
110
|
-
* - `maxEntries` —
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* `
|
|
115
|
-
*
|
|
127
|
+
* Options for `createInMemoryCacheStore`.
|
|
128
|
+
*
|
|
129
|
+
* - `maxEntries` — the most values kept, a positive integer; the least recently used is evicted
|
|
130
|
+
* first. Default 1000.
|
|
131
|
+
* - `ttlMs` — how long a value lives after its `set`, and how long a key's version is kept after
|
|
132
|
+
* the `unset` that moved it, a positive number of milliseconds. Default 60 000. Versions are not
|
|
133
|
+
* bounded by `maxEntries`. A forgotten version reads as 0, so a read that takes longer than
|
|
134
|
+
* `ttlMs` and overlaps an `invalidate` can store stale rows. `Infinity` never expires values or
|
|
135
|
+
* versions: the versions of invalidated keys are never forgotten, and grow with the number of
|
|
136
|
+
* distinct keys invalidated.
|
|
137
|
+
* - `clock` — the time source for expiry. Default `Date.now`.
|
|
116
138
|
*/
|
|
117
139
|
interface InMemoryCacheStoreOptions {
|
|
118
|
-
readonly maxEntries
|
|
140
|
+
readonly maxEntries?: number;
|
|
141
|
+
readonly ttlMs?: number;
|
|
119
142
|
readonly clock?: () => number;
|
|
120
143
|
}
|
|
121
144
|
/**
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
* above `maxEntries`, the least recently used entry is evicted.
|
|
128
|
-
* Setting an existing key updates the entry in place and refreshes its
|
|
129
|
-
* recency without changing the live count.
|
|
130
|
-
* - On `get` of an existing key, recency is bumped (so the entry is no
|
|
131
|
-
* longer the LRU candidate).
|
|
132
|
-
* - On `get` of an expired entry, the entry is removed from the map and
|
|
133
|
-
* `undefined` is returned. The slot becomes available for new writes
|
|
134
|
-
* without counting against `maxEntries`.
|
|
135
|
-
*
|
|
136
|
-
* `Map` insertion order is the LRU order: the first key is the LRU
|
|
137
|
-
* candidate; the last key is the most recently used. Bumping recency is
|
|
138
|
-
* a delete-then-set on the underlying map.
|
|
139
|
-
*
|
|
140
|
-
* The default store is **not** coherent across processes or replicas —
|
|
141
|
-
* each process holds its own Map. Users who need a shared cache supply
|
|
142
|
-
* their own `CacheStore` (Redis, Memcached, etc.).
|
|
145
|
+
* The default cache store: a least-recently-used map with one lifetime for every value, local to
|
|
146
|
+
* the process. It holds `CachedRows` unless given another `TValue`. A key's version is kept for
|
|
147
|
+
* `ttlMs` after the `unset` that moved it (see `InMemoryCacheStoreOptions`). It ignores `meta` in
|
|
148
|
+
* `get` and `set`, and its `unset` rejects any `meta`, including `null`, before changing anything.
|
|
149
|
+
* It throws `RUNTIME.ARGUMENT_INVALID` for a `maxEntries` or `ttlMs` outside the ranges above.
|
|
143
150
|
*/
|
|
144
|
-
declare function createInMemoryCacheStore(options
|
|
151
|
+
declare function createInMemoryCacheStore<TMeta = unknown, TValue = CachedRows>(options?: InMemoryCacheStoreOptions): CacheStore<TMeta, TValue>;
|
|
145
152
|
//#endregion
|
|
146
153
|
//#region src/cache-middleware.d.ts
|
|
147
154
|
/**
|
|
148
155
|
* Options accepted by `createCacheMiddleware`.
|
|
149
156
|
*
|
|
150
|
-
* - `store` —
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
* clock to make commit-time observable. Note: TTL math lives inside
|
|
159
|
-
* the store, not the middleware — supplying a clock here only affects
|
|
160
|
-
* the `storedAt` field on committed `CachedEntry` values.
|
|
157
|
+
* - `store` — the cache backend. Defaults to `createInMemoryCacheStore()`: 1000 values, 60 s.
|
|
158
|
+
* - `deriveKey` — computes the key of a cached read whose annotation has no `key`. Defaults to
|
|
159
|
+
* `deriveKeyFromContentHash`. It runs on every such read, hit or miss, and an error from it
|
|
160
|
+
* fails the read. It must return different keys whenever the rows can differ; build on
|
|
161
|
+
* `deriveKeyFromContentHash` to keep the statement, parameters and storage hash.
|
|
162
|
+
*
|
|
163
|
+
* `TMeta` is the store's meta type; `createCacheMiddleware` infers it from `store`. The store holds
|
|
164
|
+
* `CachedRows`.
|
|
161
165
|
*/
|
|
162
|
-
interface CacheMiddlewareOptions {
|
|
163
|
-
readonly store?: CacheStore
|
|
164
|
-
readonly
|
|
165
|
-
readonly clock?: () => number;
|
|
166
|
+
interface CacheMiddlewareOptions<TMeta = unknown> {
|
|
167
|
+
readonly store?: CacheStore<TMeta, CachedRows>;
|
|
168
|
+
readonly deriveKey?: (exec: ExecutionPlan, ctx: RuntimeMiddlewareContext) => string | Promise<string>;
|
|
166
169
|
}
|
|
167
170
|
/**
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
* -
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
171
|
+
* The cache middleware.
|
|
172
|
+
*
|
|
173
|
+
* `invalidate` removes entries through one `store.unset({ keys, meta })` call. It does nothing
|
|
174
|
+
* when `keys` is empty or absent and `meta` is absent. The store moves the version of every key it
|
|
175
|
+
* removes, so a read that missed before the call and finishes after it does not store its rows:
|
|
176
|
+
* its conditional `store.set` returns `false`. This holds across processes that share a store. An
|
|
177
|
+
* error from the store propagates.
|
|
178
|
+
*
|
|
179
|
+
* `TMeta` is the store's meta type, which `invalidate`'s `meta` must have.
|
|
180
|
+
*/
|
|
181
|
+
type CacheMiddleware<TMeta = unknown> = CrossFamilyMiddleware & {
|
|
182
|
+
readonly invalidate: (target: {
|
|
183
|
+
readonly keys?: readonly string[];
|
|
184
|
+
readonly meta?: TMeta;
|
|
185
|
+
}) => Promise<void>;
|
|
186
|
+
};
|
|
187
|
+
/**
|
|
188
|
+
* The default `deriveKey`: the family runtime's content hash of the plan, which covers the
|
|
189
|
+
* statement, its parameters and the storage hash.
|
|
190
|
+
*/
|
|
191
|
+
declare function deriveKeyFromContentHash(exec: ExecutionPlan, ctx: RuntimeMiddlewareContext): Promise<string>;
|
|
192
|
+
/**
|
|
193
|
+
* Creates a read-through cache middleware that works with every family runtime.
|
|
194
|
+
*
|
|
195
|
+
* It caches a read when the plan carries `cacheAnnotation`, the annotation does not set
|
|
196
|
+
* `bypass`, and the read runs in runtime scope (not inside a connection or transaction). The key
|
|
197
|
+
* is the annotation's `key`, else `deriveKey(exec, ctx)`.
|
|
198
|
+
*
|
|
199
|
+
* - `interceptQuery` — on a hit, returns the stored rows and the driver does not run. On a miss,
|
|
200
|
+
* starts collecting rows.
|
|
201
|
+
* - `onRow` — collects each row of a miss.
|
|
202
|
+
* - `afterQuery` — when the driver completed the read, calls `store.set(entry, rows)` with the
|
|
203
|
+
* entry `store.get` returned, which stores only if the key's version has not moved. If an
|
|
204
|
+
* `unset` moved it meanwhile, `set` returns `false` and the middleware logs
|
|
205
|
+
* `middleware.cache.store-skipped`.
|
|
199
206
|
*
|
|
200
207
|
* @example
|
|
201
208
|
* ```typescript
|
|
202
|
-
*
|
|
209
|
+
* const cache = createCacheMiddleware();
|
|
210
|
+
* const db = postgres<Contract>({ contractJson, url, middleware: [cache] });
|
|
203
211
|
*
|
|
204
|
-
* const
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
* middleware: [createCacheMiddleware({ maxEntries: 1000 })],
|
|
208
|
-
* });
|
|
209
|
-
*
|
|
210
|
-
* const user = await db.User.first(
|
|
211
|
-
* { id },
|
|
212
|
-
* (meta) => meta.annotate(cacheAnnotation({ ttl: 60_000 })),
|
|
212
|
+
* const user = await db.orm.public.User.first(
|
|
213
|
+
* { id: 1 },
|
|
214
|
+
* (meta) => meta.annotate(cacheAnnotation({ key: 'user-1' })),
|
|
213
215
|
* );
|
|
216
|
+
* await db.orm.public.User.where({ id: 1 }).update({ name: 'Alicia' });
|
|
217
|
+
* await cache.invalidate({ keys: ['user-1'] });
|
|
214
218
|
* ```
|
|
215
219
|
*/
|
|
216
|
-
declare function createCacheMiddleware(options?: CacheMiddlewareOptions):
|
|
220
|
+
declare function createCacheMiddleware<TMeta = unknown>(options?: CacheMiddlewareOptions<TMeta>): CacheMiddleware<TMeta>;
|
|
217
221
|
//#endregion
|
|
218
|
-
export { type
|
|
222
|
+
export { type CacheAnnotationHandle, type CacheAnnotationOptions, type CacheEntry, type CacheMiddleware, type CacheMiddlewareOptions, type CacheStore, type CachedRows, type InMemoryCacheStoreOptions, cacheAnnotation, createCacheMiddleware, createInMemoryCacheStore, deriveKeyFromContentHash };
|
|
219
223
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../../../3-extensions/middleware-cache/dist/index.d.mts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../../../3-extensions/middleware-cache/dist/index.d.mts"],"mappings":";;;;;;;;;;;;;;;UAcU,uBAAuB;WACtB;WACA,OAAO;WACP;;;;;;KAMN,0BAA0B,iBAAiB,SAAS,uBAAuB,WAAW,gBAAgB;WAChG;WACA,cAAc;WACd,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;cA4BpB,iBAAiB;;;;;;;;KAQ1B,sBAAsB;;;;;;;;;;;UAWjB,WAAW,iBAAiB,SAAS;WACpC;WACA,MAAM;WACN;WACA;aACE;;aAEA;aACA,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAgCV,WAAW,iBAAiB,SAAS;EAC7C,IAAI;aACO;aACA,MAAM;MACb,QAAQ,WAAW,OAAO;EAC9B,IAAI,OAAO,WAAW,OAAO,SAAS,OAAO,SAAS;EACtD,MAAM;aACK;aACA,MAAM;MACb;;;;;;;;;;;;;;;UAeI;WACC;WACA;WACA;;;;;;;;;iBASM,yBAAyB,iBAAiB,SAAS,YAAY,UAAU,4BAA4B,WAAW,OAAO;;;;;;;;;;;;;;;UAe9H,uBAAuB;WACtB,QAAQ,WAAW,OAAO;WAC1B,aAAa,MAAM,eAAe,KAAK,sCAAsC;;;;;;;;;;;;;KAanF,gBAAgB,mBAAmB;WAC7B,aAAa;aACX;aACA,OAAO;QACZ;;;;;;iBAMS,yBAAyB,MAAM,eAAe,KAAK,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA6B9E,sBAAsB,iBAAiB,UAAU,uBAAuB,SAAS,gBAAgB"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,207 +1,194 @@
|
|
|
1
1
|
import { defineAnnotation } from "@prisma/orm-framework/components/runtime";
|
|
2
|
+
import { structuredError } from "@prisma/orm-framework/utils/structured-error";
|
|
2
3
|
//#region ../../../3-extensions/middleware-cache/dist/index.mjs
|
|
3
4
|
/**
|
|
4
|
-
*
|
|
5
|
+
* Marks a read for the cache middleware. It applies to reads only: a write terminal refuses it at
|
|
6
|
+
* compile time and at run time. The middleware reads it from `plan.meta.annotations.cache`.
|
|
5
7
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* (
|
|
9
|
-
* check); the join `K extends Kinds` fails for this annotation, making
|
|
10
|
-
* "cache a mutation" structurally impossible without an `as any` cast
|
|
11
|
-
* bypass at both type *and* runtime levels.
|
|
12
|
-
*
|
|
13
|
-
* Stored under namespace `'cache'` in `plan.meta.annotations`. The cache
|
|
14
|
-
* middleware reads it via `cacheAnnotation.read(plan)`.
|
|
8
|
+
* `TMeta` types `meta` for this call only. Nothing ties it to the store's `TMeta`, so a store
|
|
9
|
+
* package should export a wrapper typed with its own meta, such as
|
|
10
|
+
* `(o: CacheAnnotationOptions<TagMeta>) => cacheAnnotation<TagMeta>(o)`.
|
|
15
11
|
*
|
|
16
12
|
* @example
|
|
17
13
|
* ```typescript
|
|
18
14
|
* import { cacheAnnotation } from '@internal/middleware-cache';
|
|
19
15
|
*
|
|
20
16
|
* // ORM read terminal — accepts the read-only annotation via the meta callback.
|
|
21
|
-
* const user = await db.User.first(
|
|
17
|
+
* const user = await db.orm.public.User.first(
|
|
22
18
|
* { id },
|
|
23
|
-
* (meta) => meta.annotate(cacheAnnotation({
|
|
19
|
+
* (meta) => meta.annotate(cacheAnnotation({ key: `user-${id}` })),
|
|
24
20
|
* );
|
|
25
21
|
*
|
|
26
22
|
* // SQL DSL select builder — chainable.
|
|
27
23
|
* const plan = db.sql
|
|
28
24
|
* .from(tables.user)
|
|
29
|
-
* .annotate(cacheAnnotation({
|
|
25
|
+
* .annotate(cacheAnnotation({}))
|
|
30
26
|
* .select({ id: tables.user.columns.id })
|
|
31
27
|
* .build();
|
|
32
28
|
* ```
|
|
33
29
|
*/
|
|
34
|
-
const cacheAnnotation = defineAnnotation()({
|
|
30
|
+
const cacheAnnotation = withTypedMeta(defineAnnotation()({
|
|
35
31
|
namespace: "cache",
|
|
36
32
|
applicableTo: ["read"]
|
|
37
|
-
});
|
|
33
|
+
}));
|
|
34
|
+
function withTypedMeta(handle) {
|
|
35
|
+
function cacheAnnotationWithMeta(options) {
|
|
36
|
+
return handle(options);
|
|
37
|
+
}
|
|
38
|
+
return Object.freeze(Object.assign(cacheAnnotationWithMeta, {
|
|
39
|
+
namespace: handle.namespace,
|
|
40
|
+
applicableTo: handle.applicableTo,
|
|
41
|
+
read: handle.read
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
function metaUnsupported() {
|
|
45
|
+
return structuredError("RUNTIME.CACHE_STORE_META_UNSUPPORTED", "The in-memory cache store cannot remove entries by meta because it does not index meta", { fix: "Invalidate by keys, or supply a CacheStore that indexes meta in set and unset." });
|
|
46
|
+
}
|
|
47
|
+
function invalidOption(argument, received, expected) {
|
|
48
|
+
return structuredError("RUNTIME.ARGUMENT_INVALID", `createInMemoryCacheStore: ${argument} must be ${expected}`, {
|
|
49
|
+
fix: `Pass ${argument} as ${expected}, or leave it unset for the default.`,
|
|
50
|
+
meta: {
|
|
51
|
+
helper: "createInMemoryCacheStore",
|
|
52
|
+
argument,
|
|
53
|
+
received
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
38
57
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* above `maxEntries`, the least recently used entry is evicted.
|
|
45
|
-
* Setting an existing key updates the entry in place and refreshes its
|
|
46
|
-
* recency without changing the live count.
|
|
47
|
-
* - On `get` of an existing key, recency is bumped (so the entry is no
|
|
48
|
-
* longer the LRU candidate).
|
|
49
|
-
* - On `get` of an expired entry, the entry is removed from the map and
|
|
50
|
-
* `undefined` is returned. The slot becomes available for new writes
|
|
51
|
-
* without counting against `maxEntries`.
|
|
52
|
-
*
|
|
53
|
-
* `Map` insertion order is the LRU order: the first key is the LRU
|
|
54
|
-
* candidate; the last key is the most recently used. Bumping recency is
|
|
55
|
-
* a delete-then-set on the underlying map.
|
|
56
|
-
*
|
|
57
|
-
* The default store is **not** coherent across processes or replicas —
|
|
58
|
-
* each process holds its own Map. Users who need a shared cache supply
|
|
59
|
-
* their own `CacheStore` (Redis, Memcached, etc.).
|
|
58
|
+
* The default cache store: a least-recently-used map with one lifetime for every value, local to
|
|
59
|
+
* the process. It holds `CachedRows` unless given another `TValue`. A key's version is kept for
|
|
60
|
+
* `ttlMs` after the `unset` that moved it (see `InMemoryCacheStoreOptions`). It ignores `meta` in
|
|
61
|
+
* `get` and `set`, and its `unset` rejects any `meta`, including `null`, before changing anything.
|
|
62
|
+
* It throws `RUNTIME.ARGUMENT_INVALID` for a `maxEntries` or `ttlMs` outside the ranges above.
|
|
60
63
|
*/
|
|
61
64
|
function createInMemoryCacheStore(options) {
|
|
62
|
-
const maxEntries = options
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
65
|
+
const maxEntries = options?.maxEntries ?? 1e3;
|
|
66
|
+
const ttlMs = options?.ttlMs ?? 6e4;
|
|
67
|
+
const clock = options?.clock ?? Date.now;
|
|
68
|
+
if (!Number.isInteger(maxEntries) || maxEntries <= 0) throw invalidOption("maxEntries", maxEntries, "a positive integer");
|
|
69
|
+
if (!(ttlMs > 0)) throw invalidOption("ttlMs", ttlMs, "a positive number of milliseconds, or Infinity");
|
|
70
|
+
const records = /* @__PURE__ */ new Map();
|
|
71
|
+
const versions = /* @__PURE__ */ new Map();
|
|
72
|
+
function forgetExpiredVersions() {
|
|
73
|
+
const now = clock();
|
|
74
|
+
for (const [key, record] of versions) {
|
|
75
|
+
if (now < record.expiresAt) return;
|
|
76
|
+
versions.delete(key);
|
|
71
77
|
}
|
|
72
|
-
map.delete(key);
|
|
73
|
-
map.set(key, record);
|
|
74
|
-
return Promise.resolve(record.entry);
|
|
75
78
|
}
|
|
76
|
-
function
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
function versionOf(key) {
|
|
80
|
+
forgetExpiredVersions();
|
|
81
|
+
return versions.get(key)?.version ?? 0;
|
|
82
|
+
}
|
|
83
|
+
function liveRecord(key) {
|
|
84
|
+
const record = records.get(key);
|
|
85
|
+
if (record === void 0) return;
|
|
86
|
+
records.delete(key);
|
|
87
|
+
if (clock() >= record.expiresAt) return;
|
|
88
|
+
records.set(key, record);
|
|
89
|
+
return record;
|
|
90
|
+
}
|
|
91
|
+
async function get(target) {
|
|
92
|
+
const record = liveRecord(target.key);
|
|
93
|
+
return {
|
|
94
|
+
key: target.key,
|
|
95
|
+
meta: target.meta,
|
|
96
|
+
version: versionOf(target.key),
|
|
97
|
+
data: record === void 0 ? { empty: true } : {
|
|
98
|
+
empty: false,
|
|
99
|
+
value: record.value
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
async function set(entry, value) {
|
|
104
|
+
if (entry.version !== versionOf(entry.key)) return false;
|
|
105
|
+
records.delete(entry.key);
|
|
106
|
+
records.set(entry.key, {
|
|
107
|
+
value,
|
|
108
|
+
expiresAt: clock() + ttlMs
|
|
82
109
|
});
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
110
|
+
for (const leastRecentlyUsed of records.keys()) {
|
|
111
|
+
if (records.size <= maxEntries) break;
|
|
112
|
+
records.delete(leastRecentlyUsed);
|
|
113
|
+
}
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
async function unset(target) {
|
|
117
|
+
if (target.meta !== void 0) throw metaUnsupported();
|
|
118
|
+
for (const key of target.keys ?? []) {
|
|
119
|
+
records.delete(key);
|
|
120
|
+
const version = versionOf(key) + 1;
|
|
121
|
+
versions.delete(key);
|
|
122
|
+
versions.set(key, {
|
|
123
|
+
version,
|
|
124
|
+
expiresAt: clock() + ttlMs
|
|
125
|
+
});
|
|
87
126
|
}
|
|
88
|
-
return Promise.resolve();
|
|
89
127
|
}
|
|
90
128
|
return {
|
|
91
129
|
get,
|
|
92
|
-
set
|
|
130
|
+
set,
|
|
131
|
+
unset
|
|
93
132
|
};
|
|
94
133
|
}
|
|
95
134
|
/**
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
* different bounds supply a custom `CacheStore`.
|
|
99
|
-
*/
|
|
100
|
-
const DEFAULT_MAX_ENTRIES = 1e3;
|
|
101
|
-
/**
|
|
102
|
-
* Reads the cache payload from the plan, if present and branded.
|
|
103
|
-
*
|
|
104
|
-
* Returns `undefined` when:
|
|
105
|
-
* - the plan has no `meta.annotations`, or
|
|
106
|
-
* - the `cache` namespace key is absent, or
|
|
107
|
-
* - the value under `cache` is not a branded `AnnotationValue` (the
|
|
108
|
-
* `cacheAnnotation.read` defensive check covers this).
|
|
109
|
-
*/
|
|
110
|
-
function readCachePayload(plan) {
|
|
111
|
-
return cacheAnnotation.read(plan);
|
|
112
|
-
}
|
|
113
|
-
/**
|
|
114
|
-
* Computes the cache key for an execution.
|
|
115
|
-
*
|
|
116
|
-
* Two-tier resolution:
|
|
117
|
-
*
|
|
118
|
-
* 1. Per-query override: `cacheAnnotation({ key })` — the supplied
|
|
119
|
-
* string is used verbatim. Not rehashed; the user is responsible for
|
|
120
|
-
* keeping the string bounded and free of sensitive data.
|
|
121
|
-
* 2. Default: `ctx.contentHash(exec)` — the family runtime owns this and
|
|
122
|
-
* returns an opaque, bounded digest (SHA-512 in the SQL and Mongo
|
|
123
|
-
* runtimes today).
|
|
124
|
-
*
|
|
125
|
-
* The returned string is consumed directly as the `Map<string, …>` key
|
|
126
|
-
* by the underlying `CacheStore`; the cache middleware does not perform
|
|
127
|
-
* any further transformation.
|
|
135
|
+
* The default `deriveKey`: the family runtime's content hash of the plan, which covers the
|
|
136
|
+
* statement, its parameters and the storage hash.
|
|
128
137
|
*/
|
|
129
|
-
|
|
130
|
-
if (payload.key !== void 0) return payload.key;
|
|
138
|
+
function deriveKeyFromContentHash(exec, ctx) {
|
|
131
139
|
return ctx.contentHash(exec);
|
|
132
140
|
}
|
|
133
141
|
/**
|
|
134
|
-
* Creates a
|
|
135
|
-
*
|
|
136
|
-
* The middleware uses three hooks:
|
|
137
|
-
*
|
|
138
|
-
* - `interceptQuery` — on each execution, checks the cache. On a hit, returns
|
|
139
|
-
* the cached raw rows; the runtime skips `runDriver` and `onRow`
|
|
140
|
-
* (`beforeQuery` is not affected — it has already run for every
|
|
141
|
-
* middleware before any `interceptQuery` is consulted) and yields the
|
|
142
|
-
* cached rows to the consumer (which, in the SQL runtime, sees them
|
|
143
|
-
* after the standard `decodeRow` pass — i.e. the cache stores
|
|
144
|
-
* wire-format values). On a miss, records a pending buffer keyed on
|
|
145
|
-
* the `exec` object identity and returns `undefined` (passthrough).
|
|
146
|
-
* - `onRow` — on the miss path, appends each row yielded by the driver
|
|
147
|
-
* to the pending buffer.
|
|
148
|
-
* - `afterQuery` — on the miss path, commits the buffer to the store
|
|
149
|
-
* if and only if `result.completed === true && result.source === 'driver'`.
|
|
150
|
-
* Failed executions and middleware-served executions never populate
|
|
151
|
-
* the cache. The pending buffer is cleared in all branches so a stale
|
|
152
|
-
* `WeakMap` entry cannot leak between executions sharing an `exec`.
|
|
142
|
+
* Creates a read-through cache middleware that works with every family runtime.
|
|
153
143
|
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
* - the annotation has no `ttl`, or
|
|
158
|
-
* - `ctx.scope !== 'runtime'` (connection / transaction scopes opt out).
|
|
144
|
+
* It caches a read when the plan carries `cacheAnnotation`, the annotation does not set
|
|
145
|
+
* `bypass`, and the read runs in runtime scope (not inside a connection or transaction). The key
|
|
146
|
+
* is the annotation's `key`, else `deriveKey(exec, ctx)`.
|
|
159
147
|
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
* `
|
|
164
|
-
*
|
|
148
|
+
* - `interceptQuery` — on a hit, returns the stored rows and the driver does not run. On a miss,
|
|
149
|
+
* starts collecting rows.
|
|
150
|
+
* - `onRow` — collects each row of a miss.
|
|
151
|
+
* - `afterQuery` — when the driver completed the read, calls `store.set(entry, rows)` with the
|
|
152
|
+
* entry `store.get` returned, which stores only if the key's version has not moved. If an
|
|
153
|
+
* `unset` moved it meanwhile, `set` returns `false` and the middleware logs
|
|
154
|
+
* `middleware.cache.store-skipped`.
|
|
165
155
|
*
|
|
166
156
|
* @example
|
|
167
157
|
* ```typescript
|
|
168
|
-
*
|
|
158
|
+
* const cache = createCacheMiddleware();
|
|
159
|
+
* const db = postgres<Contract>({ contractJson, url, middleware: [cache] });
|
|
169
160
|
*
|
|
170
|
-
* const
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
* middleware: [createCacheMiddleware({ maxEntries: 1000 })],
|
|
174
|
-
* });
|
|
175
|
-
*
|
|
176
|
-
* const user = await db.User.first(
|
|
177
|
-
* { id },
|
|
178
|
-
* (meta) => meta.annotate(cacheAnnotation({ ttl: 60_000 })),
|
|
161
|
+
* const user = await db.orm.public.User.first(
|
|
162
|
+
* { id: 1 },
|
|
163
|
+
* (meta) => meta.annotate(cacheAnnotation({ key: 'user-1' })),
|
|
179
164
|
* );
|
|
165
|
+
* await db.orm.public.User.where({ id: 1 }).update({ name: 'Alicia' });
|
|
166
|
+
* await cache.invalidate({ keys: ['user-1'] });
|
|
180
167
|
* ```
|
|
181
168
|
*/
|
|
182
169
|
function createCacheMiddleware(options) {
|
|
183
|
-
const store = options?.store ?? createInMemoryCacheStore(
|
|
184
|
-
const
|
|
170
|
+
const store = options?.store ?? createInMemoryCacheStore();
|
|
171
|
+
const deriveKey = options?.deriveKey ?? deriveKeyFromContentHash;
|
|
185
172
|
const pending = /* @__PURE__ */ new WeakMap();
|
|
186
173
|
async function interceptQuery(exec, ctx) {
|
|
187
174
|
if (ctx.scope !== "runtime") return;
|
|
188
|
-
const
|
|
189
|
-
if (
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
175
|
+
const annotation = cacheAnnotation.read(exec);
|
|
176
|
+
if (annotation === void 0 || annotation.bypass === true) return;
|
|
177
|
+
const key = annotation.key ?? await deriveKey(exec, ctx);
|
|
178
|
+
const entry = await store.get({
|
|
179
|
+
key,
|
|
180
|
+
meta: annotation.meta
|
|
181
|
+
});
|
|
182
|
+
if (!entry.data.empty) {
|
|
195
183
|
ctx.log.debug?.({
|
|
196
184
|
event: "middleware.cache.hit",
|
|
197
185
|
middleware: "cache",
|
|
198
186
|
key
|
|
199
187
|
});
|
|
200
|
-
return { rows:
|
|
188
|
+
return { rows: entry.data.value };
|
|
201
189
|
}
|
|
202
190
|
pending.set(exec, {
|
|
203
|
-
|
|
204
|
-
ttlMs: payload.ttl,
|
|
191
|
+
entry,
|
|
205
192
|
buffer: []
|
|
206
193
|
});
|
|
207
194
|
ctx.log.debug?.({
|
|
@@ -211,33 +198,37 @@ function createCacheMiddleware(options) {
|
|
|
211
198
|
});
|
|
212
199
|
}
|
|
213
200
|
async function onRow(row, exec, _ctx) {
|
|
214
|
-
|
|
215
|
-
if (slot === void 0) return;
|
|
216
|
-
slot.buffer.push(row);
|
|
201
|
+
pending.get(exec)?.buffer.push(row);
|
|
217
202
|
}
|
|
218
203
|
async function afterQuery(exec, result, ctx) {
|
|
219
|
-
const
|
|
220
|
-
if (
|
|
204
|
+
const miss = pending.get(exec);
|
|
205
|
+
if (miss === void 0) return;
|
|
221
206
|
pending.delete(exec);
|
|
222
207
|
if (!result.completed || result.source !== "driver") return;
|
|
223
|
-
await store.set(
|
|
224
|
-
rows: slot.buffer,
|
|
225
|
-
storedAt: clock()
|
|
226
|
-
}, slot.ttlMs);
|
|
208
|
+
const stored = await store.set(miss.entry, miss.buffer);
|
|
227
209
|
ctx.log.debug?.({
|
|
228
|
-
event: "middleware.cache.store",
|
|
210
|
+
event: stored ? "middleware.cache.store" : "middleware.cache.store-skipped",
|
|
229
211
|
middleware: "cache",
|
|
230
|
-
key:
|
|
212
|
+
key: miss.entry.key
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
async function invalidate(target) {
|
|
216
|
+
const keys = target.keys !== void 0 && target.keys.length > 0 ? target.keys : void 0;
|
|
217
|
+
if (keys === void 0 && target.meta === void 0) return;
|
|
218
|
+
await store.unset({
|
|
219
|
+
keys,
|
|
220
|
+
meta: target.meta
|
|
231
221
|
});
|
|
232
222
|
}
|
|
233
223
|
return {
|
|
234
224
|
name: "cache",
|
|
235
225
|
interceptQuery,
|
|
236
226
|
onRow,
|
|
237
|
-
afterQuery
|
|
227
|
+
afterQuery,
|
|
228
|
+
invalidate
|
|
238
229
|
};
|
|
239
230
|
}
|
|
240
231
|
//#endregion
|
|
241
|
-
export { cacheAnnotation, createCacheMiddleware, createInMemoryCacheStore };
|
|
232
|
+
export { cacheAnnotation, createCacheMiddleware, createInMemoryCacheStore, deriveKeyFromContentHash };
|
|
242
233
|
|
|
243
234
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../../../3-extensions/middleware-cache/dist/index.mjs"],"sourcesContent":["import { defineAnnotation } from \"@internal/framework-components/runtime\";\n//#region src/cache-annotation.ts\n/**\n* Read-only annotation handle for the cache middleware.\n*\n* Declared with `applicableTo: ['read']`. Write terminals supply\n* `K = 'write'` to the type-level `ValidAnnotations<'write', As>` gate\n* (and the runtime `assertAnnotationsApplicable(annotations, 'write', ...)`\n* check); the join `K extends Kinds` fails for this annotation, making\n* \"cache a mutation\" structurally impossible without an `as any` cast\n* bypass at both type *and* runtime levels.\n*\n* Stored under namespace `'cache'` in `plan.meta.annotations`. The cache\n* middleware reads it via `cacheAnnotation.read(plan)`.\n*\n* @example\n* ```typescript\n* import { cacheAnnotation } from '@internal/middleware-cache';\n*\n* // ORM read terminal — accepts the read-only annotation via the meta callback.\n* const user = await db.User.first(\n* { id },\n* (meta) => meta.annotate(cacheAnnotation({ ttl: 60_000 })),\n* );\n*\n* // SQL DSL select builder — chainable.\n* const plan = db.sql\n* .from(tables.user)\n* .annotate(cacheAnnotation({ ttl: 60_000 }))\n* .select({ id: tables.user.columns.id })\n* .build();\n* ```\n*/\nconst cacheAnnotation = defineAnnotation()({\n\tnamespace: \"cache\",\n\tapplicableTo: [\"read\"]\n});\n//#endregion\n//#region src/cache-store.ts\n/**\n* Default cache backend. An LRU with per-entry TTL, backed by a `Map`.\n*\n* Eviction policy:\n*\n* - On `set` of a fresh key whose insertion would push the live count\n* above `maxEntries`, the least recently used entry is evicted.\n* Setting an existing key updates the entry in place and refreshes its\n* recency without changing the live count.\n* - On `get` of an existing key, recency is bumped (so the entry is no\n* longer the LRU candidate).\n* - On `get` of an expired entry, the entry is removed from the map and\n* `undefined` is returned. The slot becomes available for new writes\n* without counting against `maxEntries`.\n*\n* `Map` insertion order is the LRU order: the first key is the LRU\n* candidate; the last key is the most recently used. Bumping recency is\n* a delete-then-set on the underlying map.\n*\n* The default store is **not** coherent across processes or replicas —\n* each process holds its own Map. Users who need a shared cache supply\n* their own `CacheStore` (Redis, Memcached, etc.).\n*/\nfunction createInMemoryCacheStore(options) {\n\tconst maxEntries = options.maxEntries;\n\tconst clock = options.clock ?? Date.now;\n\tconst map = /* @__PURE__ */ new Map();\n\tfunction get(key) {\n\t\tconst record = map.get(key);\n\t\tif (record === void 0) return Promise.resolve(void 0);\n\t\tif (clock() >= record.expiresAt) {\n\t\t\tmap.delete(key);\n\t\t\treturn Promise.resolve(void 0);\n\t\t}\n\t\tmap.delete(key);\n\t\tmap.set(key, record);\n\t\treturn Promise.resolve(record.entry);\n\t}\n\tfunction set(key, entry, ttlMs) {\n\t\tconst expiresAt = clock() + ttlMs;\n\t\tif (map.has(key)) map.delete(key);\n\t\tmap.set(key, {\n\t\t\tentry,\n\t\t\texpiresAt\n\t\t});\n\t\twhile (map.size > maxEntries) {\n\t\t\tconst oldest = map.keys().next();\n\t\t\tif (oldest.done) break;\n\t\t\tmap.delete(oldest.value);\n\t\t}\n\t\treturn Promise.resolve();\n\t}\n\treturn {\n\t\tget,\n\t\tset\n\t};\n}\n//#endregion\n//#region src/cache-middleware.ts\n/**\n* Default `maxEntries` for the built-in in-memory store. Bounded so a\n* runaway producer cannot exhaust process memory; users who need\n* different bounds supply a custom `CacheStore`.\n*/\nconst DEFAULT_MAX_ENTRIES = 1e3;\n/**\n* Reads the cache payload from the plan, if present and branded.\n*\n* Returns `undefined` when:\n* - the plan has no `meta.annotations`, or\n* - the `cache` namespace key is absent, or\n* - the value under `cache` is not a branded `AnnotationValue` (the\n* `cacheAnnotation.read` defensive check covers this).\n*/\nfunction readCachePayload(plan) {\n\treturn cacheAnnotation.read(plan);\n}\n/**\n* Computes the cache key for an execution.\n*\n* Two-tier resolution:\n*\n* 1. Per-query override: `cacheAnnotation({ key })` — the supplied\n* string is used verbatim. Not rehashed; the user is responsible for\n* keeping the string bounded and free of sensitive data.\n* 2. Default: `ctx.contentHash(exec)` — the family runtime owns this and\n* returns an opaque, bounded digest (SHA-512 in the SQL and Mongo\n* runtimes today).\n*\n* The returned string is consumed directly as the `Map<string, …>` key\n* by the underlying `CacheStore`; the cache middleware does not perform\n* any further transformation.\n*/\nasync function resolveCacheKey(payload, exec, ctx) {\n\tif (payload.key !== void 0) return payload.key;\n\treturn ctx.contentHash(exec);\n}\n/**\n* Creates a family-agnostic caching middleware.\n*\n* The middleware uses three hooks:\n*\n* - `interceptQuery` — on each execution, checks the cache. On a hit, returns\n* the cached raw rows; the runtime skips `runDriver` and `onRow`\n* (`beforeQuery` is not affected — it has already run for every\n* middleware before any `interceptQuery` is consulted) and yields the\n* cached rows to the consumer (which, in the SQL runtime, sees them\n* after the standard `decodeRow` pass — i.e. the cache stores\n* wire-format values). On a miss, records a pending buffer keyed on\n* the `exec` object identity and returns `undefined` (passthrough).\n* - `onRow` — on the miss path, appends each row yielded by the driver\n* to the pending buffer.\n* - `afterQuery` — on the miss path, commits the buffer to the store\n* if and only if `result.completed === true && result.source === 'driver'`.\n* Failed executions and middleware-served executions never populate\n* the cache. The pending buffer is cleared in all branches so a stale\n* `WeakMap` entry cannot leak between executions sharing an `exec`.\n*\n* The middleware bypasses the cache entirely when:\n* - the plan has no `cache` annotation, or\n* - the annotation has `skip: true`, or\n* - the annotation has no `ttl`, or\n* - `ctx.scope !== 'runtime'` (connection / transaction scopes opt out).\n*\n* Returns a cross-family `RuntimeMiddleware` (no `familyId` /\n* `targetId`). The package depends only on\n* `@internal/framework-components/runtime`; cache keys come from\n* `ctx.contentHash(exec)`, populated by the family runtime, so SQL and\n* Mongo runtimes both work out of the box.\n*\n* @example\n* ```typescript\n* import { createCacheMiddleware, cacheAnnotation } from '@internal/middleware-cache';\n*\n* const db = postgres({\n* contractJson,\n* url: process.env['DATABASE_URL']!,\n* middleware: [createCacheMiddleware({ maxEntries: 1000 })],\n* });\n*\n* const user = await db.User.first(\n* { id },\n* (meta) => meta.annotate(cacheAnnotation({ ttl: 60_000 })),\n* );\n* ```\n*/\nfunction createCacheMiddleware(options) {\n\tconst store = options?.store ?? createInMemoryCacheStore({ maxEntries: options?.maxEntries ?? DEFAULT_MAX_ENTRIES });\n\tconst clock = options?.clock ?? Date.now;\n\tconst pending = /* @__PURE__ */ new WeakMap();\n\tasync function interceptQuery(exec, ctx) {\n\t\tif (ctx.scope !== \"runtime\") return;\n\t\tconst payload = readCachePayload(exec);\n\t\tif (payload === void 0) return;\n\t\tif (payload.skip === true) return;\n\t\tif (payload.ttl === void 0) return;\n\t\tconst key = await resolveCacheKey(payload, exec, ctx);\n\t\tconst hit = await store.get(key);\n\t\tif (hit !== void 0) {\n\t\t\tctx.log.debug?.({\n\t\t\t\tevent: \"middleware.cache.hit\",\n\t\t\t\tmiddleware: \"cache\",\n\t\t\t\tkey\n\t\t\t});\n\t\t\treturn { rows: hit.rows };\n\t\t}\n\t\tpending.set(exec, {\n\t\t\tkey,\n\t\t\tttlMs: payload.ttl,\n\t\t\tbuffer: []\n\t\t});\n\t\tctx.log.debug?.({\n\t\t\tevent: \"middleware.cache.miss\",\n\t\t\tmiddleware: \"cache\",\n\t\t\tkey\n\t\t});\n\t}\n\tasync function onRow(row, exec, _ctx) {\n\t\tconst slot = pending.get(exec);\n\t\tif (slot === void 0) return;\n\t\tslot.buffer.push(row);\n\t}\n\tasync function afterQuery(exec, result, ctx) {\n\t\tconst slot = pending.get(exec);\n\t\tif (slot === void 0) return;\n\t\tpending.delete(exec);\n\t\tif (!result.completed || result.source !== \"driver\") return;\n\t\tawait store.set(slot.key, {\n\t\t\trows: slot.buffer,\n\t\t\tstoredAt: clock()\n\t\t}, slot.ttlMs);\n\t\tctx.log.debug?.({\n\t\t\tevent: \"middleware.cache.store\",\n\t\t\tmiddleware: \"cache\",\n\t\t\tkey: slot.key\n\t\t});\n\t}\n\treturn {\n\t\tname: \"cache\",\n\t\tinterceptQuery,\n\t\tonRow,\n\t\tafterQuery\n\t};\n}\n//#endregion\nexport { cacheAnnotation, createCacheMiddleware, createInMemoryCacheStore };\n\n//# sourceMappingURL=index.mjs.map"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../../../3-extensions/middleware-cache/dist/index.mjs"],"sourcesContent":["import { defineAnnotation } from \"@internal/framework-components/runtime\";\nimport { structuredError } from \"@internal/utils/structured-error\";\n//#region src/cache-annotation.ts\n/**\n* Marks a read for the cache middleware. It applies to reads only: a write terminal refuses it at\n* compile time and at run time. The middleware reads it from `plan.meta.annotations.cache`.\n*\n* `TMeta` types `meta` for this call only. Nothing ties it to the store's `TMeta`, so a store\n* package should export a wrapper typed with its own meta, such as\n* `(o: CacheAnnotationOptions<TagMeta>) => cacheAnnotation<TagMeta>(o)`.\n*\n* @example\n* ```typescript\n* import { cacheAnnotation } from '@internal/middleware-cache';\n*\n* // ORM read terminal — accepts the read-only annotation via the meta callback.\n* const user = await db.orm.public.User.first(\n* { id },\n* (meta) => meta.annotate(cacheAnnotation({ key: `user-${id}` })),\n* );\n*\n* // SQL DSL select builder — chainable.\n* const plan = db.sql\n* .from(tables.user)\n* .annotate(cacheAnnotation({}))\n* .select({ id: tables.user.columns.id })\n* .build();\n* ```\n*/\nconst cacheAnnotation = withTypedMeta(defineAnnotation()({\n\tnamespace: \"cache\",\n\tapplicableTo: [\"read\"]\n}));\nfunction withTypedMeta(handle) {\n\tfunction cacheAnnotationWithMeta(options) {\n\t\treturn handle(options);\n\t}\n\treturn Object.freeze(Object.assign(cacheAnnotationWithMeta, {\n\t\tnamespace: handle.namespace,\n\t\tapplicableTo: handle.applicableTo,\n\t\tread: handle.read\n\t}));\n}\n//#endregion\n//#region src/cache-store.ts\nfunction metaUnsupported() {\n\treturn structuredError(\"RUNTIME.CACHE_STORE_META_UNSUPPORTED\", \"The in-memory cache store cannot remove entries by meta because it does not index meta\", { fix: \"Invalidate by keys, or supply a CacheStore that indexes meta in set and unset.\" });\n}\nfunction invalidOption(argument, received, expected) {\n\treturn structuredError(\"RUNTIME.ARGUMENT_INVALID\", `createInMemoryCacheStore: ${argument} must be ${expected}`, {\n\t\tfix: `Pass ${argument} as ${expected}, or leave it unset for the default.`,\n\t\tmeta: {\n\t\t\thelper: \"createInMemoryCacheStore\",\n\t\t\targument,\n\t\t\treceived\n\t\t}\n\t});\n}\n/**\n* The default cache store: a least-recently-used map with one lifetime for every value, local to\n* the process. It holds `CachedRows` unless given another `TValue`. A key's version is kept for\n* `ttlMs` after the `unset` that moved it (see `InMemoryCacheStoreOptions`). It ignores `meta` in\n* `get` and `set`, and its `unset` rejects any `meta`, including `null`, before changing anything.\n* It throws `RUNTIME.ARGUMENT_INVALID` for a `maxEntries` or `ttlMs` outside the ranges above.\n*/\nfunction createInMemoryCacheStore(options) {\n\tconst maxEntries = options?.maxEntries ?? 1e3;\n\tconst ttlMs = options?.ttlMs ?? 6e4;\n\tconst clock = options?.clock ?? Date.now;\n\tif (!Number.isInteger(maxEntries) || maxEntries <= 0) throw invalidOption(\"maxEntries\", maxEntries, \"a positive integer\");\n\tif (!(ttlMs > 0)) throw invalidOption(\"ttlMs\", ttlMs, \"a positive number of milliseconds, or Infinity\");\n\tconst records = /* @__PURE__ */ new Map();\n\tconst versions = /* @__PURE__ */ new Map();\n\tfunction forgetExpiredVersions() {\n\t\tconst now = clock();\n\t\tfor (const [key, record] of versions) {\n\t\t\tif (now < record.expiresAt) return;\n\t\t\tversions.delete(key);\n\t\t}\n\t}\n\tfunction versionOf(key) {\n\t\tforgetExpiredVersions();\n\t\treturn versions.get(key)?.version ?? 0;\n\t}\n\tfunction liveRecord(key) {\n\t\tconst record = records.get(key);\n\t\tif (record === void 0) return;\n\t\trecords.delete(key);\n\t\tif (clock() >= record.expiresAt) return;\n\t\trecords.set(key, record);\n\t\treturn record;\n\t}\n\tasync function get(target) {\n\t\tconst record = liveRecord(target.key);\n\t\treturn {\n\t\t\tkey: target.key,\n\t\t\tmeta: target.meta,\n\t\t\tversion: versionOf(target.key),\n\t\t\tdata: record === void 0 ? { empty: true } : {\n\t\t\t\tempty: false,\n\t\t\t\tvalue: record.value\n\t\t\t}\n\t\t};\n\t}\n\tasync function set(entry, value) {\n\t\tif (entry.version !== versionOf(entry.key)) return false;\n\t\trecords.delete(entry.key);\n\t\trecords.set(entry.key, {\n\t\t\tvalue,\n\t\t\texpiresAt: clock() + ttlMs\n\t\t});\n\t\tfor (const leastRecentlyUsed of records.keys()) {\n\t\t\tif (records.size <= maxEntries) break;\n\t\t\trecords.delete(leastRecentlyUsed);\n\t\t}\n\t\treturn true;\n\t}\n\tasync function unset(target) {\n\t\tif (target.meta !== void 0) throw metaUnsupported();\n\t\tfor (const key of target.keys ?? []) {\n\t\t\trecords.delete(key);\n\t\t\tconst version = versionOf(key) + 1;\n\t\t\tversions.delete(key);\n\t\t\tversions.set(key, {\n\t\t\t\tversion,\n\t\t\t\texpiresAt: clock() + ttlMs\n\t\t\t});\n\t\t}\n\t}\n\treturn {\n\t\tget,\n\t\tset,\n\t\tunset\n\t};\n}\n//#endregion\n//#region src/cache-middleware.ts\n/**\n* The default `deriveKey`: the family runtime's content hash of the plan, which covers the\n* statement, its parameters and the storage hash.\n*/\nfunction deriveKeyFromContentHash(exec, ctx) {\n\treturn ctx.contentHash(exec);\n}\n/**\n* Creates a read-through cache middleware that works with every family runtime.\n*\n* It caches a read when the plan carries `cacheAnnotation`, the annotation does not set\n* `bypass`, and the read runs in runtime scope (not inside a connection or transaction). The key\n* is the annotation's `key`, else `deriveKey(exec, ctx)`.\n*\n* - `interceptQuery` — on a hit, returns the stored rows and the driver does not run. On a miss,\n* starts collecting rows.\n* - `onRow` — collects each row of a miss.\n* - `afterQuery` — when the driver completed the read, calls `store.set(entry, rows)` with the\n* entry `store.get` returned, which stores only if the key's version has not moved. If an\n* `unset` moved it meanwhile, `set` returns `false` and the middleware logs\n* `middleware.cache.store-skipped`.\n*\n* @example\n* ```typescript\n* const cache = createCacheMiddleware();\n* const db = postgres<Contract>({ contractJson, url, middleware: [cache] });\n*\n* const user = await db.orm.public.User.first(\n* { id: 1 },\n* (meta) => meta.annotate(cacheAnnotation({ key: 'user-1' })),\n* );\n* await db.orm.public.User.where({ id: 1 }).update({ name: 'Alicia' });\n* await cache.invalidate({ keys: ['user-1'] });\n* ```\n*/\nfunction createCacheMiddleware(options) {\n\tconst store = options?.store ?? createInMemoryCacheStore();\n\tconst deriveKey = options?.deriveKey ?? deriveKeyFromContentHash;\n\tconst pending = /* @__PURE__ */ new WeakMap();\n\tasync function interceptQuery(exec, ctx) {\n\t\tif (ctx.scope !== \"runtime\") return;\n\t\tconst annotation = cacheAnnotation.read(exec);\n\t\tif (annotation === void 0 || annotation.bypass === true) return;\n\t\tconst key = annotation.key ?? await deriveKey(exec, ctx);\n\t\tconst entry = await store.get({\n\t\t\tkey,\n\t\t\tmeta: annotation.meta\n\t\t});\n\t\tif (!entry.data.empty) {\n\t\t\tctx.log.debug?.({\n\t\t\t\tevent: \"middleware.cache.hit\",\n\t\t\t\tmiddleware: \"cache\",\n\t\t\t\tkey\n\t\t\t});\n\t\t\treturn { rows: entry.data.value };\n\t\t}\n\t\tpending.set(exec, {\n\t\t\tentry,\n\t\t\tbuffer: []\n\t\t});\n\t\tctx.log.debug?.({\n\t\t\tevent: \"middleware.cache.miss\",\n\t\t\tmiddleware: \"cache\",\n\t\t\tkey\n\t\t});\n\t}\n\tasync function onRow(row, exec, _ctx) {\n\t\tpending.get(exec)?.buffer.push(row);\n\t}\n\tasync function afterQuery(exec, result, ctx) {\n\t\tconst miss = pending.get(exec);\n\t\tif (miss === void 0) return;\n\t\tpending.delete(exec);\n\t\tif (!result.completed || result.source !== \"driver\") return;\n\t\tconst stored = await store.set(miss.entry, miss.buffer);\n\t\tctx.log.debug?.({\n\t\t\tevent: stored ? \"middleware.cache.store\" : \"middleware.cache.store-skipped\",\n\t\t\tmiddleware: \"cache\",\n\t\t\tkey: miss.entry.key\n\t\t});\n\t}\n\tasync function invalidate(target) {\n\t\tconst keys = target.keys !== void 0 && target.keys.length > 0 ? target.keys : void 0;\n\t\tif (keys === void 0 && target.meta === void 0) return;\n\t\tawait store.unset({\n\t\t\tkeys,\n\t\t\tmeta: target.meta\n\t\t});\n\t}\n\treturn {\n\t\tname: \"cache\",\n\t\tinterceptQuery,\n\t\tonRow,\n\t\tafterQuery,\n\t\tinvalidate\n\t};\n}\n//#endregion\nexport { cacheAnnotation, createCacheMiddleware, createInMemoryCacheStore, deriveKeyFromContentHash };\n\n//# sourceMappingURL=index.mjs.map"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prisma/orm-extension-middleware-cache",
|
|
3
|
-
"version": "8.0.0-rc.14-dev.
|
|
3
|
+
"version": "8.0.0-rc.14-dev.9",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
"dist"
|
|
10
10
|
],
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@prisma/orm-framework": "8.0.0-rc.14-dev.
|
|
12
|
+
"@prisma/orm-framework": "8.0.0-rc.14-dev.9"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
|
-
"@internal/middleware-cache": "8.0.0-rc.14-dev.
|
|
16
|
-
"@repo/tsconfig": "8.0.0-rc.14-dev.
|
|
17
|
-
"@repo/tsdown": "8.0.0-rc.14-dev.
|
|
15
|
+
"@internal/middleware-cache": "8.0.0-rc.14-dev.9",
|
|
16
|
+
"@repo/tsconfig": "8.0.0-rc.14-dev.9",
|
|
17
|
+
"@repo/tsdown": "8.0.0-rc.14-dev.9",
|
|
18
18
|
"tsdown": "0.22.14",
|
|
19
19
|
"typescript": "5.9.3"
|
|
20
20
|
},
|