@prisma/orm-extension-middleware-cache 8.0.0-rc.18-dev.16 → 8.0.0-rc.18-dev.18

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
@@ -20,23 +20,23 @@ import {
20
20
 
21
21
  ## Responsibilities
22
22
 
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.
23
+ A read-through cache middleware. An annotated ORM call, such as `db.orm.public.User.first(...)`, is cached as a whole: on a hit the middleware supplies the call's result and the ORM sends no statement; on a miss it stores the value the call returned. An annotated statement, such as a SQL builder query, is cached as rows: on a hit it returns the 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 an ORM call is keyed by its content hash, a digest of the terminal method and the statement it sends, and a statement by the `deriveKey` option (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. A read inside a transaction or on a held connection, such as a Supabase role session, is neither served nor stored.
24
24
 
25
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
26
 
27
27
  ## The store
28
28
 
29
- The store is a cache of values; this middleware stores `CachedRows`, the raw rows of one read.
29
+ The store is a cache of values; this middleware stores `CachedValue`, the raw rows of one statement or the result of one ORM call. The middleware tags each value `statement-rows` or `call-result` and serves it only to the same kind of read; under one key, a value with the other tag is a miss.
30
30
 
31
31
  ```ts
32
- interface CacheEntry<TMeta = unknown, TValue = CachedRows> {
32
+ interface CacheEntry<TMeta = unknown, TValue = CachedValue> {
33
33
  readonly key: string;
34
34
  readonly meta: TMeta | undefined;
35
35
  readonly version: number;
36
36
  readonly data: { readonly empty: true } | { readonly empty: false; readonly value: TValue };
37
37
  }
38
38
 
39
- interface CacheStore<TMeta = unknown, TValue = CachedRows> {
39
+ interface CacheStore<TMeta = unknown, TValue = CachedValue> {
40
40
  get(target: {
41
41
  readonly key: string;
42
42
  readonly meta: TMeta | undefined;
@@ -49,9 +49,13 @@ interface CacheStore<TMeta = unknown, TValue = CachedRows> {
49
49
  }
50
50
 
51
51
  type CachedRows = readonly Record<string, unknown>[];
52
+
53
+ type CachedValue =
54
+ | { readonly kind: 'statement-rows'; readonly rows: CachedRows }
55
+ | { readonly kind: 'call-result'; readonly result: unknown };
52
56
  ```
53
57
 
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.
58
+ 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. An error from `get` or `set` fails the read, for statements and ORM calls alike, even when the database answered it; nothing is stored, so the next read fails the same way while the store keeps throwing. A store used with this middleware holds cached values: `createCacheMiddleware` takes a `CacheStore<TMeta, CachedValue>`, and a store of any other value type, `unknown` included, is a type error. `TValue` defaults to `CachedValue`, so `CacheStore<TMeta>` is a store of cached values; it exists so the same store type can serve other caches. A store must give back a value equivalent to the one it was given, `kind` included. An ORM call's result holds the values codecs decoded, such as `Date`, `bigint`, Temporal values and extension class instances, so a store that serializes needs an application-supplied serializer that rebuilds those types; `JSON.stringify` throws on a `bigint` and turns a `Date` into a string. A deploy that changes what a codec returns needs a new key prefix for a shared store, because the key does not cover how values are decoded. The default store, `createInMemoryCacheStore<TMeta>()`, holds cached values. It keeps statement rows and ORM call results differently. Statement rows are copied one level deep on `set` and `get`: the array, and each row object with its prototype. The driver's values in them, such as BSON `ObjectId`s or a `Buffer`, are kept by reference and shared with every hit; the runtime decodes them on each hit, and a codec that returns its wire value unchanged, such as a date or byte-array codec, hands every caller that same shared object. An ORM call's result has its plain structure copied on `set` and on every `get`: plain and null-prototype objects, arrays (their elements only; holes read as `undefined`), `Map`, `Set`, `Date`, `RegExp`, `ArrayBuffer`, typed arrays (a subclass such as Node's `Buffer` keeps its class) and `DataView` (copied as a plain `DataView`). Every other value in it, such as a class instance (a Temporal value, a decimal, a vector, a BSON value, an error) or a function, is shared by reference with the first caller and every hit. So, once the call has ended, changing the plain objects and arrays of a result changes no later hit; changing a shared value in place does, so codecs should return values that are not mutated in place. The store never refuses a value: `set` returns `false` only when the key's version moved. A miss copies the result once and each hit copies it again: about the work of decoding rows, linear in the size of the result. A custom store decides for itself whether to copy and what it can store.
55
59
 
56
60
  ## Typed meta
57
61
 
@@ -79,7 +83,7 @@ export const cached = (o: CacheAnnotationOptions<TagMeta>) => cacheAnnotation<Ta
79
83
 
80
84
  ## Deriving keys
81
85
 
82
- `createCacheMiddleware({ deriveKey })` computes the key of every cached read whose annotation has no `key`. Build on the default to add a prefix:
86
+ `createCacheMiddleware({ deriveKey })` computes the key of every cached statement whose annotation has no `key`. It takes the statement's execution plan, so it does not apply to an ORM call, which is keyed by its content hash. Build on the default to add a prefix:
83
87
 
84
88
  ```ts
85
89
  const cache = createCacheMiddleware({
@@ -87,7 +91,7 @@ const cache = createCacheMiddleware({
87
91
  });
88
92
  ```
89
93
 
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.
94
+ An explicit annotation `key` is used literally and bypasses `deriveKey`, so a tenant or namespace prefix must be part of it too. An app that relies on `deriveKey` to separate tenants must also separate them for annotated ORM calls, for example with a tenant-prefixed `key`; otherwise one tenant's cached ORM read can be served to another. A key belongs to one query and one terminal method: if `first()` and `all()` share a key, the one that reads the other's stored result fails with `ORM.INTERCEPTED_RESULT_INVALID` or returns the wrong shape. 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. A builder query and an ORM call that share a key evict each other's entries, so give each its own key.
91
95
 
92
96
  ## Invalidation
93
97
 
@@ -112,4 +116,4 @@ A tag scheme is a store policy: `cacheAnnotation({ meta: { tags: ['users'] } })`
112
116
 
113
117
  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
118
 
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 middleware does not yet act on `activityFinalized`, the event that fires once a write has committed. 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.
119
+ Invalidating as part of a write is not supported yet: it must wait for the transaction to commit. Every ORM write is an `orm-call` activity that carries the user's annotations, including its own transaction, and `activityFinalized` fires for it once it has committed, but the middleware does not yet act on that event. 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
@@ -29,7 +29,7 @@ type CacheAnnotationHandle = (<TMeta = unknown>(options: CacheAnnotationOptions<
29
29
  };
30
30
  /**
31
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`.
32
+ * compile time and at run time. The middleware reads it from a statement plan's `meta.annotations.cache`, or from the annotations of a client activity, such as an ORM read, which is then cached as a whole.
33
33
  *
34
34
  * `TMeta` types `meta` for this call only. Nothing ties it to the store's `TMeta`, so a store
35
35
  * package should export a wrapper typed with its own meta, such as
@@ -57,11 +57,20 @@ declare const cacheAnnotation: CacheAnnotationHandle;
57
57
  //#endregion
58
58
  //#region src/cache-store.d.ts
59
59
  /**
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.
60
+ * The rows one statement produced, stored raw (undecoded). The SQL runtime decodes intercepted rows
61
+ * the same way as driver rows, so a hit and a miss yield the same values to the consumer.
63
62
  */
64
63
  type CachedRows = readonly Record<string, unknown>[];
64
+ /**
65
+ * The value type the cache middleware stores: a statement's raw rows, or the result of a client activity, such as an ORM call. The middleware tags each value with what produced it: `statement-rows` for a statement's rows, `call-result` for a client activity's result. It serves `statement-rows` only to a statement and `call-result` only to a client activity; under one key, a value with the other tag is a miss.
66
+ */
67
+ type CachedValue = {
68
+ readonly kind: 'statement-rows';
69
+ readonly rows: CachedRows;
70
+ } | {
71
+ readonly kind: 'call-result';
72
+ readonly result: unknown;
73
+ };
65
74
  /**
66
75
  * One key of a `CacheStore`, as `get` saw it.
67
76
  *
@@ -72,7 +81,7 @@ type CachedRows = readonly Record<string, unknown>[];
72
81
  * - `data` — `{ empty: true }` when the store holds no live value for the key, otherwise that
73
82
  * value.
74
83
  */
75
- interface CacheEntry<TMeta = unknown, TValue = CachedRows> {
84
+ interface CacheEntry<TMeta = unknown, TValue = CachedValue> {
76
85
  readonly key: string;
77
86
  readonly meta: TMeta | undefined;
78
87
  readonly version: number;
@@ -85,7 +94,7 @@ interface CacheEntry<TMeta = unknown, TValue = CachedRows> {
85
94
  }
86
95
  /**
87
96
  * A cache of values that the cache middleware reads from and writes to. The middleware stores
88
- * `CachedRows`.
97
+ * `CachedValue`.
89
98
  *
90
99
  * The store keeps a version per key, an integer it keeps even for a key that holds no value. A key
91
100
  * never seen, or forgotten, has version 0. Only `unset` changes a version.
@@ -105,14 +114,14 @@ interface CacheEntry<TMeta = unknown, TValue = CachedRows> {
105
114
  * must throw rather than ignore it. An `unset` by key must also drop that key from any `meta`
106
115
  * index the store keeps.
107
116
  *
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.
117
+ * `get` must give back a value equivalent to the one `set` was given, `kind` included. A call result holds the values codecs decoded, such as a `Date`, a `bigint`, a Temporal value or an extension's class instance, so a store that serializes needs a serializer that rebuilds those types; JSON does not.
118
+ *
119
+ * Lifetime, eviction and whether values are copied are the store's policy; the middleware hands `set` the value it returns to its caller, so a store that keeps references shares that object. A version that `unset` moved must be kept at least as long as a read can take, so that a read that started before the `unset` cannot store. `unset` must not run queries through the runtime that uses the middleware.
111
120
  *
112
121
  * `TMeta` is the shape of `meta` the store understands. The middleware does not check that a read
113
122
  * annotation's `meta` has this shape; see `cacheAnnotation`.
114
123
  */
115
- interface CacheStore<TMeta = unknown, TValue = CachedRows> {
124
+ interface CacheStore<TMeta = unknown, TValue = CachedValue> {
116
125
  get(target: {
117
126
  readonly key: string;
118
127
  readonly meta: TMeta | undefined;
@@ -142,29 +151,29 @@ interface InMemoryCacheStoreOptions {
142
151
  readonly clock?: () => number;
143
152
  }
144
153
  /**
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.
154
+ * The default cache store: a least-recently-used map of `CachedValue` with one lifetime for every value, local to the process. Statement rows are copied one level deep on `set` and `get`: the array, and each row object with its prototype. The driver's values in them are kept by reference and shared with every hit; the runtime decodes them on each hit, and a codec that returns its wire value unchanged, such as a date or byte-array codec, hands that shared object to the caller. A call result has its plain structure copied on `set` and on every `get`: plain and null-prototype objects, arrays (their elements only; holes read as `undefined`), `Map`, `Set`, `Date`, `RegExp`, `ArrayBuffer`, typed arrays (a subclass such as Node's `Buffer` keeps its class) and `DataView` (copied as a plain `DataView`). Every other value in it, such as a class instance (a Temporal value, a BSON value, an error) or a function, is shared by reference. So after `set` returns, a change to the plain structure of the value `set` was given, or of a value `get` returned, reaches no later `get`; a change made to a shared value, or made before `set`, does. It never refuses a value: `set` returns `false` only when the version moved. A key's version is kept for `ttlMs` after the `unset` that moved it (see `InMemoryCacheStoreOptions`). It ignores `meta` in `get` and `set`, and its `unset` rejects any `meta`, including `null`, before changing anything. It throws `RUNTIME.ARGUMENT_INVALID` for a `maxEntries` or `ttlMs` outside the ranges above.
150
155
  */
151
- declare function createInMemoryCacheStore<TMeta = unknown, TValue = CachedRows>(options?: InMemoryCacheStoreOptions): CacheStore<TMeta, TValue>;
156
+ declare function createInMemoryCacheStore<TMeta = unknown>(options?: InMemoryCacheStoreOptions): CacheStore<TMeta, CachedValue>;
152
157
  //#endregion
153
158
  //#region src/cache-middleware.d.ts
154
159
  /**
155
160
  * Options accepted by `createCacheMiddleware`.
156
161
  *
157
162
  * - `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.
163
+ * - `deriveKey` — computes the key of a cached statement whose annotation has no `key`. Defaults
164
+ * to `deriveKeyFromContentHash`. It runs on every such statement, hit or miss, and an error from
165
+ * it fails the statement. It must return different keys whenever the rows can differ; build on
166
+ * `deriveKeyFromContentHash` to keep the statement, parameters and storage hash. It does not
167
+ * apply to a client activity, such as an ORM call, which has no execution plan: a client
168
+ * activity whose annotation has no `key` is keyed by `activity.contentHash()`. A `deriveKey` that
169
+ * separates tenants does not separate ORM calls; give each annotated ORM call a tenant-prefixed
170
+ * `key`, or one tenant's cached ORM read can be served to another.
162
171
  *
163
172
  * `TMeta` is the store's meta type; `createCacheMiddleware` infers it from `store`. The store holds
164
- * `CachedRows`.
173
+ * `CachedValue`.
165
174
  */
166
175
  interface CacheMiddlewareOptions<TMeta = unknown> {
167
- readonly store?: CacheStore<TMeta, CachedRows>;
176
+ readonly store?: CacheStore<TMeta, CachedValue>;
168
177
  readonly deriveKey?: (exec: ExecutionPlan, ctx: ActivityContext) => string | Promise<string>;
169
178
  }
170
179
  /**
@@ -192,17 +201,26 @@ declare function deriveKeyFromContentHash(exec: ExecutionPlan, ctx: ActivityCont
192
201
  /**
193
202
  * Creates a read-through cache middleware that works with every family runtime.
194
203
  *
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)`.
204
+ * It caches an activity that carries `cacheAnnotation` without `bypass` and runs in runtime scope,
205
+ * outside any transaction: never on a held connection, whose session may change what a read
206
+ * returns, and never inside a transaction, which must read its own uncommitted writes. Two kinds
207
+ * of activity are cached:
208
+ *
209
+ * - A statement whose plan carries the annotation, such as an annotated SQL builder query. Its key
210
+ * is the annotation's `key`, else `deriveKey(exec, ctx)`; its value is the raw rows.
211
+ * - A client activity that carries the annotation, such as an annotated ORM read. Its key is the
212
+ * annotation's `key`, else `await activity.contentHash()`; its value is the result the client
213
+ * ended the activity with. It is served as a whole, so a hit sends no statement.
214
+ *
215
+ * The middleware tags each stored value `statement-rows` or `call-result` and serves `statement-rows` only to a statement and `call-result` only to a client activity; under the same key, a value with the other tag is a miss, and the read stores its own value in its place. A key therefore belongs to one query and one terminal method: an ORM call served another method's result under a shared key fails with `ORM.INTERCEPTED_RESULT_INVALID` or returns the wrong shape.
198
216
  *
199
- * - `intercept.onQuery` — on a hit, returns the stored rows and the driver does not run. On a
200
- * miss, starts collecting rows.
201
- * - `onRow` — collects each row of a miss.
202
- * - `activityEnded` — 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`.
217
+ * - `intercept.onQuery` and `intercept.onActivity` — on a hit, return the stored rows or result,
218
+ * and nothing runs. On a miss, record the pending entry.
219
+ * - `onRow` — collects each row of a statement miss.
220
+ * - `activityEnded` — when the driver completed the statement, or the client ended the call
221
+ * `succeeded` with a result (`null` is a result), calls `store.set(entry, value)` with the entry
222
+ * `store.get` returned, which stores only if the key's version has not moved. If an `unset` moved
223
+ * it meanwhile, `set` returns `false` and the middleware logs `middleware.cache.store-skipped`.
206
224
  *
207
225
  * @example
208
226
  * ```typescript
@@ -219,5 +237,5 @@ declare function deriveKeyFromContentHash(exec: ExecutionPlan, ctx: ActivityCont
219
237
  */
220
238
  declare function createCacheMiddleware<TMeta = unknown>(options?: CacheMiddlewareOptions<TMeta>): CacheMiddleware<TMeta>;
221
239
  //#endregion
222
- export { type CacheAnnotationHandle, type CacheAnnotationOptions, type CacheEntry, type CacheMiddleware, type CacheMiddlewareOptions, type CacheStore, type CachedRows, type InMemoryCacheStoreOptions, cacheAnnotation, createCacheMiddleware, createInMemoryCacheStore, deriveKeyFromContentHash };
240
+ export { type CacheAnnotationHandle, type CacheAnnotationOptions, type CacheEntry, type CacheMiddleware, type CacheMiddlewareOptions, type CacheStore, type CachedRows, type CachedValue, type InMemoryCacheStoreOptions, cacheAnnotation, createCacheMiddleware, createInMemoryCacheStore, deriveKeyFromContentHash };
223
241
  //# 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":";;;;;;;;;;;;;;;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,6BAA6B;;;;;;;;;;;;;KAa1E,gBAAgB,mBAAmB;WAC7B,aAAa;aACX;aACA,OAAO;QACZ;;;;;;iBAMS,yBAAyB,MAAM,eAAe,KAAK,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA6BrE,sBAAsB,iBAAiB,UAAU,uBAAuB,SAAS,gBAAgB"}
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;;;;;;;KAO1B,sBAAsB;;;;KAItB;WACM;WACA,MAAM;;WAEN;WACA;;;;;;;;;;;;UAYD,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;;;;;iBAKM,yBAAyB,iBAAiB,UAAU,4BAA4B,WAAW,OAAO;;;;;;;;;;;;;;;;;;;UAmBzG,uBAAuB;WACtB,QAAQ,WAAW,OAAO;WAC1B,aAAa,MAAM,eAAe,KAAK,6BAA6B;;;;;;;;;;;;;KAa1E,gBAAgB,mBAAmB;WAC7B,aAAa;aACX;aACA,OAAO;QACZ;;;;;;iBAMS,yBAAyB,MAAM,eAAe,KAAK,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAsCrE,sBAAsB,iBAAiB,UAAU,uBAAuB,SAAS,gBAAgB"}
package/dist/index.mjs CHANGED
@@ -1,9 +1,10 @@
1
1
  import { defineAnnotation } from "@prisma/orm-framework/components/runtime";
2
+ import { blindCast } from "@prisma/orm-framework/utils/casts";
2
3
  import { structuredError } from "@prisma/orm-framework/utils/structured-error";
3
4
  //#region ../../../3-extensions/middleware-cache/dist/index.mjs
4
5
  /**
5
6
  * 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`.
7
+ * compile time and at run time. The middleware reads it from a statement plan's `meta.annotations.cache`, or from the annotations of a client activity, such as an ORM read, which is then cached as a whole.
7
8
  *
8
9
  * `TMeta` types `meta` for this call only. Nothing ties it to the store's `TMeta`, so a store
9
10
  * package should export a wrapper typed with its own meta, such as
@@ -41,6 +42,76 @@ function withTypedMeta(handle) {
41
42
  read: handle.read
42
43
  }));
43
44
  }
45
+ const typedArraySlice = Reflect.get(Object.getPrototypeOf(Uint8Array.prototype), "slice");
46
+ /**
47
+ * A copy of the plain structure of `value`: plain and null-prototype objects, arrays, `Map`, `Set`,
48
+ * `Date`, `RegExp`, `ArrayBuffer`, typed arrays and `DataView` are copied, and every other value,
49
+ * such as a class instance or a function, is kept by reference. A typed array is copied with
50
+ * `TypedArray.prototype.slice`, which keeps its class, so a `Buffer` copies to a `Buffer` with its
51
+ * own memory; a `DataView` copies to a plain `DataView`; an array copies to its elements only, with
52
+ * holes read as `undefined`. Shared references and cycles are kept.
53
+ */
54
+ function copyValue(value, copies = /* @__PURE__ */ new Map()) {
55
+ if (typeof value !== "object" || value === null) return value;
56
+ const known = copies.get(value);
57
+ if (known !== void 0) return blindCast(known);
58
+ const copy = copyObject(value, copies);
59
+ copies.set(value, copy);
60
+ return blindCast(copy);
61
+ }
62
+ function copyObject(value, copies) {
63
+ if (value instanceof DataView) {
64
+ const start = value.byteOffset;
65
+ return new DataView(value.buffer.slice(start, start + value.byteLength));
66
+ }
67
+ if (ArrayBuffer.isView(value)) return typedArraySlice.call(value);
68
+ const prototype = Object.getPrototypeOf(value);
69
+ if (prototype === ArrayBuffer.prototype && value instanceof ArrayBuffer) return value.slice(0);
70
+ if (prototype === Date.prototype && value instanceof Date) return new Date(value.getTime());
71
+ if (prototype === RegExp.prototype && value instanceof RegExp) return new RegExp(value.source, value.flags);
72
+ if (prototype === Map.prototype && value instanceof Map) {
73
+ const copy = /* @__PURE__ */ new Map();
74
+ copies.set(value, copy);
75
+ for (const [key, entry] of value) copy.set(copyValue(key, copies), copyValue(entry, copies));
76
+ return copy;
77
+ }
78
+ if (prototype === Set.prototype && value instanceof Set) {
79
+ const copy = /* @__PURE__ */ new Set();
80
+ copies.set(value, copy);
81
+ for (const entry of value) copy.add(copyValue(entry, copies));
82
+ return copy;
83
+ }
84
+ if (prototype === Array.prototype && Array.isArray(value)) {
85
+ const copy = [];
86
+ copies.set(value, copy);
87
+ for (const entry of value) copy.push(copyValue(entry, copies));
88
+ return copy;
89
+ }
90
+ if (prototype !== Object.prototype && prototype !== null) return value;
91
+ const copy = Object.create(prototype);
92
+ copies.set(value, copy);
93
+ for (const key of Reflect.ownKeys(value)) if (Object.prototype.propertyIsEnumerable.call(value, key)) copy[key] = copyValue(Reflect.get(value, key), copies);
94
+ return copy;
95
+ }
96
+ /** Statement rows hold driver values, such as BSON `ObjectId`s, that this package cannot copy. So the array and each row object are copied, each row keeping its prototype, and the driver's values are kept by reference and shared with every hit. */
97
+ function copyRows(rows) {
98
+ return rows.map((row) => {
99
+ const copy = Object.create(Object.getPrototypeOf(row));
100
+ return Object.assign(copy, row);
101
+ });
102
+ }
103
+ function copyStored(value) {
104
+ switch (value.kind) {
105
+ case "statement-rows": return {
106
+ kind: "statement-rows",
107
+ rows: copyRows(value.rows)
108
+ };
109
+ case "call-result": return {
110
+ kind: "call-result",
111
+ result: copyValue(value.result)
112
+ };
113
+ }
114
+ }
44
115
  function metaUnsupported() {
45
116
  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
117
  }
@@ -55,11 +126,7 @@ function invalidOption(argument, received, expected) {
55
126
  });
56
127
  }
57
128
  /**
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.
129
+ * The default cache store: a least-recently-used map of `CachedValue` with one lifetime for every value, local to the process. Statement rows are copied one level deep on `set` and `get`: the array, and each row object with its prototype. The driver's values in them are kept by reference and shared with every hit; the runtime decodes them on each hit, and a codec that returns its wire value unchanged, such as a date or byte-array codec, hands that shared object to the caller. A call result has its plain structure copied on `set` and on every `get`: plain and null-prototype objects, arrays (their elements only; holes read as `undefined`), `Map`, `Set`, `Date`, `RegExp`, `ArrayBuffer`, typed arrays (a subclass such as Node's `Buffer` keeps its class) and `DataView` (copied as a plain `DataView`). Every other value in it, such as a class instance (a Temporal value, a BSON value, an error) or a function, is shared by reference. So after `set` returns, a change to the plain structure of the value `set` was given, or of a value `get` returned, reaches no later `get`; a change made to a shared value, or made before `set`, does. It never refuses a value: `set` returns `false` only when the version moved. A key's version is kept for `ttlMs` after the `unset` that moved it (see `InMemoryCacheStoreOptions`). It ignores `meta` in `get` and `set`, and its `unset` rejects any `meta`, including `null`, before changing anything. It throws `RUNTIME.ARGUMENT_INVALID` for a `maxEntries` or `ttlMs` outside the ranges above.
63
130
  */
64
131
  function createInMemoryCacheStore(options) {
65
132
  const maxEntries = options?.maxEntries ?? 1e3;
@@ -96,7 +163,7 @@ function createInMemoryCacheStore(options) {
96
163
  version: versionOf(target.key),
97
164
  data: record === void 0 ? { empty: true } : {
98
165
  empty: false,
99
- value: record.value
166
+ value: copyStored(record.value)
100
167
  }
101
168
  };
102
169
  }
@@ -104,7 +171,7 @@ function createInMemoryCacheStore(options) {
104
171
  if (entry.version !== versionOf(entry.key)) return false;
105
172
  records.delete(entry.key);
106
173
  records.set(entry.key, {
107
- value,
174
+ value: copyStored(value),
108
175
  expiresAt: clock() + ttlMs
109
176
  });
110
177
  for (const leastRecentlyUsed of records.keys()) {
@@ -146,20 +213,48 @@ function pendingOf(ctx) {
146
213
  function isPendingMiss(value) {
147
214
  return typeof value === "object" && value !== null && "entry" in value && "buffer" in value;
148
215
  }
216
+ function hasTag(value, tag) {
217
+ return value.kind === tag;
218
+ }
219
+ function cacheable(ctx) {
220
+ return ctx.scope === "runtime" && !ctx.inTransaction();
221
+ }
222
+ /** The value a pending miss stores when its activity ends this way, or `undefined` to store nothing. */
223
+ function valueToStore(miss, result) {
224
+ if (result.outcome !== "succeeded") return void 0;
225
+ if (result.kind === "statement") return result.source === "driver" && miss.buffer !== void 0 ? {
226
+ kind: "statement-rows",
227
+ rows: miss.buffer
228
+ } : void 0;
229
+ if (result.kind === "transaction") return void 0;
230
+ return result.source === "client" && result.result !== void 0 ? {
231
+ kind: "call-result",
232
+ result: result.result
233
+ } : void 0;
234
+ }
149
235
  /**
150
236
  * Creates a read-through cache middleware that works with every family runtime.
151
237
  *
152
- * It caches a read when the plan carries `cacheAnnotation`, the annotation does not set
153
- * `bypass`, and the read runs in runtime scope (not inside a connection or transaction). The key
154
- * is the annotation's `key`, else `deriveKey(exec, ctx)`.
238
+ * It caches an activity that carries `cacheAnnotation` without `bypass` and runs in runtime scope,
239
+ * outside any transaction: never on a held connection, whose session may change what a read
240
+ * returns, and never inside a transaction, which must read its own uncommitted writes. Two kinds
241
+ * of activity are cached:
242
+ *
243
+ * - A statement whose plan carries the annotation, such as an annotated SQL builder query. Its key
244
+ * is the annotation's `key`, else `deriveKey(exec, ctx)`; its value is the raw rows.
245
+ * - A client activity that carries the annotation, such as an annotated ORM read. Its key is the
246
+ * annotation's `key`, else `await activity.contentHash()`; its value is the result the client
247
+ * ended the activity with. It is served as a whole, so a hit sends no statement.
155
248
  *
156
- * - `intercept.onQuery` — on a hit, returns the stored rows and the driver does not run. On a
157
- * miss, starts collecting rows.
158
- * - `onRow` — collects each row of a miss.
159
- * - `activityEnded` — when the driver completed the read, calls `store.set(entry, rows)` with the
160
- * entry `store.get` returned, which stores only if the key's version has not moved. If an
161
- * `unset` moved it meanwhile, `set` returns `false` and the middleware logs
162
- * `middleware.cache.store-skipped`.
249
+ * The middleware tags each stored value `statement-rows` or `call-result` and serves `statement-rows` only to a statement and `call-result` only to a client activity; under the same key, a value with the other tag is a miss, and the read stores its own value in its place. A key therefore belongs to one query and one terminal method: an ORM call served another method's result under a shared key fails with `ORM.INTERCEPTED_RESULT_INVALID` or returns the wrong shape.
250
+ *
251
+ * - `intercept.onQuery` and `intercept.onActivity` — on a hit, return the stored rows or result,
252
+ * and nothing runs. On a miss, record the pending entry.
253
+ * - `onRow` — collects each row of a statement miss.
254
+ * - `activityEnded` — when the driver completed the statement, or the client ended the call
255
+ * `succeeded` with a result (`null` is a result), calls `store.set(entry, value)` with the entry
256
+ * `store.get` returned, which stores only if the key's version has not moved. If an `unset` moved
257
+ * it meanwhile, `set` returns `false` and the middleware logs `middleware.cache.store-skipped`.
163
258
  *
164
259
  * @example
165
260
  * ```typescript
@@ -177,26 +272,22 @@ function isPendingMiss(value) {
177
272
  function createCacheMiddleware(options) {
178
273
  const store = options?.store ?? createInMemoryCacheStore();
179
274
  const deriveKey = options?.deriveKey ?? deriveKeyFromContentHash;
180
- async function onQuery(exec, ctx) {
181
- if (ctx.scope !== "runtime") return;
182
- const annotation = cacheAnnotation.read(exec);
183
- if (annotation === void 0 || annotation.bypass === true) return;
184
- const key = annotation.key ?? await deriveKey(exec, ctx);
275
+ async function lookUp(tag, key, meta, ctx, buffer) {
185
276
  const entry = await store.get({
186
277
  key,
187
- meta: annotation.meta
278
+ meta
188
279
  });
189
- if (!entry.data.empty) {
280
+ if (!entry.data.empty && hasTag(entry.data.value, tag)) {
190
281
  ctx.log.debug?.({
191
282
  event: "middleware.cache.hit",
192
283
  middleware: "cache",
193
284
  key
194
285
  });
195
- return { rows: entry.data.value };
286
+ return entry.data.value;
196
287
  }
197
288
  ctx.state(pendingMiss)["miss"] = {
198
289
  entry,
199
- buffer: []
290
+ buffer
200
291
  };
201
292
  ctx.log.debug?.({
202
293
  event: "middleware.cache.miss",
@@ -204,15 +295,30 @@ function createCacheMiddleware(options) {
204
295
  key
205
296
  });
206
297
  }
298
+ async function onQuery(exec, ctx) {
299
+ if (!cacheable(ctx)) return;
300
+ const annotation = cacheAnnotation.read(exec);
301
+ if (annotation === void 0 || annotation.bypass === true) return;
302
+ const value = await lookUp("statement-rows", annotation.key ?? await deriveKey(exec, ctx), annotation.meta, ctx, []);
303
+ return value === void 0 ? void 0 : { rows: value.rows };
304
+ }
305
+ async function onActivity(activity, ctx) {
306
+ if (!cacheable(ctx)) return;
307
+ const annotation = cacheAnnotation.read(activity);
308
+ if (annotation === void 0 || annotation.bypass === true) return;
309
+ const value = await lookUp("call-result", annotation.key ?? await activity.contentHash(), annotation.meta, ctx, void 0);
310
+ return value === void 0 ? void 0 : { result: value.result };
311
+ }
207
312
  async function onRow(row, _exec, ctx) {
208
- pendingOf(ctx)?.buffer.push(row);
313
+ pendingOf(ctx)?.buffer?.push(row);
209
314
  }
210
315
  async function activityEnded(_activity, result, ctx) {
211
316
  const miss = pendingOf(ctx);
212
317
  if (miss === void 0) return;
213
318
  delete ctx.state(pendingMiss)["miss"];
214
- if (result.kind !== "statement" || result.outcome !== "succeeded" || result.source !== "driver") return;
215
- const stored = await store.set(miss.entry, miss.buffer);
319
+ const value = valueToStore(miss, result);
320
+ if (value === void 0) return;
321
+ const stored = await store.set(miss.entry, value);
216
322
  ctx.log.debug?.({
217
323
  event: stored ? "middleware.cache.store" : "middleware.cache.store-skipped",
218
324
  middleware: "cache",
@@ -229,7 +335,10 @@ function createCacheMiddleware(options) {
229
335
  }
230
336
  return {
231
337
  name: "cache",
232
- intercept: { onQuery },
338
+ intercept: {
339
+ onQuery,
340
+ onActivity
341
+ },
233
342
  onRow,
234
343
  activityEnded,
235
344
  invalidate
@@ -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\";\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}\nconst pendingMiss = Symbol(\"cache.pendingMiss\");\nfunction pendingOf(ctx) {\n\tconst miss = ctx.state(pendingMiss)[\"miss\"];\n\treturn isPendingMiss(miss) ? miss : void 0;\n}\nfunction isPendingMiss(value) {\n\treturn typeof value === \"object\" && value !== null && \"entry\" in value && \"buffer\" in value;\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* - `intercept.onQuery` — on a hit, returns the stored rows and the driver does not run. On a\n* miss, starts collecting rows.\n* - `onRow` — collects each row of a miss.\n* - `activityEnded` — 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\tasync function onQuery(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\tctx.state(pendingMiss)[\"miss\"] = {\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\tpendingOf(ctx)?.buffer.push(row);\n\t}\n\tasync function activityEnded(_activity, result, ctx) {\n\t\tconst miss = pendingOf(ctx);\n\t\tif (miss === void 0) return;\n\t\tdelete ctx.state(pendingMiss)[\"miss\"];\n\t\tif (result.kind !== \"statement\" || result.outcome !== \"succeeded\" || 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\tintercept: { onQuery },\n\t\tonRow,\n\t\tactivityEnded,\n\t\tinvalidate\n\t};\n}\n//#endregion\nexport { cacheAnnotation, createCacheMiddleware, createInMemoryCacheStore, deriveKeyFromContentHash };\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 { blindCast } from \"@internal/utils/casts\";\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 a statement plan's `meta.annotations.cache`, or from the annotations of a client activity, such as an ORM read, which is then cached as a whole.\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\nconst typedArraySlice = Reflect.get(Object.getPrototypeOf(Uint8Array.prototype), \"slice\");\n/**\n* A copy of the plain structure of `value`: plain and null-prototype objects, arrays, `Map`, `Set`,\n* `Date`, `RegExp`, `ArrayBuffer`, typed arrays and `DataView` are copied, and every other value,\n* such as a class instance or a function, is kept by reference. A typed array is copied with\n* `TypedArray.prototype.slice`, which keeps its class, so a `Buffer` copies to a `Buffer` with its\n* own memory; a `DataView` copies to a plain `DataView`; an array copies to its elements only, with\n* holes read as `undefined`. Shared references and cycles are kept.\n*/\nfunction copyValue(value, copies = /* @__PURE__ */ new Map()) {\n\tif (typeof value !== \"object\" || value === null) return value;\n\tconst known = copies.get(value);\n\tif (known !== void 0) return blindCast(known);\n\tconst copy = copyObject(value, copies);\n\tcopies.set(value, copy);\n\treturn blindCast(copy);\n}\nfunction copyObject(value, copies) {\n\tif (value instanceof DataView) {\n\t\tconst start = value.byteOffset;\n\t\treturn new DataView(value.buffer.slice(start, start + value.byteLength));\n\t}\n\tif (ArrayBuffer.isView(value)) return typedArraySlice.call(value);\n\tconst prototype = Object.getPrototypeOf(value);\n\tif (prototype === ArrayBuffer.prototype && value instanceof ArrayBuffer) return value.slice(0);\n\tif (prototype === Date.prototype && value instanceof Date) return new Date(value.getTime());\n\tif (prototype === RegExp.prototype && value instanceof RegExp) return new RegExp(value.source, value.flags);\n\tif (prototype === Map.prototype && value instanceof Map) {\n\t\tconst copy = /* @__PURE__ */ new Map();\n\t\tcopies.set(value, copy);\n\t\tfor (const [key, entry] of value) copy.set(copyValue(key, copies), copyValue(entry, copies));\n\t\treturn copy;\n\t}\n\tif (prototype === Set.prototype && value instanceof Set) {\n\t\tconst copy = /* @__PURE__ */ new Set();\n\t\tcopies.set(value, copy);\n\t\tfor (const entry of value) copy.add(copyValue(entry, copies));\n\t\treturn copy;\n\t}\n\tif (prototype === Array.prototype && Array.isArray(value)) {\n\t\tconst copy = [];\n\t\tcopies.set(value, copy);\n\t\tfor (const entry of value) copy.push(copyValue(entry, copies));\n\t\treturn copy;\n\t}\n\tif (prototype !== Object.prototype && prototype !== null) return value;\n\tconst copy = Object.create(prototype);\n\tcopies.set(value, copy);\n\tfor (const key of Reflect.ownKeys(value)) if (Object.prototype.propertyIsEnumerable.call(value, key)) copy[key] = copyValue(Reflect.get(value, key), copies);\n\treturn copy;\n}\n/** Statement rows hold driver values, such as BSON `ObjectId`s, that this package cannot copy. So the array and each row object are copied, each row keeping its prototype, and the driver's values are kept by reference and shared with every hit. */\nfunction copyRows(rows) {\n\treturn rows.map((row) => {\n\t\tconst copy = Object.create(Object.getPrototypeOf(row));\n\t\treturn Object.assign(copy, row);\n\t});\n}\nfunction copyStored(value) {\n\tswitch (value.kind) {\n\t\tcase \"statement-rows\": return {\n\t\t\tkind: \"statement-rows\",\n\t\t\trows: copyRows(value.rows)\n\t\t};\n\t\tcase \"call-result\": return {\n\t\t\tkind: \"call-result\",\n\t\t\tresult: copyValue(value.result)\n\t\t};\n\t}\n}\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 of `CachedValue` with one lifetime for every value, local to the process. Statement rows are copied one level deep on `set` and `get`: the array, and each row object with its prototype. The driver's values in them are kept by reference and shared with every hit; the runtime decodes them on each hit, and a codec that returns its wire value unchanged, such as a date or byte-array codec, hands that shared object to the caller. A call result has its plain structure copied on `set` and on every `get`: plain and null-prototype objects, arrays (their elements only; holes read as `undefined`), `Map`, `Set`, `Date`, `RegExp`, `ArrayBuffer`, typed arrays (a subclass such as Node's `Buffer` keeps its class) and `DataView` (copied as a plain `DataView`). Every other value in it, such as a class instance (a Temporal value, a BSON value, an error) or a function, is shared by reference. So after `set` returns, a change to the plain structure of the value `set` was given, or of a value `get` returned, reaches no later `get`; a change made to a shared value, or made before `set`, does. It never refuses a value: `set` returns `false` only when the version moved. A key's version is kept for `ttlMs` after the `unset` that moved it (see `InMemoryCacheStoreOptions`). It ignores `meta` in `get` and `set`, and its `unset` rejects any `meta`, including `null`, before changing anything. 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: copyStored(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: copyStored(value),\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}\nconst pendingMiss = Symbol(\"cache.pendingMiss\");\nfunction pendingOf(ctx) {\n\tconst miss = ctx.state(pendingMiss)[\"miss\"];\n\treturn isPendingMiss(miss) ? miss : void 0;\n}\nfunction isPendingMiss(value) {\n\treturn typeof value === \"object\" && value !== null && \"entry\" in value && \"buffer\" in value;\n}\nfunction hasTag(value, tag) {\n\treturn value.kind === tag;\n}\nfunction cacheable(ctx) {\n\treturn ctx.scope === \"runtime\" && !ctx.inTransaction();\n}\n/** The value a pending miss stores when its activity ends this way, or `undefined` to store nothing. */\nfunction valueToStore(miss, result) {\n\tif (result.outcome !== \"succeeded\") return void 0;\n\tif (result.kind === \"statement\") return result.source === \"driver\" && miss.buffer !== void 0 ? {\n\t\tkind: \"statement-rows\",\n\t\trows: miss.buffer\n\t} : void 0;\n\tif (result.kind === \"transaction\") return void 0;\n\treturn result.source === \"client\" && result.result !== void 0 ? {\n\t\tkind: \"call-result\",\n\t\tresult: result.result\n\t} : void 0;\n}\n/**\n* Creates a read-through cache middleware that works with every family runtime.\n*\n* It caches an activity that carries `cacheAnnotation` without `bypass` and runs in runtime scope,\n* outside any transaction: never on a held connection, whose session may change what a read\n* returns, and never inside a transaction, which must read its own uncommitted writes. Two kinds\n* of activity are cached:\n*\n* - A statement whose plan carries the annotation, such as an annotated SQL builder query. Its key\n* is the annotation's `key`, else `deriveKey(exec, ctx)`; its value is the raw rows.\n* - A client activity that carries the annotation, such as an annotated ORM read. Its key is the\n* annotation's `key`, else `await activity.contentHash()`; its value is the result the client\n* ended the activity with. It is served as a whole, so a hit sends no statement.\n*\n* The middleware tags each stored value `statement-rows` or `call-result` and serves `statement-rows` only to a statement and `call-result` only to a client activity; under the same key, a value with the other tag is a miss, and the read stores its own value in its place. A key therefore belongs to one query and one terminal method: an ORM call served another method's result under a shared key fails with `ORM.INTERCEPTED_RESULT_INVALID` or returns the wrong shape.\n*\n* - `intercept.onQuery` and `intercept.onActivity` — on a hit, return the stored rows or result,\n* and nothing runs. On a miss, record the pending entry.\n* - `onRow` — collects each row of a statement miss.\n* - `activityEnded` — when the driver completed the statement, or the client ended the call\n* `succeeded` with a result (`null` is a result), calls `store.set(entry, value)` with the entry\n* `store.get` returned, which stores only if the key's version has not moved. If an `unset` moved\n* it meanwhile, `set` returns `false` and the middleware logs `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\tasync function lookUp(tag, key, meta, ctx, buffer) {\n\t\tconst entry = await store.get({\n\t\t\tkey,\n\t\t\tmeta\n\t\t});\n\t\tif (!entry.data.empty && hasTag(entry.data.value, tag)) {\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 entry.data.value;\n\t\t}\n\t\tctx.state(pendingMiss)[\"miss\"] = {\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 onQuery(exec, ctx) {\n\t\tif (!cacheable(ctx)) return;\n\t\tconst annotation = cacheAnnotation.read(exec);\n\t\tif (annotation === void 0 || annotation.bypass === true) return;\n\t\tconst value = await lookUp(\"statement-rows\", annotation.key ?? await deriveKey(exec, ctx), annotation.meta, ctx, []);\n\t\treturn value === void 0 ? void 0 : { rows: value.rows };\n\t}\n\tasync function onActivity(activity, ctx) {\n\t\tif (!cacheable(ctx)) return;\n\t\tconst annotation = cacheAnnotation.read(activity);\n\t\tif (annotation === void 0 || annotation.bypass === true) return;\n\t\tconst value = await lookUp(\"call-result\", annotation.key ?? await activity.contentHash(), annotation.meta, ctx, void 0);\n\t\treturn value === void 0 ? void 0 : { result: value.result };\n\t}\n\tasync function onRow(row, _exec, ctx) {\n\t\tpendingOf(ctx)?.buffer?.push(row);\n\t}\n\tasync function activityEnded(_activity, result, ctx) {\n\t\tconst miss = pendingOf(ctx);\n\t\tif (miss === void 0) return;\n\t\tdelete ctx.state(pendingMiss)[\"miss\"];\n\t\tconst value = valueToStore(miss, result);\n\t\tif (value === void 0) return;\n\t\tconst stored = await store.set(miss.entry, value);\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\tintercept: {\n\t\t\tonQuery,\n\t\t\tonActivity\n\t\t},\n\t\tonRow,\n\t\tactivityEnded,\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.18-dev.16",
3
+ "version": "8.0.0-rc.18-dev.18",
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.18-dev.16"
12
+ "@prisma/orm-framework": "8.0.0-rc.18-dev.18"
13
13
  },
14
14
  "devDependencies": {
15
- "@internal/middleware-cache": "8.0.0-rc.18-dev.16",
16
- "@repo/tsconfig": "8.0.0-rc.18-dev.16",
17
- "@repo/tsdown": "8.0.0-rc.18-dev.16",
15
+ "@internal/middleware-cache": "8.0.0-rc.18-dev.18",
16
+ "@repo/tsconfig": "8.0.0-rc.18-dev.18",
17
+ "@repo/tsdown": "8.0.0-rc.18-dev.18",
18
18
  "tsdown": "0.22.14",
19
19
  "typescript": "5.9.3"
20
20
  },