@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/pipeline.js
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file The transport-neutral core of every server-side binding: validate
|
|
4
|
+
* the assembled input, call the handler through ONE uniform promise
|
|
5
|
+
* boundary — a synchronous throw, a non-promise return and a rejection
|
|
6
|
+
* settle exactly alike — classify the settlement (a declared operation
|
|
7
|
+
* failure, with its details validated against the declaration's schema,
|
|
8
|
+
* or a host fault), and validate the output. The HTTP binding wraps the
|
|
9
|
+
* result in statuses, headers and bodies; the `local` and `port`
|
|
10
|
+
* bindings wrap it in D6 outcomes and frames — the classification is
|
|
11
|
+
* decided here once so the three can never disagree.
|
|
12
|
+
*
|
|
13
|
+
* Total for everything a handler can do: a hostile value whose `then`
|
|
14
|
+
* accessor throws is a rejection at the boundary (`JC2008`-class), a
|
|
15
|
+
* value whose other members throw survives it and dies in output
|
|
16
|
+
* validation (`JC2010`-class); nothing here throws for a settlement and
|
|
17
|
+
* nothing renders a message — the binding renders at its edge.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { isJsonValue } from '@jarenjs/core/object';
|
|
21
|
+
|
|
22
|
+
import { ContractRuntimeError, isContractFailure } from './errors.js';
|
|
23
|
+
import { HTTP_ERRORS, verdict, projectValidationDetails } from './http/wire.js';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {import('./compile.js').CompiledOperation} CompiledOperation
|
|
27
|
+
* @typedef {import('./compile.js').CompiledErrorDecl} CompiledErrorDecl
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The neutral subset of a binding's prepared route — what the pipeline
|
|
32
|
+
* reads per operation. The HTTP binding's `Route` carries these members
|
|
33
|
+
* verbatim; the `local` and `port` bindings prepare exactly this.
|
|
34
|
+
* @typedef {Object} PipelineRoute
|
|
35
|
+
* @property {CompiledOperation} op
|
|
36
|
+
* @property {((input: any, ctx: any) => unknown) | null} handler
|
|
37
|
+
* @property {boolean} raw - opaque: the value is the binding's to check, never the output validator's
|
|
38
|
+
* @property {((value: unknown) => any) | null} validateInput
|
|
39
|
+
* @property {(value: unknown) => any} validateOutput
|
|
40
|
+
* @property {'none' | 'paths' | 'full'} details
|
|
41
|
+
* @property {Readonly<Record<string, CompiledErrorDecl>>} errors
|
|
42
|
+
* @property {ReadonlySet<string>} retryOn
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The classified settlement of one operation:
|
|
47
|
+
*
|
|
48
|
+
* - `value` — the handler's output, validated;
|
|
49
|
+
* - `failure` — a declared operation error (`errors[code]`), its
|
|
50
|
+
* `details` validated, `retryable` from the failure or the retry
|
|
51
|
+
* policy, `status` the declaration's (a status-less binding ignores
|
|
52
|
+
* it);
|
|
53
|
+
* - `contract` — the host broke the contract: `JC2006` the input fails
|
|
54
|
+
* its validator (`details` by `policy.errors.details`), `JC2008` the
|
|
55
|
+
* handler threw a non-declared error, rejected or answered an
|
|
56
|
+
* undeclared code, `JC2010` the output or a declared error's details
|
|
57
|
+
* fail their schema. `cause` is for the binding's `onError` observer
|
|
58
|
+
* (`undefined` when there is nothing to report); it never crosses a
|
|
59
|
+
* wire.
|
|
60
|
+
*
|
|
61
|
+
* @typedef {{ kind: 'value', value: unknown }
|
|
62
|
+
* | { kind: 'failure', code: string, params: Readonly<Record<string, unknown>>, details: unknown, retryable: boolean, status: number }
|
|
63
|
+
* | { kind: 'contract', code: 'JC2006' | 'JC2008' | 'JC2010', details: unknown, cause: unknown }} OperationResult
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The request-time codes of the `local` and `port` bindings —
|
|
68
|
+
* code → `{ msgid, retryable }`, the same table-as-data shape as
|
|
69
|
+
* `HTTP_ERRORS` and `CLIENT_ERRORS`. The normative table is
|
|
70
|
+
* docs/CONTRACT-FORMAT.md §15–§16; a test holds them equal.
|
|
71
|
+
*/
|
|
72
|
+
export const PORT_LOCAL_ERRORS = Object.freeze({
|
|
73
|
+
JC2070: Object.freeze({ msgid: 'contract/local-handler-failed', retryable: false }),
|
|
74
|
+
JC2071: Object.freeze({ msgid: 'contract/unknown-operation', retryable: false }),
|
|
75
|
+
JC2072: Object.freeze({ msgid: 'contract/port-timeout', retryable: true }),
|
|
76
|
+
JC2073: Object.freeze({ msgid: 'contract/malformed-frame', retryable: false }),
|
|
77
|
+
JC2074: Object.freeze({ msgid: 'contract/channel-closed', retryable: false }),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {'JC2006' | 'JC2008' | 'JC2010'} code
|
|
82
|
+
* @param {unknown} details
|
|
83
|
+
* @param {unknown} cause
|
|
84
|
+
* @returns {OperationResult}
|
|
85
|
+
*/
|
|
86
|
+
function contractResult(code, details, cause) {
|
|
87
|
+
return { kind: 'contract', code, details, cause };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* A server trace id from a host generator. TOTAL: a generator that
|
|
92
|
+
* throws or answers a non-string is replaced by the platform's UUID.
|
|
93
|
+
* @param {() => string} trace
|
|
94
|
+
* @returns {string}
|
|
95
|
+
*/
|
|
96
|
+
export function safeTrace(trace) {
|
|
97
|
+
try {
|
|
98
|
+
const t = trace();
|
|
99
|
+
if (typeof t === 'string' && t.length > 0) return t;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// fall through
|
|
103
|
+
}
|
|
104
|
+
return globalThis.crypto.randomUUID();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Validate the assembled input against the operation's input validator.
|
|
109
|
+
* `null` when valid (or the operation validates nothing); a `JC2006`
|
|
110
|
+
* contract result otherwise, its `details` projected by
|
|
111
|
+
* `policy.errors.details` and its `cause` the validator's own throw when
|
|
112
|
+
* it had one.
|
|
113
|
+
* @param {PipelineRoute} route
|
|
114
|
+
* @param {unknown} input
|
|
115
|
+
* @returns {OperationResult | null}
|
|
116
|
+
*/
|
|
117
|
+
export function validateOperationInput(route, input) {
|
|
118
|
+
if (route.validateInput === null) return null;
|
|
119
|
+
const v = verdict(route.validateInput, input);
|
|
120
|
+
if (v.valid) return null;
|
|
121
|
+
return contractResult('JC2006', projectValidationDetails(route.details, v.errors), v.thrown);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* A declared operation failure: the code must be declared (`JC2008`-class
|
|
126
|
+
* otherwise — the handler broke its own contract), the details must pass
|
|
127
|
+
* the declaration's schema, or be a JSON value when it declares none
|
|
128
|
+
* (`JC2010`-class otherwise), and `retryable` is the failure's own or
|
|
129
|
+
* whether `policy.retry.on` names the code.
|
|
130
|
+
* @param {PipelineRoute} route
|
|
131
|
+
* @param {string} code
|
|
132
|
+
* @param {Readonly<Record<string, unknown>>} params
|
|
133
|
+
* @param {unknown} details
|
|
134
|
+
* @param {boolean | null} retryable
|
|
135
|
+
* @returns {OperationResult}
|
|
136
|
+
*/
|
|
137
|
+
function declaredResult(route, code, params, details, retryable) {
|
|
138
|
+
const decl = typeof code === 'string' && Object.hasOwn(route.errors, code) ? route.errors[code] : undefined;
|
|
139
|
+
if (decl === undefined) {
|
|
140
|
+
return contractResult('JC2008', undefined, new ContractRuntimeError('JC2008',
|
|
141
|
+
`the handler of operation '${route.op.id}' answered the undeclared error code ${JSON.stringify(code)}`,
|
|
142
|
+
{ msgid: HTTP_ERRORS.JC2008.msgid, params: { op: route.op.id }, status: 500 }));
|
|
143
|
+
}
|
|
144
|
+
if (decl.validate !== null) {
|
|
145
|
+
const v = verdict(decl.validate, details);
|
|
146
|
+
if (!v.valid) {
|
|
147
|
+
return contractResult('JC2010', undefined, v.thrown !== undefined ? v.thrown : new ContractRuntimeError('JC2010',
|
|
148
|
+
`the details of declared error '${code}' of operation '${route.op.id}' fail its schema`,
|
|
149
|
+
{ msgid: HTTP_ERRORS.JC2010.msgid, params: { op: route.op.id }, status: 500, cause: v.errors }));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
else if (details !== undefined && !isJsonValue(details)) {
|
|
153
|
+
return contractResult('JC2010', undefined, new ContractRuntimeError('JC2010',
|
|
154
|
+
`the details of declared error '${code}' of operation '${route.op.id}' are not a JSON value`,
|
|
155
|
+
{ msgid: HTTP_ERRORS.JC2010.msgid, params: { op: route.op.id }, status: 500 }));
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
kind: 'failure', code, params, details,
|
|
159
|
+
retryable: retryable !== null ? retryable : route.retryOn.has(code),
|
|
160
|
+
status: decl.status,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Classify the handler's resolved value: a `ContractFailure` is a
|
|
166
|
+
* declared failure; anything else is the output, validated unless the
|
|
167
|
+
* route is raw or validation is off.
|
|
168
|
+
* @param {PipelineRoute} route
|
|
169
|
+
* @param {unknown} value
|
|
170
|
+
* @param {boolean} validateOutput
|
|
171
|
+
* @returns {OperationResult}
|
|
172
|
+
*/
|
|
173
|
+
function settleValue(route, value, validateOutput) {
|
|
174
|
+
if (isContractFailure(value)) return declaredResult(route, value.code, value.params, value.details, value.retryable);
|
|
175
|
+
if (!route.raw && validateOutput) {
|
|
176
|
+
const v = verdict(route.validateOutput, value);
|
|
177
|
+
if (!v.valid) {
|
|
178
|
+
return contractResult('JC2010', undefined, v.thrown !== undefined ? v.thrown : new ContractRuntimeError('JC2010',
|
|
179
|
+
`the value of operation '${route.op.id}' fails its output schema`,
|
|
180
|
+
{ msgid: HTTP_ERRORS.JC2010.msgid, params: { op: route.op.id }, status: 500, cause: v.errors }));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return { kind: 'value', value };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Classify a rejection or throw: a `ContractRuntimeError` whose code the
|
|
188
|
+
* operation declares is a declared failure; anything else — including a
|
|
189
|
+
* hostile value whose prototype walk throws — is a `JC2008` contract
|
|
190
|
+
* result carrying the rejection as `cause`, never onto a wire.
|
|
191
|
+
* @param {PipelineRoute} route
|
|
192
|
+
* @param {unknown} err
|
|
193
|
+
* @returns {OperationResult}
|
|
194
|
+
*/
|
|
195
|
+
function settleThrown(route, err) {
|
|
196
|
+
let declared = null;
|
|
197
|
+
try {
|
|
198
|
+
if (err instanceof ContractRuntimeError && typeof err.code === 'string' && Object.hasOwn(route.errors, err.code)) {
|
|
199
|
+
declared = { code: err.code, params: err.params, retryable: typeof err.retryable === 'boolean' ? err.retryable : null };
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
declared = null;
|
|
204
|
+
}
|
|
205
|
+
if (declared !== null) return declaredResult(route, declared.code, declared.params, undefined, declared.retryable);
|
|
206
|
+
return contractResult('JC2008', undefined, err);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Call the handler through the uniform promise boundary and classify the
|
|
211
|
+
* settlement. Never rejects; the returned promise always resolves an
|
|
212
|
+
* {@link OperationResult}.
|
|
213
|
+
* @param {PipelineRoute} route
|
|
214
|
+
* @param {any} input - the validated input (`null` for an input-less operation)
|
|
215
|
+
* @param {any} ctx - the frozen per-request context the handler receives
|
|
216
|
+
* @param {boolean} validateOutput
|
|
217
|
+
* @returns {Promise<OperationResult>}
|
|
218
|
+
*/
|
|
219
|
+
export function settleOperation(route, input, ctx, validateOutput) {
|
|
220
|
+
const handler = /** @type {(input: any, ctx: any) => unknown} */ (route.handler);
|
|
221
|
+
return new Promise((resolve) => { resolve(handler(input, ctx)); }).then(
|
|
222
|
+
(value) => settleValue(route, value, validateOutput),
|
|
223
|
+
(err) => settleThrown(route, err));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* The whole neutral pipeline of one operation: validate the input, then
|
|
228
|
+
* call and classify — for a binding with nothing of its own between the
|
|
229
|
+
* two steps (the HTTP binding interposes its idempotency claim and calls
|
|
230
|
+
* the two halves itself).
|
|
231
|
+
* @param {PipelineRoute} route
|
|
232
|
+
* @param {any} input
|
|
233
|
+
* @param {any} ctx
|
|
234
|
+
* @param {{ validateOutput?: boolean }} [options]
|
|
235
|
+
* @returns {Promise<OperationResult>}
|
|
236
|
+
*/
|
|
237
|
+
export function runOperation(route, input, ctx, options = {}) {
|
|
238
|
+
const invalid = validateOperationInput(route, input);
|
|
239
|
+
if (invalid !== null) return Promise.resolve(invalid);
|
|
240
|
+
return settleOperation(route, input, ctx, options.validateOutput !== false);
|
|
241
|
+
}
|