@jarenjs/contract 0.43.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 +508 -0
- package/dist/types/adapters/fetch.d.ts +27 -0
- package/dist/types/adapters/node.d.ts +47 -0
- package/dist/types/app/binding.d.ts +122 -0
- package/dist/types/app/effect.d.ts +77 -0
- package/dist/types/app/index.d.ts +31 -0
- package/dist/types/app/subscription.d.ts +82 -0
- package/dist/types/bundle.d.ts +43 -0
- package/dist/types/cli.d.ts +15 -0
- package/dist/types/client/http.d.ts +242 -0
- package/dist/types/client/outcome.d.ts +289 -0
- package/dist/types/compat.d.ts +36 -0
- package/dist/types/compile.d.ts +196 -0
- package/dist/types/describe.d.ts +115 -0
- package/dist/types/diff.d.ts +91 -0
- package/dist/types/errors.d.ts +205 -0
- package/dist/types/http/dispatch.d.ts +148 -0
- package/dist/types/http/serve.d.ts +154 -0
- package/dist/types/http/wire.d.ts +334 -0
- package/dist/types/index.d.ts +39 -0
- package/dist/types/ledger.d.ts +207 -0
- package/dist/types/local/index.d.ts +127 -0
- package/dist/types/messages.d.ts +63 -0
- package/dist/types/path.d.ts +119 -0
- package/dist/types/pipeline.d.ts +157 -0
- package/dist/types/port/client.d.ts +142 -0
- package/dist/types/port/frame.d.ts +195 -0
- package/dist/types/port/serve.d.ts +102 -0
- package/dist/types/project/index.d.ts +34 -0
- package/dist/types/project/markdown.d.ts +28 -0
- package/dist/types/project/openapi.d.ts +102 -0
- package/dist/types/project/tools.d.ts +57 -0
- package/dist/types/project/typescript.d.ts +59 -0
- package/dist/types/public.d.ts +73 -0
- package/dist/types/revision.d.ts +36 -0
- package/dist/types/stream/client.d.ts +104 -0
- package/dist/types/stream/server.d.ts +106 -0
- package/dist/types/stream/sse.d.ts +62 -0
- package/docs/APP-INTEGRATION.md +301 -0
- package/docs/CONTRACT-FORMAT.md +1923 -0
- package/package.json +110 -0
- package/schemas/jaren-contract-port.draft-07.schema.json +241 -0
- package/schemas/jaren-contract-port.schema.json +241 -0
- package/schemas/jaren-contract.draft-07.schema.json +287 -0
- package/schemas/jaren-contract.schema.json +287 -0
- package/src/adapters/fetch.js +109 -0
- package/src/adapters/node.js +238 -0
- package/src/app/binding.js +426 -0
- package/src/app/effect.js +190 -0
- package/src/app/index.js +26 -0
- package/src/app/subscription.js +130 -0
- package/src/bundle.js +168 -0
- package/src/cli.js +264 -0
- package/src/client/http.js +1150 -0
- package/src/client/outcome.js +364 -0
- package/src/compat.js +62 -0
- package/src/compile.js +1162 -0
- package/src/describe.js +109 -0
- package/src/diff.js +610 -0
- package/src/errors.js +236 -0
- package/src/http/dispatch.js +1054 -0
- package/src/http/serve.js +301 -0
- package/src/http/wire.js +469 -0
- package/src/index.js +33 -0
- package/src/ledger.js +225 -0
- package/src/local/index.js +363 -0
- package/src/messages.js +68 -0
- package/src/path.js +471 -0
- package/src/pipeline.js +241 -0
- package/src/port/client.js +518 -0
- package/src/port/frame.js +196 -0
- package/src/port/serve.js +442 -0
- package/src/project/index.js +29 -0
- package/src/project/markdown.js +244 -0
- package/src/project/openapi.js +564 -0
- package/src/project/openapi.jslt.json +149 -0
- package/src/project/tools.js +139 -0
- package/src/project/typescript.js +152 -0
- package/src/project/typescript.jtlt.json +72 -0
- package/src/public.js +206 -0
- package/src/revision.js +90 -0
- package/src/stream/client.js +212 -0
- package/src/stream/server.js +306 -0
- package/src/stream/sse.js +67 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The client-side outcome (docs/CONTRACT-FORMAT.md §10): the JSON
|
|
3
|
+
* value every `invoke` resolves to — `{ ok: true, value, meta }` or
|
|
4
|
+
* `{ ok: false, kind, error, meta }` with `kind` one of `failure`
|
|
5
|
+
* (a declared operation error, or a taxonomy error the server answered),
|
|
6
|
+
* `network` (the transport failed), `contract` (the peer violated the
|
|
7
|
+
* contract: an invalid response, an undeclared code, a malformed frame —
|
|
8
|
+
* or the client refused pre-send) and `cancelled` (a local abort). Never
|
|
9
|
+
* an `Error`, a `Response` or a `Headers`: JSON only, so the value can
|
|
10
|
+
* land in app state unchanged.
|
|
11
|
+
*
|
|
12
|
+
* `assembleOutcome` is binding-neutral: it takes what a wire answered as
|
|
13
|
+
* `{ status | null, headers, text | value | error }` and the operation's
|
|
14
|
+
* prepared route, and classifies. The HTTP client feeds it a status, the
|
|
15
|
+
* response headers and the body text; an in-process or message-port
|
|
16
|
+
* binding (no statuses) feeds `status: null` with a parsed `value` or a
|
|
17
|
+
* parsed error envelope. Nothing here performs I/O.
|
|
18
|
+
*
|
|
19
|
+
* Outcome, error and meta objects are built with a fixed member order so
|
|
20
|
+
* each shape is one hidden class — and the shapes `makeMeta` and
|
|
21
|
+
* `outcomeError` build ARE the D6 shapes of every binding (03A): `error`
|
|
22
|
+
* is always `{ code, message, status, details, retryable }`, `meta` is
|
|
23
|
+
* always `{ op, attempt, trace, revision, etag, notModified }`, and no
|
|
24
|
+
* member is ever `undefined` (`isJsonValue` — the predicate the app's
|
|
25
|
+
* task effect and state honor — rejects it, and the outcome would fall
|
|
26
|
+
* back to a string). A binding that cannot carry a member carries `null`
|
|
27
|
+
* (`status`, `etag`, `trace`) or `false` (`notModified`) and says so in
|
|
28
|
+
* its `capabilities`; it never omits the member. The member lists are
|
|
29
|
+
* exported (`OUTCOME_ERROR_MEMBERS`, `OUTCOME_META_MEMBERS`) so a later
|
|
30
|
+
* binding asserts against them instead of restating them.
|
|
31
|
+
*/
|
|
32
|
+
export type CompiledOperation = import('../compile.js').CompiledOperation;
|
|
33
|
+
export type Catalog = import('../http/wire.js').Catalog;
|
|
34
|
+
export type OutcomeMeta = {
|
|
35
|
+
op: string;
|
|
36
|
+
attempt: unknown;
|
|
37
|
+
trace: string | null;
|
|
38
|
+
revision: string | null;
|
|
39
|
+
etag: string | null;
|
|
40
|
+
notModified: boolean;
|
|
41
|
+
};
|
|
42
|
+
export type OutcomeError = {
|
|
43
|
+
code: string;
|
|
44
|
+
message: string;
|
|
45
|
+
status: number | null;
|
|
46
|
+
details: unknown;
|
|
47
|
+
retryable: boolean;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* @typedef {import('../compile.js').CompiledOperation} CompiledOperation
|
|
51
|
+
* @typedef {import('../http/wire.js').Catalog} Catalog
|
|
52
|
+
*/
|
|
53
|
+
/**
|
|
54
|
+
* The correlation members of every outcome. `op` is the operation id;
|
|
55
|
+
* `attempt` is the CALLER's attempt id (`ctx.attempt`, `null` when the
|
|
56
|
+
* caller gave none) and is never read from a response; `trace` is the
|
|
57
|
+
* SERVER's request id (`x-jaren-trace`), `null` when the wire carried
|
|
58
|
+
* none; `revision` is reserved for the contract revision; `etag` is the
|
|
59
|
+
* entity tag a success carried (`null` otherwise); `notModified` is true
|
|
60
|
+
* exactly for a 304.
|
|
61
|
+
* @typedef {Object} OutcomeMeta
|
|
62
|
+
* @property {string} op
|
|
63
|
+
* @property {unknown} attempt
|
|
64
|
+
* @property {string | null} trace
|
|
65
|
+
* @property {string | null} revision
|
|
66
|
+
* @property {string | null} etag
|
|
67
|
+
* @property {boolean} notModified
|
|
68
|
+
*/
|
|
69
|
+
/**
|
|
70
|
+
* The error member of a failed outcome: a stable `code` (a declared
|
|
71
|
+
* error code, a `JC2xxx` taxonomy code, or a `JC205x` client code), a
|
|
72
|
+
* rendered `message`, the HTTP `status` when the binding carries one
|
|
73
|
+
* (`null` otherwise), `details` (`null` when none — an outcome is JSON,
|
|
74
|
+
* so no member is ever `undefined`) and whether the caller may retry.
|
|
75
|
+
* @typedef {Object} OutcomeError
|
|
76
|
+
* @property {string} code
|
|
77
|
+
* @property {string} message
|
|
78
|
+
* @property {number | null} status
|
|
79
|
+
* @property {unknown} details
|
|
80
|
+
* @property {boolean} retryable
|
|
81
|
+
*/
|
|
82
|
+
/**
|
|
83
|
+
* The members of every outcome `error`, in order (D6). Frozen.
|
|
84
|
+
* @type {readonly ['code', 'message', 'status', 'details', 'retryable']}
|
|
85
|
+
*/
|
|
86
|
+
export declare const OUTCOME_ERROR_MEMBERS: readonly ['code', 'message', 'status', 'details', 'retryable'];
|
|
87
|
+
/**
|
|
88
|
+
* The members of every outcome `meta`, in order (D6). Frozen.
|
|
89
|
+
* @type {readonly ['op', 'attempt', 'trace', 'revision', 'etag', 'notModified']}
|
|
90
|
+
*/
|
|
91
|
+
export declare const OUTCOME_META_MEMBERS: readonly ['op', 'attempt', 'trace', 'revision', 'etag', 'notModified'];
|
|
92
|
+
export type OkOutcome = {
|
|
93
|
+
ok: true;
|
|
94
|
+
value: unknown;
|
|
95
|
+
meta: OutcomeMeta;
|
|
96
|
+
};
|
|
97
|
+
export type FailedOutcome = {
|
|
98
|
+
ok: false;
|
|
99
|
+
kind: 'failure' | 'network' | 'contract' | 'cancelled';
|
|
100
|
+
error: OutcomeError;
|
|
101
|
+
meta: OutcomeMeta;
|
|
102
|
+
};
|
|
103
|
+
export type Outcome = OkOutcome | FailedOutcome;
|
|
104
|
+
/**
|
|
105
|
+
* @typedef {{ ok: true, value: unknown, meta: OutcomeMeta }} OkOutcome
|
|
106
|
+
* @typedef {{ ok: false, kind: 'failure' | 'network' | 'contract' | 'cancelled', error: OutcomeError, meta: OutcomeMeta }} FailedOutcome
|
|
107
|
+
* @typedef {OkOutcome | FailedOutcome} Outcome
|
|
108
|
+
*/
|
|
109
|
+
/**
|
|
110
|
+
* The client-side taxonomy as data: code → `{ msgid, retryable }`. The
|
|
111
|
+
* normative table is docs/CONTRACT-FORMAT.md §10; a test holds the two
|
|
112
|
+
* equal, and equal to `CONTRACT_CODES` and the English catalog.
|
|
113
|
+
* `retryable` of `JC2055` is decided per response (5xx and 429 are
|
|
114
|
+
* retryable); the row carries the default.
|
|
115
|
+
*/
|
|
116
|
+
export declare const CLIENT_ERRORS: Readonly<{
|
|
117
|
+
JC2050: Readonly<{
|
|
118
|
+
msgid: "contract/client-invalid-input";
|
|
119
|
+
retryable: false;
|
|
120
|
+
}>;
|
|
121
|
+
JC2051: Readonly<{
|
|
122
|
+
msgid: "contract/network";
|
|
123
|
+
retryable: true;
|
|
124
|
+
}>;
|
|
125
|
+
JC2052: Readonly<{
|
|
126
|
+
msgid: "contract/cancelled";
|
|
127
|
+
retryable: false;
|
|
128
|
+
}>;
|
|
129
|
+
JC2053: Readonly<{
|
|
130
|
+
msgid: "contract/invalid-response";
|
|
131
|
+
retryable: false;
|
|
132
|
+
}>;
|
|
133
|
+
JC2054: Readonly<{
|
|
134
|
+
msgid: "contract/key-storage-failed";
|
|
135
|
+
retryable: false;
|
|
136
|
+
}>;
|
|
137
|
+
JC2055: Readonly<{
|
|
138
|
+
msgid: "contract/undeclared-response";
|
|
139
|
+
retryable: false;
|
|
140
|
+
}>;
|
|
141
|
+
JC2056: Readonly<{
|
|
142
|
+
msgid: "contract/not-a-contract";
|
|
143
|
+
retryable: false;
|
|
144
|
+
}>;
|
|
145
|
+
JC2057: Readonly<{
|
|
146
|
+
msgid: "contract/incompatible";
|
|
147
|
+
retryable: false;
|
|
148
|
+
}>;
|
|
149
|
+
JC2058: Readonly<{
|
|
150
|
+
msgid: "contract/host-failed";
|
|
151
|
+
retryable: false;
|
|
152
|
+
}>;
|
|
153
|
+
}>;
|
|
154
|
+
export type OutcomeRoute = {
|
|
155
|
+
id: string;
|
|
156
|
+
validateOutput: (value: unknown) => any;
|
|
157
|
+
errors: Readonly<Record<string, import('../compile.js').CompiledErrorDecl>>;
|
|
158
|
+
retryOn: ReadonlySet<string>;
|
|
159
|
+
details: 'none' | 'paths' | 'full';
|
|
160
|
+
};
|
|
161
|
+
/**
|
|
162
|
+
* What the outcome assembler reads per operation, decided once.
|
|
163
|
+
* @typedef {Object} OutcomeRoute
|
|
164
|
+
* @property {string} id
|
|
165
|
+
* @property {(value: unknown) => any} validateOutput
|
|
166
|
+
* @property {Readonly<Record<string, import('../compile.js').CompiledErrorDecl>>} errors
|
|
167
|
+
* @property {ReadonlySet<string>} retryOn
|
|
168
|
+
* @property {'none' | 'paths' | 'full'} details
|
|
169
|
+
*/
|
|
170
|
+
/**
|
|
171
|
+
* Prepare an operation for the assembler.
|
|
172
|
+
* @param {CompiledOperation} op
|
|
173
|
+
* @returns {OutcomeRoute}
|
|
174
|
+
*/
|
|
175
|
+
export declare function prepareOutcomeRoute(op: CompiledOperation): OutcomeRoute;
|
|
176
|
+
/**
|
|
177
|
+
* A meta object in its fixed member order.
|
|
178
|
+
* @param {string} op
|
|
179
|
+
* @param {unknown} attempt
|
|
180
|
+
* @param {string | null} trace
|
|
181
|
+
* @returns {OutcomeMeta}
|
|
182
|
+
*/
|
|
183
|
+
export declare function makeMeta(op: string, attempt: unknown, trace: string | null): OutcomeMeta;
|
|
184
|
+
/**
|
|
185
|
+
* @param {unknown} value
|
|
186
|
+
* @param {OutcomeMeta} meta
|
|
187
|
+
* @returns {OkOutcome}
|
|
188
|
+
*/
|
|
189
|
+
export declare function okOutcome(value: unknown, meta: OutcomeMeta): OkOutcome;
|
|
190
|
+
/**
|
|
191
|
+
* @param {'failure' | 'network' | 'contract' | 'cancelled'} kind
|
|
192
|
+
* @param {OutcomeError} error
|
|
193
|
+
* @param {OutcomeMeta} meta
|
|
194
|
+
* @returns {FailedOutcome}
|
|
195
|
+
*/
|
|
196
|
+
export declare function failedOutcome(kind: 'failure' | 'network' | 'contract' | 'cancelled', error: OutcomeError, meta: OutcomeMeta): FailedOutcome;
|
|
197
|
+
/**
|
|
198
|
+
* An error object in its fixed member order.
|
|
199
|
+
* @param {string} code
|
|
200
|
+
* @param {string} message
|
|
201
|
+
* @param {number | null} status
|
|
202
|
+
* @param {unknown} details
|
|
203
|
+
* @param {boolean} retryable
|
|
204
|
+
* @returns {OutcomeError}
|
|
205
|
+
*/
|
|
206
|
+
export declare function outcomeError(code: string, message: string, status: number | null, details: unknown, retryable: boolean): OutcomeError;
|
|
207
|
+
/**
|
|
208
|
+
* A client-originated error (`JC205x`): message from the catalog, the
|
|
209
|
+
* row's `retryable` unless overridden.
|
|
210
|
+
* @param {Catalog | null} catalog
|
|
211
|
+
* @param {keyof typeof CLIENT_ERRORS} code
|
|
212
|
+
* @param {Record<string, unknown>} params
|
|
213
|
+
* @param {number | null} status
|
|
214
|
+
* @param {unknown} details
|
|
215
|
+
* @param {boolean} [retryable]
|
|
216
|
+
* @returns {OutcomeError}
|
|
217
|
+
*/
|
|
218
|
+
export declare function clientError(catalog: Catalog | null, code: keyof typeof CLIENT_ERRORS, params: Record<string, unknown>, status: number | null, details: unknown, retryable?: boolean): OutcomeError;
|
|
219
|
+
/**
|
|
220
|
+
* True for a value shaped like an outcome: a plain object with a
|
|
221
|
+
* boolean `ok`, a `meta` carrying every D6 meta member, and, when
|
|
222
|
+
* failed, a `kind` and an `error` carrying every D6 error member with a
|
|
223
|
+
* string `code` — no member `undefined`. Reads guardedly, so a hostile
|
|
224
|
+
* value classifies as "not an outcome".
|
|
225
|
+
* @param {unknown} value
|
|
226
|
+
* @returns {value is Outcome}
|
|
227
|
+
*/
|
|
228
|
+
export declare function isOutcome(value: unknown): value is Outcome;
|
|
229
|
+
export type WireMessage = {
|
|
230
|
+
status: number | null;
|
|
231
|
+
headers: Readonly<Record<string, string>> | null;
|
|
232
|
+
text?: string | null;
|
|
233
|
+
value?: unknown;
|
|
234
|
+
error?: unknown;
|
|
235
|
+
};
|
|
236
|
+
/**
|
|
237
|
+
* What a wire answered, as the assembler reads it. Exactly one of `text`,
|
|
238
|
+
* `value` or `error` carries the body: `text` is the raw body of an HTTP
|
|
239
|
+
* response (parsed here; `''`/`null` is an empty body), `value` a
|
|
240
|
+
* success value already decoded by a JSON-framed binding, `error` an
|
|
241
|
+
* error envelope (`{ code, message?, details?, retryable? }`) already
|
|
242
|
+
* decoded by such a binding. `status` is the HTTP status or `null` on a
|
|
243
|
+
* binding that carries none; `headers` holds `etag` when the binding
|
|
244
|
+
* carries entity tags.
|
|
245
|
+
* @typedef {Object} WireMessage
|
|
246
|
+
* @property {number | null} status
|
|
247
|
+
* @property {Readonly<Record<string, string>> | null} headers
|
|
248
|
+
* @property {string | null} [text]
|
|
249
|
+
* @property {unknown} [value]
|
|
250
|
+
* @property {unknown} [error]
|
|
251
|
+
*/
|
|
252
|
+
/**
|
|
253
|
+
* Assemble the outcome of one response. TOTAL: every shape the peer can
|
|
254
|
+
* answer classifies; nothing throws.
|
|
255
|
+
*
|
|
256
|
+
* - a 2xx (or `status: null` with a `value`): an empty body is `null`;
|
|
257
|
+
* otherwise the text is parsed (`JC2053` when not JSON) and the value
|
|
258
|
+
* validated against the output schema (`JC2053` with details by
|
|
259
|
+
* `policy.errors.details`) → `{ ok: true, value, meta }` with `meta.etag`
|
|
260
|
+
* from the header;
|
|
261
|
+
* - a 304 → `{ ok: true, value: null, meta }` with `notModified: true`
|
|
262
|
+
* and the `etag`;
|
|
263
|
+
* - any other status: the body is parsed as JSON; a string `code` that
|
|
264
|
+
* the operation declares, or a `JC2xxx` taxonomy code, is `kind:
|
|
265
|
+
* "failure"` with `{ code, message, status, details?, retryable }` —
|
|
266
|
+
* the body's `message` when it is a string, else rendered; `retryable`
|
|
267
|
+
* the body's boolean, else whether `policy.retry.on` names the code;
|
|
268
|
+
* anything else (a non-JSON body, no string code, an unknown code) is
|
|
269
|
+
* `kind: "contract"` `JC2055` with the status kept and `retryable`
|
|
270
|
+
* for 5xx/429.
|
|
271
|
+
*
|
|
272
|
+
* @param {OutcomeRoute} route
|
|
273
|
+
* @param {WireMessage} message
|
|
274
|
+
* @param {OutcomeMeta} meta - mutated: `etag`/`notModified` are set here
|
|
275
|
+
* @param {Catalog | null} catalog
|
|
276
|
+
* @returns {Outcome}
|
|
277
|
+
*/
|
|
278
|
+
export declare function assembleOutcome(route: OutcomeRoute, message: WireMessage, meta: OutcomeMeta, catalog: Catalog | null): Outcome;
|
|
279
|
+
/**
|
|
280
|
+
* The outcome a handler projects a THROWN host value into when it must
|
|
281
|
+
* settle with an outcome (the app effect): `kind: "contract"` `JC2058`,
|
|
282
|
+
* the message from the catalog, nothing of the thrown value (its text
|
|
283
|
+
* may carry anything).
|
|
284
|
+
* @param {string} op
|
|
285
|
+
* @param {unknown} attempt
|
|
286
|
+
* @param {Catalog | null} catalog
|
|
287
|
+
* @returns {Outcome}
|
|
288
|
+
*/
|
|
289
|
+
export declare function hostFailureOutcome(op: string, attempt: unknown, catalog: Catalog | null): Outcome;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Version compatibility — the one implementation of the
|
|
3
|
+
* negotiation rule (docs/CONTRACT-FORMAT.md §10.4, §13.1): two ends
|
|
4
|
+
* speak when they declare the same `version`, or when either end's
|
|
5
|
+
* `compat` list names the other's `version`. The client's `negotiate()`
|
|
6
|
+
* and any server that wants to refuse an incompatible peer both call
|
|
7
|
+
* this; `diffContracts` is the complementary question (WHAT changed),
|
|
8
|
+
* this is the declared answer (do the authors CLAIM the ends speak).
|
|
9
|
+
*/
|
|
10
|
+
export type VersionedContract = {
|
|
11
|
+
version?: string | null;
|
|
12
|
+
compat?: readonly string[] | null;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Why two ends are compatible, or `null` when they are not:
|
|
16
|
+
* `'same-version'` (equal `version`s — two unversioned contracts included),
|
|
17
|
+
* `'server-accepts'` (the server's `compat` names the client's version),
|
|
18
|
+
* `'client-accepts'` (the client's `compat` names the server's version).
|
|
19
|
+
* Checked in that order, so the strongest claim wins the reason.
|
|
20
|
+
* @param {VersionedContract} client
|
|
21
|
+
* @param {VersionedContract} server
|
|
22
|
+
* @returns {'same-version' | 'server-accepts' | 'client-accepts' | null}
|
|
23
|
+
*/
|
|
24
|
+
export declare function compatReason(client: VersionedContract, server: VersionedContract): 'same-version' | 'server-accepts' | 'client-accepts' | null;
|
|
25
|
+
/**
|
|
26
|
+
* Whether a client contract and a server contract declare themselves
|
|
27
|
+
* compatible — the negotiation rule as a predicate. Takes compiled
|
|
28
|
+
* contracts, raw documents or well-known descriptions alike (it reads
|
|
29
|
+
* only `version` and `compat`).
|
|
30
|
+
* @param {VersionedContract} clientContract
|
|
31
|
+
* @param {VersionedContract} serverContract
|
|
32
|
+
* @returns {boolean}
|
|
33
|
+
* @example
|
|
34
|
+
* isCompatible({ version: '5', compat: ['4'] }, { version: '4' }); // true — the client accepts 4
|
|
35
|
+
*/
|
|
36
|
+
export declare function isCompatible(clientContract: VersionedContract, serverContract: VersionedContract): boolean;
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file `compileContract`: a `$contract` document (docs/CONTRACT-FORMAT.md
|
|
3
|
+
* §2) becomes a frozen `Contract` — per-operation validators, transport
|
|
4
|
+
* normalizers, the materialized HTTP binding with every default resolved,
|
|
5
|
+
* and one path matcher over the operation table.
|
|
6
|
+
*
|
|
7
|
+
* Two stages, like every compiler in the suite: everything is decided
|
|
8
|
+
* here, once; nothing that runs per request allocates or re-reads the
|
|
9
|
+
* document. The document is TRUSTED input but is still read totally: it
|
|
10
|
+
* is first snapshotted through guarded access into plain JSON (a
|
|
11
|
+
* throwing accessor, a non-JSON member or a cycle is `JC0001` at the
|
|
12
|
+
* member), then validated rule by rule against a CLOSED vocabulary
|
|
13
|
+
* (`JC0013` — a silently ignored `policy` member is a behavior bug), and
|
|
14
|
+
* only then compiled. Every refusal is a `ContractCompileError` with the
|
|
15
|
+
* `docPath` of the member at fault.
|
|
16
|
+
*
|
|
17
|
+
* `$ref` resolution rides the validator: the document is registered under
|
|
18
|
+
* a synthetic id and each operation schema is compiled as a `$ref` into
|
|
19
|
+
* it, so `#/$defs/Product` means the contract's own `$defs` and an
|
|
20
|
+
* absolute `$id` means one of `options.schemas`. Unresolved is `JC0007`
|
|
21
|
+
* at compile — never at request time.
|
|
22
|
+
*/
|
|
23
|
+
import { JarenValidator } from '@jarenjs/validate';
|
|
24
|
+
export type RefScope = {
|
|
25
|
+
/**
|
|
26
|
+
* - the snapshotted document
|
|
27
|
+
*/
|
|
28
|
+
src: any;
|
|
29
|
+
anchors: Map<string, object>;
|
|
30
|
+
validator: JarenValidator<any>;
|
|
31
|
+
};
|
|
32
|
+
export type CompiledValidate = (value: unknown) => any;
|
|
33
|
+
export type InputTransport = {
|
|
34
|
+
normalize: (value: any) => any;
|
|
35
|
+
members: {
|
|
36
|
+
path: readonly string[];
|
|
37
|
+
query: readonly string[];
|
|
38
|
+
header: readonly string[];
|
|
39
|
+
repeated: readonly string[];
|
|
40
|
+
};
|
|
41
|
+
schemas: Readonly<Record<string, any>>;
|
|
42
|
+
required: readonly string[];
|
|
43
|
+
};
|
|
44
|
+
export type CompiledInput = {
|
|
45
|
+
/**
|
|
46
|
+
* - the declared input schema (frozen source)
|
|
47
|
+
*/
|
|
48
|
+
schema: any;
|
|
49
|
+
/**
|
|
50
|
+
* - the object schema `schema` resolves to: itself,
|
|
51
|
+
* or the end of its `$ref` chain (inside the document or a registered
|
|
52
|
+
* schema) — whose `properties` are the operation's members; frozen
|
|
53
|
+
*/
|
|
54
|
+
effective: any;
|
|
55
|
+
validate: CompiledValidate;
|
|
56
|
+
/**
|
|
57
|
+
* - `null` when no member travels as a string
|
|
58
|
+
*/
|
|
59
|
+
transport: InputTransport | null;
|
|
60
|
+
};
|
|
61
|
+
export type CompiledOutput = {
|
|
62
|
+
schema: any;
|
|
63
|
+
validate: CompiledValidate;
|
|
64
|
+
};
|
|
65
|
+
export type CompiledErrorDecl = {
|
|
66
|
+
status: number;
|
|
67
|
+
/**
|
|
68
|
+
* - `null` when undeclared
|
|
69
|
+
*/
|
|
70
|
+
schema: any;
|
|
71
|
+
validate: CompiledValidate | null;
|
|
72
|
+
};
|
|
73
|
+
export type CompiledPolicy = {
|
|
74
|
+
task: 'switch' | 'exhaust' | 'concat' | 'parallel';
|
|
75
|
+
idempotency: 'none' | 'optional' | 'required';
|
|
76
|
+
/**
|
|
77
|
+
* - `input:<json-pointer>` or null
|
|
78
|
+
*/
|
|
79
|
+
revision: string | null;
|
|
80
|
+
cache: 'none' | 'revision';
|
|
81
|
+
limits: {
|
|
82
|
+
maxBodyBytes: number;
|
|
83
|
+
};
|
|
84
|
+
errors: {
|
|
85
|
+
details: 'none' | 'paths' | 'full';
|
|
86
|
+
};
|
|
87
|
+
retry: {
|
|
88
|
+
max: number;
|
|
89
|
+
on: readonly string[];
|
|
90
|
+
} | null;
|
|
91
|
+
/**
|
|
92
|
+
* - the stream policy of a subscribe operation, defaults materialized; `null` on every other kind
|
|
93
|
+
*/
|
|
94
|
+
stream: {
|
|
95
|
+
resume: 'snapshot' | 'replay';
|
|
96
|
+
heartbeatMs: number;
|
|
97
|
+
maxPatchBytes: number | null;
|
|
98
|
+
} | null;
|
|
99
|
+
/**
|
|
100
|
+
* - who may see the operation: `server`
|
|
101
|
+
* keeps it out of the public projection and every projection built on it
|
|
102
|
+
*/
|
|
103
|
+
audience: 'public' | 'server';
|
|
104
|
+
};
|
|
105
|
+
export type CompiledHttp = {
|
|
106
|
+
method: string;
|
|
107
|
+
path: string;
|
|
108
|
+
template: import('./path.js').ParsedPathTemplate;
|
|
109
|
+
variables: readonly string[];
|
|
110
|
+
in: Readonly<Record<string, 'path' | 'query' | 'header' | 'body'>>;
|
|
111
|
+
body: string | null;
|
|
112
|
+
status: number;
|
|
113
|
+
media: string;
|
|
114
|
+
opaque: boolean;
|
|
115
|
+
};
|
|
116
|
+
export type CompiledOperation = {
|
|
117
|
+
id: string;
|
|
118
|
+
kind: 'read' | 'command' | 'subscribe';
|
|
119
|
+
doc: string | null;
|
|
120
|
+
input: CompiledInput | null;
|
|
121
|
+
output: CompiledOutput;
|
|
122
|
+
errors: Readonly<Record<string, CompiledErrorDecl>>;
|
|
123
|
+
policy: CompiledPolicy;
|
|
124
|
+
http: CompiledHttp;
|
|
125
|
+
};
|
|
126
|
+
export type Contract = {
|
|
127
|
+
/**
|
|
128
|
+
* - the source document, deep-frozen
|
|
129
|
+
*/
|
|
130
|
+
doc: any;
|
|
131
|
+
id: string | null;
|
|
132
|
+
version: string | null;
|
|
133
|
+
compat: readonly string[];
|
|
134
|
+
operations: Readonly<Record<string, CompiledOperation>>;
|
|
135
|
+
/**
|
|
136
|
+
* - operation ids in document order
|
|
137
|
+
*/
|
|
138
|
+
ids: readonly string[];
|
|
139
|
+
match: (method: string, path: string) => {
|
|
140
|
+
op: CompiledOperation;
|
|
141
|
+
params: Readonly<Record<string, string>>;
|
|
142
|
+
} | null;
|
|
143
|
+
/**
|
|
144
|
+
* - the methods under which
|
|
145
|
+
* this path shape reaches an operation, sorted (`[]` for none) — what a
|
|
146
|
+
* 405 answers in `Allow`; the path only, query split off, like `match`
|
|
147
|
+
*/
|
|
148
|
+
allowed: (path: string) => string[];
|
|
149
|
+
/**
|
|
150
|
+
* - a pure-JSON summary (docs/CONTRACT-FORMAT.md §3)
|
|
151
|
+
*/
|
|
152
|
+
describe: () => any;
|
|
153
|
+
/**
|
|
154
|
+
* - the SHA-256 (lowercase hex)
|
|
155
|
+
* over the RFC 8785 canonical bytes of the public projection, memoized —
|
|
156
|
+
* computed at most once per compiled contract (docs/CONTRACT-FORMAT.md §14)
|
|
157
|
+
*/
|
|
158
|
+
revision: () => Promise<string>;
|
|
159
|
+
/**
|
|
160
|
+
* - frozen view of the document's `$defs` (`{}` when absent)
|
|
161
|
+
*/
|
|
162
|
+
$defs: any;
|
|
163
|
+
};
|
|
164
|
+
export type CompileContractOptions = {
|
|
165
|
+
/**
|
|
166
|
+
* - the validator every schema
|
|
167
|
+
* compiles through; default `new JarenValidator({ collectErrors: true, skipErrors: false })`
|
|
168
|
+
*/
|
|
169
|
+
validator?: JarenValidator<any>;
|
|
170
|
+
/**
|
|
171
|
+
* - schemas registered by `$id` before compile, so
|
|
172
|
+
* an absolute `$ref` resolves
|
|
173
|
+
*/
|
|
174
|
+
schemas?: Record<string, any>[];
|
|
175
|
+
};
|
|
176
|
+
/**
|
|
177
|
+
* Compile a `$contract` document (docs/CONTRACT-FORMAT.md) into a frozen
|
|
178
|
+
* `Contract`: every operation's `input`/`output`/error validators, its
|
|
179
|
+
* transport normalizer, its resolved policy and materialized HTTP
|
|
180
|
+
* binding, and one `match(method, path)` over the whole table.
|
|
181
|
+
* Synchronous; total for a hostile document; no I/O.
|
|
182
|
+
*
|
|
183
|
+
* @param {unknown} doc - the contract document
|
|
184
|
+
* @param {CompileContractOptions} [options]
|
|
185
|
+
* @returns {Contract}
|
|
186
|
+
* @throws {ContractCompileError} when the document violates the format (`JC0001–JC0017`)
|
|
187
|
+
* @example
|
|
188
|
+
* const contract = compileContract({
|
|
189
|
+
* $contract: '0.1',
|
|
190
|
+
* operations: {
|
|
191
|
+
* 'catalog.load': { kind: 'read', output: true, http: { method: 'GET', path: '/api/catalog' } },
|
|
192
|
+
* },
|
|
193
|
+
* });
|
|
194
|
+
* contract.match('GET', '/api/catalog').op.id; // 'catalog.load'
|
|
195
|
+
*/
|
|
196
|
+
export declare function compileContract(doc: unknown, options?: CompileContractOptions): Contract;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file `describe()`: a compiled contract as pure JSON — the resolved
|
|
3
|
+
* binding and policy of every operation, with each defaulted value
|
|
4
|
+
* marked in `inferred` so a projection can tell what the author
|
|
5
|
+
* declared from what the compiler filled in. Stable member order, no
|
|
6
|
+
* functions, no schemas (those stay on the compiled operations); this is
|
|
7
|
+
* what a CLI prints and what a golden test compares. `revision` is read
|
|
8
|
+
* synchronously from the memo (`peekRevision`), so it is `null` until
|
|
9
|
+
* someone awaited `contract.revision()` — the well-known responder does
|
|
10
|
+
* exactly that before it renders this document.
|
|
11
|
+
*/
|
|
12
|
+
export type OperationDescription = {
|
|
13
|
+
id: string;
|
|
14
|
+
kind: 'read' | 'command';
|
|
15
|
+
method: string;
|
|
16
|
+
/**
|
|
17
|
+
* - the canonical `{name}` template
|
|
18
|
+
*/
|
|
19
|
+
path: string;
|
|
20
|
+
status: number;
|
|
21
|
+
media: string;
|
|
22
|
+
opaque: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* - member → location
|
|
25
|
+
*/
|
|
26
|
+
in: Readonly<Record<string, string>>;
|
|
27
|
+
/**
|
|
28
|
+
* - the whole-body member, or null
|
|
29
|
+
*/
|
|
30
|
+
body: string | null;
|
|
31
|
+
task: string;
|
|
32
|
+
idempotency: string;
|
|
33
|
+
cache: string;
|
|
34
|
+
/**
|
|
35
|
+
* - the resolved stream policy; present exactly on subscribe operations
|
|
36
|
+
*/
|
|
37
|
+
stream?: {
|
|
38
|
+
resume: string;
|
|
39
|
+
heartbeatMs: number;
|
|
40
|
+
maxPatchBytes: number | null;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* which of the above the compiler defaulted: `http` when the whole
|
|
44
|
+
* binding is the canonical `POST /<id>`, `in` listing the members whose
|
|
45
|
+
* location was not declared
|
|
46
|
+
*/
|
|
47
|
+
inferred: {
|
|
48
|
+
http: boolean;
|
|
49
|
+
status: boolean;
|
|
50
|
+
media: boolean;
|
|
51
|
+
in: readonly string[];
|
|
52
|
+
task: boolean;
|
|
53
|
+
idempotency: boolean;
|
|
54
|
+
cache: boolean;
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
export type ContractDescription = {
|
|
58
|
+
$contract: '0.1';
|
|
59
|
+
id: string | null;
|
|
60
|
+
version: string | null;
|
|
61
|
+
compat: readonly string[];
|
|
62
|
+
/**
|
|
63
|
+
* - the contract revision (64 lowercase
|
|
64
|
+
* hex; docs/CONTRACT-FORMAT.md §14) when `contract.revision()` has
|
|
65
|
+
* settled, `null` before — `describe()` stays synchronous and never
|
|
66
|
+
* computes it
|
|
67
|
+
*/
|
|
68
|
+
revision: string | null;
|
|
69
|
+
/**
|
|
70
|
+
* - document order
|
|
71
|
+
*/
|
|
72
|
+
operations: OperationDescription[];
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* The description of one operation.
|
|
76
|
+
* @typedef {Object} OperationDescription
|
|
77
|
+
* @property {string} id
|
|
78
|
+
* @property {'read' | 'command'} kind
|
|
79
|
+
* @property {string} method
|
|
80
|
+
* @property {string} path - the canonical `{name}` template
|
|
81
|
+
* @property {number} status
|
|
82
|
+
* @property {string} media
|
|
83
|
+
* @property {boolean} opaque
|
|
84
|
+
* @property {Readonly<Record<string, string>>} in - member → location
|
|
85
|
+
* @property {string | null} body - the whole-body member, or null
|
|
86
|
+
* @property {string} task
|
|
87
|
+
* @property {string} idempotency
|
|
88
|
+
* @property {string} cache
|
|
89
|
+
* @property {{ resume: string, heartbeatMs: number, maxPatchBytes: number | null }} [stream]
|
|
90
|
+
* - the resolved stream policy; present exactly on subscribe operations
|
|
91
|
+
* @property {{ http: boolean, status: boolean, media: boolean, in: readonly string[], task: boolean, idempotency: boolean, cache: boolean }} inferred
|
|
92
|
+
* which of the above the compiler defaulted: `http` when the whole
|
|
93
|
+
* binding is the canonical `POST /<id>`, `in` listing the members whose
|
|
94
|
+
* location was not declared
|
|
95
|
+
*/
|
|
96
|
+
/**
|
|
97
|
+
* The description of a contract.
|
|
98
|
+
* @typedef {Object} ContractDescription
|
|
99
|
+
* @property {'0.1'} $contract
|
|
100
|
+
* @property {string | null} id
|
|
101
|
+
* @property {string | null} version
|
|
102
|
+
* @property {readonly string[]} compat
|
|
103
|
+
* @property {string | null} revision - the contract revision (64 lowercase
|
|
104
|
+
* hex; docs/CONTRACT-FORMAT.md §14) when `contract.revision()` has
|
|
105
|
+
* settled, `null` before — `describe()` stays synchronous and never
|
|
106
|
+
* computes it
|
|
107
|
+
* @property {OperationDescription[]} operations - document order
|
|
108
|
+
*/
|
|
109
|
+
/**
|
|
110
|
+
* Describe a compiled contract. Reads the compiled operations for the
|
|
111
|
+
* resolved values and the frozen source document for what was declared.
|
|
112
|
+
* @param {import('./compile.js').Contract} contract
|
|
113
|
+
* @returns {ContractDescription}
|
|
114
|
+
*/
|
|
115
|
+
export declare function describeContract(contract: import('./compile.js').Contract): ContractDescription;
|