@ultimat3/core 20.2.1 → 21.0.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.
@@ -0,0 +1,86 @@
1
+ /**
2
+ * The ONE browser HTTP function. Every client surface — `rpc()`, `queryClient()`, a signed upload,
3
+ * the store's refetch — sends through here, so credentials, headers, the error decode, the records
4
+ * envelope and the principal fence are decided once. A GET is a read: abortable on `rescope()`,
5
+ * deduped when a `ClientFlight` is supplied. Anything else is a write: never deduped, never
6
+ * aborted by the fence, and its records never adopted across one.
7
+ *
8
+ * `createClientFlight` is NOT imported here at value level — a caller passes one, and only then
9
+ * does its graph enter the bundle. Neither is `traceHeaders()`: the trace and budget headers come
10
+ * from an outbound slot that `runWithContext`/`startSpan` fill SERVER-side (`outbound-headers.ts`),
11
+ * so a browser, which never has either, carries zero bytes of telemetry, context or logger.
12
+ *
13
+ * Measured `bun build --target=browser --minify` through the barrel, As of 2026-09-22 (before →
14
+ * after the outbound slot): `rpc` 23,164 → 18,119 B; `queryClient` 24,100 → 22,897 B (the rest is
15
+ * `@ultimat3/query`'s anchored `registry.ts`); `clientTransport` 13,571 B; `pageClient` 8,139 B;
16
+ * `UltimateError` 7,679 B. ~7.6 kB of every barrel import is the error-code registry that the
17
+ * anchored `schema-error-codes.ts` registers into — `pageClient` straight from its module is 332 B.
18
+ */
19
+
20
+ import type { Answer, TransportRequest } from './client-dispatch';
21
+ import { dispatch } from './client-dispatch';
22
+ import { transportFailed } from './client-problem';
23
+ import { scopeChanged } from './client-scope-error';
24
+ import type { RecordEnvelope } from './record-envelope';
25
+ import { decodeRecordEnvelope } from './record-envelope';
26
+ import { pageClient, recordSink } from './record-sink';
27
+
28
+ const ONCE = { attempts: 1 } as const;
29
+
30
+ export async function clientTransport<T = unknown>(req: TransportRequest): Promise<T> {
31
+ const read = req.method === 'GET';
32
+ const issued = pageClient().scope.epoch;
33
+ const flight = req.flight;
34
+ const answer: Answer =
35
+ flight === undefined
36
+ ? await dispatch(req, read, issued, undefined)
37
+ : await flight.run({
38
+ // A mutation never joins another mutation, idempotency key or not: the key is for the
39
+ // server's replay, and sharing one dispatch would hide the second intent from it.
40
+ key: read ? flight.keyFor(req.url, { signal: req.signal, fresh: req.fresh }) : undefined,
41
+ abortable: read,
42
+ // A stream is read once, so a second attempt re-sends a body that is already spent —
43
+ // and fails as the network would, until the attempts run out. One attempt, always.
44
+ retry: req.rawBody instanceof ReadableStream ? ONCE : req.retry,
45
+ run: (signal) => dispatch(req, read, issued, signal),
46
+ // `dispatch` makes every wire failure `X_CLIENT_TRANSPORT_FAILED`; a bare throw that
47
+ // reaches the flight is a caller hook's, never the network's.
48
+ classified: true,
49
+ });
50
+ const current = pageClient().scope.epoch;
51
+ // A read that raced the abort still belongs to the previous principal.
52
+ if (read && current !== issued) throw scopeChanged(req.url, issued, current);
53
+ if (req.rawBody !== undefined) return undefined as T;
54
+ const envelope = unwrap(answer, req.url, read);
55
+ // A write that crossed a rescope HAS landed, so its caller is told — but its rows are the
56
+ // previous principal's, and the new scope's store never sees them.
57
+ if (current === issued) {
58
+ adopt(envelope);
59
+ if (answer.enveloped) req.onEnvelope?.(envelope);
60
+ }
61
+ return envelope.data as T;
62
+ }
63
+
64
+ function unwrap(answer: Answer, url: string, read: boolean): RecordEnvelope {
65
+ let body: unknown;
66
+ try {
67
+ body = answer.text === '' ? undefined : (JSON.parse(answer.text) as unknown);
68
+ } catch (error) {
69
+ throw transportFailed(
70
+ 'body',
71
+ `${url} answered 2xx with a body that is not JSON — a proxy answered instead of the app`,
72
+ read ? 'retryable' : undefined,
73
+ { url },
74
+ error,
75
+ );
76
+ }
77
+ return answer.enveloped ? decodeRecordEnvelope(body) : { data: body };
78
+ }
79
+
80
+ /** Adopt, then remove: a key in both is gone, never resurrected by the same answer. */
81
+ function adopt(envelope: RecordEnvelope): void {
82
+ const sink = recordSink();
83
+ if (sink === undefined) return;
84
+ for (const [type, rows] of Object.entries(envelope.records ?? {})) sink.adopt(type, rows);
85
+ for (const [type, keys] of Object.entries(envelope.removed ?? {})) sink.remove(type, keys);
86
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * The ONE conflict vocabulary: which row survives when an optimistic local write and the server's
3
+ * answer disagree. Row-shaped because the client store is — a merge over a mutator's OUTPUT had
4
+ * nowhere to land, and realtime's rebase silently dropped it. Tier 0 so `action` and `realtime`
5
+ * (both tier 3) name the same type.
6
+ */
7
+
8
+ /** One record as the client store holds it: a JSON object, keyed by field name. */
9
+ export type Row = Readonly<Record<string, unknown>>;
10
+
11
+ export type ConflictPolicy =
12
+ | 'server-wins'
13
+ | 'last-write-wins'
14
+ | { readonly kind: 'custom'; readonly merge: (local: Row, server: Row) => Row };
15
+
16
+ export interface ResolveConflictOptions {
17
+ /**
18
+ * The field `last-write-wins` compares — a finite number (epoch ms) the SERVER wrote. Default
19
+ * `updatedAt`, the name realtime's rebase has always read.
20
+ */
21
+ readonly clockField?: string | undefined;
22
+ }
23
+
24
+ /**
25
+ * The surviving row. `last-write-wins` keeps the local row only when its clock is provably newer
26
+ * by the server's own field; a missing or non-numeric clock on either side is no proof, so the
27
+ * server's row stands — the store must never keep a guess over an answer.
28
+ */
29
+ export function resolveConflict(
30
+ policy: ConflictPolicy,
31
+ local: Row,
32
+ server: Row,
33
+ options: ResolveConflictOptions = {},
34
+ ): Row {
35
+ if (typeof policy !== 'string') return policy.merge(local, server);
36
+ if (policy === 'server-wins') return server;
37
+ const field = options.clockField ?? 'updatedAt';
38
+ const localAt = clockOf(local, field);
39
+ const serverAt = clockOf(server, field);
40
+ return localAt !== undefined && serverAt !== undefined && localAt > serverAt ? local : server;
41
+ }
42
+
43
+ function clockOf(row: Row, field: string): number | undefined {
44
+ // Own keys only: a field named `constructor` must not read `Object.prototype`'s.
45
+ if (!Object.hasOwn(row, field)) return undefined;
46
+ const value = row[field];
47
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
48
+ }
package/src/context.ts CHANGED
@@ -37,6 +37,7 @@ import { UltimateError } from './errors';
37
37
  import { finiteOption } from './finite-option';
38
38
  import { traceId as newTraceId, uuid } from './ids';
39
39
  import { type Logger, logger as rootLogger, setLoggerContextFields } from './logger';
40
+ import { installTraceHeaders } from './outbound-headers';
40
41
  import { type Role, resolveRole } from './roles';
41
42
  import { installedServices, isManagedService } from './service';
42
43
 
@@ -130,6 +131,13 @@ export interface CtxInit {
130
131
  /** Epoch ms. `@ultimat3/http`'s `startDeadline` is the one production writer. */
131
132
  readonly deadlineAt?: number | undefined;
132
133
  readonly services?: ServiceBag | undefined;
134
+ /**
135
+ * `false` installs no `defineService` factory — only `services` — and leaves the registered
136
+ * ones to the caller. `@ultimat3/http` is that caller: its context exists before the `auth`
137
+ * stage names the actor, and a service built here would act as anonymous for the whole request.
138
+ * Default `true`.
139
+ */
140
+ readonly installServices?: boolean | undefined;
133
141
  }
134
142
 
135
143
  /**
@@ -184,7 +192,8 @@ export function createContext(init: CtxInit = {}): Ctx {
184
192
  // wins over an auto-installed one of the same name — a test's hand-built mock overrides the
185
193
  // real thing on purpose.
186
194
  const preview: CtxFacts = Object.freeze({ ...explicit, ...fields, services: explicit });
187
- const services: ServiceBag = Object.freeze({ ...installedServices(preview), ...explicit });
195
+ const installed = init.installServices === false ? {} : installedServices(preview);
196
+ const services: ServiceBag = Object.freeze({ ...installed, ...explicit });
188
197
  const ctx = {
189
198
  // Services ride ON the context, not only under `ctx.services`: `CtxServices` exists to be
190
199
  // augmented, so `ctx.posts` has to BE the service. Spread first, so a service that collides
@@ -208,6 +217,8 @@ export function createContext(init: CtxInit = {}): Ctx {
208
217
  }
209
218
 
210
219
  export function runWithContext<T>(ctx: Ctx, fn: () => T): T {
220
+ // A request scope is what gives an outbound typed call a budget to forward; see the module.
221
+ installTraceHeaders();
211
222
  return requestContext.run(ctx, fn);
212
223
  }
213
224
 
@@ -0,0 +1,77 @@
1
+ // Single responsibility: core's OWN error-code titles, registered at import. A side-effect
2
+ // anchor, bare-imported by the barrel — the same shape as `schema-error-codes.ts` — so the table
3
+ // rides every barrel import and none of the light paths: `UltimateError` alone no longer carries
4
+ // 40 titles into a browser island that throws one code. Listed in `SIDE_EFFECTS_ANCHORS`.
5
+
6
+ import type { ErrorCodeDescriptor } from './error-codes';
7
+ import { descriptor, registerCoreErrorCodes } from './error-codes';
8
+
9
+ /** Codes owned by `@ultimat3/core`. Every other package calls `registerErrorCodes()`. */
10
+ const CORE_CODE_TITLES = {
11
+ X_ABORTED: 'operation aborted',
12
+ X_ASYNC_CONTEXT_UNAVAILABLE: 'async context unavailable',
13
+ // The browser seam's three (`client-transport.ts`, `record-envelope.ts`, `client-scope.ts`).
14
+ X_CLIENT_RECORD_ENVELOPE_INVALID: 'a response marked as a records envelope has the wrong shape',
15
+ X_CLIENT_SCOPE_CHANGED: 'the page changed principal while this request was in flight',
16
+ X_CLIENT_TRANSPORT_FAILED: 'a browser request got no answer from the app',
17
+ X_CONFIG_INVALID: 'app.config.ts is invalid',
18
+ X_CURSOR_INVALID: 'pagination cursor is malformed, tampered with or from another query',
19
+ X_CURSOR_SECRET_DEV: 'cursors are signed with the shipped development key',
20
+ X_DRAINING: 'process is draining and refuses new work',
21
+ X_ENV_EXAMPLE_DRIFT: '.env.example does not declare every variable the schema requires',
22
+ X_ENV_MISSING: 'required environment variables are missing or invalid',
23
+ X_ENVIRONMENT_INVALID: 'ULTIMATE_ENV is not a known environment',
24
+ X_ERROR_CODE_DUPLICATE: 'error code registered twice',
25
+ X_ERROR_REPORTER_DSN_INVALID: 'the error monitor DSN is malformed',
26
+ X_ERROR_RETRY_INVALID: 'error retry classification is unknown or already claimed',
27
+ // Core's own, and deliberately NOT `@ultimat3/http`'s `X_OVERLOADED`: that code is owned by a
28
+ // tier-2 package, and a tier-0 gate borrowing it upward is an import core may not make. The two
29
+ // read alike to an operator and the fix lines say which ceiling to widen.
30
+ X_FLIGHT_GATE_OVERLOADED: 'a concurrency gate is at its ceiling and its queue is full',
31
+ X_ID_INVALID: 'value is not a valid id',
32
+ X_IMAGE_DECODE_FAILED: 'image bytes are malformed, truncated or internally inconsistent',
33
+ X_IMAGE_TOO_LARGE: 'image exceeds the pipeline pixel ceiling',
34
+ X_IMAGE_UNSUPPORTED: 'the built-in image pipeline cannot read or write this format',
35
+ X_INTERNAL: 'unexpected internal framework error',
36
+ X_INVARIANT: 'invariant violated',
37
+ // Owned here rather than by `@ultimat3/time`, which declared it until 16.x: `@ultimat3/money`
38
+ // needs the same screen and tier 1 may not import sideways. One code, one declaration.
39
+ X_LOCALE_INVALID: 'not a well-formed BCP 47 tag',
40
+ X_METRIC_CARDINALITY:
41
+ 'a metric exceeded its series ceiling and is folding into one overflow series',
42
+ X_METRIC_NAME_INVALID:
43
+ 'metric name is malformed, or redeclared with a different kind, bounds or observer',
44
+ X_METRIC_VALUE_INVALID: 'metric value is not recordable',
45
+ X_NO_CONTEXT: 'no request context is active',
46
+ X_NOT_IMPLEMENTED: 'this driver does not implement the requested feature',
47
+ X_OTLP_ENDPOINT_INVALID: 'the OTLP collector endpoint is missing or malformed',
48
+ // Its own code rather than the endpoint's, because a title is what an agent reads first:
49
+ // `x errors explain X_OTLP_ENDPOINT_INVALID` would send it to inspect a variable that is fine.
50
+ X_OTLP_HEADERS_INVALID: 'OTEL_EXPORTER_OTLP_HEADERS is malformed',
51
+ X_OTLP_PROTOCOL_UNSUPPORTED: 'the OTLP protocol requested is not OTLP/HTTP JSON',
52
+ X_READINESS_CHECK_DUPLICATE: 'a readiness check name is registered twice',
53
+ X_REGISTRAR_CONFLICT: 'two different registrars are loaded for one primitive kind',
54
+ X_REGISTRAR_MISSING: 'no registrar is loaded for a primitive kind',
55
+ X_ROLE_INVALID: 'ROLE is not a known runtime role',
56
+ X_SERVICE_DUPLICATE: 'a service name is registered twice',
57
+ X_SERVICE_MISSING: 'service is not registered on the request context',
58
+ X_SHUTDOWN_TIMEOUT: 'graceful shutdown exceeded its deadline',
59
+ X_SUPERSEDED: 'a later generation superseded this work',
60
+ X_TELEMETRY_SAMPLER_ARG_INVALID: 'the trace sampling ratio is not a number between 0 and 1',
61
+ // Core's, though core does not throw it — the twin of `X_ABORTED`, and `@ultimat3/http` already
62
+ // calls it "borrowed (core's concept)" in `HTTP_BORROWED_ERROR_CODES`. A deadline that expired
63
+ // and a caller that went away are one pair of facts, so they are titled and classified in one
64
+ // place rather than by whichever package happened to raise one first.
65
+ X_TIMEOUT: 'operation exceeded its deadline',
66
+ X_UNREACHABLE: 'unreachable branch was reached',
67
+ } as const;
68
+
69
+ export type CoreErrorCode = keyof typeof CORE_CODE_TITLES;
70
+
71
+ export const CORE_ERROR_CODES: Readonly<Record<CoreErrorCode, ErrorCodeDescriptor>> = Object.freeze(
72
+ Object.fromEntries(
73
+ Object.entries(CORE_CODE_TITLES).map(([code, title]) => [code, descriptor({ title })]),
74
+ ) as Record<CoreErrorCode, ErrorCodeDescriptor>,
75
+ );
76
+
77
+ registerCoreErrorCodes(CORE_ERROR_CODES);
@@ -28,75 +28,26 @@ export interface ErrorCodeEntry extends ErrorCodeDescriptor {
28
28
  */
29
29
  export const ERROR_DOCS_URL = 'https://github.com/developerz-ai/ultimate/wiki/Error-Codes';
30
30
 
31
- /** Codes owned by `@ultimat3/core`. Every other package calls `registerErrorCodes()`. */
32
- const CORE_CODE_TITLES = {
33
- X_ABORTED: 'operation aborted',
34
- X_ASYNC_CONTEXT_UNAVAILABLE: 'async context unavailable',
35
- X_CONFIG_INVALID: 'app.config.ts is invalid',
36
- X_CURSOR_INVALID: 'pagination cursor is malformed, tampered with or from another query',
37
- X_CURSOR_SECRET_DEV: 'cursors are signed with the shipped development key',
38
- X_DRAINING: 'process is draining and refuses new work',
39
- X_ENV_EXAMPLE_DRIFT: '.env.example does not declare every variable the schema requires',
40
- X_ENV_MISSING: 'required environment variables are missing or invalid',
41
- X_ENVIRONMENT_INVALID: 'ULTIMATE_ENV is not a known environment',
42
- X_ERROR_CODE_DUPLICATE: 'error code registered twice',
43
- X_ERROR_REPORTER_DSN_INVALID: 'the error monitor DSN is malformed',
44
- X_ERROR_RETRY_INVALID: 'error retry classification is unknown or already claimed',
45
- // Core's own, and deliberately NOT `@ultimat3/http`'s `X_OVERLOADED`: that code is owned by a
46
- // tier-2 package, and a tier-0 gate borrowing it upward is an import core may not make. The two
47
- // read alike to an operator and the fix lines say which ceiling to widen.
48
- X_FLIGHT_GATE_OVERLOADED: 'a concurrency gate is at its ceiling and its queue is full',
49
- X_ID_INVALID: 'value is not a valid id',
50
- X_IMAGE_DECODE_FAILED: 'image bytes are malformed, truncated or internally inconsistent',
51
- X_IMAGE_TOO_LARGE: 'image exceeds the pipeline pixel ceiling',
52
- X_IMAGE_UNSUPPORTED: 'the built-in image pipeline cannot read or write this format',
53
- X_INTERNAL: 'unexpected internal framework error',
54
- X_INVARIANT: 'invariant violated',
55
- // Owned here rather than by `@ultimat3/time`, which declared it until 16.x: `@ultimat3/money`
56
- // needs the same screen and tier 1 may not import sideways. One code, one declaration.
57
- X_LOCALE_INVALID: 'not a well-formed BCP 47 tag',
58
- X_METRIC_CARDINALITY:
59
- 'a metric exceeded its series ceiling and is folding into one overflow series',
60
- X_METRIC_NAME_INVALID:
61
- 'metric name is malformed, or redeclared with a different kind, bounds or observer',
62
- X_METRIC_VALUE_INVALID: 'metric value is not recordable',
63
- X_NO_CONTEXT: 'no request context is active',
64
- X_NOT_IMPLEMENTED: 'this driver does not implement the requested feature',
65
- X_OTLP_ENDPOINT_INVALID: 'the OTLP collector endpoint is missing or malformed',
66
- // Its own code rather than the endpoint's, because a title is what an agent reads first:
67
- // `x errors explain X_OTLP_ENDPOINT_INVALID` would send it to inspect a variable that is fine.
68
- X_OTLP_HEADERS_INVALID: 'OTEL_EXPORTER_OTLP_HEADERS is malformed',
69
- X_OTLP_PROTOCOL_UNSUPPORTED: 'the OTLP protocol requested is not OTLP/HTTP JSON',
70
- X_READINESS_CHECK_DUPLICATE: 'a readiness check name is registered twice',
71
- X_REGISTRAR_CONFLICT: 'two different registrars are loaded for one primitive kind',
72
- X_REGISTRAR_MISSING: 'no registrar is loaded for a primitive kind',
73
- X_ROLE_INVALID: 'ROLE is not a known runtime role',
74
- X_SERVICE_DUPLICATE: 'a service name is registered twice',
75
- X_SERVICE_MISSING: 'service is not registered on the request context',
76
- X_SHUTDOWN_TIMEOUT: 'graceful shutdown exceeded its deadline',
77
- X_SUPERSEDED: 'a later generation superseded this work',
78
- X_TELEMETRY_SAMPLER_ARG_INVALID: 'the trace sampling ratio is not a number between 0 and 1',
79
- // Core's, though core does not throw it — the twin of `X_ABORTED`, and `@ultimat3/http` already
80
- // calls it "borrowed (core's concept)" in `HTTP_BORROWED_ERROR_CODES`. A deadline that expired
81
- // and a caller that went away are one pair of facts, so they are titled and classified in one
82
- // place rather than by whichever package happened to raise one first.
83
- X_TIMEOUT: 'operation exceeded its deadline',
84
- X_UNREACHABLE: 'unreachable branch was reached',
85
- } as const;
86
-
87
- export type CoreErrorCode = keyof typeof CORE_CODE_TITLES;
88
-
89
- function descriptor(declaration: ErrorCodeDeclaration): ErrorCodeDescriptor {
31
+ export function descriptor(declaration: ErrorCodeDeclaration): ErrorCodeDescriptor {
90
32
  return Object.freeze({ title: declaration.title, docs: declaration.docs ?? ERROR_DOCS_URL });
91
33
  }
92
34
 
93
- export const CORE_ERROR_CODES: Readonly<Record<CoreErrorCode, ErrorCodeDescriptor>> = Object.freeze(
94
- Object.fromEntries(
95
- Object.entries(CORE_CODE_TITLES).map(([code, title]) => [code, descriptor({ title })]),
96
- ) as Record<CoreErrorCode, ErrorCodeDescriptor>,
97
- );
35
+ /**
36
+ * Empty at load, on purpose: core's own titles live in `core-error-codes.ts`, which the barrel
37
+ * bare-imports as a side-effect anchor. A browser module that constructs an `UltimateError` from a
38
+ * light path pays for the class and this lookup, not a 40-row table — an untitled code renders
39
+ * through `humanize`, the trade `@ultimat3/realtime` already made for its own titles.
40
+ */
41
+ const registry = new Map<string, ErrorCodeDescriptor>();
98
42
 
99
- const registry = new Map<string, ErrorCodeDescriptor>(Object.entries(CORE_ERROR_CODES));
43
+ /** What `resetErrorCodes` restores: the codes `registerCoreErrorCodes` installed. */
44
+ const coreCodes = new Map<string, ErrorCodeDescriptor>();
45
+
46
+ /** `core-error-codes.ts`'s one call. Registered like any package's, and remembered for a reset. */
47
+ export function registerCoreErrorCodes(codes: Readonly<Record<string, ErrorCodeDescriptor>>): void {
48
+ registerErrorCodes(codes);
49
+ for (const [code, value] of Object.entries(codes)) coreCodes.set(code, value);
50
+ }
100
51
 
101
52
  /**
102
53
  * Register a package's codes. Throws `X_ERROR_CODE_DUPLICATE` on collision so two packages
@@ -145,7 +96,7 @@ export function listErrorCodes(): readonly ErrorCodeEntry[] {
145
96
  /** Test-only: drop everything a package registered, keeping core's codes. */
146
97
  export function resetErrorCodes(): void {
147
98
  registry.clear();
148
- for (const [code, value] of Object.entries(CORE_ERROR_CODES)) registry.set(code, value);
99
+ for (const [code, value] of coreCodes) registry.set(code, value);
149
100
  }
150
101
 
151
102
  /**
@@ -58,6 +58,9 @@ const CORE_ERROR_RETRY: ReadonlyMap<string, ErrorRetry> = new Map(
58
58
  // re-run produces the same refusal by construction, so an UNCLASSIFIED reading would spend a
59
59
  // job's whole retry policy proving that the world has still moved on.
60
60
  X_SUPERSEDED: 'terminal',
61
+ // The principal fence's twin of `X_SUPERSEDED`, listed for the same reason: re-sending a read
62
+ // from the previous principal's scope is refused identically every time.
63
+ X_CLIENT_SCOPE_CHANGED: 'terminal',
61
64
  } as const),
62
65
  );
63
66
 
@@ -3,14 +3,14 @@
3
3
  // built with, and the retry classification a code carries. One group because a code, its title,
4
4
  // its rendering and its retry class are one contract; `index.ts` re-exports every name explicitly.
5
5
 
6
+ export type { CoreErrorCode } from '../core-error-codes';
7
+ export { CORE_ERROR_CODES } from '../core-error-codes';
6
8
  export type {
7
- CoreErrorCode,
8
9
  ErrorCodeDeclaration,
9
10
  ErrorCodeDescriptor,
10
11
  ErrorCodeEntry,
11
12
  } from '../error-codes';
12
13
  export {
13
- CORE_ERROR_CODES,
14
14
  describeErrorCode,
15
15
  ERROR_DOCS_URL,
16
16
  errorCodeSnapshot,
@@ -51,7 +51,14 @@ function superseded(subject: string, issued: number, current: number): UltimateE
51
51
  });
52
52
  }
53
53
 
54
- /** Whether a caught value is this refusal. The one reader a caller needs; never `error.code`. */
54
+ /**
55
+ * Whether a caught value is a supersession — this fence's refusal, or `X_CLIENT_SCOPE_CHANGED`,
56
+ * the principal fence's (`client-scope.ts`). One reader for both, because a caller does the same
57
+ * thing with either: drop the answer and render nothing. Never `error.code`.
58
+ */
55
59
  export function isSuperseded(error: unknown): boolean {
56
- return isUltimateError(error) && error.code === 'X_SUPERSEDED';
60
+ return (
61
+ isUltimateError(error) &&
62
+ (error.code === 'X_SUPERSEDED' || error.code === 'X_CLIENT_SCOPE_CHANGED')
63
+ );
57
64
  }
package/src/index.ts CHANGED
@@ -14,6 +14,7 @@
14
14
  // are declared side-effecting too and are deliberately NOT anchored: each is reached by whatever
15
15
  // uses it, and anchoring `context.ts` alone measured +3,485 B on a browser chunk for a provider a
16
16
  // browser can never fire.
17
+ import './core-error-codes';
17
18
  import './schema-error-codes';
18
19
 
19
20
  export type {
@@ -43,10 +44,14 @@ export {
43
44
  export { APP_VERSION_KEY, appVersion, DEFAULT_APP_VERSION } from './app-version';
44
45
  export { assert, assertNever, type InvariantOptions, invariant } from './assert';
45
46
  export { type AsyncContext, asyncContext } from './async-context';
47
+ /** The four shapes an async region can be in — produced by `realtime`, rendered by `ui`. */
48
+ export type { AsyncState } from './async-state';
46
49
  export type { BackoffCurve, BackoffOptions, JitterMode, Random } from './backoff';
47
50
  export { backoffDelay } from './backoff';
48
51
  export { CACHE_TIERS, type CacheTierName } from './cache-vocabulary';
49
52
  export { canonicalJson, fingerprint } from './canonical-json';
53
+ export type { FetchLike, TransportRequest } from './client-dispatch';
54
+ export { IDEMPOTENCY_HEADER } from './client-dispatch';
50
55
  /**
51
56
  * Flight control for a typed client, and OPT-IN by construction: `@ultimat3/action`'s and
52
57
  * `@ultimat3/query`'s `client.ts` each name `ClientFlight` as a TYPE only, so a caller that never
@@ -61,6 +66,23 @@ export type {
61
66
  FlightPlan,
62
67
  } from './client-flight';
63
68
  export { createClientFlight, DEFAULT_CLIENT_RETRY, isTransientFailure } from './client-flight';
69
+ export type { ActionRoute } from './client-paths';
70
+ export {
71
+ actionPath,
72
+ actionRoute,
73
+ pluralize,
74
+ QUERY_PATH_PREFIX,
75
+ queryPath,
76
+ splitWords,
77
+ } from './client-paths';
78
+ export type { TransportFailure } from './client-problem';
79
+ /**
80
+ * The browser seam (plan 101): ONE HTTP function, the records envelope it decodes, the per-tab
81
+ * page handle records land in, and the principal fence every client layer subscribes to.
82
+ */
83
+ export type { ClientScope } from './client-scope';
84
+ export { onRescope, rescope } from './client-scope';
85
+ export { clientTransport } from './client-transport';
64
86
  /** What a typed client puts on the wire. `retryForStatus` is what fills a failure's `retry`. */
65
87
  export type { WireAnswer } from './client-wire';
66
88
  export { FRAMEWORK_CODE, problemOf, retryForStatus, traceHeaders } from './client-wire';
@@ -86,6 +108,8 @@ export type {
86
108
  export { defineConfig, INBOX_RETENTION_KEYS } from './config';
87
109
  export type { PwaColors, PwaConfig, PwaOfflineConfig, PwaSchemeColors } from './config-pwa';
88
110
  export { PWA_COLOR_KEYS, PWA_SCHEMES } from './config-pwa';
111
+ export type { ConflictPolicy, ResolveConflictOptions, Row } from './conflict-policy';
112
+ export { resolveConflict } from './conflict-policy';
89
113
  export type { Ctx, CtxFacts, CtxInit, CtxPatch, CtxServices, ServiceBag } from './context';
90
114
  export {
91
115
  createContext,
@@ -521,7 +545,24 @@ export type { Direction } from './locale-direction';
521
545
  export { directionOf, isRtl } from './locale-direction';
522
546
  export { isMcpExposed, type McpExposureDeclaration } from './mcp-exposure';
523
547
  export { nearestName } from './nearest-name';
548
+ /** The message pwa's `sw.js` posts and realtime's outbox listens for. */
549
+ export { OUTBOX_DRAIN_MESSAGE, type OutboxDrainMessage } from './outbox-drain';
550
+ /** The `<meta name>`s render writes and the page client, realtime and pwa read. */
551
+ export {
552
+ APP_UPDATE_MESSAGE,
553
+ CLIENT_BUILD_META,
554
+ CLIENT_PERSIST_META,
555
+ CLIENT_SCOPE_HEADER,
556
+ CLIENT_SCOPE_META,
557
+ CLIENT_SYNC_META,
558
+ CLIENT_SYNC_WORKER_META,
559
+ } from './page-meta';
524
560
  export { type CappedBody, readWithinLimit } from './read-capped';
561
+ export type { RecordEnvelope, RecordRows } from './record-envelope';
562
+ export { decodeRecordEnvelope, encodeRecordEnvelope, RECORDS_HEADER } from './record-envelope';
563
+ export { RECORDS_OPENAPI_HEADER, recordEnvelopeSchema } from './record-envelope-openapi';
564
+ export type { PageClient, RecordSink } from './record-sink';
565
+ export { pageClient } from './record-sink';
525
566
  export type {
526
567
  ModuleRegistrar,
527
568
  PrimitiveFactory,
@@ -551,7 +592,13 @@ export { DEFAULT_ROLE, isRole, ROLE_INFO, ROLES, resolveRole } from './roles';
551
592
  export type { HydrateStrategy, OfflineStrategy, RenderMode } from './route-vocabulary';
552
593
  export { HYDRATE_STRATEGIES, OFFLINE_STRATEGIES, RENDER_MODES } from './route-vocabulary';
553
594
  export { safeUrl, URL_ATTRIBUTES } from './safe-url';
554
- export { defineService, resetServices, type ServiceFactory } from './service';
595
+ export {
596
+ defineService,
597
+ installedServices,
598
+ registeredServiceNames,
599
+ resetServices,
600
+ type ServiceFactory,
601
+ } from './service';
555
602
  export type { FlightJoin, Scheduler, SingleFlight, SingleFlightOptions } from './single-flight';
556
603
  export { createSingleFlight } from './single-flight';
557
604
  export { endOfLiteral, maskLiterals, QUOTES, stripComments } from './source-mask';
@@ -584,3 +631,9 @@ export {
584
631
  webhookSignature,
585
632
  webhookSigningString,
586
633
  } from './webhook-signature';
634
+ /**
635
+ * A write's public name — the digest of its idempotency key — and the server scope that carries it
636
+ * from `@ultimat3/action`'s HTTP projection to the layers that stamp it on a `records` frame.
637
+ */
638
+ export { isWriteDigest, WRITE_DIGEST_LENGTH, writeDigest } from './write-digest';
639
+ export { currentWriteOrigin, WRITE_ORIGIN_WAL_PREFIX, withWriteOrigin } from './write-origin';
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The trace and request-budget headers a SERVER-side typed call carries onward, installed into the
3
+ * transport's slot by the code that makes them meaningful — `runWithContext` and `startSpan` — and
4
+ * never at import. A browser runs neither, so its slot stays empty and `clientTransport` carries
5
+ * zero bytes of telemetry, context or logger: that graph was 12.9 kB of every island that called
6
+ * `rpc()` or `queryClient()`, for a header a browser never has a value for.
7
+ */
8
+
9
+ import { traceHeaders } from './client-wire';
10
+ import { outboundSlot } from './record-sink';
11
+
12
+ /** Idempotent: the first call fills the slot, every later one is a property read. */
13
+ export function installTraceHeaders(): void {
14
+ const slot = outboundSlot();
15
+ if (slot.outboundHeaders === undefined) slot.outboundHeaders = traceHeaders;
16
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The one message a service worker posts to its window clients when the platform says the
3
+ * connection is back: "drain your outbox now". Tier 0 because the SENDER is `@ultimat3/pwa`'s
4
+ * emitted `sw.js` (tier 4) and the LISTENER is `@ultimat3/realtime`'s outbox (tier 3) — one literal,
5
+ * one home, never a string each side retypes.
6
+ */
7
+
8
+ /** `event.data.type` of the drain message. */
9
+ export const OUTBOX_DRAIN_MESSAGE = 'x-outbox-drain';
10
+
11
+ /** The whole message. It carries nothing else: the outbox lives in the page, not in the worker. */
12
+ export interface OutboxDrainMessage {
13
+ readonly type: typeof OUTBOX_DRAIN_MESSAGE;
14
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The `<meta name>`s a rendered document hands its page client. Tier 0 because the WRITER is
3
+ * `@ultimat3/render` (tier 4) and the READERS are core's `pageClient()`, `@ultimat3/realtime`'s
4
+ * socket host (tier 3) and `@ultimat3/pwa`'s skew check — one literal per fact, never retyped.
5
+ */
6
+
7
+ /** The page's principal: content = that principal, empty = anonymous, absent = unscoped. */
8
+ export const CLIENT_SCOPE_META = 'ultimate-scope';
9
+
10
+ /**
11
+ * The same scope on the RESPONSE of a private document, so a service worker — which never parses
12
+ * HTML — can partition its offline pages by principal. Present only where the meta is; its value
13
+ * is the meta's (`''` for the anonymous visitor).
14
+ */
15
+ export const CLIENT_SCOPE_HEADER = 'x-ultimate-scope';
16
+
17
+ /**
18
+ * The build id the document was rendered by. `x-ultimate-build` — the same spelling as the header,
19
+ * and the one `@ultimat3/pwa` shipped as `BUILD_ID_META`, so no shipped reader changes.
20
+ */
21
+ export const CLIENT_BUILD_META = 'x-ultimate-build';
22
+
23
+ /**
24
+ * `event.data.type` of the message a service worker posts to its windows when a newer build is
25
+ * waiting (`{ type, to: <build id> }`). Sent by `@ultimat3/pwa`'s emitted `sw.js`, read by an app's
26
+ * update banner — one literal both import, beside the build id it is about.
27
+ */
28
+ export const APP_UPDATE_MESSAGE = 'AppUpdateAvailable';
29
+
30
+ /** Where the page's one socket dials: `/_x/sync`, or the deployment's absolute `SYNC_URL`. */
31
+ export const CLIENT_SYNC_META = 'ultimate-sync';
32
+
33
+ /** The worker script that hosts the socket; absent when the app has no realtime. */
34
+ export const CLIENT_SYNC_WORKER_META = 'ultimate-sync-worker';
35
+
36
+ /**
37
+ * The record types the client keeps on disk — the entities registered with `persist: true`,
38
+ * comma-separated (`post,comment`). The browser holds no entity declarations, so this is the one
39
+ * place it learns which types `@ultimat3/realtime`'s persister may write. Absent = none.
40
+ */
41
+ export const CLIENT_PERSIST_META = 'ultimate-persist';
package/src/page.ts ADDED
@@ -0,0 +1,52 @@
1
+ /**
2
+ * `@ultimat3/core/page` — the ONE light entry for browser code: the page handle, the principal
3
+ * fence, the names a document, a service worker and an island agree on, and `UltimateError` with
4
+ * the render helper a refusal's `fix:` needs. What it never carries is a titles TABLE: any barrel
5
+ * import retains the anchored `core-error-codes.ts` and `schema-error-codes.ts`, and a browser
6
+ * that loaded no table titles a code from its name. The barrel re-exports every name here;
7
+ * `page-bundle.test.ts` fails if this entry ever grows a table back.
8
+ */
9
+
10
+ // The client seam's own functions, for the hooks a browser island calls: the one transport, the
11
+ // URL rule, the principal-supersession reader, and the small helpers realtime's browser half uses.
12
+ // Each reaches no titles table — `page-bundle.test.ts` builds all of them together to prove it.
13
+ export { invariant } from './assert';
14
+ export type { AsyncState } from './async-state';
15
+ export type { JitterMode, Random } from './backoff';
16
+ export { backoffDelay } from './backoff';
17
+ export type { FetchLike, TransportRequest } from './client-dispatch';
18
+ export { IDEMPOTENCY_HEADER } from './client-dispatch';
19
+ export { actionPath, queryPath, splitWords } from './client-paths';
20
+ export type { ClientScope } from './client-scope';
21
+ export { onRescope, rescope } from './client-scope';
22
+ export { clientTransport } from './client-transport';
23
+ export { type Clock, systemClock } from './clock';
24
+ export type { ConflictPolicy, Row } from './conflict-policy';
25
+ // Registry-free: it merges two rows and throws nothing, so the page's record store settles a
26
+ // write under a conflict policy without loading the error table.
27
+ export { resolveConflict } from './conflict-policy';
28
+ export { renderFixShellArg, renderThrowable, stringField } from './error-render';
29
+ export { classifyThrown } from './error-retry';
30
+ export type { UltimateErrorInit } from './errors';
31
+ export { isUltimateError, UltimateError } from './errors';
32
+ export { finiteCount, finiteOption } from './finite-option';
33
+ export { isSuperseded } from './generation-fence';
34
+ export { uuid } from './ids';
35
+ export { isJsonObject } from './json-object';
36
+ export type { OutboxDrainMessage } from './outbox-drain';
37
+ export { OUTBOX_DRAIN_MESSAGE } from './outbox-drain';
38
+ export {
39
+ APP_UPDATE_MESSAGE,
40
+ CLIENT_BUILD_META,
41
+ CLIENT_PERSIST_META,
42
+ CLIENT_SCOPE_HEADER,
43
+ CLIENT_SCOPE_META,
44
+ CLIENT_SYNC_META,
45
+ CLIENT_SYNC_WORKER_META,
46
+ } from './page-meta';
47
+ export type { RecordEnvelope, RecordRows } from './record-envelope';
48
+ export { decodeRecordEnvelope, RECORDS_HEADER } from './record-envelope';
49
+ export type { PageClient, RecordSink } from './record-sink';
50
+ export { pageClient } from './record-sink';
51
+ // A write's public name, so the page's store can recognise the `records` frame its own write made.
52
+ export { isWriteDigest, WRITE_DIGEST_LENGTH, writeDigest } from './write-digest';