@orkestrel/workflow 0.0.17 → 0.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -3
- package/dist/src/browser/index.d.ts +16 -12
- package/dist/src/browser/index.js +13 -9
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/core/index.cjs +267 -230
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +657 -547
- package/dist/src/core/index.d.ts +657 -547
- package/dist/src/core/index.js +267 -230
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +5 -3
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +7 -5
- package/dist/src/server/index.d.ts +7 -5
- package/dist/src/server/index.js +5 -3
- package/dist/src/server/index.js.map +1 -1
- package/package.json +20 -21
package/dist/src/core/index.cjs
CHANGED
|
@@ -7,11 +7,12 @@ let _orkestrel_timeout = require("@orkestrel/timeout");
|
|
|
7
7
|
let _orkestrel_queue = require("@orkestrel/queue");
|
|
8
8
|
//#region src/core/errors.ts
|
|
9
9
|
/**
|
|
10
|
-
* Represents an error
|
|
10
|
+
* Represents an error the workflow runtime raises for an operation it refuses — a
|
|
11
|
+
* {@link WorkflowErrorCode} (`TRANSITION`, `RESTORE`, `MUTATION`, `SCHEDULE`, or `INVARIANT`)
|
|
12
|
+
* beside an optional `context` naming the node or the parameter at fault.
|
|
11
13
|
*
|
|
12
14
|
* @remarks
|
|
13
|
-
*
|
|
14
|
-
* offending node id / status / parameter. Raised for an illegal lifecycle transition
|
|
15
|
+
* Raised for an illegal lifecycle transition
|
|
15
16
|
* (`TRANSITION`), a structurally invalid {@link import('./types.js').WorkflowSnapshot}
|
|
16
17
|
* boundary (`RESTORE`), a refused structural/activity edit (`MUTATION`), a host
|
|
17
18
|
* schedule refused before arming because the caller's `signal` is not a native
|
|
@@ -52,10 +53,14 @@ function isWorkflowError(value) {
|
|
|
52
53
|
}
|
|
53
54
|
//#endregion
|
|
54
55
|
//#region src/core/constants.ts
|
|
55
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* Names the default {@link import('./types.js').WorkflowDefinition.bail}, `false` — the graceful
|
|
58
|
+
* policy that records a leaf failure and finishes every phase.
|
|
59
|
+
*/
|
|
56
60
|
var DEFAULT_BAIL = false;
|
|
57
61
|
/**
|
|
58
|
-
* Lists every {@link LifecycleStatus} value, frozen — the vocabulary every tier draws from
|
|
62
|
+
* Lists every {@link LifecycleStatus} value, frozen — the vocabulary every tier draws from,
|
|
63
|
+
* in the order `pending`, `running`, `completed`, `failed`, `skipped`, `stopped`.
|
|
59
64
|
*
|
|
60
65
|
* @remarks
|
|
61
66
|
* Ordered pending → running → terminal (`completed` / `failed` / `skipped` /
|
|
@@ -71,8 +76,8 @@ var LIFECYCLE_STATUSES = Object.freeze([
|
|
|
71
76
|
"stopped"
|
|
72
77
|
]);
|
|
73
78
|
/**
|
|
74
|
-
* Lists the {@link LifecycleStatus} values
|
|
75
|
-
*
|
|
79
|
+
* Lists the terminal {@link LifecycleStatus} values, frozen — `completed`, `failed`, `skipped`,
|
|
80
|
+
* and `stopped`, each a state a node never transitions out of.
|
|
76
81
|
*
|
|
77
82
|
* @remarks
|
|
78
83
|
* The source of truth behind {@link import('./helpers.js').isTerminalStatus}.
|
|
@@ -117,30 +122,32 @@ var TASK_TRANSITIONS = Object.freeze({
|
|
|
117
122
|
/**
|
|
118
123
|
* Names the default per-phase task concurrency the {@link import('./factories.js').createWorkflowRunner}
|
|
119
124
|
* runner applies when a {@link import('./types.js').PhaseDefinition} omits its `concurrency`
|
|
120
|
-
* throttle — a cap that is effectively unbounded for any realistic phase.
|
|
125
|
+
* throttle — `1024`, a cap that is effectively unbounded for any realistic phase.
|
|
121
126
|
*
|
|
122
127
|
* @remarks
|
|
123
|
-
* The determinism principle fixes that a phase's tasks run
|
|
128
|
+
* The determinism principle fixes that a phase's tasks run concurrently; `concurrency` is
|
|
124
129
|
* only an optional resource throttle (max-in-flight). With none declared, the runner runs
|
|
125
130
|
* all of a phase's tasks at once — modelled as this finite cap so the value flows straight
|
|
126
131
|
* into the substrate {@link import('./types.js').RunnerInterface}'s `concurrency` (which
|
|
127
132
|
* expects a positive integer) without a special unbounded branch. No realistic phase
|
|
128
133
|
* declares enough tasks to reach it, so it behaves as "run them all".
|
|
129
134
|
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
135
|
+
* why `1024` and not a huge sentinel like `1_000_000`: the backing `@orkestrel/queue` Runner
|
|
136
|
+
* eagerly spawns one parked worker loop per concurrency unit at construction, so this default
|
|
132
137
|
* must be a value whose eager allocation cost is negligible for every default-concurrency
|
|
133
138
|
* phase — a million-unit default meant ~1e6 promise/closure allocations per such phase. A
|
|
134
|
-
* phase may still
|
|
139
|
+
* phase may still declare a larger explicit `concurrency` and pays that allocation knowingly.
|
|
135
140
|
*/
|
|
136
141
|
var DEFAULT_PHASE_CONCURRENCY = 1024;
|
|
137
142
|
/**
|
|
138
|
-
* Names the largest delay representable by the host timer APIs without overflow or clamping
|
|
143
|
+
* Names the largest delay representable by the host timer APIs without overflow or clamping,
|
|
144
|
+
* `2_147_483_647` milliseconds.
|
|
139
145
|
*/
|
|
140
146
|
var MAX_TIMER_MS = 2147483647;
|
|
141
147
|
/**
|
|
142
148
|
* Lists the {@link WorkflowEventMap} / {@link PhaseEventMap} events that make a durable observer
|
|
143
|
-
* re-persist the live tree, frozen
|
|
149
|
+
* re-persist the live tree, frozen — `start`, `complete`, `fail`, `skip`, `stop`, `move`, and
|
|
150
|
+
* `update`.
|
|
144
151
|
*
|
|
145
152
|
* @remarks
|
|
146
153
|
* The two maps carry the same event names, so one list serves both tiers. It is the source of
|
|
@@ -159,7 +166,8 @@ var PERSISTED_NODE_EVENTS = Object.freeze([
|
|
|
159
166
|
"update"
|
|
160
167
|
]);
|
|
161
168
|
/**
|
|
162
|
-
* Lists the {@link TaskEventMap} events that make a durable observer re-persist the live tree,
|
|
169
|
+
* Lists the {@link TaskEventMap} events that make a durable observer re-persist the live tree,
|
|
170
|
+
* frozen — `start`, `complete`, `fail`, `skip`, `stop`, `report`, and `pulse`.
|
|
163
171
|
*
|
|
164
172
|
* @remarks
|
|
165
173
|
* The leaf counterpart of {@link PERSISTED_NODE_EVENTS}, and the source of truth behind the task
|
|
@@ -230,8 +238,8 @@ function isTaskFailure(value) {
|
|
|
230
238
|
* The discriminator behind the overloaded
|
|
231
239
|
* {@link import('./types.js').WorkflowRunnerInterface.execute}: a
|
|
232
240
|
* {@link import('./types.js').WorkflowInterface} is the only one of the two carrying `destroyed`
|
|
233
|
-
* (
|
|
234
|
-
* {@link import('./types.js').WorkflowDefinition})
|
|
241
|
+
* (runtime-only, never a field on the pure-JSON
|
|
242
|
+
* {@link import('./types.js').WorkflowDefinition}) and a callable `snapshot`. Requiring both is
|
|
235
243
|
* sturdier than `destroyed` alone — a definition could coincidentally carry a `destroyed` field as
|
|
236
244
|
* arbitrary data, and pairing it with a function-typed `snapshot` narrows to the actual entity
|
|
237
245
|
* shape without an `as`. It reads a live class instance, so it tests object identity rather than a
|
|
@@ -423,7 +431,7 @@ function isTaskActivityInput(value) {
|
|
|
423
431
|
* Tests whether an unknown value is valid persisted task activity.
|
|
424
432
|
*
|
|
425
433
|
* @remarks
|
|
426
|
-
* The persisted counterpart of {@link isTaskActivityInput}: the same frame plus the
|
|
434
|
+
* The persisted counterpart of {@link isTaskActivityInput}: the same frame plus the required
|
|
427
435
|
* `operations`, `constraints`, and a finite non-negative `updated` stamp, because a stored frame
|
|
428
436
|
* has already been accepted and normalized. Total — a hostile prototype or accessor answers
|
|
429
437
|
* `false` rather than throwing.
|
|
@@ -495,11 +503,11 @@ function captureWorkflowOptions(options) {
|
|
|
495
503
|
});
|
|
496
504
|
}
|
|
497
505
|
/**
|
|
498
|
-
* Tests whether a {@link LifecycleStatus} is
|
|
499
|
-
*
|
|
506
|
+
* Tests whether a {@link LifecycleStatus} is terminal — `completed`, `failed`, `skipped`, or
|
|
507
|
+
* `stopped`, the states a node never transitions out of.
|
|
500
508
|
*
|
|
501
509
|
* @remarks
|
|
502
|
-
* The
|
|
510
|
+
* The one terminal check across every tier (AGENTS.md § Design laws, "one concept, one term"):
|
|
503
511
|
* a task, a phase, and a workflow share the same {@link LifecycleStatus} vocabulary, so a
|
|
504
512
|
* single predicate covers them — {@link derivePhaseStatus} and {@link deriveWorkflowStatus}
|
|
505
513
|
* both consult it to tell a settled node from an in-flight one. It reads the terminal set from
|
|
@@ -514,12 +522,13 @@ function isTerminalStatus(status) {
|
|
|
514
522
|
return TERMINAL_STATUSES.includes(status);
|
|
515
523
|
}
|
|
516
524
|
/**
|
|
517
|
-
* Tests whether a driving run must stop giving a workflow
|
|
525
|
+
* Tests whether a driving run must stop giving a workflow — or one forced phase of it —
|
|
526
|
+
* more work.
|
|
518
527
|
*
|
|
519
528
|
* @remarks
|
|
520
529
|
* The halt gate a {@link import('./WorkflowRunner.js').WorkflowRunner} consults before starting a
|
|
521
530
|
* phase, before dispatching a task, and after every cooperative gate. A workflow is halted after
|
|
522
|
-
* its derived status is terminal but
|
|
531
|
+
* its derived status is terminal but not `completed` — a `bail: true` failure, a caller's own
|
|
523
532
|
* graceful `stop()`, or a forced `skip`. `completed` is excluded deliberately: a workflow that
|
|
524
533
|
* completed vacuously is settled, not halted, and the distinction is what keeps the run from
|
|
525
534
|
* sweeping a finished tree. When a `phase` is supplied, its own forced `skipped` / `stopped` halts
|
|
@@ -542,11 +551,11 @@ function isHalted(workflow, phase) {
|
|
|
542
551
|
return isTerminalStatus(status) && status !== "completed" || phase?.status === "skipped" || phase?.status === "stopped";
|
|
543
552
|
}
|
|
544
553
|
/**
|
|
545
|
-
* Tests whether forcing a workflow `stopped` would still record
|
|
554
|
+
* Tests whether forcing a workflow `stopped` would still record the cancellation.
|
|
546
555
|
*
|
|
547
556
|
* @remarks
|
|
548
557
|
* `stop()` is a no-op after a workflow's status becomes terminal, so a run that must record a
|
|
549
|
-
* cancellation forces it only while this holds. It is
|
|
558
|
+
* cancellation forces it only while this holds. It is not the negation of
|
|
550
559
|
* {@link isTerminalStatus}: `completed` and `skipped` both pass, because a run-level cancel that
|
|
551
560
|
* lands on a vacuously-completed or fully-skipped tree still records `stopped` as the outcome the
|
|
552
561
|
* caller asked for. Only an already-`failed` or already-`stopped` workflow has a terminal state
|
|
@@ -572,7 +581,7 @@ function isStoppable(workflow) {
|
|
|
572
581
|
* @remarks
|
|
573
582
|
* A run that walked every phase and still derives `pending` executed nothing — zero phases, or
|
|
574
583
|
* every phase empty — so it is vacuously done and the run settles it `completed`. Gated on
|
|
575
|
-
*
|
|
584
|
+
* exactly `pending` so a real `completed`, a `bail: true` `failed`, a `stopped`, or a derived
|
|
576
585
|
* `skipped` is never overridden. The tree-is-empty half of the rule is
|
|
577
586
|
* {@link WorkflowInterface.complete}'s own guard, which refuses a pending tree that still holds
|
|
578
587
|
* tasks.
|
|
@@ -596,7 +605,7 @@ function isCompletable(workflow) {
|
|
|
596
605
|
* running task's folded signal, and only two of them mean "skip this task": the task's own
|
|
597
606
|
* `signal` (its `stop` / `skip`), and the unit or run signal (a sibling fail-fast under
|
|
598
607
|
* `bail: true`, or a run-level abort / timeout / budget / `destroy`). A bare per-attempt timeout
|
|
599
|
-
* fires
|
|
608
|
+
* fires neither — it aborts only the deadline portion of the attempt signal — so it stays a
|
|
600
609
|
* retryable failure of that attempt instead of skipping the leaf and losing the recorded fault.
|
|
601
610
|
* Read fresh at each call so a cancel that lands mid-dispatch is seen.
|
|
602
611
|
*
|
|
@@ -618,7 +627,7 @@ function isSkipping(task, controller, runSignal) {
|
|
|
618
627
|
*
|
|
619
628
|
* @remarks
|
|
620
629
|
* A retried task is re-dispatched while an earlier attempt's handler may still be resolving, so
|
|
621
|
-
* every settlement path re-checks ownership before touching the leaf. Ownership needs
|
|
630
|
+
* every settlement path re-checks ownership before touching the leaf. Ownership needs both
|
|
622
631
|
* halves: the run-local `owners` ledger must still name this attempt, and the live task's own
|
|
623
632
|
* `attempts` tally must still match it. A superseded attempt reads `false` and returns without
|
|
624
633
|
* recording anything, so a late resolution can never overwrite the newer attempt's outcome.
|
|
@@ -639,14 +648,14 @@ function ownsAttempt(owners, task, attempt) {
|
|
|
639
648
|
return owners.get(task.id) === attempt && task.attempts === attempt;
|
|
640
649
|
}
|
|
641
650
|
/**
|
|
642
|
-
* Derives a phase's status from its tasks' statuses
|
|
643
|
-
* is an order-insensitive reduction).
|
|
651
|
+
* Derives a phase's status from its tasks' statuses, the most severe terminal status winning
|
|
652
|
+
* (tasks are concurrent, so this is an order-insensitive reduction).
|
|
644
653
|
*
|
|
645
654
|
* @remarks
|
|
646
655
|
* The truth table (most-severe terminal wins; `bail`-agnostic — a phase surfaces a
|
|
647
656
|
* task failure as `failed` so the workflow's `bail` policy can decide):
|
|
648
657
|
* - no tasks ⇒ `pending`.
|
|
649
|
-
* - any task `running`,
|
|
658
|
+
* - any task `running`, or a mix of started-and-unsettled tasks (some non-`pending`
|
|
650
659
|
* but not all terminal) ⇒ `running`.
|
|
651
660
|
* - every task `pending` ⇒ `pending`.
|
|
652
661
|
* - all terminal: any `failed` ⇒ `failed`; else any `stopped` ⇒ `stopped`; else any
|
|
@@ -670,23 +679,24 @@ function derivePhaseStatus(tasks) {
|
|
|
670
679
|
}
|
|
671
680
|
/**
|
|
672
681
|
* Derives a workflow's status from its phases' {@link PhaseDerivation}s — each phase's status
|
|
673
|
-
* paired with the
|
|
674
|
-
* failure outcome is
|
|
675
|
-
*
|
|
682
|
+
* paired with the effective `bail` it ran under (`phase.bail ?? workflow.bail`) — so the
|
|
683
|
+
* failure outcome is aware of each phase's own policy, and `failed` is reachable only where
|
|
684
|
+
* that policy is `true` (phases are sequential, but the derivation is an order-insensitive
|
|
685
|
+
* reduction over the settled set).
|
|
676
686
|
*
|
|
677
687
|
* @remarks
|
|
678
688
|
* `bail` is a per-phase override, so it is carried on each
|
|
679
|
-
* {@link PhaseDerivation} rather than passed as one scalar. It is the
|
|
689
|
+
* {@link PhaseDerivation} rather than passed as one scalar. It is the only axis that changes
|
|
680
690
|
* the failure outcome, decided per phase:
|
|
681
691
|
* - **A `failed` phase whose effective `bail` is `true` (halt)** propagates ⇒ the workflow is
|
|
682
692
|
* `failed` (the database-transaction halt) — even when the workflow default is graceful.
|
|
683
|
-
* - **A `failed` phase whose effective `bail` is `false` (graceful)** is
|
|
684
|
-
* failure — it folds into completion like a settled phase. A graceful failed phase
|
|
693
|
+
* - **A `failed` phase whose effective `bail` is `false` (graceful)** is data, not a workflow
|
|
694
|
+
* failure — it folds into completion like a settled phase. A graceful failed phase never
|
|
685
695
|
* makes the workflow `failed` — even when the workflow default is strict.
|
|
686
696
|
*
|
|
687
697
|
* The rest of the table is shared:
|
|
688
698
|
* - no phases ⇒ `pending`.
|
|
689
|
-
* - any phase `running`,
|
|
699
|
+
* - any phase `running`, or a mix of started-and-unsettled phases (some non-`pending`
|
|
690
700
|
* but not all terminal) ⇒ `running`.
|
|
691
701
|
* - every phase `pending` ⇒ `pending`.
|
|
692
702
|
* - all terminal (a `failed` phase counts as terminal here): any `stopped` ⇒ `stopped`; else
|
|
@@ -706,18 +716,19 @@ function deriveWorkflowStatus(phases) {
|
|
|
706
716
|
return "skipped";
|
|
707
717
|
}
|
|
708
718
|
/**
|
|
709
|
-
* Derives the
|
|
710
|
-
* the index of the first entry in the contiguous trailing run of `pending` entries
|
|
719
|
+
* Derives the pending-suffix boundary of a positional list of {@link LifecycleStatus}es —
|
|
720
|
+
* the index of the first entry in the contiguous trailing run of `pending` entries, or the
|
|
721
|
+
* list's length where it has none.
|
|
711
722
|
*
|
|
712
723
|
* @remarks
|
|
713
724
|
* The native, hook-free replacement for a runner-installed cursor: a
|
|
714
725
|
* {@link import('./types.js').WorkflowInterface}'s `add` / `remove` / `move` / `update`
|
|
715
726
|
* reads this over its live phases' statuses to decide which positions are safe to edit.
|
|
716
|
-
* Because entries run
|
|
717
|
-
* already-started entry forms a contiguous
|
|
727
|
+
* Because entries run sequentially (phases sequential, AGENTS determinism), every
|
|
728
|
+
* already-started entry forms a contiguous leading prefix and every still-`pending`
|
|
718
729
|
* entry forms the trailing suffix — so the boundary is the count of leading
|
|
719
730
|
* non-`pending` entries: the index of the first `pending` entry, or the full length when
|
|
720
|
-
* none is `pending` (nothing is safely editable). A `pending` container's entries are
|
|
731
|
+
* none is `pending` (nothing is safely editable). A `pending` container's entries are all
|
|
721
732
|
* `pending`, so the boundary is `0` and every position is naturally accepted — callers
|
|
722
733
|
* need no special case for that.
|
|
723
734
|
*
|
|
@@ -753,7 +764,8 @@ function canTransitionTask(from, to) {
|
|
|
753
764
|
return TASK_TRANSITIONS[from].includes(to);
|
|
754
765
|
}
|
|
755
766
|
/**
|
|
756
|
-
* Resolves a task's runtime silence window against its workflow default
|
|
767
|
+
* Resolves a task's runtime silence window against its workflow default, to a host-safe
|
|
768
|
+
* `1..MAX_TIMER_MS` window or to `undefined` where the task disables it.
|
|
757
769
|
*
|
|
758
770
|
* @param value - The task-level override; any present non-positive or non-finite value disables
|
|
759
771
|
* @param fallback - The workflow-level default
|
|
@@ -820,7 +832,7 @@ function errorToMessage(error) {
|
|
|
820
832
|
*
|
|
821
833
|
* @remarks
|
|
822
834
|
* The shared leaf behind {@link import('./phases/Phase.js').Phase} and
|
|
823
|
-
* {@link import('./Workflow.js').Workflow}'s own `#failure` — each gathers
|
|
835
|
+
* {@link import('./Workflow.js').Workflow}'s own `#failure` — each gathers its tier's
|
|
824
836
|
* results (a phase's own settled tasks, a workflow's flattened `results()`) and feeds
|
|
825
837
|
* them here; the tier-local method keeps the invariant throw (a derived `failed`
|
|
826
838
|
* status means a failing result exists) because throwing on `undefined` is
|
|
@@ -860,7 +872,7 @@ function buildWorkflowContext(node) {
|
|
|
860
872
|
* Builds a {@link PhaseContext} — a phase's own identity plus a back-reference to its
|
|
861
873
|
* workflow — from the parent {@link WorkflowContext} and the phase node's identity.
|
|
862
874
|
*
|
|
863
|
-
* @param workflow - The parent workflow context (the lineage pointer
|
|
875
|
+
* @param workflow - The parent workflow context (the lineage pointer up the tree)
|
|
864
876
|
* @param node - The phase's identity (`id` / `name` / optional `description`)
|
|
865
877
|
* @returns The {@link PhaseContext}
|
|
866
878
|
*/
|
|
@@ -875,7 +887,7 @@ function buildPhaseContext(workflow, node) {
|
|
|
875
887
|
* (and, transitively, its workflow) — from the parent {@link PhaseContext} and the task
|
|
876
888
|
* node's identity.
|
|
877
889
|
*
|
|
878
|
-
* @param phase - The parent phase context (carrying the full lineage
|
|
890
|
+
* @param phase - The parent phase context (carrying the full lineage up the tree)
|
|
879
891
|
* @param node - The task's identity (`id` / `name` / optional `description`)
|
|
880
892
|
* @returns The {@link TaskContext}
|
|
881
893
|
*/
|
|
@@ -886,9 +898,9 @@ function buildTaskContext(phase, node) {
|
|
|
886
898
|
});
|
|
887
899
|
}
|
|
888
900
|
/**
|
|
889
|
-
* Converts a {@link WorkflowDefinition} into an
|
|
890
|
-
* node `pending`, no results, empty metadata — so the live W-b tree has
|
|
891
|
-
* path
|
|
901
|
+
* Converts a {@link WorkflowDefinition} into an initial {@link WorkflowSnapshot} — every
|
|
902
|
+
* node `pending`, no results, empty metadata — so the live W-b tree has one construction
|
|
903
|
+
* path, snapshot-driven, for a fresh build and for a restore alike.
|
|
892
904
|
*
|
|
893
905
|
* @remarks
|
|
894
906
|
* The structural fields (`id` / `name` / `description` + the ordered phases / tasks)
|
|
@@ -897,20 +909,20 @@ function buildTaskContext(phase, node) {
|
|
|
897
909
|
* `retries` / `timeout` (persisted on the {@link TaskSnapshot}, like `bail` / `concurrency`,
|
|
898
910
|
* so a restore + a {@link import('./types.js').WorkflowOptions.functions} registry resumes
|
|
899
911
|
* real work). The `bail` policy carries over — at the
|
|
900
|
-
* workflow tier
|
|
901
|
-
*
|
|
912
|
+
* workflow tier and, per phase, the
|
|
913
|
+
* effective policy (`phase.bail ?? workflowBail`) on each {@link PhaseSnapshot} — so the seeded
|
|
902
914
|
* snapshot is self-contained; a fresh seed has no `override`. `created` / `updated` are stamped at that point.
|
|
903
915
|
* {@link import('./factories.js').createWorkflow} builds from this.
|
|
904
916
|
*
|
|
905
|
-
* The optional `bail` override is the
|
|
917
|
+
* The optional `bail` override is the effective workflow policy the tree will run under
|
|
906
918
|
* (`createWorkflow` / the runner resolve `options.bail ?? definition.bail ?? DEFAULT_BAIL` and
|
|
907
|
-
* pass it here), so an `options.bail` override reaches
|
|
919
|
+
* pass it here), so an `options.bail` override reaches both the workflow tier and the
|
|
908
920
|
* inheritance default of every phase that declares no `bail` of its own — otherwise the
|
|
909
921
|
* per-phase seeds would silently ignore the override. Omitted ⇒ the definition's own `bail`
|
|
910
922
|
* (defaulting to the graceful {@link import('./constants.js').DEFAULT_BAIL}).
|
|
911
923
|
*
|
|
912
924
|
* @param definition - The workflow definition to seed from
|
|
913
|
-
* @param bail - The
|
|
925
|
+
* @param bail - The effective workflow bail to seed both tiers with (defaults to the definition's)
|
|
914
926
|
* @returns An initial, all-`pending` {@link WorkflowSnapshot}
|
|
915
927
|
*/
|
|
916
928
|
function definitionToSnapshot(definition, bail) {
|
|
@@ -932,7 +944,7 @@ function definitionToSnapshot(definition, bail) {
|
|
|
932
944
|
* {@link PhaseSnapshot} — the per-phase step of {@link definitionToSnapshot}.
|
|
933
945
|
*
|
|
934
946
|
* @remarks
|
|
935
|
-
* The snapshot persists the
|
|
947
|
+
* The snapshot persists the effective failure policy this phase runs under: the phase's own
|
|
936
948
|
* `bail` when it declares one, else the `workflowBail` it inherits — so a restore reinstates
|
|
937
949
|
* the same per-phase policy without a silent default (`effectiveBail = phase.bail ?? workflowBail`).
|
|
938
950
|
* `concurrency` (the resource throttle) carries over verbatim, omitted when undefined.
|
|
@@ -1062,7 +1074,7 @@ function recoverWorkflowSnapshot(snapshot) {
|
|
|
1062
1074
|
*
|
|
1063
1075
|
* @remarks
|
|
1064
1076
|
* The equality rule a lineage check needs: two descriptions match when they are the same value
|
|
1065
|
-
*
|
|
1077
|
+
* and that value is either a string or genuine absence. Anything else — a number, an object, a
|
|
1066
1078
|
* `null` — never matches, even against itself, so a lineage stamped with a non-string description
|
|
1067
1079
|
* is rejected rather than silently accepted.
|
|
1068
1080
|
*
|
|
@@ -1085,7 +1097,7 @@ function matchesDescription(left, right) {
|
|
|
1085
1097
|
*
|
|
1086
1098
|
* @remarks
|
|
1087
1099
|
* The four arguments are the result and the three snapshot nodes it claims to belong to, read
|
|
1088
|
-
* from the
|
|
1100
|
+
* from the outside in: a {@link TaskResult} is self-describing, so restoring one is only safe
|
|
1089
1101
|
* when every identity it carries agrees with the tree it was found in. It checks the exact key
|
|
1090
1102
|
* set at each level, that `status` equals the owning task's, and that the `task` / `phase` /
|
|
1091
1103
|
* `workflow` contexts — including the nested `task.phase.workflow` lineage — carry the same `id`,
|
|
@@ -1312,7 +1324,7 @@ function scheduleHost(start, signal) {
|
|
|
1312
1324
|
* {@link import('./Scheduler.js').Scheduler}, both Node primitives, and every browser backend's
|
|
1313
1325
|
* `delay` and macrotask fallback route here, so the timer is armed and cleared in one place. It
|
|
1314
1326
|
* composes {@link scheduleHost}, which owns listener safety, the cancellation race, the exact
|
|
1315
|
-
* caller reason, and once-only settlement. It does
|
|
1327
|
+
* caller reason, and once-only settlement. It does not validate `ms`: the value passes straight to
|
|
1316
1328
|
* the host `setTimeout`, which clamps a negative value or `NaN` to about zero, so an
|
|
1317
1329
|
* out-of-domain `ms` resumes on the next host turn rather than throwing. Pass a non-negative
|
|
1318
1330
|
* finite `ms`.
|
|
@@ -1335,10 +1347,10 @@ function delayHost(ms, signal) {
|
|
|
1335
1347
|
}
|
|
1336
1348
|
/**
|
|
1337
1349
|
* Parks until `signal` aborts — a promise-parked wait, never a timer or
|
|
1338
|
-
* busy-loop, that
|
|
1350
|
+
* busy-loop, that resolves on the abort event and never rejects.
|
|
1339
1351
|
*
|
|
1340
1352
|
* @remarks
|
|
1341
|
-
* Resolves
|
|
1353
|
+
* Resolves immediately when `signal` is already aborted; otherwise attaches a one-shot
|
|
1342
1354
|
* `abort` listener and resolves when it fires, removing the listener either way. The
|
|
1343
1355
|
* shared leaf behind the duplicate abort-wiring an execution engine otherwise hand-rolls
|
|
1344
1356
|
* at every fold point.
|
|
@@ -1373,9 +1385,9 @@ function parkSignal(signal) {
|
|
|
1373
1385
|
* (`task` / `tasks`, `phase` / `phases`). The `Map`'s insertion order is the single source of
|
|
1374
1386
|
* positional truth; `add` and `move` rebuild it through the pure
|
|
1375
1387
|
* {@link import('./helpers.js').insertEntry} / {@link import('./helpers.js').moveEntry} leaves.
|
|
1376
|
-
* - **Gated mutation API.** `append` is the build-time wiring path and
|
|
1388
|
+
* - **Gated mutation API.** `append` is the build-time wiring path and throws on a
|
|
1377
1389
|
* duplicate id; `add` / `remove` / `move` / `update` return a graceful `MUTATION`
|
|
1378
|
-
* {@link WorkflowError} failure instead. Gating reads
|
|
1390
|
+
* {@link WorkflowError} failure instead. Gating reads only the target's own existence, `pending`
|
|
1379
1391
|
* status, id, and bounds — a container's own status is the owning entity's gate, applied before
|
|
1380
1392
|
* it delegates here.
|
|
1381
1393
|
* - **Event-free.** A purely structural container; the entity that owns it emits on success.
|
|
@@ -1468,13 +1480,13 @@ var Collection = class {
|
|
|
1468
1480
|
* browser and Node.
|
|
1469
1481
|
*
|
|
1470
1482
|
* @remarks
|
|
1471
|
-
* - **Cross-environment.** Uses
|
|
1483
|
+
* - **Cross-environment.** Uses only `setTimeout` / `clearTimeout` — universally
|
|
1472
1484
|
* available. It deliberately avoids env-specific fast paths (`setImmediate`,
|
|
1473
1485
|
* `scheduler.yield`, `requestAnimationFrame`, `node:timers/promises`,
|
|
1474
1486
|
* `MessageChannel`); those belong to the environment backends, built with the
|
|
1475
1487
|
* agent loop that consumes them.
|
|
1476
1488
|
* - **`yield` is a macrotask host-turn, not a microtask.** `yield()` waits on a
|
|
1477
|
-
* `setTimeout(0)`,
|
|
1489
|
+
* `setTimeout(0)`, not `queueMicrotask`. A microtask drains before the host
|
|
1478
1490
|
* regains control, so it would not actually let pending I/O, timers, or
|
|
1479
1491
|
* rendering run — it only defers within the current task. A zero-delay timer is
|
|
1480
1492
|
* the correct cross-environment "give the host a turn".
|
|
@@ -1499,7 +1511,7 @@ var Collection = class {
|
|
|
1499
1511
|
var Scheduler = class {
|
|
1500
1512
|
/**
|
|
1501
1513
|
* Yields control back to the host so other tasks (I/O, timers, rendering) can
|
|
1502
|
-
* run, then resumes — a macrotask turn through `setTimeout(0)` (
|
|
1514
|
+
* run, then resumes — a macrotask turn through `setTimeout(0)` (not a microtask,
|
|
1503
1515
|
* which would resume before the host regains control).
|
|
1504
1516
|
*/
|
|
1505
1517
|
yield(options) {
|
|
@@ -1754,28 +1766,28 @@ var phaseUpdateShape = (0, _orkestrel_contract.objectShape)({
|
|
|
1754
1766
|
//#region src/core/stores/DatabaseWorkflowStore.ts
|
|
1755
1767
|
/**
|
|
1756
1768
|
* Implements a {@link WorkflowStoreInterface} backed by one table of the `databases` layer — a
|
|
1757
|
-
* workflow's durable run
|
|
1769
|
+
* workflow's durable run state is a row, so persistence reduces to keyed point-access
|
|
1758
1770
|
* (`get` / `set` / `delete`) over a `TableInterface`, the driver-pluggable twin of the
|
|
1759
1771
|
* plain-`Map` {@link import('./MemoryWorkflowStore.js').MemoryWorkflowStore}.
|
|
1760
1772
|
*
|
|
1761
1773
|
* @remarks
|
|
1762
1774
|
* The store is driver-agnostic: it holds a single {@link TableInterface} whose backend
|
|
1763
1775
|
* (memory, JSON, SQLite, IndexedDB) is chosen by whoever builds it (the factories), so a
|
|
1764
|
-
* JSON / SQLite / IndexedDB backend swaps in
|
|
1776
|
+
* JSON / SQLite / IndexedDB backend swaps in without touching the runner or the entity tree
|
|
1765
1777
|
* — the same seam as `@orkestrel/queue`'s `DatabaseQueueStore`.
|
|
1766
1778
|
* The driver defaults to memory ({@link import('../factories.js').createDatabaseWorkflowStore}
|
|
1767
|
-
* passes `createMemoryDriver()`), so it
|
|
1779
|
+
* passes `createMemoryDriver()`), so it also works in memory out of the box; you opt into the
|
|
1768
1780
|
* durable plumbing by passing a JSON / SQLite / IndexedDB driver.
|
|
1769
1781
|
*
|
|
1770
|
-
* The {@link WorkflowSnapshot} is stored as
|
|
1782
|
+
* The {@link WorkflowSnapshot} is stored as one opaque JSON column — the table is a row of
|
|
1771
1783
|
* `{ id; snapshot }` ({@link WorkflowSnapshotRow}), the snapshot the whole JSON blob (a `rawShape`
|
|
1772
1784
|
* column the factory builds) — exactly as `DatabaseQueueStore` stores its `input`. The snapshot is
|
|
1773
|
-
* already a
|
|
1785
|
+
* already a complete, self-contained, pure-JSON payload, so storing it whole is lossless and
|
|
1774
1786
|
* sidesteps a TS2589 instantiation-depth blow-up: a structured multi-column table would force the
|
|
1775
1787
|
* contract to `Infer` the deeply-nested snapshot shape (workflow → phases → tasks → results),
|
|
1776
1788
|
* tripping the compiler — one JSON column keeps the row type flat (`snapshot` reads back as `unknown`).
|
|
1777
1789
|
*
|
|
1778
|
-
* - **`set(snapshot)` upserts under the snapshot's
|
|
1790
|
+
* - **`set(snapshot)` upserts under the snapshot's own `id`** (no separate id param) — it writes
|
|
1779
1791
|
* the row `{ id: snapshot.id, snapshot }`.
|
|
1780
1792
|
* - **`get(id)` resolves the stored snapshot for an id**, owning and narrowing the opaque JSON
|
|
1781
1793
|
* column back to a {@link WorkflowSnapshot} through
|
|
@@ -1785,9 +1797,9 @@ var phaseUpdateShape = (0, _orkestrel_contract.objectShape)({
|
|
|
1785
1797
|
* differs from the requested key rejects with normalized `RESTORE` evidence.
|
|
1786
1798
|
* - **`delete(id)` drops a snapshot by id**; an absent id is a no-op (no throw).
|
|
1787
1799
|
*
|
|
1788
|
-
*
|
|
1800
|
+
* unlike the server package's `SessionStoreInterface` there is no
|
|
1789
1801
|
* idle-TTL / eviction — a persisted run-state is durable orchestration state that lives until an
|
|
1790
|
-
* explicit `delete`. The public surface is
|
|
1802
|
+
* explicit `delete`. The public surface is exactly `get` / `set` / `delete` — no extra members (the
|
|
1791
1803
|
* guide's method bijection with {@link WorkflowStoreInterface}). Restore stays a caller concern: read a
|
|
1792
1804
|
* snapshot back and rebuild the live tree with {@link import('../factories.js').createRestoredWorkflow}.
|
|
1793
1805
|
*
|
|
@@ -1821,7 +1833,7 @@ var DatabaseWorkflowStore = class {
|
|
|
1821
1833
|
if (row === void 0) return void 0;
|
|
1822
1834
|
return cloneWorkflowSnapshot(row.snapshot, id);
|
|
1823
1835
|
}
|
|
1824
|
-
/** Inserts or replaces under the snapshot's
|
|
1836
|
+
/** Inserts or replaces under the snapshot's own `id` (no separate id param) — the row is `{ id, snapshot }`. */
|
|
1825
1837
|
async set(snapshot) {
|
|
1826
1838
|
const owned = cloneWorkflowSnapshot(snapshot);
|
|
1827
1839
|
await this.#table.set({
|
|
@@ -1838,27 +1850,24 @@ var DatabaseWorkflowStore = class {
|
|
|
1838
1850
|
//#region src/core/stores/MemoryWorkflowStore.ts
|
|
1839
1851
|
/**
|
|
1840
1852
|
* Implements the in-memory {@link WorkflowStoreInterface} — a process-lifetime `Map` of
|
|
1841
|
-
* {@link WorkflowSnapshot}s keyed by workflow id, the
|
|
1842
|
-
* {@link import('../factories.js').createMemoryWorkflowStore} builds.
|
|
1853
|
+
* {@link WorkflowSnapshot}s keyed by workflow id, the default store
|
|
1854
|
+
* {@link import('../factories.js').createMemoryWorkflowStore} builds. It expires nothing: a
|
|
1855
|
+
* persisted snapshot lives until an explicit `delete`.
|
|
1843
1856
|
*
|
|
1844
1857
|
* @remarks
|
|
1845
1858
|
* A plain `Map<string, WorkflowSnapshot>` (the snapshot is already pure,
|
|
1846
|
-
* self-contained JSON, so no encoding is needed for the memory tier).
|
|
1847
|
-
*
|
|
1848
|
-
* NO idle-TTL and NO eviction: a persisted workflow run-state is durable orchestration state
|
|
1849
|
-
* that lives until an explicit `delete`, never silently aging out (a run that vanished
|
|
1850
|
-
* mid-flight would be a silent data loss, not a freed session). A durable backend (JSON /
|
|
1851
|
-
* SQLite / IndexedDB) swaps in through the SAME interface without touching the runner or the
|
|
1859
|
+
* self-contained JSON, so no encoding is needed for the memory tier). A durable backend (JSON /
|
|
1860
|
+
* SQLite / IndexedDB) swaps in through the same interface without touching the runner or the
|
|
1852
1861
|
* entity tree — its driver-pluggable twin is
|
|
1853
1862
|
* {@link import('./DatabaseWorkflowStore.js').DatabaseWorkflowStore} (the snapshot as one opaque
|
|
1854
1863
|
* JSON column), exactly as `@orkestrel/queue`'s `MemoryQueueStore`
|
|
1855
1864
|
* twins `DatabaseQueueStore`.
|
|
1856
1865
|
*
|
|
1857
1866
|
* - **`get` resolves the persisted snapshot for an id**, or `undefined` if none is stored.
|
|
1858
|
-
* - **`set` inserts / replaces under the snapshot's
|
|
1867
|
+
* - **`set` inserts / replaces under the snapshot's own `id`** (no separate id param).
|
|
1859
1868
|
* - **`delete` drops a snapshot by id**; an absent id is a no-op (no throw).
|
|
1860
1869
|
*
|
|
1861
|
-
* The public surface is
|
|
1870
|
+
* The public surface is exactly `get` / `set` / `delete` — no extra members (the guide's method
|
|
1862
1871
|
* bijection with {@link WorkflowStoreInterface}). Restore is a caller concern: read a snapshot
|
|
1863
1872
|
* back and rebuild the live tree with {@link import('../factories.js').createRestoredWorkflow}.
|
|
1864
1873
|
*
|
|
@@ -1901,27 +1910,27 @@ var MemoryWorkflowStore = class {
|
|
|
1901
1910
|
* - **Guarded transitions.** `start` (→ `running`), then `complete(value)`
|
|
1902
1911
|
* (→ `completed`, records a {@link import('@orkestrel/contract').Success}), `fail(error)`
|
|
1903
1912
|
* (→ `failed`, records a {@link import('@orkestrel/contract').Failure}), `skip` (→ `skipped`),
|
|
1904
|
-
* `stop` (→ `stopped`). Each consults {@link canTransitionTask}
|
|
1913
|
+
* `stop` (→ `stopped`). Each consults {@link canTransitionTask} first and throws a
|
|
1905
1914
|
* `TRANSITION` {@link WorkflowError} on an illegal move (for example, completing a
|
|
1906
1915
|
* non-`running` task) — the legal graph is the single source of truth, so the leaf can never
|
|
1907
1916
|
* reach an impossible state.
|
|
1908
1917
|
* - **Snapshot fidelity.** A leaf needs no override: `skipped` / `stopped` are explicit terminal
|
|
1909
1918
|
* statuses, and restore reinstates the leaf directly from {@link TaskSnapshot.status}.
|
|
1910
1919
|
* - **The cascade.** Every status change records its boxed result (when any), fires the leaf's
|
|
1911
|
-
*
|
|
1912
|
-
* transition propagates
|
|
1913
|
-
* order means an observer sees the
|
|
1920
|
+
* own event, then calls the parent phase's `#recompute` (injected at construction) so the
|
|
1921
|
+
* transition propagates up (Task → Phase → Workflow re-derive). The own-event-before-cascade
|
|
1922
|
+
* order means an observer sees the cause (this leaf changed) before the effect (the parents
|
|
1914
1923
|
* re-derive) — the project precedent (`Runner.#settle` emits its own `fail` before propagating).
|
|
1915
1924
|
* - **Observable.** The owned {@link emitter} ({@link TaskEventMap}) fires the
|
|
1916
|
-
* matching event strictly
|
|
1925
|
+
* matching event strictly after the state change, before the cascade; the emitter isolates
|
|
1917
1926
|
* a listener throw and routes it to its `error` handler (the `error` option), so a buggy
|
|
1918
1927
|
* observer can never corrupt a transition.
|
|
1919
|
-
* - **Declarative config.** `behavior` / `retries` / `timeout`
|
|
1928
|
+
* - **Declarative config.** `behavior` / `retries` / `timeout` persist in a
|
|
1920
1929
|
* {@link TaskSnapshot} (like a phase's `bail` / `concurrency`), carried verbatim from the
|
|
1921
1930
|
* matching {@link import('../types.js').TaskDefinition} / {@link TaskSnapshot} field. `handler`
|
|
1922
|
-
* is the
|
|
1931
|
+
* is the runtime-only counterpart — `behavior` resolved once at construction against the
|
|
1923
1932
|
* workflow-level {@link import('../types.js').WorkflowOptions.functions} registry — and is
|
|
1924
|
-
*
|
|
1933
|
+
* never persisted; `undefined` when `behavior` is omitted or unregistered. Only omission is a
|
|
1925
1934
|
* deliberate no-op; unresolved named work is rejected before dispatch.
|
|
1926
1935
|
*/
|
|
1927
1936
|
var Task = class {
|
|
@@ -2145,12 +2154,12 @@ var Task = class {
|
|
|
2145
2154
|
return this.#paused && this.#gate !== void 0 ? this.#gate.promise : Promise.resolve();
|
|
2146
2155
|
}
|
|
2147
2156
|
/**
|
|
2148
|
-
* Applies a validated declarative patch to
|
|
2157
|
+
* Applies a validated declarative patch to self (`name` / `description`).
|
|
2149
2158
|
*
|
|
2150
2159
|
* @remarks
|
|
2151
2160
|
* Defense-in-depth: the owning
|
|
2152
|
-
* {@link import('../types.js').TaskManagerInterface.update} gates
|
|
2153
|
-
* exists + `pending`), so this is the second, redundant check — it
|
|
2161
|
+
* {@link import('../types.js').TaskManagerInterface.update} gates first (target
|
|
2162
|
+
* exists + `pending`), so this is the second, redundant check — it throws a
|
|
2154
2163
|
* `MUTATION` {@link WorkflowError} unless this task's own `status` is `pending`.
|
|
2155
2164
|
*
|
|
2156
2165
|
* @param value - The {@link TaskUpdate} fields to apply
|
|
@@ -2259,10 +2268,10 @@ var Task = class {
|
|
|
2259
2268
|
* drift apart.
|
|
2260
2269
|
* - **Positional store.** `append` adds one live {@link TaskInterface} at the end (the build-time
|
|
2261
2270
|
* wiring path), `task(id)` looks one up, `tasks()` lists them in positional order, `count` is
|
|
2262
|
-
* the tally. A `skip` is a
|
|
2271
|
+
* the tally. A `skip` is a status change on a stored task (never a removal), so order survives
|
|
2263
2272
|
* it; a snapshot RESTORE re-`append`s in the snapshot's order, reproducing it exactly.
|
|
2264
2273
|
* - **Gated mutation API.** `add` / `remove` / `move` / `update` are the graceful
|
|
2265
|
-
* `Result` counterparts to `append`, gating
|
|
2274
|
+
* `Result` counterparts to `append`, gating only on the target's own existence/status/id/bounds
|
|
2266
2275
|
* — a duplicate id, an absent/non-`pending` target, an out-of-bounds `index`, or a patch that
|
|
2267
2276
|
* fails {@link taskUpdateShape} validation all fail gracefully with a `MUTATION`
|
|
2268
2277
|
* {@link WorkflowError} instead of throwing.
|
|
@@ -2310,47 +2319,47 @@ var TaskManager = class {
|
|
|
2310
2319
|
//#endregion
|
|
2311
2320
|
//#region src/core/phases/Phase.ts
|
|
2312
2321
|
/**
|
|
2313
|
-
* Implements the live
|
|
2322
|
+
* Implements the live derived state machine (W-b) for one phase — an observable whose
|
|
2314
2323
|
* {@link LifecycleStatus} is computed from its tasks (never set directly) and recomputed
|
|
2315
2324
|
* reactively as a task transitions (the middle tier of the cascade).
|
|
2316
2325
|
*
|
|
2317
2326
|
* @remarks
|
|
2318
2327
|
* - **Derived status.** `status` is `#override` when one is in force, else
|
|
2319
2328
|
* {@link derivePhaseStatus} over the live tasks' statuses. `#recompute` (passed to
|
|
2320
|
-
* each child {@link Task}) re-derives on every child transition; a
|
|
2321
|
-
* event
|
|
2322
|
-
* - **Override.** `skip` / `stop`
|
|
2323
|
-
* phase), overriding the derived value; the override is
|
|
2324
|
-
* `override` field and restored
|
|
2329
|
+
* each child {@link Task}) re-derives on every child transition; a change emits the matching
|
|
2330
|
+
* event and escalates to the workflow (`#escalate`, the upward step of the cascade).
|
|
2331
|
+
* - **Override.** `skip` / `stop` force the phase's status (for example, skipping a whole
|
|
2332
|
+
* phase), overriding the derived value; the override is persisted in the snapshot's own
|
|
2333
|
+
* `override` field and restored directly (no divergence guess), so a forced phase round-trips.
|
|
2325
2334
|
* - **Children.** `tasks` is the lean {@link TaskManager} (an accessor + `count`,
|
|
2326
2335
|
* no batch matrix); built positionally from the snapshot so order survives an interior `skip`.
|
|
2327
2336
|
* `results()` collects the settled tasks' {@link TaskResult}s (the phase tier of the result
|
|
2328
|
-
* tree); `workflow` navigates
|
|
2337
|
+
* tree); `workflow` navigates up to the live parent.
|
|
2329
2338
|
* - **Observable.** The owned {@link emitter} ({@link PhaseEventMap}) fires
|
|
2330
2339
|
* `start` / `complete` / `fail` / `pause` / `resume` / `skip` / `stop` after the
|
|
2331
2340
|
* corresponding status or runtime-gate change. Status events fire after the phase recomputes
|
|
2332
2341
|
* and before it escalates to the workflow, preserving child/phase cause before parent effect.
|
|
2333
2342
|
* The emitter isolates a listener throw and routes it to its `error` handler (the `error`
|
|
2334
2343
|
* option); `fail` carries the failing task's {@link TaskResult}.
|
|
2335
|
-
* - **Structural API.** `add` / `remove` / `move` / `update` gate
|
|
2344
|
+
* - **Structural API.** `add` / `remove` / `move` / `update` gate before
|
|
2336
2345
|
* delegating to {@link tasks} (the manager gates the target's own existence/status/id/
|
|
2337
|
-
* bounds), then emit the matching {@link PhaseEventMap} event on success only.
|
|
2346
|
+
* bounds), then emit the matching {@link PhaseEventMap} event on success only. Native
|
|
2338
2347
|
* gating, purely from this phase's own derived `status` (no runner-installed hook): while
|
|
2339
|
-
* `pending`, any valid `index` is accepted; while `running`, `add` accepts
|
|
2348
|
+
* `pending`, any valid `index` is accepted; while `running`, `add` accepts only a pure
|
|
2340
2349
|
* append (a live runner subscribed to the `add` event picks it up), and `remove` / `move` /
|
|
2341
2350
|
* `update` always fail gracefully (the tasks are already handed to the execution
|
|
2342
2351
|
* substrate); while terminal, everything is refused.
|
|
2343
|
-
* - **Patch.** `patch` applies a validated {@link PhaseUpdate} to
|
|
2352
|
+
* - **Patch.** `patch` applies a validated {@link PhaseUpdate} to self
|
|
2344
2353
|
* (`name` / `description` / `concurrency` / `bail`) — defense-in-depth: it throws a
|
|
2345
2354
|
* `MUTATION` {@link WorkflowError} unless this phase's own `status` is `pending`, mirroring
|
|
2346
2355
|
* the owning {@link WorkflowInterface.update}'s gate.
|
|
2347
|
-
* - **Minting.** {@link add}
|
|
2348
|
-
* (converts it to a {@link TaskSnapshot}, builds the task wired to
|
|
2356
|
+
* - **Minting.** {@link add} mints a live {@link Task} from a {@link TaskDefinition}
|
|
2357
|
+
* (converts it to a {@link TaskSnapshot}, builds the task wired to this phase) — the same
|
|
2349
2358
|
* construction path {@link #append} uses at build time, so a live mint and a restored/built
|
|
2350
|
-
* task are wired
|
|
2359
|
+
* task are wired identically. At construction, the workflow-level
|
|
2351
2360
|
* {@link import('../types.js').WorkflowRegistry} registry (threaded from
|
|
2352
2361
|
* {@link import('../types.js').WorkflowOptions.functions}) resolves every unique initial `behavior`
|
|
2353
|
-
* name
|
|
2362
|
+
* name once before any task is built; siblings sharing a name receive the exact same captured
|
|
2354
2363
|
* runtime {@link import('../types.js').TaskInterface.handler}. A later live {@link add} reads
|
|
2355
2364
|
* that name once from the retained registry at its own mint moment. An omitted or unregistered
|
|
2356
2365
|
* `behavior` resolves to no handler; only omission is a no-op, while an unresolved present name makes
|
|
@@ -2358,7 +2367,7 @@ var TaskManager = class {
|
|
|
2358
2367
|
* - **Runtime lifecycle.** `pause` / `resume` / `wait` mirror the workflow's own
|
|
2359
2368
|
* quartet, scoped to this phase — a driving
|
|
2360
2369
|
* {@link import('../types.js').WorkflowRunnerInterface.execute} gates a task's own
|
|
2361
|
-
* pre-dispatch on the workflow's gate
|
|
2370
|
+
* pre-dispatch on the workflow's gate first, then this phase's gate, without touching
|
|
2362
2371
|
* {@link status} — `paused` is runtime-only, never persisted. `skip` / `stop` (this phase's
|
|
2363
2372
|
* own terminal forcing) always release a parked {@link wait} waiter, mirroring
|
|
2364
2373
|
* {@link import('../Workflow.js').Workflow.destroy}'s cascade — a permanently-ended phase
|
|
@@ -2617,7 +2626,7 @@ var Phase = class {
|
|
|
2617
2626
|
* looks one up, `phases()` lists them in positional order, `count` is the tally. A snapshot
|
|
2618
2627
|
* RESTORE re-`append`s in the snapshot's order, reproducing it exactly.
|
|
2619
2628
|
* - **Gated mutation API.** `add` / `remove` / `move` / `update` are the graceful
|
|
2620
|
-
* `Result` counterparts to `append`, gating
|
|
2629
|
+
* `Result` counterparts to `append`, gating only on the target's own existence/status/id/bounds
|
|
2621
2630
|
* — a duplicate id, an absent/non-`pending` target, an out-of-bounds `index`, or a patch that
|
|
2622
2631
|
* fails {@link phaseUpdateShape} validation all fail gracefully with a `MUTATION`
|
|
2623
2632
|
* {@link WorkflowError} instead of throwing.
|
|
@@ -2665,7 +2674,7 @@ var PhaseManager = class {
|
|
|
2665
2674
|
//#endregion
|
|
2666
2675
|
//#region src/core/Workflow.ts
|
|
2667
2676
|
/**
|
|
2668
|
-
* Implements the live
|
|
2677
|
+
* Implements the live derived state machine (W-b) for a whole workflow — the observable root
|
|
2669
2678
|
* whose {@link LifecycleStatus} is computed from its phases under the `bail` policy and
|
|
2670
2679
|
* recomputed reactively as the cascade propagates up from a task transition.
|
|
2671
2680
|
*
|
|
@@ -2676,16 +2685,16 @@ var PhaseManager = class {
|
|
|
2676
2685
|
* passes a persisted one). Each child {@link Phase} is wired to escalate to `#recompute`.
|
|
2677
2686
|
* - **Derived status.** `status` is `#override` when forced, else
|
|
2678
2687
|
* {@link deriveWorkflowStatus} over the live phases' statuses feeding `bail`. `failed` is
|
|
2679
|
-
* reachable
|
|
2688
|
+
* reachable only under `bail: true` (a single failed task halts the workflow); under
|
|
2680
2689
|
* `bail: false` a failed phase folds into `completed`. `#recompute` diffs on each phase
|
|
2681
|
-
* change; a
|
|
2682
|
-
* - **Override.** `skip` / `stop`
|
|
2683
|
-
* may also be force-completed vacuously. The override is
|
|
2684
|
-
* `override` field and restored
|
|
2690
|
+
* change; a change emits.
|
|
2691
|
+
* - **Override.** `skip` / `stop` force the status; an executed task-free pending tree
|
|
2692
|
+
* may also be force-completed vacuously. The override is persisted in the snapshot's own
|
|
2693
|
+
* `override` field and restored directly (no divergence guess). The snapshot also persists
|
|
2685
2694
|
* `bail`, so a restore re-derives status identically without a silent policy default.
|
|
2686
2695
|
* - **Result tree.** `results()` flattens every phase's `results()` ({@link collectResults}) — the
|
|
2687
|
-
* workflow tier; `phase(id)` + each `phase.task(id)` navigate
|
|
2688
|
-
* navigate
|
|
2696
|
+
* workflow tier; `phase(id)` + each `phase.task(id)` navigate down, a task's `phase` / `workflow`
|
|
2697
|
+
* navigate up.
|
|
2689
2698
|
* - **Snapshot.** `snapshot()` serializes the whole live tree to a {@link WorkflowSnapshot} (pure
|
|
2690
2699
|
* JSON); {@link import('./factories.js').createRestoredWorkflow} rebuilds an equivalent live tree.
|
|
2691
2700
|
* - **Observable.** The owned {@link emitter} ({@link WorkflowEventMap}) fires
|
|
@@ -2693,17 +2702,17 @@ var PhaseManager = class {
|
|
|
2693
2702
|
* corresponding status or runtime-gate change; the emitter isolates a listener throw and
|
|
2694
2703
|
* routes it to its `error` handler (the `error` option); `fail` carries the failing task's
|
|
2695
2704
|
* {@link TaskResult}.
|
|
2696
|
-
* - **Structural API.** `add` / `remove` / `move` / `update` gate
|
|
2705
|
+
* - **Structural API.** `add` / `remove` / `move` / `update` gate before
|
|
2697
2706
|
* delegating to {@link phases} (the manager gates the target's own existence/status/id/
|
|
2698
|
-
* bounds), then emit the matching {@link WorkflowEventMap} event on success only.
|
|
2707
|
+
* bounds), then emit the matching {@link WorkflowEventMap} event on success only. Native,
|
|
2699
2708
|
* bottom-up gating (no runner-installed hook): refused outright while this workflow's own
|
|
2700
|
-
* `status` is terminal; otherwise a target position must fall within the
|
|
2709
|
+
* `status` is terminal; otherwise a target position must fall within the pending suffix —
|
|
2701
2710
|
* the contiguous trailing run of `pending` phases — whose boundary is
|
|
2702
2711
|
* {@link import('./helpers.js').deriveBoundary} over the live phases' statuses. A `pending`
|
|
2703
2712
|
* workflow's phases are all `pending`, so the boundary is `0` and every position is
|
|
2704
2713
|
* naturally accepted.
|
|
2705
2714
|
* - **Runtime lifecycle.** `pause` / `resume` / `wait` gate execution at the runner's
|
|
2706
|
-
* phase/task boundaries
|
|
2715
|
+
* phase/task boundaries without touching {@link status} — `paused` is runtime-only, never
|
|
2707
2716
|
* persisted. `destroy` is a terminal teardown: it `stop`s every non-terminal task and
|
|
2708
2717
|
* phase (releasing their gates and liveness resources), aborts {@link signal}, forces the
|
|
2709
2718
|
* workflow `stop` override when needed, releases its parked waiter, and marks
|
|
@@ -2992,14 +3001,14 @@ var Workflow = class {
|
|
|
2992
3001
|
* mints a live {@link WorkflowInterface} through the same construction path
|
|
2993
3002
|
* {@link import('./factories.js').createWorkflow} takes (flowing the manager's
|
|
2994
3003
|
* `functions` registry in) and stores it under `definition.id` — an already-present id
|
|
2995
|
-
*
|
|
3004
|
+
* overwrites (last write wins). `count` is the map size, `workflow(id)` looks one up,
|
|
2996
3005
|
* `workflows()` lists them in insertion order.
|
|
2997
3006
|
* - **Durable open / save.** `open(id)` returns an already-registered workflow directly; same-id
|
|
2998
3007
|
* misses share one hydration. A concurrent `add` wins, while `remove` / `clear` invalidate
|
|
2999
3008
|
* earlier reads; wrong-key payloads reject with `RESTORE`. `save(id)` captures a registered
|
|
3000
3009
|
* workflow's snapshot at invocation and serializes same-id writes without coupling other ids.
|
|
3001
3010
|
* Both remain lenient without a store or registered id.
|
|
3002
|
-
* - **Removal.** `remove` drops one by id, or a batch (array overload
|
|
3011
|
+
* - **Removal.** `remove` drops one by id, or a batch (array overload first) — `true` only when
|
|
3003
3012
|
* every id was removed. `clear` empties the registry.
|
|
3004
3013
|
* - **No active pointer.** Unlike its `ConversationManager` / `WorkspaceManager` twins, there is
|
|
3005
3014
|
* no `active` / `switch` — nothing in the workflow domain renders "the current workflow".
|
|
@@ -3009,7 +3018,7 @@ var Workflow = class {
|
|
|
3009
3018
|
* const manager = new WorkflowManager({
|
|
3010
3019
|
* functions: { compile: async (controller) => `built ${controller.task.id}` },
|
|
3011
3020
|
* })
|
|
3012
|
-
* const workflow = manager.add(definition) // minted, registered,
|
|
3021
|
+
* const workflow = manager.add(definition) // minted, registered, runnable
|
|
3013
3022
|
* manager.workflow(workflow.id) // the same workflow
|
|
3014
3023
|
* manager.count // 1
|
|
3015
3024
|
* ```
|
|
@@ -3235,11 +3244,11 @@ var RunHolder = class {
|
|
|
3235
3244
|
* unit it dispatches, handing it the unit's `id`, `input`, the unit's `Abort`
|
|
3236
3245
|
* handle, the queue attempt's `signal`, and a `spawn` callback that launches a
|
|
3237
3246
|
* sibling through the same queue.
|
|
3238
|
-
* - **Signal.** `signal` is the queue attempt's signal, which
|
|
3247
|
+
* - **Signal.** `signal` is the queue attempt's signal, which any-combines the
|
|
3239
3248
|
* unit's own abort, the runner-level abort (the runner aborts every unit), and
|
|
3240
3249
|
* the per-attempt timeout — so it fires on any of the three. `aborted` and
|
|
3241
3250
|
* `abort(reason)` delegate to the unit's `Abort` (the cancellation source of
|
|
3242
|
-
* truth); because the attempt signal
|
|
3251
|
+
* truth); because the attempt signal any-includes that abort, `abort()` fires
|
|
3243
3252
|
* `signal` too.
|
|
3244
3253
|
* - **`wait` promise-parks (never a timer).** It resolves the instant the unit's
|
|
3245
3254
|
* `signal` fires (immediately if already aborted) through a one-shot listener — no
|
|
@@ -3299,13 +3308,13 @@ var Controller = class {
|
|
|
3299
3308
|
* bounded concurrency, retries, and the per-attempt timeout are all the Queue's —
|
|
3300
3309
|
* the Runner adds only orchestration (launching, ordering, draining, fail-fast).
|
|
3301
3310
|
* - **Spawns actually run, results stay ordered (the B2 fix).** Declared inputs and
|
|
3302
|
-
* `spawn`ed siblings flow through the
|
|
3311
|
+
* `spawn`ed siblings flow through the same `#launch`, which appends the unit's `id`
|
|
3303
3312
|
* to an ordered `#order` list and records its settled value into `#values` by `id`.
|
|
3304
3313
|
* Results are read back as `#order.map(id => #values.get(id))` — declared first (in
|
|
3305
3314
|
* input order), then spawns (in spawn order). There is no one-time task snapshot,
|
|
3306
3315
|
* so a unit spawned mid-handler is run and ordered like any other.
|
|
3307
3316
|
* - **`execute` awaits the full spawn closure through a count gate.** `#launch` increments
|
|
3308
|
-
* an outstanding-unit `#count`
|
|
3317
|
+
* an outstanding-unit `#count` before enqueuing and every settle decrements it,
|
|
3309
3318
|
* resolving the `#drained` deferred at zero. Because `spawn` calls `#launch` (so
|
|
3310
3319
|
* `#count += 1`) before the parent handler returns, the count never reaches zero
|
|
3311
3320
|
* mid-run — `execute` parks on `#drained` and so awaits the entire transitive
|
|
@@ -3317,26 +3326,26 @@ var Controller = class {
|
|
|
3317
3326
|
* spawn by a bounded handler can still deadlock — that caveat is the caller's.)
|
|
3318
3327
|
* - **Per-unit Controller + signal.** Each unit gets a `Controller` carrying its `id`,
|
|
3319
3328
|
* `input`, the unit's `Abort` (so `aborted` / `abort` delegate to it), and the queue
|
|
3320
|
-
* attempt's `signal` (which
|
|
3329
|
+
* attempt's `signal` (which any-combines the unit abort + runner abort + timeout). A
|
|
3321
3330
|
* `spawn` callback is injected so `controller.spawn(input)` delegates to `#launch`.
|
|
3322
3331
|
* - **One-shot + fail-fast.** `execute` runs once (a second call throws). The first
|
|
3323
3332
|
* unit failure (after its retries) records the error and `abort()`s the run, so every
|
|
3324
3333
|
* sibling's signal fires; later failures are ignored and `execute` rejects with the
|
|
3325
3334
|
* first error. A user `abort(reason)` likewise rejects a running `execute`.
|
|
3326
3335
|
* - **`pause` / `resume` / `stop` ride the backing Queue.** `pause` / `resume`
|
|
3327
|
-
* delegate straight to the Queue's own pause/resume (holding/releasing the
|
|
3336
|
+
* delegate straight to the Queue's own pause/resume (holding/releasing the next
|
|
3328
3337
|
* dispatch while an in-flight unit finishes); `paused` mirrors the Queue's. `stop` is a
|
|
3329
|
-
*
|
|
3330
|
-
* units are rejected by the Queue's own stop
|
|
3338
|
+
* graceful permanent end, distinct from `abort`: still-pending (never-dispatched)
|
|
3339
|
+
* units are rejected by the Queue's own stop without their handler ever running, and
|
|
3331
3340
|
* `#settle` reads that fact (`#dispatched`) to treat the rejection as a stop artifact —
|
|
3332
3341
|
* not a failure, never tripping fail-fast — while an in-flight unit still runs to
|
|
3333
|
-
* completion and settles normally. `execute`
|
|
3342
|
+
* completion and settles normally. `execute` resolves (never rejects) after every unit
|
|
3334
3343
|
* has settled, with whatever results actually completed.
|
|
3335
3344
|
* - **Observable.** The owned {@link emitter} ({@link RunnerEventMap}) carries the run
|
|
3336
3345
|
* lifecycle — `start` / `unit` / `spawn` / `settle` / `fail` / `finish` / `abort` — for
|
|
3337
|
-
* fire-and-forget observers. Every event is emitted directly, strictly
|
|
3346
|
+
* fire-and-forget observers. Every event is emitted directly, strictly after the relevant
|
|
3338
3347
|
* launch / settle / drain transition; the emitter isolates a listener throw and routes it
|
|
3339
|
-
* to its `error` handler (the `error` option), so a buggy observer can
|
|
3348
|
+
* to its `error` handler (the `error` option), so a buggy observer can never reorder, throw
|
|
3340
3349
|
* into, or corrupt the one-shot / fail-fast / spawn-tracking engine: the outstanding-unit
|
|
3341
3350
|
* count gate stays balanced and fail-fast still fires regardless of what a listener does.
|
|
3342
3351
|
* Observation is purely a side-channel.
|
|
@@ -3395,15 +3404,15 @@ var Runner = class {
|
|
|
3395
3404
|
return this.#queue.paused;
|
|
3396
3405
|
}
|
|
3397
3406
|
/**
|
|
3398
|
-
* Injects one more unit into an
|
|
3399
|
-
* `Controller.spawn`, called from
|
|
3407
|
+
* Injects one more unit into an in-flight `execute` run — a live counterpart to a
|
|
3408
|
+
* `Controller.spawn`, called from outside any unit's handler.
|
|
3400
3409
|
*
|
|
3401
3410
|
* @remarks
|
|
3402
3411
|
* Returns `undefined` synchronously (graceful, non-throwing) unless the
|
|
3403
3412
|
* runner is mid-`execute` and not yet stopped — covering "never started",
|
|
3404
3413
|
* "already drained", "aborted", and "destroyed". Otherwise the unit is routed through
|
|
3405
|
-
* the
|
|
3406
|
-
* unit count gate increments
|
|
3414
|
+
* the same backing queue as a declared/`spawn`ed unit through `#launch` — the outstanding-
|
|
3415
|
+
* unit count gate increments before this call returns, so an in-flight `execute`
|
|
3407
3416
|
* keeps awaiting it (the drain race: `#running` flips to `false` as the very first
|
|
3408
3417
|
* step after `execute`'s `await drained.promise` settles, so a `spawn` reaching this
|
|
3409
3418
|
* method after the run has fully drained is cleanly rejected with `undefined` —
|
|
@@ -3463,7 +3472,7 @@ var Runner = class {
|
|
|
3463
3472
|
}
|
|
3464
3473
|
/**
|
|
3465
3474
|
* Suspends dispatch (resumable): delegates to the backing queue's own
|
|
3466
|
-
* `pause`, which holds the
|
|
3475
|
+
* `pause`, which holds the next dispatch while any in-flight unit finishes.
|
|
3467
3476
|
*
|
|
3468
3477
|
* @remarks
|
|
3469
3478
|
* A no-op after the runner is `stopped` — a stopped runner has no dispatch left to
|
|
@@ -3487,10 +3496,10 @@ var Runner = class {
|
|
|
3487
3496
|
this.#queue.resume();
|
|
3488
3497
|
}
|
|
3489
3498
|
/**
|
|
3490
|
-
* Ends the runner permanently — a
|
|
3499
|
+
* Ends the runner permanently — a graceful stop, distinct from `abort`.
|
|
3491
3500
|
* Marks the runner `stopping` + `stopped`, then stops the backing queue: every
|
|
3492
|
-
* still-
|
|
3493
|
-
* "queue is stopped" error,
|
|
3501
|
+
* still-pending (never-dispatched) unit is rejected by the queue with its own
|
|
3502
|
+
* "queue is stopped" error, without running its handler; every already-in-flight unit
|
|
3494
3503
|
* keeps running to completion and settles normally. `#settle` reads `#stopping` to
|
|
3495
3504
|
* classify a never-dispatched unit's rejection as a stop artifact (decrement the count
|
|
3496
3505
|
* gate, no recorded failure, no fail-fast trip) rather than a genuine failure — a
|
|
@@ -3619,21 +3628,21 @@ var Runner = class {
|
|
|
3619
3628
|
* Implements the attempt-scoped handle a {@link import('./types.js').WorkflowFunction} receives.
|
|
3620
3629
|
*
|
|
3621
3630
|
* @remarks
|
|
3622
|
-
* - **A leaf handle,
|
|
3631
|
+
* - **A leaf handle, not the runner `Controller`.** A workflow task is a leaf of the
|
|
3623
3632
|
* declarative W-b tree, not a fan-out unit, so it has no `spawn`; its `wait` instead
|
|
3624
3633
|
* checkpoints the workflow, phase, and task cooperative gates.
|
|
3625
|
-
* - **Folded signal.** `signal` is the cancellation folded for
|
|
3634
|
+
* - **Folded signal.** `signal` is the cancellation folded for this attempt: its per-attempt
|
|
3626
3635
|
* deadline, task stop/skip, workflow abort/timeout/budget/destroy, or a sibling fail-fast.
|
|
3627
3636
|
* A handler races its work against it; `aborted` reads it.
|
|
3628
3637
|
* - **Attempt ownership.** `report` / `pulse` are closures supplied by the runner and refuse
|
|
3629
3638
|
* after this signal aborts or a retry token supersedes this handle.
|
|
3630
3639
|
* - **Input + lineage.** `input` is the task's open `metadata` bag (its
|
|
3631
3640
|
* {@link import('./types.js').TaskInput} payload, `{}` when none); `task` is the full
|
|
3632
|
-
* {@link TaskContext}, so `task.phase` / `task.phase.workflow` navigate
|
|
3641
|
+
* {@link TaskContext}, so `task.phase` / `task.phase.workflow` navigate up the lineage.
|
|
3633
3642
|
* - **Read-up results.** `results()` returns every settled task's {@link TaskResult} across
|
|
3634
3643
|
* the phases that have already finished (a closure over the live
|
|
3635
3644
|
* {@link import('./types.js').WorkflowInterface}), so a `function` task can read an earlier
|
|
3636
|
-
* phase's output. Read-only — a task records its
|
|
3645
|
+
* phase's output. Read-only — a task records its own outcome by returning / throwing, not
|
|
3637
3646
|
* by mutating the tree.
|
|
3638
3647
|
* - **Event-free.** Like the runner `Controller`, the per-task handle carries no Emitter;
|
|
3639
3648
|
* observe the W-b entities' own emitters (`task.emitter` / `phase.emitter`) instead.
|
|
@@ -3727,11 +3736,12 @@ var TaskController = class {
|
|
|
3727
3736
|
//#endregion
|
|
3728
3737
|
//#region src/core/WorkflowPersistence.ts
|
|
3729
3738
|
/**
|
|
3730
|
-
* Coordinates advanced run-local snapshot persistence with one writer and one coalesced most
|
|
3739
|
+
* Coordinates advanced run-local snapshot persistence with one writer and one coalesced most
|
|
3740
|
+
* recent obligation, normally composed through `execute({ store })` rather than built directly.
|
|
3731
3741
|
*
|
|
3732
3742
|
* @remarks
|
|
3733
|
-
*
|
|
3734
|
-
*
|
|
3743
|
+
* Exported for hosts that need to coordinate the same required boundaries around their own runner
|
|
3744
|
+
* integration.
|
|
3735
3745
|
*
|
|
3736
3746
|
* @example
|
|
3737
3747
|
* ```ts
|
|
@@ -3897,8 +3907,8 @@ var WorkflowPersistence = class {
|
|
|
3897
3907
|
//#endregion
|
|
3898
3908
|
//#region src/core/WorkflowRunner.ts
|
|
3899
3909
|
/**
|
|
3900
|
-
* Implements the thin orchestrator that
|
|
3901
|
-
* substrate — phases sequential, tasks concurrent — dispatching each task through its
|
|
3910
|
+
* Implements the thin orchestrator that executes a live W-b workflow tree by composing the shipped
|
|
3911
|
+
* substrate — phases sequential, tasks concurrent — dispatching each task through its own
|
|
3902
3912
|
* resolved handler under the `bail` policy.
|
|
3903
3913
|
*
|
|
3904
3914
|
* @remarks
|
|
@@ -3909,33 +3919,33 @@ var WorkflowPersistence = class {
|
|
|
3909
3919
|
* timeout / budget / entity `signal` fold through the `@orkestrel/abort` signal contract,
|
|
3910
3920
|
* {@link createTimeout}, and `AbortSignal.any` (exactly as the agent runtime folds its bounds);
|
|
3911
3921
|
* pacing is the shipped
|
|
3912
|
-
* {@link SchedulerInterface}. The runner writes
|
|
3922
|
+
* {@link SchedulerInterface}. The runner writes zero concurrency / retry / abort logic of
|
|
3913
3923
|
* its own — it only sequences phases, dispatches a task's own handler, and drives the live
|
|
3914
3924
|
* entity. The workflow layer owns per-task deadlines because timeout settlement must
|
|
3915
3925
|
* update the live leaf under the phase's `bail` policy before the substrate unit settles.
|
|
3916
3926
|
* - **Pure engine — no integration registry.** The runner carries no behavior or provider
|
|
3917
3927
|
* registry: each live {@link TaskInterface} already
|
|
3918
3928
|
* resolved its own {@link import('./types.js').WorkflowFunction} into
|
|
3919
|
-
* {@link import('./types.js').TaskInterface.handler}
|
|
3929
|
+
* {@link import('./types.js').TaskInterface.handler} once at construction (build, restore,
|
|
3920
3930
|
* or a live mint all resolve it identically, from {@link WorkflowOptions.functions}), so
|
|
3921
3931
|
* dispatch is "invoke the task's own handler". Provider, protocol, and tool
|
|
3922
3932
|
* integrations remain application-owned {@link import('./types.js').WorkflowFunction}s
|
|
3923
3933
|
* composed into {@link WorkflowOptions.functions}. This module imports none of them.
|
|
3924
|
-
* - **Two `execute` forms, one engine.** `execute(definition, options)`
|
|
3934
|
+
* - **Two `execute` forms, one engine.** `execute(definition, options)` builds the live tree
|
|
3925
3935
|
* from a {@link WorkflowDefinition} (single source of truth for the `behavior` / `concurrency`
|
|
3926
|
-
* metadata); `execute(workflow, options)`
|
|
3936
|
+
* metadata); `execute(workflow, options)` drives a caller-owned, already-built
|
|
3927
3937
|
* {@link WorkflowInterface} instead — the entity-native control surface
|
|
3928
3938
|
* (`pause` / `resume` / `add` / `stop` / `destroy` live on the entity itself). Both forms
|
|
3929
|
-
* converge on the
|
|
3930
|
-
* exists — `#runTask` reads each task's
|
|
3931
|
-
* / `retries` / `timeout`, and `#runPhase` reads each phase's
|
|
3939
|
+
* converge on the same `#execute` engine: neither reads a `WorkflowDefinition` after the tree
|
|
3940
|
+
* exists — `#runTask` reads each task's own {@link import('./types.js').TaskInterface.handler}
|
|
3941
|
+
* / `retries` / `timeout`, and `#runPhase` reads each phase's own
|
|
3932
3942
|
* {@link PhaseInterface.concurrency} / `bail`, so a live `add`-minted phase or task (V5)
|
|
3933
|
-
* runs under
|
|
3934
|
-
* - **Phases sequential, tasks concurrent —
|
|
3935
|
-
* order,
|
|
3943
|
+
* runs under exactly the same rules as one built from the original definition.
|
|
3944
|
+
* - **Phases sequential, tasks concurrent — live continuity.** `#execute` drives the phases in
|
|
3945
|
+
* order, re-reading `workflow.phases.phases()` every iteration (a cursor over the live
|
|
3936
3946
|
* manager, not a one-time snapshot) so a caller's `workflow.add(phaseDefinition)` mid-run is
|
|
3937
|
-
* picked up. Within a phase, `#runPhase` subscribes to that phase's `add` event
|
|
3938
|
-
* capturing its task list, then `spawn`s any task added mid-phase onto the
|
|
3947
|
+
* picked up. Within a phase, `#runPhase` subscribes to that phase's `add` event before
|
|
3948
|
+
* capturing its task list, then `spawn`s any task added mid-phase onto the same substrate
|
|
3939
3949
|
* Runner (so it is actually dispatched, under the same `concurrency`); a task added too late
|
|
3940
3950
|
* for `spawn` to accept (the runner already drained) is swept `skip`ped afterward so the
|
|
3941
3951
|
* phase always reaches a coherent terminal state.
|
|
@@ -3944,30 +3954,30 @@ var WorkflowPersistence = class {
|
|
|
3944
3954
|
* auto-completes with JSON `null`; a present unresolved name is rejected by the synchronous
|
|
3945
3955
|
* execution claim and never false-completes.
|
|
3946
3956
|
* - **`bail` → substrate.** Under `bail: true` (halt) a genuine task failure `fail`s the leaf
|
|
3947
|
-
*
|
|
3957
|
+
* then re-throws, so the substrate Runner fail-fasts — it aborts the in-flight siblings
|
|
3948
3958
|
* (their `controller.signal` fires; a mid-flight sibling `skip`s) and rejects the phase run;
|
|
3949
3959
|
* `#execute` then `skip`s the remaining tasks / phases (the workflow derives `failed`).
|
|
3950
|
-
* Under `bail: false` (graceful) a failure `fail`s the leaf and
|
|
3960
|
+
* Under `bail: false` (graceful) a failure `fail`s the leaf and resolves (never throws), so
|
|
3951
3961
|
* the Runner settles every unit (allSettled) and the run finishes (the workflow derives
|
|
3952
3962
|
* `completed`, the failure recorded in the result tree).
|
|
3953
3963
|
* - **Pause / stop / destroy gates.** Workflow, phase, and task gates are checked before
|
|
3954
3964
|
* dispatch, and a running handler can checkpoint their folded state through
|
|
3955
3965
|
* {@link import('./types.js').TaskControllerInterface.wait}. Because the substrate acquires
|
|
3956
3966
|
* concurrency before this handler gate, a paused task occupies one phase slot until resume;
|
|
3957
|
-
* already-running siblings continue and its per-attempt timeout keeps counting. A
|
|
3967
|
+
* already-running siblings continue and its per-attempt timeout keeps counting. A graceful
|
|
3958
3968
|
* `workflow.stop()` (no signal involved) is caught at
|
|
3959
3969
|
* those same gates: not-yet-started work is `skip`ped, in-flight work finishes naturally. A
|
|
3960
|
-
*
|
|
3970
|
+
* hard `workflow.destroy()` aborts {@link WorkflowInterface.signal}, which `#fold` has folded
|
|
3961
3971
|
* into the run's composed signal — so it cancels the active phase Runner (and every
|
|
3962
|
-
* in-flight task) exactly like an external abort / timeout / budget fire.
|
|
3963
|
-
* `wait()` gate is
|
|
3964
|
-
*
|
|
3972
|
+
* in-flight task) exactly like an external abort / timeout / budget fire. Every park on a
|
|
3973
|
+
* `wait()` gate is raced against that same run signal (`#raceWait`, S2) — so a cancel firing
|
|
3974
|
+
* while parked unparks the engine promptly instead of hanging until `resume`; the existing
|
|
3965
3975
|
* halt / abort re-checks after the gate then decide the outcome.
|
|
3966
3976
|
* - **Abort / Timeout / Budget / entity-signal fold.** `#execute` folds the live workflow's
|
|
3967
3977
|
* own {@link WorkflowInterface.signal}, the run's external `signal`, a
|
|
3968
3978
|
* {@link TimeoutInterface}, and the `@orkestrel/budget` package's `BudgetInterface`'s
|
|
3969
3979
|
* `signal` into one `runSignal` (`AbortSignal.any`); a fire aborts the active phase's Runner
|
|
3970
|
-
* (cancelling every in-flight task) and
|
|
3980
|
+
* (cancelling every in-flight task) and halts the run — the remaining tasks / phases `skip`
|
|
3971
3981
|
* and the workflow is force-`stop`ped (settles `stopped`). Each task's
|
|
3972
3982
|
* {@link TaskController} signal `AbortSignal.any`-combines the substrate per-unit signal with
|
|
3973
3983
|
* `runSignal`, so a handler observes either cause directly.
|
|
@@ -4385,20 +4395,20 @@ function createWorkflowContract() {
|
|
|
4385
4395
|
* Builds the live W-b entity tree from a {@link WorkflowDefinition} — the whole
|
|
4386
4396
|
* {@link WorkflowInterface} → {@link import('./types.js').PhaseInterface} →
|
|
4387
4397
|
* {@link import('./types.js').TaskInterface} tree, each level wired with its lineage
|
|
4388
|
-
* context, its emitter, and the cascade
|
|
4398
|
+
* context, its emitter, and the cascade, and every node born `pending`.
|
|
4389
4399
|
*
|
|
4390
4400
|
* @remarks
|
|
4391
|
-
* The definition is the
|
|
4401
|
+
* The definition is the declarative blueprint; this seeds an initial all-`pending`
|
|
4392
4402
|
* {@link WorkflowSnapshot} from it ({@link definitionToSnapshot}) and constructs the live
|
|
4393
4403
|
* tree over that one path. The `bail` failure policy resolves to `options.bail`, else the
|
|
4394
4404
|
* definition's `bail`, else the graceful {@link import('./constants.js').DEFAULT_BAIL}; it
|
|
4395
4405
|
* feeds {@link import('./helpers.js').deriveWorkflowStatus}. Per-phase / per-task initial
|
|
4396
4406
|
* listeners + metadata travel through `options.phases[id].on` /
|
|
4397
4407
|
* `options.phases[id].tasks[id]` (the nested-by-id bag). The W-b tree is the
|
|
4398
|
-
* state machine
|
|
4408
|
+
* state machine only — it does not execute tasks (W-c drives the transitions).
|
|
4399
4409
|
*
|
|
4400
4410
|
* `options.functions` is the {@link import('./types.js').WorkflowRegistry} registry each live
|
|
4401
|
-
* task's `behavior` name resolves against
|
|
4411
|
+
* task's `behavior` name resolves against once at construction into its runtime
|
|
4402
4412
|
* {@link import('./types.js').TaskInterface.handler}. An omitted name is the deliberate no-op;
|
|
4403
4413
|
* an unresolved present name remains inspectable but is rejected if execution is attempted.
|
|
4404
4414
|
*
|
|
@@ -4425,14 +4435,14 @@ function createWorkflow(definition, options) {
|
|
|
4425
4435
|
* @remarks
|
|
4426
4436
|
* Seeds an initial all-`pending` {@link WorkflowSnapshot} from the definition and constructs the
|
|
4427
4437
|
* live {@link WorkflowInterface} over it. `bail` is the caller's own override, forwarded to
|
|
4428
|
-
* {@link definitionToSnapshot} so it reaches
|
|
4438
|
+
* {@link definitionToSnapshot} so it reaches both tiers: the workflow snapshot and the inheritance
|
|
4429
4439
|
* default of every phase that declares no `bail` of its own, while a phase declaring one still
|
|
4430
4440
|
* wins. Omitted, the definition's own `bail` governs, defaulting to the graceful
|
|
4431
4441
|
* {@link import('./constants.js').DEFAULT_BAIL}.
|
|
4432
4442
|
*
|
|
4433
|
-
* `captured` is forwarded to the entity
|
|
4443
|
+
* `captured` is forwarded to the entity unchanged — its own `bail` is deliberately not replaced
|
|
4434
4444
|
* with the resolved policy, because the snapshot already carries the resolved value at both tiers
|
|
4435
|
-
* and an injected one would make `Workflow` read it as an
|
|
4445
|
+
* and an injected one would make `Workflow` read it as an explicit uniform override and clobber
|
|
4436
4446
|
* the per-phase overrides. Each task's `behavior` / `retries` / `timeout` travel onto the snapshot
|
|
4437
4447
|
* too, so `captured.functions` resolves every handler identically whether the tree is built fresh
|
|
4438
4448
|
* or restored. Pass a bag {@link captureWorkflowOptions} already owns: this constructs over it
|
|
@@ -4462,11 +4472,11 @@ function createWorkflowTree(definition, captured) {
|
|
|
4462
4472
|
*
|
|
4463
4473
|
* @remarks
|
|
4464
4474
|
* Round-trip fidelity is paramount: a `snapshot()` → `createRestoredWorkflow()` reproduces the
|
|
4465
|
-
* same status at every node (each `#override` restored
|
|
4475
|
+
* same status at every node (each `#override` restored directly from the snapshot's own
|
|
4466
4476
|
* `override` field, not guessed from a status divergence), the same recorded
|
|
4467
4477
|
* {@link import('./types.js').TaskResult}s, and the same positional order (an interior
|
|
4468
|
-
* `skip` / `remove` survives). The snapshot is
|
|
4469
|
-
* policy it ran under, so the restore re-derives status
|
|
4478
|
+
* `skip` / `remove` survives). The snapshot is self-contained — it persists the `bail`
|
|
4479
|
+
* policy it ran under, so the restore re-derives status identically without a silent
|
|
4470
4480
|
* default; the snapshot's `bail` is the source of truth, while an explicit `options.bail`
|
|
4471
4481
|
* still wins when supplied (to deliberately re-run under a different policy). A structurally
|
|
4472
4482
|
* invalid snapshot (a status — or override — outside the lifecycle vocabulary, or a
|
|
@@ -4492,7 +4502,8 @@ function createRestoredWorkflow(snapshot, options) {
|
|
|
4492
4502
|
return new Workflow(cloneWorkflowSnapshot(snapshot), captured);
|
|
4493
4503
|
}
|
|
4494
4504
|
/**
|
|
4495
|
-
* Builds an interrupted workflow back to life at its remaining retry budget
|
|
4505
|
+
* Builds an interrupted workflow back to life at its remaining retry budget, normalizing a
|
|
4506
|
+
* leaf whose attempts are exhausted into a recovery failure.
|
|
4496
4507
|
*
|
|
4497
4508
|
* @remarks
|
|
4498
4509
|
* Each phase captures every unique initial `behavior` binding once before constructing tasks. Recovery
|
|
@@ -4521,18 +4532,17 @@ function createRecoveredWorkflow(snapshot, options) {
|
|
|
4521
4532
|
}
|
|
4522
4533
|
/**
|
|
4523
4534
|
* Creates the in-memory durable {@link WorkflowStoreInterface} — a process-lifetime
|
|
4524
|
-
* {@link MemoryWorkflowStore} persisting {@link WorkflowSnapshot}s by workflow id, the
|
|
4525
|
-
* backend behind the W-d persistence seam.
|
|
4535
|
+
* {@link MemoryWorkflowStore} persisting {@link WorkflowSnapshot}s by workflow id, the default
|
|
4536
|
+
* backend behind the W-d persistence seam. It takes no options and expires nothing: a
|
|
4537
|
+
* persisted run state lives until an explicit `delete`.
|
|
4526
4538
|
*
|
|
4527
4539
|
* @remarks
|
|
4528
4540
|
* The snapshot analogue of the server package's `createMemorySessionStore`
|
|
4529
|
-
* (and the `createMemoryQueueStore` family)
|
|
4530
|
-
*
|
|
4531
|
-
* an explicit `delete`. This is
|
|
4532
|
-
* the zero-plumbing DEFAULT (a plain `Map`); its driver-pluggable twin is
|
|
4541
|
+
* (and the `createMemoryQueueStore` family) is the zero-plumbing default (a plain `Map`); its
|
|
4542
|
+
* driver-pluggable twin is
|
|
4533
4543
|
* {@link createDatabaseWorkflowStore} (the snapshot as one opaque JSON column over a `databases`
|
|
4534
|
-
* table) — for a
|
|
4535
|
-
* driver, and it swaps in
|
|
4544
|
+
* table) — for a durable store (run-state surviving a restart) pass it a JSON / SQLite / IndexedDB
|
|
4545
|
+
* driver, and it swaps in without touching the runner or the entity tree. Restore stays a caller
|
|
4536
4546
|
* concern: read a snapshot back and rebuild the live tree with {@link createRestoredWorkflow}.
|
|
4537
4547
|
*
|
|
4538
4548
|
* @returns A memory-backed {@link WorkflowStoreInterface}
|
|
@@ -4554,22 +4564,22 @@ function createMemoryWorkflowStore() {
|
|
|
4554
4564
|
/**
|
|
4555
4565
|
* Creates a {@link DatabaseWorkflowStore} over any {@link DriverInterface} — the durable,
|
|
4556
4566
|
* driver-pluggable backing for the W-d persistence seam, the opt-in twin of
|
|
4557
|
-
* {@link createMemoryWorkflowStore}.
|
|
4567
|
+
* {@link createMemoryWorkflowStore}. It holds the snapshot as one opaque JSON column, and its
|
|
4568
|
+
* `driver` defaults to memory, so it works before any durable driver is passed.
|
|
4558
4569
|
*
|
|
4559
4570
|
* @remarks
|
|
4560
|
-
* Builds a one-table database (`snapshots`, keyed by `id`) over the supplied driver
|
|
4561
|
-
*
|
|
4571
|
+
* Builds a one-table database (`snapshots`, keyed by `id`) over the supplied driver. The column map
|
|
4572
|
+
* is `{ id; snapshot }` where `snapshot` is a
|
|
4562
4573
|
* `rawShape` (a JSON blob), exactly as `createDatabaseQueueStore` stores its `input`. The
|
|
4563
|
-
* snapshot is already a
|
|
4564
|
-
*
|
|
4574
|
+
* snapshot is already a complete, self-contained, pure-JSON payload, so storing it whole is lossless
|
|
4575
|
+
* and keeps the row type flat — a structured multi-column snapshot table would force the contract to
|
|
4565
4576
|
* `Infer` the deeply-nested snapshot shape (workflow → phases → tasks → results) and trip TS2589;
|
|
4566
4577
|
* the opaque column sidesteps it (the column reads back as `unknown`, owned and narrowed on `get` by
|
|
4567
4578
|
* {@link cloneWorkflowSnapshot}, whose semantic pass is
|
|
4568
|
-
* {@link import('./validators.js').isOwnedWorkflowSnapshot}).
|
|
4569
|
-
*
|
|
4570
|
-
* `createJSONDriver` / `createSQLiteDriver` (or a browser IndexedDB driver) for a persistent one —
|
|
4579
|
+
* {@link import('./validators.js').isOwnedWorkflowSnapshot}). Pass a server `createJSONDriver` /
|
|
4580
|
+
* `createSQLiteDriver` (or a browser IndexedDB driver) for a persistent one —
|
|
4571
4581
|
* the durability is the driver's job, the store engine is shared. It swaps in behind
|
|
4572
|
-
* {@link WorkflowStoreInterface}
|
|
4582
|
+
* {@link WorkflowStoreInterface} without touching the runner or the entity tree.
|
|
4573
4583
|
*
|
|
4574
4584
|
* @param driver - The storage backend the snapshots persist to (defaults to {@link createMemoryDriver})
|
|
4575
4585
|
* @returns A {@link WorkflowStoreInterface} over the driver
|
|
@@ -4597,22 +4607,22 @@ function createDatabaseWorkflowStore(driver = (0, _orkestrel_database.createMemo
|
|
|
4597
4607
|
}).table("snapshots"));
|
|
4598
4608
|
}
|
|
4599
4609
|
/**
|
|
4600
|
-
* Creates the thin orchestrator — a {@link WorkflowRunnerInterface} — that
|
|
4601
|
-
* workflow tree by
|
|
4602
|
-
* task dispatched through its
|
|
4610
|
+
* Creates the thin orchestrator — a {@link WorkflowRunnerInterface} — that executes a live W-b
|
|
4611
|
+
* workflow tree by composing the shipped substrate: phases sequential, tasks concurrent, each
|
|
4612
|
+
* task dispatched through its own resolved handler under the workflow's `bail` policy. The
|
|
4613
|
+
* engine is pure — it carries no behavior or provider registry, and its only option is the
|
|
4614
|
+
* scheduler it paces phase boundaries with.
|
|
4603
4615
|
*
|
|
4604
4616
|
* @remarks
|
|
4605
|
-
*
|
|
4606
|
-
*
|
|
4607
|
-
* resolved its own {@link import('./types.js').WorkflowFunction} into
|
|
4608
|
-
* {@link import('./types.js').TaskInterface.handler} ONCE at construction, from the
|
|
4617
|
+
* Each live task already resolved its own {@link import('./types.js').WorkflowFunction} into
|
|
4618
|
+
* {@link import('./types.js').TaskInterface.handler} once at construction, from the
|
|
4609
4619
|
* {@link WorkflowOptions.functions} registry supplied to `execute` / {@link createWorkflow}.
|
|
4610
4620
|
* Per-phase bounded concurrency is one {@link createRunner} per phase; `bail` maps onto that
|
|
4611
4621
|
* Runner's fail-fast (`true` — the first failure aborts the in-flight siblings + skips the
|
|
4612
4622
|
* rest) vs settle-all (`false` — failures are recorded, the run finishes); the run-level abort
|
|
4613
4623
|
* / timeout / budget ({@link import('./types.js').WorkflowRunOptions}) fold through
|
|
4614
4624
|
* `AbortSignal.any` (the agent runtime's pattern); pacing is the shipped scheduler.
|
|
4615
|
-
* `execute(definition, options?)`
|
|
4625
|
+
* `execute(definition, options?)` builds the live tree from the definition itself (through
|
|
4616
4626
|
* {@link createWorkflow} — one source of truth, returned in `WorkflowResult.workflow`), drives
|
|
4617
4627
|
* the live entity (`start` → `complete` / `fail`), and resolves a
|
|
4618
4628
|
* {@link import('./types.js').WorkflowResult}.
|
|
@@ -4626,19 +4636,43 @@ function createDatabaseWorkflowStore(driver = (0, _orkestrel_database.createMemo
|
|
|
4626
4636
|
* See {@link WorkflowRunnerOptions}.
|
|
4627
4637
|
* @returns A working {@link WorkflowRunnerInterface}
|
|
4628
4638
|
*
|
|
4629
|
-
* @example
|
|
4639
|
+
* @example Author a definition and run it
|
|
4630
4640
|
* ```ts
|
|
4631
4641
|
* import { createWorkflowRunner } from '@orkestrel/workflow'
|
|
4642
|
+
* import type { WorkflowDefinition } from '@orkestrel/workflow'
|
|
4643
|
+
*
|
|
4644
|
+
* const definition: WorkflowDefinition = {
|
|
4645
|
+
* id: 'release',
|
|
4646
|
+
* name: 'Release',
|
|
4647
|
+
* phases: [
|
|
4648
|
+
* {
|
|
4649
|
+
* id: 'build',
|
|
4650
|
+
* name: 'Build',
|
|
4651
|
+
* tasks: [
|
|
4652
|
+
* { id: 'compile', name: 'Compile', behavior: 'compile' },
|
|
4653
|
+
* { id: 'lint', name: 'Lint', behavior: 'lint' },
|
|
4654
|
+
* ],
|
|
4655
|
+
* },
|
|
4656
|
+
* {
|
|
4657
|
+
* id: 'ship',
|
|
4658
|
+
* name: 'Ship',
|
|
4659
|
+
* tasks: [{ id: 'publish', name: 'Publish', behavior: 'publish' }],
|
|
4660
|
+
* },
|
|
4661
|
+
* ],
|
|
4662
|
+
* }
|
|
4663
|
+
*
|
|
4664
|
+
* const runner = createWorkflowRunner() // a pure engine — no registries
|
|
4632
4665
|
*
|
|
4633
|
-
* const runner = createWorkflowRunner()
|
|
4634
|
-
* const definition = { id: 'w', name: 'W', phases: [{ id: 'p', name: 'P', tasks: [
|
|
4635
|
-
* { id: 't', name: 'T', behavior: 'compile' },
|
|
4636
|
-
* ] }] }
|
|
4637
4666
|
* const result = await runner.execute(definition, {
|
|
4638
|
-
* functions: {
|
|
4667
|
+
* functions: {
|
|
4668
|
+
* compile: async (controller) => `built ${controller.task.id}`,
|
|
4669
|
+
* lint: async () => 'clean',
|
|
4670
|
+
* publish: async () => 'published',
|
|
4671
|
+
* },
|
|
4639
4672
|
* })
|
|
4640
4673
|
* result.status // 'completed'
|
|
4641
|
-
* result.workflow.phase('
|
|
4674
|
+
* result.workflow.phase('build')?.task('compile')?.status // 'completed'
|
|
4675
|
+
* result.results // every settled task's TaskResult, in positional order
|
|
4642
4676
|
* ```
|
|
4643
4677
|
*/
|
|
4644
4678
|
function createWorkflowRunner(options) {
|
|
@@ -4647,15 +4681,17 @@ function createWorkflowRunner(options) {
|
|
|
4647
4681
|
/**
|
|
4648
4682
|
* Creates a {@link WorkflowManagerInterface} — the store-backed registry of
|
|
4649
4683
|
* {@link WorkflowInterface}s, the additive manager tier mirroring the `@orkestrel/agent`
|
|
4650
|
-
* line's `createConversationManager` / `createWorkspaceManager`.
|
|
4684
|
+
* line's `createConversationManager` / `createWorkspaceManager`. The returned registry makes
|
|
4685
|
+
* hydrated named work runnable when `options.functions` is supplied, and leaves it
|
|
4686
|
+
* inspectable when it is not.
|
|
4651
4687
|
*
|
|
4652
4688
|
* @remarks
|
|
4653
4689
|
* `options.functions` flows into every workflow the manager mints (`add`, through
|
|
4654
4690
|
* {@link createWorkflow}) or hydrates (`open`'s registry-miss path, through
|
|
4655
|
-
* {@link createRestoredWorkflow}), so a hydrated workflow is
|
|
4656
|
-
* mirror. `options.store` is the
|
|
4691
|
+
* {@link createRestoredWorkflow}), so a hydrated workflow is runnable rather than a dead snapshot
|
|
4692
|
+
* mirror. `options.store` is the exact analogue of the twins' `store` seam — omitted ⇒ the
|
|
4657
4693
|
* manager is registry-only (`open` resolves only what is registered, `save` is a no-op). This
|
|
4658
|
-
* is
|
|
4694
|
+
* is purely additive: direct {@link WorkflowStoreInterface} use and
|
|
4659
4695
|
* {@link createRestoredWorkflow} remain valid — the manager is one more caller-driven persistence
|
|
4660
4696
|
* seam, not a replacement.
|
|
4661
4697
|
*
|
|
@@ -4670,7 +4706,7 @@ function createWorkflowRunner(options) {
|
|
|
4670
4706
|
* store: createMemoryWorkflowStore(),
|
|
4671
4707
|
* functions: { compile: async (controller) => `built ${controller.task.id}` },
|
|
4672
4708
|
* })
|
|
4673
|
-
* const workflow = manager.add(definition) // minted, registered,
|
|
4709
|
+
* const workflow = manager.add(definition) // minted, registered, runnable
|
|
4674
4710
|
* await manager.save(workflow.id) // persisted to the store
|
|
4675
4711
|
* const reopened = await manager.open(workflow.id) // already registered — no store hit
|
|
4676
4712
|
* ```
|
|
@@ -4726,12 +4762,13 @@ function createScheduler() {
|
|
|
4726
4762
|
}
|
|
4727
4763
|
/**
|
|
4728
4764
|
* Creates a thin generic orchestrator that drives declared units — and any they
|
|
4729
|
-
* `spawn` — through a bounded-concurrency queue, collecting their results in order
|
|
4765
|
+
* `spawn` — through a bounded-concurrency queue, collecting their results in order and
|
|
4766
|
+
* failing the run fast on the first genuine unit failure.
|
|
4730
4767
|
*
|
|
4731
4768
|
* @remarks
|
|
4732
4769
|
* The Runner composes the workers `Queue` for backpressure, FIFO ordering, bounded
|
|
4733
4770
|
* concurrency, retries, and the per-attempt timeout — it adds only orchestration, not
|
|
4734
|
-
* a second concurrency engine. `execute(inputs)` runs the unit set
|
|
4771
|
+
* a second concurrency engine. `execute(inputs)` runs the unit set once (a second call
|
|
4735
4772
|
* throws) and resolves the units' results in order: the declared inputs first, then
|
|
4736
4773
|
* any `spawn`ed siblings in spawn order. Each unit's handler gets a `Controller` — its
|
|
4737
4774
|
* `id` / `input`, a `signal` that fires on the unit's `abort`, a runner-level `abort`,
|
|
@@ -4741,8 +4778,8 @@ function createScheduler() {
|
|
|
4741
4778
|
* typed `emitter` surfaces `start` / `unit` / `spawn` / `settle` / `fail` / `finish` / `abort`.
|
|
4742
4779
|
*
|
|
4743
4780
|
* Because `spawn` is fire-and-track (the runner awaits the whole spawn closure through an
|
|
4744
|
-
* outstanding-unit count, not a one-time snapshot), a handler need
|
|
4745
|
-
* for them to run — and on a bounded runner do
|
|
4781
|
+
* outstanding-unit count, not a one-time snapshot), a handler need not await its spawns
|
|
4782
|
+
* for them to run — and on a bounded runner do not `await` a spawn inline (a slot-holding
|
|
4746
4783
|
* handler awaiting its own spawn can deadlock); fan out and return instead.
|
|
4747
4784
|
*
|
|
4748
4785
|
* @typeParam TInput - The work input each unit carries
|