@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.
- package/README.md +191 -30
- package/dist/types/adapters/fetch.d.ts +11 -10
- package/dist/types/adapters/node.d.ts +24 -10
- package/dist/types/client/http.d.ts +77 -10
- package/dist/types/compat.d.ts +1 -1
- package/dist/types/errors.d.ts +3 -0
- package/dist/types/host.d.ts +179 -0
- package/dist/types/http/body.d.ts +147 -0
- package/dist/types/http/dispatch.d.ts +26 -2
- package/dist/types/http/serve.d.ts +46 -3
- package/dist/types/http/wire.d.ts +31 -17
- package/dist/types/ledger.d.ts +57 -12
- package/dist/types/local/index.d.ts +7 -1
- package/dist/types/messages.d.ts +2 -0
- package/dist/types/path.d.ts +4 -2
- package/dist/types/pipeline.d.ts +15 -1
- package/dist/types/port/client.d.ts +18 -1
- package/dist/types/port/serve.d.ts +38 -5
- package/dist/types/project/tools.d.ts +1 -1
- package/dist/types/project/typescript.d.ts +11 -0
- package/dist/types/runtime.d.ts +25 -0
- package/dist/types/stream/client.d.ts +14 -3
- package/dist/types/stream/server.d.ts +218 -44
- package/dist/types/stream/sse.d.ts +10 -0
- package/docs/APP-INTEGRATION.md +4 -2
- package/docs/CONTRACT-FORMAT.md +617 -145
- package/package.json +5 -5
- package/src/adapters/fetch.js +144 -25
- package/src/adapters/node.js +246 -82
- package/src/cli.js +22 -16
- package/src/client/http.js +588 -189
- package/src/compat.js +1 -1
- package/src/errors.js +3 -0
- package/src/host.js +319 -0
- package/src/http/body.js +337 -0
- package/src/http/dispatch.js +511 -75
- package/src/http/serve.js +39 -5
- package/src/http/wire.js +33 -14
- package/src/ledger.js +119 -36
- package/src/local/index.js +91 -35
- package/src/messages.js +2 -0
- package/src/path.js +9 -3
- package/src/pipeline.js +18 -1
- package/src/port/client.js +39 -6
- package/src/port/serve.js +207 -69
- package/src/project/tools.js +9 -2
- package/src/project/typescript.js +91 -1
- package/src/project/typescript.jtlt.json +39 -7
- package/src/runtime.js +36 -0
- package/src/stream/client.js +40 -6
- package/src/stream/server.js +573 -138
- 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
|
|
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`
|
package/dist/types/messages.d.ts
CHANGED
|
@@ -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>>;
|
package/dist/types/path.d.ts
CHANGED
|
@@ -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 `{}`
|
|
69
|
-
* identity `JC0010`
|
|
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
|
*/
|
package/dist/types/pipeline.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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';
|
|
@@ -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(
|
|
6
|
-
* resumption, read and validate the snapshot, forward each
|
|
7
|
-
* and guarantee `stop()` then `close()` run **exactly once**
|
|
8
|
-
* the stream ends — peer disconnect, unsubscribe, server
|
|
9
|
-
* error emission, an invalid snapshot
|
|
10
|
-
* intents this module raises as SSE
|
|
11
|
-
* them as push frames — the sequencing
|
|
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` —
|
|
17
|
-
* `
|
|
18
|
-
* wire) or is swallowed at close, and
|
|
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) => (() =>
|
|
25
|
-
close: () =>
|
|
26
|
-
replay?: (
|
|
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
|
|
30
|
-
|
|
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
|
-
}) =>
|
|
35
|
-
error: (intent:
|
|
36
|
-
end: (reason: string, lastSeq: number) =>
|
|
37
|
-
|
|
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
|
|
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) => (() =>
|
|
56
|
-
* close: () =>
|
|
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
|
|
60
|
-
* event,
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
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,
|
|
68
|
-
* @property {(seq: number, emission: { patch: unknown[], seq: number }) =>
|
|
69
|
-
* @property {(intent:
|
|
70
|
-
* @property {(reason: string, lastSeq: number) =>
|
|
71
|
-
* @property {() =>
|
|
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
|
|
93
|
-
*
|
|
94
|
-
*
|
|
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 {
|
|
277
|
+
* @returns {StreamRunner}
|
|
102
278
|
*/
|
|
103
|
-
export declare function runSubscription(route: PipelineRoute, sub: SubscriptionLike, hooks: StreamHooks
|
|
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[];
|
package/docs/APP-INTEGRATION.md
CHANGED
|
@@ -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
|
|
299
|
-
|
|
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.
|