@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
@@ -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
+ }