@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,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file `contractAppBinding(contract, options)`: the generated
|
|
3
|
+
* `@jarenjs/app` documents of a contract (docs/CONTRACT-FORMAT.md §11) —
|
|
4
|
+
* pure JSON, the `fsmToApp` shape, with no import in either direction.
|
|
5
|
+
* For every operation the app uses it emits one task slot in a state
|
|
6
|
+
* slice, a `start`, a `done` and a `reset` action in the async-task
|
|
7
|
+
* convention of `@jarenjs/app`'s TASKS.md (a monotonic slot `id`, the
|
|
8
|
+
* start patches the slot AND hands the new id to the effect, the
|
|
9
|
+
* completion guards on that id so an out-of-order response is a
|
|
10
|
+
* provable no-op), and a JSON Schema for the slice to compose into
|
|
11
|
+
* `validateState`. The app document names ONE effect, `run: "contract"`;
|
|
12
|
+
* the per-operation task mode is never NAMED in the document — it comes
|
|
13
|
+
* from `policy.task` through `createContractEffect` — but the generator
|
|
14
|
+
* derives the document's state-side guards from it exactly as it
|
|
15
|
+
* derives the document's shape from `kind`, so that the slot and the
|
|
16
|
+
* effect tell the same story in every mode (D10): an `exhaust`
|
|
17
|
+
* operation's `start` is a no-op in state while its slot is `loading`
|
|
18
|
+
* (the effect would ignore the duplicate start; state decides first, so
|
|
19
|
+
* the one completion that arrives carries the current id and lands).
|
|
20
|
+
*/
|
|
21
|
+
export type Contract = import('../compile.js').Contract;
|
|
22
|
+
export type ContractAppBindingOptions = {
|
|
23
|
+
/**
|
|
24
|
+
* - the action-name prefix; default `contract/`
|
|
25
|
+
*/
|
|
26
|
+
namespace?: string;
|
|
27
|
+
/**
|
|
28
|
+
* - where the slice lives in app state, as a
|
|
29
|
+
* chain of identifier-safe segments; default `/contract`
|
|
30
|
+
*/
|
|
31
|
+
statePath?: string;
|
|
32
|
+
/**
|
|
33
|
+
* - the operations the app uses; default every operation
|
|
34
|
+
*/
|
|
35
|
+
ops?: readonly string[];
|
|
36
|
+
};
|
|
37
|
+
export type TaskSlot = {
|
|
38
|
+
id: number;
|
|
39
|
+
status: 'idle' | 'loading' | 'done' | 'error';
|
|
40
|
+
kind: 'failure' | 'network' | 'contract' | null;
|
|
41
|
+
value: unknown;
|
|
42
|
+
error: unknown;
|
|
43
|
+
meta: unknown;
|
|
44
|
+
};
|
|
45
|
+
export type StreamSlot = {
|
|
46
|
+
id: number;
|
|
47
|
+
status: 'idle' | 'live' | 'error';
|
|
48
|
+
kind: 'failure' | 'network' | 'contract' | null;
|
|
49
|
+
input: unknown;
|
|
50
|
+
value: unknown;
|
|
51
|
+
error: unknown;
|
|
52
|
+
meta: unknown;
|
|
53
|
+
seq: number;
|
|
54
|
+
};
|
|
55
|
+
export type ContractAppBinding = {
|
|
56
|
+
/**
|
|
57
|
+
* - operation id → its initial slot; mount it at `statePath`
|
|
58
|
+
*/
|
|
59
|
+
slice: Record<string, TaskSlot | StreamSlot>;
|
|
60
|
+
/**
|
|
61
|
+
* - per read/command operation
|
|
62
|
+
* `<namespace><op>/start`, `/done` and `/reset`; per subscribe operation
|
|
63
|
+
* `/start`, `/stop`, `/snapshot`, `/patch`, `/error` and `/reset`
|
|
64
|
+
*/
|
|
65
|
+
actions: Record<string, any>;
|
|
66
|
+
/**
|
|
67
|
+
* - one subscription entry per subscribe operation
|
|
68
|
+
* (`run: "contract-stream"`); spread into the app document's `subs`
|
|
69
|
+
*/
|
|
70
|
+
subs: any[];
|
|
71
|
+
/**
|
|
72
|
+
* - the slice's JSON Schema, `$defs` of the contract carried, for `validateState`
|
|
73
|
+
*/
|
|
74
|
+
schema: any;
|
|
75
|
+
/**
|
|
76
|
+
* - the effect name the task actions invoke
|
|
77
|
+
*/
|
|
78
|
+
effect: 'contract';
|
|
79
|
+
/**
|
|
80
|
+
* - the handler name the subs entries run
|
|
81
|
+
*/
|
|
82
|
+
subscription: 'contract-stream';
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Generate the app documents of a contract: a state slice with one task
|
|
86
|
+
* slot per operation, `start`/`done`/`reset` actions per operation with
|
|
87
|
+
* the TASKS.md id guard built in, and the slice's JSON Schema.
|
|
88
|
+
*
|
|
89
|
+
* The `start` action takes the operation's input as `$payload`: it
|
|
90
|
+
* increments the slot `id`, sets `status: "loading"`, clears `kind` and
|
|
91
|
+
* `error`, and runs the `contract` effect with `{ op, input: $payload, id, done,
|
|
92
|
+
* slot }` — the `id` written as the same increment expression the patch
|
|
93
|
+
* uses (everything in an action evaluates against the PRE-transition
|
|
94
|
+
* state). For an operation whose `policy.task` is `exhaust` the whole
|
|
95
|
+
* start is wrapped in `$if: [{ $ne: [<slot>.status, "loading"] }, …]`
|
|
96
|
+
* — a `$if` without else is the empty sequence (APP-FORMAT §3.2): no
|
|
97
|
+
* patch, no effect, no render — so a duplicate start leaves the slot id
|
|
98
|
+
* where it is and the single completion lands. The `done` action guards
|
|
99
|
+
* on `$payload.id` against the slot id, reads the outcome from
|
|
100
|
+
* `$payload.result` (or `$payload.error`, where a host failure projected
|
|
101
|
+
* by the effect lands), and stores it: `ok` → `status: "done"`, `kind:
|
|
102
|
+
* null`, `value`, `meta`, `error: null`; otherwise `status: "error"`,
|
|
103
|
+
* `kind` (the outcome's), `error`, `meta`, and `value` UNTOUCHED — a
|
|
104
|
+
* failed reload keeps the last good value. The `reset` action releases
|
|
105
|
+
* the slot: `status: "idle"`, `kind: null`, `error: null`; `id`, `value`
|
|
106
|
+
* and `meta` stay (the id must stay monotonic so a late completion of a
|
|
107
|
+
* cancelled attempt is still rejected; the last good value survives a
|
|
108
|
+
* reset as it survives an error).
|
|
109
|
+
*
|
|
110
|
+
* @param {Contract} contract
|
|
111
|
+
* @param {ContractAppBindingOptions} [options]
|
|
112
|
+
* @returns {ContractAppBinding}
|
|
113
|
+
* @throws {ContractHostError} `JC1007` for an unknown/uncarriable operation in `ops` or a malformed option
|
|
114
|
+
* @example
|
|
115
|
+
* const { slice, actions, schema } = contractAppBinding(contract, { ops: ['catalog.load', 'product.save'] });
|
|
116
|
+
* createApp({ state: { contract: slice }, view, actions: { ...actions, ...own } }, {
|
|
117
|
+
* effects: { contract: createContractEffect(client, { createTaskEffect }) },
|
|
118
|
+
* validateState: (state) => validate(state), // validate compiled over { properties: { contract: schema } }
|
|
119
|
+
* });
|
|
120
|
+
* app.dispatch('contract/catalog.load/start', { since: '2026-01-01T00:00:00Z' });
|
|
121
|
+
*/
|
|
122
|
+
export declare function contractAppBinding(contract: Contract, options?: ContractAppBindingOptions): ContractAppBinding;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file `createContractEffect(client, options)`: ONE `@jarenjs/app`
|
|
3
|
+
* effect handler (`run: "contract"`) that calls any operation of a
|
|
4
|
+
* client and settles every descriptor with a D6 outcome
|
|
5
|
+
* (docs/CONTRACT-FORMAT.md §11). It owns one task effect per distinct
|
|
6
|
+
* `policy.task` mode the contract uses — built lazily through the
|
|
7
|
+
* `createTaskEffect` factory the host hands in from `@jarenjs/app` —
|
|
8
|
+
* and routes each descriptor `{ op, input, id, done, fail?, slot? }` to
|
|
9
|
+
* the mode's inner effect, so the per-operation concurrency semantics
|
|
10
|
+
* come from the contract's policy and never appear in the app document.
|
|
11
|
+
* This package imports nothing from `@jarenjs/app`; the factory crosses
|
|
12
|
+
* as a function, the documents as JSON.
|
|
13
|
+
*
|
|
14
|
+
* Settlement: the inner effect's `run` invokes the client with the
|
|
15
|
+
* slot's signal and the descriptor's `id` as the attempt; an outcome of
|
|
16
|
+
* `kind: "cancelled"` is turned into an `AbortError` so the task effect
|
|
17
|
+
* dispatches nothing (a superseded task is dead by design); every other
|
|
18
|
+
* outcome resolves and lands as `{ id, result: outcome }`; a host throw
|
|
19
|
+
* is projected — never flattened to a string — into a `JC2058` outcome
|
|
20
|
+
* that lands as `{ id, error: outcome }`.
|
|
21
|
+
*/
|
|
22
|
+
export type Contract = import('../compile.js').Contract;
|
|
23
|
+
export type Outcome = import('../client/outcome.js').Outcome;
|
|
24
|
+
export type ClientLike = {
|
|
25
|
+
invoke: (op: string, input: unknown, ctx: {
|
|
26
|
+
signal: AbortSignal;
|
|
27
|
+
attempt: unknown;
|
|
28
|
+
}) => Promise<Outcome> | Outcome;
|
|
29
|
+
contract: Contract;
|
|
30
|
+
};
|
|
31
|
+
export type TaskEffectFactory = (run: (props: any, signal: AbortSignal) => any, options: {
|
|
32
|
+
mode: string;
|
|
33
|
+
projectError: (err: unknown, props: any) => any;
|
|
34
|
+
}) => TaskEffectLike;
|
|
35
|
+
export type TaskEffectLike = ((props: any, dispatch: (name: string, payload?: any) => void) => void) & {
|
|
36
|
+
cancel: (slot?: string) => void;
|
|
37
|
+
cancelAll: () => void;
|
|
38
|
+
dispose: () => void;
|
|
39
|
+
};
|
|
40
|
+
export type ContractEffectOptions = {
|
|
41
|
+
/**
|
|
42
|
+
* - `createTaskEffect` from `@jarenjs/app` (required)
|
|
43
|
+
*/
|
|
44
|
+
createTaskEffect: TaskEffectFactory;
|
|
45
|
+
/**
|
|
46
|
+
* - consulted
|
|
47
|
+
* first when the host throws; a result that is not an outcome falls
|
|
48
|
+
* back to the `JC2058` outcome
|
|
49
|
+
*/
|
|
50
|
+
projectError?: (err: unknown, props: any) => any;
|
|
51
|
+
/**
|
|
52
|
+
* - compiled catalog for the `JC2058` message
|
|
53
|
+
*/
|
|
54
|
+
catalog?: Record<string, (params: object) => string> | null;
|
|
55
|
+
};
|
|
56
|
+
export type ContractEffect = ((props: any, dispatch: (name: string, payload?: any) => void) => void) & {
|
|
57
|
+
cancel: (slot?: string) => void;
|
|
58
|
+
cancelAll: () => void;
|
|
59
|
+
dispose: () => void;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Make the `contract` effect of an app over a client. Register it as
|
|
63
|
+
* `effects: { contract: createContractEffect(client, { createTaskEffect }) }`;
|
|
64
|
+
* the generated actions of `contractAppBinding` invoke it.
|
|
65
|
+
*
|
|
66
|
+
* @param {ClientLike} client - an open client (`openHttpClient`, …)
|
|
67
|
+
* @param {ContractEffectOptions} options
|
|
68
|
+
* @returns {ContractEffect}
|
|
69
|
+
* @throws {ContractHostError} `JC1008` for a malformed client or option
|
|
70
|
+
* @example
|
|
71
|
+
* // createApp and createTaskEffect are the host's, imported from @jarenjs/app;
|
|
72
|
+
* // openHttpClient from '@jarenjs/contract/client', createContractEffect from '@jarenjs/contract/app'
|
|
73
|
+
* const client = openHttpClient(contract, { baseUrl });
|
|
74
|
+
* const effect = createContractEffect(client, { createTaskEffect });
|
|
75
|
+
* createApp(doc, { effects: { contract: effect } });
|
|
76
|
+
*/
|
|
77
|
+
export declare function createContractEffect(client: ClientLike, options: ContractEffectOptions): ContractEffect;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The `@jarenjs/app` binding of a contract (docs/CONTRACT-FORMAT.md
|
|
3
|
+
* §11): `contractAppBinding` generates the state slice, the
|
|
4
|
+
* start/done/reset actions and the slice schema as pure JSON (the
|
|
5
|
+
* state-side guards derived from `policy.task`, the mode never named);
|
|
6
|
+
* `createContractEffect`
|
|
7
|
+
* makes the one `contract` effect those actions invoke, over any open
|
|
8
|
+
* client. Neither imports `@jarenjs/app` — the documents cross as JSON
|
|
9
|
+
* and the task-effect factory crosses as a function the host passes in.
|
|
10
|
+
*/
|
|
11
|
+
export { contractAppBinding } from './binding.js';
|
|
12
|
+
export { createContractEffect } from './effect.js';
|
|
13
|
+
export { createContractSubscription } from './subscription.js';
|
|
14
|
+
export type ContractAppBinding = import('./binding.js').ContractAppBinding;
|
|
15
|
+
export type ContractAppBindingOptions = import('./binding.js').ContractAppBindingOptions;
|
|
16
|
+
export type TaskSlot = import('./binding.js').TaskSlot;
|
|
17
|
+
export type StreamSlot = import('./binding.js').StreamSlot;
|
|
18
|
+
export type ContractEffect = import('./effect.js').ContractEffect;
|
|
19
|
+
export type ContractEffectOptions = import('./effect.js').ContractEffectOptions;
|
|
20
|
+
export type ClientLike = import('./effect.js').ClientLike;
|
|
21
|
+
export type StreamClientLike = import('./subscription.js').StreamClientLike;
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {import('./binding.js').ContractAppBinding} ContractAppBinding
|
|
24
|
+
* @typedef {import('./binding.js').ContractAppBindingOptions} ContractAppBindingOptions
|
|
25
|
+
* @typedef {import('./binding.js').TaskSlot} TaskSlot
|
|
26
|
+
* @typedef {import('./binding.js').StreamSlot} StreamSlot
|
|
27
|
+
* @typedef {import('./effect.js').ContractEffect} ContractEffect
|
|
28
|
+
* @typedef {import('./effect.js').ContractEffectOptions} ContractEffectOptions
|
|
29
|
+
* @typedef {import('./effect.js').ClientLike} ClientLike
|
|
30
|
+
* @typedef {import('./subscription.js').StreamClientLike} StreamClientLike
|
|
31
|
+
*/
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file `createContractSubscription(client)`: the ONE `contract-stream`
|
|
3
|
+
* subscription handler the generated subs entries of
|
|
4
|
+
* `contractAppBinding` run (docs/CONTRACT-FORMAT.md §11.4). A handler is
|
|
5
|
+
* `(props, dispatch) => cleanup` — the `@jarenjs/app` subscription
|
|
6
|
+
* shape — so no task-effect factory is needed and this package still
|
|
7
|
+
* imports nothing from `@jarenjs/app`: the entry documents cross as
|
|
8
|
+
* JSON and the handler crosses as a function over a client.
|
|
9
|
+
*
|
|
10
|
+
* The handler subscribes through `client.subscribe`, maintains the
|
|
11
|
+
* document HOST-SIDE by applying each `{ patch, seq }` emission with
|
|
12
|
+
* `@jarenjs/json/patch` (copy-on-write — structural sharing is
|
|
13
|
+
* preserved end to end, which is what keeps the O(k) renderer's fast
|
|
14
|
+
* paths alive), and dispatches the named actions with `{ id, … }`
|
|
15
|
+
* payloads — the generated actions guard on the id (and, for `patch`,
|
|
16
|
+
* on a strictly greater `seq`), so a stale instance's dispatch is a
|
|
17
|
+
* provable no-op in state. An `onError` outcome lands in the error
|
|
18
|
+
* action as-is; a server `end` lands there too, as a `network`-kind
|
|
19
|
+
* outcome with the channel-closed code — the stream is gone and the
|
|
20
|
+
* slot must say so, so a view can offer a reconnect (a new `start`).
|
|
21
|
+
*/
|
|
22
|
+
import { CLIENT_ERRORS } from '../client/outcome.js';
|
|
23
|
+
export type Contract = import('../compile.js').Contract;
|
|
24
|
+
export type Outcome = import('../client/outcome.js').Outcome;
|
|
25
|
+
export type Catalog = import('../http/wire.js').Catalog;
|
|
26
|
+
export type StreamClientLike = {
|
|
27
|
+
subscribe: (op: string, input: unknown, options: object) => {
|
|
28
|
+
stop: () => void;
|
|
29
|
+
};
|
|
30
|
+
contract: Contract;
|
|
31
|
+
};
|
|
32
|
+
export type ContractSubscriptionOptions = {
|
|
33
|
+
/**
|
|
34
|
+
* - compiled catalog for the end/apply messages
|
|
35
|
+
*/
|
|
36
|
+
catalog?: Catalog | null;
|
|
37
|
+
};
|
|
38
|
+
export type StreamProps = {
|
|
39
|
+
op: string;
|
|
40
|
+
id: number;
|
|
41
|
+
input: unknown;
|
|
42
|
+
snapshot: string;
|
|
43
|
+
patch: string;
|
|
44
|
+
error: string;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* @typedef {import('../compile.js').Contract} Contract
|
|
48
|
+
* @typedef {import('../client/outcome.js').Outcome} Outcome
|
|
49
|
+
* @typedef {import('../http/wire.js').Catalog} Catalog
|
|
50
|
+
*/
|
|
51
|
+
/**
|
|
52
|
+
* A client binding that carries streams: what `openHttpClient` and
|
|
53
|
+
* `openPortClient` return. The handler reads `subscribe` only.
|
|
54
|
+
* @typedef {{ subscribe: (op: string, input: unknown, options: object) => { stop: () => void }, contract: Contract }} StreamClientLike
|
|
55
|
+
*/
|
|
56
|
+
/**
|
|
57
|
+
* @typedef {Object} ContractSubscriptionOptions
|
|
58
|
+
* @property {Catalog | null} [catalog] - compiled catalog for the end/apply messages
|
|
59
|
+
*/
|
|
60
|
+
/**
|
|
61
|
+
* The props of one generated subs entry: the operation, the slot id and
|
|
62
|
+
* input resolved from state, and the action names to dispatch.
|
|
63
|
+
* @typedef {{ op: string, id: number, input: unknown, snapshot: string, patch: string, error: string }} StreamProps
|
|
64
|
+
*/
|
|
65
|
+
/**
|
|
66
|
+
* Make the `contract-stream` subscription handler of an app over a
|
|
67
|
+
* client. Register it as `subs: { 'contract-stream':
|
|
68
|
+
* createContractSubscription(client) }`; the generated entries of
|
|
69
|
+
* `contractAppBinding` run it.
|
|
70
|
+
*
|
|
71
|
+
* @param {StreamClientLike} client - an open client whose `capabilities.stream` is true
|
|
72
|
+
* @param {ContractSubscriptionOptions} [options]
|
|
73
|
+
* @returns {(props: StreamProps, dispatch: (name: string, payload?: any) => void) => (() => void)}
|
|
74
|
+
* @throws {ContractHostError} `JC1008` for a malformed client or option
|
|
75
|
+
* @example
|
|
76
|
+
* createApp({ ...doc, subs: [...binding.subs] }, {
|
|
77
|
+
* effects: { contract: createContractEffect(client, { createTaskEffect }) },
|
|
78
|
+
* subs: { 'contract-stream': createContractSubscription(client) },
|
|
79
|
+
* });
|
|
80
|
+
*/
|
|
81
|
+
export declare function createContractSubscription(client: StreamClientLike, options?: ContractSubscriptionOptions): (props: StreamProps, dispatch: (name: string, payload?: any) => void) => (() => void);
|
|
82
|
+
export { CLIENT_ERRORS };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Same-document schema reachability and bundling — the one place in
|
|
3
|
+
* the suite that answers "which `$defs` does this schema need?" and
|
|
4
|
+
* "make this schema stand alone". The public projection keeps exactly
|
|
5
|
+
* the reachable `$defs` of the retained operations (05 hashes that); a
|
|
6
|
+
* tool definition carries its input schema with the reachable `$defs`
|
|
7
|
+
* inlined under the schema's own `$defs`, because a tool schema must be
|
|
8
|
+
* self-contained.
|
|
9
|
+
*
|
|
10
|
+
* Reachability follows same-document `$ref`s (`#`, `#/…` pointers,
|
|
11
|
+
* `#anchor`) with the resolver `compileContract` itself uses
|
|
12
|
+
* (`@jarenjs/validate/normalize`), so a reference the compiler accepted
|
|
13
|
+
* resolves here by the same rules. A target is attributed to the `$defs`
|
|
14
|
+
* entry that CONTAINS it — `#/$defs/Product/properties/id` needs
|
|
15
|
+
* `Product` — and the entry is walked in turn. References that do not
|
|
16
|
+
* land inside `$defs` (`#`, a pointer into another operation's schema,
|
|
17
|
+
* an absolute `$id`) are left as written: the document states where
|
|
18
|
+
* shared schemas live, and that is `$defs`.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* The names of the document's `$defs` reachable from `roots` by
|
|
22
|
+
* same-document `$ref`, transitively, in first-reference order: the
|
|
23
|
+
* roots are walked in the order given, depth-first in member order, and
|
|
24
|
+
* a `$defs` entry is walked at the moment it is first reached. Data
|
|
25
|
+
* keywords (`const`, `enum`, `default`, `examples`) are not walked.
|
|
26
|
+
* @param {readonly unknown[]} roots - schemas (objects or booleans) rooted in `doc`
|
|
27
|
+
* @param {any} doc - the document holding `$defs` (a contract document, or any schema root)
|
|
28
|
+
* @returns {string[]}
|
|
29
|
+
*/
|
|
30
|
+
export declare function reachableDefs(roots: readonly unknown[], doc: any): string[];
|
|
31
|
+
/**
|
|
32
|
+
* Make a schema stand alone: the schema with the `$defs` it reaches
|
|
33
|
+
* (transitively, first-reference order) copied under its own `$defs`, so
|
|
34
|
+
* every `#/$defs/X` it carries resolves inside the returned document. A
|
|
35
|
+
* boolean schema is returned as is; a schema that reaches nothing gains no
|
|
36
|
+
* `$defs`. A schema that declared its own `$defs` member loses it — at the
|
|
37
|
+
* new root that member would shadow the document's, and its entries were
|
|
38
|
+
* only addressable by a pointer through the operation anyway.
|
|
39
|
+
* @param {any} schema - a schema rooted in `doc`
|
|
40
|
+
* @param {any} doc - the document holding `$defs`
|
|
41
|
+
* @returns {any} a fresh top-level object sharing the schema's subtrees
|
|
42
|
+
*/
|
|
43
|
+
export declare function bundleSameDocument(schema: any, doc: any): any;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @file The `jaren-contract` command: compile a contract document and
|
|
4
|
+
* print or write one of its projections — `describe` (the `describe()`
|
|
5
|
+
* summary), `public` (the public projection), `openapi`, `types`
|
|
6
|
+
* (TypeScript declarations), `docs` (Markdown) — with `--check` to fail
|
|
7
|
+
* CI when a written artifact has drifted from what the document projects
|
|
8
|
+
* today; plus `diff --from a.json --to b.json`, the classified change
|
|
9
|
+
* report (docs/CONTRACT-FORMAT.md §13), whose `--fail-on <classes>`
|
|
10
|
+
* makes it a compatibility gate. Exit codes: 0 current/written (diff:
|
|
11
|
+
* no failing class), 1 drift under `--check` (diff: a `--fail-on` class
|
|
12
|
+
* is non-empty), 2 a usage error, an unreadable document or a compile
|
|
13
|
+
* refusal (printed as `code docPath message`).
|
|
14
|
+
*/
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file `openHttpClient(contract, options)`: the HTTP client binding of a
|
|
3
|
+
* compiled contract (docs/CONTRACT-FORMAT.md §10) — the `open(contract,
|
|
4
|
+
* options) → Client` half of the driver pair whose server half is
|
|
5
|
+
* `serveHttp`. `invoke(op, input, ctx)` validates the input with the
|
|
6
|
+
* SAME compiled validator the server will run, splits it by the declared
|
|
7
|
+
* locations (path, query, header, body), sends it through an injectable
|
|
8
|
+
* `fetch`, and resolves a D6 outcome for every possible result — success,
|
|
9
|
+
* declared failure, network failure, contract violation, cancellation —
|
|
10
|
+
* keeping the three identities apart: the caller's `attempt` (carried
|
|
11
|
+
* in `meta`, never sent), the server's `trace` (read from
|
|
12
|
+
* `x-jaren-trace`, never generated here) and the idempotency `key`
|
|
13
|
+
* (generated here per command, sent as `Idempotency-Key`, optionally
|
|
14
|
+
* recorded in a durable `storage` WITHOUT the input).
|
|
15
|
+
*
|
|
16
|
+
* `invoke` NEVER rejects for anything a server or a network can do; it
|
|
17
|
+
* throws only for the host's own mistakes (`JC1005`: an unknown or
|
|
18
|
+
* opaque operation). `url(op, input)` builds the URL of any operation —
|
|
19
|
+
* what an `<img src>` uses for an opaque one. `negotiate()` asks the
|
|
20
|
+
* server's well-known description whether the two ends speak compatible
|
|
21
|
+
* versions. Everything per operation is decided once at `open`.
|
|
22
|
+
*/
|
|
23
|
+
import { JarenValidator } from '@jarenjs/validate';
|
|
24
|
+
import { CLIENT_ERRORS } from './outcome.js';
|
|
25
|
+
export { CLIENT_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('./outcome.js').Outcome;
|
|
30
|
+
export type OutcomeMeta = import('./outcome.js').OutcomeMeta;
|
|
31
|
+
export type OutcomeError = import('./outcome.js').OutcomeError;
|
|
32
|
+
export type OutcomeRoute = import('./outcome.js').OutcomeRoute;
|
|
33
|
+
export type KeyStorage = {
|
|
34
|
+
read: () => any;
|
|
35
|
+
write: (value: any) => any;
|
|
36
|
+
};
|
|
37
|
+
export type HttpClientOptions = {
|
|
38
|
+
/**
|
|
39
|
+
* - default `globalThis.fetch`
|
|
40
|
+
*/
|
|
41
|
+
fetch?: (url: string, init: RequestInit) => Promise<any>;
|
|
42
|
+
/**
|
|
43
|
+
* - prefixed to every path; default `''` (relative URLs)
|
|
44
|
+
*/
|
|
45
|
+
baseUrl?: string;
|
|
46
|
+
/**
|
|
47
|
+
* - static headers, merged under per-call ones
|
|
48
|
+
*/
|
|
49
|
+
headers?: Record<string, string>;
|
|
50
|
+
/**
|
|
51
|
+
* - the idempotency key generator; default `crypto.randomUUID`
|
|
52
|
+
*/
|
|
53
|
+
keys?: () => string;
|
|
54
|
+
/**
|
|
55
|
+
* - durable key records; default `null`
|
|
56
|
+
*/
|
|
57
|
+
storage?: KeyStorage | null;
|
|
58
|
+
/**
|
|
59
|
+
* - per request; `0` (default) means none; composed with `ctx.signal`
|
|
60
|
+
*/
|
|
61
|
+
timeoutMs?: number;
|
|
62
|
+
/**
|
|
63
|
+
* - the retry backoff sleeper (injectable for tests)
|
|
64
|
+
*/
|
|
65
|
+
sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
66
|
+
/**
|
|
67
|
+
* - a message catalog consulted before the English one
|
|
68
|
+
*/
|
|
69
|
+
catalog?: Record<string, string | ((params: object) => string)>;
|
|
70
|
+
/**
|
|
71
|
+
* - the server's description path; default `/.well-known/jaren-contract`
|
|
72
|
+
*/
|
|
73
|
+
wellKnown?: string;
|
|
74
|
+
/**
|
|
75
|
+
* - the clock stamped into key records; default `Date.now`
|
|
76
|
+
*/
|
|
77
|
+
now?: () => number;
|
|
78
|
+
/**
|
|
79
|
+
* - the validator `url()` compiles its path/query check with
|
|
80
|
+
*/
|
|
81
|
+
validator?: JarenValidator<any>;
|
|
82
|
+
};
|
|
83
|
+
export type InvokeContext = {
|
|
84
|
+
/**
|
|
85
|
+
* - cancels the request (`kind: "cancelled"`)
|
|
86
|
+
*/
|
|
87
|
+
signal?: AbortSignal;
|
|
88
|
+
/**
|
|
89
|
+
* - the caller's attempt id, echoed in `meta.attempt`, never sent
|
|
90
|
+
*/
|
|
91
|
+
attempt?: unknown;
|
|
92
|
+
/**
|
|
93
|
+
* - the key to send instead of a generated one
|
|
94
|
+
*/
|
|
95
|
+
idempotencyKey?: string;
|
|
96
|
+
/**
|
|
97
|
+
* - per-call headers (over the static ones)
|
|
98
|
+
*/
|
|
99
|
+
headers?: Record<string, string>;
|
|
100
|
+
/**
|
|
101
|
+
* - sent as `If-None-Match`
|
|
102
|
+
*/
|
|
103
|
+
ifNoneMatch?: string;
|
|
104
|
+
/**
|
|
105
|
+
* - sent as `If-Match`
|
|
106
|
+
*/
|
|
107
|
+
ifMatch?: string;
|
|
108
|
+
};
|
|
109
|
+
export type Negotiation = {
|
|
110
|
+
compatible: boolean;
|
|
111
|
+
reason: 'same-version' | 'server-accepts' | 'client-accepts' | 'version-mismatch' | 'unreachable' | 'not-a-contract';
|
|
112
|
+
server: {
|
|
113
|
+
id: string | null;
|
|
114
|
+
version: string | null;
|
|
115
|
+
compat: string[];
|
|
116
|
+
revision: string | null;
|
|
117
|
+
} | null;
|
|
118
|
+
/**
|
|
119
|
+
* - `null` when compatible
|
|
120
|
+
*/
|
|
121
|
+
error: {
|
|
122
|
+
code: string;
|
|
123
|
+
message: string;
|
|
124
|
+
} | null;
|
|
125
|
+
};
|
|
126
|
+
export type HttpClientCapabilities = {
|
|
127
|
+
name: 'http';
|
|
128
|
+
status: true;
|
|
129
|
+
headers: true;
|
|
130
|
+
media: true;
|
|
131
|
+
etag: true;
|
|
132
|
+
idempotency: true;
|
|
133
|
+
/**
|
|
134
|
+
* - a `storage` was given
|
|
135
|
+
*/
|
|
136
|
+
durableKeys: boolean;
|
|
137
|
+
/**
|
|
138
|
+
* - `subscribe` carries SSE streams (docs/CONTRACT-FORMAT.md §19)
|
|
139
|
+
*/
|
|
140
|
+
stream: true;
|
|
141
|
+
cancel: 'signal';
|
|
142
|
+
};
|
|
143
|
+
export type SubscribeOptions = {
|
|
144
|
+
onSnapshot?: (value: unknown, info: {
|
|
145
|
+
seq: number;
|
|
146
|
+
resumed: boolean;
|
|
147
|
+
}) => void;
|
|
148
|
+
onPatch?: (emission: {
|
|
149
|
+
patch: unknown[];
|
|
150
|
+
seq: number;
|
|
151
|
+
}) => void;
|
|
152
|
+
onError?: (outcome: Outcome) => void;
|
|
153
|
+
onEnd?: (info: {
|
|
154
|
+
reason: string;
|
|
155
|
+
}) => void;
|
|
156
|
+
/**
|
|
157
|
+
* - stops the subscription silently
|
|
158
|
+
*/
|
|
159
|
+
signal?: AbortSignal;
|
|
160
|
+
/**
|
|
161
|
+
* - the resume seq (what a reconnect passes)
|
|
162
|
+
*/
|
|
163
|
+
lastSeq?: number;
|
|
164
|
+
};
|
|
165
|
+
export type HttpClient = {
|
|
166
|
+
invoke: (op: string, input?: unknown, ctx?: InvokeContext) => Promise<Outcome>;
|
|
167
|
+
subscribe: (op: string, input?: unknown, options?: SubscribeOptions) => {
|
|
168
|
+
stop: () => void;
|
|
169
|
+
};
|
|
170
|
+
url: (op: string, input?: unknown) => string;
|
|
171
|
+
negotiate: (options?: {
|
|
172
|
+
signal?: AbortSignal;
|
|
173
|
+
}) => Promise<Negotiation>;
|
|
174
|
+
/**
|
|
175
|
+
* - the key records a restart must reconcile
|
|
176
|
+
*/
|
|
177
|
+
pending: () => Promise<{
|
|
178
|
+
op: string;
|
|
179
|
+
key: string;
|
|
180
|
+
}[]>;
|
|
181
|
+
capabilities: HttpClientCapabilities;
|
|
182
|
+
contract: Contract;
|
|
183
|
+
describe: () => any;
|
|
184
|
+
/**
|
|
185
|
+
* - aborts every in-flight request; later invokes resolve `cancelled`
|
|
186
|
+
*/
|
|
187
|
+
close: () => void;
|
|
188
|
+
};
|
|
189
|
+
export type TransportCheck = {
|
|
190
|
+
validate: ((value: unknown) => any) | null;
|
|
191
|
+
};
|
|
192
|
+
export type ClientRoute = {
|
|
193
|
+
op: CompiledOperation;
|
|
194
|
+
id: string;
|
|
195
|
+
method: string;
|
|
196
|
+
segments: readonly import('../path.js').PathSegment[];
|
|
197
|
+
queryMembers: readonly string[];
|
|
198
|
+
queryRepeated: ReadonlySet<string>;
|
|
199
|
+
headerMembers: readonly string[];
|
|
200
|
+
headerNames: readonly string[];
|
|
201
|
+
/**
|
|
202
|
+
* - body-located members when the body is their object
|
|
203
|
+
*/
|
|
204
|
+
bodyMembers: readonly string[];
|
|
205
|
+
/**
|
|
206
|
+
* - the member whose value IS the body
|
|
207
|
+
*/
|
|
208
|
+
wholeBody: string | null;
|
|
209
|
+
hasBody: boolean;
|
|
210
|
+
media: string;
|
|
211
|
+
opaque: boolean;
|
|
212
|
+
hasInput: boolean;
|
|
213
|
+
validateInput: ((value: unknown) => any) | null;
|
|
214
|
+
details: 'none' | 'paths' | 'full';
|
|
215
|
+
idempotency: 'none' | 'optional' | 'required';
|
|
216
|
+
retry: {
|
|
217
|
+
max: number;
|
|
218
|
+
on: readonly string[];
|
|
219
|
+
} | null;
|
|
220
|
+
retryOn: ReadonlySet<string>;
|
|
221
|
+
outcome: OutcomeRoute;
|
|
222
|
+
/**
|
|
223
|
+
* - lazily compiled for `url()`
|
|
224
|
+
*/
|
|
225
|
+
transport: TransportCheck;
|
|
226
|
+
};
|
|
227
|
+
/**
|
|
228
|
+
* Open an HTTP client over a compiled contract.
|
|
229
|
+
*
|
|
230
|
+
* @param {Contract} contract
|
|
231
|
+
* @param {HttpClientOptions} [options]
|
|
232
|
+
* @returns {HttpClient}
|
|
233
|
+
* @throws {ContractHostError} `JC1008` for a malformed argument or option
|
|
234
|
+
* @example
|
|
235
|
+
* const client = openHttpClient(contract, { baseUrl: 'https://api.example', timeoutMs: 5000 });
|
|
236
|
+
* const outcome = await client.invoke('catalog.load', { since: '2026-01-01T00:00:00Z' });
|
|
237
|
+
* if (outcome.ok) render(outcome.value);
|
|
238
|
+
* else if (outcome.kind === 'failure') show(outcome.error.code); // a declared error, e.g. 'stale'
|
|
239
|
+
* else if (outcome.kind === 'network') retryLater();
|
|
240
|
+
*/
|
|
241
|
+
export declare function openHttpClient(contract: Contract, options?: HttpClientOptions): HttpClient;
|
|
242
|
+
export { okOutcome, failedOutcome, makeMeta, outcomeError, isOutcome, assembleOutcome, prepareOutcomeRoute, hostFailureOutcome, OUTCOME_ERROR_MEMBERS, OUTCOME_META_MEMBERS, } from './outcome.js';
|