@jarenjs/contract 0.49.2 → 0.66.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 (52) hide show
  1. package/README.md +191 -30
  2. package/dist/types/adapters/fetch.d.ts +11 -10
  3. package/dist/types/adapters/node.d.ts +24 -10
  4. package/dist/types/client/http.d.ts +77 -10
  5. package/dist/types/compat.d.ts +1 -1
  6. package/dist/types/errors.d.ts +3 -0
  7. package/dist/types/host.d.ts +179 -0
  8. package/dist/types/http/body.d.ts +147 -0
  9. package/dist/types/http/dispatch.d.ts +26 -2
  10. package/dist/types/http/serve.d.ts +46 -3
  11. package/dist/types/http/wire.d.ts +31 -17
  12. package/dist/types/ledger.d.ts +57 -12
  13. package/dist/types/local/index.d.ts +7 -1
  14. package/dist/types/messages.d.ts +2 -0
  15. package/dist/types/path.d.ts +4 -2
  16. package/dist/types/pipeline.d.ts +15 -1
  17. package/dist/types/port/client.d.ts +18 -1
  18. package/dist/types/port/serve.d.ts +38 -5
  19. package/dist/types/project/tools.d.ts +1 -1
  20. package/dist/types/project/typescript.d.ts +11 -0
  21. package/dist/types/runtime.d.ts +25 -0
  22. package/dist/types/stream/client.d.ts +14 -3
  23. package/dist/types/stream/server.d.ts +218 -44
  24. package/dist/types/stream/sse.d.ts +10 -0
  25. package/docs/APP-INTEGRATION.md +4 -2
  26. package/docs/CONTRACT-FORMAT.md +617 -145
  27. package/package.json +5 -5
  28. package/src/adapters/fetch.js +144 -25
  29. package/src/adapters/node.js +246 -82
  30. package/src/cli.js +22 -16
  31. package/src/client/http.js +588 -189
  32. package/src/compat.js +1 -1
  33. package/src/errors.js +3 -0
  34. package/src/host.js +319 -0
  35. package/src/http/body.js +337 -0
  36. package/src/http/dispatch.js +511 -75
  37. package/src/http/serve.js +39 -5
  38. package/src/http/wire.js +33 -14
  39. package/src/ledger.js +119 -36
  40. package/src/local/index.js +91 -35
  41. package/src/messages.js +2 -0
  42. package/src/path.js +9 -3
  43. package/src/pipeline.js +18 -1
  44. package/src/port/client.js +39 -6
  45. package/src/port/serve.js +207 -69
  46. package/src/project/tools.js +9 -2
  47. package/src/project/typescript.js +91 -1
  48. package/src/project/typescript.jtlt.json +39 -7
  49. package/src/runtime.js +36 -0
  50. package/src/stream/client.js +40 -6
  51. package/src/stream/server.js +573 -138
  52. package/src/stream/sse.js +2 -0
@@ -39,9 +39,15 @@ export type OutcomeMeta = import('../client/outcome.js').OutcomeMeta;
39
39
  export type PipelineRoute = import('../pipeline.js').PipelineRoute;
40
40
  export type LocalOptions = {
41
41
  /**
42
- * - the trace generator; default `crypto.randomUUID`
42
+ * - the trace generator; default the
43
+ * runtime record's `uuid`, itself `crypto.randomUUID` by default
43
44
  */
44
45
  trace?: () => string;
46
+ /**
47
+ * - the host's runtime record: its `uuid` generates the trace where
48
+ * `trace` is absent
49
+ */
50
+ runtime?: Partial<import('@jarenjs/core/runtime').Runtime>;
45
51
  /**
46
52
  * - `'never'` is a declared
47
53
  * downgrade, reported in `capabilities.validatedOutput`
@@ -58,6 +58,8 @@ export declare const contractMessagesEn: Readonly<{
58
58
  'contract/seq-regression': "the stream of operation {op} violated its seq order";
59
59
  'contract/stream-error': "the stream of operation {op} ended with a server error ({code})";
60
60
  'contract/heartbeat-missed': "the stream of operation {op} went silent for {ms}ms";
61
+ 'contract/slow-consumer': "the stream of operation {op} ended: the consumer fell behind its bounded queue";
62
+ 'contract/reconnect-exhausted': "the stream of operation {op} could not be re-established after {attempts} attempts (last: {lastCode})";
61
63
  }>;
62
64
  /** The compiled English catalog (module-level singleton). */
63
65
  export declare const contractCatalogEn: Readonly<Record<string, (params: object, error?: object) => string>>;
@@ -65,8 +65,10 @@ export type ParsedPathTemplate = {
65
65
  */
66
66
  export declare function parsePathTemplate(source: unknown): ParsedPathTemplate;
67
67
  /**
68
- * The shape of a template with every variable normalized to `{}` — the
69
- * identity `JC0010` is decided on.
68
+ * The shape of a template with every variable normalized to `{}` and
69
+ * static delimiters re-escaped from decoded text — the identity `JC0010`
70
+ * is decided on. Escaped separators and braces stay inside their static
71
+ * segment, distinct from path boundaries and variable markers.
70
72
  * @param {ParsedPathTemplate} parsed
71
73
  * @returns {string}
72
74
  */
@@ -114,7 +114,12 @@ export declare const PORT_LOCAL_ERRORS: Readonly<{
114
114
  }>;
115
115
  /**
116
116
  * A server trace id from a host generator. TOTAL: a generator that
117
- * throws or answers a non-string is replaced by the platform's UUID.
117
+ * throws or answers a non-string is replaced by the platform's UUID
118
+ * the one last-resort platform read in this package, reached only when
119
+ * the injected generator (a `trace` option or the runtime record's
120
+ * `uuid`) has itself failed, so a request still carries a trace; a run
121
+ * whose generator fails was not the deterministic run the record
122
+ * configures, and the fallback says nothing about it.
118
123
  * @param {() => string} trace
119
124
  * @returns {string}
120
125
  */
@@ -130,6 +135,15 @@ export declare function safeTrace(trace: () => string): string;
130
135
  * @returns {OperationResult | null}
131
136
  */
132
137
  export declare function validateOperationInput(route: PipelineRoute, input: unknown): OperationResult | null;
138
+ /**
139
+ * Classify a declared failure a HOST hook answered (`identify`/`acquire`,
140
+ * docs/CONTRACT-FORMAT.md §7.7): the same rules as a handler's `ctx.fail`
141
+ * — the code must be declared, the details must pass the declaration.
142
+ * @param {PipelineRoute} route
143
+ * @param {import('./errors.js').ContractFailureValue} failure
144
+ * @returns {OperationResult}
145
+ */
146
+ export declare function classifyDeclared(route: PipelineRoute, failure: import('./errors.js').ContractFailureValue): OperationResult;
133
147
  /**
134
148
  * Call the handler through the uniform promise boundary and classify the
135
149
  * settlement. Never rejects; the returned promise always resolves an
@@ -43,6 +43,12 @@ export type PortClientOptions = {
43
43
  * - a message catalog consulted before the English one
44
44
  */
45
45
  catalog?: Record<string, string | ((params: object) => string)>;
46
+ /**
47
+ * - the host's runtime record: its `uuid` mints the client id every
48
+ * request id of this client is prefixed with; `crypto.randomUUID` by
49
+ * default
50
+ */
51
+ runtime?: Partial<import('@jarenjs/core/runtime').Runtime>;
46
52
  };
47
53
  export type PortInvokeContext = {
48
54
  /**
@@ -71,6 +77,9 @@ export type PortSubscribeOptions = {
71
77
  onSnapshot?: (value: unknown, info: {
72
78
  seq: number;
73
79
  resumed: boolean;
80
+ reset: boolean;
81
+ earliestAvailable: number | null;
82
+ highWatermark: number | null;
74
83
  }) => void;
75
84
  onPatch?: (emission: {
76
85
  patch: unknown[];
@@ -85,9 +94,17 @@ export type PortSubscribeOptions = {
85
94
  */
86
95
  signal?: AbortSignal;
87
96
  /**
88
- * - the resume seq (what a reconnect passes)
97
+ * - the resume seq (what a re-entered subscribe passes)
89
98
  */
90
99
  lastSeq?: number;
100
+ /**
101
+ * - validated as on the HTTP client, then
102
+ * nothing: a channel has no network loss to reconnect from (a closed channel is
103
+ * `JC2074`, final), so the same options object serves both clients
104
+ */
105
+ reconnect?: {
106
+ max: number;
107
+ };
91
108
  };
92
109
  export type PortClient = {
93
110
  invoke: (op: string, input?: unknown, ctx?: PortInvokeContext) => Promise<Outcome>;
@@ -34,7 +34,8 @@ export type ServePortOptions = {
34
34
  */
35
35
  channel: ChannelLike;
36
36
  /**
37
- * - the server trace generator; default `crypto.randomUUID`
37
+ * - the server trace generator; default
38
+ * the runtime record's `uuid`, itself `crypto.randomUUID` by default
38
39
  */
39
40
  trace?: () => string;
40
41
  /**
@@ -46,14 +47,46 @@ export type ServePortOptions = {
46
47
  * - a message catalog consulted before the English one
47
48
  */
48
49
  catalog?: Record<string, string | ((params: object) => string)>;
49
- /**
50
- * - observes the cause behind every `JC2070` frame, validator throws
51
- * and a channel whose `postMessage` throws
52
- */
53
50
  onError?: (error: unknown, ctx: {
54
51
  op: string;
55
52
  trace: string;
56
53
  } | null) => void;
54
+ /**
55
+ * - the host lifecycle's first hook (docs/CONTRACT-FORMAT.md §7.7), run
56
+ * after the operation resolved and before the input is validated;
57
+ * `meta.carrier` is `'port'` and the request-line members are `null`
58
+ */
59
+ identify?: (meta: import('../host.js').IdentifyMeta) => unknown;
60
+ /**
61
+ * - the second hook, run after the input validated; a `settlement` on
62
+ * its lease is accepted and unused — this binding carries no
63
+ * idempotency
64
+ * - observes the cause behind every `JC2070` frame, validator throws
65
+ * and a channel whose `postMessage` throws
66
+ */
67
+ acquire?: (input: unknown, identity: unknown, enter: (lease: unknown) => Promise<unknown>) => unknown;
68
+ /**
69
+ * - the host's runtime record: its `uuid` generates the server trace
70
+ * where `trace` is absent
71
+ */
72
+ runtime?: Partial<import('@jarenjs/core/runtime').Runtime>;
73
+ /**
74
+ * - the bounds of every push-frame stream (docs/CONTRACT-FORMAT.md
75
+ * §18.1): a replay page asks for at most `replay.limit` emissions /
76
+ * `replay.maxBytes` patch bytes (default 256 / 1 MiB); the undelivered
77
+ * queue holds at most `queue.events` frames / `queue.bytes` frame
78
+ * bytes (default 256 / 1 MiB) before the stream ends with `JC2096`
79
+ */
80
+ streamLimits?: {
81
+ replay?: {
82
+ limit?: number;
83
+ maxBytes?: number;
84
+ };
85
+ queue?: {
86
+ events?: number;
87
+ bytes?: number;
88
+ };
89
+ };
57
90
  };
58
91
  export type PortServerCapabilities = {
59
92
  name: 'port';
@@ -24,7 +24,7 @@ export type ToolDefinition = {
24
24
  execute: (args: any) => any;
25
25
  };
26
26
  export type ToolClient = {
27
- invoke: (op: string, input: any, ctx?: any) => any;
27
+ invoke(op: string, input: any, ctx?: any): any;
28
28
  };
29
29
  export type ContractToolsOptions = {
30
30
  /**
@@ -22,6 +22,17 @@
22
22
  * stylesheet — the D6 shapes as every binding carries them
23
23
  * (`OUTCOME_META_MEMBERS` / `OUTCOME_ERROR_MEMBERS` in the client module
24
24
  * are the runtime twins; a test holds the text to them).
25
+ *
26
+ * One convention rides on top of emit's reading, the suite's: a string
27
+ * with `format: "date-time"` or `format: "date"` is the `DateTime`
28
+ * brand, so a consumer's generated types agree with `@jarenjs/db`'s
29
+ * entity types (`entityEmitModel`) and `@jarenjs/linq`'s schema pen,
30
+ * which both read a date format that way. Emit itself records a format
31
+ * only as a dropped constraint, so the brand is applied HERE, by
32
+ * rewriting date-formatted string nodes to a `$ref` of one shared
33
+ * definition before emit reads the document — in every position, an
34
+ * array item as much as a member — and giving that definition the brand
35
+ * intersection.
25
36
  */
26
37
  export type Contract = import('../compile.js').Contract;
27
38
  export type CompiledOperation = import('../compile.js').CompiledOperation;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @file The one place a contract binding reads its `runtime` option:
3
+ * the record is resolved through `@jarenjs/core/runtime` — nothing given
4
+ * is the platform's own clock, identifier and source — and a malformed
5
+ * record is refused as the binding's OWN host error (`JC1001` for a
6
+ * server, `JC1008` for a client), naming the option, so the refusal
7
+ * reads like every other option refusal of that constructor. Every
8
+ * binding then takes its host facts from the record only where its
9
+ * explicit option (`trace`, `keys`, `now`) is absent: the record
10
+ * injects, it never replaces a published option.
11
+ */
12
+ export type Runtime = import('@jarenjs/core/runtime').Runtime;
13
+ /**
14
+ * @typedef {import('@jarenjs/core/runtime').Runtime} Runtime
15
+ */
16
+ /**
17
+ * Resolve a binding's `options.runtime`, or refuse it as that binding's
18
+ * host error under the code it refuses every malformed option with.
19
+ * @param {Partial<Runtime> | undefined | null} candidate - `options.runtime`
20
+ * @param {(code: any, reason: string) => Error} host - the binding's host
21
+ * error constructor
22
+ * @param {'JC1001' | 'JC1008'} code - the binding's malformed-option code
23
+ * @returns {Readonly<Runtime>}
24
+ */
25
+ export declare function resolveHostRuntime(candidate: Partial<Runtime> | undefined | null, host: (code: any, reason: string) => Error, code: 'JC1001' | 'JC1008'): Readonly<Runtime>;
@@ -21,6 +21,9 @@ export type StreamCallbacks = {
21
21
  onSnapshot?: (value: unknown, info: {
22
22
  seq: number;
23
23
  resumed: boolean;
24
+ reset: boolean;
25
+ earliestAvailable: number | null;
26
+ highWatermark: number | null;
24
27
  }) => void;
25
28
  onPatch?: (emission: {
26
29
  patch: unknown[];
@@ -60,8 +63,12 @@ export type StreamConsumerOptions = {
60
63
  */
61
64
  /**
62
65
  * The callbacks of one `client.subscribe` call; every one optional.
66
+ * `onSnapshot`'s `info` is stable in shape: `reset` says the snapshot
67
+ * re-seeds a consumer whose cursor fell behind the server's retention
68
+ * (its `seq` is then the cursor to resume from), and the two watermarks
69
+ * are the server log's when it reported them (`null` on a fresh stream).
63
70
  * @typedef {Object} StreamCallbacks
64
- * @property {(value: unknown, info: { seq: number, resumed: boolean }) => void} [onSnapshot]
71
+ * @property {(value: unknown, info: { seq: number, resumed: boolean, reset: boolean, earliestAvailable: number | null, highWatermark: number | null }) => void} [onSnapshot]
65
72
  * @property {(emission: { patch: unknown[], seq: number }) => void} [onPatch]
66
73
  * @property {(outcome: Outcome) => void} [onError]
67
74
  * @property {(info: { reason: string }) => void} [onEnd]
@@ -82,7 +89,9 @@ export type StreamConsumerOptions = {
82
89
  * wire carried (the SSE id, the frame's `seq`) — `null` falls back to
83
90
  * the data's own `seq`; `error`/`end` take the event data; `fail` takes
84
91
  * a ready outcome (a transport failure the carrier classified). All are
85
- * no-ops once finished.
92
+ * no-ops once finished. `lastSeq` reads the cursor: the resume seq the
93
+ * caller passed until a snapshot or patch moves it — what a further
94
+ * attempt resumes from.
86
95
  * @param {StreamConsumerOptions} options
87
96
  * @returns {{ snapshot: (seq: number | null, data: unknown) => void,
88
97
  * patch: (seq: number | null, data: unknown) => void,
@@ -90,7 +99,8 @@ export type StreamConsumerOptions = {
90
99
  * end: (data: unknown) => void,
91
100
  * fail: (outcome: Outcome) => void,
92
101
  * cancel: () => void,
93
- * finished: () => boolean }}
102
+ * finished: () => boolean,
103
+ * lastSeq: () => number | null }}
94
104
  */
95
105
  export declare function createStreamConsumer(options: StreamConsumerOptions): {
96
106
  snapshot: (seq: number | null, data: unknown) => void;
@@ -100,5 +110,6 @@ export declare function createStreamConsumer(options: StreamConsumerOptions): {
100
110
  fail: (outcome: Outcome) => void;
101
111
  cancel: () => void;
102
112
  finished: () => boolean;
113
+ lastSeq: () => number | null;
103
114
  };
104
115
  export { STREAM_ERRORS };
@@ -2,39 +2,111 @@
2
2
  * @file The server half of the stream binding, carrier-neutral
3
3
  * (docs/CONTRACT-FORMAT.md §17–§18): take the subscription a handler
4
4
  * settled (the duck-typed LIVE shape — `result`/`snapshot()`,
5
- * `subscribe(cb) → stop`, `close()`, optional `replay(seq)`), decide
6
- * resumption, read and validate the snapshot, forward each emission,
7
- * and guarantee `stop()` then `close()` run **exactly once** however
8
- * the stream ends — peer disconnect, unsubscribe, server shutdown, an
9
- * error emission, an invalid snapshot. The HTTP dispatcher renders the
10
- * intents this module raises as SSE events; the port server renders
11
- * them as push frames — the sequencing is decided here once so the two
12
- * carriers can never disagree.
5
+ * `subscribe(cb) → stop`, `close()`, optional `replay(after, options)`),
6
+ * decide resumption, read and validate the snapshot, forward each
7
+ * emission, and guarantee `stop()` then `close()` run **exactly once**
8
+ * however the stream ends — peer disconnect, unsubscribe, server
9
+ * shutdown, an error emission, an invalid snapshot, a slow consumer.
10
+ * The HTTP dispatcher renders the intents this module raises as SSE
11
+ * events; the port server renders them as push frames — the sequencing
12
+ * is decided here once so the two carriers can never disagree.
13
+ *
14
+ * The carrier encodes, the runner decides. A hook answers the WIRE
15
+ * FRAME of one event (an SSE text, a port frame object) or `null` to
16
+ * skip it; the runner measures it through the carrier's `size`, charges
17
+ * it to ONE bounded queue, and writes it through the carrier's `write`
18
+ * one frame at a time, each behind the previous one's settlement (the
19
+ * suite's awaited-sink primitive): an emission that arrives while a
20
+ * write is pending — or while a replay page is loading — waits its turn
21
+ * in order, never overlapped, never dropped. A frame's count and bytes
22
+ * stay charged until its write settled, so a blocked write cannot hide
23
+ * memory outside the bound; when the next frame would cross either
24
+ * bound the stream ends with `JC2096` and the carrier is told to tear
25
+ * its sink down. A write that rejects means the carrier is gone (the
26
+ * peer dropped the socket, the consumer cancelled the stream): the
27
+ * runner releases silently.
28
+ *
29
+ * Replay is paged, never an array (§18.1): `replay(after, { limit,
30
+ * maxBytes, signal })` answers one `changes.page()`-shaped page —
31
+ * `{ items, next?, earliestAvailable, highWatermark, hasMore,
32
+ * resetRequired }` — validated defensively; the first page's
33
+ * `highWatermark` is the target, and paging stops there however busy
34
+ * the writer is. A page with `resetRequired` delivers no suffix: the
35
+ * runner reads a fresh snapshot and emits it with `reset: true`, both
36
+ * watermarks, and an event id no lower than any live emission already
37
+ * buffered, so a patch the snapshot already reflects is never replayed.
38
+ *
39
+ * A source's `stop()` and `close()` may answer promises; the runner's
40
+ * `done` settles only after stop, then close, then the carrier's `done`
41
+ * hook have all settled, once — the completion signal a host's
42
+ * finalizers wait on.
13
43
  *
14
44
  * Total for everything a handler's subscription can do: a throwing
15
45
  * `snapshot()`/`result` accessor, a `subscribe` that throws, a hostile
16
- * emission, a throwing `stop`/`close` — every one settles into the
17
- * `fault` intent (the cause for the binding's `onError`, never the
18
- * wire) or is swallowed at close, and the stream still terminates.
46
+ * emission, a malformed or rejecting page, a throwing `stop`/`close` —
47
+ * every one settles into the `error` intent (the cause for the
48
+ * binding's `onError`, never the wire) or is swallowed at close, and
49
+ * the stream still terminates.
19
50
  */
20
51
  export type PipelineRoute = import('../pipeline.js').PipelineRoute;
52
+ export type ReplayOptions = {
53
+ limit: number;
54
+ maxBytes: number;
55
+ signal: AbortSignal;
56
+ };
57
+ export type ReplayPage = {
58
+ items: {
59
+ patch: unknown[];
60
+ seq: number;
61
+ }[];
62
+ next?: number;
63
+ earliestAvailable: number | null;
64
+ highWatermark: number;
65
+ hasMore: boolean;
66
+ resetRequired: boolean;
67
+ };
21
68
  export type SubscriptionLike = {
22
69
  result?: unknown;
23
70
  snapshot?: () => unknown;
24
- subscribe: (cb: (emission: any) => void) => (() => void);
25
- close: () => void;
26
- replay?: (seq: number) => unknown;
71
+ subscribe: (cb: (emission: any) => void) => (() => unknown);
72
+ close: () => unknown;
73
+ replay?: (after: number, options: ReplayOptions) => ReplayPage | null | undefined | Promise<ReplayPage | null | undefined>;
27
74
  mode?: unknown;
28
75
  };
29
- export type StreamHooks = {
30
- snapshot: (seq: number, value: unknown, resumed: boolean) => void;
76
+ export type SnapshotData = {
77
+ value: unknown;
78
+ resumed: boolean;
79
+ reset: boolean;
80
+ earliestAvailable: number | null;
81
+ highWatermark: number | null;
82
+ };
83
+ export type ErrorIntent = 'invalid-snapshot' | 'source' | 'slow-consumer' | 'declared';
84
+ export type DeclaredStreamFailure = {
85
+ code: string;
86
+ details: unknown;
87
+ retryable: boolean;
88
+ };
89
+ export type StreamHooks<F = unknown> = {
90
+ snapshot: (seq: number, data: SnapshotData) => F | null;
31
91
  patch: (seq: number, emission: {
32
92
  patch: unknown[];
33
93
  seq: number;
34
- }) => void;
35
- error: (intent: 'invalid-snapshot' | 'source', cause: unknown, lastSeq: number) => void;
36
- end: (reason: string, lastSeq: number) => void;
37
- done: () => void;
94
+ }) => F | null;
95
+ error: (intent: ErrorIntent, cause: unknown, lastSeq: number, declared: DeclaredStreamFailure | null) => F | null;
96
+ end: (reason: string, lastSeq: number) => F | null;
97
+ size: (frame: F) => number;
98
+ write: (frame: F) => unknown;
99
+ done: (reason: 'slow-consumer' | null) => unknown;
100
+ };
101
+ export type StreamLimits = {
102
+ replay: {
103
+ limit: number;
104
+ maxBytes: number;
105
+ };
106
+ queue: {
107
+ events: number;
108
+ bytes: number;
109
+ };
38
110
  };
39
111
  export type StreamOptions = {
40
112
  /**
@@ -45,36 +117,140 @@ export type StreamOptions = {
45
117
  * - whether snapshots run the output validator
46
118
  */
47
119
  validate: boolean;
120
+ /**
121
+ * - the bounds; the defaults when absent
122
+ */
123
+ limits?: StreamLimits;
124
+ };
125
+ export type StreamRunner = {
126
+ stop: (reason: string | null) => void;
127
+ done: Promise<void>;
48
128
  };
49
129
  /**
50
130
  * @typedef {import('../pipeline.js').PipelineRoute} PipelineRoute
51
131
  */
52
132
  /**
53
- * The duck-typed subscription of docs/CONTRACT-FORMAT.md §17.1.
133
+ * The options of one replay page call (§18.1): at most `limit`
134
+ * emissions and `maxBytes` serialized patch bytes, and the runner's
135
+ * signal, aborted when the stream stops while the page is loading.
136
+ * @typedef {{ limit: number, maxBytes: number, signal: AbortSignal }} ReplayOptions
137
+ */
138
+ /**
139
+ * One replay page, in the shape `@jarenjs/db`'s `changes.page()`
140
+ * answers: the emissions after `after` in seq order, `next` the seq to
141
+ * continue from, the log's two watermarks, whether more remain, and
142
+ * the total refusal `resetRequired` — under which `items` is empty and
143
+ * `next` absent.
144
+ * @typedef {{ items: { patch: unknown[], seq: number }[], next?: number,
145
+ * earliestAvailable: number | null, highWatermark: number, hasMore: boolean,
146
+ * resetRequired: boolean }} ReplayPage
147
+ */
148
+ /**
149
+ * The duck-typed subscription of docs/CONTRACT-FORMAT.md §17.1. `stop`
150
+ * (what `subscribe` answers) and `close` may answer a promise; the
151
+ * runner awaits each before it reports completion. `replay` answers one
152
+ * page per call, value or promise.
54
153
  * @typedef {{ result?: unknown, snapshot?: () => unknown,
55
- * subscribe: (cb: (emission: any) => void) => (() => void),
56
- * close: () => void, replay?: (seq: number) => unknown, mode?: unknown }} SubscriptionLike
154
+ * subscribe: (cb: (emission: any) => void) => (() => unknown),
155
+ * close: () => unknown,
156
+ * replay?: (after: number, options: ReplayOptions) => ReplayPage | null | undefined | Promise<ReplayPage | null | undefined>,
157
+ * mode?: unknown }} SubscriptionLike
158
+ */
159
+ /**
160
+ * The data of a snapshot event (§18.1): the validated document, whether
161
+ * the stream resumed (never, today: a resumed stream starts with
162
+ * patches), whether this snapshot re-seeds a consumer whose cursor fell
163
+ * behind the log's retention (`reset`), and the log's watermarks as the
164
+ * replay source reported them (`null` on a fresh stream).
165
+ * @typedef {{ value: unknown, resumed: boolean, reset: boolean,
166
+ * earliestAvailable: number | null, highWatermark: number | null }} SnapshotData
167
+ */
168
+ /**
169
+ * The reasons a stream ends with an `error` event: `'invalid-snapshot'`
170
+ * (send `JC2091`), `'source'` (the subscription emitted `{ error }`
171
+ * with no declared code, or a page/subscribe/snapshot fault — send the
172
+ * host fault code), `'slow-consumer'` (the bounded queue would overflow
173
+ * — send `JC2096`), `'declared'` (the subscription emitted `{ error }`
174
+ * whose `code` the operation declares — send that code as the declared
175
+ * failure it is, with the {@link DeclaredStreamFailure} the runner
176
+ * classified).
177
+ * @typedef {'invalid-snapshot' | 'source' | 'slow-consumer' | 'declared'} ErrorIntent
178
+ */
179
+ /**
180
+ * A source error the operation declares (§17.1): the declared `code`,
181
+ * JSON-safe `details` when the error carried some (`undefined`
182
+ * otherwise), and `retryable` — the error's own boolean, else whether
183
+ * `policy.retry.on` names the code. Nothing of the error's message or
184
+ * stack is here: the carrier renders the operation's declared message.
185
+ * @typedef {{ code: string, details: unknown, retryable: boolean }} DeclaredStreamFailure
57
186
  */
58
187
  /**
59
- * What the carrier renders. Every hook is called at most once per
60
- * event, in wire order; after `error` or `end` no further hook fires.
61
- * `error` carries the intent (`'invalid-snapshot'` send `JC2091`;
62
- * `'source'` the subscription emitted `{ error }`, send the host
63
- * fault code) and the cause for the observer. `done` fires exactly once
64
- * after the stream terminated for any reason the carrier releases
65
- * its resources (timers, registries) there.
188
+ * What the carrier renders, as FRAMES. Each event hook answers the wire
189
+ * frame of one event — the SSE text, the port frame or `null` to skip
190
+ * it (a frame the wire cannot carry, observed by the carrier). The
191
+ * runner measures a frame with `size` (its bytes on the wire), charges
192
+ * it to the bounded queue, and writes it with `write`, one at a time,
193
+ * each behind the previous write's settlement; `write` may answer a
194
+ * promise. After the `error` or `end` frame no further frame is
195
+ * written. A `write` that throws or rejects is read as "the carrier can
196
+ * no longer deliver": the subscription is released silently. `done`
197
+ * fires exactly once after the stream terminated for any reason, with
198
+ * `'slow-consumer'` when the queue overflowed — the carrier tears its
199
+ * sink down instead of ending it — else `null`; the carrier releases
200
+ * its resources (timers, registries, the sink) there.
201
+ * @template [F=unknown]
66
202
  * @typedef {Object} StreamHooks
67
- * @property {(seq: number, value: unknown, resumed: boolean) => void} snapshot
68
- * @property {(seq: number, emission: { patch: unknown[], seq: number }) => void} patch
69
- * @property {(intent: 'invalid-snapshot' | 'source', cause: unknown, lastSeq: number) => void} error
70
- * @property {(reason: string, lastSeq: number) => void} end
71
- * @property {() => void} done
203
+ * @property {(seq: number, data: SnapshotData) => F | null} snapshot
204
+ * @property {(seq: number, emission: { patch: unknown[], seq: number }) => F | null} patch
205
+ * @property {(intent: ErrorIntent, cause: unknown, lastSeq: number, declared: DeclaredStreamFailure | null) => F | null} error
206
+ * @property {(reason: string, lastSeq: number) => F | null} end
207
+ * @property {(frame: F) => number} size
208
+ * @property {(frame: F) => unknown} write
209
+ * @property {(reason: 'slow-consumer' | null) => unknown} done
210
+ */
211
+ /**
212
+ * The bounds of one stream (§18.1): a replay page asks for at most
213
+ * `replay.limit` emissions and `replay.maxBytes` serialized patch
214
+ * bytes; the queue holds at most `queue.events` undelivered frames and
215
+ * `queue.bytes` of their wire bytes.
216
+ * @typedef {{ replay: { limit: number, maxBytes: number }, queue: { events: number, bytes: number } }} StreamLimits
72
217
  */
73
218
  /**
74
219
  * @typedef {Object} StreamOptions
75
220
  * @property {number | null} lastSeq - the peer's `Last-Event-ID` / `lastSeq`, or `null`
76
221
  * @property {boolean} validate - whether snapshots run the output validator
222
+ * @property {StreamLimits} [limits] - the bounds; the defaults when absent
223
+ */
224
+ /**
225
+ * What `runSubscription` answers: the stopper, and the completion
226
+ * signal. `stop(reason)` with a string renders the `end` event with
227
+ * that reason first (a server shutdown) and then releases; `stop(null)`
228
+ * is silent (the peer is gone, or asked) and does not wait for a
229
+ * carrier write still pending — a late settlement is ignored. `done`
230
+ * settles (it never rejects) once the source's `stop()` and `close()`
231
+ * and the carrier's `done` hook have all run, in that order, once.
232
+ * @typedef {{ stop: (reason: string | null) => void, done: Promise<void> }} StreamRunner
77
233
  */
234
+ /** The bounds every stream runs under unless the server says otherwise. */
235
+ export declare const STREAM_LIMITS_DEFAULT: Readonly<{
236
+ replay: Readonly<{
237
+ limit: 256;
238
+ maxBytes: number;
239
+ }>;
240
+ queue: Readonly<{
241
+ events: 256;
242
+ bytes: number;
243
+ }>;
244
+ }>;
245
+ /**
246
+ * Resolve and validate a server's `streamLimits` option against the
247
+ * defaults: every member a positive integer (`Infinity` allowed), else
248
+ * the binding's host error.
249
+ * @param {unknown} option - the server's `streamLimits`, or `undefined`
250
+ * @param {(reason: string) => Error} refuse - the binding's host-error factory
251
+ * @returns {StreamLimits}
252
+ */
253
+ export declare function resolveStreamLimits(option: unknown, refuse: (reason: string) => Error): StreamLimits;
78
254
  /**
79
255
  * Whether a settled handler value is a usable subscription.
80
256
  * Reads guardedly; a hostile value classifies as "not a subscription".
@@ -89,18 +265,16 @@ export declare function isSubscriptionLike(value: unknown): value is Subscriptio
89
265
  * snapshot at that emission's seq (§18.1); an `{ error }` emission —
90
266
  * and a hostile one — raises the `error` intent and ends the stream.
91
267
  *
92
- * Returns the stopper: `stop(reason)` with a string emits the `end`
93
- * event with that reason first (a server shutdown); `stop(null)` is
94
- * silent (the peer is gone, or asked). Idempotent; the subscription's
95
- * own `stop()` and `close()` run exactly once either way.
268
+ * Returns the stopper and the completion signal ({@link StreamRunner}).
269
+ * The stopper is idempotent; the subscription's own `stop()` and
270
+ * `close()` run exactly once either way.
96
271
  *
272
+ * @template F
97
273
  * @param {PipelineRoute} route
98
274
  * @param {SubscriptionLike} sub
99
- * @param {StreamHooks} hooks
275
+ * @param {StreamHooks<F>} hooks
100
276
  * @param {StreamOptions} options
101
- * @returns {{ stop: (reason: string | null) => void }}
277
+ * @returns {StreamRunner}
102
278
  */
103
- export declare function runSubscription(route: PipelineRoute, sub: SubscriptionLike, hooks: StreamHooks, options: StreamOptions): {
104
- stop: (reason: string | null) => void;
105
- };
279
+ export declare function runSubscription<F>(route: PipelineRoute, sub: SubscriptionLike, hooks: StreamHooks<F>, options: StreamOptions): StreamRunner;
106
280
  export { STREAM_ERRORS, STREAM_EVENTS, STREAM_MEDIA, HEARTBEAT_LINE, encodeStreamEvent } from './sse.js';
@@ -41,6 +41,16 @@ export declare const STREAM_ERRORS: Readonly<{
41
41
  msgid: "contract/heartbeat-missed";
42
42
  retryable: true;
43
43
  }>;
44
+ JC2096: Readonly<{
45
+ kind: "network";
46
+ msgid: "contract/slow-consumer";
47
+ retryable: true;
48
+ }>;
49
+ JC2097: Readonly<{
50
+ kind: "network";
51
+ msgid: "contract/reconnect-exhausted";
52
+ retryable: false;
53
+ }>;
44
54
  }>;
45
55
  /** The event names of the stream wire, on both carriers. */
46
56
  export declare const STREAM_EVENTS: readonly string[];
@@ -295,7 +295,9 @@ slots:
295
295
  (`status: 'error'`, the outcome's `kind`/`error`/`meta`); a server
296
296
  `end` lands the same way as a `network`-kind outcome with the
297
297
  channel-closed code — the stream is gone and the slot says so, so a
298
- view can offer a reconnect (a fresh `start`; reconnection is never
299
- automatic).
298
+ view can offer a reconnect: a fresh `start`. The binding passes no
299
+ `reconnect` to `client.subscribe`, so the client's opt-in reconnect
300
+ (CONTRACT-FORMAT.md §19) is for subscriptions the host opens itself;
301
+ a slot is re-entered by the view.
300
302
  - **`reset` releases the slot** exactly as for tasks: `status 'idle'`,
301
303
  `kind`/`error` cleared, `id`/`input`/`value`/`meta`/`seq` kept.