@jarenjs/contract 0.73.0 → 0.83.2

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 CHANGED
@@ -1,5 +1,7 @@
1
1
  # @jarenjs/contract
2
2
 
3
+ Providers and permanent commands compose with injected transport, authority and receipt repositories. The [adoption evidence ledger](../../docs/ADOPTION-EVIDENCE.md) separates synthetic qualification from actual provider guarantees; unknown single-send outcomes require explicit reconciliation.
4
+
3
5
  Operation contracts for the Jaren suite. A **`$contract` document** — the
4
6
  sibling of `$model`, `$fsm` and `jaren-app` — declares the operations two
5
7
  Jaren ends may exchange: JSON in, JSON out, each with a *kind* (`read` or
@@ -734,8 +736,47 @@ Every subpath a consumer can import, derived from the manifest by
734
736
  | `@jarenjs/contract/schemas/jaren-contract.draft-07.schema.json` | schema | — |
735
737
  | `@jarenjs/contract/schemas/jaren-contract.schema.json` | schema | — |
736
738
  | `@jarenjs/contract/package.json` | metadata | — |
739
+ | `@jarenjs/contract/provider` | JavaScript | declared |
740
+ | `@jarenjs/contract/command` | JavaScript | declared |
737
741
  <!--/fact-->
738
742
 
739
743
  Author JSON template catalogs and MessageSpec references with the
740
744
  [messages pen](../linq/docs/MESSAGES-PEN.md); existing locale render functions
741
745
  retain their pluralization and formatting behavior.
746
+
747
+ ## Structured query inputs and app reconnect
748
+
749
+ Object- and array-typed query members use one JSON-encoded parameter,
750
+ including empty arrays, nulls and nested values. Scalar strings stay
751
+ literal; parsed JSON is validated without coercion. The client, URL
752
+ builder, server dispatcher and OpenAPI projection share this codec.
753
+ Malformed or repeated JSON members are `JC2012`. Handwritten callers
754
+ must migrate repeated array keys (`tag=a&tag=b`) to one encoded JSON array;
755
+ deploy matching client/server versions and revise the contract's `version`
756
+ for revision negotiation. See [the wire rules](docs/CONTRACT-FORMAT.md#4-the-http-binding-and-member-locations).
757
+
758
+ Opt into HTTP subscription recovery per operation:
759
+
760
+ ```javascript
761
+ const binding = contractAppBinding(contract, {
762
+ subs: { 'board.feed': { reconnect: { max: 2 } } },
763
+ });
764
+ ```
765
+
766
+ The generated subscription forwards the option to `client.subscribe`.
767
+ The slot stays live with its id and last value while reconnecting; replay
768
+ continues from the last delivered sequence. Exhaustion surfaces `JC2097`.
769
+ Stop, reset and app destruction stop recovery. Without the option,
770
+ network loss is surfaced immediately; an explicit server end remains
771
+ terminal. Port and local channel lifecycles are unchanged.
772
+
773
+ The fixed-heap host-seam test measures the Node response's writable queue
774
+ and the producer's unwritten chunk separately from bytes already accepted
775
+ by TCP. The in-process Fetch leg can measure produced-minus-consumed
776
+ bytes directly. Complete-byte hashes, cursor pulls and cancellation
777
+ finalizers remain part of the same end-to-end test.
778
+
779
+ `@jarenjs/contract/provider` supplies bounded provider execution, compiled JSON
780
+ REST/GraphQL dialects and private run authority. Partial observations retain
781
+ wire text and never become complete snapshots. See
782
+ [provider descriptors and ingestion](docs/PROVIDER-FORMAT.md).
@@ -33,6 +33,16 @@ export type ContractAppBindingOptions = {
33
33
  * - the operations the app uses; default every operation
34
34
  */
35
35
  ops?: readonly string[];
36
+ /**
37
+ * - per
38
+ * subscribe-operation options. Reconnect is opt-in; the slot stays live
39
+ * while the HTTP client resumes, and reports error after exhaustion.
40
+ */
41
+ subs?: Record<string, {
42
+ reconnect: {
43
+ max: number;
44
+ };
45
+ }>;
36
46
  };
37
47
  export type TaskSlot = {
38
48
  id: number;
@@ -10,7 +10,7 @@
10
10
  */
11
11
  export { contractAppBinding } from './binding.js';
12
12
  export { createContractEffect } from './effect.js';
13
- export { createContractSubscription } from './subscription.js';
13
+ export { createContractSubscription, createRunPageHandler } from './subscription.js';
14
14
  export type ContractAppBinding = import('./binding.js').ContractAppBinding;
15
15
  export type ContractAppBindingOptions = import('./binding.js').ContractAppBindingOptions;
16
16
  export type TaskSlot = import('./binding.js').TaskSlot;
@@ -42,6 +42,9 @@ export type StreamProps = {
42
42
  snapshot: string;
43
43
  patch: string;
44
44
  error: string;
45
+ reconnect?: {
46
+ max: number;
47
+ };
45
48
  };
46
49
  /**
47
50
  * @typedef {import('../compile.js').Contract} Contract
@@ -60,7 +63,7 @@ export type StreamProps = {
60
63
  /**
61
64
  * The props of one generated subs entry: the operation, the slot id and
62
65
  * 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
66
+ * @typedef {{ op: string, id: number, input: unknown, snapshot: string, patch: string, error: string, reconnect?: { max: number } }} StreamProps
64
67
  */
65
68
  /**
66
69
  * Make the `contract-stream` subscription handler of an app over a
@@ -80,3 +83,13 @@ export type StreamProps = {
80
83
  */
81
84
  export declare function createContractSubscription(client: StreamClientLike, options?: ContractSubscriptionOptions): (props: StreamProps, dispatch: (name: string, payload?: any) => void) => (() => void);
82
85
  export { CLIENT_ERRORS };
86
+ /**
87
+ * Authorize each bounded run-page read before touching durable history. Register
88
+ * as an ordinary read handler; callers may poll or resume from their last cursor.
89
+ * @param {{ page: (id: string, options: any) => any, authorize: (input: any, context: any) => any, maxPage?: number }} options
90
+ */
91
+ export declare function createRunPageHandler(options: {
92
+ page: (id: string, options: any) => any;
93
+ authorize: (input: any, context: any) => any;
94
+ maxPage?: number;
95
+ }): (input: any, context: any) => Promise<any>;
@@ -262,7 +262,7 @@ export type ClientRoute = {
262
262
  method: string;
263
263
  segments: readonly import('../path.js').PathSegment[];
264
264
  queryMembers: readonly string[];
265
- queryRepeated: ReadonlySet<string>;
265
+ queryJson: ReadonlySet<string>;
266
266
  headerMembers: readonly string[];
267
267
  headerNames: readonly string[];
268
268
  /**
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Compile once around the existing neutral operation pipeline. Identity and
3
+ * current authorization are application policy, never inferred from a receipt.
4
+ * Handlers receive the repository's transaction as ctx.host; current-state reads
5
+ * belong to a separate read operation. Transport TTL policy remains independent.
6
+ * @param {import('./compile.js').CompiledOperation} operation
7
+ * @param {{ repository: { execute: Function }, identity: (input: any, context: any) => any,
8
+ * authorize: (input: any, context: any) => any, handler: (input: any, context: any) => any,
9
+ * commitFailures?: string[], references?: (result: any, context: any) => any[],
10
+ * refuse?: (reason: string, context: any) => any }} options
11
+ */
12
+ export declare function createCommand(operation: import('./compile.js').CompiledOperation, options: {
13
+ repository: {
14
+ execute: Function;
15
+ };
16
+ identity: (input: any, context: any) => any;
17
+ authorize: (input: any, context: any) => any;
18
+ handler: (input: any, context: any) => any;
19
+ commitFailures?: string[];
20
+ references?: (result: any, context: any) => any[];
21
+ refuse?: (reason: string, context: any) => any;
22
+ }): Readonly<{
23
+ execute: (input: any, context?: any) => Promise<any>;
24
+ /** A deliberate carrier wrapper around the one settlement implementation.
25
+ * @param {any} input @param {any} context */
26
+ handler(input: any, context: any): Promise<any>;
27
+ }>;
@@ -38,6 +38,11 @@ export type InputTransport = {
38
38
  header: readonly string[];
39
39
  repeated: readonly string[];
40
40
  };
41
+ /**
42
+ * - query members with a declared
43
+ * object/array type, encoded as one JSON value (including nullable unions)
44
+ */
45
+ queryJson: readonly string[];
41
46
  schemas: Readonly<Record<string, any>>;
42
47
  required: readonly string[];
43
48
  };
@@ -49,6 +49,7 @@ export declare const CONTRACT_CODES: Readonly<{
49
49
  JC0018: "a subscribe operation declares a policy.task other than switch — a subscription slot is replaced, never queued";
50
50
  JC0019: "a subscribe operation is bound to a method other than GET — a stream is fetched, not sent";
51
51
  JC0020: "a subscribe operation declares a policy.idempotency other than none — a subscription registers, it does not commit";
52
+ JC0021: "a provider protocol descriptor is malformed or names an undeclared transform capability";
52
53
  JC0060: "the OpenAPI projection met a schema keyword it cannot map honestly: a boolean required (draft-04 style) or a same-document $ref that lands outside $defs (both dropped and reported under lenient), or a components member inside a schema";
53
54
  JC0061: "the public projection is not canonicalizable, so no revision exists — a string with an unpaired surrogate, say; docPath points at the offending value inside the projection";
54
55
  JC1001: "serveHttp, serveLocal or servePort: handlers is not an object, a key names no operation of the contract, a value is not a function, or an option (a channel without postMessage, say) is malformed";
@@ -62,6 +63,8 @@ export declare const CONTRACT_CODES: Readonly<{
62
63
  JC1009: "encodeSseEvent (the stream wire): an event, id or data string the SSE frame cannot carry — a bare carriage return inside data, a line terminator inside event or id";
63
64
  JC1010: "client.subscribe was asked for an operation that is not a subscribe operation (invoke carries reads and commands; subscribe carries streams)";
64
65
  JC1011: "a ledger commit or fail named a ref that settles no started record: the key expired, was reclaimed under a newer generation, or was settled already — the settlement is refused; the binding reports it to onError and the response still goes out";
66
+ JC1012: "a provider executor, descriptor host or run capability is malformed";
67
+ JC1013: "a durable command settlement capability is malformed";
65
68
  JC2001: "no operation matches the request method and path (404)";
66
69
  JC2002: "the path shape is served under other methods (405, Allow lists them)";
67
70
  JC2003: "the request body exceeds policy.limits.maxBodyBytes, by content-length or by read length (413)";
@@ -99,6 +102,7 @@ export declare const CONTRACT_CODES: Readonly<{
99
102
  JC2095: "a requested resume was refused — informational, carried as resumed:false in the fresh snapshot's event data, never an outcome";
100
103
  JC2096: "the stream's bounded queue would overflow — the consumer reads slower than the source emits, or a replay page outran it (kind network, retryable; the stream ends with an error event carrying this code and the carrier tears the connection down)";
101
104
  JC2097: "the client's reconnect budget is exhausted: every attempt after a network loss failed the same way (kind network, not retryable; details carry the attempts made and the last network code, client-side)";
105
+ JC2110: "a durable command was refused or failed validation";
102
106
  }>;
103
107
  /**
104
108
  * A defect in the contract document itself, raised while
@@ -102,9 +102,9 @@ export type Route = {
102
102
  pathMembers: readonly string[];
103
103
  queryMembers: ReadonlySet<string>;
104
104
  /**
105
- * - array-typed query members
105
+ * - JSON-encoded query members
106
106
  */
107
- repeated: ReadonlySet<string>;
107
+ queryJson: ReadonlySet<string>;
108
108
  /**
109
109
  * - member names
110
110
  */
@@ -292,18 +292,19 @@ export declare function formatEntityTag(tag: string, strong: boolean): string;
292
292
  /**
293
293
  * Decode a query string into the declared members of an input object:
294
294
  * only declared names are set (an undeclared key is never merged, so no
295
- * request can smuggle a member); a `repeated` member collects every
296
- * occurrence into an array, every other member is last-wins; a `+` is a
295
+ * request can smuggle a member); JSON members decode exactly one value,
296
+ * scalar members are last-wins; a `+` is a
297
297
  * space and escapes decode as `application/x-www-form-urlencoded`
298
298
  * (`URLSearchParams`). Returns `false` when the query is not decodable
299
- * (a malformed percent-escape or invalid UTF-8) the `JC2012` case.
299
+ * (malformed percent-escape, UTF-8 or JSON, or a repeated JSON member)
300
+ * — the `JC2012` case.
300
301
  * @param {string} query - the part after `?`, possibly empty
301
302
  * @param {ReadonlySet<string>} declared - the query member names
302
- * @param {ReadonlySet<string>} repeated - the array-typed ones
303
303
  * @param {Record<string, unknown>} out - the input object under assembly
304
+ * @param {ReadonlySet<string>} json - schema-directed JSON members
304
305
  * @returns {boolean} false when not decodable
305
306
  */
306
- export declare function decodeQuery(query: string, declared: ReadonlySet<string>, repeated: ReadonlySet<string>, out: Record<string, unknown>): boolean;
307
+ export declare function decodeQuery(query: string, declared: ReadonlySet<string>, out: Record<string, unknown>, json: ReadonlySet<string>): boolean;
307
308
  /**
308
309
  * The verdict of a compiled validator under either contract: the
309
310
  * default `{ valid, errors }` or a host-injected boolean validator.
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Compile selectors/transforms once using the existing Query/JSLT engines.
3
+ * Named callbacks receive cloned protocol data only, never the run host.
4
+ * These are trusted host functions, not a sandbox for arbitrary JavaScript.
5
+ * @param {any} document
6
+ * @param {{ callbacks?: Record<string, (value: any) => any>, compileSchema?: (schema: any) => (value: any) => boolean }} [options]
7
+ */
8
+ export declare function compileProvider(document: any, options?: {
9
+ callbacks?: Record<string, (value: any) => any>;
10
+ compileSchema?: (schema: any) => (value: any) => boolean;
11
+ }): Readonly<{
12
+ document: any;
13
+ pages: (input: any, context: any) => AsyncGenerator<{
14
+ state: any;
15
+ reason: any;
16
+ pages: number;
17
+ rows: number;
18
+ bytes: number;
19
+ attempts: number;
20
+ cursor: any;
21
+ } | {
22
+ state: any;
23
+ reason: any;
24
+ pages: number;
25
+ rows: number;
26
+ bytes: number;
27
+ attempts: number;
28
+ cursor: any;
29
+ response: any;
30
+ raw?: undefined;
31
+ text?: undefined;
32
+ ids?: undefined;
33
+ continuation?: undefined;
34
+ complete?: undefined;
35
+ errors?: undefined;
36
+ cost?: undefined;
37
+ version?: undefined;
38
+ } | {
39
+ state: any;
40
+ reason: any;
41
+ pages: number;
42
+ rows: number;
43
+ bytes: number;
44
+ attempts: number;
45
+ cursor: any;
46
+ text: any;
47
+ raw?: undefined;
48
+ ids?: undefined;
49
+ continuation?: undefined;
50
+ complete?: undefined;
51
+ errors?: undefined;
52
+ cost?: undefined;
53
+ version?: undefined;
54
+ } | {
55
+ state: string;
56
+ raw: any;
57
+ text: any;
58
+ rows: any[];
59
+ ids: any[];
60
+ cursor: any;
61
+ continuation: any;
62
+ complete: boolean;
63
+ errors: any;
64
+ cost: any;
65
+ version: any;
66
+ reason: string | null;
67
+ bytes: any;
68
+ attempts: any;
69
+ }, void, unknown>;
70
+ /** Collect a bounded pull; streaming consumers should iterate pages instead.
71
+ * @param {any} input @param {any} context @returns {Promise<any>} */
72
+ pull(input: any, context: any): Promise<any>;
73
+ }>;
@@ -0,0 +1,104 @@
1
+ import { createAttemptBudget } from '@jarenjs/core/retry';
2
+ import { ContractHostError } from '../errors.js';
3
+ /** @param {string} reason @returns {ContractHostError} */
4
+ export declare const providerHostError: (reason: string) => ContractHostError;
5
+ export type ProviderRequest = {
6
+ url: string;
7
+ method?: string;
8
+ /**
9
+ * - public headers only; credentials belong to host transport
10
+ */
11
+ headers?: Record<string, string>;
12
+ body?: string;
13
+ safety: 'safe-read' | 'provider-idempotent' | 'single-send';
14
+ /**
15
+ * - required evidence for provider-idempotent
16
+ */
17
+ idempotencyKey?: string;
18
+ /**
19
+ * - opaque scheduler scope, never credentials
20
+ */
21
+ account?: string;
22
+ };
23
+ export type ProviderExecutorOptions = {
24
+ transport?: (request: ProviderRequest, context: {
25
+ signal: AbortSignal;
26
+ attempt: number;
27
+ maxAttempts: 1;
28
+ }) => Promise<Response>;
29
+ attempts?: number;
30
+ overallMs?: number;
31
+ attemptMs?: number;
32
+ /**
33
+ * - response bytes across all attempts
34
+ */
35
+ maxBytes?: number;
36
+ maxRequestBytes?: number;
37
+ baseMs?: number;
38
+ maxMs?: number;
39
+ retryAfter?: 'http' | 'milliseconds' | 'none';
40
+ retryAfterHeader?: string;
41
+ concurrency?: number;
42
+ maxQueue?: number;
43
+ maxScopes?: number;
44
+ spacingMs?: number;
45
+ now?: () => number;
46
+ random?: () => number;
47
+ sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
48
+ };
49
+ /**
50
+ * @typedef {Object} ProviderRequest
51
+ * @property {string} url
52
+ * @property {string} [method]
53
+ * @property {Record<string, string>} [headers] - public headers only; credentials belong to host transport
54
+ * @property {string} [body]
55
+ * @property {'safe-read' | 'provider-idempotent' | 'single-send'} safety
56
+ * @property {string} [idempotencyKey] - required evidence for provider-idempotent
57
+ * @property {string} [account] - opaque scheduler scope, never credentials
58
+ */
59
+ /**
60
+ * @typedef {Object} ProviderExecutorOptions
61
+ * @property {(request: ProviderRequest, context: { signal: AbortSignal, attempt: number, maxAttempts: 1 }) => Promise<Response>} [transport]
62
+ * @property {number} [attempts]
63
+ * @property {number} [overallMs]
64
+ * @property {number} [attemptMs]
65
+ * @property {number} [maxBytes] - response bytes across all attempts
66
+ * @property {number} [maxRequestBytes]
67
+ * @property {number} [baseMs]
68
+ * @property {number} [maxMs]
69
+ * @property {'http' | 'milliseconds' | 'none'} [retryAfter]
70
+ * @property {string} [retryAfterHeader]
71
+ * @property {number} [concurrency]
72
+ * @property {number} [maxQueue]
73
+ * @property {number} [maxScopes]
74
+ * @property {number} [spacingMs]
75
+ * @property {() => number} [now]
76
+ * @property {() => number} [random]
77
+ * @property {(ms: number, signal?: AbortSignal) => Promise<void>} [sleep]
78
+ */
79
+ /**
80
+ * Injected transports must issue exactly one request and honor the supplied
81
+ * signal; SDK retry loops must be disabled. Shutdown awaits transport and body
82
+ * settlement even when a transport ignores its signal. JSON outcomes never
83
+ * contain exceptions, request headers, controllers or live response handles.
84
+ * @param {ProviderExecutorOptions} [options]
85
+ */
86
+ export declare function createProviderExecutor(options?: ProviderExecutorOptions): Readonly<{
87
+ /** @param {ProviderRequest} request
88
+ * @param {{ signal?: AbortSignal, deadline?: number, maxBytes?: number, budget?: ReturnType<typeof createAttemptBudget>, beforeDispatch?: (request: ProviderRequest) => boolean | Promise<boolean> }} [context]
89
+ * @returns {Promise<any>} */
90
+ execute(request: ProviderRequest, context?: {
91
+ signal?: AbortSignal;
92
+ deadline?: number;
93
+ maxBytes?: number;
94
+ budget?: ReturnType<typeof createAttemptBudget>;
95
+ beforeDispatch?: (request: ProviderRequest) => boolean | Promise<boolean>;
96
+ }): Promise<any>;
97
+ /** Stop new work and drain requests, body readers and retry waits. */
98
+ close(): Promise<void>;
99
+ stats: () => {
100
+ active: number;
101
+ queued: number;
102
+ closed: boolean;
103
+ };
104
+ }>;
@@ -0,0 +1,4 @@
1
+ /** Bounded provider protocols with host-injected transport and authority. */
2
+ export { createProviderExecutor } from './execute.js';
3
+ export { compileProvider } from './compile.js';
4
+ export { withProviderRun } from './run.js';
@@ -0,0 +1,33 @@
1
+ export type ProviderAuthority = {
2
+ runId: string;
3
+ actor: string;
4
+ environment: string;
5
+ destination: string;
6
+ revision: string;
7
+ lease: string;
8
+ };
9
+ /**
10
+ * @typedef {{ runId: string, actor: string, environment: string,
11
+ * destination: string, revision: string, lease: string }} ProviderAuthority
12
+ */
13
+ /**
14
+ * Run a callback with private, isolated transport resources. current() must
15
+ * re-read live authority and return the matching evidence, or null to revoke.
16
+ * The host owns membership, destination resolution, OAuth and account locks.
17
+ * Only opaque evidence and JSON callback results can leave this lifetime.
18
+ * @param {ProviderAuthority} authority
19
+ * @param {{ identify: Function, acquire: Function,
20
+ * current: (evidence: ProviderAuthority, host: any, operation: any) => ProviderAuthority | null | Promise<ProviderAuthority | null>,
21
+ * transport: (request: import('./execute.js').ProviderRequest, context: any) => Promise<Response> }} host
22
+ * @param {(run: any) => any} work
23
+ * @param {import('./execute.js').ProviderExecutorOptions & { signal?: AbortSignal }} [options]
24
+ * @returns {Promise<any>}
25
+ */
26
+ export declare function withProviderRun(authority: ProviderAuthority, host: {
27
+ identify: Function;
28
+ acquire: Function;
29
+ current: (evidence: ProviderAuthority, host: any, operation: any) => ProviderAuthority | null | Promise<ProviderAuthority | null>;
30
+ transport: (request: import('./execute.js').ProviderRequest, context: any) => Promise<Response>;
31
+ }, work: (run: any) => any, options?: import('./execute.js').ProviderExecutorOptions & {
32
+ signal?: AbortSignal;
33
+ }): Promise<any>;