@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
|
@@ -29,25 +29,57 @@
|
|
|
29
29
|
" url<K extends keyof UrlOperations>(op: K, input: UrlOperations[K]): string;\n",
|
|
30
30
|
" close(): void;\n",
|
|
31
31
|
"}\n\n",
|
|
32
|
+
"/** Every opaque public operation by id, with its input type, for `HttpClient.bytes`. */\n",
|
|
33
|
+
"export interface ByteOperations {\n",
|
|
34
|
+
[{ "$apply": ["$.operations[?@.opaque == true]", "url"] }],
|
|
35
|
+
"}\n\n",
|
|
36
|
+
"/** Per-call options of bytes: the members of InvokeContext that apply to an opaque call, plus the request body to send — text, bytes, a Web stream, an async iterable of chunks, or none. */\n",
|
|
37
|
+
"export interface ByteContext { signal?: AbortSignal; attempt?: unknown; headers?: Record<string, string>; ifNoneMatch?: string; ifMatch?: string; body?: string | Uint8Array | ReadableStream<Uint8Array> | AsyncIterable<Uint8Array> | null }\n\n",
|
|
38
|
+
"/** The success value of bytes: the status, the response headers (lowercase names), the response media (null when none) and the live response body — a stream the caller reads; null when the response carries none. */\n",
|
|
39
|
+
"export type ByteResponse = { status: number; headers: Record<string, string>; media: string | null; body: ReadableStream<Uint8Array> | null };\n\n",
|
|
40
|
+
"/** The HTTP client: the binding-neutral Client plus bytes over the opaque operations, whose success owns a live stream rather than a JSON value. */\n",
|
|
41
|
+
"export interface HttpClient extends Client {\n",
|
|
42
|
+
" bytes<K extends keyof ByteOperations>(op: K, input: ByteOperations[K], ctx?: ByteContext): Promise<Outcome<ByteResponse>>;\n",
|
|
43
|
+
"}\n\n",
|
|
32
44
|
"/** A declared failure a handler returns (ctx.fail): the declared code, the catalog parameters, the wire details and whether the caller may retry (null defers to the operation's retry policy). */\n",
|
|
33
45
|
"export type Failure = { code: string; params: Readonly<Record<string, unknown>>; details: unknown; retryable: boolean | null };\n\n",
|
|
34
|
-
"/** The
|
|
35
|
-
"export
|
|
46
|
+
"/** The binding a handler context comes from. */\n",
|
|
47
|
+
"export type CarrierName = 'http' | 'port' | 'local';\n\n",
|
|
48
|
+
"/** The members every carrier's context shares; `host` is the host lifecycle's acquired value (null by default). */\n",
|
|
49
|
+
"export interface HandlerContextBase<Host = null> {\n",
|
|
36
50
|
" op: unknown;\n",
|
|
37
51
|
" trace: string;\n",
|
|
52
|
+
" host: Host;\n",
|
|
53
|
+
" headers: Readonly<Record<string, string>>;\n",
|
|
54
|
+
" signal: AbortSignal | null;\n",
|
|
55
|
+
" fail(code: string, params?: Record<string, unknown>, details?: unknown, options?: { retryable?: boolean }): Failure;\n",
|
|
56
|
+
"}\n\n",
|
|
57
|
+
"/** The HTTP binding's context: the request line, the raw body of an opaque operation, the idempotency key, and the entity-tag and status arms. */\n",
|
|
58
|
+
"export interface HttpHandlerContext<Host = null> extends HandlerContextBase<Host> {\n",
|
|
59
|
+
" carrier: 'http';\n",
|
|
38
60
|
" method: string;\n",
|
|
39
61
|
" path: string;\n",
|
|
40
62
|
" params: Readonly<Record<string, string>>;\n",
|
|
41
|
-
"
|
|
42
|
-
" body: string | Uint8Array | null;\n",
|
|
43
|
-
" signal: AbortSignal | null;\n",
|
|
63
|
+
" body: string | Uint8Array | AsyncIterable<Uint8Array> | null;\n",
|
|
44
64
|
" idempotency: Readonly<{ key: string; scope: string }> | null;\n",
|
|
45
|
-
" fail(code: string, params?: Record<string, unknown>, details?: unknown, options?: { retryable?: boolean }): Failure;\n",
|
|
46
65
|
" etag(tag: string, options?: { strong?: boolean }): void;\n",
|
|
47
66
|
" status(status: number): void;\n",
|
|
48
67
|
"}\n\n",
|
|
68
|
+
"/** The port and local bindings' context: no request line, no body, no key, and no callable etag or status — spelled null, never omitted. */\n",
|
|
69
|
+
"export interface ChannelHandlerContext<Host = null, Carrier extends 'port' | 'local' = 'port' | 'local'> extends HandlerContextBase<Host> {\n",
|
|
70
|
+
" carrier: Carrier;\n",
|
|
71
|
+
" method: null;\n",
|
|
72
|
+
" path: null;\n",
|
|
73
|
+
" params: null;\n",
|
|
74
|
+
" body: null;\n",
|
|
75
|
+
" idempotency: null;\n",
|
|
76
|
+
" etag: null;\n",
|
|
77
|
+
" status: null;\n",
|
|
78
|
+
"}\n\n",
|
|
79
|
+
"/** The per-request context a server binding hands a handler, selected by carrier: the HTTP context by default; a carrier union is a discriminated union to narrow on `carrier`. */\n",
|
|
80
|
+
"export type HandlerContext<Host = null, Carrier extends CarrierName = 'http'> = Extract<HttpHandlerContext<Host> | ChannelHandlerContext<Host, 'port'> | ChannelHandlerContext<Host, 'local'>, { carrier: Carrier }>;\n\n",
|
|
49
81
|
"/** The typed handler table of a server binding: one handler per invokable operation, answering the output, a declared failure, or a promise of either. */\n",
|
|
50
|
-
"export type Handlers = { [K in keyof Operations]: (input: Operations[K]['input'], ctx: HandlerContext) => Operations[K]['output'] | Failure | Promise<Operations[K]['output'] | Failure> };\n"
|
|
82
|
+
"export type Handlers<Host = null, Carrier extends CarrierName = 'http'> = { [K in keyof Operations]: (input: Operations[K]['input'], ctx: HandlerContext<Host, Carrier>) => Operations[K]['output'] | Failure | Promise<Operations[K]['output'] | Failure> };\n"
|
|
51
83
|
]
|
|
52
84
|
},
|
|
53
85
|
{
|
package/src/runtime.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file The one place a contract binding reads its `runtime` option:
|
|
4
|
+
* the record is resolved through `@jarenjs/core/runtime` — nothing given
|
|
5
|
+
* is the platform's own clock, identifier and source — and a malformed
|
|
6
|
+
* record is refused as the binding's OWN host error (`JC1001` for a
|
|
7
|
+
* server, `JC1008` for a client), naming the option, so the refusal
|
|
8
|
+
* reads like every other option refusal of that constructor. Every
|
|
9
|
+
* binding then takes its host facts from the record only where its
|
|
10
|
+
* explicit option (`trace`, `keys`, `now`) is absent: the record
|
|
11
|
+
* injects, it never replaces a published option.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { resolveRuntime } from '@jarenjs/core/runtime';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {import('@jarenjs/core/runtime').Runtime} Runtime
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Resolve a binding's `options.runtime`, or refuse it as that binding's
|
|
22
|
+
* host error under the code it refuses every malformed option with.
|
|
23
|
+
* @param {Partial<Runtime> | undefined | null} candidate - `options.runtime`
|
|
24
|
+
* @param {(code: any, reason: string) => Error} host - the binding's host
|
|
25
|
+
* error constructor
|
|
26
|
+
* @param {'JC1001' | 'JC1008'} code - the binding's malformed-option code
|
|
27
|
+
* @returns {Readonly<Runtime>}
|
|
28
|
+
*/
|
|
29
|
+
export function resolveHostRuntime(candidate, host, code) {
|
|
30
|
+
try {
|
|
31
|
+
return resolveRuntime(candidate);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
throw host(code, `options.runtime: ${error instanceof Error ? error.message : String(error)}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
package/src/stream/client.js
CHANGED
|
@@ -27,8 +27,12 @@ import { STREAM_ERRORS } from './sse.js';
|
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
29
|
* The callbacks of one `client.subscribe` call; every one optional.
|
|
30
|
+
* `onSnapshot`'s `info` is stable in shape: `reset` says the snapshot
|
|
31
|
+
* re-seeds a consumer whose cursor fell behind the server's retention
|
|
32
|
+
* (its `seq` is then the cursor to resume from), and the two watermarks
|
|
33
|
+
* are the server log's when it reported them (`null` on a fresh stream).
|
|
30
34
|
* @typedef {Object} StreamCallbacks
|
|
31
|
-
* @property {(value: unknown, info: { seq: number, resumed: boolean }) => void} [onSnapshot]
|
|
35
|
+
* @property {(value: unknown, info: { seq: number, resumed: boolean, reset: boolean, earliestAvailable: number | null, highWatermark: number | null }) => void} [onSnapshot]
|
|
32
36
|
* @property {(emission: { patch: unknown[], seq: number }) => void} [onPatch]
|
|
33
37
|
* @property {(outcome: Outcome) => void} [onError]
|
|
34
38
|
* @property {(info: { reason: string }) => void} [onEnd]
|
|
@@ -51,7 +55,9 @@ import { STREAM_ERRORS } from './sse.js';
|
|
|
51
55
|
* wire carried (the SSE id, the frame's `seq`) — `null` falls back to
|
|
52
56
|
* the data's own `seq`; `error`/`end` take the event data; `fail` takes
|
|
53
57
|
* a ready outcome (a transport failure the carrier classified). All are
|
|
54
|
-
* no-ops once finished.
|
|
58
|
+
* no-ops once finished. `lastSeq` reads the cursor: the resume seq the
|
|
59
|
+
* caller passed until a snapshot or patch moves it — what a further
|
|
60
|
+
* attempt resumes from.
|
|
55
61
|
* @param {StreamConsumerOptions} options
|
|
56
62
|
* @returns {{ snapshot: (seq: number | null, data: unknown) => void,
|
|
57
63
|
* patch: (seq: number | null, data: unknown) => void,
|
|
@@ -59,11 +65,13 @@ import { STREAM_ERRORS } from './sse.js';
|
|
|
59
65
|
* end: (data: unknown) => void,
|
|
60
66
|
* fail: (outcome: Outcome) => void,
|
|
61
67
|
* cancel: () => void,
|
|
62
|
-
* finished: () => boolean
|
|
68
|
+
* finished: () => boolean,
|
|
69
|
+
* lastSeq: () => number | null }}
|
|
63
70
|
*/
|
|
64
71
|
export function createStreamConsumer(options) {
|
|
65
72
|
const { route, catalog, meta, callbacks, finish } = options;
|
|
66
73
|
let lastSeq = options.lastSeq;
|
|
74
|
+
let delivered = false;
|
|
67
75
|
let done = false;
|
|
68
76
|
|
|
69
77
|
/**
|
|
@@ -135,13 +143,27 @@ export function createStreamConsumer(options) {
|
|
|
135
143
|
clientError(catalog, 'JC2053', { op: route.id }, null, projectValidationDetails(route.details, v.errors)), meta));
|
|
136
144
|
return;
|
|
137
145
|
}
|
|
138
|
-
|
|
139
|
-
|
|
146
|
+
const reset = envelope.reset === true;
|
|
147
|
+
// a mid-stream snapshot (a maxPatchBytes replacement) must still
|
|
148
|
+
// advance; a reset snapshot may land AT the resume cursor — the
|
|
149
|
+
// server's watermark had not moved past what the consumer held.
|
|
150
|
+
// A zero seed may replace the resume cursor only before this
|
|
151
|
+
// attempt delivers anything, when replay falls back to a fresh source.
|
|
152
|
+
const initialSeed = !delivered && at === 0;
|
|
153
|
+
if (at !== null && lastSeq !== null && !initialSeed && (reset ? at < lastSeq : at <= lastSeq)) {
|
|
140
154
|
if (terminate()) call(callbacks.onError, streamOutcome('JC2092', {}));
|
|
141
155
|
return;
|
|
142
156
|
}
|
|
143
157
|
if (at !== null) lastSeq = at;
|
|
144
|
-
|
|
158
|
+
delivered = true;
|
|
159
|
+
const watermark = (/** @type {unknown} */ v) => (typeof v === 'number' && Number.isFinite(v) ? v : null);
|
|
160
|
+
call(callbacks.onSnapshot, envelope.value, {
|
|
161
|
+
seq: at === null ? 0 : at,
|
|
162
|
+
resumed: envelope.resumed === true,
|
|
163
|
+
reset,
|
|
164
|
+
earliestAvailable: watermark(envelope.earliestAvailable),
|
|
165
|
+
highWatermark: watermark(envelope.highWatermark),
|
|
166
|
+
});
|
|
145
167
|
},
|
|
146
168
|
patch(seq, data) {
|
|
147
169
|
if (done) return;
|
|
@@ -158,6 +180,7 @@ export function createStreamConsumer(options) {
|
|
|
158
180
|
return;
|
|
159
181
|
}
|
|
160
182
|
lastSeq = at;
|
|
183
|
+
delivered = true;
|
|
161
184
|
call(callbacks.onPatch, { patch, seq: at });
|
|
162
185
|
},
|
|
163
186
|
error(data) {
|
|
@@ -175,6 +198,16 @@ export function createStreamConsumer(options) {
|
|
|
175
198
|
call(callbacks.onError, failedOutcome('failure', outcomeError(code, message, null, record.details, retryable), meta));
|
|
176
199
|
return;
|
|
177
200
|
}
|
|
201
|
+
// a stream code the server ends with that is a NETWORK verdict (the
|
|
202
|
+
// consumer fell behind, JC2096) is a network outcome under its own
|
|
203
|
+
// code — retryable, and what a reconnect policy keys on
|
|
204
|
+
if (code !== null && Object.hasOwn(STREAM_ERRORS, code)
|
|
205
|
+
&& STREAM_ERRORS[/** @type {keyof typeof STREAM_ERRORS} */ (code)].kind === 'network') {
|
|
206
|
+
const row = STREAM_ERRORS[/** @type {keyof typeof STREAM_ERRORS} */ (code)];
|
|
207
|
+
const message = typeof record.message === 'string' ? record.message : renderMessage(catalog, row.msgid, { op: route.id });
|
|
208
|
+
call(callbacks.onError, failedOutcome('network', outcomeError(code, message, null, record.details, row.retryable), meta));
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
178
211
|
// an undeclared server error ends the stream as a contract violation;
|
|
179
212
|
// the server's record rides in details so a JC2091 stays visible
|
|
180
213
|
const details = code === null ? null : {
|
|
@@ -206,6 +239,7 @@ export function createStreamConsumer(options) {
|
|
|
206
239
|
}
|
|
207
240
|
},
|
|
208
241
|
finished: () => done,
|
|
242
|
+
lastSeq: () => lastSeq,
|
|
209
243
|
};
|
|
210
244
|
}
|
|
211
245
|
|