@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
@@ -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
- export { HTTP_ERRORS, WELL_KNOWN_PATH };
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 `crypto.randomUUID`
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; default `Date.now`
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
- write: (chunk: string) => void;
29
- end: () => void;
30
- }) => (() => void);
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 bytes/text as received (`null` for none);
56
- * `signal` is the request's abort signal when the host has one.
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) or
67
- * `null` (HEAD, 204, 304). A streaming response (a subscribe operation
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; the returned function
72
- * stops the stream when the consumer cancels.
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: { write: (chunk: string) => void, end: () => void }) => (() => void)} [stream]
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. `*` is
249
- * reported as `null` in the list; a weak indicator is dropped
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
@@ -16,9 +16,16 @@
16
16
  */
17
17
  export type LedgerRecord = {
18
18
  /**
19
- * - `"<op>|<scope>|<key>"`
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: unknown;
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
- * @param {{ ttlMs?: number, now?: () => number }} [options]
84
- * @returns {Ledger & { sweep(): number, size: number }}
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` (`"<op>|<scope>|<key>"`), indexed on
96
- * `expiresAt` (the sweep) and `status` (the in-flight scan). A host
97
- * opens it with `@jarenjs/db`'s `openStore` and implements the `Ledger`
98
- * interface over the collection; the record shape is exactly what
99
- * `createMemoryLedger` keeps.
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;