@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
package/src/errors.js
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file Error types for @jarenjs/contract, built on `@jarenjs/core`'s
|
|
4
|
+
* coded contract: every failure carries a stable `code`, a bare
|
|
5
|
+
* `reason`, a composed `message`, and — for compile errors — the
|
|
6
|
+
* `docPath` of the offending member of the contract document. The
|
|
7
|
+
* normative table lives in docs/CONTRACT-FORMAT.md §6, proven in sync
|
|
8
|
+
* with `CONTRACT_CODES` below by a test.
|
|
9
|
+
*
|
|
10
|
+
* The `JC` code space is partitioned by range so the layers of this
|
|
11
|
+
* package never collide:
|
|
12
|
+
*
|
|
13
|
+
* - `JC0001–JC0049` document compile (`ContractCompileError`)
|
|
14
|
+
* - `JC0050–JC0069` binding declaration and projection compile
|
|
15
|
+
* (`JC0060`: the OpenAPI keyword policy)
|
|
16
|
+
* - `JC1001–JC1049` host programming errors (thrown `TypeError`s)
|
|
17
|
+
* - `JC2001–JC2049` http request-time (`ContractRuntimeError`)
|
|
18
|
+
* - `JC2050–JC2069` client-side
|
|
19
|
+
* - `JC2070–JC2089` port/local bindings
|
|
20
|
+
* - `JC2090–JC2109` stream binding
|
|
21
|
+
*
|
|
22
|
+
* The document-compile range, the projection code, the host range, the
|
|
23
|
+
* http request-time range, the client range and the port/local range are
|
|
24
|
+
* populated; stream is reserved for its binding and is listed here so a
|
|
25
|
+
* later addition lands in its range rather than at the next free number.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { CodedError } from '@jarenjs/core/errors';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The runtime code table: one entry per code this package can raise,
|
|
32
|
+
* proven in sync with CONTRACT-FORMAT.md §6's normative table by a test.
|
|
33
|
+
*/
|
|
34
|
+
export const CONTRACT_CODES = Object.freeze({
|
|
35
|
+
JC0001: 'the document is not a well-formed contract object: not an object, $contract is not "0.1", $defs is not a map of schemas, a member is not a JSON value, or a member threw when read',
|
|
36
|
+
JC0002: 'operations is not an object with at least one member',
|
|
37
|
+
JC0003: 'an operation id is not a dotted lowercase identifier',
|
|
38
|
+
JC0004: 'kind is none of read, command, subscribe',
|
|
39
|
+
JC0005: 'input is not a schema whose effective type is object',
|
|
40
|
+
JC0006: 'output is absent or not a schema',
|
|
41
|
+
JC0007: 'a $ref resolves neither within the document nor against the registered schemas',
|
|
42
|
+
JC0008: 'http.path is not a valid path template',
|
|
43
|
+
JC0009: 'a path variable, http.in key or http.body names no input member, or a member is mapped to a location it cannot travel in',
|
|
44
|
+
JC0010: 'two operations share method and canonical path shape',
|
|
45
|
+
JC0011: 'errors is malformed: not an object, a code is not a lowercase hyphenated word, a status is not a 100–599 integer, or a schema is not a schema',
|
|
46
|
+
JC0012: 'http.method is not an uppercase token of the supported set, http.status is not a 200–299 integer, or http.media is not a media type',
|
|
47
|
+
JC0013: 'an unknown member in a closed object (the document root, an operation, policy, http, limits, retry, or an error declaration)',
|
|
48
|
+
JC0014: 'a policy member is mistyped or outside its declared set',
|
|
49
|
+
JC0015: 'id, version, compat or an operation doc is mistyped',
|
|
50
|
+
JC0016: 'an operation bound to GET or HEAD carries a body-located member (a GET body)',
|
|
51
|
+
JC0017: 'an opaque operation (a non-JSON http.media) declares a body-located member — its body is bytes the contract never decodes, so the member could never be validated',
|
|
52
|
+
JC0018: 'a subscribe operation declares a policy.task other than switch — a subscription slot is replaced, never queued',
|
|
53
|
+
JC0019: 'a subscribe operation is bound to a method other than GET — a stream is fetched, not sent',
|
|
54
|
+
JC0020: 'a subscribe operation declares a policy.idempotency other than none — a subscription registers, it does not commit',
|
|
55
|
+
// ——— projection compile (ContractCompileError, docPath into the contract document) ———
|
|
56
|
+
JC0060: 'the OpenAPI projection met a schema keyword it cannot map honestly: a boolean required (draft-04 style) or a same-document $ref that lands outside $defs (both dropped and reported under lenient), or a components member inside a schema',
|
|
57
|
+
JC0061: 'the public projection is not canonicalizable, so no revision exists — a string with an unpaired surrogate, say; docPath points at the offending value inside the projection',
|
|
58
|
+
// ——— host programming errors (thrown ContractHostError, a TypeError) ———
|
|
59
|
+
JC1001: 'serveHttp, serveLocal or servePort: handlers is not an object, a key names no operation of the contract, a value is not a function, or an option (a channel without postMessage, say) is malformed',
|
|
60
|
+
JC1002: 'serveHttp, serveLocal or servePort: an operation has no handler (and, on serveHttp, options.partial is not set; an opaque operation needs none on the status-less bindings)',
|
|
61
|
+
JC1003: 'a binding cannot carry a declared feature: an operation declares idempotency and serveHttp was given no ledger',
|
|
62
|
+
JC1004: 'dispatch received a malformed request object (method or url not a string, headers not an object, body not a string, Uint8Array or null)',
|
|
63
|
+
JC1005: 'a client or the contract effect was asked for an operation the contract does not declare, or invoke was asked for an opaque operation (use client.url on http; the status-less bindings cannot carry it at all)',
|
|
64
|
+
JC1006: 'ctx.status(n) was called with a status that is not an integer in 200–299',
|
|
65
|
+
JC1007: 'contractAppBinding: ops names an operation the contract does not declare, or namespace/statePath is malformed',
|
|
66
|
+
JC1008: 'openHttpClient, openPortClient, client.url, createContractEffect, createContractSubscription or a projection (publicProjection, toOpenApi, toTypeScript, toMarkdown, contractTools): an argument or option is malformed (not a compiled contract, fetch/keys/sleep/createTaskEffect/projectError not a function, storage without read/write, a non-object input to url, an ops entry naming no or an opaque operation, a tool name outside ^[a-zA-Z0-9_-]{1,64}$ or shared by two operations)',
|
|
67
|
+
JC1009: 'encodeSseEvent (the stream wire): an event, id or data string the SSE frame cannot carry — a bare carriage return inside data, a line terminator inside event or id',
|
|
68
|
+
JC1010: 'client.subscribe was asked for an operation that is not a subscribe operation (invoke carries reads and commands; subscribe carries streams)',
|
|
69
|
+
// ——— http request-time (ContractRuntimeError, mapped onto the wire) ———
|
|
70
|
+
JC2001: 'no operation matches the request method and path (404)',
|
|
71
|
+
JC2002: 'the path shape is served under other methods (405, Allow lists them)',
|
|
72
|
+
JC2003: 'the request body exceeds policy.limits.maxBodyBytes, by content-length or by read length (413)',
|
|
73
|
+
JC2004: 'a body-carrying operation received a content-type that is not its declared media (415)',
|
|
74
|
+
JC2005: 'the request body is present and is not valid JSON, or its bytes are not valid UTF-8 (400)',
|
|
75
|
+
JC2006: 'the reassembled input fails the operation\'s input validator (400)',
|
|
76
|
+
JC2007: 'the operation requires an Idempotency-Key header and none was sent (400)',
|
|
77
|
+
JC2008: 'the handler threw a non-declared error, rejected, or returned a hostile value (500; onError sees it)',
|
|
78
|
+
JC2009: 'the idempotency ledger reports the key in progress (retryable) or bound to a different request (mismatch) (409)',
|
|
79
|
+
JC2010: 'the handler value fails the output validator or a declared error\'s details fail its schema — the server broke the contract (500)',
|
|
80
|
+
JC2011: 'the request path carries a malformed percent-escape (400)',
|
|
81
|
+
JC2012: 'the query string is not decodable (400)',
|
|
82
|
+
JC2013: 'the operation has no handler on this partial server (501)',
|
|
83
|
+
JC2014: 'the If-Match precondition does not match the entity tag the handler armed (412)',
|
|
84
|
+
JC2015: 'a declared header member is repeated when its schema is scalar, or fails transport decoding (400)',
|
|
85
|
+
// ——— client-side (outcomes of invoke; never thrown) ———
|
|
86
|
+
JC2050: 'the input fails the operation\'s input validator before anything was sent (kind contract)',
|
|
87
|
+
JC2051: 'the request did not complete: the transport rejected or timed out (kind network, retryable)',
|
|
88
|
+
JC2052: 'the request was cancelled through the caller\'s signal or client.close() (kind cancelled)',
|
|
89
|
+
JC2053: 'a success response is not JSON or fails the operation\'s output validator (kind contract)',
|
|
90
|
+
JC2054: 'the durable idempotency-key storage threw before the request was sent (kind contract)',
|
|
91
|
+
JC2055: 'the server answered a status with a body that is neither a declared error nor a taxonomy error (kind contract; retryable for 5xx and 429)',
|
|
92
|
+
JC2056: 'negotiation: the server does not answer a jaren-contract description at the well-known path, or describes another contract id',
|
|
93
|
+
JC2057: 'negotiation: the server speaks a version neither end declares compatible',
|
|
94
|
+
JC2058: 'the host threw while invoking an operation through the contract effect — projected into an outcome, never a string',
|
|
95
|
+
// ——— port/local bindings (outcomes of invoke or wire error frames; never thrown) ———
|
|
96
|
+
JC2070: 'the handler failed on a local or port serving host: it threw a non-declared error, rejected, answered an undeclared code, or broke its output or error-details schema (kind contract; onError sees the cause)',
|
|
97
|
+
JC2071: 'a request frame names no operation this channel serves (unknown, or opaque — a port carries JSON only)',
|
|
98
|
+
JC2072: 'a port request got no answer within timeoutMs (kind network, retryable)',
|
|
99
|
+
JC2073: 'a response frame addressed to this client does not match the frame grammar (kind contract)',
|
|
100
|
+
JC2074: 'the channel refused the request frame — closed or detached (kind network)',
|
|
101
|
+
// ——— stream binding (outcomes of subscribe, or wire error events; never thrown) ———
|
|
102
|
+
JC2090: 'the server answered a subscribe request with a non-stream response (kind contract)',
|
|
103
|
+
JC2091: 'a snapshot fails the operation\'s output validator — the server broke the contract; the stream ends with an error event carrying this code',
|
|
104
|
+
JC2092: 'a stream event\'s seq is not strictly greater than the last one delivered (kind contract, client-side)',
|
|
105
|
+
JC2093: 'the stream ended with a server error event whose code the operation does not declare (kind contract; a declared code is a failure outcome under its own code)',
|
|
106
|
+
JC2094: 'the stream went silent for twice policy.stream.heartbeatMs (kind network, client-side)',
|
|
107
|
+
JC2095: 'a requested resume was refused — informational, carried as resumed:false in the fresh snapshot\'s event data, never an outcome',
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* A defect in the contract document itself, raised while
|
|
112
|
+
* `compileContract` compiles it. Every instance carries the JSON
|
|
113
|
+
* Pointer of the offending member as `docPath` (`''` is the document
|
|
114
|
+
* root; an operation id is one reference token, so a dotted id such as
|
|
115
|
+
* `product.save` appears unescaped and only `~` and `/` are escaped per
|
|
116
|
+
* RFC 6901). The codes are `JC0001–JC0049` (docs/CONTRACT-FORMAT.md §6).
|
|
117
|
+
*/
|
|
118
|
+
export class ContractCompileError extends CodedError {
|
|
119
|
+
/**
|
|
120
|
+
* @param {string} code
|
|
121
|
+
* @param {string} reason - The bare reason; `message` is composed per
|
|
122
|
+
* the coded contract.
|
|
123
|
+
* @param {string} [docPath] - JSON Pointer into the contract document;
|
|
124
|
+
* `''` is the document root, `undefined` means no location.
|
|
125
|
+
* @param {Error} [cause]
|
|
126
|
+
*/
|
|
127
|
+
constructor(code, reason, docPath, cause) {
|
|
128
|
+
super('ContractCompileError', code, reason, docPath,
|
|
129
|
+
cause !== undefined ? { cause } : undefined);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* A request-time failure (`JC2xxx`) as it exists in-process before a
|
|
135
|
+
* binding maps it onto its wire: a stable `code`, a `msgid`
|
|
136
|
+
* (`contract/<slug>`) with `params` for the locale catalogs, and the
|
|
137
|
+
* transport hints a binding may carry (`status`, `retryable`). It has no
|
|
138
|
+
* document location — a request is not a document — so `docPath` is
|
|
139
|
+
* always `undefined`. Never thrown across a binding: a binding settles
|
|
140
|
+
* it into a wire error.
|
|
141
|
+
*/
|
|
142
|
+
export class ContractRuntimeError extends CodedError {
|
|
143
|
+
/**
|
|
144
|
+
* @param {string} code
|
|
145
|
+
* @param {string} reason - The bare English reason; `message` is
|
|
146
|
+
* composed per the coded contract.
|
|
147
|
+
* @param {{ msgid: string, params?: Record<string, unknown>, status?: number, retryable?: boolean, cause?: unknown }} options
|
|
148
|
+
* `msgid` is the catalog key; `cause` is installed as an own property
|
|
149
|
+
* exactly when the key is present (`hasOwn` form).
|
|
150
|
+
*/
|
|
151
|
+
constructor(code, reason, options) {
|
|
152
|
+
super('ContractRuntimeError', code, reason, undefined,
|
|
153
|
+
options !== undefined && Object.hasOwn(options, 'cause') ? { cause: options.cause } : undefined);
|
|
154
|
+
/** @type {string} */
|
|
155
|
+
this.msgid = options.msgid;
|
|
156
|
+
/** @type {Record<string, unknown>} */
|
|
157
|
+
this.params = options.params ?? {};
|
|
158
|
+
/** @type {number | undefined} */
|
|
159
|
+
this.status = options.status;
|
|
160
|
+
/** @type {boolean | undefined} */
|
|
161
|
+
this.retryable = options.retryable;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* A host programming error at a binding's construction or use — a
|
|
167
|
+
* handler table that names no operation, a missing ledger for a declared
|
|
168
|
+
* idempotency policy, a malformed request object handed to `dispatch`, a
|
|
169
|
+
* `ctx.status` outside 2xx. `TypeError`, thrown, never a wire response:
|
|
170
|
+
* the mistake is the host's, not the request's. The codes are
|
|
171
|
+
* `JC1001–JC1049` (docs/CONTRACT-FORMAT.md §7).
|
|
172
|
+
*/
|
|
173
|
+
export class ContractHostError extends TypeError {
|
|
174
|
+
/**
|
|
175
|
+
* @param {string} code
|
|
176
|
+
* @param {string} reason - The bare reason; `message` is `${code}: ${reason}`.
|
|
177
|
+
*/
|
|
178
|
+
constructor(code, reason) {
|
|
179
|
+
super(`${code}: ${reason}`);
|
|
180
|
+
this.name = 'ContractHostError';
|
|
181
|
+
/** @type {string} */
|
|
182
|
+
this.code = code;
|
|
183
|
+
/** @type {string} */
|
|
184
|
+
this.reason = reason;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* A declared operation failure as a handler returns it: pure JSON — the
|
|
190
|
+
* declared error `code`, the catalog `params` for its message, the wire
|
|
191
|
+
* `details` (validated against the declaration's schema when it has
|
|
192
|
+
* one) and whether the caller may retry. Made by `ContractFailure` and
|
|
193
|
+
* recognized by identity, never by shape: a hostile handler value cannot
|
|
194
|
+
* forge one and classifying it reads no property.
|
|
195
|
+
* @typedef {Object} ContractFailureValue
|
|
196
|
+
* @property {string} code
|
|
197
|
+
* @property {Readonly<Record<string, unknown>>} params
|
|
198
|
+
* @property {unknown} details - `undefined` when the failure carries none
|
|
199
|
+
* @property {boolean | null} retryable - `null` defers to the operation's retry policy
|
|
200
|
+
*/
|
|
201
|
+
|
|
202
|
+
/** The identity brand of every value `ContractFailure` produced. */
|
|
203
|
+
const failures = new WeakSet();
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Make a declared failure value: what a handler returns (or `ctx.fail`
|
|
207
|
+
* returns for it) to answer with one of the operation's declared error
|
|
208
|
+
* codes. A branded plain-object factory, not a class: the value crosses
|
|
209
|
+
* no binding as an `Error` and carries only JSON.
|
|
210
|
+
* @param {string} code - A code the operation declares in `errors`
|
|
211
|
+
* @param {Record<string, unknown>} [params] - Message parameters for the catalog
|
|
212
|
+
* @param {unknown} [details] - The wire `details` member
|
|
213
|
+
* @param {{ retryable?: boolean }} [options] - `retryable` overrides the
|
|
214
|
+
* default taken from the operation's `policy.retry.on`
|
|
215
|
+
* @returns {ContractFailureValue}
|
|
216
|
+
*/
|
|
217
|
+
export function ContractFailure(code, params, details, options) {
|
|
218
|
+
const value = Object.freeze({
|
|
219
|
+
code,
|
|
220
|
+
params: Object.freeze(params === undefined || params === null ? {} : { ...params }),
|
|
221
|
+
details,
|
|
222
|
+
retryable: options !== undefined && options !== null && typeof options.retryable === 'boolean' ? options.retryable : null,
|
|
223
|
+
});
|
|
224
|
+
failures.add(value);
|
|
225
|
+
return value;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* True exactly for a value `ContractFailure` produced. Reads nothing
|
|
230
|
+
* from the value, so it is total for a hostile object.
|
|
231
|
+
* @param {unknown} value
|
|
232
|
+
* @returns {value is ContractFailureValue}
|
|
233
|
+
*/
|
|
234
|
+
export function isContractFailure(value) {
|
|
235
|
+
return (typeof value === 'object' && value !== null) && failures.has(value);
|
|
236
|
+
}
|