@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
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
# The Jaren flow formats
|
|
2
|
+
|
|
3
|
+
This document is the normative contract for the `@jarenjs/flow` document
|
|
4
|
+
formats. The key words MUST, MUST NOT, SHOULD and MAY are to be
|
|
5
|
+
interpreted as described in RFC 2119. The structural grammar is
|
|
6
|
+
published as JSON Schema in [`schemas/`](../schemas/) (draft 2020-12
|
|
7
|
+
plus a mechanically derived draft-07 twin per artifact); this prose is
|
|
8
|
+
authoritative for everything a structural schema cannot express, and
|
|
9
|
+
the compiler enforces it.
|
|
10
|
+
|
|
11
|
+
Section map: §1 scope, §2 the jaren-fsm document, §3 the evaluation
|
|
12
|
+
scope, §4 the selection rule, §5 errors. §6 (the jaren-dag document)
|
|
13
|
+
and §7 (dag execution) are reserved for the dataflow format.
|
|
14
|
+
|
|
15
|
+
## §1 Scope
|
|
16
|
+
|
|
17
|
+
A **jaren-fsm** document is an executable finite state machine: named
|
|
18
|
+
control states, one current state at a time, and a transition table
|
|
19
|
+
answering named events. It is the executable superset of the shape the
|
|
20
|
+
`@jarenjs/mermaid` state-diagram projection produces (`jaren-workflow`):
|
|
21
|
+
every document valid under that projection contract MUST compile here
|
|
22
|
+
unchanged. This projection-shaped document — no `$fsm` key, string
|
|
23
|
+
states, null events and guards — is already a complete machine:
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"initial": "draft",
|
|
28
|
+
"states": ["draft", "review", "published"],
|
|
29
|
+
"transitions": [
|
|
30
|
+
{ "from": "draft", "event": "submit", "guard": null, "to": "review" },
|
|
31
|
+
{ "from": "review", "event": "approve", "guard": null, "to": "published" },
|
|
32
|
+
{ "from": "review", "event": null, "guard": null, "to": "draft" }
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The engine is **headless and pure**. Compiling yields a step function
|
|
38
|
+
from `(state, event, options)` to a transition result; the machine holds
|
|
39
|
+
no mutable state, executes no side effects and touches no host API.
|
|
40
|
+
Effects are **descriptors returned as data** — the host's registry runs
|
|
41
|
+
them, the same boundary discipline `@jarenjs/app` keeps.
|
|
42
|
+
|
|
43
|
+
### §1.1 Non-goals of format 0.1
|
|
44
|
+
|
|
45
|
+
Named so nobody reads absence as oversight:
|
|
46
|
+
|
|
47
|
+
- **Hierarchy.** No compound or nested states; a mermaid composite state
|
|
48
|
+
arrives flattened.
|
|
49
|
+
- **Eventless chains.** A transition only ever fires in answer to a
|
|
50
|
+
`step`/`send` call; there are no spontaneous microsteps and no
|
|
51
|
+
always-transitions that cascade.
|
|
52
|
+
- **History, parallel regions, delayed/timed transitions.** Statechart
|
|
53
|
+
vocabulary deferred until a use case demands it.
|
|
54
|
+
- **Effect execution.** The engine resolves descriptors and returns
|
|
55
|
+
them; it MUST NOT invoke handlers.
|
|
56
|
+
- **Persistence.** A machine's current state is a string; storing it is
|
|
57
|
+
the host's business.
|
|
58
|
+
|
|
59
|
+
### §1.2 Hosting
|
|
60
|
+
|
|
61
|
+
The engine is deliberately host-agnostic. For `@jarenjs/app` the
|
|
62
|
+
package ships a generator, `fsmToApp`, that projects a machine into
|
|
63
|
+
standard app documents — a state slice and one action document per
|
|
64
|
+
named event, with this section's scope mapped onto app vocabulary
|
|
65
|
+
(the host's state is the `context`). That convention, including its
|
|
66
|
+
honestly-stated divergences, is specified in
|
|
67
|
+
[APP-INTEGRATION.md](APP-INTEGRATION.md); nothing there changes the
|
|
68
|
+
format defined here.
|
|
69
|
+
|
|
70
|
+
## §2 The jaren-fsm document
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"$fsm": "0.1",
|
|
75
|
+
"initial": "idle",
|
|
76
|
+
"states": [
|
|
77
|
+
"idle",
|
|
78
|
+
{ "id": "loading", "entry": [{ "run": "fetch", "with": { "url": "$.context.url" } }] },
|
|
79
|
+
{ "id": "done", "final": true }
|
|
80
|
+
],
|
|
81
|
+
"transitions": [
|
|
82
|
+
{ "from": "idle", "event": "start", "to": "loading" },
|
|
83
|
+
{ "from": "loading", "event": "ok", "guard": "$.payload.fresh", "to": "done" },
|
|
84
|
+
{ "from": "loading", "event": "fail", "to": "idle",
|
|
85
|
+
"effects": [{ "run": "toast", "with": { "text": "retrying" } }] }
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
- **`$fsm`** MAY be present; when present it MUST be `"0.1"`. Absent
|
|
91
|
+
implies 0.1 — the projection contract predates the key.
|
|
92
|
+
- **`initial`** MUST be the id of a declared state, or `null` when the
|
|
93
|
+
document does not choose one (a session then requires an explicit
|
|
94
|
+
start state).
|
|
95
|
+
- **`states`** MUST be an array of declarations, each a string id or an
|
|
96
|
+
object `{ id, entry?, exit?, final? }`; a string is shorthand for
|
|
97
|
+
`{ id }`. Ids MUST be unique. `final: true` marks a terminal state —
|
|
98
|
+
advisory to hosts (a session reports `done`); the engine itself
|
|
99
|
+
happily steps out of a final state if a transition says so.
|
|
100
|
+
- **`transitions`** MUST be an array of
|
|
101
|
+
`{ from, event?, guard?, to, effects? }` entries. `from` and `to`
|
|
102
|
+
MUST name declared states. `event` is a string, or null/absent — a
|
|
103
|
+
**wildcard** matching any event name. `guard` is a Jaren JSON Query
|
|
104
|
+
document (§3), or null/absent for none.
|
|
105
|
+
- **Effect descriptors** (`entry`, `exit`, transition `effects`) MUST be
|
|
106
|
+
`{ "run": name, "with"?: query }` with a non-empty string `run`.
|
|
107
|
+
- Unknown members anywhere are ignored for forward compatibility, as in
|
|
108
|
+
the app format.
|
|
109
|
+
|
|
110
|
+
## §3 The evaluation scope
|
|
111
|
+
|
|
112
|
+
Guards and effect `with` members are Jaren JSON Query documents
|
|
113
|
+
(QUERY-FORMAT.md), compiled once at `compileFsm` time. At step time each
|
|
114
|
+
evaluates with `$` bound to one scope object:
|
|
115
|
+
|
|
116
|
+
| member | value |
|
|
117
|
+
|---|---|
|
|
118
|
+
| `$.state` | the current state id (the transition's `from`) |
|
|
119
|
+
| `$.event` | the event name being stepped |
|
|
120
|
+
| `$.payload` | the caller's payload, or `null` when absent |
|
|
121
|
+
| `$.context` | caller-supplied extended data, or `null` when absent |
|
|
122
|
+
|
|
123
|
+
**Control state lives in the machine; data state lives in the host.**
|
|
124
|
+
The engine carries no context of its own — the caller passes `context`
|
|
125
|
+
per call, which is what keeps the machine a pure function and the host
|
|
126
|
+
(an `@jarenjs/app` state slice, a server session, a test) the single
|
|
127
|
+
owner of its data.
|
|
128
|
+
|
|
129
|
+
A guard is asserted by **effective boolean value** (QUERY-FORMAT's EBV
|
|
130
|
+
rules). Two consequences worth stating plainly:
|
|
131
|
+
|
|
132
|
+
- A guard that is a plain string **not** starting with `$` is a literal
|
|
133
|
+
string, and a non-empty literal is EBV-true: the guard is **vacuously
|
|
134
|
+
true**. This is deliberate — the mermaid projection carries opaque
|
|
135
|
+
display guards (`[count > 3]` from a diagram label), and a picture's
|
|
136
|
+
annotation MUST NOT change execution. Write `$`-paths or operator
|
|
137
|
+
documents for real conditions.
|
|
138
|
+
- A guard whose result has no effective boolean value (a multi-item
|
|
139
|
+
sequence) does not fail the step: it reads **false** and the failure
|
|
140
|
+
is recorded (§5.2).
|
|
141
|
+
|
|
142
|
+
An effect's `with` evaluates against the same scope; an empty query
|
|
143
|
+
result omits the `with` member from the resolved descriptor.
|
|
144
|
+
|
|
145
|
+
## §4 The selection rule
|
|
146
|
+
|
|
147
|
+
**Document order is the whole priority scheme.** For a step from state
|
|
148
|
+
S on event E, the first entry of `transitions` — in document order —
|
|
149
|
+
whose `from` equals S, whose `event` equals E or is a wildcard, and
|
|
150
|
+
whose guard is absent or EBV-true, fires. There is no specificity
|
|
151
|
+
ranking (a wildcard listed first beats a named match listed later), no
|
|
152
|
+
backtracking, and order among guards is the author's to arrange —
|
|
153
|
+
fallbacks go last.
|
|
154
|
+
|
|
155
|
+
When a transition fires, the step result reports `changed: true`,
|
|
156
|
+
`state` = the transition's `to`, and the resolved effect descriptors in
|
|
157
|
+
this order:
|
|
158
|
+
|
|
159
|
+
1. the `exit` effects of the from-state — only when the state actually
|
|
160
|
+
changed (`from ≠ to`);
|
|
161
|
+
2. the transition's own `effects` — always;
|
|
162
|
+
3. the `entry` effects of the to-state — only when the state actually
|
|
163
|
+
changed.
|
|
164
|
+
|
|
165
|
+
A **self-transition** (`from = to`) therefore runs its transition
|
|
166
|
+
effects but neither exit nor entry: format 0.1 has no internal/external
|
|
167
|
+
transition distinction, and re-running entry work on a self-loop is the
|
|
168
|
+
surprising default. `changed` means **a transition fired** — a
|
|
169
|
+
self-transition reports `changed: true` with an unchanged `state`.
|
|
170
|
+
|
|
171
|
+
A step whose event no transition answers is **ignored**: the result is
|
|
172
|
+
`changed: false`, the same state, no effects — the conventional FSM
|
|
173
|
+
reading of an unhandled event, and the shape hosts can treat as a
|
|
174
|
+
no-op. It MUST NOT be an error.
|
|
175
|
+
|
|
176
|
+
## §5 Errors
|
|
177
|
+
|
|
178
|
+
Every failure carries a stable `code` and the `docPath` of the
|
|
179
|
+
offending document member. The classes live in `src/errors.js`; the
|
|
180
|
+
tables there and here MUST stay in sync.
|
|
181
|
+
|
|
182
|
+
### §5.1 Compile errors (`FlowCompileError`, thrown)
|
|
183
|
+
|
|
184
|
+
| code | condition |
|
|
185
|
+
|---|---|
|
|
186
|
+
| JF0001 | the document is not an object, or `$fsm` is present and not `"0.1"` |
|
|
187
|
+
| JF0002 | `states` is not an array, or a state entry is malformed |
|
|
188
|
+
| JF0003 | two state entries share one id |
|
|
189
|
+
| JF0004 | `initial` is neither null nor a declared state id |
|
|
190
|
+
| JF0005 | `transitions` is not an array, or an entry is malformed |
|
|
191
|
+
| JF0006 | a transition's `from` or `to` names no declared state |
|
|
192
|
+
| JF0007 | a guard failed to compile (`cause` carries the query error) |
|
|
193
|
+
| JF0008 | an effects list or effect descriptor is malformed |
|
|
194
|
+
| JF0009 | an effect's `with` failed to compile (`cause`) |
|
|
195
|
+
| JF0010 | the dag document is not an object, or `$dag` is not `"0.1"` |
|
|
196
|
+
| JF0011 | `nodes` is not an object, or a node declaration is malformed |
|
|
197
|
+
| JF0012 | `edges` is not an array, or an edge entry is malformed |
|
|
198
|
+
| JF0013 | an edge's `from` or `to` names no declared node |
|
|
199
|
+
| JF0014 | an embedded query/stylesheet/`with`/`select` failed to compile (`cause`) |
|
|
200
|
+
| JF0015 | wiring rules violated: inbound into `input`/`const`, outbound from `output`, incomplete/duplicate ports, or a consumer with no inbound edge |
|
|
201
|
+
| JF0016 | the graph has a cycle (member ids in the message, `docPath` at the first edge inside it) |
|
|
202
|
+
| JF0017 | not exactly one `output` node |
|
|
203
|
+
| JF0018 | a `task` node names no registered handler |
|
|
204
|
+
|
|
205
|
+
### §5.2 Runtime: thrown vs recorded
|
|
206
|
+
|
|
207
|
+
Only **caller mistakes** throw (`FlowRuntimeError`):
|
|
208
|
+
|
|
209
|
+
| code | condition |
|
|
210
|
+
|---|---|
|
|
211
|
+
| JF2001 | an undeclared state id passed to `step`/`events`/`final`, or as a session start |
|
|
212
|
+
| JF2002 | a non-string event passed to `step` |
|
|
213
|
+
| JF2005 | a session created with no start state anywhere |
|
|
214
|
+
|
|
215
|
+
**Document-level evaluation failures never throw.** They fail closed
|
|
216
|
+
and are recorded as plain data in the step result's `errors` array,
|
|
217
|
+
each record `{ code, docPath, message }`:
|
|
218
|
+
|
|
219
|
+
| code | condition | fail-closed reading |
|
|
220
|
+
|---|---|---|
|
|
221
|
+
| JF2003 | a guard threw while evaluating | the guard is false; selection continues |
|
|
222
|
+
| JF2004 | an effect's `with` threw while evaluating | the effect is omitted; the step completes |
|
|
223
|
+
|
|
224
|
+
A step with a non-empty `errors` array still returns a valid result —
|
|
225
|
+
hosts SHOULD surface the records through their own error channel, and a
|
|
226
|
+
repair loop gets a `docPath` pointing at exactly the query that failed.
|
|
227
|
+
|
|
228
|
+
**Dag runs are different, deliberately** (§7.3): a dag has no
|
|
229
|
+
recorded-error channel. A failure rejects the whole run promise —
|
|
230
|
+
because a dataflow result assembled from partially failed nodes is
|
|
231
|
+
exactly the kind of partial result D7 forbids:
|
|
232
|
+
|
|
233
|
+
| code | condition |
|
|
234
|
+
|---|---|
|
|
235
|
+
| JF2006 | a node failed while evaluating (own `nodeId` property beside `docPath` and `cause`) |
|
|
236
|
+
| JF2007 | the caller's signal aborted the run |
|
|
237
|
+
| JF2008 | a node declared `checkpoint: true` but produced a value that is not JSON-serializable; the run rejects at save time |
|
|
238
|
+
| JF2009 | the checkpoint store threw while loading, saving or completing; the run rejects |
|
|
239
|
+
|
|
240
|
+
## §6 The jaren-dag document
|
|
241
|
+
|
|
242
|
+
A **jaren-dag** document is an executable, acyclic dataflow: named
|
|
243
|
+
nodes wired by edges that carry data, run to completion for one input.
|
|
244
|
+
The nodes are the suite's own engines — a closed vocabulary, which is
|
|
245
|
+
what keeps the schema a real contract for constrained decoding;
|
|
246
|
+
extending it is a format revision, not an option.
|
|
247
|
+
|
|
248
|
+
```json
|
|
249
|
+
{
|
|
250
|
+
"$dag": "0.1",
|
|
251
|
+
"nodes": {
|
|
252
|
+
"rows": { "kind": "input" },
|
|
253
|
+
"adults": { "kind": "query",
|
|
254
|
+
"query": { "$for": { "r": "$[*]" }, "$where": { "$ge": ["$r.age", 18] }, "$return": "$r" } },
|
|
255
|
+
"names": { "kind": "jslt",
|
|
256
|
+
"stylesheet": [{ "match": "$", "body": ["ul", {},
|
|
257
|
+
[{ "$for": { "p": "$[*]" }, "$return": ["li", {}, "$p.name"] }]] }] },
|
|
258
|
+
"out": { "kind": "output" }
|
|
259
|
+
},
|
|
260
|
+
"edges": [
|
|
261
|
+
{ "from": "rows", "to": "adults" },
|
|
262
|
+
{ "from": "adults", "to": "names" },
|
|
263
|
+
{ "from": "names", "to": "out" }
|
|
264
|
+
]
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
- **`$dag`** MUST be `"0.1"` and MUST be present — unlike `$fsm` there
|
|
269
|
+
is no earlier contract to stay compatible with, so the document says
|
|
270
|
+
what it is.
|
|
271
|
+
- **`nodes`** MUST be an object of id → declaration. The kinds:
|
|
272
|
+
|
|
273
|
+
| kind | members | meaning |
|
|
274
|
+
|---|---|---|
|
|
275
|
+
| `input` | — | yields the `run(input)` value (`null` when absent) |
|
|
276
|
+
| `const` | `value` (required, any JSON) | yields its literal value |
|
|
277
|
+
| `query` | `query` | a Jaren JSON Query over the node's input scope |
|
|
278
|
+
| `jslt` | `stylesheet` | a JSLT stylesheet over the node's input scope |
|
|
279
|
+
| `task` | `run`, `with?` | a registered async handler (§7.2) |
|
|
280
|
+
| `output` | — | its input scope value is the run's result |
|
|
281
|
+
|
|
282
|
+
- **`edges`** MUST be an array of `{ from, to, port?, select? }`.
|
|
283
|
+
`select` is a query applied to the source value before delivery.
|
|
284
|
+
Wiring rules (all compile-time): `input` and `const` nodes accept no
|
|
285
|
+
inbound edge; the `output` node has no outbound edge; `query`,
|
|
286
|
+
`jslt`, `task` and `output` nodes MUST have at least one inbound
|
|
287
|
+
edge; the graph MUST be acyclic; exactly one `output` node MUST be
|
|
288
|
+
declared. Unknown members are ignored for forward compatibility.
|
|
289
|
+
|
|
290
|
+
### §6.1 The input scope
|
|
291
|
+
|
|
292
|
+
A node's `$` is decided by its inbound edges:
|
|
293
|
+
|
|
294
|
+
- **One unported edge** — `$` is the delivered value, verbatim.
|
|
295
|
+
- **Ported edges** — when any inbound edge names a `port`, every
|
|
296
|
+
inbound edge MUST name one, ports MUST be unique, and `$` is the
|
|
297
|
+
object of port-named values, members in **edge document order**. A
|
|
298
|
+
single ported edge therefore yields `{ port: value }` — the way to
|
|
299
|
+
force the object shape.
|
|
300
|
+
- Two or more unported inbound edges are a compile error (JF0015).
|
|
301
|
+
|
|
302
|
+
A delivery whose `select` yields the empty sequence delivers `null`; a
|
|
303
|
+
`query`/`jslt` node whose own result is empty likewise yields `null` —
|
|
304
|
+
`undefined` is not a JSON value and never flows through a graph.
|
|
305
|
+
Values pass **by reference**: nodes and hosts MUST NOT mutate what
|
|
306
|
+
they receive.
|
|
307
|
+
|
|
308
|
+
## §7 Dag execution
|
|
309
|
+
|
|
310
|
+
### §7.1 Compile once, run many
|
|
311
|
+
|
|
312
|
+
`compileDag(doc, { tasks })` validates the document (§5.1 codes),
|
|
313
|
+
compiles every embedded query, stylesheet, `with` and `select` exactly
|
|
314
|
+
once, resolves every `task` node against the registry (a missing
|
|
315
|
+
handler is compile-time JF0018 — fail early, not mid-run), and proves
|
|
316
|
+
acyclicity. `run(input, { signal?, onNode? })` may then be called any
|
|
317
|
+
number of times, concurrently; runs share nothing but the compiled
|
|
318
|
+
closures.
|
|
319
|
+
|
|
320
|
+
### §7.2 The run
|
|
321
|
+
|
|
322
|
+
Nodes evaluate when their inputs are ready — topological order with
|
|
323
|
+
insertion-order tie-break; independent branches run **concurrently**.
|
|
324
|
+
Determinism is *same input → same output values*, never same timing:
|
|
325
|
+
results are keyed and port objects are assembled in edge document
|
|
326
|
+
order, so completion order cannot change a value. Each node evaluates
|
|
327
|
+
at most once per run; every declared node evaluates, reachable from
|
|
328
|
+
the output or not.
|
|
329
|
+
|
|
330
|
+
A `task` node's handler is called as
|
|
331
|
+
`handler({ with, input }, signal)` — `with` is its query resolved
|
|
332
|
+
against the node's input scope (`null` when absent or empty), `input`
|
|
333
|
+
is the scope value, and `signal` is the run's shared `AbortSignal`.
|
|
334
|
+
The handler MAY return a plain value or a promise; a resolved
|
|
335
|
+
`undefined` reads as `null`. **Handlers MUST honor the signal**: an
|
|
336
|
+
ignoring handler can never block a run's rejection, but it blocks a
|
|
337
|
+
run's successful resolution (the run resolves only when every node
|
|
338
|
+
settled).
|
|
339
|
+
|
|
340
|
+
### §7.3 Failure and abort
|
|
341
|
+
|
|
342
|
+
The first failing node wins: the run rejects with `JF2006` (own
|
|
343
|
+
`nodeId`, `docPath` to the failing member, `cause`), the shared signal
|
|
344
|
+
aborts, and in-flight tasks are expected to reject promptly. The
|
|
345
|
+
caller's `signal` aborting rejects the run with `JF2007`. There are no
|
|
346
|
+
retries and no partial results — rerunning is the caller's decision,
|
|
347
|
+
and the id-guard convention of `@jarenjs/app`'s TASKS.md is the
|
|
348
|
+
staleness answer when a dag runs as an app effect
|
|
349
|
+
(APP-INTEGRATION.md).
|
|
350
|
+
|
|
351
|
+
### §7.4 Observability
|
|
352
|
+
|
|
353
|
+
`onNode` receives one bounded JSON record per node **that started
|
|
354
|
+
evaluating**, at settlement: `{ id, status, ms }` with `status` one of
|
|
355
|
+
`ok` | `error` | `aborted` | `restored` and `ms` the node's own
|
|
356
|
+
evaluation time (waiting for inputs excluded). `restored` is the one
|
|
357
|
+
exception to "started evaluating": a RESUMED run (§7.6) fires it
|
|
358
|
+
first, `ms: 0`, for every node whose checkpointed value was seeded
|
|
359
|
+
instead of evaluated. The first failure records `error`;
|
|
360
|
+
concurrent losers and abort victims record `aborted`; nodes whose
|
|
361
|
+
inputs never arrived produce no record. Records for stragglers MAY
|
|
362
|
+
arrive after the run promise already rejected. A throwing observer is
|
|
363
|
+
isolated and ignored — observation MUST NOT change a run.
|
|
364
|
+
|
|
365
|
+
### §7.5 Non-goals of format 0.1
|
|
366
|
+
|
|
367
|
+
- **Streaming.** A run is one value in, one value out; feeding a dag
|
|
368
|
+
from the `@jarenjs/josl` incremental readers chunk by chunk is the
|
|
369
|
+
natural 0.2 composition, and doing it well changes the node contract
|
|
370
|
+
— so it is not bolted on here.
|
|
371
|
+
- **Retries.** A failed run is rerun by its caller; retry policy
|
|
372
|
+
belongs to the host (or the app's task convention), not the graph.
|
|
373
|
+
- **Persistence / resume — amended.** A run STILL holds no durable
|
|
374
|
+
state by default, because checkpoints would make every node contract
|
|
375
|
+
a serialization contract. §7.6 makes that contract EXPLICIT and
|
|
376
|
+
opt-in instead of universal: nothing changes for any document or
|
|
377
|
+
caller that does not ask.
|
|
378
|
+
- **Cross-run caching.** Same-input memoization is a host concern;
|
|
379
|
+
the engine promising it would outlaw impure task handlers the
|
|
380
|
+
format explicitly allows.
|
|
381
|
+
|
|
382
|
+
### §7.6 Checkpointing — the explicit serialization contract
|
|
383
|
+
|
|
384
|
+
Durable runs are OPT-IN twice: the caller provides a store, and each
|
|
385
|
+
node that wants its value persisted declares it.
|
|
386
|
+
|
|
387
|
+
```js
|
|
388
|
+
const dag = compileDag(doc, { tasks, checkpoint: {
|
|
389
|
+
load(runId) {}, // → { values: { [nodeId]: value } } | null
|
|
390
|
+
save(runId, nodeId, value) {}, // record one declared node's value
|
|
391
|
+
complete(runId, result) {}, // record the run's result
|
|
392
|
+
} });
|
|
393
|
+
await dag.run(input, { runId: 'run-42' });
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
- A node declares `"checkpoint": true` to assert its output is JSON.
|
|
397
|
+
A declared node whose value fails RFC 8785 canonicalization is
|
|
398
|
+
**`JF2008` at save time, never a silent skip** — the node's author
|
|
399
|
+
claimed a serialization contract and broke it. Undeclared nodes are
|
|
400
|
+
simply RECOMPUTED on resume, which preserves the exact 0.1 contract
|
|
401
|
+
for every document written before this section existed.
|
|
402
|
+
- A resumed run (`run(input, { runId })` with recorded values) SEEDS
|
|
403
|
+
the per-run memo from `load` and restarts the rest — the execution
|
|
404
|
+
model is untouched; only where the memo comes from changed. Seeded
|
|
405
|
+
nodes fire `restored` records (§7.4).
|
|
406
|
+
- `save` runs after the node's value exists and before its `ok`
|
|
407
|
+
record: a crash between the two re-runs the node on resume —
|
|
408
|
+
at-least-once, stated plainly. A store member MAY return a promise;
|
|
409
|
+
a THROWING store fails the run with `JF2009`.
|
|
410
|
+
- `complete(runId, result)` runs after the output settles; a queue
|
|
411
|
+
backing the store can mark its job done in the same transaction —
|
|
412
|
+
which is the entire point of the shape.
|
|
413
|
+
- **Idempotency is the caller's.** A task node with side effects that
|
|
414
|
+
runs twice after a crash is the caller's bug, bluntly. The
|
|
415
|
+
mitigation is an idempotency key threaded through the node's
|
|
416
|
+
`with` props and honoured by the effectful system itself.
|
|
417
|
+
- Resuming under a DIFFERENT document than the one that saved is
|
|
418
|
+
undefined behaviour — keep the document stable with the run (the
|
|
419
|
+
`@jarenjs/db` queue stores it on the job row for exactly this
|
|
420
|
+
reason). Values recorded for node ids the current document does not
|
|
421
|
+
declare (or no longer declares `checkpoint`) are ignored.
|
|
422
|
+
|
|
423
|
+
### §7.7 FSM persistence
|
|
424
|
+
|
|
425
|
+
Needs nothing new: `step` is pure and a session's whole durable state
|
|
426
|
+
IS its current state string. `@jarenjs/flow` ships three thin helpers
|
|
427
|
+
— `snapshotFsm(session)` → `{ state }`, `resumeFsmSession(fsm,
|
|
428
|
+
snapshot)` (an undeclared state refuses with the session's own
|
|
429
|
+
JF2001), and `createDurableFsmSession(fsm, { load, save })`, which
|
|
430
|
+
persists through a SYNCHRONOUS store on every state CHANGE, before
|
|
431
|
+
the step result returns; a throwing `save` fails the send rather than
|
|
432
|
+
lose a transition. A worked example over a `@jarenjs/db` collection:
|
|
433
|
+
|
|
434
|
+
```js
|
|
435
|
+
const machine = compileFsm(doc);
|
|
436
|
+
const orders = store.sync.collection('fsm');
|
|
437
|
+
const session = createDurableFsmSession(machine, {
|
|
438
|
+
load: () => orders.get('order-7')?.state ?? null,
|
|
439
|
+
save: (state) => orders.put({ id: 'order-7', state }, 'order-7'),
|
|
440
|
+
});
|
|
441
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jarenjs/flow",
|
|
3
|
+
"private": false,
|
|
4
|
+
"version": "0.34.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"types": "./dist/types/index.d.ts",
|
|
8
|
+
"sideEffects": false,
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/types/index.d.ts",
|
|
12
|
+
"default": "./src/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./schemas/*": "./schemas/*",
|
|
15
|
+
"./package.json": "./package.json"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist/types/",
|
|
19
|
+
"src/",
|
|
20
|
+
"docs/",
|
|
21
|
+
"schemas/"
|
|
22
|
+
],
|
|
23
|
+
"description": "Executable workflow documents for the Jaren suite: the jaren-fsm finite-state-machine format compiled to a pure step function, with query-document guards and effect descriptors as data",
|
|
24
|
+
"author": "joham",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/jklarenbeek/jarenjs.git",
|
|
28
|
+
"directory": "packages/flow"
|
|
29
|
+
},
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=24"
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public",
|
|
36
|
+
"registry": "https://registry.npmjs.org/"
|
|
37
|
+
},
|
|
38
|
+
"keywords": [
|
|
39
|
+
"jaren",
|
|
40
|
+
"json",
|
|
41
|
+
"fsm",
|
|
42
|
+
"finite-state-machine",
|
|
43
|
+
"workflow",
|
|
44
|
+
"state-machine"
|
|
45
|
+
],
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "npm run build:types",
|
|
48
|
+
"build:types": "tsc -p tsconfig.json",
|
|
49
|
+
"prepack": "npm run build:types"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"@jarenjs/core": "^0.34.0",
|
|
53
|
+
"@jarenjs/json": "^0.34.0"
|
|
54
|
+
}
|
|
55
|
+
}
|