@jarenjs/flow 0.34.0
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 +234 -0
- package/dist/types/app.d.ts +65 -0
- package/dist/types/dag.d.ts +89 -0
- package/dist/types/errors.d.ts +155 -0
- package/dist/types/fsm.d.ts +150 -0
- package/dist/types/index.d.ts +11 -0
- package/dist/types/persist.d.ts +61 -0
- package/docs/APP-INTEGRATION.md +241 -0
- package/docs/FLOW-FORMAT.md +441 -0
- package/package.json +55 -0
- package/schemas/jaren-dag.draft-07.schema.json +200 -0
- package/schemas/jaren-dag.schema.json +200 -0
- package/schemas/jaren-fsm.draft-07.schema.json +127 -0
- package/schemas/jaren-fsm.schema.json +94 -0
- package/src/app.js +206 -0
- package/src/dag.js +560 -0
- package/src/errors.js +169 -0
- package/src/fsm.js +396 -0
- package/src/index.js +13 -0
- package/src/persist.js +65 -0
package/README.md
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# @jarenjs/flow
|
|
2
|
+
|
|
3
|
+
Executable workflow documents, in two formats. The **jaren-fsm format**
|
|
4
|
+
is a finite state machine as one JSON value — declared states, an
|
|
5
|
+
initial state, and a document-ordered transition table whose guards and
|
|
6
|
+
effect props are [Jaren JSON Query](../json/docs/QUERY-FORMAT.md)
|
|
7
|
+
documents — compiled, once, into a **pure step function**. The
|
|
8
|
+
**jaren-dag format** is an acyclic dataflow whose nodes are the suite's
|
|
9
|
+
own engines — query documents, JSLT stylesheets, registered async
|
|
10
|
+
tasks — wired by edges that carry data and compiled into a
|
|
11
|
+
run-to-completion executor.
|
|
12
|
+
|
|
13
|
+
It is the executable half of a round trip the suite already ships: a
|
|
14
|
+
`stateDiagram-v2` parsed by [`@jarenjs/mermaid`](../../components/mermaid)
|
|
15
|
+
projects (via a JSLT stylesheet) into exactly this shape, and this
|
|
16
|
+
engine runs it. The format is a strict superset of that projection
|
|
17
|
+
contract — every projected document compiles unchanged.
|
|
18
|
+
|
|
19
|
+
The grammar is published as JSON Schema in
|
|
20
|
+
[`schemas/jaren-fsm.schema.json`](schemas/jaren-fsm.schema.json) (with a
|
|
21
|
+
mechanically derived draft-07 twin for providers pinned to older
|
|
22
|
+
drafts) — hand it to a constrained decoder and a language model cannot
|
|
23
|
+
emit a machine with an unknown member or a malformed guard. The
|
|
24
|
+
normative contract is [docs/FLOW-FORMAT.md](docs/FLOW-FORMAT.md). Zero
|
|
25
|
+
dependencies outside the suite; no `eval`, CSP-safe; the only runtime
|
|
26
|
+
import is `@jarenjs/json`.
|
|
27
|
+
|
|
28
|
+
## The format in one glance
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"$fsm": "0.1",
|
|
33
|
+
"initial": "idle",
|
|
34
|
+
"states": [
|
|
35
|
+
"idle",
|
|
36
|
+
{ "id": "loading", "entry": [{ "run": "fetch", "with": { "url": "$.context.url" } }] },
|
|
37
|
+
{ "id": "done", "final": true }
|
|
38
|
+
],
|
|
39
|
+
"transitions": [
|
|
40
|
+
{ "from": "idle", "event": "start", "to": "loading" },
|
|
41
|
+
{ "from": "loading", "event": "ok", "guard": "$.payload.fresh", "to": "done" },
|
|
42
|
+
{ "from": "loading", "event": "fail", "to": "idle" }
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Guards are asserted by effective boolean value against one scope —
|
|
48
|
+
`{ state, event, payload, context }` — and **effects are data**: a fired
|
|
49
|
+
transition returns resolved `{ run, with? }` descriptors (exit →
|
|
50
|
+
transition → entry); the engine never executes them. Which registry runs
|
|
51
|
+
them is the host's business, the same boundary discipline as
|
|
52
|
+
`@jarenjs/app` effects.
|
|
53
|
+
|
|
54
|
+
## Usage
|
|
55
|
+
|
|
56
|
+
```javascript
|
|
57
|
+
import { compileFsm, createFsmSession } from '@jarenjs/flow';
|
|
58
|
+
|
|
59
|
+
const fsm = compileFsm(doc); // throws FlowCompileError (JF0xxx) on a bad document
|
|
60
|
+
fsm.initial; // 'idle'
|
|
61
|
+
fsm.states; // ['idle', 'loading', 'done'] (frozen)
|
|
62
|
+
fsm.events('loading'); // ['ok', 'fail'] (frozen)
|
|
63
|
+
|
|
64
|
+
// The pure core: state in, result out. Nothing is held, nothing runs.
|
|
65
|
+
const r = fsm.step('loading', 'ok', { payload: { fresh: true }, context: { url: '/api' } });
|
|
66
|
+
r; // { changed: true, state: 'done', effects: [], final: true, errors: [] }
|
|
67
|
+
|
|
68
|
+
// The thin mutable convenience over it:
|
|
69
|
+
const s = createFsmSession(fsm); // starts at doc initial
|
|
70
|
+
s.can('start'); // true — a dry run (step is pure, so can() IS step())
|
|
71
|
+
s.send('start'); // advances; returns the same result shape
|
|
72
|
+
s.state; // 'loading'
|
|
73
|
+
s.done; // final-state flag
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`step` is **total over machine input**: an event no transition answers
|
|
77
|
+
is ignored (`changed: false`, no effects) — the conventional FSM
|
|
78
|
+
reading. Only caller mistakes throw: an undeclared state id or a
|
|
79
|
+
non-string event (`FlowRuntimeError`, JF2001/JF2002).
|
|
80
|
+
|
|
81
|
+
## Fail closed, report precisely
|
|
82
|
+
|
|
83
|
+
A guard that throws at evaluation reads **false**; an effect whose
|
|
84
|
+
`with` throws is **omitted**. Neither aborts the step — each appends a
|
|
85
|
+
plain-data record `{ code, docPath, message }` to the result's `errors`
|
|
86
|
+
array, where `docPath` is a JSON Pointer to exactly the query that
|
|
87
|
+
failed (`/transitions/2/guard`). That is the suite's standard
|
|
88
|
+
machine-repairable error shape: a host surfaces the records through its
|
|
89
|
+
own error channel, and a repair loop knows precisely what to fix.
|
|
90
|
+
|
|
91
|
+
Compile-time failures throw `FlowCompileError` with the same `code` +
|
|
92
|
+
`docPath` discipline (JF0001–JF0009, table in
|
|
93
|
+
[FLOW-FORMAT.md §5](docs/FLOW-FORMAT.md)); a guard that cannot compile
|
|
94
|
+
is a compile error, never a runtime surprise.
|
|
95
|
+
|
|
96
|
+
## Hosting a machine in @jarenjs/app
|
|
97
|
+
|
|
98
|
+
`fsmToApp` turns the same document into **generated standard app
|
|
99
|
+
documents** — a state slice plus one plain-JSON action per named event,
|
|
100
|
+
compiled by the app like any hand-written action, with no flow code at
|
|
101
|
+
runtime:
|
|
102
|
+
|
|
103
|
+
```javascript
|
|
104
|
+
import { fsmToApp, fsmStateSchema } from '@jarenjs/flow';
|
|
105
|
+
import { createApp } from '@jarenjs/app';
|
|
106
|
+
|
|
107
|
+
const { slice, actions, events } = fsmToApp(doc); // { pointer: '/fsm', namespace: 'fsm/' }
|
|
108
|
+
const app = createApp({
|
|
109
|
+
state: { fsm: slice },
|
|
110
|
+
view,
|
|
111
|
+
actions: { ...actions, ...hostActions },
|
|
112
|
+
}, { node, effects, validateState }); // fsmStateSchema(doc) guards the slice
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Control state lives at `<pointer>/current`; the host's app state **is**
|
|
116
|
+
the machine's `context`; guards and effect props evaluate
|
|
117
|
+
pre-transition in both worlds, and target states are baked as literals.
|
|
118
|
+
Selection semantics are provably the headless engine's — the test
|
|
119
|
+
suite drives both through the same scripts and asserts state and
|
|
120
|
+
effects agree. The convention, the scope mapping table, multi-machine
|
|
121
|
+
layout and the honestly-stated divergences (a throwing guard fails the
|
|
122
|
+
hosted transaction instead of reading false) live in
|
|
123
|
+
[docs/APP-INTEGRATION.md](docs/APP-INTEGRATION.md).
|
|
124
|
+
|
|
125
|
+
## The dataflow half — jaren-dag
|
|
126
|
+
|
|
127
|
+
"Connect different parts together" as one schema-validated JSON value:
|
|
128
|
+
nodes from a closed vocabulary (`input`, `output`, `const`, `query`,
|
|
129
|
+
`jslt`, `task`) wired by edges, run to completion for one input. The
|
|
130
|
+
suite's engines compose as **data** — a query filters, a stylesheet
|
|
131
|
+
projects, and the result can be a vnode tree without this package ever
|
|
132
|
+
importing a rendering line:
|
|
133
|
+
|
|
134
|
+
```javascript
|
|
135
|
+
import { compileDag } from '@jarenjs/flow';
|
|
136
|
+
|
|
137
|
+
const dag = compileDag({
|
|
138
|
+
$dag: '0.1',
|
|
139
|
+
nodes: {
|
|
140
|
+
rows: { kind: 'input' },
|
|
141
|
+
adults: { kind: 'query',
|
|
142
|
+
query: { $for: { r: '$[*]' }, $where: { $ge: ['$r.age', 18] }, $return: '$r' } },
|
|
143
|
+
view: { kind: 'jslt',
|
|
144
|
+
stylesheet: [{ match: '$', body: ['ul', {},
|
|
145
|
+
[{ $for: { p: '$[*]' }, $return: ['li', {}, '$p.name'] }]] }] },
|
|
146
|
+
out: { kind: 'output' },
|
|
147
|
+
},
|
|
148
|
+
edges: [
|
|
149
|
+
{ from: 'rows', to: 'adults' },
|
|
150
|
+
{ from: 'adults', to: 'view' },
|
|
151
|
+
{ from: 'view', to: 'out' },
|
|
152
|
+
],
|
|
153
|
+
}, { tasks: {} });
|
|
154
|
+
|
|
155
|
+
await dag.run(people); // ['ul', {}, [['li', {}, 'ada'], …]] — a vnode, as JSON
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Cycles, port rules and the exactly-one-output rule are **compile-time**
|
|
159
|
+
rejections (JF0xxx with `docPath`); a `task` node's handler is resolved
|
|
160
|
+
at compile too, and called as `handler({ with, input }, signal)` with
|
|
161
|
+
the run's shared `AbortSignal`. Independent branches run concurrently,
|
|
162
|
+
but determinism is *same input → same output values* — results are
|
|
163
|
+
keyed and port objects assemble in edge document order, so completion
|
|
164
|
+
timing can never change a value. Failure is fail-closed: the first
|
|
165
|
+
failing node aborts the shared signal and rejects the whole run
|
|
166
|
+
(`JF2006` with `nodeId`, `docPath`, `cause`; a caller abort is
|
|
167
|
+
`JF2007`) — no retries, no partial results. Per-node observability is
|
|
168
|
+
the `onNode` record stream (`{ id, status, ms }`), and a compiled dag
|
|
169
|
+
hosts in an app as **one effect** through `createTaskEffect` — the
|
|
170
|
+
recipe is in [docs/APP-INTEGRATION.md](docs/APP-INTEGRATION.md), the
|
|
171
|
+
contract in [docs/FLOW-FORMAT.md](docs/FLOW-FORMAT.md) §6–§7.
|
|
172
|
+
|
|
173
|
+
## One scope, one honest quirk
|
|
174
|
+
|
|
175
|
+
`$.state`, `$.event`, `$.payload`, `$.context` — control state lives in
|
|
176
|
+
the machine, data state lives in the host, passed per call. A guard
|
|
177
|
+
written as a plain string that does not start with `$` is a *literal*
|
|
178
|
+
(query semantics), and a non-empty literal is EBV-**true**: a display
|
|
179
|
+
guard carried over from a diagram label (`count > 3`) is vacuously true
|
|
180
|
+
by design, because a picture's annotation must not change execution.
|
|
181
|
+
Real conditions are `$`-paths (`"$.payload.fresh"`) or operator
|
|
182
|
+
documents (`{ "$gt": ["$.context.count", 3] }`).
|
|
183
|
+
|
|
184
|
+
## Performance (measured)
|
|
185
|
+
|
|
186
|
+
`npm run benchmark:flow` (run it yourself — the FSM head-to-head needs
|
|
187
|
+
the `xstate` benchmark devDependency, and the memory row needs
|
|
188
|
+
`node --expose-gc`, which the script passes). The same logical machine is
|
|
189
|
+
built with `@jarenjs/flow` and XState v5 and asserted to agree before
|
|
190
|
+
timing:
|
|
191
|
+
|
|
192
|
+
- **Transitions** — the pure `step` runs several times faster than an
|
|
193
|
+
XState actor's `send` (≈<!--bm:flow.fsmBand-->5.6–8.1<!--/bm-->× across 5/50/500-state machines); the
|
|
194
|
+
`createFsmSession` wrapper is on the page too.
|
|
195
|
+
- **Compile** — `compileFsm` beats `createMachine` + `createActor`
|
|
196
|
+
≈1.5–2.6×. Not like-for-like: XState builds a scheduling actor, so the
|
|
197
|
+
row is each engine's description→drivable cost.
|
|
198
|
+
- **The wedge** — a conformance fact, not a timing: a jaren-fsm document
|
|
199
|
+
is JSON *including its guards*, so it survives `JSON.stringify` →
|
|
200
|
+
`JSON.parse` and still compiles and still fires its guard. XState's
|
|
201
|
+
guards are functions JSON drops, so the round-tripped machine throws
|
|
202
|
+
"Guard not implemented" at the guarded transition. Serialize, store,
|
|
203
|
+
diff, ship, replay — that is the whole reason to speak JSON all the way
|
|
204
|
+
down.
|
|
205
|
+
- **Where we lose** — a compiled Jaren machine holds **more** memory than
|
|
206
|
+
the XState actor (every guard compiles to its own query closure); the
|
|
207
|
+
benchmark publishes the KiB-per-machine loss beside the wins.
|
|
208
|
+
- **The dag tax** — no npm library executes schema-validated JSON
|
|
209
|
+
dataflow, so the honest rival is the same pipeline hand-written in
|
|
210
|
+
JavaScript. A `compileDag` run costs ~8–20× the hand-written baseline —
|
|
211
|
+
the published price of dataflow as one serializable,
|
|
212
|
+
constrained-decodable JSON value, sitting beside what it buys.
|
|
213
|
+
|
|
214
|
+
The full tables, the wedge as a conformance row, and the fairness notes
|
|
215
|
+
are on the [benchmarks page](https://jklarenbeek.github.io/jarenjs/#/benchmarks?suite=flow).
|
|
216
|
+
|
|
217
|
+
## Authoring with a model
|
|
218
|
+
|
|
219
|
+
A jaren-fsm or jaren-dag document is JSON published as a schema, so a
|
|
220
|
+
constrained decoder can author one — and because every compile error
|
|
221
|
+
carries a `code` and a `docPath`, a schema-valid but semantically broken
|
|
222
|
+
machine (a transition to an undeclared state, say) repairs in a bounded
|
|
223
|
+
loop rather than flailing. `@jarenjs/ai`'s
|
|
224
|
+
[*Authoring engine documents*](../ai/README.md#authoring-engine-documents-validate-and-compile)
|
|
225
|
+
section shows the `composeChecks(schema, compileGate)` recipe, and
|
|
226
|
+
[*A model as a dataflow node*](../ai/README.md#a-model-as-a-dataflow-node)
|
|
227
|
+
runs a model as an ordinary dag `task`. Neither package imports the
|
|
228
|
+
other — the composition is data.
|
|
229
|
+
|
|
230
|
+
## Development
|
|
231
|
+
|
|
232
|
+
Unit tests live in `test/flow/` at the repository root
|
|
233
|
+
(`npm run test:flow`). See the repository [README](../../README.md) for
|
|
234
|
+
the full suite documentation.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The @jarenjs/app adapter: turn a jaren-fsm document into the
|
|
3
|
+
* standard app documents that host it (docs/APP-INTEGRATION.md). This
|
|
4
|
+
* is a GENERATOR, not a runtime — it emits pure JSON action documents
|
|
5
|
+
* the app compiles like any hand-written action, and nothing from this
|
|
6
|
+
* package runs afterwards. Selection semantics are the headless
|
|
7
|
+
* engine's, reproduced structurally: one conditional chain per named
|
|
8
|
+
* event, transitions in document order, `$and`'s first-false stop
|
|
9
|
+
* playing the role of the step function's guard gate.
|
|
10
|
+
*
|
|
11
|
+
* The scope mapping (APP-INTEGRATION.md §scope): a guard or `with`
|
|
12
|
+
* query authored against FLOW-FORMAT §3's `{ state, event, payload,
|
|
13
|
+
* context }` is rewritten to read the reserved variable `__fsm`, bound
|
|
14
|
+
* once per action to `{ state: <pointer>.current, event: <literal>,
|
|
15
|
+
* payload: $payload, context: $ }` — all pre-transition, exactly like
|
|
16
|
+
* the headless step scope.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Generate the standard app documents that host a jaren-fsm machine:
|
|
20
|
+
* a state slice, one action document per distinct named event, and the
|
|
21
|
+
* event vocabulary. The output is pure JSON with the machine's target
|
|
22
|
+
* states, event names and effect origins baked as literals; guards and
|
|
23
|
+
* effect props run through the app's own query engine at dispatch
|
|
24
|
+
* time. See docs/APP-INTEGRATION.md for the convention this implements.
|
|
25
|
+
*
|
|
26
|
+
* @param {any} fsmDoc - a jaren-fsm document (FLOW-FORMAT §2)
|
|
27
|
+
* @param {{ pointer?: string, namespace?: string }} [options] -
|
|
28
|
+
* `pointer` (default `/fsm`) is where the slice lives in app state,
|
|
29
|
+
* as a chain of identifier-safe segments; `namespace` (default
|
|
30
|
+
* `fsm/`) prefixes the generated action names.
|
|
31
|
+
* @returns {{ slice: { current: string|null }, actions: Record<string, any>, events: string[] }}
|
|
32
|
+
* @throws {import('./errors.js').FlowCompileError} on a bad machine
|
|
33
|
+
* document (the same JF0xxx codes as `compileFsm`)
|
|
34
|
+
* @throws {TypeError} on malformed options or a document binding the
|
|
35
|
+
* reserved `__fsm` variable name
|
|
36
|
+
*/
|
|
37
|
+
export declare function fsmToApp(fsmDoc: any, options?: {
|
|
38
|
+
pointer?: string;
|
|
39
|
+
namespace?: string;
|
|
40
|
+
}): {
|
|
41
|
+
slice: {
|
|
42
|
+
current: string | null;
|
|
43
|
+
};
|
|
44
|
+
actions: Record<string, any>;
|
|
45
|
+
events: string[];
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* The state slice's JSON Schema: `current` as an enum of the machine's
|
|
49
|
+
* declared state ids — compose it into a `validateState` schema at the
|
|
50
|
+
* slice pointer so no hand-written action can corrupt the control
|
|
51
|
+
* state (APP-INTEGRATION.md §fail-closed).
|
|
52
|
+
* @param {any} fsmDoc - a jaren-fsm document
|
|
53
|
+
* @returns {{ type: 'object', required: string[], properties: { current: { description: string, enum: string[] } } }}
|
|
54
|
+
* @throws {import('./errors.js').FlowCompileError} on a bad machine document
|
|
55
|
+
*/
|
|
56
|
+
export declare function fsmStateSchema(fsmDoc: any): {
|
|
57
|
+
type: 'object';
|
|
58
|
+
required: string[];
|
|
59
|
+
properties: {
|
|
60
|
+
current: {
|
|
61
|
+
description: string;
|
|
62
|
+
enum: string[];
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The jaren-dag engine: compile an acyclic dataflow document
|
|
3
|
+
* (docs/FLOW-FORMAT.md §6) once — every embedded query, stylesheet,
|
|
4
|
+
* `with` and `select` becomes a closure, the task registry is resolved,
|
|
5
|
+
* the wiring rules are proven — and run it many times. A run resolves
|
|
6
|
+
* nodes as their inputs arrive (independent branches concurrently),
|
|
7
|
+
* delivers values by reference, and fails closed: the first failing
|
|
8
|
+
* node aborts the shared signal and rejects the whole run (§7.3) — no
|
|
9
|
+
* retries, no partial results.
|
|
10
|
+
*
|
|
11
|
+
* Determinism is same input → same output VALUES, never same timing:
|
|
12
|
+
* results are keyed per node and port objects assemble in edge
|
|
13
|
+
* document order, so completion order cannot change a value (§7.2).
|
|
14
|
+
*/
|
|
15
|
+
export type DagNodeRecord = {
|
|
16
|
+
id: string;
|
|
17
|
+
status: 'ok' | 'error' | 'aborted' | 'restored';
|
|
18
|
+
ms: number;
|
|
19
|
+
};
|
|
20
|
+
export type DagCheckpointStore = {
|
|
21
|
+
load: (runId: string) => any;
|
|
22
|
+
save: (runId: string, nodeId: string, value: any) => any;
|
|
23
|
+
complete: (runId: string, result: any) => any;
|
|
24
|
+
};
|
|
25
|
+
export type CompiledDag = {
|
|
26
|
+
/**
|
|
27
|
+
* - Declared node ids, document order.
|
|
28
|
+
*/
|
|
29
|
+
nodes: readonly string[];
|
|
30
|
+
/**
|
|
31
|
+
* - The output node's id.
|
|
32
|
+
*/
|
|
33
|
+
output: string;
|
|
34
|
+
/**
|
|
35
|
+
* -
|
|
36
|
+
* Execute the graph for one input (`undefined` reads as `null`).
|
|
37
|
+
*/
|
|
38
|
+
run: (input?: any, opts?: {
|
|
39
|
+
signal?: AbortSignal;
|
|
40
|
+
onNode?: (record: DagNodeRecord) => void;
|
|
41
|
+
runId?: string;
|
|
42
|
+
}) => Promise<any>;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* A settlement record handed to `onNode` (§7.4). `restored` fires at
|
|
46
|
+
* the start of a RESUMED run for every node whose checkpointed value
|
|
47
|
+
* was seeded instead of evaluated (§7.6).
|
|
48
|
+
* @typedef {{ id: string, status: 'ok'|'error'|'aborted'|'restored', ms: number }} DagNodeRecord
|
|
49
|
+
*/
|
|
50
|
+
/**
|
|
51
|
+
* The opt-in checkpoint store (§7.6): `load` answers a prior run's
|
|
52
|
+
* recorded values (or null), `save` records one declared node's
|
|
53
|
+
* value, `complete` records the run's result. Any member may return a
|
|
54
|
+
* promise; a throwing store fails the run (JF2009), never silently.
|
|
55
|
+
* @typedef {Object} DagCheckpointStore
|
|
56
|
+
* @property {(runId: string) => any} load
|
|
57
|
+
* @property {(runId: string, nodeId: string, value: any) => any} save
|
|
58
|
+
* @property {(runId: string, result: any) => any} complete
|
|
59
|
+
*/
|
|
60
|
+
/**
|
|
61
|
+
* A compiled jaren-dag graph.
|
|
62
|
+
* @typedef {Object} CompiledDag
|
|
63
|
+
* @property {readonly string[]} nodes - Declared node ids, document order.
|
|
64
|
+
* @property {string} output - The output node's id.
|
|
65
|
+
* @property {(input?: any, opts?: { signal?: AbortSignal, onNode?: (record: DagNodeRecord) => void, runId?: string }) => Promise<any>} run -
|
|
66
|
+
* Execute the graph for one input (`undefined` reads as `null`).
|
|
67
|
+
*/
|
|
68
|
+
/**
|
|
69
|
+
* Compile a jaren-dag document (docs/FLOW-FORMAT.md §6–§7) against a
|
|
70
|
+
* task registry. Everything is decided here: structural validation,
|
|
71
|
+
* the wiring rules, acyclicity, embedded-document compilation and
|
|
72
|
+
* registry resolution — `run` only executes closures.
|
|
73
|
+
*
|
|
74
|
+
* @param {any} doc - the jaren-dag document
|
|
75
|
+
* @param {{ tasks?: Record<string, (props: { with: any, input: any }, signal: AbortSignal) => any>,
|
|
76
|
+
* checkpoint?: DagCheckpointStore }} [options]
|
|
77
|
+
* @returns {CompiledDag}
|
|
78
|
+
* @throws {FlowCompileError} when the document violates the format (JF0xxx)
|
|
79
|
+
* @throws {TypeError} when the options are malformed (a registry that is
|
|
80
|
+
* not an object, a registered handler that is not a function, or a
|
|
81
|
+
* checkpoint store missing one of load/save/complete)
|
|
82
|
+
*/
|
|
83
|
+
export declare function compileDag(doc: any, options?: {
|
|
84
|
+
tasks?: Record<string, (props: {
|
|
85
|
+
with: any;
|
|
86
|
+
input: any;
|
|
87
|
+
}, signal: AbortSignal) => any>;
|
|
88
|
+
checkpoint?: DagCheckpointStore;
|
|
89
|
+
}): CompiledDag;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Error types for @jarenjs/flow, built on `@jarenjs/core`'s coded
|
|
3
|
+
* contract: every failure carries a stable `code` (JF0xxx compile,
|
|
4
|
+
* JF2xxx runtime), a bare `reason`, a composed `message`, and — where
|
|
5
|
+
* one exists — the `docPath` of the offending member of the flow
|
|
6
|
+
* document. The feedback shape a repair loop needs. The normative
|
|
7
|
+
* table lives in docs/FLOW-FORMAT.md §5, proven in sync with
|
|
8
|
+
* `FLOW_CODES` below by a test.
|
|
9
|
+
*/
|
|
10
|
+
import { CodedError } from '@jarenjs/core/errors';
|
|
11
|
+
/**
|
|
12
|
+
* The runtime code table (the `CSV_CODES` shape): one entry per code
|
|
13
|
+
* this package can raise, proven in sync with FLOW-FORMAT.md §5's
|
|
14
|
+
* normative table by a test — the "must stay in sync by hand" note this
|
|
15
|
+
* file used to carry is now a checked fact.
|
|
16
|
+
*/
|
|
17
|
+
export declare const FLOW_CODES: Readonly<{
|
|
18
|
+
JF0001: "the document is not an object, or $fsm is not 0.1";
|
|
19
|
+
JF0002: "states is not an array, or a state entry is malformed";
|
|
20
|
+
JF0003: "two state entries share one id";
|
|
21
|
+
JF0004: "initial is neither null nor a declared state id";
|
|
22
|
+
JF0005: "transitions is not an array, or an entry is malformed";
|
|
23
|
+
JF0006: "a transition from/to names no declared state";
|
|
24
|
+
JF0007: "a guard failed to compile as a query document";
|
|
25
|
+
JF0008: "an effects list or effect descriptor is malformed";
|
|
26
|
+
JF0009: "an effect with failed to compile as a query document";
|
|
27
|
+
JF0010: "the dag document is not an object, or $dag is not 0.1";
|
|
28
|
+
JF0011: "nodes is not an object, or a node declaration is malformed";
|
|
29
|
+
JF0012: "edges is not an array, or an edge entry is malformed";
|
|
30
|
+
JF0013: "an edge from/to names no declared node";
|
|
31
|
+
JF0014: "an embedded document failed to compile";
|
|
32
|
+
JF0015: "the wiring rules are violated";
|
|
33
|
+
JF0016: "the graph has a cycle";
|
|
34
|
+
JF0017: "the document does not declare exactly one output node";
|
|
35
|
+
JF0018: "a task node names a handler the registry does not provide";
|
|
36
|
+
JF2001: "a state id the machine does not declare";
|
|
37
|
+
JF2002: "step was called with a non-string event";
|
|
38
|
+
JF2003: "a guard threw while evaluating";
|
|
39
|
+
JF2004: "an effect with threw while evaluating";
|
|
40
|
+
JF2005: "a session was created with no start state";
|
|
41
|
+
JF2006: "a dag node failed while evaluating; the run rejects";
|
|
42
|
+
JF2007: "the caller signal aborted the run";
|
|
43
|
+
JF2008: "a declared checkpoint value is not JSON-serializable";
|
|
44
|
+
JF2009: "the checkpoint store failed";
|
|
45
|
+
}>;
|
|
46
|
+
/**
|
|
47
|
+
* A defect in the flow document itself, raised while `compileFsm`
|
|
48
|
+
* compiles it. Codes:
|
|
49
|
+
*
|
|
50
|
+
* - `JF0001` — the document is not an object, or `$fsm` is present and
|
|
51
|
+
* not `'0.1'`
|
|
52
|
+
* - `JF0002` — `states` is not an array, or a state entry is neither a
|
|
53
|
+
* string nor an object with a string `id` (or carries a non-boolean
|
|
54
|
+
* `final`)
|
|
55
|
+
* - `JF0003` — two state entries share one id
|
|
56
|
+
* - `JF0004` — `initial` is neither null nor the id of a declared state
|
|
57
|
+
* - `JF0005` — `transitions` is not an array, or a transition entry is
|
|
58
|
+
* malformed (not an object; `from`/`to` not strings; `event` neither
|
|
59
|
+
* a string nor null)
|
|
60
|
+
* - `JF0006` — a transition's `from` or `to` names no declared state
|
|
61
|
+
* - `JF0007` — a guard failed to compile as a query document (see
|
|
62
|
+
* `cause`)
|
|
63
|
+
* - `JF0008` — an effects list is not an array, or an effect
|
|
64
|
+
* descriptor is not an object with a non-empty string `run`
|
|
65
|
+
* - `JF0009` — an effect's `with` failed to compile as a query
|
|
66
|
+
* document (see `cause`)
|
|
67
|
+
*
|
|
68
|
+
* Dag documents (`compileDag`):
|
|
69
|
+
*
|
|
70
|
+
* - `JF0010` — the dag document is not an object, or `$dag` is not
|
|
71
|
+
* `'0.1'` (the key is required — the dag format has no legacy
|
|
72
|
+
* contract to stay compatible with)
|
|
73
|
+
* - `JF0011` — `nodes` is not an object, or a node declaration is
|
|
74
|
+
* malformed (not an object; unknown `kind`; a kind-specific member
|
|
75
|
+
* missing or mistyped: `const` needs `value`, `query` needs
|
|
76
|
+
* `query`, `jslt` needs `stylesheet`, `task` needs a non-empty
|
|
77
|
+
* string `run`)
|
|
78
|
+
* - `JF0012` — `edges` is not an array, or an edge entry is malformed
|
|
79
|
+
* (not an object; `from`/`to` not strings; `port` present but not a
|
|
80
|
+
* non-empty string)
|
|
81
|
+
* - `JF0013` — an edge's `from` or `to` names no declared node
|
|
82
|
+
* - `JF0014` — an embedded document failed to compile (a node's
|
|
83
|
+
* `query`/`stylesheet`/`with` or an edge's `select`; see `cause`)
|
|
84
|
+
* - `JF0015` — the wiring rules are violated: an edge enters an
|
|
85
|
+
* `input`/`const` node or leaves the `output` node; fan-in without
|
|
86
|
+
* complete unique ports (a ported inbound set must be all-ported
|
|
87
|
+
* and duplicate-free); or a consuming node (`query`/`jslt`/`task`/
|
|
88
|
+
* `output`) has no inbound edge
|
|
89
|
+
* - `JF0016` — the graph has a cycle (the message lists the member
|
|
90
|
+
* ids; `docPath` points at the first edge inside it)
|
|
91
|
+
* - `JF0017` — the document does not declare exactly one `output`
|
|
92
|
+
* node
|
|
93
|
+
* - `JF0018` — a `task` node names a handler the compile-time
|
|
94
|
+
* registry does not provide
|
|
95
|
+
*/
|
|
96
|
+
export declare class FlowCompileError extends CodedError {
|
|
97
|
+
/**
|
|
98
|
+
* @param {string} code
|
|
99
|
+
* @param {string} reason - The bare reason; `message` is composed per
|
|
100
|
+
* the coded contract.
|
|
101
|
+
* @param {string} [docPath] - JSON Pointer into the flow document;
|
|
102
|
+
* `''` is the document root, `undefined` means no location.
|
|
103
|
+
* @param {Error} [cause]
|
|
104
|
+
*/
|
|
105
|
+
constructor(code: string, reason: string, docPath?: string, cause?: Error);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* A failure while a compiled machine is being driven. Only caller
|
|
109
|
+
* mistakes throw; document-level evaluation failures never do — they
|
|
110
|
+
* fail closed and are recorded as plain data on the step result
|
|
111
|
+
* (FLOW-FORMAT §5.2). Codes:
|
|
112
|
+
*
|
|
113
|
+
* - `JF2001` — `step`, `events` or `final` was called with a state id
|
|
114
|
+
* the machine does not declare (thrown; a caller bug, not machine
|
|
115
|
+
* input)
|
|
116
|
+
* - `JF2002` — `step` was called with a non-string event (thrown)
|
|
117
|
+
* - `JF2003` — a guard threw while evaluating (recorded on the step
|
|
118
|
+
* result; the guard reads false and selection continues)
|
|
119
|
+
* - `JF2004` — an effect's `with` threw while evaluating (recorded on
|
|
120
|
+
* the step result; the effect is omitted)
|
|
121
|
+
* - `JF2005` — a session was created with no start state (`initial`
|
|
122
|
+
* is null and none was given) (thrown)
|
|
123
|
+
*
|
|
124
|
+
* Dag runs (`compileDag(...).run`) have no recorded-error channel —
|
|
125
|
+
* a failure rejects the run promise (fail closed, no partial results):
|
|
126
|
+
*
|
|
127
|
+
* - `JF2006` — a node failed while evaluating; the run rejects, the
|
|
128
|
+
* shared signal aborts in-flight siblings, and the error carries
|
|
129
|
+
* the failing node's id as an own `nodeId` property beside
|
|
130
|
+
* `docPath` and `cause`
|
|
131
|
+
* - `JF2007` — the caller's `signal` aborted the run (`cause` is the
|
|
132
|
+
* abort reason when one was given)
|
|
133
|
+
*/
|
|
134
|
+
export declare class FlowRuntimeError extends CodedError {
|
|
135
|
+
/**
|
|
136
|
+
* @param {string} code
|
|
137
|
+
* @param {string} reason - The bare reason; `message` is composed per
|
|
138
|
+
* the coded contract.
|
|
139
|
+
* @param {string} [docPath] - JSON Pointer into the flow document;
|
|
140
|
+
* `''` is the document root, `undefined` means no location.
|
|
141
|
+
* @param {Error} [cause]
|
|
142
|
+
*/
|
|
143
|
+
constructor(code: string, reason: string, docPath?: string, cause?: Error);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Normalize a thrown value to an `Error`. A dag run rejects with whatever
|
|
147
|
+
* a registered task threw, and a task is host code free to throw a
|
|
148
|
+
* non-Error, so the rejection contract needs one: a bare value becomes an
|
|
149
|
+
* `Error` naming the type it arrived as. In the fsm engine the only throw
|
|
150
|
+
* sources are the query engine's own error classes, so there it is
|
|
151
|
+
* belt-and-braces rather than hostile-input hardening.
|
|
152
|
+
* @param {unknown} v
|
|
153
|
+
* @returns {Error}
|
|
154
|
+
*/
|
|
155
|
+
export declare function asError(v: unknown): Error;
|