@jarenjs/flow 0.72.2 → 0.73.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 +60 -3
- package/dist/types/dag.d.ts +6 -4
- package/dist/types/errors.d.ts +10 -0
- package/dist/types/fsm.d.ts +24 -0
- package/dist/types/index.d.ts +18 -0
- package/dist/types/statechart.d.ts +82 -0
- package/dist/types/workflow.d.ts +97 -0
- package/docs/APP-INTEGRATION.md +8 -0
- package/docs/FLOW-FORMAT.md +67 -11
- package/docs/STATECHART-FORMAT.md +186 -0
- package/docs/WORKFLOW-FORMAT.md +260 -0
- package/package.json +4 -4
- package/schemas/jaren-statechart-state.draft-07.schema.json +72 -0
- package/schemas/jaren-statechart-state.schema.json +72 -0
- package/schemas/jaren-statechart.authoring.schema.json +216 -0
- package/schemas/jaren-statechart.draft-07.schema.json +216 -0
- package/schemas/jaren-statechart.schema.json +216 -0
- package/schemas/jaren-workflow.authoring.schema.json +282 -0
- package/schemas/jaren-workflow.draft-07.schema.json +288 -0
- package/schemas/jaren-workflow.schema.json +288 -0
- package/src/dag.js +52 -24
- package/src/errors.js +10 -0
- package/src/fsm.js +2 -2
- package/src/index.js +11 -0
- package/src/statechart.js +420 -0
- package/src/workflow.js +461 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# Jaren statecharts — jaren-fsm 0.2
|
|
2
|
+
|
|
3
|
+
`compileStatechart` compiles a JSON document into pure `start`, `step` and
|
|
4
|
+
`advance` functions. It provides compound states, parallel regions, shallow
|
|
5
|
+
and deep history, completion/eventless transitions and delayed transitions.
|
|
6
|
+
It executes no effect and reads no clock. `compileFsm`, `createFsmSession`,
|
|
7
|
+
`fsmToApp` and the 0.1 schemas retain their existing contracts.
|
|
8
|
+
|
|
9
|
+
The canonical grammar is [jaren-statechart.schema.json](../schemas/jaren-statechart.schema.json)
|
|
10
|
+
(`https://jarenjs.dev/schemas/jaren-fsm/0.2`), with draft-07 and authoring twins.
|
|
11
|
+
Register the query grammar when validating. Compilation checks references,
|
|
12
|
+
hierarchy and transition domains that JSON Schema cannot prove. Unknown
|
|
13
|
+
statechart, state and transition members are compile errors.
|
|
14
|
+
|
|
15
|
+
## 1. Document and state tree
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"$fsm": "0.2",
|
|
20
|
+
"initial": "working",
|
|
21
|
+
"states": [
|
|
22
|
+
{ "id": "working", "initial": "editing" },
|
|
23
|
+
{ "id": "editing", "parent": "working" },
|
|
24
|
+
{ "id": "review", "parent": "working" },
|
|
25
|
+
{ "id": "memory", "parent": "working", "history": "deep" },
|
|
26
|
+
"paused",
|
|
27
|
+
{ "id": "expired", "final": true }
|
|
28
|
+
],
|
|
29
|
+
"transitions": [
|
|
30
|
+
{ "from": "editing", "event": "submit", "to": "review" },
|
|
31
|
+
{ "from": "working", "event": "pause", "to": "paused" },
|
|
32
|
+
{ "from": "paused", "event": "resume", "to": "memory" },
|
|
33
|
+
{ "from": "review", "after": 60000, "to": "expired" }
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
States have globally unique, nonempty string ids. A bare string declares an
|
|
39
|
+
atomic state. An object supports `id`, `parent`, `initial`, `type`, `history`,
|
|
40
|
+
`final`, `entry` and `exit`. The parent relation MUST be acyclic.
|
|
41
|
+
|
|
42
|
+
| Type | Contract |
|
|
43
|
+
|---|---|
|
|
44
|
+
| `atomic` | No children. The default when no ordinary children exist. |
|
|
45
|
+
| `compound` | Exactly one ordinary child active; `initial` MUST name an immediate ordinary child. Inferred when ordinary children exist. |
|
|
46
|
+
| `parallel` | All ordinary children active. Each region recursively enters its initial configuration. No `initial` on the parallel state itself. |
|
|
47
|
+
| `final` | No children or outgoing transitions. `final: true` is shorthand. |
|
|
48
|
+
| `history` | A pseudo-state with a parent and `history: "shallow"` or `"deep"`; no children, outgoing transitions or effects. `history` infers this type. |
|
|
49
|
+
|
|
50
|
+
The document's `initial` MUST name an ordinary root state. State effects use
|
|
51
|
+
FLOW-FORMAT's `{run,with?}` descriptors. Final and history declarations cannot
|
|
52
|
+
conflict with an explicit `type`. Parents precede children in runtime tree
|
|
53
|
+
order even if the flattened declarations list a child first; siblings retain
|
|
54
|
+
declaration order.
|
|
55
|
+
|
|
56
|
+
Entering a compound target recursively enters its initial child. Entering a
|
|
57
|
+
parallel target recursively enters every region. An explicit descendant target
|
|
58
|
+
enters its ancestors and fills any unmentioned parallel regions with defaults.
|
|
59
|
+
|
|
60
|
+
## 2. Selection, effects and completion
|
|
61
|
+
|
|
62
|
+
A transition supports `from`, `to`, `guard`, `effects`, `type` and at most one
|
|
63
|
+
trigger: `event`, `after`, `always: true` or `done: true`. An absent or null
|
|
64
|
+
event is a wildcard over **external string events**, as in 0.1. It does not
|
|
65
|
+
match timers or completion. A transition MUST name declared endpoints.
|
|
66
|
+
|
|
67
|
+
For each active leaf, search its own transitions first, then its ancestors
|
|
68
|
+
nearest first. Within a source, document order decides; a wildcard before a
|
|
69
|
+
named transition wins. Evaluate a shared ancestor guard once per microstep.
|
|
70
|
+
Independent parallel regions can transition in the same step. Conflicting
|
|
71
|
+
exit sets are resolved in favor of the descendant source; unrelated conflicts
|
|
72
|
+
use tree order of the selecting leaves. The selected transition effects run
|
|
73
|
+
in transition document order.
|
|
74
|
+
|
|
75
|
+
`type: "external"` is the default in **0.2**, including self-transitions.
|
|
76
|
+
Its domain is the least common **proper compound ancestor** of source and
|
|
77
|
+
target (or the virtual root); a parallel ancestor is not such a domain.
|
|
78
|
+
Exiting that domain's active descendants makes a cross-region transition
|
|
79
|
+
leave and re-enter the parallel state, restoring untargeted regions to their
|
|
80
|
+
defaults. `type: "internal"` can target the source or its descendants: the
|
|
81
|
+
source stays entered, while its active descendants exit. An atomic internal
|
|
82
|
+
self-transition therefore runs only its transition effects. This is an
|
|
83
|
+
explicit version difference from 0.1's implicit internal self-transition.
|
|
84
|
+
|
|
85
|
+
Each microstep resolves effects against its pre-transition scope in order:
|
|
86
|
+
exit effects in reverse tree order, transition effects in document order,
|
|
87
|
+
entry effects in tree order. `$.state` is the ordered **array of active leaf
|
|
88
|
+
ids**; `$.event` is the external name, or null for initial entry/timers;
|
|
89
|
+
`$.payload` and `$.context` are caller values, defaulting to null. Guards
|
|
90
|
+
use query effective boolean value. Evaluation failures retain FLOW-FORMAT's
|
|
91
|
+
JF2003/JF2004 records and fail-closed behavior.
|
|
92
|
+
|
|
93
|
+
`always` transitions participate in automatic stabilization after initial
|
|
94
|
+
entry, an external event or a timer. `done` is enabled when its compound
|
|
95
|
+
source has an active immediate final child, or all regions of its parallel
|
|
96
|
+
source have completed. Completion sources MUST be compound/parallel. A
|
|
97
|
+
completed root reports `final: true`; it is still possible to send events to
|
|
98
|
+
that root's ancestors/sources until the host chooses to stop. There is no
|
|
99
|
+
implicit context assignment, raised-event queue or invocation machinery.
|
|
100
|
+
|
|
101
|
+
`maxMicrosteps` (default 1000, positive safe integer) bounds one call's
|
|
102
|
+
automatic transitions and timer processing. Exhaustion throws JF2012; pure
|
|
103
|
+
inputs and session state remain unchanged. There is no partial-result commit.
|
|
104
|
+
These rules borrow hierarchy/history and transition-domain definitions from
|
|
105
|
+
[SCXML's control semantics](https://www.w3.org/TR/scxml/), but this JSON format
|
|
106
|
+
is not an SCXML implementation or an SCXML conformance claim.
|
|
107
|
+
|
|
108
|
+
## 3. History
|
|
109
|
+
|
|
110
|
+
Immediately before exiting a compound/parallel state, remember its active
|
|
111
|
+
configuration for each declared history child. Shallow history stores the
|
|
112
|
+
active immediate ordinary children; re-entry initializes their descendants.
|
|
113
|
+
Deep history stores active atomic/final leaves, including all parallel
|
|
114
|
+
regions, and restores those leaves. Targeting history before any record exists
|
|
115
|
+
uses the parent's default initial configuration. Entering the parent normally
|
|
116
|
+
uses its defaults even if history was recorded; history is an explicit target.
|
|
117
|
+
|
|
118
|
+
History restores **control**, not elapsed timers or host context. Restored
|
|
119
|
+
states run their entry effects and schedule fresh delays. A timer that belonged
|
|
120
|
+
to an exited state remains cancelled.
|
|
121
|
+
|
|
122
|
+
## 4. Time is input
|
|
123
|
+
|
|
124
|
+
```js
|
|
125
|
+
const chart = compileStatechart(doc);
|
|
126
|
+
const initial = chart.start({ now: 1000, context });
|
|
127
|
+
const next = chart.step(initial.state, 'submit', { now: 1200, context });
|
|
128
|
+
const later = chart.advance(next.state, 61200, { context });
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`now` is a finite number in the host's millisecond time domain; fresh starts
|
|
132
|
+
default to zero. Omitted step time preserves the snapshot's time. Time MUST
|
|
133
|
+
NOT move backwards (JF2011). The engine never calls `Date.now`, starts a host
|
|
134
|
+
timer, or waits. A host may arm a timer from the earliest returned deadline
|
|
135
|
+
and call `advance` when notified, including after process restart.
|
|
136
|
+
|
|
137
|
+
`after` is a finite nonnegative duration. Entry creates one timer per delayed
|
|
138
|
+
transition, storing its absolute deadline, transition index and monotonically
|
|
139
|
+
increasing token. Exit cancels the source's timers. A timer is consumed once,
|
|
140
|
+
even if its guard is false. Equal deadlines use transition order. Newly entered
|
|
141
|
+
delays use logical firing time, not the time the host eventually delivered the
|
|
142
|
+
wake-up. Overflowing deadlines/tokens are JF2011.
|
|
143
|
+
|
|
144
|
+
`step` does not implicitly fire overdue timers. The host chooses whether an
|
|
145
|
+
external event or a due timer happens first. `advance` consumes due timers up
|
|
146
|
+
to its supplied time and stabilizes after each. `{one: true}` consumes at most
|
|
147
|
+
one timer plus its stabilization, leaving time at that logical firing instant;
|
|
148
|
+
when none is due it advances to the requested time. The composition bridge
|
|
149
|
+
uses this mode to persist visit counts between timer deliveries. Timer indices
|
|
150
|
+
cannot be forged as string events: external dispatch and time advancement are
|
|
151
|
+
separate APIs.
|
|
152
|
+
|
|
153
|
+
## 5. Snapshot and session
|
|
154
|
+
|
|
155
|
+
`StatechartState` is the whole control snapshot:
|
|
156
|
+
|
|
157
|
+
```jsonc
|
|
158
|
+
{
|
|
159
|
+
"active": ["review"],
|
|
160
|
+
"history": { "memory": ["review"] },
|
|
161
|
+
"timers": [{ "transition": 3, "at": 61200, "token": 1 }],
|
|
162
|
+
"serial": 1,
|
|
163
|
+
"time": 1200
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Snapshots are frozen JSON. `restore(snapshot)` validates legal active and
|
|
168
|
+
history configurations, timer sources/deadlines/tokens and numeric counters;
|
|
169
|
+
it normalizes leaf/history/timer ordering and returns an independent frozen
|
|
170
|
+
copy. Invalid snapshots are JF2010. A snapshot's schema is
|
|
171
|
+
[jaren-statechart-state.schema.json](../schemas/jaren-statechart-state.schema.json).
|
|
172
|
+
Standalone callers bind snapshots to their machine revision; composed workflows
|
|
173
|
+
perform that comparison themselves.
|
|
174
|
+
|
|
175
|
+
Results carry `changed`, `state`, `final`, `effects`, `errors`, `entered`,
|
|
176
|
+
`exited` and `transitions` (indices into the compiled document). `changed`
|
|
177
|
+
means a transition fired. An ignored event or a consumed false timer can still
|
|
178
|
+
advance snapshot time, so persist the returned state even when `changed` is false.
|
|
179
|
+
|
|
180
|
+
`createStatechartSession(chart, {state?, now?, context?, store?})` exposes
|
|
181
|
+
`state`, `done`, `initial`, `send`, `advance` and `can`. `initial` contains entry
|
|
182
|
+
effects for a fresh start and is null when restoring; the host executes these
|
|
183
|
+
and later result effects. Optional synchronous `store.load()`/`store.save(state)`
|
|
184
|
+
persist a fresh initial state and every successful send/advance **before** the
|
|
185
|
+
session advances. A failed save leaves the session unchanged. Async stores
|
|
186
|
+
belong at the workflow boundary. `can` is a pure dry run with no store write.
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
# Composed workflows — jaren-workflow 0.2
|
|
2
|
+
|
|
3
|
+
`compileWorkflow(document, {tasks, store?})` lowers a neutral JSON workflow
|
|
4
|
+
onto `compileStatechart` and `compileDag`. The statechart chooses control;
|
|
5
|
+
the DAG executor owns dependency scheduling, fan-out/fan-in and task abort.
|
|
6
|
+
The bridge transfers results, persists snapshots and dispatches completion.
|
|
7
|
+
Host roles, prompts, model settings, network clients and tools remain in the
|
|
8
|
+
versioned task registry.
|
|
9
|
+
|
|
10
|
+
The composed format uses `$workflow: "0.2"` and schema id
|
|
11
|
+
`https://jarenjs.dev/schemas/jaren-workflow/0.2`. This deliberately avoids
|
|
12
|
+
reusing Mermaid's historical `jaren-workflow/0.1` schema identity, which denotes
|
|
13
|
+
the unversioned flat-FSM projection. The new schema is
|
|
14
|
+
[jaren-workflow.schema.json](../schemas/jaren-workflow.schema.json), with
|
|
15
|
+
draft-07 and derived authoring twins. Register the DAG, query and JSLT grammars
|
|
16
|
+
for full validation; schema acceptance is followed by compilation.
|
|
17
|
+
|
|
18
|
+
## 1. The document
|
|
19
|
+
|
|
20
|
+
```json
|
|
21
|
+
{
|
|
22
|
+
"$workflow": "0.2",
|
|
23
|
+
"revision": "review/1",
|
|
24
|
+
"initial": "prepare",
|
|
25
|
+
"states": {
|
|
26
|
+
"prepare": {
|
|
27
|
+
"work": { "task": "prepare", "version": "1" },
|
|
28
|
+
"then": "check",
|
|
29
|
+
"limit": 3
|
|
30
|
+
},
|
|
31
|
+
"check": {
|
|
32
|
+
"choose": [{ "guard": "$.context.data.ready", "to": "review" }],
|
|
33
|
+
"otherwise": "prepare"
|
|
34
|
+
},
|
|
35
|
+
"review": {
|
|
36
|
+
"on": [
|
|
37
|
+
{ "event": "approve", "guard": "$.payload.accept", "to": "done" },
|
|
38
|
+
{ "event": "retry", "to": "prepare" }
|
|
39
|
+
],
|
|
40
|
+
"after": { "ms": 60000, "to": "expired" }
|
|
41
|
+
},
|
|
42
|
+
"done": { "final": true },
|
|
43
|
+
"expired": { "final": true }
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`revision` is a nonblank host-declared revision. `initial` names a state in
|
|
49
|
+
`states`, a nonempty object keyed by nonempty ids. References are local to
|
|
50
|
+
that object. Each state declares exactly one form; unknown members refuse.
|
|
51
|
+
|
|
52
|
+
| Form | Members and meaning |
|
|
53
|
+
|---|---|
|
|
54
|
+
| Work | `work: {task, version, with?}` or `work: {dag: <jaren-dag document>}`; `then` required, `input` and `catch` optional. |
|
|
55
|
+
| Choice | `choose: [{guard,to}, …]` and `otherwise`; first true guard wins. |
|
|
56
|
+
| Wait | `on: [{event,to,guard?}, …]`; optional `after: {ms,to}`. At least one event or delay is required. |
|
|
57
|
+
| Nested flow | `flow: {initial,states}` and `then`; a compound state whose child final completes it. |
|
|
58
|
+
| Final | `final: true`; ends its scope and returns its current data. |
|
|
59
|
+
|
|
60
|
+
Every state may declare `limit`, a positive safe integer. It bounds total
|
|
61
|
+
entries to that fully qualified state **for the whole run**, including
|
|
62
|
+
re-entries through an external event or nested flow. A loop is a back-edge in
|
|
63
|
+
the control graph. Compilation proves that every automatically traversable
|
|
64
|
+
cycle crosses a limited state; otherwise it rejects with JF0022. External
|
|
65
|
+
event edges do not advance themselves and are excluded from this proof.
|
|
66
|
+
Timer edges ARE automatic. Runtime admission counts all entered states and
|
|
67
|
+
rejects an excess with JF2015 before starting work in them. Nested depth is
|
|
68
|
+
bounded at 64. An exhausted bound is an error, not successful completion.
|
|
69
|
+
|
|
70
|
+
Static concurrent branches belong inside `work.dag`, using its existing port,
|
|
71
|
+
query, stylesheet and task vocabulary. Parallel statechart regions are available
|
|
72
|
+
through `compileStatechart`; a composed workflow's dynamic control intentionally
|
|
73
|
+
has one active leaf and one pending work region. Nested control composes by
|
|
74
|
+
lowering; it does not start a second workflow scheduler.
|
|
75
|
+
|
|
76
|
+
## 2. Scope, data and errors
|
|
77
|
+
|
|
78
|
+
Guards and a work state's `input` query read
|
|
79
|
+
`{state,event,payload,context}`. `state` is the active-leaf array. Context is:
|
|
80
|
+
|
|
81
|
+
- `input`: the original run input, unchanged across the workflow;
|
|
82
|
+
- `data`: the current value, initially the input and replaced after each work;
|
|
83
|
+
- `event`: the last accepted external `{type,payload?}` event, initially null;
|
|
84
|
+
- `results`: the latest completed work result by qualified state id;
|
|
85
|
+
- `visits`: entry counts by qualified state id.
|
|
86
|
+
|
|
87
|
+
The default work input is `$.context.data`. A custom `input` query also sees
|
|
88
|
+
`$.context.activation = {runId,state,visit,key}`. `key` is the canonical JSON
|
|
89
|
+
encoding of `[runId, qualifiedStateId, visit]`; it remains the same after a
|
|
90
|
+
crash/resume and differs between loop iterations. For an effectful task, pass
|
|
91
|
+
it explicitly with the work input and have the external system deduplicate it:
|
|
92
|
+
|
|
93
|
+
```jsonc
|
|
94
|
+
{
|
|
95
|
+
"work": { "task": "charge", "version": "billing/3",
|
|
96
|
+
"with": { "idempotencyKey": "$.key", "order": "$.data" } },
|
|
97
|
+
"input": { "key": "$.context.activation.key", "data": "$.context.data" },
|
|
98
|
+
"then": "done"
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
A task handler has the existing DAG signature `({with,input}, signal)`.
|
|
103
|
+
`work.with` reads the **DAG input**, not the outer control scope. Missing
|
|
104
|
+
query results become null. Work inputs, outputs and snapshots must survive
|
|
105
|
+
canonical JSON serialization. Snapshot ownership is separate from task-owned
|
|
106
|
+
values; restoring a checkpoint does not hand a task mutable durable state.
|
|
107
|
+
|
|
108
|
+
On success, the bridge records the result, replaces `data`, clears pending
|
|
109
|
+
work and sends the lowered completion event. A `catch` target handles JF2006
|
|
110
|
+
node failures by replacing data with `{error:{code,message,nodeId}}` and
|
|
111
|
+
transitioning there. Store, provenance, cancellation and visit-bound errors
|
|
112
|
+
are never swallowed by `catch`. Uncaught task failures reject; persisted
|
|
113
|
+
checkpoints remain available for a later retry.
|
|
114
|
+
|
|
115
|
+
Unlike a standalone statechart, a workflow rejects a guard evaluation error
|
|
116
|
+
as JF2016 before advancing control: choosing a fallback after a broken work
|
|
117
|
+
predicate is not a successful workflow decision. The lower-level diagnostic
|
|
118
|
+
path remains on the error. Task effects are at-least-once across crash windows;
|
|
119
|
+
checkpointing does not make an external service exactly-once. Implementation
|
|
120
|
+
versions remain host declarations: the compiler cannot detect code changes
|
|
121
|
+
hidden behind an unchanged version token.
|
|
122
|
+
|
|
123
|
+
## 3. Deterministic lowering
|
|
124
|
+
|
|
125
|
+
`lowerWorkflow(document)` returns frozen JSON containing the original
|
|
126
|
+
`document`, lowering `version`, declared `revision`, `fsm`, `dags`, `specs` and
|
|
127
|
+
`sources`. It resolves no host handler and performs no work. Each state gets
|
|
128
|
+
a stable JSON Pointer id, for example `/states/prepare` or
|
|
129
|
+
`/states/round/flow/states/prepare`; each id segment escapes `~` and `/`.
|
|
130
|
+
|
|
131
|
+
Work shorthand lowers to input → versioned checkpointed task → checkpointed
|
|
132
|
+
output. Inline DAGs retain their wiring and checkpoint declarations, with
|
|
133
|
+
the output checkpoint enabled because a completed region is serializable.
|
|
134
|
+
Consequently **every task in a composed workflow must declare a version**, and
|
|
135
|
+
its registry entry must supply the same version. Intermediate undeclared
|
|
136
|
+
checkpoints recompute, per FLOW-FORMAT §7.6.
|
|
137
|
+
|
|
138
|
+
Choices and work completions become explicit internal event names. Wait
|
|
139
|
+
names receive a separate `event:` prefix, so an external caller cannot forge
|
|
140
|
+
a work-completion event. Delays lower to statechart `after`; nested flows to
|
|
141
|
+
compound states and `done` transitions. No third node scheduler is generated.
|
|
142
|
+
|
|
143
|
+
Compilation exposes `lowered`, `revisions` (control and DAG content
|
|
144
|
+
fingerprints), and a sorted `taskVersions` map qualified by control and node
|
|
145
|
+
path. Trace and checkpoint records refer to these lowered identities. Exact
|
|
146
|
+
canonical documents and maps, rather than a short content hash alone, decide
|
|
147
|
+
resume compatibility. Runtime traces can vary in completion timing; values,
|
|
148
|
+
control decisions and lowering are deterministic for equal inputs/events,
|
|
149
|
+
explicit time and equal task results.
|
|
150
|
+
|
|
151
|
+
## 4. Run, wait and resume
|
|
152
|
+
|
|
153
|
+
```js
|
|
154
|
+
const workflow = compileWorkflow(doc, {
|
|
155
|
+
tasks: { prepare: { version: '1', run: async ({input}, signal) => prepare(input, signal) } },
|
|
156
|
+
store,
|
|
157
|
+
});
|
|
158
|
+
const first = await workflow.run(input, { runId: 'review-7', now: 1000 });
|
|
159
|
+
// first.status: 'waiting' or 'done'; first.snapshot is frozen JSON.
|
|
160
|
+
const next = await workflow.run(input, {
|
|
161
|
+
runId: 'review-7', now: 2000,
|
|
162
|
+
expectedGeneration: first.snapshot.generation,
|
|
163
|
+
event: { type: 'approve', payload: { accept: true } },
|
|
164
|
+
});
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
A run proceeds until a final state or an unhandled wait. The return value is
|
|
168
|
+
`{status: 'waiting'|'done', result, snapshot}`; `result` is current data on
|
|
169
|
+
completion and null while waiting. With no store, pass the previous snapshot
|
|
170
|
+
as `snapshot` on the next call. Store and explicit snapshot are mutually
|
|
171
|
+
exclusive. Resume requires the original input and matching exact identities;
|
|
172
|
+
JF2013 is raised before loading any node result when they differ.
|
|
173
|
+
|
|
174
|
+
One supplied event is delivered to the first wait reached by that call, then
|
|
175
|
+
consumed even if no guard accepts it. Unknown events leave a wait unchanged.
|
|
176
|
+
`expectedGeneration` rejects a stale event/resume with JF2014 before work.
|
|
177
|
+
An accepted event is persisted in `context.event` before subsequent work, so
|
|
178
|
+
an input query can consume `$.context.event.payload` after a durable wait.
|
|
179
|
+
Callers needing durable event receipts maintain those in their host; duplicate
|
|
180
|
+
external-event delivery is not an exactly-once input protocol.
|
|
181
|
+
|
|
182
|
+
`now` is explicit statechart time. A resumed computation uses the supplied
|
|
183
|
+
wake-up time for its next control transition. At a wait the bridge processes a due timer
|
|
184
|
+
before the supplied external event, persisting each admission; this ordering
|
|
185
|
+
is fixed. Fresh runs default to time zero. Work is not automatically timed
|
|
186
|
+
out; cancellation belongs to the supplied `signal`. A host can wake a saved
|
|
187
|
+
workflow using its snapshot's earliest deadline. No sleeping timer lives in
|
|
188
|
+
the bridge.
|
|
189
|
+
|
|
190
|
+
`onTrace` receives frozen records with `runId`, source `revision`, and lowered
|
|
191
|
+
`revisions`. Node records add `type:'node'`, qualified `state`, activation
|
|
192
|
+
visit, and the DAG's `id/status/ms`; transition records add ordered transition
|
|
193
|
+
indices, entered/exited ids, active leaves and snapshot generation. A final
|
|
194
|
+
`waiting`/`done` record names the settled control. Observer exceptions are
|
|
195
|
+
isolated. Aborted DAG stragglers can still produce observation records; they
|
|
196
|
+
cannot commit new work checkpoints after cancellation.
|
|
197
|
+
|
|
198
|
+
## 5. Store, atomicity and crash windows
|
|
199
|
+
|
|
200
|
+
A store implements async-or-sync methods:
|
|
201
|
+
|
|
202
|
+
```js
|
|
203
|
+
load(runId); // snapshot or null
|
|
204
|
+
save(runId, snapshot, expectedGeneration); // true on atomic success, false if stale
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
`save` MUST atomically compare the existing generation, replace the complete
|
|
208
|
+
snapshot and return true. Expected generation zero means the run is absent.
|
|
209
|
+
The bridge claims a fresh generation before work, serializes concurrent DAG
|
|
210
|
+
checkpoint writes, and awaits every durable write before the corresponding
|
|
211
|
+
control change is visible. A failed write is JF2009; a failed comparison is
|
|
212
|
+
JF2014. Same-instance concurrent calls for one run id refuse immediately;
|
|
213
|
+
independent compiles/processes are fenced by the store's CAS. This is optimistic
|
|
214
|
+
fencing, not an execution lease: overlapping workers can start external work
|
|
215
|
+
before one loses its next CAS, so side effects still need the idempotency key.
|
|
216
|
+
|
|
217
|
+
A snapshot carries format, run id, exact identities, generation, statechart
|
|
218
|
+
control, data context, status and nullable pending DAG activation. Pending
|
|
219
|
+
work carries state id, visit, captured input, per-node values, DAG provenance
|
|
220
|
+
and, after completion, its result. On resume, declared node checkpoints seed
|
|
221
|
+
the existing DAG memo; a recorded completed region skips the DAG altogether.
|
|
222
|
+
Malformed control, visits, pending provenance and node declarations refuse.
|
|
223
|
+
|
|
224
|
+
A crash after an effect succeeds but before its node checkpoint repeats that
|
|
225
|
+
effect on resume. A crash after region completion but before control changes
|
|
226
|
+
reuses the region result. Store implementations choose transaction durability
|
|
227
|
+
and retention; the library promises no fsync, queue leasing or automatic
|
|
228
|
+
compaction. All run data is currently stored in one bounded-by-the-host JSON
|
|
229
|
+
snapshot; large histories should use a store with appropriate limits.
|
|
230
|
+
|
|
231
|
+
Standalone DAGs can opt into the same exact provenance comparison with
|
|
232
|
+
`compileDag(doc, {tasks,checkpoint,revision})`; see FLOW-FORMAT §7.9.
|
|
233
|
+
|
|
234
|
+
## 6. Repository consumer and verification
|
|
235
|
+
|
|
236
|
+
[The flow benchmark document](../../../benchmark/fixtures/flow-workflow.json)
|
|
237
|
+
is the workflow executed by `npm run benchmark:flow`. Two correctness suites
|
|
238
|
+
fan out in one DAG. A nested flow then runs the existing FSM and DAG benchmarks
|
|
239
|
+
**sequentially**, retaining their measurement isolation and original assertions.
|
|
240
|
+
A report task assembles their values. A choice optionally enters a persisted
|
|
241
|
+
review wait; `retry` loops through at most three measurement rounds.
|
|
242
|
+
|
|
243
|
+
```sh
|
|
244
|
+
npm run benchmark:flow -- --quick
|
|
245
|
+
npm run benchmark:flow -- --quick --review --checkpoint /tmp/flow-runs.json
|
|
246
|
+
npm run benchmark:flow -- --checkpoint /tmp/flow-runs.json --accept
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Omit `--quick` for normal benchmark iterations. Quick timings are smoke data,
|
|
250
|
+
not replacement release measurements. Use a new `--run-id` for a fresh run;
|
|
251
|
+
resuming a completed run intentionally returns its saved result. The file
|
|
252
|
+
store uses an exclusive short lock plus atomic compare/rename; a lock left by
|
|
253
|
+
a killed process must be inspected and removed by the host, never stolen based
|
|
254
|
+
on elapsed time. Node/browser hosts may inject another CAS store.
|
|
255
|
+
|
|
256
|
+
Tests cover this exact document's concurrency, ordered measurements and saved
|
|
257
|
+
review/retry, plus general task/choice/loop/nesting/wait, failure/resume,
|
|
258
|
+
checkpoint reuse, generation races, changed identities, cancellation and
|
|
259
|
+
schema/compiler agreement. Streaming input, graphical editing, action-document
|
|
260
|
+
awaiting and domain orchestration projections are separate contracts.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jarenjs/flow",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.73.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"types": "./dist/types/index.d.ts",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"docs/",
|
|
21
21
|
"schemas/"
|
|
22
22
|
],
|
|
23
|
-
"description": "Executable
|
|
23
|
+
"description": "Executable JSON workflows: pure FSMs and statecharts, concurrent DAGs, deterministic composition and durable checkpoint/resume",
|
|
24
24
|
"author": "joham",
|
|
25
25
|
"repository": {
|
|
26
26
|
"type": "git",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"prepack": "npm run build:types"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@jarenjs/core": "^0.
|
|
53
|
-
"@jarenjs/json": "^0.
|
|
52
|
+
"@jarenjs/core": "^0.73.0",
|
|
53
|
+
"@jarenjs/json": "^0.73.0"
|
|
54
54
|
}
|
|
55
55
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://jarenjs.dev/schemas/jaren-fsm/0.2/state/draft-07",
|
|
4
|
+
"title": "Jaren statechart snapshot",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"properties": {
|
|
7
|
+
"active": {
|
|
8
|
+
"type": "array",
|
|
9
|
+
"items": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"minLength": 1
|
|
12
|
+
},
|
|
13
|
+
"minItems": 1,
|
|
14
|
+
"uniqueItems": true
|
|
15
|
+
},
|
|
16
|
+
"history": {
|
|
17
|
+
"type": "object",
|
|
18
|
+
"additionalProperties": {
|
|
19
|
+
"type": "array",
|
|
20
|
+
"items": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"minLength": 1
|
|
23
|
+
},
|
|
24
|
+
"minItems": 1,
|
|
25
|
+
"uniqueItems": true
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"timers": {
|
|
29
|
+
"type": "array",
|
|
30
|
+
"items": {
|
|
31
|
+
"type": "object",
|
|
32
|
+
"properties": {
|
|
33
|
+
"transition": {
|
|
34
|
+
"type": "integer",
|
|
35
|
+
"minimum": 0
|
|
36
|
+
},
|
|
37
|
+
"at": {
|
|
38
|
+
"type": "number"
|
|
39
|
+
},
|
|
40
|
+
"token": {
|
|
41
|
+
"type": "integer",
|
|
42
|
+
"minimum": 1,
|
|
43
|
+
"maximum": 9007199254740991
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"required": [
|
|
47
|
+
"transition",
|
|
48
|
+
"at",
|
|
49
|
+
"token"
|
|
50
|
+
],
|
|
51
|
+
"additionalProperties": false
|
|
52
|
+
},
|
|
53
|
+
"minItems": 0
|
|
54
|
+
},
|
|
55
|
+
"serial": {
|
|
56
|
+
"type": "integer",
|
|
57
|
+
"minimum": 0,
|
|
58
|
+
"maximum": 9007199254740991
|
|
59
|
+
},
|
|
60
|
+
"time": {
|
|
61
|
+
"type": "number"
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"required": [
|
|
65
|
+
"active",
|
|
66
|
+
"history",
|
|
67
|
+
"timers",
|
|
68
|
+
"serial",
|
|
69
|
+
"time"
|
|
70
|
+
],
|
|
71
|
+
"additionalProperties": false
|
|
72
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://jarenjs.dev/schemas/jaren-fsm/0.2/state",
|
|
4
|
+
"title": "Jaren statechart snapshot",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"properties": {
|
|
7
|
+
"active": {
|
|
8
|
+
"type": "array",
|
|
9
|
+
"items": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"minLength": 1
|
|
12
|
+
},
|
|
13
|
+
"minItems": 1,
|
|
14
|
+
"uniqueItems": true
|
|
15
|
+
},
|
|
16
|
+
"history": {
|
|
17
|
+
"type": "object",
|
|
18
|
+
"additionalProperties": {
|
|
19
|
+
"type": "array",
|
|
20
|
+
"items": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"minLength": 1
|
|
23
|
+
},
|
|
24
|
+
"minItems": 1,
|
|
25
|
+
"uniqueItems": true
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"timers": {
|
|
29
|
+
"type": "array",
|
|
30
|
+
"items": {
|
|
31
|
+
"type": "object",
|
|
32
|
+
"properties": {
|
|
33
|
+
"transition": {
|
|
34
|
+
"type": "integer",
|
|
35
|
+
"minimum": 0
|
|
36
|
+
},
|
|
37
|
+
"at": {
|
|
38
|
+
"type": "number"
|
|
39
|
+
},
|
|
40
|
+
"token": {
|
|
41
|
+
"type": "integer",
|
|
42
|
+
"minimum": 1,
|
|
43
|
+
"maximum": 9007199254740991
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"required": [
|
|
47
|
+
"transition",
|
|
48
|
+
"at",
|
|
49
|
+
"token"
|
|
50
|
+
],
|
|
51
|
+
"additionalProperties": false
|
|
52
|
+
},
|
|
53
|
+
"minItems": 0
|
|
54
|
+
},
|
|
55
|
+
"serial": {
|
|
56
|
+
"type": "integer",
|
|
57
|
+
"minimum": 0,
|
|
58
|
+
"maximum": 9007199254740991
|
|
59
|
+
},
|
|
60
|
+
"time": {
|
|
61
|
+
"type": "number"
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"required": [
|
|
65
|
+
"active",
|
|
66
|
+
"history",
|
|
67
|
+
"timers",
|
|
68
|
+
"serial",
|
|
69
|
+
"time"
|
|
70
|
+
],
|
|
71
|
+
"additionalProperties": false
|
|
72
|
+
}
|