@ultimat3/query 11.0.0 → 11.2.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 +47 -1
- package/README.md +48 -0
- package/package.json +10 -6
- package/src/client.ts +58 -34
- package/src/errors.ts +6 -1
- package/src/index.ts +26 -0
- package/src/stable.ts +11 -9
package/CLAUDE.md
CHANGED
|
@@ -18,6 +18,7 @@ Owns the `query` primitive: reads, live reads, cursors, the incremental matcher.
|
|
|
18
18
|
| `http.ts` | route projection (`GET /_x/query/<kebab>`, `enforcedBy: 'handler'`) |
|
|
19
19
|
| `mcp-tool.ts` | MCP read descriptor, same `sourceFor` |
|
|
20
20
|
| `client.ts` | typed read client (browser-safe: no server imports) |
|
|
21
|
+
| — | opt-in flight control is **`@ultimat3/core`**'s `client-flight.ts` + `client-wire.ts`, re-exported from `src/index.ts`. There is no local copy and must not be one |
|
|
21
22
|
| `naming.ts` | export name → `/_x/query/<kebab>`. Pure string math. **Paths only** — no tool name |
|
|
22
23
|
| `registry.ts` | export-name registration, `describeQueries()`, and the `registerPrimitiveRegistrar('query', …)` announcement |
|
|
23
24
|
| `live.ts` | `LiveQuery` descriptor + cursor arithmetic |
|
|
@@ -162,6 +163,50 @@ Owns the `query` primitive: reads, live reads, cursors, the incremental matcher.
|
|
|
162
163
|
- `registry.ts` announces `registerQueries` in core's registrar table at import. That is how
|
|
163
164
|
`defineApi({ queries })` in `@ultimat3/action` registers a read without importing this package
|
|
164
165
|
sideways. Never remove the announcement: `defineApi` would then throw `X_REGISTRAR_MISSING`.
|
|
166
|
+
- **Flight control is `@ultimat3/core`'s, and this package RE-EXPORTS it.** `client-flight.ts` and
|
|
167
|
+
`client-wire.ts` shipped here and in the other tier-3 client package as byte-identical copies —
|
|
168
|
+
288 and 85 lines — kept in step by a `client-twin.test.ts` in each. A test that makes drift LOUD
|
|
169
|
+
is not the same as a file that cannot drift, and this package's own thesis is that duplication is
|
|
170
|
+
the defect. Both files import nothing but tier 0, which was always the argument for where they
|
|
171
|
+
belong; the one blocker was `isJsonObject`, now `@ultimat3/core`'s `json-object.ts`, re-exported
|
|
172
|
+
from `./stable` here. `createClientFlight`, `DEFAULT_CLIENT_RETRY`, `isTransientFailure`,
|
|
173
|
+
`isSuperseded`, `ClientFlight`, `ClientFlightOptions`, `ClientRetry`, `FlightKeyOptions`,
|
|
174
|
+
`FlightPlan` and `WireAnswer` are all still importable from `@ultimat3/query` — the same names,
|
|
175
|
+
and now literally the same objects the other package exports. Never re-declare one here; the
|
|
176
|
+
fix for anything wrong with the pipeline is an edit in `packages/core/src/client-flight.ts`.
|
|
177
|
+
- **Every mechanism underneath the flight is `@ultimat3/core`'s, the pipeline included.**
|
|
178
|
+
`createSingleFlight` for dedup, `createFence`/`isSuperseded` for supersession, `createFlightGate`
|
|
179
|
+
for the ceiling, `retry` + `backoffDelay` for the schedule, `isRetryableStatus` for the status
|
|
180
|
+
table, `X_TIMEOUT` for the deadline — and `createClientFlight`, which composes them. This package
|
|
181
|
+
declares NO new error code for any of it; never add a second curve, a second fence or a private
|
|
182
|
+
retry loop here, and `bun run flight-copies` is what says so.
|
|
183
|
+
- **`isTransientFailure` INVERTS `retryDecision`'s unclassified default, and the inversion must
|
|
184
|
+
survive** (`As of 2026-08-23`). `retryDecision` sends a throw nobody classified again until the
|
|
185
|
+
attempts run out; `@ultimat3/ai` and `@ultimat3/db` each refused the executor outright over it.
|
|
186
|
+
The client keeps the executor and supplies a predicate instead: a declared
|
|
187
|
+
`retryable`/`retry-after`, plus a dispatch that produced no response at all (`fetch` rejecting
|
|
188
|
+
with a plain `TypeError`), and nothing else — a caller's own `AbortError` and a foreign value are
|
|
189
|
+
terminal. The loop is stopped by RESOLVING to a private sentinel rather than by throwing, so the
|
|
190
|
+
original value still reaches the caller unwrapped, which is the property `retry`'s own header
|
|
191
|
+
promises. It lives in `packages/core/src/client-flight.ts` now; the tests that pin it from this
|
|
192
|
+
side still drive it through this package's own client.
|
|
193
|
+
- **`ClientFlight` is a TYPE inside `client.ts` and never a value.** That erasure is the entire
|
|
194
|
+
tree-shaking story: `rpc` alone is 14,759 B minified for the browser and `queryClient` alone is
|
|
195
|
+
12,755 B, against 20,292 B / 17,912 B with `createClientFlight` imported beside them — ±376 B run
|
|
196
|
+
to run, which is `Bun.build` 1.4.0 dropping core's `schema-error-codes.ts` (issue #273). A caller
|
|
197
|
+
who wants a plain typed fetch must not pay for the fence, the dedup map or the retry loop —
|
|
198
|
+
`packages/cli/src/templates/resource-form-island.ts` and `examples/dummy`'s contact-sales island
|
|
199
|
+
both write a bare `fetch` today because that bill used to be unavoidable. Never import
|
|
200
|
+
`createClientFlight` for a VALUE from `client.ts` — `ClientFlight` and `ClientRetry` are
|
|
201
|
+
`import type` from `@ultimat3/core` and must stay that way.
|
|
202
|
+
- **The `sideEffects` array is what makes the barrel shakable, and it is load-bearing** (`As of
|
|
203
|
+
2026-08-23`). Declaring nothing meant a bundler had to assume every module ran at import, so
|
|
204
|
+
`import { rpc } from '@ultimat3/action'` was 43,104 B and `import { queryClient } from
|
|
205
|
+
'@ultimat3/query'` was 40,859 B — three times the deep-import cost, through the ONLY specifier
|
|
206
|
+
the `exports` map offers. The arrays are the ones `bun run scripts/side-effects.ts --explain
|
|
207
|
+
--json` measures, and they must stay that: `errors.ts` runs `registerErrorCodes` at import in both
|
|
208
|
+
packages, and query's `registry.ts` runs `registerPrimitiveRegistrar('query', …)` — drop either
|
|
209
|
+
and a bundled app loses its error titles or throws `X_REGISTRAR_MISSING`. Never `false`.
|
|
165
210
|
- Policy runs per subscriber for live queries. Never cache a decision across actors.
|
|
166
211
|
- The matcher patches from `QueryShape`, never from SQL text.
|
|
167
212
|
- `paginate` has no `offset` parameter and must never grow one, and it is reachable **only** as
|
|
@@ -304,7 +349,8 @@ Owns the `query` primitive: reads, live reads, cursors, the incremental matcher.
|
|
|
304
349
|
date window shared one live query id. The hash form tags a `Date`, a `Map` and a `Set`
|
|
305
350
|
(`Date(<epoch>)`, `Map(…)`, `Set(…)`) beside the bare `NaN` / `±Infinity` / `-0` tokens it
|
|
306
351
|
already emitted, all for the reason a bare token exists: `'null'` collided with JSON `null` and
|
|
307
|
-
`String(-0)` is `"0"`. `stable.ts` keeps `
|
|
352
|
+
`String(-0)` is `"0"`. `stable.ts` keeps `columnOf` and a re-export of core's `isJsonObject` —
|
|
353
|
+
that predicate went down to tier 0 the same way, when `client-wire.ts` did. Ordinary
|
|
308
354
|
inputs are byte-identical, so the durable-key cost is confined to reads whose input carries a
|
|
309
355
|
`Date`, a `Map` or a `Set`: those cursors answer `X_CURSOR_INVALID` once, and their cache entries
|
|
310
356
|
are cold once. `query-hash.test.ts` is the pin, at `queryHash` and at `cacheKeyFor`.
|
package/README.md
CHANGED
|
@@ -53,6 +53,54 @@ const [post] = await queries.publicPost({ slug }); // typed input, typed rows
|
|
|
53
53
|
|
|
54
54
|
Both spellings run `queryClientMethodFor`, so a read has one URL however it is addressed.
|
|
55
55
|
|
|
56
|
+
### Flight control — `createClientFlight`, opt-in
|
|
57
|
+
|
|
58
|
+
A typed read is one `fetch` and nothing else until a `flight` is installed. With one, N concurrent
|
|
59
|
+
identical reads become ONE dispatch, a fence can retire everything issued before now, and a failure
|
|
60
|
+
worth sending again is sent again on `@ultimat3/core`'s one backoff curve.
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import { createClientFlight, isSuperseded, queryClient } from '@ultimat3/query';
|
|
64
|
+
|
|
65
|
+
declare const session: () => { userId: string };
|
|
66
|
+
declare const baseUrl: string;
|
|
67
|
+
type Api = { queries: Record<string, never> };
|
|
68
|
+
|
|
69
|
+
const flight = createClientFlight({
|
|
70
|
+
principal: () => session().userId, // dedup is OFF without this — see below
|
|
71
|
+
retry: { attempts: 3 }, // default is `attempts: 1`, i.e. no retry
|
|
72
|
+
deadlineMs: 10_000,
|
|
73
|
+
limit: { maxConcurrent: 6, maxQueued: 12 },
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
export const queries = queryClient<Api['queries']>({ baseUrl, flight });
|
|
77
|
+
|
|
78
|
+
// A route change, a sign-out, a tenant switch: everything already issued stops being addressed
|
|
79
|
+
// to anybody, and every read still open is aborted.
|
|
80
|
+
flight.bump();
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
| Rule | Why it is that way |
|
|
84
|
+
|---|---|
|
|
85
|
+
| the dedup key is `[principal, url]`, and **no principal means no dedup** | a key that is only the URL lets one caller join another's still-open read across a sign-in or a tenant switch |
|
|
86
|
+
| a caller-supplied `signal` is **never** shared | the leader owns the request, so one caller's abort would cancel every other caller's read; refcounting joiners is the "correct" fix and this is the one that cannot be wrong |
|
|
87
|
+
| `{ fresh: true }` opts one read out | the case it exists for is "this read exists BECAUSE something just changed", which must not join a dispatch that left before the change did |
|
|
88
|
+
| every joiner parses its own rows | what is shared is the immutable body TEXT; handing two callers one array hands each of them the other's edits |
|
|
89
|
+
| a fenced answer is `X_SUPERSEDED`, never a failure | `isSuperseded(error)` is the read; a caller that cannot tell it from a refusal retries a request its own context has already replaced |
|
|
90
|
+
| an **unclassified** throw is not retried | core's `retryDecision` retries anything nobody classified — a caller's own `AbortError` included. A client retries a declared `retryable`/`retry-after`, plus a dispatch that produced no response at all |
|
|
91
|
+
| a deadline is `X_TIMEOUT` | core's code, already classified `retryable`, so a caller's own loop needs no table |
|
|
92
|
+
|
|
93
|
+
`createClientFlight` is **`@ultimat3/core`'s**, re-exported here: it is the same object
|
|
94
|
+
`@ultimat3/action` re-exports, because both packages are tier 3 and neither may import the other.
|
|
95
|
+
It shipped as a byte-identical copy in each; the copies are gone and every name is importable from
|
|
96
|
+
this package exactly as before.
|
|
97
|
+
|
|
98
|
+
Bundle cost is why `ClientFlight` is a TYPE inside `client.ts` and never a value: importing
|
|
99
|
+
`queryClient` alone from this package is **12,755 B** minified for the browser, and adding
|
|
100
|
+
`createClientFlight` is **17,912 B**. A caller who wants a plain typed fetch pays for none of it.
|
|
101
|
+
Expect ±376 B run to run — `Bun.build` 1.4.0 drops `@ultimat3/core`'s `schema-error-codes.ts` from
|
|
102
|
+
some builds even though `sideEffects` names it (issue #273).
|
|
103
|
+
|
|
56
104
|
The declaration is lifted too: `.input`, `.policy`, `.cache`, `.mcp`, `.isLive`. `sql` is not
|
|
57
105
|
among them — it lives in a private store inside `read.ts`, so `sourceFor` is the only thing
|
|
58
106
|
that can build a source and there is nowhere for a second authz path to hide. Something that
|
package/package.json
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/query",
|
|
3
|
-
"version": "11.
|
|
3
|
+
"version": "11.2.0",
|
|
4
4
|
"description": "The query primitive: a policy-checked read, optionally live, with cursor pagination and an incremental matcher",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
|
+
"sideEffects": [
|
|
8
|
+
"./src/errors.ts",
|
|
9
|
+
"./src/registry.ts"
|
|
10
|
+
],
|
|
7
11
|
"repository": {
|
|
8
12
|
"type": "git",
|
|
9
13
|
"url": "git+https://github.com/developerz-ai/ultimate.git",
|
|
@@ -31,10 +35,10 @@
|
|
|
31
35
|
"test": "bun test"
|
|
32
36
|
},
|
|
33
37
|
"dependencies": {
|
|
34
|
-
"@ultimat3/cache": "11.
|
|
35
|
-
"@ultimat3/core": "11.
|
|
36
|
-
"@ultimat3/http": "11.
|
|
37
|
-
"@ultimat3/policy": "11.
|
|
38
|
-
"@ultimat3/schema": "11.
|
|
38
|
+
"@ultimat3/cache": "11.2.0",
|
|
39
|
+
"@ultimat3/core": "11.2.0",
|
|
40
|
+
"@ultimat3/http": "11.2.0",
|
|
41
|
+
"@ultimat3/policy": "11.2.0",
|
|
42
|
+
"@ultimat3/schema": "11.2.0"
|
|
39
43
|
}
|
|
40
44
|
}
|
package/src/client.ts
CHANGED
|
@@ -8,9 +8,16 @@
|
|
|
8
8
|
* output schema — row types come from the `SqlSource` its `sql:` returns — so there is nothing
|
|
9
9
|
* here to rehydrate a `Date` with, and an instant reaches a caller as the ISO string
|
|
10
10
|
* `JSON.stringify` wrote. A surface that formats one converts at its own edge.
|
|
11
|
+
*
|
|
12
|
+
* `ClientFlight` is a TYPE here and never a value: dedup, retry, the deadline and the fence are
|
|
13
|
+
* `@ultimat3/core`'s `client-flight.ts`, and a caller that never calls `createClientFlight` does
|
|
14
|
+
* not pay a byte for any of them — an `import type` is erased and the value import would not be.
|
|
15
|
+
* That erasure is the whole reason two islands in this repo write a bare `fetch` instead of
|
|
16
|
+
* importing a typed client.
|
|
11
17
|
*/
|
|
12
18
|
|
|
13
|
-
import {
|
|
19
|
+
import type { ClientFlight, ClientRetry, WireAnswer } from '@ultimat3/core';
|
|
20
|
+
import { problemOf, traceHeaders } from '@ultimat3/core';
|
|
14
21
|
import type { InferInput, StandardSchemaV1 } from '@ultimat3/schema';
|
|
15
22
|
import { QueryRequestFailedError } from './errors';
|
|
16
23
|
import { derivePath } from './naming';
|
|
@@ -23,10 +30,24 @@ export interface QueryClientOptions {
|
|
|
23
30
|
readonly baseUrl: string;
|
|
24
31
|
readonly fetch?: FetchLike;
|
|
25
32
|
readonly headers?: Readonly<Record<string, string>>;
|
|
33
|
+
/**
|
|
34
|
+
* Opt-in flight control for every read this client makes — `createClientFlight({ principal })`.
|
|
35
|
+
* Absent, a read is one `fetch` and nothing else, which is what every caller written before this
|
|
36
|
+
* option existed already gets.
|
|
37
|
+
*/
|
|
38
|
+
readonly flight?: ClientFlight;
|
|
26
39
|
}
|
|
27
40
|
|
|
28
41
|
export interface QueryCallOptions {
|
|
29
42
|
readonly signal?: AbortSignal;
|
|
43
|
+
/**
|
|
44
|
+
* Refuse to join an identical read already in flight. The case it exists for: this read exists
|
|
45
|
+
* BECAUSE something just changed, so an answer dispatched before the change is the wrong one.
|
|
46
|
+
* Named for `read.ts`'s `fresh`, which means the same thing one layer down — do not join.
|
|
47
|
+
*/
|
|
48
|
+
readonly fresh?: boolean;
|
|
49
|
+
/** Overrides the flight's retry policy for this one read. Ignored with no `flight` installed. */
|
|
50
|
+
readonly retry?: ClientRetry;
|
|
30
51
|
}
|
|
31
52
|
|
|
32
53
|
/** `feed({ orgId })` with the input schema and the row type both inferred. */
|
|
@@ -107,41 +128,50 @@ async function read(
|
|
|
107
128
|
): Promise<unknown> {
|
|
108
129
|
const search = searchOf(input);
|
|
109
130
|
const url = `${base}${derivePath(name)}${search === '' ? '' : `?${search}`}`;
|
|
131
|
+
const dispatch = (signal: AbortSignal | undefined): Promise<WireAnswer> =>
|
|
132
|
+
fetchOnce(doFetch, url, options, name, signal ?? callOptions.signal);
|
|
133
|
+
|
|
134
|
+
const flight = options.flight;
|
|
135
|
+
const answer =
|
|
136
|
+
flight === undefined
|
|
137
|
+
? await dispatch(undefined)
|
|
138
|
+
: await flight.run({
|
|
139
|
+
key: flight.keyFor(url, callOptions),
|
|
140
|
+
// A caller holding its own signal owns this read's lifecycle: it is neither shared nor
|
|
141
|
+
// aborted by a fence bump, and its signal is the only one that reaches the wire.
|
|
142
|
+
abortable: callOptions.signal === undefined,
|
|
143
|
+
...(callOptions.retry === undefined ? {} : { retry: callOptions.retry }),
|
|
144
|
+
run: dispatch,
|
|
145
|
+
});
|
|
146
|
+
// Parsed per CALLER, never once per dispatch: N joiners of one deduped read may not be handed
|
|
147
|
+
// one mutable array between them, and the shared value is the immutable body TEXT for that
|
|
148
|
+
// reason alone.
|
|
149
|
+
return JSON.parse(answer.text) as unknown;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** One dispatch. Everything above it decides how many times this happens; it decides none. */
|
|
153
|
+
async function fetchOnce(
|
|
154
|
+
doFetch: FetchLike,
|
|
155
|
+
url: string,
|
|
156
|
+
options: QueryClientOptions,
|
|
157
|
+
name: string,
|
|
158
|
+
signal: AbortSignal | undefined,
|
|
159
|
+
): Promise<WireAnswer> {
|
|
110
160
|
const init: RequestInit = {
|
|
111
|
-
method: 'GET',
|
|
112
161
|
// `traceHeaders()` before the caller's, so an explicit `traceparent` still wins. Without it a
|
|
113
162
|
// service-to-service read started a fresh root trace on the other side, and "which of my
|
|
114
163
|
// downstreams is slow" was unanswerable across every Ultimate-to-Ultimate hop.
|
|
115
164
|
headers: { accept: 'application/json', ...traceHeaders(), ...options.headers },
|
|
116
|
-
|
|
165
|
+
method: 'GET',
|
|
166
|
+
...(signal === undefined ? {} : { signal }),
|
|
117
167
|
};
|
|
118
168
|
|
|
119
169
|
const response = await doFetch(url, init);
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/** A `traceparent` is `00-<32 hex>-<16 hex>-<2 hex>`, and nothing else may be sent as one. */
|
|
127
|
-
const TRACE_ID = /^[0-9a-f]{32}$/;
|
|
128
|
-
const SPAN_ID = /^[0-9a-f]{16}$/;
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* The current trace, as the W3C header — or nothing at all. `currentSpanContext()` answers with
|
|
132
|
-
* an empty `spanId` when a request context exists but no span is active, and `00-<trace>--01` is
|
|
133
|
-
* a header every collector drops, so an incomplete context sends none. In a browser there is no
|
|
134
|
-
* ambient context and this is always empty, which is also what keeps a cross-origin read from
|
|
135
|
-
* acquiring a CORS preflight it did not have.
|
|
136
|
-
*
|
|
137
|
-
* `@ultimat3/action`'s client carries the twin of this function: both are tier 3, so neither may
|
|
138
|
-
* import the other — the same reason `naming.ts` is ported rather than shared.
|
|
139
|
-
*/
|
|
140
|
-
function traceHeaders(): Record<string, string> {
|
|
141
|
-
const context = currentSpanContext();
|
|
142
|
-
if (context === undefined) return {};
|
|
143
|
-
if (!TRACE_ID.test(context.traceId) || !SPAN_ID.test(context.spanId)) return {};
|
|
144
|
-
return { traceparent: traceparent(context) };
|
|
170
|
+
// Read as TEXT once: a `Response` body is a single-use stream, so the failure path and the row
|
|
171
|
+
// path cannot both have it, and a shared answer has to be something a joiner can re-read.
|
|
172
|
+
const text = await response.text();
|
|
173
|
+
if (!response.ok) throw new QueryRequestFailedError(name, response.status, problemOf(text));
|
|
174
|
+
return { status: response.status, text };
|
|
145
175
|
}
|
|
146
176
|
|
|
147
177
|
/**
|
|
@@ -160,9 +190,3 @@ function searchOf(input: unknown): string {
|
|
|
160
190
|
}
|
|
161
191
|
return params.toString();
|
|
162
192
|
}
|
|
163
|
-
|
|
164
|
-
/** `application/problem+json`, or nothing when a proxy answered instead of the app. */
|
|
165
|
-
async function problemOf(response: Response): Promise<Record<string, unknown>> {
|
|
166
|
-
const body: unknown = await response.json().catch(() => null);
|
|
167
|
-
return isJsonObject(body) ? body : {};
|
|
168
|
-
}
|
package/src/errors.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Every failure @ultimat3/query can produce, one subclass per stable code. */
|
|
2
|
-
import { assertNever, registerErrorCodes, UltimateError } from '@ultimat3/core';
|
|
2
|
+
import { assertNever, registerErrorCodes, retryForStatus, UltimateError } from '@ultimat3/core';
|
|
3
3
|
import type { SurfaceDenial } from '@ultimat3/policy';
|
|
4
4
|
|
|
5
5
|
// No `docs:` on the classes below, with one exception noted at `QueryRequestFailedError`.
|
|
@@ -279,6 +279,11 @@ export class QueryRequestFailedError extends UltimateError {
|
|
|
279
279
|
const served = text(problem.docs);
|
|
280
280
|
super({
|
|
281
281
|
code,
|
|
282
|
+
// The status is what says "send it again", and only where nobody has classified the code:
|
|
283
|
+
// `UltimateError` otherwise fills `retry` from `retryFor(code)`, which fails closed, so a
|
|
284
|
+
// 502 out of a typed read announced itself as `terminal` on the one field the framework
|
|
285
|
+
// promises a client never has to infer.
|
|
286
|
+
retry: retryForStatus(code, status),
|
|
282
287
|
cause: text(problem.cause) ?? text(problem.detail) ?? `${name} returned HTTP ${status}`,
|
|
283
288
|
fix:
|
|
284
289
|
text(problem.fix) ??
|
package/src/index.ts
CHANGED
|
@@ -6,6 +6,32 @@
|
|
|
6
6
|
* authorize or execute on its own. One authz system, structurally.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Flight control for the typed client, and OPT-IN by construction: `client.ts` names `ClientFlight`
|
|
11
|
+
* as a TYPE only, so a caller that never mentions `createClientFlight` pays nothing for the fence,
|
|
12
|
+
* the dedup map or the retry loop. Every mechanism underneath is `@ultimat3/core`'s — one fence,
|
|
13
|
+
* one flight map, one gate, one backoff curve for the whole framework — and so is the pipeline
|
|
14
|
+
* itself: it shipped as a byte-identical copy here and in `@ultimat3/action`, and
|
|
15
|
+
* two tier-3 packages may not import each other, so the one copy lives at tier 0.
|
|
16
|
+
*
|
|
17
|
+
* Re-exported rather than re-declared, so every name is importable from this package exactly as
|
|
18
|
+
* before. `isSuperseded` is core's too: reading a fenced answer is the point of installing a
|
|
19
|
+
* flight, and it should not cost a second import.
|
|
20
|
+
*/
|
|
21
|
+
export type {
|
|
22
|
+
ClientFlight,
|
|
23
|
+
ClientFlightOptions,
|
|
24
|
+
ClientRetry,
|
|
25
|
+
FlightKeyOptions,
|
|
26
|
+
FlightPlan,
|
|
27
|
+
WireAnswer,
|
|
28
|
+
} from '@ultimat3/core';
|
|
29
|
+
export {
|
|
30
|
+
createClientFlight,
|
|
31
|
+
DEFAULT_CLIENT_RETRY,
|
|
32
|
+
isSuperseded,
|
|
33
|
+
isTransientFailure,
|
|
34
|
+
} from '@ultimat3/core';
|
|
9
35
|
/** Re-exported so a `query` file needs one import, not two. Same object as schema's. */
|
|
10
36
|
export type { Infer } from '@ultimat3/schema';
|
|
11
37
|
export { t } from '@ultimat3/schema';
|
package/src/stable.ts
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* How a column is read off a row that declares no index signature — one predicate the read path
|
|
3
|
+
* needs everywhere, and one re-export beside it.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Both halves used to be declared here and no longer are. `canonicalJson` and `fingerprint` are
|
|
6
|
+
* `@ultimat3/core`'s, because `@ultimat3/action` and `@ultimat3/realtime` need the identical
|
|
7
|
+
* function and all three are tier 3 — so a copy in any of them was a second answer for the other
|
|
8
|
+
* two, and the copies had already diverged. This one rendered every `Date` as `{}`. `isJsonObject`
|
|
9
|
+
* went the same way for the same reason, when `client-wire.ts` moved to tier 0.
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
import { isJsonObject } from '@ultimat3/core';
|
|
13
|
+
|
|
14
|
+
/** Re-exported, not re-declared: `./stable` stays this package's one import path for it. */
|
|
15
|
+
export { isJsonObject };
|
|
14
16
|
|
|
15
17
|
/** Column read that works for interfaces without an index signature. */
|
|
16
18
|
export function columnOf(row: object, column: string): unknown {
|