@prisma/orm-extension-middleware-cache 8.0.0-rc.14-dev.7 → 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 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 { cache, cacheAnnotation, type CacheStore } from '@prisma/orm-extension-middleware-cache';
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 runtime middleware that short-circuits repeated reads: on a hit it returns cached rows and never invokes the driver; on a miss it buffers the driver's rows and commits them to the store only when the execution completes successfully. Cache keys come from the family runtime's content hash of the execution, or from a per-query `cacheAnnotation({ key })` override; `ttl` and `skip` are per-query too. Connection- and transaction-scoped executions bypass the cache.
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
- It ships an in-memory LRU-with-TTL store and exposes the `CacheStore` interface so Redis, Memcached, or any other backend can be dropped in.
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
- * Payload accepted when calling the `cacheAnnotation` handle.
6
- *
7
- * - `ttl` — Time-to-live for the cached entry, in milliseconds. When
8
- * omitted, the cache middleware passes the query through untouched —
9
- * presence of the annotation alone is not sufficient to enable caching.
10
- * This makes the cache strictly opt-in per query.
11
- * - `skip` — When `true`, the cache middleware passes the query through
12
- * untouched even if a `ttl` is set. Useful for selectively bypassing
13
- * the cache on a per-call basis without removing the annotation
14
- * entirely (e.g. a "force refresh" knob in user code).
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 CachePayload {
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
- * Read-only annotation handle for the cache middleware.
29
- *
30
- * Declared with `applicableTo: ['read']`. Write terminals supply
31
- * `K = 'write'` to the type-level `ValidAnnotations<'write', As>` gate
32
- * (and the runtime `assertAnnotationsApplicable(annotations, 'write', ...)`
33
- * check); the join `K extends Kinds` fails for this annotation, making
34
- * "cache a mutation" structurally impossible without an `as any` cast
35
- * bypass at both type *and* runtime levels.
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
- * Stored under namespace `'cache'` in `plan.meta.annotations`. The cache
38
- * middleware reads it via `cacheAnnotation.read(plan)`.
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({ ttl: 60_000 })),
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({ ttl: 60_000 }))
51
+ * .annotate(cacheAnnotation({}))
54
52
  * .select({ id: tables.user.columns.id })
55
53
  * .build();
56
54
  * ```
57
55
  */
58
- declare const cacheAnnotation: import("@prisma/orm-framework/components/runtime").AnnotationHandle<CachePayload, "read">;
56
+ declare const cacheAnnotation: CacheAnnotationHandle;
59
57
  //#endregion
60
58
  //#region src/cache-store.d.ts
61
59
  /**
62
- * A cached set of rows produced by a single execution.
63
- *
64
- * - `rows` are stored raw (undecoded). The SQL runtime's `decodeRow` pass
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
- interface CachedEntry {
76
- readonly rows: readonly Record<string, unknown>[];
77
- readonly storedAt: number;
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
- * Pluggable cache backend used by the cache middleware.
81
- *
82
- * The default implementation is an in-memory LRU with TTL produced by
83
- * `createInMemoryCacheStore`. Users can supply Redis, Memcached, or any
84
- * other backend by implementing this interface.
85
- *
86
- * The interface is intentionally minimal:
87
- *
88
- * - `get` returns the entry if it exists and has not expired, or
89
- * `undefined` otherwise. Implementations that gate on TTL should
90
- * treat an expired entry as absent (return `undefined`) and may
91
- * evict it as a side effect.
92
- * - `set` writes the entry under the key with an associated TTL in
93
- * milliseconds. Implementations may evict other entries to make
94
- * room (LRU, LFU, etc.) and may treat the operation as fire-and-
95
- * forget at scale; the cache middleware does not rely on `set`
96
- * completing before subsequent `get`s.
97
- *
98
- * Both methods are async to leave the door open for I/O-backed stores
99
- * (Redis, S3, etc.). The default in-memory store completes
100
- * synchronously and wraps the result in `Promise.resolve` for type
101
- * conformance.
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(key: string): Promise<CachedEntry | undefined>;
105
- set(key: string, entry: CachedEntry, ttlMs: number): Promise<void>;
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 accepted by `createInMemoryCacheStore`.
109
- *
110
- * - `maxEntries` — hard cap on the number of live entries. Once the cap
111
- * is exceeded, the least recently used entry is evicted. Reads and
112
- * writes both count as "uses" for ordering purposes.
113
- * - `clock` — injectable time source for TTL math. Defaults to
114
- * `Date.now`. Tests inject a controlled clock to verify expiry without
115
- * real-time waits.
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: number;
140
+ readonly maxEntries?: number;
141
+ readonly ttlMs?: number;
119
142
  readonly clock?: () => number;
120
143
  }
121
144
  /**
122
- * Default cache backend. An LRU with per-entry TTL, backed by a `Map`.
123
- *
124
- * Eviction policy:
125
- *
126
- * - On `set` of a fresh key whose insertion would push the live count
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: InMemoryCacheStoreOptions): CacheStore;
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` — pluggable cache backend. Defaults to an in-process LRU
151
- * produced by `createInMemoryCacheStore`. Users supply Redis,
152
- * Memcached, or any other backend by implementing the `CacheStore`
153
- * interface.
154
- * - `maxEntries` — only consulted when `store` is omitted. Sets the
155
- * `maxEntries` cap on the default in-memory store. Defaults to 1000.
156
- * - `clock` — injectable time source for `storedAt` stamping on
157
- * committed entries. Defaults to `Date.now`. Tests inject a controlled
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 maxEntries?: number;
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
- * Creates a family-agnostic caching middleware.
169
- *
170
- * The middleware uses three hooks:
171
- *
172
- * - `interceptQuery` — on each execution, checks the cache. On a hit, returns
173
- * the cached raw rows; the runtime skips `runDriver` and `onRow`
174
- * (`beforeQuery` is not affected — it has already run for every
175
- * middleware before any `interceptQuery` is consulted) and yields the
176
- * cached rows to the consumer (which, in the SQL runtime, sees them
177
- * after the standard `decodeRow` pass — i.e. the cache stores
178
- * wire-format values). On a miss, records a pending buffer keyed on
179
- * the `exec` object identity and returns `undefined` (passthrough).
180
- * - `onRow` — on the miss path, appends each row yielded by the driver
181
- * to the pending buffer.
182
- * - `afterQuery` — on the miss path, commits the buffer to the store
183
- * if and only if `result.completed === true && result.source === 'driver'`.
184
- * Failed executions and middleware-served executions never populate
185
- * the cache. The pending buffer is cleared in all branches so a stale
186
- * `WeakMap` entry cannot leak between executions sharing an `exec`.
187
- *
188
- * The middleware bypasses the cache entirely when:
189
- * - the plan has no `cache` annotation, or
190
- * - the annotation has `skip: true`, or
191
- * - the annotation has no `ttl`, or
192
- * - `ctx.scope !== 'runtime'` (connection / transaction scopes opt out).
193
- *
194
- * Returns a cross-family `RuntimeMiddleware` (no `familyId` /
195
- * `targetId`). The package depends only on
196
- * `@internal/framework-components/runtime`; cache keys come from
197
- * `ctx.contentHash(exec)`, populated by the family runtime, so SQL and
198
- * Mongo runtimes both work out of the box.
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
- * import { createCacheMiddleware, cacheAnnotation } from '@internal/middleware-cache';
209
+ * const cache = createCacheMiddleware();
210
+ * const db = postgres<Contract>({ contractJson, url, middleware: [cache] });
203
211
  *
204
- * const db = postgres({
205
- * contractJson,
206
- * url: process.env['DATABASE_URL']!,
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): CrossFamilyMiddleware;
220
+ declare function createCacheMiddleware<TMeta = unknown>(options?: CacheMiddlewareOptions<TMeta>): CacheMiddleware<TMeta>;
217
221
  //#endregion
218
- export { type CacheMiddlewareOptions, type CachePayload, type CacheStore, type CachedEntry, type InMemoryCacheStoreOptions, cacheAnnotation, createCacheMiddleware, createInMemoryCacheStore };
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
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../../../../3-extensions/middleware-cache/dist/index.d.mts"],"mappings":";;;;;;;;;;;;;;;;;;;;;UAoBU;WACC;WACA;WACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAiCG,kEAAkE,iBAAiB;;;;;;;;;;;;;;;;;UAiBvF;WACC,eAAe;WACf;;;;;;;;;;;;;;;;;;;;;;;;;;UA0BD;EACR,IAAI,cAAc,QAAQ;EAC1B,IAAI,aAAa,OAAO,aAAa,gBAAgB;;;;;;;;;;;;UAY7C;WACC;WACA;;;;;;;;;;;;;;;;;;;;;;;;;iBAyBM,yBAAyB,SAAS,4BAA4B;;;;;;;;;;;;;;;;;;UAkBrE;WACC,QAAQ;WACR;WACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAmDM,sBAAsB,UAAU,yBAAyB"}
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
- * Read-only annotation handle for the cache middleware.
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
- * Declared with `applicableTo: ['read']`. Write terminals supply
7
- * `K = 'write'` to the type-level `ValidAnnotations<'write', As>` gate
8
- * (and the runtime `assertAnnotationsApplicable(annotations, 'write', ...)`
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({ ttl: 60_000 })),
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({ ttl: 60_000 }))
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
- * Default cache backend. An LRU with per-entry TTL, backed by a `Map`.
40
- *
41
- * Eviction policy:
42
- *
43
- * - On `set` of a fresh key whose insertion would push the live count
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.maxEntries;
63
- const clock = options.clock ?? Date.now;
64
- const map = /* @__PURE__ */ new Map();
65
- function get(key) {
66
- const record = map.get(key);
67
- if (record === void 0) return Promise.resolve(void 0);
68
- if (clock() >= record.expiresAt) {
69
- map.delete(key);
70
- return Promise.resolve(void 0);
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 set(key, entry, ttlMs) {
77
- const expiresAt = clock() + ttlMs;
78
- if (map.has(key)) map.delete(key);
79
- map.set(key, {
80
- entry,
81
- expiresAt
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
- while (map.size > maxEntries) {
84
- const oldest = map.keys().next();
85
- if (oldest.done) break;
86
- map.delete(oldest.value);
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
- * Default `maxEntries` for the built-in in-memory store. Bounded so a
97
- * runaway producer cannot exhaust process memory; users who need
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
- async function resolveCacheKey(payload, exec, ctx) {
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 family-agnostic caching middleware.
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
- * The middleware bypasses the cache entirely when:
155
- * - the plan has no `cache` annotation, or
156
- * - the annotation has `skip: true`, or
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
- * Returns a cross-family `RuntimeMiddleware` (no `familyId` /
161
- * `targetId`). The package depends only on
162
- * `@internal/framework-components/runtime`; cache keys come from
163
- * `ctx.contentHash(exec)`, populated by the family runtime, so SQL and
164
- * Mongo runtimes both work out of the box.
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
- * import { createCacheMiddleware, cacheAnnotation } from '@internal/middleware-cache';
158
+ * const cache = createCacheMiddleware();
159
+ * const db = postgres<Contract>({ contractJson, url, middleware: [cache] });
169
160
  *
170
- * const db = postgres({
171
- * contractJson,
172
- * url: process.env['DATABASE_URL']!,
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({ maxEntries: options?.maxEntries ?? DEFAULT_MAX_ENTRIES });
184
- const clock = options?.clock ?? Date.now;
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 payload = readCachePayload(exec);
189
- if (payload === void 0) return;
190
- if (payload.skip === true) return;
191
- if (payload.ttl === void 0) return;
192
- const key = await resolveCacheKey(payload, exec, ctx);
193
- const hit = await store.get(key);
194
- if (hit !== void 0) {
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: hit.rows };
188
+ return { rows: entry.data.value };
201
189
  }
202
190
  pending.set(exec, {
203
- key,
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
- const slot = pending.get(exec);
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 slot = pending.get(exec);
220
- if (slot === void 0) return;
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(slot.key, {
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: slot.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
@@ -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.7",
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.7"
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.7",
16
- "@repo/tsconfig": "8.0.0-rc.14-dev.7",
17
- "@repo/tsdown": "8.0.0-rc.14-dev.7",
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
  },