@ultimat3/query 31.6.1 → 31.7.0

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/CLAUDE.md CHANGED
@@ -261,7 +261,8 @@ Owns the `query` primitive: reads, live reads, cursors, the incremental matcher.
261
261
  ## Invariants — caching
262
262
 
263
263
  - **The request memo holds the read, not the rows**: `readOnce` publishes the in-flight promise
264
- before its first await; a rejection is evicted. `requestMemo(ctx)` is `Map<string, Promise<unknown>>`.
264
+ before its first await; a rejection is evicted. `requestMemo(ctx)` is `memoFlights(ctx,
265
+ 'query')` (`@ultimat3/cache`): one store, 8 MiB per ctx. Never a second `WeakMap` here.
265
266
  - **Every read is memoized; only a `cache:` read goes through the tier** (`readThrough`). One key
266
267
  function, `cacheKeyFor`, over the PARSED input `admitAndBuild` returns — never `raw`, or a
267
268
  stripped `?junk=N` is an entry and a read per N (`read-cache-key.test.ts`). Outside, a key
package/README.md CHANGED
@@ -609,9 +609,15 @@ round trip. A rejection is evicted — a failed read is not the request's answer
609
609
 
610
610
  | Layer | Applies to | Lifetime |
611
611
  |---|---|---|
612
- | request memo (`readOnce`) | **every** query, `cache:` or not | the request — a `Ctx`'s identity is the key |
612
+ | request memo (`readOnce`) | **every** query, `cache:` or not | the request — a `Ctx`'s identity is the key; at most `MEMO_MAX_BYTES` (8 MiB) per ctx, oldest evicted (a job attempt reading page after page) |
613
613
  | tag-keyed tier (`readThrough`) | a query that declares `cache:` | `ttlMs`, or until an `invalidates` fan-out drops the tag |
614
614
 
615
+ **`requestMemo(ctx)` returns `MemoFlights`** (`@ultimat3/cache`), not a `Map` — `get`, `has`,
616
+ `publish`, `bytes` over `memoFlights(ctx, 'query')`, the one per-ctx memo store. A caller that wrote
617
+ `requestMemo(ctx).set(…)`/`.delete(…)` no longer compiles: publish with `readFresh(ctx, key, run)`,
618
+ and let a rejection evict itself. A held read is charged by `approxBytes` — a bounded sample of
619
+ the rows (≈2 µs for 50 or 500 rows), never a `JSON.stringify` (≈155 µs / 1.5 ms).
620
+
615
621
  `cache:` buys the tier, never the memo: an uncached lookup called once per row of a list would
616
622
  otherwise cost one round trip per row. Parsing, the policy and `sql()` still run on every call —
617
623
  the memo holds the execution, not the decision — and `.as()` reads in a child context, so an
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/query",
3
- "version": "31.6.1",
3
+ "version": "31.7.0",
4
4
  "description": "The query primitive: a policy-checked read, optionally live, with cursor pagination and an incremental matcher",
5
5
  "keywords": [
6
6
  "ultimate",
@@ -49,12 +49,12 @@
49
49
  "test": "bun test"
50
50
  },
51
51
  "dependencies": {
52
- "@ultimat3/cache": "31.6.1",
53
- "@ultimat3/core": "31.6.1",
54
- "@ultimat3/db": "31.6.1",
55
- "@ultimat3/entity": "31.6.1",
56
- "@ultimat3/http": "31.6.1",
57
- "@ultimat3/policy": "31.6.1",
58
- "@ultimat3/schema": "31.6.1"
52
+ "@ultimat3/cache": "31.7.0",
53
+ "@ultimat3/core": "31.7.0",
54
+ "@ultimat3/db": "31.7.0",
55
+ "@ultimat3/entity": "31.7.0",
56
+ "@ultimat3/http": "31.7.0",
57
+ "@ultimat3/policy": "31.7.0",
58
+ "@ultimat3/schema": "31.7.0"
59
59
  }
60
60
  }
package/src/cache.ts CHANGED
@@ -5,8 +5,8 @@
5
5
  * read gets the memo; the ladder is the half a query opts into.
6
6
  */
7
7
 
8
- import type { CacheStack, CacheTag, CacheTier } from '@ultimat3/cache';
9
- import { cacheStack, registeredTiers, tagKeys } from '@ultimat3/cache';
8
+ import type { CacheStack, CacheTag, CacheTier, MemoFlights } from '@ultimat3/cache';
9
+ import { cacheStack, memoFlights, registeredTiers, tagKeys } from '@ultimat3/cache';
10
10
  import type { Actor, Clock, Ctx } from '@ultimat3/core';
11
11
  import { assertNever, fingerprint } from '@ultimat3/core';
12
12
 
@@ -18,22 +18,18 @@ import { assertNever, fingerprint } from '@ultimat3/core';
18
18
  export const DEFAULT_READ_CACHE_TTL_MS = 60_000;
19
19
 
20
20
  /**
21
- * Request-scoped memo. Keyed by ctx identity so it dies with the request.
21
+ * Request-scoped memo: `@ultimat3/cache`'s per-ctx memo store, under its `MEMO_MAX_BYTES` bound —
22
+ * never a second store. Keyed by ctx identity so it dies with the request; bounded because a job's
23
+ * ctx is the whole attempt, and an unbounded memo held every page a backfill read until it ended.
22
24
  *
23
25
  * An entry is the read *in flight*, not its value: unsettled it is the answer a caller is
24
26
  * already waiting for, settled it is the answer. That is what makes two concurrent identical
25
27
  * reads one round trip — and it is why no sentinel is needed for a legitimately `undefined`
26
28
  * value, which a value-keyed memo cannot tell apart from a miss. A promise is never `undefined`.
29
+ * An evicted entry is one more read of the source, never a wrong answer.
27
30
  */
28
- const memos = new WeakMap<object, Map<string, Promise<unknown>>>();
29
-
30
- export function requestMemo(ctx: Ctx): Map<string, Promise<unknown>> {
31
- const key: object = ctx;
32
- const existing = memos.get(key);
33
- if (existing !== undefined) return existing;
34
- const created = new Map<string, Promise<unknown>>();
35
- memos.set(key, created);
36
- return created;
31
+ export function requestMemo(ctx: Ctx): MemoFlights {
32
+ return memoFlights(ctx, 'query');
37
33
  }
38
34
 
39
35
  /**
@@ -153,28 +149,21 @@ export async function readOnce<T>(ctx: Ctx, key: string, run: () => Promise<T>):
153
149
  * the same request the answer this one just proved stale — so the guarantee would end at the one
154
150
  * call that asked for it.
155
151
  */
156
- export function readFresh<T>(ctx: Ctx, key: string, run: () => Promise<T>): Promise<T> {
157
- return publish(requestMemo(ctx), key, run);
152
+ export async function readFresh<T>(ctx: Ctx, key: string, run: () => Promise<T>): Promise<T> {
153
+ // `async`, so a `run` that throws synchronously rejects like every other failed read.
154
+ return await publish(requestMemo(ctx), key, run);
158
155
  }
159
156
 
160
- /** The read in flight: published before its first await, evicted if it rejects. */
161
- async function publish<T>(
162
- memo: Map<string, Promise<unknown>>,
163
- key: string,
164
- run: () => Promise<T>,
165
- ): Promise<T> {
157
+ /**
158
+ * The read in flight: published before its first await. A rejection is not an answer, so the store
159
+ * drops it and a later read in the same request retries — only ours: a fresh read may have
160
+ * replaced this entry already, and evicting that one would discard a live answer (`holdValue`).
161
+ */
162
+ function publish<T>(memo: MemoFlights, key: string, run: () => Promise<T>): Promise<T> {
166
163
  // Published before the first await, so a reader arriving in the same tick finds this read.
167
164
  const flight = run();
168
- memo.set(key, flight);
169
- try {
170
- return await flight;
171
- } catch (error) {
172
- // A rejection is not an answer. Drop it so a later read in the same request retries
173
- // instead of replaying one failure until the request ends. Only ours: a fresh read may have
174
- // replaced this entry already, and evicting that one would discard a live answer.
175
- if (memo.get(key) === flight) memo.delete(key);
176
- throw error;
177
- }
165
+ memo.publish(key, flight);
166
+ return flight;
178
167
  }
179
168
 
180
169
  /**