@jarenjs/contract 0.56.0 → 0.67.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +139 -25
- 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/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 +580 -128
- 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/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
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The host lifecycle (docs/CONTRACT-FORMAT.md §7.7): one
|
|
3
|
+
* carrier-neutral coordinator every server binding runs a request
|
|
4
|
+
* through. A host names two hooks at construction — `identify(meta)`,
|
|
5
|
+
* before anything of the request is parsed, and `acquire(input,
|
|
6
|
+
* identity, enter)`, after the input validated and (on HTTP) the
|
|
7
|
+
* idempotency claim answered `new` — each answering a LEASE `{ host,
|
|
8
|
+
* release? }` the handler sees as `ctx.host`, or a declared failure the
|
|
9
|
+
* binding renders like the handler's own. `acquire` hands its lease to
|
|
10
|
+
* `enter`, the binding's continuation that runs the handler, validates
|
|
11
|
+
* the output, serializes the response and — when the lease carries
|
|
12
|
+
* `settlement: { ledger, required: true }` — settles the idempotency
|
|
13
|
+
* claim through that ledger BEFORE `enter` resolves, so a host that
|
|
14
|
+
* opened a transaction around `enter` commits the domain write and the
|
|
15
|
+
* receipt together or not at all. Releases run once each, acquired
|
|
16
|
+
* before identity, at the boundary the binding declares (the response
|
|
17
|
+
* exposed, the opaque body settled, the stream done).
|
|
18
|
+
*
|
|
19
|
+
* The defaults are exactly `identify → { host: null }` and `acquire →
|
|
20
|
+
* enter({ host: identity.host })`: a host that names only `identify`
|
|
21
|
+
* sees its host in `scope(ctx)` and in the handler; one that names
|
|
22
|
+
* `acquire` owns the handler's host. Every returned lease is validated
|
|
23
|
+
* by shape (an object with an own `host`); a hook that throws, answers
|
|
24
|
+
* a malformed lease, never calls `enter` or calls it twice is the host's
|
|
25
|
+
* fault — observed through the binding's observer and rendered as the
|
|
26
|
+
* binding's host-fault code (`JC2008` on HTTP, `JC2070` on port and
|
|
27
|
+
* local); a declared failure is recognized by the `ContractFailure`
|
|
28
|
+
* brand, never by shape, and validated against the operation like a
|
|
29
|
+
* handler's. No policy data of the hooks reaches a wire.
|
|
30
|
+
*/
|
|
31
|
+
import { ContractFailure } from './errors.js';
|
|
32
|
+
export type IdentifyMeta = {
|
|
33
|
+
op: import('./compile.js').CompiledOperation;
|
|
34
|
+
trace: string;
|
|
35
|
+
signal: AbortSignal | null;
|
|
36
|
+
carrier: 'http' | 'port' | 'local';
|
|
37
|
+
/**
|
|
38
|
+
* - the request line on HTTP; `null` elsewhere
|
|
39
|
+
*/
|
|
40
|
+
method: string | null;
|
|
41
|
+
path: string | null;
|
|
42
|
+
/**
|
|
43
|
+
* - the raw request headers on HTTP; `null` elsewhere
|
|
44
|
+
*/
|
|
45
|
+
headers: Readonly<Record<string, string | readonly string[]>> | null;
|
|
46
|
+
/**
|
|
47
|
+
* - the declared-failure factory
|
|
48
|
+
*/
|
|
49
|
+
fail: typeof ContractFailure;
|
|
50
|
+
};
|
|
51
|
+
export type Lease = {
|
|
52
|
+
host: unknown;
|
|
53
|
+
release: (() => unknown) | undefined;
|
|
54
|
+
settlement: {
|
|
55
|
+
ledger: import('./ledger.js').Ledger;
|
|
56
|
+
required: true;
|
|
57
|
+
} | null;
|
|
58
|
+
};
|
|
59
|
+
export type Lifecycle = {
|
|
60
|
+
identify: (meta: IdentifyMeta) => unknown;
|
|
61
|
+
acquire: (input: unknown, identity: {
|
|
62
|
+
host: unknown;
|
|
63
|
+
}, enter: (lease: unknown) => unknown) => unknown;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* A host fault of the lifecycle: what the binding observes and renders
|
|
67
|
+
* as its host-fault code.
|
|
68
|
+
*/
|
|
69
|
+
export declare class HostLifecycleError extends Error {
|
|
70
|
+
/**
|
|
71
|
+
* @param {string} message
|
|
72
|
+
* @param {unknown} [cause]
|
|
73
|
+
*/
|
|
74
|
+
constructor(message: string, cause?: unknown);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Validate the hooks a binding was given; each defaults. Rejections are
|
|
78
|
+
* the binding's `JC1001` through `host`.
|
|
79
|
+
* @param {{ identify?: unknown, acquire?: unknown }} options
|
|
80
|
+
* @param {(reason: string) => Error} host - the binding's construction refusal
|
|
81
|
+
* @returns {Lifecycle}
|
|
82
|
+
*/
|
|
83
|
+
export declare function resolveLifecycle(options: {
|
|
84
|
+
identify?: unknown;
|
|
85
|
+
acquire?: unknown;
|
|
86
|
+
}, host: (reason: string) => Error): Lifecycle;
|
|
87
|
+
/**
|
|
88
|
+
* Read a hook's answer as a lease: an object with an own `host`, an
|
|
89
|
+
* optional function `release`, and (when `acquired`) an optional
|
|
90
|
+
* `settlement` whose `ledger` has `commit`/`fail` and whose `required`
|
|
91
|
+
* is `true`. Anything else is the host's fault.
|
|
92
|
+
* @param {unknown} value
|
|
93
|
+
* @param {'identify' | 'acquire'} hook
|
|
94
|
+
* @returns {Lease}
|
|
95
|
+
* @throws {HostLifecycleError}
|
|
96
|
+
*/
|
|
97
|
+
export declare function leaseOf(value: unknown, hook: 'identify' | 'acquire'): Lease;
|
|
98
|
+
/**
|
|
99
|
+
* A release that runs at most once, whatever the number of exits that
|
|
100
|
+
* reach it; its own failure is handed to `observed` and swallowed, and
|
|
101
|
+
* the answer says whether the release was clean (`true`) — a later
|
|
102
|
+
* call answers `true` without running anything again.
|
|
103
|
+
* @param {(() => unknown) | undefined} release
|
|
104
|
+
* @param {(error: unknown) => void} observed
|
|
105
|
+
* @returns {() => Promise<boolean>}
|
|
106
|
+
*/
|
|
107
|
+
export declare function once(release: (() => unknown) | undefined, observed: (error: unknown) => void): () => Promise<boolean>;
|
|
108
|
+
/**
|
|
109
|
+
* Classify a hook's settled answer: a lease, a declared failure, or a
|
|
110
|
+
* host fault. Total for hostile values.
|
|
111
|
+
* @param {unknown} value
|
|
112
|
+
* @param {'identify' | 'acquire'} hook
|
|
113
|
+
* @returns {{ kind: 'lease', lease: Lease } | { kind: 'failure', failure: import('./errors.js').ContractFailureValue } | { kind: 'fault', cause: unknown }}
|
|
114
|
+
*/
|
|
115
|
+
export declare function classifyAnswer(value: unknown, hook: 'identify' | 'acquire'): {
|
|
116
|
+
kind: 'lease';
|
|
117
|
+
lease: Lease;
|
|
118
|
+
} | {
|
|
119
|
+
kind: 'failure';
|
|
120
|
+
failure: import('./errors.js').ContractFailureValue;
|
|
121
|
+
} | {
|
|
122
|
+
kind: 'fault';
|
|
123
|
+
cause: unknown;
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* Run `identify`: the hook's answer (sync or async) classified.
|
|
127
|
+
* @param {Lifecycle} lifecycle
|
|
128
|
+
* @param {IdentifyMeta} meta
|
|
129
|
+
* @returns {Promise<ReturnType<typeof classifyAnswer>> | ReturnType<typeof classifyAnswer>}
|
|
130
|
+
*/
|
|
131
|
+
export declare function identify(lifecycle: Lifecycle, meta: IdentifyMeta): Promise<ReturnType<typeof classifyAnswer>> | ReturnType<typeof classifyAnswer>;
|
|
132
|
+
/**
|
|
133
|
+
* The private carrier a required settlement's failure travels in: the
|
|
134
|
+
* wire fault the dispatcher intends is preserved while the host's
|
|
135
|
+
* transaction around `enter` rolls back on the rejection.
|
|
136
|
+
*/
|
|
137
|
+
export declare class RollbackCarrier extends Error {
|
|
138
|
+
response: unknown;
|
|
139
|
+
/**
|
|
140
|
+
* @param {unknown} response - the response the binding will answer
|
|
141
|
+
* @param {unknown} cause - what failed inside `enter`
|
|
142
|
+
*/
|
|
143
|
+
constructor(response: unknown, cause: unknown);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Run `acquire` around `enter`. `enter` receives the validated lease and
|
|
147
|
+
* answers the binding's result (a response, an outcome) or a promise of
|
|
148
|
+
* it; what the hook itself answers is the value of `enter` — a hook
|
|
149
|
+
* that resolves before `enter` settled, or to something else, is a
|
|
150
|
+
* fault. The result: `{ kind: 'entered', lease, result }` when `enter`
|
|
151
|
+
* ran and settled (its rejection, when it is not a RollbackCarrier of
|
|
152
|
+
* ours, is a fault of the binding's continuation); `{ kind: 'failure' }`
|
|
153
|
+
* for a declared failure answered instead of entering; `{ kind:
|
|
154
|
+
* 'fault' }` for everything else — `enter` never called, called twice,
|
|
155
|
+
* a malformed lease, a throw.
|
|
156
|
+
* @param {Lifecycle} lifecycle
|
|
157
|
+
* @param {unknown} input
|
|
158
|
+
* @param {{ host: unknown }} identity
|
|
159
|
+
* @param {(lease: Lease) => unknown} enter
|
|
160
|
+
* @returns {Promise<{ kind: 'entered', lease: Lease, result: unknown, rolledBack: RollbackCarrier | null, afterFault?: unknown }
|
|
161
|
+
* | { kind: 'failure', failure: import('./errors.js').ContractFailureValue }
|
|
162
|
+
* | { kind: 'fault', cause: unknown }>}
|
|
163
|
+
*/
|
|
164
|
+
export declare function acquire(lifecycle: Lifecycle, input: unknown, identity: {
|
|
165
|
+
host: unknown;
|
|
166
|
+
}, enter: (lease: Lease) => unknown): Promise<{
|
|
167
|
+
kind: 'entered';
|
|
168
|
+
lease: Lease;
|
|
169
|
+
result: unknown;
|
|
170
|
+
rolledBack: RollbackCarrier | null;
|
|
171
|
+
afterFault?: unknown;
|
|
172
|
+
} | {
|
|
173
|
+
kind: 'failure';
|
|
174
|
+
failure: import('./errors.js').ContractFailureValue;
|
|
175
|
+
} | {
|
|
176
|
+
kind: 'fault';
|
|
177
|
+
cause: unknown;
|
|
178
|
+
}>;
|
|
179
|
+
export { ContractFailure };
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Byte bodies of the HTTP binding, carrier-neutral: the one
|
|
3
|
+
* normalizer that turns whatever an adapter or a handler hands over (a
|
|
4
|
+
* string, bytes, an async iterable of chunks, a Web `ReadableStream`)
|
|
5
|
+
* into the shape the pipeline reads; the bounded collector a JSON
|
|
6
|
+
* operation drains its source through (a JSON body must be parsed and
|
|
7
|
+
* validated whole, so it materializes — under the operation's limit
|
|
8
|
+
* and never past it); and the counting source an opaque handler
|
|
9
|
+
* receives, which never yields a byte past the limit and cancels its
|
|
10
|
+
* upstream exactly once. Nothing here keeps a chunk it has handed on:
|
|
11
|
+
* the opaque path holds one chunk at a time, and the JSON path holds
|
|
12
|
+
* at most `maxBodyBytes`.
|
|
13
|
+
*/
|
|
14
|
+
export type Body = string | Uint8Array | AsyncIterable<Uint8Array> | null;
|
|
15
|
+
/**
|
|
16
|
+
* A body as the pipeline reads it: text, bytes, a pull source of byte
|
|
17
|
+
* chunks, or none.
|
|
18
|
+
* @typedef {string | Uint8Array | AsyncIterable<Uint8Array> | null} Body
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* The limit crossing a counting source raises to whoever is pulling
|
|
22
|
+
* it: recognized by class, so a handler that lets it propagate answers
|
|
23
|
+
* `JC2003` rather than a host fault, and a handler that catches it
|
|
24
|
+
* decides for itself.
|
|
25
|
+
*/
|
|
26
|
+
export declare class BodyLimitError extends Error {
|
|
27
|
+
/** @type {number} */
|
|
28
|
+
limit: number;
|
|
29
|
+
/** @param {number} limit */
|
|
30
|
+
constructor(limit: number);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Whether a value is an async iterable — the pull shape of a byte source.
|
|
34
|
+
* @param {unknown} value
|
|
35
|
+
* @returns {value is AsyncIterable<Uint8Array>}
|
|
36
|
+
*/
|
|
37
|
+
export declare function isAsyncByteSource(value: unknown): value is AsyncIterable<Uint8Array>;
|
|
38
|
+
/**
|
|
39
|
+
* Whether a value is a Web `ReadableStream` (by its reader, the one
|
|
40
|
+
* member every platform's stream has).
|
|
41
|
+
* @param {unknown} value
|
|
42
|
+
* @returns {value is ReadableStream<Uint8Array>}
|
|
43
|
+
*/
|
|
44
|
+
export declare function isReadableStream(value: unknown): value is ReadableStream<Uint8Array>;
|
|
45
|
+
/**
|
|
46
|
+
* Normalize a body: `undefined`/`null` → `null`; a string or bytes
|
|
47
|
+
* pass; a Web stream becomes an async iterable over its reader; an
|
|
48
|
+
* async iterable passes. Anything else answers `undefined` — not a
|
|
49
|
+
* body, for the caller to refuse.
|
|
50
|
+
* @param {unknown} body
|
|
51
|
+
* @returns {Body | undefined}
|
|
52
|
+
*/
|
|
53
|
+
export declare function normalizeBody(body: unknown): Body | undefined;
|
|
54
|
+
export type Collected = {
|
|
55
|
+
ok: true;
|
|
56
|
+
bytes: Uint8Array;
|
|
57
|
+
} | {
|
|
58
|
+
ok: false;
|
|
59
|
+
kind: 'limit' | 'aborted';
|
|
60
|
+
} | {
|
|
61
|
+
ok: false;
|
|
62
|
+
kind: 'error';
|
|
63
|
+
cause: unknown;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* What the collector answers: the whole body, or why it stopped —
|
|
67
|
+
* `limit` (the read crossed `limit`; the crossing chunk was never
|
|
68
|
+
* retained), `aborted` (the signal fired between pulls), `error` (the
|
|
69
|
+
* source threw, or yielded a non-byte chunk). In every failed case the
|
|
70
|
+
* upstream iterator's `return()` ran exactly once.
|
|
71
|
+
* @typedef {{ ok: true, bytes: Uint8Array }
|
|
72
|
+
* | { ok: false, kind: 'limit' | 'aborted' }
|
|
73
|
+
* | { ok: false, kind: 'error', cause: unknown }} Collected
|
|
74
|
+
*/
|
|
75
|
+
/**
|
|
76
|
+
* Drain a byte source into one `Uint8Array` under a limit: at most
|
|
77
|
+
* `limit` bytes are ever held; the chunk that would cross it is not
|
|
78
|
+
* retained, the upstream is cancelled once, and the crossing is
|
|
79
|
+
* reported.
|
|
80
|
+
* @param {AsyncIterable<Uint8Array>} source
|
|
81
|
+
* @param {number} limit - inclusive
|
|
82
|
+
* @param {AbortSignal | null} signal - checked between pulls
|
|
83
|
+
* @returns {Promise<Collected>}
|
|
84
|
+
*/
|
|
85
|
+
export declare function collectBytes(source: AsyncIterable<Uint8Array>, limit: number, signal: AbortSignal | null): Promise<Collected>;
|
|
86
|
+
export type SourceState = {
|
|
87
|
+
/**
|
|
88
|
+
* - a chunk was pulled
|
|
89
|
+
*/
|
|
90
|
+
started: boolean;
|
|
91
|
+
/**
|
|
92
|
+
* - EOF was reached, or the source was cancelled
|
|
93
|
+
*/
|
|
94
|
+
finished: boolean;
|
|
95
|
+
/**
|
|
96
|
+
* - `return()` ran (a crossing, a consumer's return, the binding's cancel)
|
|
97
|
+
*/
|
|
98
|
+
cancelled: boolean;
|
|
99
|
+
/**
|
|
100
|
+
* - the limit was crossed
|
|
101
|
+
*/
|
|
102
|
+
crossed: boolean;
|
|
103
|
+
/**
|
|
104
|
+
* - the bytes yielded so far
|
|
105
|
+
*/
|
|
106
|
+
bytes: number;
|
|
107
|
+
};
|
|
108
|
+
export type CountingSource = AsyncIterable<Uint8Array> & {
|
|
109
|
+
cancel: () => Promise<void>;
|
|
110
|
+
state: SourceState;
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* The state of a counting source, readable by the binding that made it.
|
|
114
|
+
* @typedef {Object} SourceState
|
|
115
|
+
* @property {boolean} started - a chunk was pulled
|
|
116
|
+
* @property {boolean} finished - EOF was reached, or the source was cancelled
|
|
117
|
+
* @property {boolean} cancelled - `return()` ran (a crossing, a consumer's return, the binding's cancel)
|
|
118
|
+
* @property {boolean} crossed - the limit was crossed
|
|
119
|
+
* @property {number} bytes - the bytes yielded so far
|
|
120
|
+
*/
|
|
121
|
+
/**
|
|
122
|
+
* A counting source over an upstream: what an opaque handler receives.
|
|
123
|
+
* @typedef {AsyncIterable<Uint8Array> & { cancel: () => Promise<void>, state: SourceState }} CountingSource
|
|
124
|
+
*/
|
|
125
|
+
/**
|
|
126
|
+
* Wrap an upstream source so it never yields a byte past `limit`: the
|
|
127
|
+
* chunk that would cross it is not yielded — the upstream is cancelled
|
|
128
|
+
* once and a `BodyLimitError` is thrown to the puller. `return()` (a
|
|
129
|
+
* consumer that stops early) and `cancel()` (the binding, when a
|
|
130
|
+
* response goes out with the request unread) both cancel the upstream
|
|
131
|
+
* exactly once; `state` says what happened.
|
|
132
|
+
* @param {AsyncIterable<Uint8Array>} upstream
|
|
133
|
+
* @param {number} limit
|
|
134
|
+
* @returns {CountingSource}
|
|
135
|
+
*/
|
|
136
|
+
export declare function countingSource(upstream: AsyncIterable<Uint8Array>, limit: number): CountingSource;
|
|
137
|
+
/**
|
|
138
|
+
* Run `after` once when a response source completes, throws or is
|
|
139
|
+
* cancelled by its consumer — the hook a binding uses to release what
|
|
140
|
+
* the response held (an unread request source). A throw from the
|
|
141
|
+
* source reaches the consumer after the hook; the hook's own answer
|
|
142
|
+
* is awaited but never replaces the source's outcome.
|
|
143
|
+
* @param {AsyncIterable<Uint8Array>} source
|
|
144
|
+
* @param {(cause: unknown) => unknown} after - `cause` is the throw, or `undefined` on EOF/return
|
|
145
|
+
* @returns {AsyncIterable<Uint8Array>}
|
|
146
|
+
*/
|
|
147
|
+
export declare function onSettled(source: AsyncIterable<Uint8Array>, after: (cause: unknown) => unknown): AsyncIterable<Uint8Array>;
|
|
@@ -30,11 +30,19 @@ export type Ledger = import('../ledger.js').Ledger;
|
|
|
30
30
|
export type RequestContext = {
|
|
31
31
|
op: CompiledOperation;
|
|
32
32
|
trace: string;
|
|
33
|
+
/**
|
|
34
|
+
* - the binding this context comes from
|
|
35
|
+
*/
|
|
36
|
+
carrier: 'http';
|
|
37
|
+
/**
|
|
38
|
+
* - the identity's host until `acquire` entered; the acquired host in the handler's context (§7.7) — `any`, so a table typed by the projection's `HandlerContext<Host>` is a `Handler`
|
|
39
|
+
*/
|
|
40
|
+
host: any;
|
|
33
41
|
method: string;
|
|
34
42
|
path: string;
|
|
35
43
|
params: Readonly<Record<string, string>>;
|
|
36
44
|
headers: Readonly<Record<string, string>>;
|
|
37
|
-
body: string | Uint8Array | null;
|
|
45
|
+
body: string | Uint8Array | AsyncIterable<Uint8Array> | null;
|
|
38
46
|
signal: AbortSignal | null;
|
|
39
47
|
idempotency: Readonly<{
|
|
40
48
|
key: string;
|
|
@@ -52,7 +60,7 @@ export type Handler = (input: any, ctx: RequestContext) => unknown;
|
|
|
52
60
|
export type RawResponse = {
|
|
53
61
|
status: number;
|
|
54
62
|
headers?: Record<string, string>;
|
|
55
|
-
body?: string | Uint8Array | null;
|
|
63
|
+
body?: string | Uint8Array | AsyncIterable<Uint8Array> | ReadableStream<Uint8Array> | null;
|
|
56
64
|
};
|
|
57
65
|
export type TagResolver = (input: any, ctx: RequestContext) => string | {
|
|
58
66
|
tag: string;
|
|
@@ -142,6 +150,21 @@ export type Server = {
|
|
|
142
150
|
* streams' stoppers; the dispatcher's `close()` ends them all
|
|
143
151
|
*/
|
|
144
152
|
streams: Set<(reason: string | null) => void>;
|
|
153
|
+
/**
|
|
154
|
+
* - the bounds of every SSE stream
|
|
155
|
+
*/
|
|
156
|
+
streamLimits: import('../stream/server.js').StreamLimits;
|
|
157
|
+
/**
|
|
158
|
+
* - the host lifecycle hooks (§7.7)
|
|
159
|
+
*/
|
|
160
|
+
lifecycle: import('../host.js').Lifecycle;
|
|
161
|
+
};
|
|
162
|
+
export type Life = {
|
|
163
|
+
op: string;
|
|
164
|
+
trace: string;
|
|
165
|
+
identity: () => Promise<boolean>;
|
|
166
|
+
acquired: (() => Promise<boolean>) | null;
|
|
167
|
+
deferred: boolean;
|
|
145
168
|
};
|
|
146
169
|
/**
|
|
147
170
|
* The one entry point. Validates the request object (a malformed one is
|
|
@@ -160,4 +183,5 @@ export type Armed = {
|
|
|
160
183
|
outcome: number;
|
|
161
184
|
retryable: boolean;
|
|
162
185
|
decided: boolean;
|
|
186
|
+
settled: boolean;
|
|
163
187
|
};
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
* decided here, once, into a `Route` per operation.
|
|
16
16
|
*/
|
|
17
17
|
import { HTTP_ERRORS, WELL_KNOWN_PATH } from './wire.js';
|
|
18
|
-
|
|
18
|
+
import { BodyLimitError } from './body.js';
|
|
19
|
+
export { HTTP_ERRORS, WELL_KNOWN_PATH, BodyLimitError };
|
|
19
20
|
export type HttpRequest = import('./wire.js').HttpRequest;
|
|
20
21
|
export type HttpResponse = import('./wire.js').HttpResponse;
|
|
21
22
|
export type WireErrorBody = import('./wire.js').WireErrorBody;
|
|
@@ -29,7 +30,8 @@ export type Contract = import('../compile.js').Contract;
|
|
|
29
30
|
export type CompiledOperation = import('../compile.js').CompiledOperation;
|
|
30
31
|
export type ServeHttpOptions = {
|
|
31
32
|
/**
|
|
32
|
-
* - the server trace generator; default
|
|
33
|
+
* - the server trace generator; default
|
|
34
|
+
* the runtime record's `uuid`, itself `crypto.randomUUID` by default
|
|
33
35
|
*/
|
|
34
36
|
trace?: () => string;
|
|
35
37
|
/**
|
|
@@ -85,9 +87,50 @@ export type ServeHttpOptions = {
|
|
|
85
87
|
*/
|
|
86
88
|
catalog?: Record<string, string | ((params: object) => string)>;
|
|
87
89
|
/**
|
|
88
|
-
* - the clock stamped into ledger claims;
|
|
90
|
+
* - the clock stamped into ledger claims;
|
|
91
|
+
* default the runtime record's `now`, itself `Date.now` by default
|
|
89
92
|
*/
|
|
90
93
|
now?: () => number;
|
|
94
|
+
/**
|
|
95
|
+
* - the host's runtime record: its `uuid` generates the server trace
|
|
96
|
+
* and its `now` is the clock, each only where `trace` / `now` is absent
|
|
97
|
+
*/
|
|
98
|
+
runtime?: Partial<import('@jarenjs/core/runtime').Runtime>;
|
|
99
|
+
/**
|
|
100
|
+
* - the bounds of every SSE stream (docs/CONTRACT-FORMAT.md §18.1):
|
|
101
|
+
* a replay page asks for at most `replay.limit` emissions / `replay.
|
|
102
|
+
* maxBytes` patch bytes (default 256 / 1 MiB); the undelivered queue
|
|
103
|
+
* holds at most `queue.events` frames / `queue.bytes` SSE bytes
|
|
104
|
+
* (default 256 / 1 MiB) before the stream ends with `JC2096`
|
|
105
|
+
*/
|
|
106
|
+
streamLimits?: {
|
|
107
|
+
replay?: {
|
|
108
|
+
limit?: number;
|
|
109
|
+
maxBytes?: number;
|
|
110
|
+
};
|
|
111
|
+
queue?: {
|
|
112
|
+
events?: number;
|
|
113
|
+
bytes?: number;
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
* - the host lifecycle's first hook (docs/CONTRACT-FORMAT.md §7.7):
|
|
118
|
+
* runs after the route resolved and before any byte of the body is
|
|
119
|
+
* read; answers a lease `{ host, release? }` — `host` is what `scope`
|
|
120
|
+
* and, without `acquire`, the handler see as `ctx.host` — or a
|
|
121
|
+
* declared failure (`meta.fail`); default `{ host: null }`
|
|
122
|
+
*/
|
|
123
|
+
identify?: (meta: import('../host.js').IdentifyMeta) => unknown;
|
|
124
|
+
/**
|
|
125
|
+
* - the second hook: runs after the input validated and, on a claimed
|
|
126
|
+
* command, after the claim answered `new`; calls `enter({ host,
|
|
127
|
+
* release?, settlement? })` exactly once and answers what `enter`
|
|
128
|
+
* answers — a host that opens a transaction around `enter` commits it
|
|
129
|
+
* when `enter` resolves and rolls it back when it rejects; `settlement:
|
|
130
|
+
* { ledger, required: true }` records the claim through that ledger
|
|
131
|
+
* inside `enter`; default `enter({ host: identity.host })`
|
|
132
|
+
*/
|
|
133
|
+
acquire?: (input: unknown, identity: RequestContext, enter: (lease: unknown) => Promise<unknown>) => unknown;
|
|
91
134
|
};
|
|
92
135
|
export type HttpCapabilities = {
|
|
93
136
|
name: 'http';
|
|
@@ -17,17 +17,17 @@ export type HttpRequest = {
|
|
|
17
17
|
method: string;
|
|
18
18
|
url: string;
|
|
19
19
|
headers: Readonly<Record<string, string | readonly string[]>>;
|
|
20
|
-
body: string | Uint8Array | null;
|
|
20
|
+
body: string | Uint8Array | AsyncIterable<Uint8Array> | ReadableStream<Uint8Array> | null;
|
|
21
21
|
signal?: AbortSignal | null;
|
|
22
22
|
};
|
|
23
23
|
export type HttpResponse = {
|
|
24
24
|
status: number;
|
|
25
25
|
headers: Readonly<Record<string, string>>;
|
|
26
|
-
body: string | Uint8Array | null;
|
|
27
|
-
stream?: (sink: {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
26
|
+
body: string | Uint8Array | AsyncIterable<Uint8Array> | null;
|
|
27
|
+
stream?: (sink: import('@jarenjs/core/async').SinkLike<string>) => {
|
|
28
|
+
stop: () => void;
|
|
29
|
+
done: Promise<void>;
|
|
30
|
+
};
|
|
31
31
|
};
|
|
32
32
|
export type WireErrorBody = {
|
|
33
33
|
/**
|
|
@@ -52,29 +52,43 @@ export type WireErrorRow = {
|
|
|
52
52
|
* test hands to `dispatch` directly. `url` is origin-less: the path plus
|
|
53
53
|
* an optional `?query`; header names are lowercase; a header value is a
|
|
54
54
|
* string, or an array of strings when the adapter can see repeated field
|
|
55
|
-
* lines; `body` is the
|
|
56
|
-
*
|
|
55
|
+
* lines; `body` is the text or bytes as received, or a pull source of
|
|
56
|
+
* chunks (an async iterable, or a Web `ReadableStream` the dispatcher
|
|
57
|
+
* normalizes) the pipeline drains under the operation's limit for a
|
|
58
|
+
* JSON operation and hands an opaque handler one chunk at a time
|
|
59
|
+
* (`null` for none); `signal` is the request's abort signal when the
|
|
60
|
+
* host has one.
|
|
57
61
|
* @typedef {Object} HttpRequest
|
|
58
62
|
* @property {string} method
|
|
59
63
|
* @property {string} url
|
|
60
64
|
* @property {Readonly<Record<string, string | readonly string[]>>} headers
|
|
61
|
-
* @property {string | Uint8Array | null} body
|
|
65
|
+
* @property {string | Uint8Array | AsyncIterable<Uint8Array> | ReadableStream<Uint8Array> | null} body
|
|
62
66
|
* @property {AbortSignal | null} [signal]
|
|
63
67
|
*/
|
|
64
68
|
/**
|
|
65
69
|
* A response as the binding answers it: a status, lowercase header names,
|
|
66
|
-
* and a body that is a string (JSON text), bytes (an opaque operation)
|
|
67
|
-
*
|
|
70
|
+
* and a body that is a string (JSON text), bytes (an opaque operation), a
|
|
71
|
+
* pull source of chunks (an opaque handler's streamed body: the adapter
|
|
72
|
+
* writes it chunk by chunk behind the socket's backpressure and cancels
|
|
73
|
+
* it once when the peer goes away) or `null` (HEAD, 204, 304). A
|
|
74
|
+
* streaming response (a subscribe operation
|
|
68
75
|
* under `accept: text/event-stream`) carries `body: null` plus `stream`:
|
|
69
76
|
* the adapter writes the headers, then MUST call `stream` exactly once
|
|
70
77
|
* with its sink — the pump writes SSE text through `sink.write` and
|
|
71
|
-
* calls `sink.end()` when the stream terminates
|
|
72
|
-
*
|
|
78
|
+
* calls `sink.end()` when the stream terminates. The sink is a
|
|
79
|
+
* `SinkLike<string>` (`@jarenjs/core/async`): `write` may answer a
|
|
80
|
+
* promise that settles when the platform has taken the chunk — a Node
|
|
81
|
+
* response that answered `false` resolves on `drain`; a Web stream
|
|
82
|
+
* bridge resolves on the consumer's pull — and the pump writes the next
|
|
83
|
+
* chunk only after that; the optional `abort(reason)` is how the pump
|
|
84
|
+
* tears the carrier down when it must. The pump answers `{ stop, done }`:
|
|
85
|
+
* `stop()` ends the stream when the consumer cancels, `done` settles
|
|
86
|
+
* once the subscription is released and the sink has ended.
|
|
73
87
|
* @typedef {Object} HttpResponse
|
|
74
88
|
* @property {number} status
|
|
75
89
|
* @property {Readonly<Record<string, string>>} headers
|
|
76
|
-
* @property {string | Uint8Array | null} body
|
|
77
|
-
* @property {(sink:
|
|
90
|
+
* @property {string | Uint8Array | AsyncIterable<Uint8Array> | null} body
|
|
91
|
+
* @property {(sink: import('@jarenjs/core/async').SinkLike<string>) => { stop: () => void, done: Promise<void> }} [stream]
|
|
78
92
|
*/
|
|
79
93
|
/**
|
|
80
94
|
* The D7 error body of every non-2xx JSON response.
|
|
@@ -245,8 +259,8 @@ export declare function mediaMatches(contentType: string | undefined, media: str
|
|
|
245
259
|
*/
|
|
246
260
|
export declare function exceedsBytes(body: string | Uint8Array, limit: number): boolean;
|
|
247
261
|
/**
|
|
248
|
-
* Parse an `If-Match`/`If-None-Match` field into its opaque tags. `*`
|
|
249
|
-
*
|
|
262
|
+
* Parse an `If-Match`/`If-None-Match` field into its opaque tags. `*` sets
|
|
263
|
+
* `any`; commas inside quoted tags are retained. A weak indicator is dropped
|
|
250
264
|
* (`W/"x"` → `x`) — the caller decides weak/strong comparison because a
|
|
251
265
|
* strong comparison must reject weak tags, which `weak[i]` records.
|
|
252
266
|
* @param {string} value
|
package/dist/types/ledger.d.ts
CHANGED
|
@@ -16,9 +16,16 @@
|
|
|
16
16
|
*/
|
|
17
17
|
export type LedgerRecord = {
|
|
18
18
|
/**
|
|
19
|
-
* -
|
|
19
|
+
* - {@link ledgerId} of the tuple
|
|
20
20
|
*/
|
|
21
21
|
id: string;
|
|
22
|
+
/**
|
|
23
|
+
* - the identity of the claim that started
|
|
24
|
+
* this record: minted per `started` record, carried by the `ref`, and
|
|
25
|
+
* verified by `commit`/`fail` — a ref of an earlier generation settles
|
|
26
|
+
* nothing (`JC1011`)
|
|
27
|
+
*/
|
|
28
|
+
generation: string;
|
|
22
29
|
op: string;
|
|
23
30
|
scope: string;
|
|
24
31
|
key: string;
|
|
@@ -48,9 +55,13 @@ export type LedgerRecord = {
|
|
|
48
55
|
*/
|
|
49
56
|
expiresAt: number;
|
|
50
57
|
};
|
|
58
|
+
export type LedgerRef = {
|
|
59
|
+
readonly id: string;
|
|
60
|
+
readonly generation: string;
|
|
61
|
+
};
|
|
51
62
|
export type ClaimResult = {
|
|
52
63
|
state: 'new';
|
|
53
|
-
ref:
|
|
64
|
+
ref: LedgerRef;
|
|
54
65
|
} | {
|
|
55
66
|
state: 'replay';
|
|
56
67
|
response: any;
|
|
@@ -67,36 +78,66 @@ export type Ledger = {
|
|
|
67
78
|
hash: string;
|
|
68
79
|
now?: number;
|
|
69
80
|
}) => ClaimResult | Promise<ClaimResult>;
|
|
70
|
-
commit: (ref: unknown, response: any) => void | Promise<void>;
|
|
71
|
-
fail: (ref: unknown, retryable: boolean, response?: any) => void | Promise<void>;
|
|
81
|
+
commit: (ref: unknown, response: any, now?: number) => void | Promise<void>;
|
|
82
|
+
fail: (ref: unknown, retryable: boolean, response?: any, now?: number) => void | Promise<void>;
|
|
72
83
|
lookup: (key: {
|
|
73
84
|
op: string;
|
|
74
85
|
scope: string;
|
|
75
86
|
key: string;
|
|
87
|
+
now?: number;
|
|
76
88
|
}) => LedgerRecord | null | Promise<LedgerRecord | null>;
|
|
77
89
|
};
|
|
90
|
+
/**
|
|
91
|
+
* The id of one `(op, scope, key)` tuple: the version `1`, a colon, the
|
|
92
|
+
* JSON array of the three. Injective — a `|`, a control character or
|
|
93
|
+
* any Unicode inside a member cannot spell another tuple — and readable
|
|
94
|
+
* in a store. A record written under the legacy `"<op>|<scope>|<key>"`
|
|
95
|
+
* spelling is matched by no claim again: it expires by its own
|
|
96
|
+
* `expiresAt` (`sweep`), or a host rewrites its `id` once
|
|
97
|
+
* (docs/CONTRACT-FORMAT.md §8).
|
|
98
|
+
* @param {string} op
|
|
99
|
+
* @param {string} scope
|
|
100
|
+
* @param {string} key
|
|
101
|
+
* @returns {string}
|
|
102
|
+
*/
|
|
103
|
+
export declare function ledgerId(op: string, scope: string, key: string): string;
|
|
78
104
|
/**
|
|
79
105
|
* The reference ledger over a `Map`: synchronous, single-process,
|
|
80
106
|
* expiring on `claim` (a record past `expiresAt` is dropped and the key
|
|
81
107
|
* is `new` again). `sweep()` drops every expired record — a host may
|
|
82
108
|
* call it on a timer.
|
|
83
|
-
*
|
|
84
|
-
*
|
|
109
|
+
* ONE clock judges a record from claim to expiry. The ledger's own clock
|
|
110
|
+
* is `now`, else the runtime record's `now` (`@jarenjs/core/runtime`);
|
|
111
|
+
* given neither, the ledger has no clock of its own and FOLLOWS the
|
|
112
|
+
* binding: every stamp and every expiry decision uses the instant the
|
|
113
|
+
* binding passed with the call, and a host-side `lookup`/`sweep` that
|
|
114
|
+
* passes none uses the latest instant a binding reported. (A record
|
|
115
|
+
* stamped by an injected server clock and judged by the platform's was
|
|
116
|
+
* dropped as expired the moment `sweep()` ran, and the command ran
|
|
117
|
+
* twice.) A ledger WITH its own clock uses it for everything and is
|
|
118
|
+
* given the same record as the binding, never a second one. The runtime
|
|
119
|
+
* record's `uuid` mints each record's `generation`; `lookup` answers a
|
|
120
|
+
* copy, never the ledger's own record.
|
|
121
|
+
* @param {{ ttlMs?: number, now?: () => number,
|
|
122
|
+
* runtime?: Partial<import('@jarenjs/core/runtime').Runtime> }} [options]
|
|
123
|
+
* @returns {Ledger & { sweep(now?: number): number, size: number }}
|
|
85
124
|
*/
|
|
86
125
|
export declare function createMemoryLedger(options?: {
|
|
87
126
|
ttlMs?: number;
|
|
88
127
|
now?: () => number;
|
|
128
|
+
runtime?: Partial<import('@jarenjs/core/runtime').Runtime>;
|
|
89
129
|
}): Ledger & {
|
|
90
|
-
sweep(): number;
|
|
130
|
+
sweep(now?: number): number;
|
|
91
131
|
size: number;
|
|
92
132
|
};
|
|
93
133
|
/**
|
|
94
134
|
* The `$model` 0.1 document of a durable ledger: one collection,
|
|
95
|
-
* `ledger`, keyed by `/id` (
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
135
|
+
* `ledger`, keyed by `/id` ({@link ledgerId}), indexed on `expiresAt`
|
|
136
|
+
* (the sweep) and `status` (the in-flight scan). A host opens it with
|
|
137
|
+
* `@jarenjs/db`'s `openStore` and implements the `Ledger` interface over
|
|
138
|
+
* the collection — `createDbLedger` in `@jarenjs/linq/db` is that
|
|
139
|
+
* implementation over the typed client; the record shape is exactly
|
|
140
|
+
* what `createMemoryLedger` keeps, the `generation` included.
|
|
100
141
|
*/
|
|
101
142
|
export declare const idempotencyLedgerModel: Readonly<{
|
|
102
143
|
$model: "0.1";
|
|
@@ -110,6 +151,10 @@ export declare const idempotencyLedgerModel: Readonly<{
|
|
|
110
151
|
type: string;
|
|
111
152
|
minLength: number;
|
|
112
153
|
};
|
|
154
|
+
generation: {
|
|
155
|
+
type: string;
|
|
156
|
+
minLength: number;
|
|
157
|
+
};
|
|
113
158
|
op: {
|
|
114
159
|
type: string;
|
|
115
160
|
minLength: number;
|