@jarenjs/contract 0.75.0 → 0.83.3
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 +9 -0
- package/dist/types/app/index.d.ts +1 -1
- package/dist/types/app/subscription.d.ts +10 -0
- package/dist/types/command.d.ts +27 -0
- package/dist/types/errors.d.ts +4 -0
- package/dist/types/provider/compile.d.ts +73 -0
- package/dist/types/provider/execute.d.ts +104 -0
- package/dist/types/provider/index.d.ts +4 -0
- package/dist/types/provider/run.d.ts +33 -0
- package/docs/CONTRACT-FORMAT.md +41 -0
- package/docs/DURABLE.md +66 -0
- package/docs/PROVIDER-FORMAT.md +157 -0
- package/package.json +14 -6
- package/src/app/index.js +1 -1
- package/src/app/subscription.js +21 -0
- package/src/client/http.js +3 -40
- package/src/command.js +70 -0
- package/src/errors.js +4 -0
- package/src/provider/compile.js +200 -0
- package/src/provider/execute.js +191 -0
- package/src/provider/index.js +5 -0
- package/src/provider/run.js +138 -0
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,6 +736,8 @@ 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
|
|
@@ -771,3 +775,8 @@ and the producer's unwritten chunk separately from bytes already accepted
|
|
|
771
775
|
by TCP. The in-process Fetch leg can measure produced-minus-consumed
|
|
772
776
|
bytes directly. Complete-byte hashes, cursor pulls and cancellation
|
|
773
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).
|
|
@@ -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;
|
|
@@ -83,3 +83,13 @@ export type StreamProps = {
|
|
|
83
83
|
*/
|
|
84
84
|
export declare function createContractSubscription(client: StreamClientLike, options?: ContractSubscriptionOptions): (props: StreamProps, dispatch: (name: string, payload?: any) => void) => (() => void);
|
|
85
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>;
|
|
@@ -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
|
+
}>;
|
package/dist/types/errors.d.ts
CHANGED
|
@@ -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
|
|
@@ -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,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>;
|
package/docs/CONTRACT-FORMAT.md
CHANGED
|
@@ -432,6 +432,7 @@ carries the same codes and a test holds them equal.
|
|
|
432
432
|
| JC0018 | a subscribe operation declares a `policy.task` other than `switch` — a subscription slot is replaced, never queued (§17) |
|
|
433
433
|
| JC0019 | a subscribe operation is bound to a method other than `GET` — a stream is fetched, not sent (§17) |
|
|
434
434
|
| JC0020 | a subscribe operation declares a `policy.idempotency` other than `none` — a subscription registers, it does not commit (§17) |
|
|
435
|
+
| JC0021 | a provider descriptor is malformed or names an undeclared transform capability |
|
|
435
436
|
|
|
436
437
|
`JC0021–JC0049` are reserved for further document-level rules and are
|
|
437
438
|
appended to this table when they land; `JC0050–JC0069` are the binding-
|
|
@@ -792,6 +793,9 @@ wire response:
|
|
|
792
793
|
| `JC1009` | the stream wire's SSE encoder was handed text the frame cannot carry: a bare carriage return inside `data`, a line terminator inside `event` or `id` (§18) |
|
|
793
794
|
| `JC1010` | `client.subscribe` was asked for an operation that is not a subscribe operation (§19) |
|
|
794
795
|
| `JC1011` | a ledger `commit`/`fail` named a ref that settles no started record — expired, reclaimed under a newer generation, or settled already (§8); refused by the ledger, reported to `onError` by the binding |
|
|
796
|
+
| `JC1013` | a durable command settlement capability is malformed |
|
|
797
|
+
| `JC2110` | a durable command was refused or failed validation |
|
|
798
|
+
| `JC1012` | a provider executor, descriptor host or run capability is malformed (PROVIDER-FORMAT.md) |
|
|
795
799
|
|
|
796
800
|
### §7.4 Headers
|
|
797
801
|
|
|
@@ -2597,3 +2601,40 @@ a body that ends without an `end` event is then `onEnd({ reason:
|
|
|
2597
2601
|
as the HTTP client does and then does nothing with it: a channel has no
|
|
2598
2602
|
network loss to reconnect from (a closed channel is `JC2074`, final),
|
|
2599
2603
|
and one options object serves both clients.
|
|
2604
|
+
|
|
2605
|
+
## Durable business commands
|
|
2606
|
+
|
|
2607
|
+
`createCommand(operation, options)` from `@jarenjs/contract/command` composes
|
|
2608
|
+
business settlement with the existing neutral input/output/error pipeline.
|
|
2609
|
+
It accepts a JSON command with `policy.idempotency: "none"`, an injected
|
|
2610
|
+
`repository.execute(identity, work, context)`, `identity(input, context)`,
|
|
2611
|
+
`authorize(input, context)` and `handler(input, context)`. The identity declares
|
|
2612
|
+
tenant, environment, aggregate, operation, command key, hash version and payload
|
|
2613
|
+
hash. The host is responsible for a collision-resistant hash over every field
|
|
2614
|
+
that affects the command; canonical JSON itself is a lossless alternative.
|
|
2615
|
+
|
|
2616
|
+
The handler receives the repository transaction as `ctx.host`. Expected entity
|
|
2617
|
+
revisions, domain writes, receipt writes and `ctx.host.jobs.enqueue` belong to
|
|
2618
|
+
that transaction. The neutral pipeline validates the handler's result before
|
|
2619
|
+
settlement. Any invalid output/error, failed receipt write or failed commit
|
|
2620
|
+
rolls back the transaction. Only failure codes explicitly listed in
|
|
2621
|
+
`commitFailures` may commit observations; ordinary declared failures roll back.
|
|
2622
|
+
The optional `references(outcome, context)` preserves application audit IDs.
|
|
2623
|
+
|
|
2624
|
+
Use the same `command.handler` in HTTP/local/port handler tables and
|
|
2625
|
+
`command.execute(input, context)` in jobs. Direct execution reports `committed`,
|
|
2626
|
+
`replay`, `uncommitted` or `refused`; committed/replayed outcomes live in
|
|
2627
|
+
`receipt.outcome`. `historic: true` means the stored effect, never current entity
|
|
2628
|
+
state. A separate read operation observes current state. Every invocation checks
|
|
2629
|
+
current authorization before accessing a receipt. Refused execution exposes no
|
|
2630
|
+
historic result. Bindings render a refusal as a generic host fault unless the
|
|
2631
|
+
host supplies `refuse(reason, context)` returning a declared operation failure.
|
|
2632
|
+
|
|
2633
|
+
Transport response caching is deliberately disallowed on a durable command:
|
|
2634
|
+
an HTTP ledger replay could bypass the command's current authorization hook.
|
|
2635
|
+
Existing HTTP TTL policies and local `capabilities.idempotency: false` retain
|
|
2636
|
+
their meanings. Business replay is supplied by the mapped receipt repository,
|
|
2637
|
+
not by the transport capability. Cancellation cannot undo an already committed
|
|
2638
|
+
receipt or establish that a remote effect did not happen.
|
|
2639
|
+
|
|
2640
|
+
See [durable composition](DURABLE.md) for the crash matrix and public recipe.
|
package/docs/DURABLE.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Durable business operations
|
|
2
|
+
|
|
3
|
+
Commands, mapped receipts, existing jobs and workflow checkpoints compose through
|
|
4
|
+
injected capabilities. Application models own business history, field mapping,
|
|
5
|
+
authorization, revision policy and arithmetic. No second authoritative ledger or
|
|
6
|
+
scheduler is created. These are local atomicity and recovery guarantees; external
|
|
7
|
+
exactly-once delivery is not claimed.
|
|
8
|
+
|
|
9
|
+
| Boundary | Durable observation | Automatic send/replay |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| Same command identity/hash on HTTP, local or job | Original validated outcome | Business effect replays without another mutation |
|
|
12
|
+
| Different payload/hash version | Identity mismatch | Refused before mutation |
|
|
13
|
+
| Current actor unauthorized | No historic disclosure | Refused before receipt read |
|
|
14
|
+
| Output/error/receipt/commit failure | Entire local transaction rolls back | No business receipt exists |
|
|
15
|
+
| Declared committed failure | Validated observation and stable references | Historic failure replays |
|
|
16
|
+
| Lease/HTTP TTL expires | Business receipt remains | Receipt wins before work admission |
|
|
17
|
+
| Before external sending intent | Prepared leg | A valid job fence may admit its first send |
|
|
18
|
+
| Sending intent, lost response or lost settlement | Unresolved leg | No automatic resend |
|
|
19
|
+
| One confirmed leg, one unresolved leg | Both per-leg observations retained | Confirmed leg skipped; unresolved leg awaits evidence |
|
|
20
|
+
| Provider idempotency guarantee and explicit retry decision | Original key and finite attempt budget | Only the approved next attempt is admitted |
|
|
21
|
+
| Probe absent without guarantee | Uncertainty retained | Refused as retry proof |
|
|
22
|
+
| Lease takeover | New job fence | Stale begin/settlement refused |
|
|
23
|
+
| Observer navigation | Run persists; observation detaches | Resume by durable cursor |
|
|
24
|
+
| Explicit cancellation | Intent, worker drain, final observation | Resources release after final persistence |
|
|
25
|
+
| Incompatible checkpoint | Stored history retained | Refused before reuse |
|
|
26
|
+
| Reset/compaction | Receipt identity and audit references retained | Cannot authorize unresolved replay |
|
|
27
|
+
|
|
28
|
+
Use installed public exports:
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
import { createCommand } from '@jarenjs/contract/command';
|
|
32
|
+
import { createDbReceipts, createDbEffectStore, createDbRunStore } from '@jarenjs/linq/db';
|
|
33
|
+
import { createExternalEffects, createDomainRun } from '@jarenjs/flow';
|
|
34
|
+
import { createProviderExecutor } from '@jarenjs/contract/provider';
|
|
35
|
+
import { createRunPageHandler } from '@jarenjs/contract/app';
|
|
36
|
+
import { createRunObservation } from '@jarenjs/app';
|
|
37
|
+
|
|
38
|
+
// Application declarations: client, compiledContract, identity, authorize, adjust.
|
|
39
|
+
const receipts = createDbReceipts(client, { receipts: 'history', leases: 'claims' });
|
|
40
|
+
const command = createCommand(compiledContract.operations['item.adjust'], {
|
|
41
|
+
repository: receipts, identity, authorize, handler: adjust,
|
|
42
|
+
});
|
|
43
|
+
// HTTP/local use { 'item.adjust': command.handler }; jobs call command.execute.
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`test/consumer/durable.js` exercises the installed composition with synthetic data.
|
|
47
|
+
The receipt, external-effect and run tests include cross-process races, interrupted
|
|
48
|
+
multi-leg execution, second-run no-op checks and current authorization. The measured
|
|
49
|
+
cost probe is `npm run benchmark:durable`; its results below report overhead as
|
|
50
|
+
well as resource bounds. Real provider guarantees, downstream cutover, native
|
|
51
|
+
executables, PostgreSQL behavior and operator reconciliation acceptance require
|
|
52
|
+
separate host/application qualification. Synthetic Node/Bun results cannot prove
|
|
53
|
+
those claims. The retained reference fixtures remain unchanged.
|
|
54
|
+
|
|
55
|
+
<!--fact:durable.measurements-->
|
|
56
|
+
|
|
57
|
+
Measured on v24.19.0, linux/x64, AMD Ryzen 9 5900HX with Radeon Graphics.
|
|
58
|
+
|
|
59
|
+
| Commands | Domain/outbox ms | Durable command ms | Receipt replay ms | Added cost ratio | Sends / unresolved resends | Second writes / revisions | Events | Heap / RSS MiB | Teardown ms / resources |
|
|
60
|
+
|---:|---:|---:|---:|---:|---|---|---:|---|---|
|
|
61
|
+
| 32 | 7.09 | 15.88 | 5.42 | 2.24x | 2 / 0 | 0 / 0 | 8 | 23.69 / 112.07 | 0.28 / 0 |
|
|
62
|
+
| 128 | 14.73 | 33.22 | 13.35 | 2.26x | 2 / 0 | 0 / 0 | 8 | 41.05 / 123.25 | 0.13 / 0 |
|
|
63
|
+
|
|
64
|
+
Synthetic local SQLite commands and interrupted multi-leg effects. Baseline executes the same domain/outbox work without a receipt; durable execution adds validation, authorization and immutable replay. Limits are fixed acceptance ceilings, not performance claims. Real providers, downstream acceptance, native executables, PostgreSQL and operator reconciliation are pending.
|
|
65
|
+
|
|
66
|
+
<!--/fact-->
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# Provider descriptors, authority and complete ingestion
|
|
2
|
+
|
|
3
|
+
`@jarenjs/contract/provider` exports `createProviderExecutor`, `compileProvider`
|
|
4
|
+
and `withProviderRun`. The executor composes core scheduling with one injected
|
|
5
|
+
HTTP attempt. The compiler describes JSON REST and explicitly declared GraphQL
|
|
6
|
+
dialects. Credentials, destination rules and provider schemas remain host inputs.
|
|
7
|
+
|
|
8
|
+
## Executor
|
|
9
|
+
|
|
10
|
+
```js
|
|
11
|
+
import { createProviderExecutor, compileProvider } from '@jarenjs/contract/provider';
|
|
12
|
+
|
|
13
|
+
const executor = createProviderExecutor({ attempts: 3, overallMs: 30000,
|
|
14
|
+
attemptMs: 10000, maxBytes: 262144, concurrency: 4, maxQueue: 64 });
|
|
15
|
+
const provider = compileProvider({
|
|
16
|
+
$provider: '0.1', id: 'inventory', apiVersion: '2026-01',
|
|
17
|
+
endpoint: 'https://inventory.example/items', protocol: 'rest',
|
|
18
|
+
method: 'GET', safety: 'safe-read',
|
|
19
|
+
query: { environment: '$.input.environment' },
|
|
20
|
+
response: { rows: '$.items', id: '$.id', cursor: '$.next', version: '$.version' },
|
|
21
|
+
pagination: { cursorParam: 'cursor', empty: 'complete' },
|
|
22
|
+
limits: { pages: 3, rows: 256, bytes: 262144 },
|
|
23
|
+
});
|
|
24
|
+
try {
|
|
25
|
+
const result = await provider.pull({ environment: 'test' }, { executor });
|
|
26
|
+
// Inspect result.state and the preserved result.observations before using it.
|
|
27
|
+
} finally { await executor.close(); }
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`execute(request, context)` requires an absolute HTTP(S) URL without embedded
|
|
31
|
+
credentials and explicit safety: `safe-read`, `provider-idempotent`, or
|
|
32
|
+
`single-send`. Provider-idempotent also requires `idempotencyKey`; the injected
|
|
33
|
+
transport must bind that key using the provider's documented mechanism. The
|
|
34
|
+
default transport refuses provider-idempotent requests with
|
|
35
|
+
`idempotency-transport-required` because it has no dialect-specific key binding. An
|
|
36
|
+
idempotency key does not qualify business reconciliation or durable receipts.
|
|
37
|
+
Single-send transport/body uncertainty returns `unresolved` and never resends
|
|
38
|
+
inside the executor. Supply the same core attempt budget in `context.budget`
|
|
39
|
+
when an outer job or workflow can invoke the same request again.
|
|
40
|
+
|
|
41
|
+
The optional injected `transport(request, {signal,attempt,maxAttempts})` returns
|
|
42
|
+
a Fetch `Response`. It must make exactly one request (`maxAttempts:1`), disable
|
|
43
|
+
SDK retries, and honor abort. The default uses `fetch` with redirects disabled.
|
|
44
|
+
Custom redirect handling and idempotency binding remain transport obligations.
|
|
45
|
+
JSON results preserve status, exact response text, consumed byte count, attempts
|
|
46
|
+
and Retry-After, but never raw exceptions, credentials or response handles.
|
|
47
|
+
`state` is `ok`, `failed`, `refused`, `unresolved` or `cancelled`.
|
|
48
|
+
|
|
49
|
+
Defaults are three total attempts, an overall deadline of 30 seconds, ten seconds
|
|
50
|
+
per attempt, 262144 response bytes across attempts and 262144 request-body bytes.
|
|
51
|
+
`baseMs` defaults to 500 and `maxMs` to 8000. `now`, `random` and `sleep` are
|
|
52
|
+
injectable. Core concurrency, queue, scope and spacing options are forwarded.
|
|
53
|
+
The `account` request field combines with URL origin for fair rate scopes.
|
|
54
|
+
`context.deadline` and `context.maxBytes` can tighten the executor's limits.
|
|
55
|
+
Stream readers stop on byte exhaustion; late replies cannot become successes
|
|
56
|
+
after abort/deadline. Close drains transport, body readers and retry waits.
|
|
57
|
+
|
|
58
|
+
Retryable transport failures and HTTP 408/429/5xx consume the same total budget.
|
|
59
|
+
`retryAfter` selects `http` (seconds/date), `milliseconds`, or `none`;
|
|
60
|
+
`retryAfterHeader` names the header. A server delay which cannot fit the remaining
|
|
61
|
+
deadline refuses with `deadline`. It is never shortened to the backoff cap.
|
|
62
|
+
AI's declared clamp and the contract client's jitter remain their explicit
|
|
63
|
+
[compatibility policies](../../core/docs/SCHEDULING.md).
|
|
64
|
+
|
|
65
|
+
## Versioned descriptor
|
|
66
|
+
|
|
67
|
+
`$provider:'0.1'`, `id`, `apiVersion`, `endpoint`, `protocol`, `method`, `safety`
|
|
68
|
+
and `response` are required. API version is public data available to request
|
|
69
|
+
selectors; the descriptor explicitly binds it in the endpoint, headers or query.
|
|
70
|
+
Unknown fields are refused at compile time (`JC0021`). Malformed executor/run
|
|
71
|
+
host options use `JC1012`.
|
|
72
|
+
|
|
73
|
+
| Declaration | Contract |
|
|
74
|
+
|---|---|
|
|
75
|
+
| `headers` | Static public string headers. Authentication/cookie/key headers are refused; host transport owns credentials. |
|
|
76
|
+
| `query`, `body` | Existing Query documents over `{input,cursor,apiVersion,partition,sourceVersion}`; query returns scalar parameters, body becomes canonical JSON. GET/HEAD bodies are refused. |
|
|
77
|
+
| `graphql` | POST with `{query,variables?}`; variables is a Query document. `pagination.cursorVariable` receives the continuation. |
|
|
78
|
+
| `response.rows`, `id` | Query selectors for an array and each source identity. String identities remain strings; numeric IDs must be safe integers. |
|
|
79
|
+
| `response.cursor`, `hasMore` | Scalar continuation and optional explicit boolean. GraphQL terminal cursors may remain present when hasMore is false. |
|
|
80
|
+
| `response.errors`, `cost`, `version` | Extracted protocol evidence. GraphQL errors default to `$.errors`; any partial errors make the pull incomplete. |
|
|
81
|
+
| `response.transform` | `{kind:'query',expression}`, `{kind:'jslt',expression}`, or `{callback:name}`. Source raw/text/IDs remain beside transformed rows. |
|
|
82
|
+
| `inputSchema`, `response.schema` | Optional schemas compiled by injected `compileSchema`; absence of that capability refuses the descriptor. |
|
|
83
|
+
| `pagination` | `cursorParam`, `cursorVariable`, and explicit `empty:'complete'` or `'incomplete'`. Empty pages with continuation are always incomplete. |
|
|
84
|
+
| `limits` | Positive finite `pages`, `rows`, `bytes`; defaults match the example. Budgets apply across one iterator, including failed request bytes. |
|
|
85
|
+
| `capability` | `json` is supported. `upload`, `media`, `bulk`, `binary` refuse before dispatch. |
|
|
86
|
+
|
|
87
|
+
Named selector callbacks are supplied in `compileProvider(doc,{callbacks})`.
|
|
88
|
+
They receive cloned protocol data only. They are trusted host functions, not a
|
|
89
|
+
JavaScript sandbox; the framework never supplies run resources to them.
|
|
90
|
+
Schema, Query and JSLT each retain their existing compiler and semantics.
|
|
91
|
+
|
|
92
|
+
`pages(input,context)` is an async iterator. Each `page` contains exact `text`,
|
|
93
|
+
parsed `raw`, transformed `rows`, source `ids`, input `cursor`, `continuation`,
|
|
94
|
+
`complete`, `version`, `errors`, `cost`, `reason`, `bytes` and `attempts`.
|
|
95
|
+
The consumer's next pull admits the next request. A terminal `complete`,
|
|
96
|
+
`incomplete` or `refused` record reports totals; `pull` collects that bounded
|
|
97
|
+
sequence into `observations`. Repeated cursors, duplicate IDs, empty intermediate
|
|
98
|
+
pages, partial errors, broken transforms and exhausted credits remain incomplete.
|
|
99
|
+
Accepted partial pages remain inspectable. Bytes refused at a stream limit are
|
|
100
|
+
discarded, and malformed response text is returned without claiming parsed rows.
|
|
101
|
+
Raw text retains wire distinctions that JSON numbers alone cannot represent.
|
|
102
|
+
|
|
103
|
+
## Private run authority
|
|
104
|
+
|
|
105
|
+
`withProviderRun(evidence,host,work,options)` composes the existing
|
|
106
|
+
identify/acquire/release hooks. Evidence is a closed JSON record of nonempty
|
|
107
|
+
opaque strings: `runId`, `actor`, `environment`, `destination`, `revision`, `lease`.
|
|
108
|
+
The host implements `identify(meta)`, `acquire(input,identity,enter)`,
|
|
109
|
+
`current(evidence,privateHost,operation)` and `transport(request,context)`.
|
|
110
|
+
`current` re-reads live authority and returns matching evidence or null.
|
|
111
|
+
The framework compares every field before dispatch and publication, including
|
|
112
|
+
after asynchronous refresh. All allowlists and membership rules stay in current.
|
|
113
|
+
|
|
114
|
+
The callback receives a run with enumerable `evidence` only. Its private
|
|
115
|
+
`execute`, `check`, `publish` and `signal` members cannot enter ordinary JSON
|
|
116
|
+
state. Transport alone receives `context.host`; privileged clients are unique
|
|
117
|
+
to concurrent runs. Host acquisition must serialize account switching where
|
|
118
|
+
required. Shutdown stops admission and drains workers and callbacks before
|
|
119
|
+
acquired and identity resources release, in that order. Released run callbacks
|
|
120
|
+
cannot dispatch or publish again. The library suppresses raw host errors;
|
|
121
|
+
applications must also keep secrets out of explicitly returned business data.
|
|
122
|
+
|
|
123
|
+
Flow DAG/workflow `run(...,{resources})` passes resources as the task handler's
|
|
124
|
+
third argument, separate from JSON input, checkpoint identity and trace data.
|
|
125
|
+
Tasks with resources drain on abort before the workflow rejects. Compile the
|
|
126
|
+
workflow once, then provide the particular run on each invocation. Authority
|
|
127
|
+
is reacquired on resume; persisted references never restore live credentials.
|
|
128
|
+
|
|
129
|
+
## Complete snapshots and qualification
|
|
130
|
+
|
|
131
|
+
[`createIngestion`](../../flow/docs/WORKFLOW-FORMAT.md#complete-provider-ingestion)
|
|
132
|
+
composes bounded pages with the existing workflow engine and an injected store.
|
|
133
|
+
[`createDbIngestionStore`](../../linq/docs/DB-CLIENT.md#complete-ingestion-store)
|
|
134
|
+
co-commits pages/checkpoints and publishes only complete requested partitions.
|
|
135
|
+
Source snapshot or monotonic revision evidence is mandatory, including a version
|
|
136
|
+
on every page. Identical published input is a no-op; application reconciliation
|
|
137
|
+
owns manual provenance. No network operation holds a page transaction.
|
|
138
|
+
|
|
139
|
+
Offline REST, partial GraphQL and archive-link transcripts and two independent
|
|
140
|
+
bounded consumers are executable through `npm run test:packed` on Node and Bun.
|
|
141
|
+
Node's abrupt-process recovery, rollback and zero-write tests run in the unit
|
|
142
|
+
suite. Real provider authority/read-back, native external SDK behavior and
|
|
143
|
+
downstream cutover remain separate qualifications. External writes await durable
|
|
144
|
+
receipt/reconciliation capability; streaming DAG inputs are not required here.
|
|
145
|
+
|
|
146
|
+
<!--fact:providers.measurements-->
|
|
147
|
+
|
|
148
|
+
Measured on v24.19.0, linux/x64, AMD Ryzen 9 5900HX with Radeon Graphics.
|
|
149
|
+
|
|
150
|
+
| Consumer | Rows | Requests / budget | Response bytes / budget | Retained reader ms | Native ingestion ms | Second writes / revisions | Teardown ms / remaining resources | Sampled heap / RSS MiB |
|
|
151
|
+
|---|---:|---|---|---:|---:|---|---|---|
|
|
152
|
+
| catalog | 256 | 3 / 3 | 66426 / 262144 | 0.96 | 64.45 | 0 / 0 | 0.27 / 0 | 35.69 / 116.45 |
|
|
153
|
+
| archive-stock | 512 | 6 / 6 | 135886 / 524288 | 1.31 | 79.46 | 0 / 0 | 0.11 / 0 | 42.55 / 173.56 |
|
|
154
|
+
|
|
155
|
+
Offline synthetic Node SQLite ingestion. The retained reader only extracts recorded transcripts; native timings include opening storage, descriptor execution, private authority checks, page/checkpoint commits, publication and zero-write replay. Their timings describe different work. No real provider latency, credentials, external write reconciliation or production cutover is qualified.
|
|
156
|
+
|
|
157
|
+
<!--/fact-->
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jarenjs/contract",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.83.3",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"types": "./dist/types/index.d.ts",
|
|
@@ -59,7 +59,15 @@
|
|
|
59
59
|
"default": "./src/project/index.js"
|
|
60
60
|
},
|
|
61
61
|
"./schemas/*": "./schemas/*",
|
|
62
|
-
"./package.json": "./package.json"
|
|
62
|
+
"./package.json": "./package.json",
|
|
63
|
+
"./provider": {
|
|
64
|
+
"types": "./dist/types/provider/index.d.ts",
|
|
65
|
+
"default": "./src/provider/index.js"
|
|
66
|
+
},
|
|
67
|
+
"./command": {
|
|
68
|
+
"types": "./dist/types/command.d.ts",
|
|
69
|
+
"default": "./src/command.js"
|
|
70
|
+
}
|
|
63
71
|
},
|
|
64
72
|
"files": [
|
|
65
73
|
"dist/types/",
|
|
@@ -102,9 +110,9 @@
|
|
|
102
110
|
"prepack": "npm run build:types"
|
|
103
111
|
},
|
|
104
112
|
"dependencies": {
|
|
105
|
-
"@jarenjs/core": "^0.
|
|
106
|
-
"@jarenjs/json": "^0.
|
|
107
|
-
"@jarenjs/validate": "^0.
|
|
108
|
-
"@jarenjs/emit": "^0.
|
|
113
|
+
"@jarenjs/core": "^0.83.3",
|
|
114
|
+
"@jarenjs/json": "^0.83.3",
|
|
115
|
+
"@jarenjs/validate": "^0.83.3",
|
|
116
|
+
"@jarenjs/emit": "^0.83.3"
|
|
109
117
|
}
|
|
110
118
|
}
|