@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.
Files changed (84) hide show
  1. package/README.md +508 -0
  2. package/dist/types/adapters/fetch.d.ts +27 -0
  3. package/dist/types/adapters/node.d.ts +47 -0
  4. package/dist/types/app/binding.d.ts +122 -0
  5. package/dist/types/app/effect.d.ts +77 -0
  6. package/dist/types/app/index.d.ts +31 -0
  7. package/dist/types/app/subscription.d.ts +82 -0
  8. package/dist/types/bundle.d.ts +43 -0
  9. package/dist/types/cli.d.ts +15 -0
  10. package/dist/types/client/http.d.ts +242 -0
  11. package/dist/types/client/outcome.d.ts +289 -0
  12. package/dist/types/compat.d.ts +36 -0
  13. package/dist/types/compile.d.ts +196 -0
  14. package/dist/types/describe.d.ts +115 -0
  15. package/dist/types/diff.d.ts +91 -0
  16. package/dist/types/errors.d.ts +205 -0
  17. package/dist/types/http/dispatch.d.ts +148 -0
  18. package/dist/types/http/serve.d.ts +154 -0
  19. package/dist/types/http/wire.d.ts +334 -0
  20. package/dist/types/index.d.ts +39 -0
  21. package/dist/types/ledger.d.ts +207 -0
  22. package/dist/types/local/index.d.ts +127 -0
  23. package/dist/types/messages.d.ts +63 -0
  24. package/dist/types/path.d.ts +119 -0
  25. package/dist/types/pipeline.d.ts +157 -0
  26. package/dist/types/port/client.d.ts +142 -0
  27. package/dist/types/port/frame.d.ts +195 -0
  28. package/dist/types/port/serve.d.ts +102 -0
  29. package/dist/types/project/index.d.ts +34 -0
  30. package/dist/types/project/markdown.d.ts +28 -0
  31. package/dist/types/project/openapi.d.ts +102 -0
  32. package/dist/types/project/tools.d.ts +57 -0
  33. package/dist/types/project/typescript.d.ts +59 -0
  34. package/dist/types/public.d.ts +73 -0
  35. package/dist/types/revision.d.ts +36 -0
  36. package/dist/types/stream/client.d.ts +104 -0
  37. package/dist/types/stream/server.d.ts +106 -0
  38. package/dist/types/stream/sse.d.ts +62 -0
  39. package/docs/APP-INTEGRATION.md +301 -0
  40. package/docs/CONTRACT-FORMAT.md +1923 -0
  41. package/package.json +110 -0
  42. package/schemas/jaren-contract-port.draft-07.schema.json +241 -0
  43. package/schemas/jaren-contract-port.schema.json +241 -0
  44. package/schemas/jaren-contract.draft-07.schema.json +287 -0
  45. package/schemas/jaren-contract.schema.json +287 -0
  46. package/src/adapters/fetch.js +109 -0
  47. package/src/adapters/node.js +238 -0
  48. package/src/app/binding.js +426 -0
  49. package/src/app/effect.js +190 -0
  50. package/src/app/index.js +26 -0
  51. package/src/app/subscription.js +130 -0
  52. package/src/bundle.js +168 -0
  53. package/src/cli.js +264 -0
  54. package/src/client/http.js +1150 -0
  55. package/src/client/outcome.js +364 -0
  56. package/src/compat.js +62 -0
  57. package/src/compile.js +1162 -0
  58. package/src/describe.js +109 -0
  59. package/src/diff.js +610 -0
  60. package/src/errors.js +236 -0
  61. package/src/http/dispatch.js +1054 -0
  62. package/src/http/serve.js +301 -0
  63. package/src/http/wire.js +469 -0
  64. package/src/index.js +33 -0
  65. package/src/ledger.js +225 -0
  66. package/src/local/index.js +363 -0
  67. package/src/messages.js +68 -0
  68. package/src/path.js +471 -0
  69. package/src/pipeline.js +241 -0
  70. package/src/port/client.js +518 -0
  71. package/src/port/frame.js +196 -0
  72. package/src/port/serve.js +442 -0
  73. package/src/project/index.js +29 -0
  74. package/src/project/markdown.js +244 -0
  75. package/src/project/openapi.js +564 -0
  76. package/src/project/openapi.jslt.json +149 -0
  77. package/src/project/tools.js +139 -0
  78. package/src/project/typescript.js +152 -0
  79. package/src/project/typescript.jtlt.json +72 -0
  80. package/src/public.js +206 -0
  81. package/src/revision.js +90 -0
  82. package/src/stream/client.js +212 -0
  83. package/src/stream/server.js +306 -0
  84. 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
+ }