@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,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The wire shapes of the HTTP server binding: the request-time
|
|
3
|
+
* error taxonomy as data (code → status, msgid, retryable), the error
|
|
4
|
+
* body every non-2xx JSON response carries, header reading, media
|
|
5
|
+
* matching, entity-tag comparison and the query decoder. Nothing here
|
|
6
|
+
* calls a handler or touches a ledger; `dispatch.js` composes these.
|
|
7
|
+
*
|
|
8
|
+
* Every response header name is lowercase; every error response carries
|
|
9
|
+
* `x-jaren-trace` (the server trace, `requestId` in the body) and
|
|
10
|
+
* `cache-control: no-store`. A message is rendered from the msgid
|
|
11
|
+
* through the catalog and never interpolates a request value — the
|
|
12
|
+
* parameters are the operation id, a declared limit, a media type, a
|
|
13
|
+
* method list, a declared header name or a declared error code
|
|
14
|
+
* (docs/CONTRACT-FORMAT.md §7).
|
|
15
|
+
*/
|
|
16
|
+
export type HttpRequest = {
|
|
17
|
+
method: string;
|
|
18
|
+
url: string;
|
|
19
|
+
headers: Readonly<Record<string, string | readonly string[]>>;
|
|
20
|
+
body: string | Uint8Array | null;
|
|
21
|
+
signal?: AbortSignal | null;
|
|
22
|
+
};
|
|
23
|
+
export type HttpResponse = {
|
|
24
|
+
status: number;
|
|
25
|
+
headers: Readonly<Record<string, string>>;
|
|
26
|
+
body: string | Uint8Array | null;
|
|
27
|
+
stream?: (sink: {
|
|
28
|
+
write: (chunk: string) => void;
|
|
29
|
+
end: () => void;
|
|
30
|
+
}) => (() => void);
|
|
31
|
+
};
|
|
32
|
+
export type WireErrorBody = {
|
|
33
|
+
/**
|
|
34
|
+
* - a `JC2xxx` code, or the declared error code
|
|
35
|
+
*/
|
|
36
|
+
code: string;
|
|
37
|
+
message: string;
|
|
38
|
+
/**
|
|
39
|
+
* - the server trace (`x-jaren-trace`)
|
|
40
|
+
*/
|
|
41
|
+
requestId: string;
|
|
42
|
+
details?: unknown;
|
|
43
|
+
retryable: boolean;
|
|
44
|
+
};
|
|
45
|
+
export type WireErrorRow = {
|
|
46
|
+
status: number;
|
|
47
|
+
msgid: string;
|
|
48
|
+
retryable: boolean;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* A request as the binding sees it — what an adapter builds and what a
|
|
52
|
+
* test hands to `dispatch` directly. `url` is origin-less: the path plus
|
|
53
|
+
* an optional `?query`; header names are lowercase; a header value is a
|
|
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.
|
|
57
|
+
* @typedef {Object} HttpRequest
|
|
58
|
+
* @property {string} method
|
|
59
|
+
* @property {string} url
|
|
60
|
+
* @property {Readonly<Record<string, string | readonly string[]>>} headers
|
|
61
|
+
* @property {string | Uint8Array | null} body
|
|
62
|
+
* @property {AbortSignal | null} [signal]
|
|
63
|
+
*/
|
|
64
|
+
/**
|
|
65
|
+
* 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
|
|
68
|
+
* under `accept: text/event-stream`) carries `body: null` plus `stream`:
|
|
69
|
+
* the adapter writes the headers, then MUST call `stream` exactly once
|
|
70
|
+
* 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.
|
|
73
|
+
* @typedef {Object} HttpResponse
|
|
74
|
+
* @property {number} status
|
|
75
|
+
* @property {Readonly<Record<string, string>>} headers
|
|
76
|
+
* @property {string | Uint8Array | null} body
|
|
77
|
+
* @property {(sink: { write: (chunk: string) => void, end: () => void }) => (() => void)} [stream]
|
|
78
|
+
*/
|
|
79
|
+
/**
|
|
80
|
+
* The D7 error body of every non-2xx JSON response.
|
|
81
|
+
* @typedef {Object} WireErrorBody
|
|
82
|
+
* @property {string} code - a `JC2xxx` code, or the declared error code
|
|
83
|
+
* @property {string} message
|
|
84
|
+
* @property {string} requestId - the server trace (`x-jaren-trace`)
|
|
85
|
+
* @property {unknown} [details]
|
|
86
|
+
* @property {boolean} retryable
|
|
87
|
+
*/
|
|
88
|
+
/**
|
|
89
|
+
* The taxonomy row of one request-time code.
|
|
90
|
+
* @typedef {{ status: number, msgid: string, retryable: boolean }} WireErrorRow
|
|
91
|
+
*/
|
|
92
|
+
/**
|
|
93
|
+
* The request-time taxonomy as data: code → `{ status, msgid, retryable }`.
|
|
94
|
+
* The normative table is docs/CONTRACT-FORMAT.md §7; a test holds the
|
|
95
|
+
* two equal, and equal to `CONTRACT_CODES` and the English catalog. The
|
|
96
|
+
* client binding assembles outcomes from exactly these statuses.
|
|
97
|
+
*/
|
|
98
|
+
export declare const HTTP_ERRORS: Readonly<{
|
|
99
|
+
JC2001: Readonly<{
|
|
100
|
+
status: 404;
|
|
101
|
+
msgid: "contract/not-found";
|
|
102
|
+
retryable: false;
|
|
103
|
+
}>;
|
|
104
|
+
JC2002: Readonly<{
|
|
105
|
+
status: 405;
|
|
106
|
+
msgid: "contract/method-not-allowed";
|
|
107
|
+
retryable: false;
|
|
108
|
+
}>;
|
|
109
|
+
JC2003: Readonly<{
|
|
110
|
+
status: 413;
|
|
111
|
+
msgid: "contract/body-too-large";
|
|
112
|
+
retryable: false;
|
|
113
|
+
}>;
|
|
114
|
+
JC2004: Readonly<{
|
|
115
|
+
status: 415;
|
|
116
|
+
msgid: "contract/unsupported-media";
|
|
117
|
+
retryable: false;
|
|
118
|
+
}>;
|
|
119
|
+
JC2005: Readonly<{
|
|
120
|
+
status: 400;
|
|
121
|
+
msgid: "contract/malformed-json";
|
|
122
|
+
retryable: false;
|
|
123
|
+
}>;
|
|
124
|
+
JC2006: Readonly<{
|
|
125
|
+
status: 400;
|
|
126
|
+
msgid: "contract/invalid-input";
|
|
127
|
+
retryable: false;
|
|
128
|
+
}>;
|
|
129
|
+
JC2007: Readonly<{
|
|
130
|
+
status: 400;
|
|
131
|
+
msgid: "contract/idempotency-key-required";
|
|
132
|
+
retryable: false;
|
|
133
|
+
}>;
|
|
134
|
+
JC2008: Readonly<{
|
|
135
|
+
status: 500;
|
|
136
|
+
msgid: "contract/handler-failed";
|
|
137
|
+
retryable: false;
|
|
138
|
+
}>;
|
|
139
|
+
JC2009: Readonly<{
|
|
140
|
+
status: 409;
|
|
141
|
+
msgid: "contract/idempotency-conflict";
|
|
142
|
+
retryable: false;
|
|
143
|
+
}>;
|
|
144
|
+
JC2010: Readonly<{
|
|
145
|
+
status: 500;
|
|
146
|
+
msgid: "contract/invalid-output";
|
|
147
|
+
retryable: false;
|
|
148
|
+
}>;
|
|
149
|
+
JC2011: Readonly<{
|
|
150
|
+
status: 400;
|
|
151
|
+
msgid: "contract/malformed-path";
|
|
152
|
+
retryable: false;
|
|
153
|
+
}>;
|
|
154
|
+
JC2012: Readonly<{
|
|
155
|
+
status: 400;
|
|
156
|
+
msgid: "contract/malformed-query";
|
|
157
|
+
retryable: false;
|
|
158
|
+
}>;
|
|
159
|
+
JC2013: Readonly<{
|
|
160
|
+
status: 501;
|
|
161
|
+
msgid: "contract/not-implemented";
|
|
162
|
+
retryable: false;
|
|
163
|
+
}>;
|
|
164
|
+
JC2014: Readonly<{
|
|
165
|
+
status: 412;
|
|
166
|
+
msgid: "contract/precondition-failed";
|
|
167
|
+
retryable: false;
|
|
168
|
+
}>;
|
|
169
|
+
JC2015: Readonly<{
|
|
170
|
+
status: 400;
|
|
171
|
+
msgid: "contract/invalid-header";
|
|
172
|
+
retryable: false;
|
|
173
|
+
}>;
|
|
174
|
+
}>;
|
|
175
|
+
/** The msgid of a declared operation error that has no message of its own. */
|
|
176
|
+
export declare const HANDLER_ERROR_MSGID = "contract/handler-error";
|
|
177
|
+
/** The default well-known negotiation path, served by `serveHttp` and asked by the client. */
|
|
178
|
+
export declare const WELL_KNOWN_PATH = "/.well-known/jaren-contract";
|
|
179
|
+
/** The default JSON media of the binding. */
|
|
180
|
+
export declare const JSON_MEDIA = "application/json";
|
|
181
|
+
/** The response `content-type` of a JSON body. */
|
|
182
|
+
export declare const JSON_CONTENT_TYPE = "application/json; charset=utf-8";
|
|
183
|
+
export type Catalog = Readonly<Record<string, (params: object, error?: object) => string>>;
|
|
184
|
+
/**
|
|
185
|
+
* A compiled catalog: msgid → render.
|
|
186
|
+
* @typedef {Readonly<Record<string, (params: object, error?: object) => string>>} Catalog
|
|
187
|
+
*/
|
|
188
|
+
/**
|
|
189
|
+
* Render a message: the host catalog first, the English catalog second,
|
|
190
|
+
* a fixed fallback last. TOTAL: a rendering closure that throws yields
|
|
191
|
+
* the fallback rather than escaping into the response path.
|
|
192
|
+
* @param {Catalog | null} catalog - the host catalog, or null for English only
|
|
193
|
+
* @param {string} msgid
|
|
194
|
+
* @param {Record<string, unknown>} params
|
|
195
|
+
* @returns {string}
|
|
196
|
+
*/
|
|
197
|
+
export declare function renderMessage(catalog: Catalog | null, msgid: string, params: Record<string, unknown>): string;
|
|
198
|
+
/**
|
|
199
|
+
* The message of a declared operation error, as every server-side
|
|
200
|
+
* binding renders it: `contract/error/<code>` from the host catalog when
|
|
201
|
+
* it defines one, else the generic `contract/handler-error` — with the
|
|
202
|
+
* failure's own params plus `op` and `code`.
|
|
203
|
+
* @param {Catalog | null} catalog
|
|
204
|
+
* @param {string} op - the operation id
|
|
205
|
+
* @param {string} code - the declared error code
|
|
206
|
+
* @param {Readonly<Record<string, unknown>>} params
|
|
207
|
+
* @returns {string}
|
|
208
|
+
*/
|
|
209
|
+
export declare function declaredMessage(catalog: Catalog | null, op: string, code: string, params: Readonly<Record<string, unknown>>): string;
|
|
210
|
+
/**
|
|
211
|
+
* The single string value of a header: a repeated field is combined with
|
|
212
|
+
* `, ` (RFC 9110 §5.3), an absent one is `undefined`. For the protocol
|
|
213
|
+
* headers the binding itself reads (`content-type`, `content-length`,
|
|
214
|
+
* `idempotency-key`, `if-match`, `if-none-match`).
|
|
215
|
+
* @param {Readonly<Record<string, string | readonly string[]>>} headers
|
|
216
|
+
* @param {string} name - lowercase
|
|
217
|
+
* @returns {string | undefined}
|
|
218
|
+
*/
|
|
219
|
+
export declare function headerValue(headers: Readonly<Record<string, string | readonly string[]>>, name: string): string | undefined;
|
|
220
|
+
/**
|
|
221
|
+
* The declared `content-length` as a non-negative integer, or `-1` when
|
|
222
|
+
* absent or not a plain decimal number (a malformed value never blocks
|
|
223
|
+
* the read-length check that follows).
|
|
224
|
+
* @param {Readonly<Record<string, string | readonly string[]>>} headers
|
|
225
|
+
* @returns {number}
|
|
226
|
+
*/
|
|
227
|
+
export declare function contentLength(headers: Readonly<Record<string, string | readonly string[]>>): number;
|
|
228
|
+
/**
|
|
229
|
+
* Whether a request `content-type` names the operation's media:
|
|
230
|
+
* type/subtype compared case-insensitively, parameters ignored, and a
|
|
231
|
+
* `+json` structured-syntax suffix accepted for `application/json`.
|
|
232
|
+
* @param {string | undefined} contentType
|
|
233
|
+
* @param {string} media - the operation's declared media
|
|
234
|
+
* @returns {boolean}
|
|
235
|
+
*/
|
|
236
|
+
export declare function mediaMatches(contentType: string | undefined, media: string): boolean;
|
|
237
|
+
/**
|
|
238
|
+
* The byte length of a body — text measured as UTF-8. Cheap on the
|
|
239
|
+
* common path: a string shorter than the limit in code units cannot
|
|
240
|
+
* exceed it, and one longer than a third of the limit in code units is
|
|
241
|
+
* measured exactly only when it could.
|
|
242
|
+
* @param {string | Uint8Array} body
|
|
243
|
+
* @param {number} limit
|
|
244
|
+
* @returns {boolean} true when the body exceeds `limit` bytes
|
|
245
|
+
*/
|
|
246
|
+
export declare function exceedsBytes(body: string | Uint8Array, limit: number): boolean;
|
|
247
|
+
/**
|
|
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
|
|
250
|
+
* (`W/"x"` → `x`) — the caller decides weak/strong comparison because a
|
|
251
|
+
* strong comparison must reject weak tags, which `weak[i]` records.
|
|
252
|
+
* @param {string} value
|
|
253
|
+
* @returns {{ any: boolean, tags: string[], weak: boolean[] }}
|
|
254
|
+
*/
|
|
255
|
+
export declare function parseEntityTags(value: string): {
|
|
256
|
+
any: boolean;
|
|
257
|
+
tags: string[];
|
|
258
|
+
weak: boolean[];
|
|
259
|
+
};
|
|
260
|
+
/**
|
|
261
|
+
* Whether a conditional header matches the tag the handler armed.
|
|
262
|
+
* Weak comparison ignores the weak indicators (RFC 9110 §8.8.3.2);
|
|
263
|
+
* strong comparison requires both tags strong.
|
|
264
|
+
* @param {string} header - the raw `if-match`/`if-none-match` value
|
|
265
|
+
* @param {string} tag - the armed opaque tag
|
|
266
|
+
* @param {boolean} tagStrong - whether the armed tag is strong
|
|
267
|
+
* @param {boolean} strong - strong comparison
|
|
268
|
+
* @returns {boolean}
|
|
269
|
+
*/
|
|
270
|
+
export declare function entityTagMatches(header: string, tag: string, tagStrong: boolean, strong: boolean): boolean;
|
|
271
|
+
/**
|
|
272
|
+
* The `etag` header value of an armed tag.
|
|
273
|
+
* @param {string} tag
|
|
274
|
+
* @param {boolean} strong
|
|
275
|
+
* @returns {string}
|
|
276
|
+
*/
|
|
277
|
+
export declare function formatEntityTag(tag: string, strong: boolean): string;
|
|
278
|
+
/**
|
|
279
|
+
* Decode a query string into the declared members of an input object:
|
|
280
|
+
* only declared names are set (an undeclared key is never merged, so no
|
|
281
|
+
* request can smuggle a member); a `repeated` member collects every
|
|
282
|
+
* occurrence into an array, every other member is last-wins; a `+` is a
|
|
283
|
+
* space and escapes decode as `application/x-www-form-urlencoded`
|
|
284
|
+
* (`URLSearchParams`). Returns `false` when the query is not decodable
|
|
285
|
+
* (a malformed percent-escape or invalid UTF-8) — the `JC2012` case.
|
|
286
|
+
* @param {string} query - the part after `?`, possibly empty
|
|
287
|
+
* @param {ReadonlySet<string>} declared - the query member names
|
|
288
|
+
* @param {ReadonlySet<string>} repeated - the array-typed ones
|
|
289
|
+
* @param {Record<string, unknown>} out - the input object under assembly
|
|
290
|
+
* @returns {boolean} false when not decodable
|
|
291
|
+
*/
|
|
292
|
+
export declare function decodeQuery(query: string, declared: ReadonlySet<string>, repeated: ReadonlySet<string>, out: Record<string, unknown>): boolean;
|
|
293
|
+
/**
|
|
294
|
+
* The verdict of a compiled validator under either contract: the
|
|
295
|
+
* default `{ valid, errors }` or a host-injected boolean validator.
|
|
296
|
+
* TOTAL: a validator that throws is a failed verdict carrying the throw.
|
|
297
|
+
* Shared by the server pipeline and the client.
|
|
298
|
+
* @param {(value: unknown) => any} validate
|
|
299
|
+
* @param {unknown} value
|
|
300
|
+
* @returns {{ valid: boolean, errors: any[], thrown: unknown }}
|
|
301
|
+
*/
|
|
302
|
+
export declare function verdict(validate: (value: unknown) => any, value: unknown): {
|
|
303
|
+
valid: boolean;
|
|
304
|
+
errors: any[];
|
|
305
|
+
thrown: unknown;
|
|
306
|
+
};
|
|
307
|
+
/**
|
|
308
|
+
* The details member of a validation failure by policy: `none` → absent,
|
|
309
|
+
* `paths` → `[{ path, keyword }]`, `full` → the validator's own error
|
|
310
|
+
* records (`toJSON()` when it has one) with their `params`.
|
|
311
|
+
* @param {'none' | 'paths' | 'full'} policy
|
|
312
|
+
* @param {any[]} errors - the validator's error list
|
|
313
|
+
* @returns {unknown}
|
|
314
|
+
*/
|
|
315
|
+
export declare function projectValidationDetails(policy: 'none' | 'paths' | 'full', errors: any[]): unknown;
|
|
316
|
+
/**
|
|
317
|
+
* Build the D7 error body and the response around it. `override` is the
|
|
318
|
+
* host's `errorBody` option: called with the wire record (the body plus
|
|
319
|
+
* `status`) and the request context; TOTAL — a projector that throws or
|
|
320
|
+
* returns a non-JSON value falls back to the D7 shape.
|
|
321
|
+
* @param {number} status
|
|
322
|
+
* @param {string} code
|
|
323
|
+
* @param {string} message
|
|
324
|
+
* @param {string} trace
|
|
325
|
+
* @param {unknown} details - `undefined` for none
|
|
326
|
+
* @param {boolean} retryable
|
|
327
|
+
* @param {Readonly<Record<string, string>> | null} extraHeaders - `allow`, `retry-after`, `etag`
|
|
328
|
+
* @param {((wire: WireErrorBody & { status: number }, ctx: any) => unknown) | null} override
|
|
329
|
+
* @param {any} ctx - the request context, or null before an operation matched
|
|
330
|
+
* @returns {HttpResponse}
|
|
331
|
+
*/
|
|
332
|
+
export declare function errorResponse(status: number, code: string, message: string, trace: string, details: unknown, retryable: boolean, extraHeaders: Readonly<Record<string, string>> | null, override: ((wire: WireErrorBody & {
|
|
333
|
+
status: number;
|
|
334
|
+
}, ctx: any) => unknown) | null, ctx: any): HttpResponse;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Public surface of @jarenjs/contract: `compileContract` turns a
|
|
3
|
+
* `$contract` document into a frozen `Contract` (docs/CONTRACT-FORMAT.md);
|
|
4
|
+
* the error classes, the `JC` code table and the `ContractFailure`
|
|
5
|
+
* factory are what a host catches, reads and returns. The bindings live
|
|
6
|
+
* behind their own subpaths (`./http`, `./fetch`, `./node`, `./ledger`)
|
|
7
|
+
* so a consumer that only compiles never loads them. The path matcher is
|
|
8
|
+
* deliberately NOT exported — it is reached only through `contract.match`
|
|
9
|
+
* and `contract.allowed`.
|
|
10
|
+
*/
|
|
11
|
+
export { compileContract } from './compile.js';
|
|
12
|
+
export { ContractCompileError, ContractRuntimeError, ContractHostError, ContractFailure, isContractFailure, CONTRACT_CODES, } from './errors.js';
|
|
13
|
+
export { contractMessagesEn, contractCatalogEn } from './messages.js';
|
|
14
|
+
export type Contract = import('./compile.js').Contract;
|
|
15
|
+
export type CompiledOperation = import('./compile.js').CompiledOperation;
|
|
16
|
+
export type CompiledHttp = import('./compile.js').CompiledHttp;
|
|
17
|
+
export type CompiledPolicy = import('./compile.js').CompiledPolicy;
|
|
18
|
+
export type CompiledInput = import('./compile.js').CompiledInput;
|
|
19
|
+
export type CompiledOutput = import('./compile.js').CompiledOutput;
|
|
20
|
+
export type CompiledErrorDecl = import('./compile.js').CompiledErrorDecl;
|
|
21
|
+
export type InputTransport = import('./compile.js').InputTransport;
|
|
22
|
+
export type CompileContractOptions = import('./compile.js').CompileContractOptions;
|
|
23
|
+
export type ContractDescription = import('./describe.js').ContractDescription;
|
|
24
|
+
export type OperationDescription = import('./describe.js').OperationDescription;
|
|
25
|
+
export type ContractFailureValue = import('./errors.js').ContractFailureValue;
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {import('./compile.js').Contract} Contract
|
|
28
|
+
* @typedef {import('./compile.js').CompiledOperation} CompiledOperation
|
|
29
|
+
* @typedef {import('./compile.js').CompiledHttp} CompiledHttp
|
|
30
|
+
* @typedef {import('./compile.js').CompiledPolicy} CompiledPolicy
|
|
31
|
+
* @typedef {import('./compile.js').CompiledInput} CompiledInput
|
|
32
|
+
* @typedef {import('./compile.js').CompiledOutput} CompiledOutput
|
|
33
|
+
* @typedef {import('./compile.js').CompiledErrorDecl} CompiledErrorDecl
|
|
34
|
+
* @typedef {import('./compile.js').InputTransport} InputTransport
|
|
35
|
+
* @typedef {import('./compile.js').CompileContractOptions} CompileContractOptions
|
|
36
|
+
* @typedef {import('./describe.js').ContractDescription} ContractDescription
|
|
37
|
+
* @typedef {import('./describe.js').OperationDescription} OperationDescription
|
|
38
|
+
* @typedef {import('./errors.js').ContractFailureValue} ContractFailureValue
|
|
39
|
+
*/
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Idempotency as data: the ledger INTERFACE the http server
|
|
3
|
+
* binding calls when an operation declares `policy.idempotency`, a
|
|
4
|
+
* `createMemoryLedger` reference implementation for tests and
|
|
5
|
+
* single-process hosts, and the two documents a durable host opens with
|
|
6
|
+
* the rest of the suite — `idempotencyLedgerModel` (a `$model` 0.1
|
|
7
|
+
* document for `@jarenjs/db`) and `commandLifecycleFsm` (a `$fsm` 0.1
|
|
8
|
+
* document for `@jarenjs/flow`). Both are JSON only: this package never
|
|
9
|
+
* imports db or flow (docs/CONTRACT-FORMAT.md §8).
|
|
10
|
+
*
|
|
11
|
+
* The three identities stay apart here as everywhere: the idempotency
|
|
12
|
+
* KEY is the caller's (sent as `Idempotency-Key`, scoped by the host's
|
|
13
|
+
* `scope`), the request HASH is the binding's (SHA-256 over the RFC 8785
|
|
14
|
+
* canonical input), and the TRACE is never stored — a replay carries a
|
|
15
|
+
* fresh one.
|
|
16
|
+
*/
|
|
17
|
+
export type LedgerRecord = {
|
|
18
|
+
/**
|
|
19
|
+
* - `"<op>|<scope>|<key>"`
|
|
20
|
+
*/
|
|
21
|
+
id: string;
|
|
22
|
+
op: string;
|
|
23
|
+
scope: string;
|
|
24
|
+
key: string;
|
|
25
|
+
/**
|
|
26
|
+
* - lowercase hex SHA-256 over the canonical input
|
|
27
|
+
*/
|
|
28
|
+
hash: string;
|
|
29
|
+
status: 'started' | 'committed' | 'failed';
|
|
30
|
+
/**
|
|
31
|
+
* - the stored `{ status, headers, body }`, or null
|
|
32
|
+
*/
|
|
33
|
+
response: any;
|
|
34
|
+
/**
|
|
35
|
+
* - of a failed record; null otherwise
|
|
36
|
+
*/
|
|
37
|
+
retryable: boolean | null;
|
|
38
|
+
/**
|
|
39
|
+
* - epoch ms
|
|
40
|
+
*/
|
|
41
|
+
createdAt: number;
|
|
42
|
+
/**
|
|
43
|
+
* - epoch ms
|
|
44
|
+
*/
|
|
45
|
+
updatedAt: number;
|
|
46
|
+
/**
|
|
47
|
+
* - epoch ms
|
|
48
|
+
*/
|
|
49
|
+
expiresAt: number;
|
|
50
|
+
};
|
|
51
|
+
export type ClaimResult = {
|
|
52
|
+
state: 'new';
|
|
53
|
+
ref: unknown;
|
|
54
|
+
} | {
|
|
55
|
+
state: 'replay';
|
|
56
|
+
response: any;
|
|
57
|
+
} | {
|
|
58
|
+
state: 'in-progress';
|
|
59
|
+
} | {
|
|
60
|
+
state: 'mismatch';
|
|
61
|
+
};
|
|
62
|
+
export type Ledger = {
|
|
63
|
+
claim: (claim: {
|
|
64
|
+
op: string;
|
|
65
|
+
scope: string;
|
|
66
|
+
key: string;
|
|
67
|
+
hash: string;
|
|
68
|
+
now?: number;
|
|
69
|
+
}) => ClaimResult | Promise<ClaimResult>;
|
|
70
|
+
commit: (ref: unknown, response: any) => void | Promise<void>;
|
|
71
|
+
fail: (ref: unknown, retryable: boolean, response?: any) => void | Promise<void>;
|
|
72
|
+
lookup: (key: {
|
|
73
|
+
op: string;
|
|
74
|
+
scope: string;
|
|
75
|
+
key: string;
|
|
76
|
+
}) => LedgerRecord | null | Promise<LedgerRecord | null>;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* The reference ledger over a `Map`: synchronous, single-process,
|
|
80
|
+
* expiring on `claim` (a record past `expiresAt` is dropped and the key
|
|
81
|
+
* is `new` again). `sweep()` drops every expired record — a host may
|
|
82
|
+
* call it on a timer.
|
|
83
|
+
* @param {{ ttlMs?: number, now?: () => number }} [options]
|
|
84
|
+
* @returns {Ledger & { sweep(): number, size: number }}
|
|
85
|
+
*/
|
|
86
|
+
export declare function createMemoryLedger(options?: {
|
|
87
|
+
ttlMs?: number;
|
|
88
|
+
now?: () => number;
|
|
89
|
+
}): Ledger & {
|
|
90
|
+
sweep(): number;
|
|
91
|
+
size: number;
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* 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.
|
|
100
|
+
*/
|
|
101
|
+
export declare const idempotencyLedgerModel: Readonly<{
|
|
102
|
+
$model: "0.1";
|
|
103
|
+
collections: {
|
|
104
|
+
ledger: {
|
|
105
|
+
schema: {
|
|
106
|
+
type: string;
|
|
107
|
+
required: string[];
|
|
108
|
+
properties: {
|
|
109
|
+
id: {
|
|
110
|
+
type: string;
|
|
111
|
+
minLength: number;
|
|
112
|
+
};
|
|
113
|
+
op: {
|
|
114
|
+
type: string;
|
|
115
|
+
minLength: number;
|
|
116
|
+
};
|
|
117
|
+
scope: {
|
|
118
|
+
type: string;
|
|
119
|
+
};
|
|
120
|
+
key: {
|
|
121
|
+
type: string;
|
|
122
|
+
minLength: number;
|
|
123
|
+
};
|
|
124
|
+
hash: {
|
|
125
|
+
type: string;
|
|
126
|
+
pattern: string;
|
|
127
|
+
};
|
|
128
|
+
status: {
|
|
129
|
+
type: string;
|
|
130
|
+
enum: string[];
|
|
131
|
+
};
|
|
132
|
+
response: {
|
|
133
|
+
oneOf: ({
|
|
134
|
+
type: string;
|
|
135
|
+
required?: undefined;
|
|
136
|
+
properties?: undefined;
|
|
137
|
+
} | {
|
|
138
|
+
type: string;
|
|
139
|
+
required: string[];
|
|
140
|
+
properties: {
|
|
141
|
+
status: {
|
|
142
|
+
type: string;
|
|
143
|
+
minimum: number;
|
|
144
|
+
maximum: number;
|
|
145
|
+
};
|
|
146
|
+
headers: {
|
|
147
|
+
type: string;
|
|
148
|
+
additionalProperties: {
|
|
149
|
+
type: string;
|
|
150
|
+
};
|
|
151
|
+
};
|
|
152
|
+
body: {
|
|
153
|
+
type: string[];
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
})[];
|
|
157
|
+
};
|
|
158
|
+
retryable: {
|
|
159
|
+
type: string[];
|
|
160
|
+
};
|
|
161
|
+
createdAt: {
|
|
162
|
+
type: string;
|
|
163
|
+
};
|
|
164
|
+
updatedAt: {
|
|
165
|
+
type: string;
|
|
166
|
+
};
|
|
167
|
+
expiresAt: {
|
|
168
|
+
type: string;
|
|
169
|
+
};
|
|
170
|
+
};
|
|
171
|
+
additionalProperties: boolean;
|
|
172
|
+
};
|
|
173
|
+
key: string;
|
|
174
|
+
indexes: {
|
|
175
|
+
name: string;
|
|
176
|
+
path: string;
|
|
177
|
+
}[];
|
|
178
|
+
};
|
|
179
|
+
};
|
|
180
|
+
}>;
|
|
181
|
+
/**
|
|
182
|
+
* The `$fsm` 0.1 document of one command's lifecycle under an
|
|
183
|
+
* idempotency key: `idle → started` on `claim`, `started → committed` on
|
|
184
|
+
* `commit`, `started → failed` on `fail`, and `failed → started` on
|
|
185
|
+
* `claim` only when the failure was retryable (`$.context.retryable`).
|
|
186
|
+
* A host compiles it with `@jarenjs/flow` to drive or audit a durable
|
|
187
|
+
* ledger; the memory ledger walks exactly these transitions.
|
|
188
|
+
*/
|
|
189
|
+
export declare const commandLifecycleFsm: Readonly<{
|
|
190
|
+
$fsm: "0.1";
|
|
191
|
+
initial: "idle";
|
|
192
|
+
states: (string | {
|
|
193
|
+
id: string;
|
|
194
|
+
final: boolean;
|
|
195
|
+
})[];
|
|
196
|
+
transitions: ({
|
|
197
|
+
from: string;
|
|
198
|
+
event: string;
|
|
199
|
+
to: string;
|
|
200
|
+
guard?: undefined;
|
|
201
|
+
} | {
|
|
202
|
+
from: string;
|
|
203
|
+
event: string;
|
|
204
|
+
guard: string;
|
|
205
|
+
to: string;
|
|
206
|
+
})[];
|
|
207
|
+
}>;
|