@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,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';