@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,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file `openLocalClient(contract, handlers, options)`: the in-process
|
|
3
|
+
* binding of a compiled contract (docs/CONTRACT-FORMAT.md §15) — the
|
|
4
|
+
* same operation pipeline the HTTP server runs, with no wire: the test
|
|
5
|
+
* seam, SSR, a CLI. The client and the server are one object;
|
|
6
|
+
* `serveLocal` is the same factory under the serve name, for symmetry
|
|
7
|
+
* with the other bindings.
|
|
8
|
+
*
|
|
9
|
+
* `invoke(op, input, ctx)` validates the input with the operation's
|
|
10
|
+
* compiled validator (a refusal is the `JC2050` outcome — nothing ran),
|
|
11
|
+
* runs the neutral pipeline against the handler with a frozen context
|
|
12
|
+
* `{ op, trace, signal, params: null, headers: {}, fail, idempotency:
|
|
13
|
+
* null }`, and resolves a D6 outcome: a declared failure is `kind:
|
|
14
|
+
* "failure"` with `status: null` (the member present, never omitted —
|
|
15
|
+
* this binding carries no statuses and `capabilities` says so); a
|
|
16
|
+
* handler fault of any class — a throw, an undeclared code, a broken
|
|
17
|
+
* output or error-details schema — is `kind: "contract"` `JC2070`, its
|
|
18
|
+
* cause reported to `onError`, so a host bug is never mistaken for a
|
|
19
|
+
* declared failure; an aborted `ctx.signal` (or `close()`) is `kind:
|
|
20
|
+
* "cancelled"`, and a handler still running then settles into nothing.
|
|
21
|
+
*
|
|
22
|
+
* What this binding cannot carry, it refuses or ignores loudly:
|
|
23
|
+
* statuses, headers, entity tags and non-JSON media do not exist here
|
|
24
|
+
* (`invoke` of an opaque operation throws `JC1005`; a handler given for
|
|
25
|
+
* one is accepted so an HTTP handler table can be reused verbatim, and
|
|
26
|
+
* never called); a declared `policy.idempotency` is ALLOWED and ignored
|
|
27
|
+
* — the same contract must serve over http and locally, and re-running
|
|
28
|
+
* a command in one process is the caller's own hand — with
|
|
29
|
+
* `capabilities.idempotency: false` saying so.
|
|
30
|
+
*/
|
|
31
|
+
import { PORT_LOCAL_ERRORS } from '../pipeline.js';
|
|
32
|
+
export { PORT_LOCAL_ERRORS };
|
|
33
|
+
export type Contract = import('../compile.js').Contract;
|
|
34
|
+
export type CompiledOperation = import('../compile.js').CompiledOperation;
|
|
35
|
+
export type Catalog = import('../http/wire.js').Catalog;
|
|
36
|
+
export type Handler = import('../http/dispatch.js').Handler;
|
|
37
|
+
export type Outcome = import('../client/outcome.js').Outcome;
|
|
38
|
+
export type OutcomeMeta = import('../client/outcome.js').OutcomeMeta;
|
|
39
|
+
export type PipelineRoute = import('../pipeline.js').PipelineRoute;
|
|
40
|
+
export type LocalOptions = {
|
|
41
|
+
/**
|
|
42
|
+
* - the trace generator; default `crypto.randomUUID`
|
|
43
|
+
*/
|
|
44
|
+
trace?: () => string;
|
|
45
|
+
/**
|
|
46
|
+
* - `'never'` is a declared
|
|
47
|
+
* downgrade, reported in `capabilities.validatedOutput`
|
|
48
|
+
*/
|
|
49
|
+
validateOutput?: 'always' | 'never';
|
|
50
|
+
/**
|
|
51
|
+
* - a message catalog consulted before the English one
|
|
52
|
+
*/
|
|
53
|
+
catalog?: Record<string, string | ((params: object) => string)>;
|
|
54
|
+
/**
|
|
55
|
+
* - observes the cause behind every `JC2070` outcome and validator throw
|
|
56
|
+
*/
|
|
57
|
+
onError?: (error: unknown, ctx: {
|
|
58
|
+
op: string;
|
|
59
|
+
trace: string;
|
|
60
|
+
} | null) => void;
|
|
61
|
+
};
|
|
62
|
+
export type LocalInvokeContext = {
|
|
63
|
+
/**
|
|
64
|
+
* - resolves the outcome `cancelled`
|
|
65
|
+
*/
|
|
66
|
+
signal?: AbortSignal;
|
|
67
|
+
/**
|
|
68
|
+
* - the caller's attempt id, echoed in `meta.attempt`
|
|
69
|
+
*/
|
|
70
|
+
attempt?: unknown;
|
|
71
|
+
};
|
|
72
|
+
export type LocalCapabilities = {
|
|
73
|
+
name: 'local';
|
|
74
|
+
status: false;
|
|
75
|
+
headers: false;
|
|
76
|
+
media: false;
|
|
77
|
+
etag: false;
|
|
78
|
+
idempotency: false;
|
|
79
|
+
/**
|
|
80
|
+
* - the output validator runs
|
|
81
|
+
*/
|
|
82
|
+
validatedOutput: boolean;
|
|
83
|
+
stream: false;
|
|
84
|
+
cancel: 'signal';
|
|
85
|
+
};
|
|
86
|
+
export type LocalClient = {
|
|
87
|
+
invoke: (op: string, input?: unknown, ctx?: LocalInvokeContext) => Promise<Outcome>;
|
|
88
|
+
capabilities: LocalCapabilities;
|
|
89
|
+
contract: Contract;
|
|
90
|
+
describe: () => any;
|
|
91
|
+
/**
|
|
92
|
+
* - later invokes resolve `cancelled`; running handlers see their signal abort
|
|
93
|
+
*/
|
|
94
|
+
close: () => void;
|
|
95
|
+
};
|
|
96
|
+
export type LocalRoute = PipelineRoute & {
|
|
97
|
+
hasInput: boolean;
|
|
98
|
+
outcome: import('../client/outcome.js').OutcomeRoute;
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Serve a compiled contract in-process and call it through the same
|
|
102
|
+
* object: the `open(contract) → Client` half and the `serve(contract,
|
|
103
|
+
* handlers)` half of the driver pair are one here. Construction refuses
|
|
104
|
+
* host mistakes (`JC1001` handler table or option, `JC1002` missing
|
|
105
|
+
* handler — an opaque operation is exempt: this binding cannot invoke
|
|
106
|
+
* it, and an HTTP handler table that carries one may be reused verbatim).
|
|
107
|
+
*
|
|
108
|
+
* @param {Contract} contract
|
|
109
|
+
* @param {Record<string, Handler>} handlers - operation id → handler
|
|
110
|
+
* @param {LocalOptions} [options]
|
|
111
|
+
* @returns {LocalClient}
|
|
112
|
+
* @throws {ContractHostError}
|
|
113
|
+
* @example
|
|
114
|
+
* const client = openLocalClient(contract, {
|
|
115
|
+
* 'catalog.load': () => catalog,
|
|
116
|
+
* 'product.save': (input, ctx) => saved ? product : ctx.fail('conflict', {}, { current }),
|
|
117
|
+
* });
|
|
118
|
+
* const outcome = await client.invoke('catalog.load', { since: '2026-01-01T00:00:00Z' });
|
|
119
|
+
* if (!outcome.ok && outcome.kind === 'failure') show(outcome.error.code); // status is null here
|
|
120
|
+
*/
|
|
121
|
+
export declare function openLocalClient(contract: Contract, handlers: Record<string, Handler>, options?: LocalOptions): LocalClient;
|
|
122
|
+
/**
|
|
123
|
+
* The serve name of the same factory: the server half IS the client half
|
|
124
|
+
* here — one object, both names, for symmetry with `serveHttp`/
|
|
125
|
+
* `openHttpClient` and `servePort`/`openPortClient`.
|
|
126
|
+
*/
|
|
127
|
+
export declare const serveLocal: typeof openLocalClient;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The English message catalog: one template per `contract/*` msgid
|
|
3
|
+
* a binding can answer with — the HTTP taxonomy, the port/local codes,
|
|
4
|
+
* `contract/handler-error` (the generic text for a declared operation
|
|
5
|
+
* error that has no message of its own) — and one per client-originated
|
|
6
|
+
* outcome (the `JC205x` codes a client resolves without a server
|
|
7
|
+
* message). Compiled ONCE at module scope
|
|
8
|
+
* with `@jarenjs/core`'s `compileMessageCatalog` (the two-stage house
|
|
9
|
+
* rule applied to messages) and exported in plain form for the locale
|
|
10
|
+
* packs to mirror key for key.
|
|
11
|
+
*
|
|
12
|
+
* A message never interpolates a request value: the parameters are the
|
|
13
|
+
* operation id, a declared limit, a media type, a method list, a
|
|
14
|
+
* declared header member name, a declared error code, a status, a
|
|
15
|
+
* platform error's NAME or a contract version — trusted artifacts or
|
|
16
|
+
* protocol facts, never something a peer sent as content (the
|
|
17
|
+
* trust-boundary rule of docs/CONTRACT-FORMAT.md §7).
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* The plain English catalog: msgid → template (`{param}` placeholders).
|
|
21
|
+
* The keys are exactly the msgids of the CONTRACT-FORMAT.md §7 taxonomy,
|
|
22
|
+
* `contract/handler-error`, and the §10 client table; a test holds them
|
|
23
|
+
* equal.
|
|
24
|
+
*/
|
|
25
|
+
export declare const contractMessagesEn: Readonly<{
|
|
26
|
+
'contract/not-found': "no operation matches the request method and path";
|
|
27
|
+
'contract/method-not-allowed': "the path is served under other methods: {allow}";
|
|
28
|
+
'contract/body-too-large': "the request body of operation {op} exceeds its {limit}-byte limit";
|
|
29
|
+
'contract/unsupported-media': "operation {op} accepts {media} bodies only";
|
|
30
|
+
'contract/malformed-json': "the request body of operation {op} is not valid JSON";
|
|
31
|
+
'contract/invalid-input': "the input of operation {op} is invalid";
|
|
32
|
+
'contract/idempotency-key-required': "operation {op} requires an Idempotency-Key header";
|
|
33
|
+
'contract/handler-failed': "operation {op} failed";
|
|
34
|
+
'contract/idempotency-conflict': "the Idempotency-Key of operation {op} conflicts with an earlier request ({kind})";
|
|
35
|
+
'contract/invalid-output': "operation {op} produced a response that violates its contract";
|
|
36
|
+
'contract/malformed-path': "the request path carries a malformed percent-escape";
|
|
37
|
+
'contract/malformed-query': "the query string is not decodable";
|
|
38
|
+
'contract/not-implemented': "operation {op} is not implemented on this server";
|
|
39
|
+
'contract/precondition-failed': "the If-Match precondition of operation {op} failed";
|
|
40
|
+
'contract/invalid-header': "the {header} header of operation {op} is invalid";
|
|
41
|
+
'contract/handler-error': "operation {op} failed with {code}";
|
|
42
|
+
'contract/client-invalid-input': "the input of operation {op} is invalid; nothing was sent";
|
|
43
|
+
'contract/network': "the request of {op} did not complete ({name})";
|
|
44
|
+
'contract/cancelled': "the request of operation {op} was cancelled";
|
|
45
|
+
'contract/invalid-response': "the response of operation {op} violates its contract";
|
|
46
|
+
'contract/key-storage-failed': "the idempotency key of operation {op} could not be stored; nothing was sent";
|
|
47
|
+
'contract/undeclared-response': "operation {op} answered an undeclared response (status {status})";
|
|
48
|
+
'contract/not-a-contract': "the server does not describe contract {id} at its well-known path";
|
|
49
|
+
'contract/incompatible': "the server speaks version {server} of contract {id}; this client speaks {client} and neither end declares the other compatible";
|
|
50
|
+
'contract/host-failed': "operation {op} failed in the host before an outcome was produced";
|
|
51
|
+
'contract/local-handler-failed': "operation {op} failed in the serving host";
|
|
52
|
+
'contract/unknown-operation': "the request names no operation served on this channel";
|
|
53
|
+
'contract/port-timeout': "operation {op} got no answer on the channel within {ms}ms";
|
|
54
|
+
'contract/malformed-frame': "the response frame of operation {op} is malformed";
|
|
55
|
+
'contract/channel-closed': "the channel of operation {op} is closed";
|
|
56
|
+
'contract/not-a-stream': "the server answered the subscription of operation {op} with a non-stream response";
|
|
57
|
+
'contract/invalid-snapshot': "operation {op} produced a snapshot that violates its contract";
|
|
58
|
+
'contract/seq-regression': "the stream of operation {op} violated its seq order";
|
|
59
|
+
'contract/stream-error': "the stream of operation {op} ended with a server error ({code})";
|
|
60
|
+
'contract/heartbeat-missed': "the stream of operation {op} went silent for {ms}ms";
|
|
61
|
+
}>;
|
|
62
|
+
/** The compiled English catalog (module-level singleton). */
|
|
63
|
+
export declare const contractCatalogEn: Readonly<Record<string, (params: object, error?: object) => string>>;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The path matcher: RFC 6570 level-1 templates (`/api/products/{id}`)
|
|
3
|
+
* compiled into one static-segment tree per HTTP method, walked by
|
|
4
|
+
* char-code scan with zero allocation until the leaf is known.
|
|
5
|
+
*
|
|
6
|
+
* Package-private (docs/CONTRACT-FORMAT.md §5). Two stages:
|
|
7
|
+
*
|
|
8
|
+
* 1. `parsePathTemplate` — the one template parser in the suite: it
|
|
9
|
+
* canonicalizes `:name` to `{name}`, splits static from variable
|
|
10
|
+
* segments and refuses every reserved form with a message that names
|
|
11
|
+
* it. `compileContract` calls it once per operation and maps a
|
|
12
|
+
* refusal to `JC0008`.
|
|
13
|
+
* 2. `compileRoutes` — the operation table becomes a tree of nodes
|
|
14
|
+
* `{ statics, variable, leaf }`; `match(method, path)` walks statics
|
|
15
|
+
* first, falls back to the variable child, and backtracks on a dead
|
|
16
|
+
* end. Static beats variable REGARDLESS of registration order — the
|
|
17
|
+
* ordering case a regex-per-route router refuses or gets wrong.
|
|
18
|
+
*
|
|
19
|
+
* Matching rules a binding relies on: exact on trailing slash (`/a/` is
|
|
20
|
+
* a different shape from `/a` and, since no template has an empty
|
|
21
|
+
* segment, never matches); a variable never binds an empty segment; each
|
|
22
|
+
* segment is percent-decoded once, statics compared in decoded space, and
|
|
23
|
+
* a decoded `/` never re-splits; a malformed escape makes `match` return
|
|
24
|
+
* `null` (never throw — a request is hostile input). `?`/`#` never reach
|
|
25
|
+
* `match`: the binding splits the query off first.
|
|
26
|
+
*
|
|
27
|
+
* Variable names live on the LEAF, not on the variable node: two
|
|
28
|
+
* templates that differ only in a variable's name (`/a/{x}/b`,
|
|
29
|
+
* `/a/{y}/c`) share the variable child and each leaf binds its own name.
|
|
30
|
+
* The one shape that cannot be represented is two leaves for one method
|
|
31
|
+
* and one shape — a duplicate the compiler has already refused as
|
|
32
|
+
* `JC0010`, so here it is a `TypeError` (a host programming guard).
|
|
33
|
+
*/
|
|
34
|
+
export type PathSegment = {
|
|
35
|
+
variable: boolean;
|
|
36
|
+
text: string;
|
|
37
|
+
};
|
|
38
|
+
export type ParsedPathTemplate = {
|
|
39
|
+
/**
|
|
40
|
+
* - The canonical template (`{name}` form).
|
|
41
|
+
*/
|
|
42
|
+
path: string;
|
|
43
|
+
/**
|
|
44
|
+
* - In order; empty for `/`.
|
|
45
|
+
*/
|
|
46
|
+
segments: readonly PathSegment[];
|
|
47
|
+
/**
|
|
48
|
+
* - Variable names in order.
|
|
49
|
+
*/
|
|
50
|
+
variables: readonly string[];
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Parse a path template into segments, canonicalizing `:name` to
|
|
54
|
+
* `{name}`. Throws a `TypeError` whose message names the rule that was
|
|
55
|
+
* broken (the compiler wraps it as `JC0008`).
|
|
56
|
+
*
|
|
57
|
+
* The dialect: a leading `/`; the root template `/` has no segments;
|
|
58
|
+
* every other segment is non-empty; a variable is a whole segment,
|
|
59
|
+
* `{name}` or `:name`, with `name` an identifier, declared once per
|
|
60
|
+
* template; the RFC 6570 operator and modifier forms (`{+name}`,
|
|
61
|
+
* `{?name}`, `{name*}`, `{name:3}`, `{a,b}`), the `{name+}` tail and the
|
|
62
|
+
* `*` wildcard are reserved and refused by name.
|
|
63
|
+
* @param {unknown} source
|
|
64
|
+
* @returns {ParsedPathTemplate}
|
|
65
|
+
*/
|
|
66
|
+
export declare function parsePathTemplate(source: unknown): ParsedPathTemplate;
|
|
67
|
+
/**
|
|
68
|
+
* The shape of a template with every variable normalized to `{}` — the
|
|
69
|
+
* identity `JC0010` is decided on.
|
|
70
|
+
* @param {ParsedPathTemplate} parsed
|
|
71
|
+
* @returns {string}
|
|
72
|
+
*/
|
|
73
|
+
export declare function pathShape(parsed: ParsedPathTemplate): string;
|
|
74
|
+
export type Leaf = {
|
|
75
|
+
key: any;
|
|
76
|
+
names: readonly string[];
|
|
77
|
+
depths: readonly number[];
|
|
78
|
+
};
|
|
79
|
+
export type Node = {
|
|
80
|
+
statics: Map<string, Node> | null;
|
|
81
|
+
variable: Node | null;
|
|
82
|
+
leaf: Leaf | null;
|
|
83
|
+
};
|
|
84
|
+
export type Router = {
|
|
85
|
+
match: (method: string, path: string) => {
|
|
86
|
+
key: any;
|
|
87
|
+
params: Record<string, string>;
|
|
88
|
+
} | null;
|
|
89
|
+
/**
|
|
90
|
+
* - the methods whose tree
|
|
91
|
+
* reaches a leaf for this path shape, sorted — what a 405 lists in
|
|
92
|
+
* `Allow`; `[]` for a malformed or unknown path
|
|
93
|
+
*/
|
|
94
|
+
allowed: (path: string) => string[];
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* A compiled router.
|
|
98
|
+
* @typedef {Object} Router
|
|
99
|
+
* @property {(method: string, path: string) => { key: any, params: Record<string, string> } | null} match
|
|
100
|
+
* @property {(path: string) => string[]} allowed - the methods whose tree
|
|
101
|
+
* reaches a leaf for this path shape, sorted — what a 405 lists in
|
|
102
|
+
* `Allow`; `[]` for a malformed or unknown path
|
|
103
|
+
*/
|
|
104
|
+
/**
|
|
105
|
+
* Compile a route table into a `Router`. `entries` are
|
|
106
|
+
* `{ method, path, key }` with `path` in canonical `{name}` form (a
|
|
107
|
+
* `:name` template is a `TypeError` here — canonicalize with
|
|
108
|
+
* `parsePathTemplate` first). Two entries with one method and one shape
|
|
109
|
+
* are a `TypeError` (the compiler's `JC0010` has already refused them);
|
|
110
|
+
* templates that differ only in a variable name under a shared parent are
|
|
111
|
+
* fine as long as they end in different leaves.
|
|
112
|
+
* @param {readonly { method: string, path: string, key: any }[]} entries
|
|
113
|
+
* @returns {Router}
|
|
114
|
+
*/
|
|
115
|
+
export declare function compileRoutes(entries: readonly {
|
|
116
|
+
method: string;
|
|
117
|
+
path: string;
|
|
118
|
+
key: any;
|
|
119
|
+
}[]): Router;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The transport-neutral core of every server-side binding: validate
|
|
3
|
+
* the assembled input, call the handler through ONE uniform promise
|
|
4
|
+
* boundary — a synchronous throw, a non-promise return and a rejection
|
|
5
|
+
* settle exactly alike — classify the settlement (a declared operation
|
|
6
|
+
* failure, with its details validated against the declaration's schema,
|
|
7
|
+
* or a host fault), and validate the output. The HTTP binding wraps the
|
|
8
|
+
* result in statuses, headers and bodies; the `local` and `port`
|
|
9
|
+
* bindings wrap it in D6 outcomes and frames — the classification is
|
|
10
|
+
* decided here once so the three can never disagree.
|
|
11
|
+
*
|
|
12
|
+
* Total for everything a handler can do: a hostile value whose `then`
|
|
13
|
+
* accessor throws is a rejection at the boundary (`JC2008`-class), a
|
|
14
|
+
* value whose other members throw survives it and dies in output
|
|
15
|
+
* validation (`JC2010`-class); nothing here throws for a settlement and
|
|
16
|
+
* nothing renders a message — the binding renders at its edge.
|
|
17
|
+
*/
|
|
18
|
+
export type CompiledOperation = import('./compile.js').CompiledOperation;
|
|
19
|
+
export type CompiledErrorDecl = import('./compile.js').CompiledErrorDecl;
|
|
20
|
+
export type PipelineRoute = {
|
|
21
|
+
op: CompiledOperation;
|
|
22
|
+
handler: ((input: any, ctx: any) => unknown) | null;
|
|
23
|
+
/**
|
|
24
|
+
* - opaque: the value is the binding's to check, never the output validator's
|
|
25
|
+
*/
|
|
26
|
+
raw: boolean;
|
|
27
|
+
validateInput: ((value: unknown) => any) | null;
|
|
28
|
+
validateOutput: (value: unknown) => any;
|
|
29
|
+
details: 'none' | 'paths' | 'full';
|
|
30
|
+
errors: Readonly<Record<string, CompiledErrorDecl>>;
|
|
31
|
+
retryOn: ReadonlySet<string>;
|
|
32
|
+
};
|
|
33
|
+
export type OperationResult = {
|
|
34
|
+
kind: 'value';
|
|
35
|
+
value: unknown;
|
|
36
|
+
} | {
|
|
37
|
+
kind: 'failure';
|
|
38
|
+
code: string;
|
|
39
|
+
params: Readonly<Record<string, unknown>>;
|
|
40
|
+
details: unknown;
|
|
41
|
+
retryable: boolean;
|
|
42
|
+
status: number;
|
|
43
|
+
} | {
|
|
44
|
+
kind: 'contract';
|
|
45
|
+
code: 'JC2006' | 'JC2008' | 'JC2010';
|
|
46
|
+
details: unknown;
|
|
47
|
+
cause: unknown;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* @typedef {import('./compile.js').CompiledOperation} CompiledOperation
|
|
51
|
+
* @typedef {import('./compile.js').CompiledErrorDecl} CompiledErrorDecl
|
|
52
|
+
*/
|
|
53
|
+
/**
|
|
54
|
+
* The neutral subset of a binding's prepared route — what the pipeline
|
|
55
|
+
* reads per operation. The HTTP binding's `Route` carries these members
|
|
56
|
+
* verbatim; the `local` and `port` bindings prepare exactly this.
|
|
57
|
+
* @typedef {Object} PipelineRoute
|
|
58
|
+
* @property {CompiledOperation} op
|
|
59
|
+
* @property {((input: any, ctx: any) => unknown) | null} handler
|
|
60
|
+
* @property {boolean} raw - opaque: the value is the binding's to check, never the output validator's
|
|
61
|
+
* @property {((value: unknown) => any) | null} validateInput
|
|
62
|
+
* @property {(value: unknown) => any} validateOutput
|
|
63
|
+
* @property {'none' | 'paths' | 'full'} details
|
|
64
|
+
* @property {Readonly<Record<string, CompiledErrorDecl>>} errors
|
|
65
|
+
* @property {ReadonlySet<string>} retryOn
|
|
66
|
+
*/
|
|
67
|
+
/**
|
|
68
|
+
* The classified settlement of one operation:
|
|
69
|
+
*
|
|
70
|
+
* - `value` — the handler's output, validated;
|
|
71
|
+
* - `failure` — a declared operation error (`errors[code]`), its
|
|
72
|
+
* `details` validated, `retryable` from the failure or the retry
|
|
73
|
+
* policy, `status` the declaration's (a status-less binding ignores
|
|
74
|
+
* it);
|
|
75
|
+
* - `contract` — the host broke the contract: `JC2006` the input fails
|
|
76
|
+
* its validator (`details` by `policy.errors.details`), `JC2008` the
|
|
77
|
+
* handler threw a non-declared error, rejected or answered an
|
|
78
|
+
* undeclared code, `JC2010` the output or a declared error's details
|
|
79
|
+
* fail their schema. `cause` is for the binding's `onError` observer
|
|
80
|
+
* (`undefined` when there is nothing to report); it never crosses a
|
|
81
|
+
* wire.
|
|
82
|
+
*
|
|
83
|
+
* @typedef {{ kind: 'value', value: unknown }
|
|
84
|
+
* | { kind: 'failure', code: string, params: Readonly<Record<string, unknown>>, details: unknown, retryable: boolean, status: number }
|
|
85
|
+
* | { kind: 'contract', code: 'JC2006' | 'JC2008' | 'JC2010', details: unknown, cause: unknown }} OperationResult
|
|
86
|
+
*/
|
|
87
|
+
/**
|
|
88
|
+
* The request-time codes of the `local` and `port` bindings —
|
|
89
|
+
* code → `{ msgid, retryable }`, the same table-as-data shape as
|
|
90
|
+
* `HTTP_ERRORS` and `CLIENT_ERRORS`. The normative table is
|
|
91
|
+
* docs/CONTRACT-FORMAT.md §15–§16; a test holds them equal.
|
|
92
|
+
*/
|
|
93
|
+
export declare const PORT_LOCAL_ERRORS: Readonly<{
|
|
94
|
+
JC2070: Readonly<{
|
|
95
|
+
msgid: "contract/local-handler-failed";
|
|
96
|
+
retryable: false;
|
|
97
|
+
}>;
|
|
98
|
+
JC2071: Readonly<{
|
|
99
|
+
msgid: "contract/unknown-operation";
|
|
100
|
+
retryable: false;
|
|
101
|
+
}>;
|
|
102
|
+
JC2072: Readonly<{
|
|
103
|
+
msgid: "contract/port-timeout";
|
|
104
|
+
retryable: true;
|
|
105
|
+
}>;
|
|
106
|
+
JC2073: Readonly<{
|
|
107
|
+
msgid: "contract/malformed-frame";
|
|
108
|
+
retryable: false;
|
|
109
|
+
}>;
|
|
110
|
+
JC2074: Readonly<{
|
|
111
|
+
msgid: "contract/channel-closed";
|
|
112
|
+
retryable: false;
|
|
113
|
+
}>;
|
|
114
|
+
}>;
|
|
115
|
+
/**
|
|
116
|
+
* A server trace id from a host generator. TOTAL: a generator that
|
|
117
|
+
* throws or answers a non-string is replaced by the platform's UUID.
|
|
118
|
+
* @param {() => string} trace
|
|
119
|
+
* @returns {string}
|
|
120
|
+
*/
|
|
121
|
+
export declare function safeTrace(trace: () => string): string;
|
|
122
|
+
/**
|
|
123
|
+
* Validate the assembled input against the operation's input validator.
|
|
124
|
+
* `null` when valid (or the operation validates nothing); a `JC2006`
|
|
125
|
+
* contract result otherwise, its `details` projected by
|
|
126
|
+
* `policy.errors.details` and its `cause` the validator's own throw when
|
|
127
|
+
* it had one.
|
|
128
|
+
* @param {PipelineRoute} route
|
|
129
|
+
* @param {unknown} input
|
|
130
|
+
* @returns {OperationResult | null}
|
|
131
|
+
*/
|
|
132
|
+
export declare function validateOperationInput(route: PipelineRoute, input: unknown): OperationResult | null;
|
|
133
|
+
/**
|
|
134
|
+
* Call the handler through the uniform promise boundary and classify the
|
|
135
|
+
* settlement. Never rejects; the returned promise always resolves an
|
|
136
|
+
* {@link OperationResult}.
|
|
137
|
+
* @param {PipelineRoute} route
|
|
138
|
+
* @param {any} input - the validated input (`null` for an input-less operation)
|
|
139
|
+
* @param {any} ctx - the frozen per-request context the handler receives
|
|
140
|
+
* @param {boolean} validateOutput
|
|
141
|
+
* @returns {Promise<OperationResult>}
|
|
142
|
+
*/
|
|
143
|
+
export declare function settleOperation(route: PipelineRoute, input: any, ctx: any, validateOutput: boolean): Promise<OperationResult>;
|
|
144
|
+
/**
|
|
145
|
+
* The whole neutral pipeline of one operation: validate the input, then
|
|
146
|
+
* call and classify — for a binding with nothing of its own between the
|
|
147
|
+
* two steps (the HTTP binding interposes its idempotency claim and calls
|
|
148
|
+
* the two halves itself).
|
|
149
|
+
* @param {PipelineRoute} route
|
|
150
|
+
* @param {any} input
|
|
151
|
+
* @param {any} ctx
|
|
152
|
+
* @param {{ validateOutput?: boolean }} [options]
|
|
153
|
+
* @returns {Promise<OperationResult>}
|
|
154
|
+
*/
|
|
155
|
+
export declare function runOperation(route: PipelineRoute, input: any, ctx: any, options?: {
|
|
156
|
+
validateOutput?: boolean;
|
|
157
|
+
}): Promise<OperationResult>;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file `openPortClient(contract, options)`: the client half of the port
|
|
3
|
+
* binding (docs/CONTRACT-FORMAT.md §16). `invoke(op, input, ctx)`
|
|
4
|
+
* validates the input with the operation's compiled validator (a refusal
|
|
5
|
+
* is the `JC2050` outcome — nothing is posted), posts one request frame,
|
|
6
|
+
* and resolves a D6 outcome for every way a channel can answer: the
|
|
7
|
+
* matching response (success validated against the output schema,
|
|
8
|
+
* `JC2053`; a declared or taxonomy error a `failure` with `status:
|
|
9
|
+
* null`; a served-host fault `kind: "contract"` with the server's
|
|
10
|
+
* `JC2070`/`JC2071` code kept), no answer within `timeoutMs` (`JC2072`,
|
|
11
|
+
* network), a malformed response frame (`JC2073`, contract), a channel
|
|
12
|
+
* whose `postMessage` throws (`JC2074`, network), and a caller abort
|
|
13
|
+
* (`cancelled` — a cancel frame is also posted so the server can stop
|
|
14
|
+
* work; the id scoping is the guarantee, so a late response for a
|
|
15
|
+
* cancelled or timed-out id is dropped silently).
|
|
16
|
+
*
|
|
17
|
+
* Request ids are `"<clientId>:<seq>"` with a UUID client id, and every
|
|
18
|
+
* incoming frame is prefix-tested against `clientId + ":"` BEFORE any
|
|
19
|
+
* map lookup — two clients on one shared channel can never cross-settle,
|
|
20
|
+
* whatever the other one sends. `close()` rejects nothing: pending
|
|
21
|
+
* invokes resolve `cancelled` and the listener is removed (the channel
|
|
22
|
+
* itself is the host's to close).
|
|
23
|
+
*/
|
|
24
|
+
import { PORT_LOCAL_ERRORS } from '../pipeline.js';
|
|
25
|
+
export { PORT_LOCAL_ERRORS };
|
|
26
|
+
export type Contract = import('../compile.js').Contract;
|
|
27
|
+
export type CompiledOperation = import('../compile.js').CompiledOperation;
|
|
28
|
+
export type Catalog = import('../http/wire.js').Catalog;
|
|
29
|
+
export type Outcome = import('../client/outcome.js').Outcome;
|
|
30
|
+
export type OutcomeMeta = import('../client/outcome.js').OutcomeMeta;
|
|
31
|
+
export type OutcomeRoute = import('../client/outcome.js').OutcomeRoute;
|
|
32
|
+
export type ChannelLike = import('./frame.js').ChannelLike;
|
|
33
|
+
export type PortClientOptions = {
|
|
34
|
+
/**
|
|
35
|
+
* - the channel to talk over (required)
|
|
36
|
+
*/
|
|
37
|
+
channel: ChannelLike;
|
|
38
|
+
/**
|
|
39
|
+
* - per request; default 15000; `0` disables
|
|
40
|
+
*/
|
|
41
|
+
timeoutMs?: number;
|
|
42
|
+
/**
|
|
43
|
+
* - a message catalog consulted before the English one
|
|
44
|
+
*/
|
|
45
|
+
catalog?: Record<string, string | ((params: object) => string)>;
|
|
46
|
+
};
|
|
47
|
+
export type PortInvokeContext = {
|
|
48
|
+
/**
|
|
49
|
+
* - resolves the outcome `cancelled` and posts a cancel frame
|
|
50
|
+
*/
|
|
51
|
+
signal?: AbortSignal;
|
|
52
|
+
/**
|
|
53
|
+
* - the caller's attempt id, echoed in `meta.attempt`, never sent
|
|
54
|
+
*/
|
|
55
|
+
attempt?: unknown;
|
|
56
|
+
};
|
|
57
|
+
export type PortClientCapabilities = {
|
|
58
|
+
name: 'port';
|
|
59
|
+
status: false;
|
|
60
|
+
headers: false;
|
|
61
|
+
media: false;
|
|
62
|
+
etag: false;
|
|
63
|
+
idempotency: false;
|
|
64
|
+
/**
|
|
65
|
+
* - `subscribe` carries push-frame streams (docs/CONTRACT-FORMAT.md §18.2)
|
|
66
|
+
*/
|
|
67
|
+
stream: true;
|
|
68
|
+
cancel: 'message';
|
|
69
|
+
};
|
|
70
|
+
export type PortSubscribeOptions = {
|
|
71
|
+
onSnapshot?: (value: unknown, info: {
|
|
72
|
+
seq: number;
|
|
73
|
+
resumed: boolean;
|
|
74
|
+
}) => void;
|
|
75
|
+
onPatch?: (emission: {
|
|
76
|
+
patch: unknown[];
|
|
77
|
+
seq: number;
|
|
78
|
+
}) => void;
|
|
79
|
+
onError?: (outcome: Outcome) => void;
|
|
80
|
+
onEnd?: (info: {
|
|
81
|
+
reason: string;
|
|
82
|
+
}) => void;
|
|
83
|
+
/**
|
|
84
|
+
* - stops the subscription silently
|
|
85
|
+
*/
|
|
86
|
+
signal?: AbortSignal;
|
|
87
|
+
/**
|
|
88
|
+
* - the resume seq (what a reconnect passes)
|
|
89
|
+
*/
|
|
90
|
+
lastSeq?: number;
|
|
91
|
+
};
|
|
92
|
+
export type PortClient = {
|
|
93
|
+
invoke: (op: string, input?: unknown, ctx?: PortInvokeContext) => Promise<Outcome>;
|
|
94
|
+
subscribe: (op: string, input?: unknown, options?: PortSubscribeOptions) => {
|
|
95
|
+
stop: () => void;
|
|
96
|
+
};
|
|
97
|
+
capabilities: PortClientCapabilities;
|
|
98
|
+
contract: Contract;
|
|
99
|
+
describe: () => any;
|
|
100
|
+
close: () => void;
|
|
101
|
+
};
|
|
102
|
+
export type PortRoute = {
|
|
103
|
+
op: CompiledOperation;
|
|
104
|
+
raw: boolean;
|
|
105
|
+
hasInput: boolean;
|
|
106
|
+
validateInput: ((value: unknown) => any) | null;
|
|
107
|
+
details: 'none' | 'paths' | 'full';
|
|
108
|
+
outcome: OutcomeRoute;
|
|
109
|
+
};
|
|
110
|
+
export type Pending = {
|
|
111
|
+
resolve: (outcome: Outcome) => void;
|
|
112
|
+
route: PortRoute;
|
|
113
|
+
meta: OutcomeMeta;
|
|
114
|
+
timer: ReturnType<typeof setTimeout> | 0;
|
|
115
|
+
/**
|
|
116
|
+
* - removes the abort listener
|
|
117
|
+
*/
|
|
118
|
+
cleanup: () => void;
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* One pending request: what the listener needs to settle it.
|
|
122
|
+
* @typedef {Object} Pending
|
|
123
|
+
* @property {(outcome: Outcome) => void} resolve
|
|
124
|
+
* @property {PortRoute} route
|
|
125
|
+
* @property {OutcomeMeta} meta
|
|
126
|
+
* @property {ReturnType<typeof setTimeout> | 0} timer
|
|
127
|
+
* @property {() => void} cleanup - removes the abort listener
|
|
128
|
+
*/
|
|
129
|
+
/**
|
|
130
|
+
* Open a port client over a compiled contract.
|
|
131
|
+
*
|
|
132
|
+
* @param {Contract} contract
|
|
133
|
+
* @param {PortClientOptions} options
|
|
134
|
+
* @returns {PortClient}
|
|
135
|
+
* @throws {ContractHostError} `JC1008` for a malformed argument or option
|
|
136
|
+
* @example
|
|
137
|
+
* const worker = new Worker(new URL('./owner.js', import.meta.url), { type: 'module' });
|
|
138
|
+
* const client = openPortClient(contract, { channel: worker });
|
|
139
|
+
* const outcome = await client.invoke('data.rows', { collection: 'notes' });
|
|
140
|
+
* if (outcome.ok) render(outcome.value); // outcome.error.status is null on this binding
|
|
141
|
+
*/
|
|
142
|
+
export declare function openPortClient(contract: Contract, options: PortClientOptions): PortClient;
|