@jarenjs/contract 0.43.1

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.
Files changed (84) hide show
  1. package/README.md +508 -0
  2. package/dist/types/adapters/fetch.d.ts +27 -0
  3. package/dist/types/adapters/node.d.ts +47 -0
  4. package/dist/types/app/binding.d.ts +122 -0
  5. package/dist/types/app/effect.d.ts +77 -0
  6. package/dist/types/app/index.d.ts +31 -0
  7. package/dist/types/app/subscription.d.ts +82 -0
  8. package/dist/types/bundle.d.ts +43 -0
  9. package/dist/types/cli.d.ts +15 -0
  10. package/dist/types/client/http.d.ts +242 -0
  11. package/dist/types/client/outcome.d.ts +289 -0
  12. package/dist/types/compat.d.ts +36 -0
  13. package/dist/types/compile.d.ts +196 -0
  14. package/dist/types/describe.d.ts +115 -0
  15. package/dist/types/diff.d.ts +91 -0
  16. package/dist/types/errors.d.ts +205 -0
  17. package/dist/types/http/dispatch.d.ts +148 -0
  18. package/dist/types/http/serve.d.ts +154 -0
  19. package/dist/types/http/wire.d.ts +334 -0
  20. package/dist/types/index.d.ts +39 -0
  21. package/dist/types/ledger.d.ts +207 -0
  22. package/dist/types/local/index.d.ts +127 -0
  23. package/dist/types/messages.d.ts +63 -0
  24. package/dist/types/path.d.ts +119 -0
  25. package/dist/types/pipeline.d.ts +157 -0
  26. package/dist/types/port/client.d.ts +142 -0
  27. package/dist/types/port/frame.d.ts +195 -0
  28. package/dist/types/port/serve.d.ts +102 -0
  29. package/dist/types/project/index.d.ts +34 -0
  30. package/dist/types/project/markdown.d.ts +28 -0
  31. package/dist/types/project/openapi.d.ts +102 -0
  32. package/dist/types/project/tools.d.ts +57 -0
  33. package/dist/types/project/typescript.d.ts +59 -0
  34. package/dist/types/public.d.ts +73 -0
  35. package/dist/types/revision.d.ts +36 -0
  36. package/dist/types/stream/client.d.ts +104 -0
  37. package/dist/types/stream/server.d.ts +106 -0
  38. package/dist/types/stream/sse.d.ts +62 -0
  39. package/docs/APP-INTEGRATION.md +301 -0
  40. package/docs/CONTRACT-FORMAT.md +1923 -0
  41. package/package.json +110 -0
  42. package/schemas/jaren-contract-port.draft-07.schema.json +241 -0
  43. package/schemas/jaren-contract-port.schema.json +241 -0
  44. package/schemas/jaren-contract.draft-07.schema.json +287 -0
  45. package/schemas/jaren-contract.schema.json +287 -0
  46. package/src/adapters/fetch.js +109 -0
  47. package/src/adapters/node.js +238 -0
  48. package/src/app/binding.js +426 -0
  49. package/src/app/effect.js +190 -0
  50. package/src/app/index.js +26 -0
  51. package/src/app/subscription.js +130 -0
  52. package/src/bundle.js +168 -0
  53. package/src/cli.js +264 -0
  54. package/src/client/http.js +1150 -0
  55. package/src/client/outcome.js +364 -0
  56. package/src/compat.js +62 -0
  57. package/src/compile.js +1162 -0
  58. package/src/describe.js +109 -0
  59. package/src/diff.js +610 -0
  60. package/src/errors.js +236 -0
  61. package/src/http/dispatch.js +1054 -0
  62. package/src/http/serve.js +301 -0
  63. package/src/http/wire.js +469 -0
  64. package/src/index.js +33 -0
  65. package/src/ledger.js +225 -0
  66. package/src/local/index.js +363 -0
  67. package/src/messages.js +68 -0
  68. package/src/path.js +471 -0
  69. package/src/pipeline.js +241 -0
  70. package/src/port/client.js +518 -0
  71. package/src/port/frame.js +196 -0
  72. package/src/port/serve.js +442 -0
  73. package/src/project/index.js +29 -0
  74. package/src/project/markdown.js +244 -0
  75. package/src/project/openapi.js +564 -0
  76. package/src/project/openapi.jslt.json +149 -0
  77. package/src/project/tools.js +139 -0
  78. package/src/project/typescript.js +152 -0
  79. package/src/project/typescript.jtlt.json +72 -0
  80. package/src/public.js +206 -0
  81. package/src/revision.js +90 -0
  82. package/src/stream/client.js +212 -0
  83. package/src/stream/server.js +306 -0
  84. package/src/stream/sse.js +67 -0
package/src/public.js ADDED
@@ -0,0 +1,206 @@
1
+ //@ts-check
2
+ /**
3
+ * @file `publicProjection`: the browser-safe subset of a compiled contract
4
+ * as a JSON document that is ITSELF a valid `$contract` 0.1 — what a
5
+ * client needs and no more, what the revision hashes, and what every other
6
+ * projection (OpenAPI, TypeScript, Markdown, tools) is built on.
7
+ *
8
+ * Retained: the operations whose `policy.audience` is not `server` (and,
9
+ * when the caller narrows with `ops`, only those), each with its resolved
10
+ * binding and policy minus the server-side knobs (`limits`,
11
+ * `errors.details`), plus the `$defs` the retained schemas reach. Member
12
+ * order is FIXED (docs/CONTRACT-FORMAT.md §12.1 — normative, because the
13
+ * revision is the SHA-256 of the canonical bytes of this document): root
14
+ * `$contract, id, version, compat, $defs, operations`; operation `kind,
15
+ * input, output, errors, policy, http, doc`; policy `task, idempotency,
16
+ * revision, cache, retry, stream, audience`; http `method, path, in, body, status,
17
+ * media`; error `status, schema`; operations in document order; `$defs` in
18
+ * first-reference order. Defaults are materialized (the projection says
19
+ * what the binding DOES, not what the author typed), so two documents
20
+ * that behave alike project alike, and the projection of a projection is
21
+ * the projection.
22
+ */
23
+
24
+ import { isJsonObject, setObjectMember } from '@jarenjs/core/object';
25
+
26
+ import { ContractHostError } from './errors.js';
27
+ import { reachableDefs } from './bundle.js';
28
+
29
+ /**
30
+ * @typedef {import('./compile.js').Contract} Contract
31
+ * @typedef {import('./compile.js').CompiledOperation} CompiledOperation
32
+ */
33
+
34
+ /**
35
+ * @typedef {Object} PublicProjectionOptions
36
+ * @property {readonly string[]} [ops] - the operations to keep, a subset
37
+ * of the contract's ids (default: all); `server` operations are never
38
+ * kept, listed or not
39
+ */
40
+
41
+ /**
42
+ * Whether `value` is a compiled contract — reads the members every
43
+ * projection needs.
44
+ * @param {unknown} value
45
+ * @returns {value is Contract}
46
+ */
47
+ export function isCompiledContract(value) {
48
+ if (value === null || typeof value !== 'object') return false;
49
+ const c = /** @type {any} */ (value);
50
+ return isJsonObject(c.doc) && Array.isArray(c.ids) && c.operations !== null && typeof c.operations === 'object'
51
+ && typeof c.match === 'function';
52
+ }
53
+
54
+ /**
55
+ * The operations a projection retains, in document order: the public
56
+ * ones, narrowed by `ops` when given. Throws `JC1008` for a malformed
57
+ * `contract` or an `ops` entry that names no operation.
58
+ * @param {Contract} contract
59
+ * @param {readonly string[] | undefined} ops
60
+ * @param {string} who - the projection's name, for the message
61
+ * @returns {CompiledOperation[]}
62
+ */
63
+ export function retainedOperations(contract, ops, who) {
64
+ if (!isCompiledContract(contract)) {
65
+ throw new ContractHostError('JC1008', `${who}: contract must be a compiled contract (compileContract(doc))`);
66
+ }
67
+ let wanted = null;
68
+ if (ops !== undefined) {
69
+ if (!Array.isArray(ops)) throw new ContractHostError('JC1008', `${who}: ops must be an array of operation ids`);
70
+ wanted = new Set();
71
+ for (let i = 0; i < ops.length; i++) {
72
+ const id = ops[i];
73
+ if (typeof id !== 'string' || !Object.hasOwn(contract.operations, id)) {
74
+ throw new ContractHostError('JC1008', `${who}: ops[${i}] names no operation of the contract (${typeof id === 'string' ? id : typeof id})`);
75
+ }
76
+ wanted.add(id);
77
+ }
78
+ }
79
+ const out = [];
80
+ for (let i = 0; i < contract.ids.length; i++) {
81
+ const op = contract.operations[contract.ids[i]];
82
+ if (op.policy.audience === 'server') continue;
83
+ if (wanted !== null && !wanted.has(op.id)) continue;
84
+ out.push(op);
85
+ }
86
+ return out;
87
+ }
88
+
89
+ /**
90
+ * The public policy of an operation: the client-facing members in the
91
+ * fixed order, defaults materialized, the server-side knobs (`limits`,
92
+ * `errors.details`) left out.
93
+ * @param {CompiledOperation} op
94
+ * @returns {Record<string, unknown>}
95
+ */
96
+ function publicPolicy(op) {
97
+ const p = op.policy;
98
+ /** @type {Record<string, unknown>} */
99
+ const policy = { task: p.task, idempotency: p.idempotency };
100
+ if (p.revision !== null) policy.revision = p.revision;
101
+ policy.cache = p.cache;
102
+ if (p.retry !== null) policy.retry = { max: p.retry.max, on: p.retry.on.slice() };
103
+ if (p.stream !== null) {
104
+ policy.stream = p.stream.maxPatchBytes === null
105
+ ? { resume: p.stream.resume, heartbeatMs: p.stream.heartbeatMs }
106
+ : { resume: p.stream.resume, heartbeatMs: p.stream.heartbeatMs, maxPatchBytes: p.stream.maxPatchBytes };
107
+ }
108
+ policy.audience = p.audience;
109
+ return policy;
110
+ }
111
+
112
+ /**
113
+ * The public binding of an operation: method, canonical path, every
114
+ * member's location, the whole-body member when declared, status, media.
115
+ * @param {CompiledOperation} op
116
+ * @returns {Record<string, unknown>}
117
+ */
118
+ function publicHttp(op) {
119
+ const h = op.http;
120
+ /** @type {Record<string, unknown>} */
121
+ const http = { method: h.method, path: h.path };
122
+ /** @type {Record<string, string>} */
123
+ const locations = {};
124
+ const members = Object.keys(h.in);
125
+ for (let i = 0; i < members.length; i++) setObjectMember(locations, members[i], h.in[members[i]]);
126
+ http.in = locations;
127
+ if (h.body !== null) http.body = h.body;
128
+ http.status = h.status;
129
+ http.media = h.media;
130
+ return http;
131
+ }
132
+
133
+ /**
134
+ * Project one operation.
135
+ * @param {CompiledOperation} op
136
+ * @returns {Record<string, unknown>}
137
+ */
138
+ function publicOperation(op) {
139
+ /** @type {Record<string, unknown>} */
140
+ const out = { kind: op.kind };
141
+ if (op.input !== null) out.input = op.input.schema;
142
+ out.output = op.output.schema;
143
+ const codes = Object.keys(op.errors);
144
+ if (codes.length > 0) {
145
+ /** @type {Record<string, unknown>} */
146
+ const errors = {};
147
+ for (let i = 0; i < codes.length; i++) {
148
+ const decl = op.errors[codes[i]];
149
+ /** @type {Record<string, unknown>} */
150
+ const e = { status: decl.status };
151
+ if (decl.schema !== null) e.schema = decl.schema;
152
+ setObjectMember(errors, codes[i], e);
153
+ }
154
+ out.errors = errors;
155
+ }
156
+ out.policy = publicPolicy(op);
157
+ out.http = publicHttp(op);
158
+ if (op.doc !== null) out.doc = op.doc;
159
+ return out;
160
+ }
161
+
162
+ /**
163
+ * The public projection of a compiled contract: a `$contract` 0.1
164
+ * document (it compiles) holding the public operations with their
165
+ * resolved bindings and policies and the `$defs` they reach, in the fixed
166
+ * member order the revision hashes. Schema subtrees are the contract's own
167
+ * (frozen); the composition is fresh.
168
+ * @param {Contract} contract
169
+ * @param {PublicProjectionOptions} [options]
170
+ * @returns {Record<string, unknown>}
171
+ * @throws {ContractHostError} `JC1008` — not a compiled contract, or `ops` names no operation
172
+ * @example
173
+ * const pub = publicProjection(contract);
174
+ * compileContract(pub).ids; // the public operations, in document order
175
+ */
176
+ export function publicProjection(contract, options = {}) {
177
+ const ops = retainedOperations(contract, options.ops, 'publicProjection');
178
+ /** @type {Record<string, unknown>} */
179
+ const out = { $contract: '0.1' };
180
+ if (contract.id !== null) out.id = contract.id;
181
+ if (contract.version !== null) out.version = contract.version;
182
+ if (contract.compat.length > 0) out.compat = contract.compat.slice();
183
+ /** @type {unknown[]} */
184
+ const roots = [];
185
+ for (let i = 0; i < ops.length; i++) {
186
+ const op = ops[i];
187
+ if (op.input !== null) roots.push(op.input.schema);
188
+ roots.push(op.output.schema);
189
+ const codes = Object.keys(op.errors);
190
+ for (let j = 0; j < codes.length; j++) {
191
+ if (op.errors[codes[j]].schema !== null) roots.push(op.errors[codes[j]].schema);
192
+ }
193
+ }
194
+ const names = reachableDefs(roots, contract.doc);
195
+ if (names.length > 0) {
196
+ /** @type {Record<string, unknown>} */
197
+ const defs = {};
198
+ for (let i = 0; i < names.length; i++) setObjectMember(defs, names[i], contract.doc.$defs[names[i]]);
199
+ out.$defs = defs;
200
+ }
201
+ /** @type {Record<string, unknown>} */
202
+ const operations = {};
203
+ for (let i = 0; i < ops.length; i++) setObjectMember(operations, ops[i].id, publicOperation(ops[i]));
204
+ out.operations = operations;
205
+ return out;
206
+ }
@@ -0,0 +1,90 @@
1
+ //@ts-check
2
+ /**
3
+ * @file The contract revision: the lowercase hex SHA-256 over the RFC
4
+ * 8785 canonical bytes of the public projection (docs/CONTRACT-FORMAT.md
5
+ * §14) — the compatibility identity two independent processes agree on.
6
+ * It hashes the PUBLIC projection, never the source document, so a
7
+ * server-audience operation, a `policy.limits` value or an error-detail
8
+ * level can change without moving the revision, while any member a
9
+ * client can observe moves it. Memoized per compiled contract in a
10
+ * private `WeakMap`, so `compileContract` stays synchronous and the
11
+ * digest is computed at most once per process; `peekRevision` is the
12
+ * synchronous read `describe()` uses (`null` until the promise settled).
13
+ */
14
+
15
+ import { canonicalSha256 } from '@jarenjs/json/canonical';
16
+
17
+ import { ContractCompileError } from './errors.js';
18
+ import { publicProjection } from './public.js';
19
+
20
+ /**
21
+ * @typedef {import('./compile.js').Contract} Contract
22
+ */
23
+
24
+ /**
25
+ * The memo: contract → `{ promise, value }`. `value` stays `null` until
26
+ * the digest resolved; a rejected computation is NOT memoized, so a
27
+ * transient host failure (`crypto.subtle` unavailable) can be retried,
28
+ * while the deterministic refusal (`JC0061`) simply recurs.
29
+ * @type {WeakMap<object, { promise: Promise<string>, value: string | null }>}
30
+ */
31
+ const revisions = new WeakMap();
32
+
33
+ /**
34
+ * Compute (once) the revision of a compiled contract: SHA-256 over the
35
+ * canonical bytes of `publicProjection(contract)`, as 64 lowercase hex
36
+ * characters. A projection that is not canonicalizable — a string member
37
+ * carrying an unpaired surrogate, say — rejects with `JC0061`
38
+ * (`ContractCompileError`, its `docPath` the offending value's pointer
39
+ * INTO THE PROJECTION).
40
+ * @param {Contract} contract
41
+ * @returns {Promise<string>}
42
+ * @example
43
+ * const contract = compileContract(doc);
44
+ * await contract.revision(); // 'e3b0c442…' — stable across compiles of equal documents
45
+ */
46
+ export function contractRevision(contract) {
47
+ const memo = revisions.get(contract);
48
+ if (memo !== undefined) return memo.promise;
49
+ /** @type {{ promise: Promise<string>, value: string | null }} */
50
+ const record = { promise: /** @type {any} */ (null), value: null };
51
+ record.promise = digest(contract, record);
52
+ revisions.set(contract, record);
53
+ return record.promise;
54
+ }
55
+
56
+ /**
57
+ * @param {Contract} contract
58
+ * @param {{ promise: Promise<string>, value: string | null }} record
59
+ * @returns {Promise<string>}
60
+ */
61
+ async function digest(contract, record) {
62
+ let hex;
63
+ try {
64
+ hex = await canonicalSha256(publicProjection(contract));
65
+ }
66
+ catch (err) {
67
+ revisions.delete(contract);
68
+ if (err !== null && typeof err === 'object' && /** @type {any} */ (err).name === 'JsonCanonicalizeError') {
69
+ throw new ContractCompileError('JC0061',
70
+ `the public projection is not canonicalizable: ${/** @type {Error} */ (err).message}`,
71
+ /** @type {any} */ (err).dataPath, /** @type {Error} */ (err));
72
+ }
73
+ throw err;
74
+ }
75
+ record.value = hex;
76
+ return hex;
77
+ }
78
+
79
+ /**
80
+ * The revision of a compiled contract if it has been computed, else
81
+ * `null` — the synchronous read `describe()` renders, so a description
82
+ * taken before anyone awaited `revision()` honestly says "not computed"
83
+ * rather than blocking.
84
+ * @param {Contract} contract
85
+ * @returns {string | null}
86
+ */
87
+ export function peekRevision(contract) {
88
+ const memo = revisions.get(contract);
89
+ return memo === undefined ? null : memo.value;
90
+ }
@@ -0,0 +1,212 @@
1
+ //@ts-check
2
+ /**
3
+ * @file The client half of the stream binding, carrier-neutral
4
+ * (docs/CONTRACT-FORMAT.md §19): one consumer state machine that both
5
+ * `client.subscribe` implementations feed — the HTTP client with
6
+ * decoded SSE events, the port client with push frames. It validates
7
+ * every snapshot against the operation's output schema, enforces the
8
+ * strictly-increasing seq (`JC2092`), classifies a server `error` event
9
+ * (a declared code is a `failure` outcome under its own code; anything
10
+ * else is `contract` `JC2093` with the server's record in `details`),
11
+ * and delivers each `on*` callback totally — a callback that throws
12
+ * never breaks the machine. After the first terminal event (`error`,
13
+ * `end`, a local failure) the machine is finished: the carrier's
14
+ * `finish` hook has run and every later event is dropped.
15
+ */
16
+
17
+ import { renderMessage, verdict, projectValidationDetails } from '../http/wire.js';
18
+ import { failedOutcome, outcomeError, clientError } from '../client/outcome.js';
19
+ import { STREAM_ERRORS } from './sse.js';
20
+
21
+ /**
22
+ * @typedef {import('../client/outcome.js').OutcomeRoute} OutcomeRoute
23
+ * @typedef {import('../client/outcome.js').OutcomeMeta} OutcomeMeta
24
+ * @typedef {import('../client/outcome.js').Outcome} Outcome
25
+ * @typedef {import('../http/wire.js').Catalog} Catalog
26
+ */
27
+
28
+ /**
29
+ * The callbacks of one `client.subscribe` call; every one optional.
30
+ * @typedef {Object} StreamCallbacks
31
+ * @property {(value: unknown, info: { seq: number, resumed: boolean }) => void} [onSnapshot]
32
+ * @property {(emission: { patch: unknown[], seq: number }) => void} [onPatch]
33
+ * @property {(outcome: Outcome) => void} [onError]
34
+ * @property {(info: { reason: string }) => void} [onEnd]
35
+ */
36
+
37
+ /**
38
+ * @typedef {Object} StreamConsumerOptions
39
+ * @property {OutcomeRoute & { details: 'none' | 'paths' | 'full' }} route
40
+ * @property {Catalog | null} catalog
41
+ * @property {OutcomeMeta} meta - mutated: `trace` is refreshed from error records
42
+ * @property {StreamCallbacks} callbacks
43
+ * @property {() => void} finish - the carrier's cleanup (remove the entry,
44
+ * cancel readers and timers); called exactly once, before the terminal callback
45
+ * @property {number | null} lastSeq - the resume seq the caller passed (the
46
+ * regression baseline until a snapshot or patch moves it)
47
+ */
48
+
49
+ /**
50
+ * The consumer the carriers feed. `snapshot`/`patch` take the seq the
51
+ * wire carried (the SSE id, the frame's `seq`) — `null` falls back to
52
+ * the data's own `seq`; `error`/`end` take the event data; `fail` takes
53
+ * a ready outcome (a transport failure the carrier classified). All are
54
+ * no-ops once finished.
55
+ * @param {StreamConsumerOptions} options
56
+ * @returns {{ snapshot: (seq: number | null, data: unknown) => void,
57
+ * patch: (seq: number | null, data: unknown) => void,
58
+ * error: (data: unknown) => void,
59
+ * end: (data: unknown) => void,
60
+ * fail: (outcome: Outcome) => void,
61
+ * cancel: () => void,
62
+ * finished: () => boolean }}
63
+ */
64
+ export function createStreamConsumer(options) {
65
+ const { route, catalog, meta, callbacks, finish } = options;
66
+ let lastSeq = options.lastSeq;
67
+ let done = false;
68
+
69
+ /**
70
+ * Run one callback totally.
71
+ * @param {((arg: any, extra?: any) => void) | undefined} cb
72
+ * @param {any} arg
73
+ * @param {any} [extra]
74
+ */
75
+ function call(cb, arg, extra) {
76
+ if (cb === undefined) return;
77
+ try {
78
+ cb(arg, extra);
79
+ }
80
+ catch {
81
+ // a consumer callback that throws never breaks the stream machine
82
+ }
83
+ }
84
+
85
+ /** Terminate: cleanup first, then the terminal callback. */
86
+ function terminate() {
87
+ if (done) return false;
88
+ done = true;
89
+ try {
90
+ finish();
91
+ }
92
+ catch {
93
+ // the carrier's cleanup must not eat the terminal callback
94
+ }
95
+ return true;
96
+ }
97
+
98
+ /**
99
+ * A stream-code outcome (`JC2090`/`JC2092`/`JC2094`), rendered here.
100
+ * @param {'JC2090' | 'JC2092' | 'JC2094'} code
101
+ * @param {Record<string, unknown>} params
102
+ * @returns {Outcome}
103
+ */
104
+ function streamOutcome(code, params) {
105
+ const row = STREAM_ERRORS[code];
106
+ return failedOutcome(row.kind === 'network' ? 'network' : 'contract',
107
+ outcomeError(code, renderMessage(catalog, row.msgid, { op: route.id, ...params }), null, null, row.retryable), meta);
108
+ }
109
+
110
+ /**
111
+ * The seq of one event: the wire's, or the data's own.
112
+ * @param {number | null} seq
113
+ * @param {any} data
114
+ * @returns {number | null}
115
+ */
116
+ function seqOf(seq, data) {
117
+ if (typeof seq === 'number' && Number.isFinite(seq)) return seq;
118
+ const own = data !== null && typeof data === 'object' ? data.seq : undefined;
119
+ return typeof own === 'number' && Number.isFinite(own) ? own : null;
120
+ }
121
+
122
+ return {
123
+ snapshot(seq, data) {
124
+ if (done) return;
125
+ const envelope = /** @type {any} */ (data);
126
+ if (envelope === null || typeof envelope !== 'object' || !Object.hasOwn(envelope, 'value')) {
127
+ if (terminate()) call(callbacks.onError, failedOutcome('contract',
128
+ clientError(catalog, 'JC2053', { op: route.id }, null, [{ path: '', keyword: 'snapshot' }]), meta));
129
+ return;
130
+ }
131
+ const at = seqOf(seq, envelope);
132
+ const v = verdict(route.validateOutput, envelope.value);
133
+ if (!v.valid) {
134
+ if (terminate()) call(callbacks.onError, failedOutcome('contract',
135
+ clientError(catalog, 'JC2053', { op: route.id }, null, projectValidationDetails(route.details, v.errors)), meta));
136
+ return;
137
+ }
138
+ // a mid-stream snapshot (a maxPatchBytes replacement) must still advance
139
+ if (at !== null && lastSeq !== null && at !== 0 && at <= lastSeq) {
140
+ if (terminate()) call(callbacks.onError, streamOutcome('JC2092', {}));
141
+ return;
142
+ }
143
+ if (at !== null) lastSeq = at;
144
+ call(callbacks.onSnapshot, envelope.value, { seq: at === null ? 0 : at, resumed: envelope.resumed === true });
145
+ },
146
+ patch(seq, data) {
147
+ if (done) return;
148
+ const emission = /** @type {any} */ (data);
149
+ const patch = emission !== null && typeof emission === 'object' ? emission.patch : undefined;
150
+ const at = seqOf(seq, emission);
151
+ if (!Array.isArray(patch) || at === null) {
152
+ if (terminate()) call(callbacks.onError, failedOutcome('contract',
153
+ clientError(catalog, 'JC2053', { op: route.id }, null, [{ path: '', keyword: 'patch' }]), meta));
154
+ return;
155
+ }
156
+ if (lastSeq !== null && at <= lastSeq) {
157
+ if (terminate()) call(callbacks.onError, streamOutcome('JC2092', {}));
158
+ return;
159
+ }
160
+ lastSeq = at;
161
+ call(callbacks.onPatch, { patch, seq: at });
162
+ },
163
+ error(data) {
164
+ if (!terminate()) return;
165
+ const record = /** @type {any} */ (data);
166
+ const code = record !== null && typeof record === 'object' && typeof record.code === 'string' ? record.code : null;
167
+ if (record !== null && typeof record === 'object' && typeof record.requestId === 'string' && record.requestId.length > 0) {
168
+ meta.trace = record.requestId;
169
+ }
170
+ if (code !== null && Object.hasOwn(route.errors, code)) {
171
+ const message = typeof record.message === 'string'
172
+ ? record.message
173
+ : renderMessage(catalog, 'contract/handler-error', { op: route.id, code });
174
+ const retryable = typeof record.retryable === 'boolean' ? record.retryable : route.retryOn.has(code);
175
+ call(callbacks.onError, failedOutcome('failure', outcomeError(code, message, null, record.details, retryable), meta));
176
+ return;
177
+ }
178
+ // an undeclared server error ends the stream as a contract violation;
179
+ // the server's record rides in details so a JC2091 stays visible
180
+ const details = code === null ? null : {
181
+ code,
182
+ message: record !== null && typeof record === 'object' && typeof record.message === 'string' ? record.message : null,
183
+ };
184
+ call(callbacks.onError, failedOutcome('contract',
185
+ outcomeError('JC2093', renderMessage(catalog, STREAM_ERRORS.JC2093.msgid, { op: route.id, code: code === null ? 'none' : code }),
186
+ null, details, false), meta));
187
+ },
188
+ end(data) {
189
+ if (!terminate()) return;
190
+ const record = /** @type {any} */ (data);
191
+ const reason = record !== null && typeof record === 'object' && typeof record.reason === 'string' ? record.reason : 'closed';
192
+ call(callbacks.onEnd, { reason });
193
+ },
194
+ fail(outcome) {
195
+ if (!terminate()) return;
196
+ call(callbacks.onError, outcome);
197
+ },
198
+ cancel() {
199
+ if (done) return;
200
+ done = true;
201
+ try {
202
+ finish();
203
+ }
204
+ catch {
205
+ // a cancel is silent either way
206
+ }
207
+ },
208
+ finished: () => done,
209
+ };
210
+ }
211
+
212
+ export { STREAM_ERRORS };