@orkestrel/workflow 0.0.16 → 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 +12 -6
- package/dist/src/browser/index.d.ts +53 -52
- package/dist/src/browser/index.js +55 -72
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/core/index.cjs +1356 -833
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +1951 -1407
- package/dist/src/core/index.d.ts +1951 -1407
- package/dist/src/core/index.js +1341 -825
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +16 -21
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +17 -16
- package/dist/src/server/index.d.ts +17 -16
- package/dist/src/server/index.js +17 -22
- package/dist/src/server/index.js.map +1 -1
- package/package.json +20 -21
package/dist/src/core/index.cjs
CHANGED
|
@@ -5,35 +5,69 @@ let _orkestrel_database = require("@orkestrel/database");
|
|
|
5
5
|
let _orkestrel_emitter = require("@orkestrel/emitter");
|
|
6
6
|
let _orkestrel_timeout = require("@orkestrel/timeout");
|
|
7
7
|
let _orkestrel_queue = require("@orkestrel/queue");
|
|
8
|
+
//#region src/core/errors.ts
|
|
9
|
+
/**
|
|
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.
|
|
13
|
+
*
|
|
14
|
+
* @remarks
|
|
15
|
+
* Raised for an illegal lifecycle transition
|
|
16
|
+
* (`TRANSITION`), a structurally invalid {@link import('./types.js').WorkflowSnapshot}
|
|
17
|
+
* boundary (`RESTORE`), a refused structural/activity edit (`MUTATION`), a host
|
|
18
|
+
* schedule refused before arming because the caller's `signal` is not a native
|
|
19
|
+
* `AbortSignal` (`SCHEDULE`, delivered as a rejected promise), or a broken internal
|
|
20
|
+
* invariant (`INVARIANT`).
|
|
21
|
+
*/
|
|
22
|
+
var WorkflowError = class extends Error {
|
|
23
|
+
code;
|
|
24
|
+
context;
|
|
25
|
+
constructor(code, message, context) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.name = "WorkflowError";
|
|
28
|
+
this.code = code;
|
|
29
|
+
if (context !== void 0) this.context = context;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Narrows an unknown caught value to a {@link WorkflowError}.
|
|
34
|
+
*
|
|
35
|
+
* @param value - The value to test (typically a `catch` binding)
|
|
36
|
+
* @returns True if `value` is a {@link WorkflowError}; false otherwise
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```ts
|
|
40
|
+
* try {
|
|
41
|
+
* task.complete('done')
|
|
42
|
+
* } catch (error) {
|
|
43
|
+
* if (isWorkflowError(error) && error.code === 'TRANSITION') retry()
|
|
44
|
+
* }
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
function isWorkflowError(value) {
|
|
48
|
+
try {
|
|
49
|
+
return value instanceof WorkflowError;
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
//#endregion
|
|
8
55
|
//#region src/core/constants.ts
|
|
9
|
-
/**
|
|
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
|
+
*/
|
|
10
60
|
var DEFAULT_BAIL = false;
|
|
11
61
|
/**
|
|
12
|
-
*
|
|
62
|
+
* Lists every {@link LifecycleStatus} value, frozen — the vocabulary every tier draws from,
|
|
63
|
+
* in the order `pending`, `running`, `completed`, `failed`, `skipped`, `stopped`.
|
|
13
64
|
*
|
|
14
65
|
* @remarks
|
|
15
66
|
* Ordered pending → running → terminal (`completed` / `failed` / `skipped` /
|
|
16
|
-
* `stopped`). The source of truth for the union
|
|
67
|
+
* `stopped`). The runtime source of truth for the union:
|
|
68
|
+
* {@link import('./validators.js').isLifecycleStatus} reads this array.
|
|
17
69
|
*/
|
|
18
|
-
var
|
|
19
|
-
"pending",
|
|
20
|
-
"running",
|
|
21
|
-
"completed",
|
|
22
|
-
"failed",
|
|
23
|
-
"skipped",
|
|
24
|
-
"stopped"
|
|
25
|
-
]);
|
|
26
|
-
/** Every {@link PhaseStatus} value, frozen — the lifecycle vocabulary of a phase. */
|
|
27
|
-
var PHASE_STATUSES = Object.freeze([
|
|
28
|
-
"pending",
|
|
29
|
-
"running",
|
|
30
|
-
"completed",
|
|
31
|
-
"failed",
|
|
32
|
-
"skipped",
|
|
33
|
-
"stopped"
|
|
34
|
-
]);
|
|
35
|
-
/** Every {@link WorkflowStatus} value, frozen — the lifecycle vocabulary of a workflow. */
|
|
36
|
-
var WORKFLOW_STATUSES = Object.freeze([
|
|
70
|
+
var LIFECYCLE_STATUSES = Object.freeze([
|
|
37
71
|
"pending",
|
|
38
72
|
"running",
|
|
39
73
|
"completed",
|
|
@@ -42,21 +76,21 @@ var WORKFLOW_STATUSES = Object.freeze([
|
|
|
42
76
|
"stopped"
|
|
43
77
|
]);
|
|
44
78
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
79
|
+
* Lists the terminal {@link LifecycleStatus} values, frozen — `completed`, `failed`, `skipped`,
|
|
80
|
+
* and `stopped`, each a state a node never transitions out of.
|
|
47
81
|
*
|
|
48
82
|
* @remarks
|
|
49
83
|
* The source of truth behind {@link import('./helpers.js').isTerminalStatus}.
|
|
50
84
|
* `pending` and `running` are the only non-terminal members.
|
|
51
85
|
*/
|
|
52
|
-
var
|
|
86
|
+
var TERMINAL_STATUSES = Object.freeze([
|
|
53
87
|
"completed",
|
|
54
88
|
"failed",
|
|
55
89
|
"skipped",
|
|
56
90
|
"stopped"
|
|
57
91
|
]);
|
|
58
92
|
/**
|
|
59
|
-
*
|
|
93
|
+
* Declares the legal {@link LifecycleStatus} transition graph of the live W-b task state machine —
|
|
60
94
|
* each current status mapped to the statuses it may move to directly, frozen.
|
|
61
95
|
*
|
|
62
96
|
* @remarks
|
|
@@ -86,94 +120,161 @@ var TASK_TRANSITIONS = Object.freeze({
|
|
|
86
120
|
stopped: []
|
|
87
121
|
});
|
|
88
122
|
/**
|
|
89
|
-
*
|
|
123
|
+
* Names the default per-phase task concurrency the {@link import('./factories.js').createWorkflowRunner}
|
|
90
124
|
* runner applies when a {@link import('./types.js').PhaseDefinition} omits its `concurrency`
|
|
91
|
-
* throttle — a cap that is effectively unbounded for any realistic phase.
|
|
125
|
+
* throttle — `1024`, a cap that is effectively unbounded for any realistic phase.
|
|
92
126
|
*
|
|
93
127
|
* @remarks
|
|
94
|
-
* The determinism principle fixes that a phase's tasks run
|
|
128
|
+
* The determinism principle fixes that a phase's tasks run concurrently; `concurrency` is
|
|
95
129
|
* only an optional resource throttle (max-in-flight). With none declared, the runner runs
|
|
96
130
|
* all of a phase's tasks at once — modelled as this finite cap so the value flows straight
|
|
97
131
|
* into the substrate {@link import('./types.js').RunnerInterface}'s `concurrency` (which
|
|
98
132
|
* expects a positive integer) without a special unbounded branch. No realistic phase
|
|
99
133
|
* declares enough tasks to reach it, so it behaves as "run them all".
|
|
100
134
|
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
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
|
|
103
137
|
* must be a value whose eager allocation cost is negligible for every default-concurrency
|
|
104
138
|
* phase — a million-unit default meant ~1e6 promise/closure allocations per such phase. A
|
|
105
|
-
* phase may still
|
|
139
|
+
* phase may still declare a larger explicit `concurrency` and pays that allocation knowingly.
|
|
106
140
|
*/
|
|
107
141
|
var DEFAULT_PHASE_CONCURRENCY = 1024;
|
|
108
142
|
/**
|
|
109
|
-
*
|
|
143
|
+
* Names the largest delay representable by the host timer APIs without overflow or clamping,
|
|
144
|
+
* `2_147_483_647` milliseconds.
|
|
110
145
|
*/
|
|
111
146
|
var MAX_TIMER_MS = 2147483647;
|
|
147
|
+
/**
|
|
148
|
+
* Lists the {@link WorkflowEventMap} / {@link PhaseEventMap} events that make a durable observer
|
|
149
|
+
* re-persist the live tree, frozen — `start`, `complete`, `fail`, `skip`, `stop`, `move`, and
|
|
150
|
+
* `update`.
|
|
151
|
+
*
|
|
152
|
+
* @remarks
|
|
153
|
+
* The two maps carry the same event names, so one list serves both tiers. It is the source of
|
|
154
|
+
* truth behind {@link import('./WorkflowPersistence.js').WorkflowPersistence}'s attach and detach
|
|
155
|
+
* passes: subscribing and unsubscribing loop over these names, so an added event reaches both
|
|
156
|
+
* passes from one edit. `add` and `remove` are deliberately absent — they carry the new or dropped
|
|
157
|
+
* child, so the persistence layer binds its own attaching handler to them instead.
|
|
158
|
+
*/
|
|
159
|
+
var PERSISTED_NODE_EVENTS = Object.freeze([
|
|
160
|
+
"start",
|
|
161
|
+
"complete",
|
|
162
|
+
"fail",
|
|
163
|
+
"skip",
|
|
164
|
+
"stop",
|
|
165
|
+
"move",
|
|
166
|
+
"update"
|
|
167
|
+
]);
|
|
168
|
+
/**
|
|
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`.
|
|
171
|
+
*
|
|
172
|
+
* @remarks
|
|
173
|
+
* The leaf counterpart of {@link PERSISTED_NODE_EVENTS}, and the source of truth behind the task
|
|
174
|
+
* attach and detach passes of
|
|
175
|
+
* {@link import('./WorkflowPersistence.js').WorkflowPersistence}. `report` and `pulse` join the
|
|
176
|
+
* lifecycle events because an accepted activity frame changes the persisted snapshot; a leaf has
|
|
177
|
+
* no children, so there is no structural event to bind separately.
|
|
178
|
+
*/
|
|
179
|
+
var PERSISTED_TASK_EVENTS = Object.freeze([
|
|
180
|
+
"start",
|
|
181
|
+
"complete",
|
|
182
|
+
"fail",
|
|
183
|
+
"skip",
|
|
184
|
+
"stop",
|
|
185
|
+
"report",
|
|
186
|
+
"pulse"
|
|
187
|
+
]);
|
|
112
188
|
//#endregion
|
|
113
|
-
//#region src/core/
|
|
189
|
+
//#region src/core/validators.ts
|
|
114
190
|
/**
|
|
115
|
-
*
|
|
191
|
+
* Checks whether an unknown value belongs to the workflow lifecycle vocabulary.
|
|
116
192
|
*
|
|
117
193
|
* @remarks
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
194
|
+
* Reads {@link import('./constants.js').LIFECYCLE_STATUSES}, the runtime array every tier draws
|
|
195
|
+
* from, so the vocabulary has one definition rather than a hard-coded copy per guard.
|
|
196
|
+
*
|
|
197
|
+
* @param value - The value to test
|
|
198
|
+
* @returns True if `value` is a {@link LifecycleStatus}; false otherwise
|
|
199
|
+
*
|
|
200
|
+
* @example
|
|
201
|
+
* ```ts
|
|
202
|
+
* isLifecycleStatus('running') // true
|
|
203
|
+
* isLifecycleStatus('paused') // false
|
|
204
|
+
* ```
|
|
124
205
|
*/
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
constructor(code, message, context) {
|
|
129
|
-
super(message);
|
|
130
|
-
this.name = "WorkflowError";
|
|
131
|
-
this.code = code;
|
|
132
|
-
if (context !== void 0) this.context = context;
|
|
133
|
-
}
|
|
134
|
-
};
|
|
206
|
+
function isLifecycleStatus(value) {
|
|
207
|
+
return LIFECYCLE_STATUSES.some((status) => status === value);
|
|
208
|
+
}
|
|
135
209
|
/**
|
|
136
|
-
*
|
|
210
|
+
* Tests a normalized persisted task failure.
|
|
137
211
|
*
|
|
138
|
-
* @
|
|
139
|
-
*
|
|
212
|
+
* @remarks
|
|
213
|
+
* The exact-record guard behind a persisted {@link TaskFailure}: exactly `origin` and `message`,
|
|
214
|
+
* an `origin` drawn from the {@link import('./types.js').TaskFailureOrigin} vocabulary, and a
|
|
215
|
+
* non-empty `message`. Total — a hostile prototype or accessor answers `false` rather than
|
|
216
|
+
* throwing.
|
|
217
|
+
*
|
|
218
|
+
* @param value - The value to test
|
|
219
|
+
* @returns True if `value` is a persisted {@link TaskFailure}; false otherwise
|
|
140
220
|
*
|
|
141
221
|
* @example
|
|
142
222
|
* ```ts
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
* } catch (error) {
|
|
146
|
-
* if (isWorkflowError(error) && error.code === 'TRANSITION') retry()
|
|
147
|
-
* }
|
|
223
|
+
* isTaskFailure({ origin: 'handler', message: 'boom' }) // true
|
|
224
|
+
* isTaskFailure({ origin: 'handler' }) // false
|
|
148
225
|
* ```
|
|
149
226
|
*/
|
|
150
|
-
function
|
|
227
|
+
function isTaskFailure(value) {
|
|
151
228
|
try {
|
|
152
|
-
return value
|
|
229
|
+
return (0, _orkestrel_contract.isRecord)(value) && Object.keys(value).every((key) => key === "origin" || key === "message") && (value.origin === "handler" || value.origin === "timeout" || value.origin === "recovery") && (0, _orkestrel_contract.isNonEmptyString)(value.message);
|
|
153
230
|
} catch {
|
|
154
231
|
return false;
|
|
155
232
|
}
|
|
156
233
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
|
|
234
|
+
/**
|
|
235
|
+
* Checks whether an unknown value is a live workflow entity rather than a definition.
|
|
236
|
+
*
|
|
237
|
+
* @remarks
|
|
238
|
+
* The discriminator behind the overloaded
|
|
239
|
+
* {@link import('./types.js').WorkflowRunnerInterface.execute}: a
|
|
240
|
+
* {@link import('./types.js').WorkflowInterface} is the only one of the two carrying `destroyed`
|
|
241
|
+
* (runtime-only, never a field on the pure-JSON
|
|
242
|
+
* {@link import('./types.js').WorkflowDefinition}) and a callable `snapshot`. Requiring both is
|
|
243
|
+
* sturdier than `destroyed` alone — a definition could coincidentally carry a `destroyed` field as
|
|
244
|
+
* arbitrary data, and pairing it with a function-typed `snapshot` narrows to the actual entity
|
|
245
|
+
* shape without an `as`. It reads a live class instance, so it tests object identity rather than a
|
|
246
|
+
* plain-record brand, and it is total: any other value answers `false`.
|
|
247
|
+
*
|
|
248
|
+
* @param value - The value to test
|
|
249
|
+
* @returns True if `value` is a live {@link WorkflowInterface}; false otherwise
|
|
250
|
+
*
|
|
251
|
+
* @example
|
|
252
|
+
* ```ts
|
|
253
|
+
* isWorkflowInterface(createWorkflow(definition)) // true
|
|
254
|
+
* isWorkflowInterface(definition) // false
|
|
255
|
+
* ```
|
|
256
|
+
*/
|
|
257
|
+
function isWorkflowInterface(value) {
|
|
165
258
|
try {
|
|
166
|
-
return (0, _orkestrel_contract.
|
|
259
|
+
return (0, _orkestrel_contract.isObject)(value) && "destroyed" in value && "snapshot" in value && (0, _orkestrel_contract.isFunction)(value.snapshot);
|
|
167
260
|
} catch {
|
|
168
261
|
return false;
|
|
169
262
|
}
|
|
170
263
|
}
|
|
171
264
|
/**
|
|
172
|
-
*
|
|
265
|
+
* Validates a safe owned JSON graph as a coherent workflow snapshot.
|
|
173
266
|
*
|
|
174
267
|
* @remarks
|
|
175
268
|
* Callers at hostile boundaries use {@link isWorkflowSnapshot}, which owns the
|
|
176
269
|
* graph first so this semantic pass never observes accessors or prototypes.
|
|
270
|
+
*
|
|
271
|
+
* @param value - The already-owned JSON graph to validate
|
|
272
|
+
* @returns True if `value` is a coherent {@link WorkflowSnapshot}; false otherwise
|
|
273
|
+
*
|
|
274
|
+
* @example
|
|
275
|
+
* ```ts
|
|
276
|
+
* isOwnedWorkflowSnapshot(workflow.snapshot()) // true
|
|
277
|
+
* ```
|
|
177
278
|
*/
|
|
178
279
|
function isOwnedWorkflowSnapshot(value) {
|
|
179
280
|
try {
|
|
@@ -195,7 +296,7 @@ function isOwnedWorkflowSnapshot(value) {
|
|
|
195
296
|
const statuses = [];
|
|
196
297
|
if (phase.tasks.length > 0) vacuous = false;
|
|
197
298
|
for (const task of phase.tasks) {
|
|
198
|
-
if (!(0, _orkestrel_contract.isRecord)(task) || !Object.keys(task).every((key) => key === "id" || key === "name" || key === "description" || key === "status" || key === "result" || key === "metadata" || key === "attempts" || key === "
|
|
299
|
+
if (!(0, _orkestrel_contract.isRecord)(task) || !Object.keys(task).every((key) => key === "id" || key === "name" || key === "description" || key === "status" || key === "result" || key === "metadata" || key === "attempts" || key === "behavior" || key === "retries" || key === "timeout" || key === "activity") || !(0, _orkestrel_contract.isNonEmptyString)(task.id) || taskIds.has(task.id) || !(0, _orkestrel_contract.isNonEmptyString)(task.name) || task.description !== void 0 && typeof task.description !== "string" || !isLifecycleStatus(task.status) || !(0, _orkestrel_contract.isRecord)(task.metadata) || !(0, _orkestrel_contract.isJSONValue)(task.metadata) || !(0, _orkestrel_contract.isInteger)(task.attempts) || task.attempts < 0 || task.behavior !== void 0 && !(0, _orkestrel_contract.isNonEmptyString)(task.behavior) || task.retries !== void 0 && (!(0, _orkestrel_contract.isInteger)(task.retries) || task.retries < 0) || task.timeout !== void 0 && (!(0, _orkestrel_contract.isInteger)(task.timeout) || task.timeout < 0 || task.timeout > 2147483647)) return false;
|
|
199
300
|
const budget = (task.retries ?? 0) + 1;
|
|
200
301
|
if (task.attempts > budget || task.status === "pending" && task.attempts >= budget) return false;
|
|
201
302
|
if (!(task.activity === void 0 || isTaskActivity(task.activity))) return false;
|
|
@@ -223,13 +324,82 @@ function isOwnedWorkflowSnapshot(value) {
|
|
|
223
324
|
return false;
|
|
224
325
|
}
|
|
225
326
|
}
|
|
226
|
-
/**
|
|
327
|
+
/**
|
|
328
|
+
* Guards the hostile boundary totally for a workflow snapshot.
|
|
329
|
+
*
|
|
330
|
+
* @remarks
|
|
331
|
+
* Owns the value first through the exact-JSON clone of `@orkestrel/contract`, then runs the
|
|
332
|
+
* semantic pass {@link isOwnedWorkflowSnapshot} over the owned copy — so no accessor, prototype,
|
|
333
|
+
* or cycle in the caller's graph is ever observed by the semantic pass. Total: an unclonable
|
|
334
|
+
* value answers `false` rather than throwing.
|
|
335
|
+
*
|
|
336
|
+
* @param value - The untrusted value to test
|
|
337
|
+
* @returns True if `value` is a coherent {@link WorkflowSnapshot}; false otherwise
|
|
338
|
+
*
|
|
339
|
+
* @example
|
|
340
|
+
* ```ts
|
|
341
|
+
* isWorkflowSnapshot(JSON.parse(payload)) // true only for a coherent snapshot
|
|
342
|
+
* ```
|
|
343
|
+
*/
|
|
227
344
|
function isWorkflowSnapshot(value) {
|
|
228
345
|
const cloned = (0, _orkestrel_contract.attempt)(() => (0, _orkestrel_contract.cloneJSONValue)(value));
|
|
229
346
|
return cloned.success && isOwnedWorkflowSnapshot(cloned.value);
|
|
230
347
|
}
|
|
231
348
|
/**
|
|
232
|
-
*
|
|
349
|
+
* Checks whether an unknown value is a valid list of task activity claims.
|
|
350
|
+
*
|
|
351
|
+
* @remarks
|
|
352
|
+
* The one guard behind both claim lists of a {@link TaskActivityInput} — its `operations` and its
|
|
353
|
+
* `constraints` — because {@link import('./types.js').TaskOperation} and
|
|
354
|
+
* {@link import('./types.js').TaskConstraint} are the same {@link TaskClaim} shape. Every member must be a plain record carrying exactly `id`, `name`, and
|
|
355
|
+
* `started`, with non-empty string `id` and `name`, a finite non-negative `started`, and an `id`
|
|
356
|
+
* unique within the list. Total: a hostile prototype, an accessor, or a cycle returns `false`
|
|
357
|
+
* rather than throwing.
|
|
358
|
+
*
|
|
359
|
+
* @param value - The value to test
|
|
360
|
+
* @returns True if `value` is a list of valid, uniquely identified claims; false otherwise
|
|
361
|
+
*
|
|
362
|
+
* @example
|
|
363
|
+
* ```ts
|
|
364
|
+
* isTaskClaimList([{ id: 'fetch', name: 'Fetch', started: 1 }]) // true
|
|
365
|
+
* isTaskClaimList([{ id: 'fetch', name: 'Fetch' }]) // false
|
|
366
|
+
* ```
|
|
367
|
+
*/
|
|
368
|
+
function isTaskClaimList(value) {
|
|
369
|
+
try {
|
|
370
|
+
if (!(0, _orkestrel_contract.isArray)(value)) return false;
|
|
371
|
+
const ids = /* @__PURE__ */ new Set();
|
|
372
|
+
for (const claim of value) {
|
|
373
|
+
if (!(0, _orkestrel_contract.isRecord)(claim)) return false;
|
|
374
|
+
const prototype = Object.getPrototypeOf(claim);
|
|
375
|
+
if (prototype !== Object.prototype && prototype !== null || !Object.keys(claim).every((key) => key === "id" || key === "name" || key === "started")) return false;
|
|
376
|
+
const id = claim.id;
|
|
377
|
+
const name = claim.name;
|
|
378
|
+
const started = claim.started;
|
|
379
|
+
if (!(0, _orkestrel_contract.isNonEmptyString)(id) || !(0, _orkestrel_contract.isNonEmptyString)(name) || !(0, _orkestrel_contract.isFiniteNumber)(started) || started < 0 || ids.has(id)) return false;
|
|
380
|
+
ids.add(id);
|
|
381
|
+
}
|
|
382
|
+
return true;
|
|
383
|
+
} catch {
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Tests whether an unknown value is a valid whole-frame activity report.
|
|
389
|
+
*
|
|
390
|
+
* @remarks
|
|
391
|
+
* The guard behind {@link import('./types.js').TaskInterface.report}: exactly the optional `note`,
|
|
392
|
+
* `progress`, `operations`, and `constraints` keys, with the two claim lists checked by
|
|
393
|
+
* {@link isTaskClaimList} and `progress` a finite non-negative value under an optional `total` at
|
|
394
|
+
* least as large. Total — a hostile prototype or accessor answers `false` rather than throwing.
|
|
395
|
+
*
|
|
396
|
+
* @param value - The value to test
|
|
397
|
+
* @returns True if `value` is a valid {@link TaskActivityInput}; false otherwise
|
|
398
|
+
*
|
|
399
|
+
* @example
|
|
400
|
+
* ```ts
|
|
401
|
+
* isTaskActivityInput({ note: 'compiling', progress: { progress: 2, total: 5 } }) // true
|
|
402
|
+
* ```
|
|
233
403
|
*/
|
|
234
404
|
function isTaskActivityInput(value) {
|
|
235
405
|
try {
|
|
@@ -250,41 +420,29 @@ function isTaskActivityInput(value) {
|
|
|
250
420
|
const message = progress.message;
|
|
251
421
|
if (!(0, _orkestrel_contract.isFiniteNumber)(reported) || reported < 0 || total !== void 0 && (!(0, _orkestrel_contract.isFiniteNumber)(total) || total < reported) || message !== void 0 && !(0, _orkestrel_contract.isNonEmptyString)(message)) return false;
|
|
252
422
|
}
|
|
253
|
-
if (operations !== void 0)
|
|
254
|
-
|
|
255
|
-
const ids = /* @__PURE__ */ new Set();
|
|
256
|
-
for (const operation of operations) {
|
|
257
|
-
if (!(0, _orkestrel_contract.isRecord)(operation)) return false;
|
|
258
|
-
const operationPrototype = Object.getPrototypeOf(operation);
|
|
259
|
-
if (operationPrototype !== Object.prototype && operationPrototype !== null || !Object.keys(operation).every((key) => key === "id" || key === "name" || key === "started")) return false;
|
|
260
|
-
const id = operation.id;
|
|
261
|
-
const name = operation.name;
|
|
262
|
-
const started = operation.started;
|
|
263
|
-
if (!(0, _orkestrel_contract.isNonEmptyString)(id) || !(0, _orkestrel_contract.isNonEmptyString)(name) || !(0, _orkestrel_contract.isFiniteNumber)(started) || started < 0 || ids.has(id)) return false;
|
|
264
|
-
ids.add(id);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
if (constraints !== void 0) {
|
|
268
|
-
if (!(0, _orkestrel_contract.isArray)(constraints)) return false;
|
|
269
|
-
const ids = /* @__PURE__ */ new Set();
|
|
270
|
-
for (const constraint of constraints) {
|
|
271
|
-
if (!(0, _orkestrel_contract.isRecord)(constraint)) return false;
|
|
272
|
-
const constraintPrototype = Object.getPrototypeOf(constraint);
|
|
273
|
-
if (constraintPrototype !== Object.prototype && constraintPrototype !== null || !Object.keys(constraint).every((key) => key === "id" || key === "name" || key === "started")) return false;
|
|
274
|
-
const id = constraint.id;
|
|
275
|
-
const name = constraint.name;
|
|
276
|
-
const started = constraint.started;
|
|
277
|
-
if (!(0, _orkestrel_contract.isNonEmptyString)(id) || !(0, _orkestrel_contract.isNonEmptyString)(name) || !(0, _orkestrel_contract.isFiniteNumber)(started) || started < 0 || ids.has(id)) return false;
|
|
278
|
-
ids.add(id);
|
|
279
|
-
}
|
|
280
|
-
}
|
|
423
|
+
if (operations !== void 0 && !isTaskClaimList(operations)) return false;
|
|
424
|
+
if (constraints !== void 0 && !isTaskClaimList(constraints)) return false;
|
|
281
425
|
return true;
|
|
282
426
|
} catch {
|
|
283
427
|
return false;
|
|
284
428
|
}
|
|
285
429
|
}
|
|
286
430
|
/**
|
|
287
|
-
*
|
|
431
|
+
* Tests whether an unknown value is valid persisted task activity.
|
|
432
|
+
*
|
|
433
|
+
* @remarks
|
|
434
|
+
* The persisted counterpart of {@link isTaskActivityInput}: the same frame plus the required
|
|
435
|
+
* `operations`, `constraints`, and a finite non-negative `updated` stamp, because a stored frame
|
|
436
|
+
* has already been accepted and normalized. Total — a hostile prototype or accessor answers
|
|
437
|
+
* `false` rather than throwing.
|
|
438
|
+
*
|
|
439
|
+
* @param value - The value to test
|
|
440
|
+
* @returns True if `value` is a persisted {@link TaskActivity}; false otherwise
|
|
441
|
+
*
|
|
442
|
+
* @example
|
|
443
|
+
* ```ts
|
|
444
|
+
* isTaskActivity({ operations: [], constraints: [], updated: 1 }) // true
|
|
445
|
+
* ```
|
|
288
446
|
*/
|
|
289
447
|
function isTaskActivity(value) {
|
|
290
448
|
try {
|
|
@@ -310,7 +468,7 @@ function isTaskActivity(value) {
|
|
|
310
468
|
//#endregion
|
|
311
469
|
//#region src/core/helpers.ts
|
|
312
470
|
/**
|
|
313
|
-
*
|
|
471
|
+
* Captures every top-level {@link WorkflowOptions} value exactly once into an owned plain bag.
|
|
314
472
|
*
|
|
315
473
|
* @remarks
|
|
316
474
|
* Direct property reads preserve inherited and non-enumerable option values while preventing
|
|
@@ -345,32 +503,159 @@ function captureWorkflowOptions(options) {
|
|
|
345
503
|
});
|
|
346
504
|
}
|
|
347
505
|
/**
|
|
348
|
-
*
|
|
349
|
-
*
|
|
506
|
+
* Tests whether a {@link LifecycleStatus} is terminal — `completed`, `failed`, `skipped`, or
|
|
507
|
+
* `stopped`, the states a node never transitions out of.
|
|
350
508
|
*
|
|
351
509
|
* @remarks
|
|
352
|
-
* The
|
|
510
|
+
* The one terminal check across every tier (AGENTS.md § Design laws, "one concept, one term"):
|
|
353
511
|
* a task, a phase, and a workflow share the same {@link LifecycleStatus} vocabulary, so a
|
|
354
512
|
* single predicate covers them — {@link derivePhaseStatus} and {@link deriveWorkflowStatus}
|
|
355
|
-
* both consult it to tell a settled node from an in-flight one.
|
|
356
|
-
* `
|
|
357
|
-
* `
|
|
513
|
+
* both consult it to tell a settled node from an in-flight one. It reads the terminal set from
|
|
514
|
+
* {@link import('./constants.js').TERMINAL_STATUSES} (`completed` / `failed` / `skipped` /
|
|
515
|
+
* `stopped`), so that constant is the one definition; the only non-terminal states are `pending`
|
|
516
|
+
* and `running`.
|
|
358
517
|
*
|
|
359
518
|
* @param status - The lifecycle status to test (a task / phase / workflow status)
|
|
360
|
-
* @returns
|
|
519
|
+
* @returns True if the status is terminal; false otherwise
|
|
361
520
|
*/
|
|
362
521
|
function isTerminalStatus(status) {
|
|
363
|
-
return status
|
|
522
|
+
return TERMINAL_STATUSES.includes(status);
|
|
364
523
|
}
|
|
365
524
|
/**
|
|
366
|
-
*
|
|
367
|
-
*
|
|
525
|
+
* Tests whether a driving run must stop giving a workflow — or one forced phase of it —
|
|
526
|
+
* more work.
|
|
527
|
+
*
|
|
528
|
+
* @remarks
|
|
529
|
+
* The halt gate a {@link import('./WorkflowRunner.js').WorkflowRunner} consults before starting a
|
|
530
|
+
* phase, before dispatching a task, and after every cooperative gate. A workflow is halted after
|
|
531
|
+
* its derived status is terminal but not `completed` — a `bail: true` failure, a caller's own
|
|
532
|
+
* graceful `stop()`, or a forced `skip`. `completed` is excluded deliberately: a workflow that
|
|
533
|
+
* completed vacuously is settled, not halted, and the distinction is what keeps the run from
|
|
534
|
+
* sweeping a finished tree. When a `phase` is supplied, its own forced `skipped` / `stopped` halts
|
|
535
|
+
* that phase's work too; a `failed` phase does not, because the workflow's own `bail` policy
|
|
536
|
+
* decides whether a failed phase ends the run.
|
|
537
|
+
*
|
|
538
|
+
* @param workflow - The live workflow the run is driving
|
|
539
|
+
* @param phase - The phase whose own forced terminal status also halts its tasks
|
|
540
|
+
* @returns True if the run must stop giving this workflow (or phase) more work; false otherwise
|
|
541
|
+
*
|
|
542
|
+
* @example
|
|
543
|
+
* ```ts
|
|
544
|
+
* isHalted(workflow) // false while pending or running
|
|
545
|
+
* workflow.stop()
|
|
546
|
+
* isHalted(workflow) // true
|
|
547
|
+
* ```
|
|
548
|
+
*/
|
|
549
|
+
function isHalted(workflow, phase) {
|
|
550
|
+
const status = workflow.status;
|
|
551
|
+
return isTerminalStatus(status) && status !== "completed" || phase?.status === "skipped" || phase?.status === "stopped";
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Tests whether forcing a workflow `stopped` would still record the cancellation.
|
|
555
|
+
*
|
|
556
|
+
* @remarks
|
|
557
|
+
* `stop()` is a no-op after a workflow's status becomes terminal, so a run that must record a
|
|
558
|
+
* cancellation forces it only while this holds. It is not the negation of
|
|
559
|
+
* {@link isTerminalStatus}: `completed` and `skipped` both pass, because a run-level cancel that
|
|
560
|
+
* lands on a vacuously-completed or fully-skipped tree still records `stopped` as the outcome the
|
|
561
|
+
* caller asked for. Only an already-`failed` or already-`stopped` workflow has a terminal state
|
|
562
|
+
* worth keeping.
|
|
563
|
+
*
|
|
564
|
+
* @param workflow - The live workflow a run-level cancel would force
|
|
565
|
+
* @returns True if forcing `stopped` would change the recorded outcome; false otherwise
|
|
566
|
+
*
|
|
567
|
+
* @example
|
|
568
|
+
* ```ts
|
|
569
|
+
* isStoppable(workflow) // true while pending, running, completed, or skipped
|
|
570
|
+
* workflow.stop()
|
|
571
|
+
* isStoppable(workflow) // false
|
|
572
|
+
* ```
|
|
573
|
+
*/
|
|
574
|
+
function isStoppable(workflow) {
|
|
575
|
+
const status = workflow.status;
|
|
576
|
+
return status !== "failed" && status !== "stopped";
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Tests whether a naturally-finished run may force its workflow `completed`.
|
|
580
|
+
*
|
|
581
|
+
* @remarks
|
|
582
|
+
* A run that walked every phase and still derives `pending` executed nothing — zero phases, or
|
|
583
|
+
* every phase empty — so it is vacuously done and the run settles it `completed`. Gated on
|
|
584
|
+
* exactly `pending` so a real `completed`, a `bail: true` `failed`, a `stopped`, or a derived
|
|
585
|
+
* `skipped` is never overridden. The tree-is-empty half of the rule is
|
|
586
|
+
* {@link WorkflowInterface.complete}'s own guard, which refuses a pending tree that still holds
|
|
587
|
+
* tasks.
|
|
588
|
+
*
|
|
589
|
+
* @param workflow - The live workflow the run has finished walking
|
|
590
|
+
* @returns True if the run may force the vacuous completion; false otherwise
|
|
591
|
+
*
|
|
592
|
+
* @example
|
|
593
|
+
* ```ts
|
|
594
|
+
* isCompletable(createWorkflow({ id: 'w', name: 'W', phases: [] })) // true
|
|
595
|
+
* ```
|
|
596
|
+
*/
|
|
597
|
+
function isCompletable(workflow) {
|
|
598
|
+
return workflow.status === "pending";
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Tests whether a task attempt is being genuinely cancelled rather than merely timed out.
|
|
602
|
+
*
|
|
603
|
+
* @remarks
|
|
604
|
+
* The discriminator that keeps a per-attempt deadline off the skip path. Three causes fire a
|
|
605
|
+
* running task's folded signal, and only two of them mean "skip this task": the task's own
|
|
606
|
+
* `signal` (its `stop` / `skip`), and the unit or run signal (a sibling fail-fast under
|
|
607
|
+
* `bail: true`, or a run-level abort / timeout / budget / `destroy`). A bare per-attempt timeout
|
|
608
|
+
* fires neither — it aborts only the deadline portion of the attempt signal — so it stays a
|
|
609
|
+
* retryable failure of that attempt instead of skipping the leaf and losing the recorded fault.
|
|
610
|
+
* Read fresh at each call so a cancel that lands mid-dispatch is seen.
|
|
611
|
+
*
|
|
612
|
+
* @param task - The live task the attempt is driving
|
|
613
|
+
* @param controller - The substrate unit handle carrying the unit-level abort
|
|
614
|
+
* @param runSignal - The run's folded cancellation signal
|
|
615
|
+
* @returns True if the attempt is being genuinely cancelled; false otherwise
|
|
616
|
+
*
|
|
617
|
+
* @example
|
|
618
|
+
* ```ts
|
|
619
|
+
* isSkipping(task, controller, runSignal) // false until a cancel fires
|
|
620
|
+
* ```
|
|
621
|
+
*/
|
|
622
|
+
function isSkipping(task, controller, runSignal) {
|
|
623
|
+
return task.signal.aborted || controller.aborted || runSignal.aborted;
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Tests whether one attempt still owns the task it launched.
|
|
627
|
+
*
|
|
628
|
+
* @remarks
|
|
629
|
+
* A retried task is re-dispatched while an earlier attempt's handler may still be resolving, so
|
|
630
|
+
* every settlement path re-checks ownership before touching the leaf. Ownership needs both
|
|
631
|
+
* halves: the run-local `owners` ledger must still name this attempt, and the live task's own
|
|
632
|
+
* `attempts` tally must still match it. A superseded attempt reads `false` and returns without
|
|
633
|
+
* recording anything, so a late resolution can never overwrite the newer attempt's outcome.
|
|
634
|
+
*
|
|
635
|
+
* @param owners - The run-local ledger of the attempt owning each task id
|
|
636
|
+
* @param task - The live task the attempt launched
|
|
637
|
+
* @param attempt - The one-based attempt number to test
|
|
638
|
+
* @returns True if `attempt` still owns `task`; false otherwise
|
|
639
|
+
*
|
|
640
|
+
* @example
|
|
641
|
+
* ```ts
|
|
642
|
+
* const owners = new Map([[task.id, 1]])
|
|
643
|
+
* ownsAttempt(owners, task, 1) // true while the task's own `attempts` is 1
|
|
644
|
+
* ownsAttempt(owners, task, 2) // false
|
|
645
|
+
* ```
|
|
646
|
+
*/
|
|
647
|
+
function ownsAttempt(owners, task, attempt) {
|
|
648
|
+
return owners.get(task.id) === attempt && task.attempts === attempt;
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
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).
|
|
368
653
|
*
|
|
369
654
|
* @remarks
|
|
370
655
|
* The truth table (most-severe terminal wins; `bail`-agnostic — a phase surfaces a
|
|
371
656
|
* task failure as `failed` so the workflow's `bail` policy can decide):
|
|
372
657
|
* - no tasks ⇒ `pending`.
|
|
373
|
-
* - any task `running`,
|
|
658
|
+
* - any task `running`, or a mix of started-and-unsettled tasks (some non-`pending`
|
|
374
659
|
* but not all terminal) ⇒ `running`.
|
|
375
660
|
* - every task `pending` ⇒ `pending`.
|
|
376
661
|
* - all terminal: any `failed` ⇒ `failed`; else any `stopped` ⇒ `stopped`; else any
|
|
@@ -381,7 +666,7 @@ function isTerminalStatus(status) {
|
|
|
381
666
|
* task makes the phase `failed`.
|
|
382
667
|
*
|
|
383
668
|
* @param tasks - The phase's task statuses, in any order
|
|
384
|
-
* @returns The derived {@link
|
|
669
|
+
* @returns The derived phase {@link LifecycleStatus}
|
|
385
670
|
*/
|
|
386
671
|
function derivePhaseStatus(tasks) {
|
|
387
672
|
if (tasks.length === 0) return "pending";
|
|
@@ -393,24 +678,25 @@ function derivePhaseStatus(tasks) {
|
|
|
393
678
|
return "skipped";
|
|
394
679
|
}
|
|
395
680
|
/**
|
|
396
|
-
*
|
|
397
|
-
* paired with the
|
|
398
|
-
* failure outcome is
|
|
399
|
-
*
|
|
681
|
+
* Derives a workflow's status from its phases' {@link PhaseDerivation}s — each phase's status
|
|
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).
|
|
400
686
|
*
|
|
401
687
|
* @remarks
|
|
402
|
-
* `bail` is
|
|
403
|
-
* {@link PhaseDerivation} rather than passed as one scalar. It is the
|
|
688
|
+
* `bail` is a per-phase override, so it is carried on each
|
|
689
|
+
* {@link PhaseDerivation} rather than passed as one scalar. It is the only axis that changes
|
|
404
690
|
* the failure outcome, decided per phase:
|
|
405
691
|
* - **A `failed` phase whose effective `bail` is `true` (halt)** propagates ⇒ the workflow is
|
|
406
692
|
* `failed` (the database-transaction halt) — even when the workflow default is graceful.
|
|
407
|
-
* - **A `failed` phase whose effective `bail` is `false` (graceful)** is
|
|
408
|
-
* 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
|
|
409
695
|
* makes the workflow `failed` — even when the workflow default is strict.
|
|
410
696
|
*
|
|
411
697
|
* The rest of the table is shared:
|
|
412
698
|
* - no phases ⇒ `pending`.
|
|
413
|
-
* - any phase `running`,
|
|
699
|
+
* - any phase `running`, or a mix of started-and-unsettled phases (some non-`pending`
|
|
414
700
|
* but not all terminal) ⇒ `running`.
|
|
415
701
|
* - every phase `pending` ⇒ `pending`.
|
|
416
702
|
* - all terminal (a `failed` phase counts as terminal here): any `stopped` ⇒ `stopped`; else
|
|
@@ -418,7 +704,7 @@ function derivePhaseStatus(tasks) {
|
|
|
418
704
|
* else (all `skipped`) ⇒ `skipped`.
|
|
419
705
|
*
|
|
420
706
|
* @param phases - The workflow's per-phase {@link PhaseDerivation}s (status + effective bail), in any order
|
|
421
|
-
* @returns The derived {@link
|
|
707
|
+
* @returns The derived workflow {@link LifecycleStatus}
|
|
422
708
|
*/
|
|
423
709
|
function deriveWorkflowStatus(phases) {
|
|
424
710
|
if (phases.length === 0) return "pending";
|
|
@@ -430,18 +716,19 @@ function deriveWorkflowStatus(phases) {
|
|
|
430
716
|
return "skipped";
|
|
431
717
|
}
|
|
432
718
|
/**
|
|
433
|
-
*
|
|
434
|
-
* 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.
|
|
435
722
|
*
|
|
436
723
|
* @remarks
|
|
437
|
-
* The native, hook-free replacement for a runner-installed cursor
|
|
724
|
+
* The native, hook-free replacement for a runner-installed cursor: a
|
|
438
725
|
* {@link import('./types.js').WorkflowInterface}'s `add` / `remove` / `move` / `update`
|
|
439
726
|
* reads this over its live phases' statuses to decide which positions are safe to edit.
|
|
440
|
-
* Because entries run
|
|
441
|
-
* already-started entry forms a contiguous
|
|
442
|
-
* entry forms the trailing suffix — so the boundary is
|
|
727
|
+
* Because entries run sequentially (phases sequential, AGENTS determinism), every
|
|
728
|
+
* already-started entry forms a contiguous leading prefix and every still-`pending`
|
|
729
|
+
* entry forms the trailing suffix — so the boundary is the count of leading
|
|
443
730
|
* non-`pending` entries: the index of the first `pending` entry, or the full length when
|
|
444
|
-
* 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
|
|
445
732
|
* `pending`, so the boundary is `0` and every position is naturally accepted — callers
|
|
446
733
|
* need no special case for that.
|
|
447
734
|
*
|
|
@@ -460,8 +747,8 @@ function deriveBoundary(statuses) {
|
|
|
460
747
|
return index === -1 ? statuses.length : index;
|
|
461
748
|
}
|
|
462
749
|
/**
|
|
463
|
-
*
|
|
464
|
-
* {@link
|
|
750
|
+
* Tests whether the live W-b task state machine may move directly from one
|
|
751
|
+
* {@link LifecycleStatus} to another — the legal-transition guard.
|
|
465
752
|
*
|
|
466
753
|
* @remarks
|
|
467
754
|
* Reads the {@link import('./constants.js').TASK_TRANSITIONS} graph: `true` only when
|
|
@@ -471,13 +758,14 @@ function deriveBoundary(statuses) {
|
|
|
471
758
|
*
|
|
472
759
|
* @param from - The task's current status
|
|
473
760
|
* @param to - The status the transition would move it to
|
|
474
|
-
* @returns
|
|
761
|
+
* @returns True if the move is legal; false otherwise
|
|
475
762
|
*/
|
|
476
763
|
function canTransitionTask(from, to) {
|
|
477
764
|
return TASK_TRANSITIONS[from].includes(to);
|
|
478
765
|
}
|
|
479
766
|
/**
|
|
480
|
-
*
|
|
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.
|
|
481
769
|
*
|
|
482
770
|
* @param value - The task-level override; any present non-positive or non-finite value disables
|
|
483
771
|
* @param fallback - The workflow-level default
|
|
@@ -488,7 +776,7 @@ function resolveTaskSilence(value, fallback) {
|
|
|
488
776
|
return fallback !== void 0 && Number.isFinite(fallback) && fallback > 0 && fallback <= 2147483647 ? fallback : void 0;
|
|
489
777
|
}
|
|
490
778
|
/**
|
|
491
|
-
*
|
|
779
|
+
* Boxes a value as a {@link Success} — the graceful outcome half of a {@link Result}.
|
|
492
780
|
*
|
|
493
781
|
* @typeParam T - The boxed value's type
|
|
494
782
|
* @param value - The value to box
|
|
@@ -506,7 +794,7 @@ function success(value) {
|
|
|
506
794
|
};
|
|
507
795
|
}
|
|
508
796
|
/**
|
|
509
|
-
*
|
|
797
|
+
* Boxes an error as a {@link Failure} — the graceful outcome half of a {@link Result}.
|
|
510
798
|
*
|
|
511
799
|
* @typeParam E - The boxed error's type
|
|
512
800
|
* @param error - The error to box
|
|
@@ -524,7 +812,7 @@ function failure(error) {
|
|
|
524
812
|
};
|
|
525
813
|
}
|
|
526
814
|
/**
|
|
527
|
-
*
|
|
815
|
+
* Normalizes an unknown thrown value to a non-empty persistence-safe message.
|
|
528
816
|
*
|
|
529
817
|
* @param error - The caught value
|
|
530
818
|
* @returns A non-empty message without stack or cause data
|
|
@@ -538,16 +826,16 @@ function errorToMessage(error) {
|
|
|
538
826
|
}
|
|
539
827
|
}
|
|
540
828
|
/**
|
|
541
|
-
*
|
|
829
|
+
* Finds the first {@link TaskResult} in a positional list whose boxed outcome is a
|
|
542
830
|
* `Failure` — the pure scan shared by a phase's and a workflow's derived-`failed`
|
|
543
831
|
* `fail`-event lookup.
|
|
544
832
|
*
|
|
545
833
|
* @remarks
|
|
546
834
|
* The shared leaf behind {@link import('./phases/Phase.js').Phase} and
|
|
547
|
-
* {@link import('./Workflow.js').Workflow}'s own `#failure` — each gathers
|
|
835
|
+
* {@link import('./Workflow.js').Workflow}'s own `#failure` — each gathers its tier's
|
|
548
836
|
* results (a phase's own settled tasks, a workflow's flattened `results()`) and feeds
|
|
549
|
-
* them here; the tier-local method keeps the
|
|
550
|
-
* status
|
|
837
|
+
* them here; the tier-local method keeps the invariant throw (a derived `failed`
|
|
838
|
+
* status means a failing result exists) because throwing on `undefined` is
|
|
551
839
|
* orchestration, not a leaf concern.
|
|
552
840
|
*
|
|
553
841
|
* @param results - The results to scan, in any order
|
|
@@ -562,7 +850,7 @@ function findFailure(results) {
|
|
|
562
850
|
return results.find((result) => result.result?.success === false);
|
|
563
851
|
}
|
|
564
852
|
/**
|
|
565
|
-
*
|
|
853
|
+
* Builds a {@link WorkflowContext} — the identity every level inherits — from a node's
|
|
566
854
|
* `id` / `name` / optional `description`.
|
|
567
855
|
*
|
|
568
856
|
* @remarks
|
|
@@ -581,10 +869,10 @@ function buildWorkflowContext(node) {
|
|
|
581
869
|
});
|
|
582
870
|
}
|
|
583
871
|
/**
|
|
584
|
-
*
|
|
872
|
+
* Builds a {@link PhaseContext} — a phase's own identity plus a back-reference to its
|
|
585
873
|
* workflow — from the parent {@link WorkflowContext} and the phase node's identity.
|
|
586
874
|
*
|
|
587
|
-
* @param workflow - The parent workflow context (the lineage pointer
|
|
875
|
+
* @param workflow - The parent workflow context (the lineage pointer up the tree)
|
|
588
876
|
* @param node - The phase's identity (`id` / `name` / optional `description`)
|
|
589
877
|
* @returns The {@link PhaseContext}
|
|
590
878
|
*/
|
|
@@ -595,11 +883,11 @@ function buildPhaseContext(workflow, node) {
|
|
|
595
883
|
});
|
|
596
884
|
}
|
|
597
885
|
/**
|
|
598
|
-
*
|
|
886
|
+
* Builds a {@link TaskContext} — a task's own identity plus a back-reference to its phase
|
|
599
887
|
* (and, transitively, its workflow) — from the parent {@link PhaseContext} and the task
|
|
600
888
|
* node's identity.
|
|
601
889
|
*
|
|
602
|
-
* @param phase - The parent phase context (carrying the full lineage
|
|
890
|
+
* @param phase - The parent phase context (carrying the full lineage up the tree)
|
|
603
891
|
* @param node - The task's identity (`id` / `name` / optional `description`)
|
|
604
892
|
* @returns The {@link TaskContext}
|
|
605
893
|
*/
|
|
@@ -610,31 +898,31 @@ function buildTaskContext(phase, node) {
|
|
|
610
898
|
});
|
|
611
899
|
}
|
|
612
900
|
/**
|
|
613
|
-
*
|
|
614
|
-
* node `pending`, no results, empty metadata — so the live W-b tree has
|
|
615
|
-
* 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.
|
|
616
904
|
*
|
|
617
905
|
* @remarks
|
|
618
906
|
* The structural fields (`id` / `name` / `description` + the ordered phases / tasks)
|
|
619
907
|
* carry over verbatim, as does each phase's `concurrency` (persisted on the
|
|
620
|
-
* {@link PhaseSnapshot} so a restore reinstates the same throttle) and each task's `
|
|
908
|
+
* {@link PhaseSnapshot} so a restore reinstates the same throttle) and each task's `behavior` /
|
|
621
909
|
* `retries` / `timeout` (persisted on the {@link TaskSnapshot}, like `bail` / `concurrency`,
|
|
622
910
|
* so a restore + a {@link import('./types.js').WorkflowOptions.functions} registry resumes
|
|
623
911
|
* real work). The `bail` policy carries over — at the
|
|
624
|
-
* workflow tier
|
|
625
|
-
*
|
|
626
|
-
* snapshot is self-contained; a fresh seed has no `override`. `created` / `updated` are stamped
|
|
912
|
+
* workflow tier and, per phase, the
|
|
913
|
+
* effective policy (`phase.bail ?? workflowBail`) on each {@link PhaseSnapshot} — so the seeded
|
|
914
|
+
* snapshot is self-contained; a fresh seed has no `override`. `created` / `updated` are stamped at that point.
|
|
627
915
|
* {@link import('./factories.js').createWorkflow} builds from this.
|
|
628
916
|
*
|
|
629
|
-
* The optional `bail` override is the
|
|
917
|
+
* The optional `bail` override is the effective workflow policy the tree will run under
|
|
630
918
|
* (`createWorkflow` / the runner resolve `options.bail ?? definition.bail ?? DEFAULT_BAIL` and
|
|
631
|
-
* 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
|
|
632
920
|
* inheritance default of every phase that declares no `bail` of its own — otherwise the
|
|
633
921
|
* per-phase seeds would silently ignore the override. Omitted ⇒ the definition's own `bail`
|
|
634
922
|
* (defaulting to the graceful {@link import('./constants.js').DEFAULT_BAIL}).
|
|
635
923
|
*
|
|
636
924
|
* @param definition - The workflow definition to seed from
|
|
637
|
-
* @param bail - The
|
|
925
|
+
* @param bail - The effective workflow bail to seed both tiers with (defaults to the definition's)
|
|
638
926
|
* @returns An initial, all-`pending` {@link WorkflowSnapshot}
|
|
639
927
|
*/
|
|
640
928
|
function definitionToSnapshot(definition, bail) {
|
|
@@ -652,11 +940,11 @@ function definitionToSnapshot(definition, bail) {
|
|
|
652
940
|
};
|
|
653
941
|
}
|
|
654
942
|
/**
|
|
655
|
-
*
|
|
943
|
+
* Converts one {@link import('./types.js').PhaseDefinition} into an initial, all-`pending`
|
|
656
944
|
* {@link PhaseSnapshot} — the per-phase step of {@link definitionToSnapshot}.
|
|
657
945
|
*
|
|
658
946
|
* @remarks
|
|
659
|
-
* The snapshot persists the
|
|
947
|
+
* The snapshot persists the effective failure policy this phase runs under: the phase's own
|
|
660
948
|
* `bail` when it declares one, else the `workflowBail` it inherits — so a restore reinstates
|
|
661
949
|
* the same per-phase policy without a silent default (`effectiveBail = phase.bail ?? workflowBail`).
|
|
662
950
|
* `concurrency` (the resource throttle) carries over verbatim, omitted when undefined.
|
|
@@ -677,14 +965,14 @@ function phaseDefinitionToSnapshot(phase, workflowBail) {
|
|
|
677
965
|
};
|
|
678
966
|
}
|
|
679
967
|
/**
|
|
680
|
-
*
|
|
968
|
+
* Converts one {@link import('./types.js').TaskDefinition} into an initial, `pending`
|
|
681
969
|
* {@link TaskSnapshot} — the per-task leaf step of {@link definitionToSnapshot} (no
|
|
682
970
|
* result yet, empty metadata).
|
|
683
971
|
*
|
|
684
972
|
* @remarks
|
|
685
|
-
* `
|
|
973
|
+
* `behavior` / `retries` / `timeout` carry over verbatim (persisted declarative config, like a
|
|
686
974
|
* phase's `bail` / `concurrency`) — a restore reinstates the same behavior reference and
|
|
687
|
-
* reliability overrides
|
|
975
|
+
* reliability overrides after pairing with a {@link import('./types.js').WorkflowOptions.functions}
|
|
688
976
|
* registry.
|
|
689
977
|
*
|
|
690
978
|
* @param task - The task definition to seed from
|
|
@@ -698,13 +986,13 @@ function taskDefinitionToSnapshot(task) {
|
|
|
698
986
|
status: "pending",
|
|
699
987
|
metadata: {},
|
|
700
988
|
attempts: 0,
|
|
701
|
-
...task.
|
|
989
|
+
...task.behavior === void 0 ? {} : { behavior: task.behavior },
|
|
702
990
|
...task.retries === void 0 ? {} : { retries: task.retries },
|
|
703
991
|
...task.timeout === void 0 ? {} : { timeout: task.timeout }
|
|
704
992
|
};
|
|
705
993
|
}
|
|
706
994
|
/**
|
|
707
|
-
*
|
|
995
|
+
* Converts interrupted running work into a recoverable pending suffix or an
|
|
708
996
|
* exhausted recovery failure without replenishing attempts.
|
|
709
997
|
*
|
|
710
998
|
* @param snapshot - A fully validated owned snapshot with no terminal overrides
|
|
@@ -781,11 +1069,58 @@ function recoverWorkflowSnapshot(snapshot) {
|
|
|
781
1069
|
updated: now
|
|
782
1070
|
};
|
|
783
1071
|
}
|
|
784
|
-
/**
|
|
1072
|
+
/**
|
|
1073
|
+
* Compares two optional description values.
|
|
1074
|
+
*
|
|
1075
|
+
* @remarks
|
|
1076
|
+
* The equality rule a lineage check needs: two descriptions match when they are the same value
|
|
1077
|
+
* and that value is either a string or genuine absence. Anything else — a number, an object, a
|
|
1078
|
+
* `null` — never matches, even against itself, so a lineage stamped with a non-string description
|
|
1079
|
+
* is rejected rather than silently accepted.
|
|
1080
|
+
*
|
|
1081
|
+
* @param left - The first description value
|
|
1082
|
+
* @param right - The second description value
|
|
1083
|
+
* @returns True if both are the same string or both absent; false otherwise
|
|
1084
|
+
*
|
|
1085
|
+
* @example
|
|
1086
|
+
* ```ts
|
|
1087
|
+
* matchesDescription('build', 'build') // true
|
|
1088
|
+
* matchesDescription(undefined, undefined) // true
|
|
1089
|
+
* matchesDescription('build', undefined) // false
|
|
1090
|
+
* ```
|
|
1091
|
+
*/
|
|
785
1092
|
function matchesDescription(left, right) {
|
|
786
1093
|
return left === right && (left === void 0 || typeof left === "string");
|
|
787
1094
|
}
|
|
788
|
-
/**
|
|
1095
|
+
/**
|
|
1096
|
+
* Tests a result's lineage against its containing snapshot nodes.
|
|
1097
|
+
*
|
|
1098
|
+
* @remarks
|
|
1099
|
+
* The four arguments are the result and the three snapshot nodes it claims to belong to, read
|
|
1100
|
+
* from the outside in: a {@link TaskResult} is self-describing, so restoring one is only safe
|
|
1101
|
+
* when every identity it carries agrees with the tree it was found in. It checks the exact key
|
|
1102
|
+
* set at each level, that `status` equals the owning task's, and that the `task` / `phase` /
|
|
1103
|
+
* `workflow` contexts — including the nested `task.phase.workflow` lineage — carry the same `id`,
|
|
1104
|
+
* `name`, and `description` as the nodes containing them. It then requires the boxed outcome to
|
|
1105
|
+
* match the status: a `Success` holding JSON for `completed`, a `Failure` holding a
|
|
1106
|
+
* {@link TaskFailure} for `failed`, and nothing for any other status. Total — a hostile
|
|
1107
|
+
* prototype, accessor, or cycle answers `false` rather than throwing.
|
|
1108
|
+
*
|
|
1109
|
+
* @param value - The candidate {@link TaskResult}
|
|
1110
|
+
* @param workflow - The workflow snapshot node containing it
|
|
1111
|
+
* @param phase - The phase snapshot node containing it
|
|
1112
|
+
* @param task - The task snapshot node the result belongs to
|
|
1113
|
+
* @returns True if `value` is a {@link TaskResult} whose lineage and outcome match
|
|
1114
|
+
* those nodes; false otherwise
|
|
1115
|
+
*
|
|
1116
|
+
* @example
|
|
1117
|
+
* ```ts
|
|
1118
|
+
* const snapshot = workflow.snapshot()
|
|
1119
|
+
* const phase = snapshot.phases[0]
|
|
1120
|
+
* const task = phase?.tasks[0]
|
|
1121
|
+
* isTaskResult(task?.result, snapshot, phase, task) // true for a settled task
|
|
1122
|
+
* ```
|
|
1123
|
+
*/
|
|
789
1124
|
function isTaskResult(value, workflow, phase, task) {
|
|
790
1125
|
try {
|
|
791
1126
|
if (!(0, _orkestrel_contract.isRecord)(value) || !(0, _orkestrel_contract.isRecord)(workflow) || !(0, _orkestrel_contract.isRecord)(phase) || !(0, _orkestrel_contract.isRecord)(task) || !Object.keys(value).every((key) => key === "task" || key === "phase" || key === "workflow" || key === "status" || key === "result" || key === "timestamp") || !isLifecycleStatus(value.status) || value.status !== task.status || !(0, _orkestrel_contract.isFiniteNumber)(value.timestamp) || value.timestamp < 0 || !(0, _orkestrel_contract.isRecord)(value.task) || !(0, _orkestrel_contract.isRecord)(value.phase) || !(0, _orkestrel_contract.isRecord)(value.workflow) || !Object.keys(value.workflow).every((key) => key === "id" || key === "name" || key === "description") || !Object.keys(value.phase).every((key) => key === "id" || key === "name" || key === "description" || key === "workflow") || !Object.keys(value.task).every((key) => key === "id" || key === "name" || key === "description" || key === "phase")) return false;
|
|
@@ -801,19 +1136,30 @@ function isTaskResult(value, workflow, phase, task) {
|
|
|
801
1136
|
}
|
|
802
1137
|
function hasWorkflowHandlers(workflow, functions) {
|
|
803
1138
|
if ("destroyed" in workflow) {
|
|
804
|
-
for (const phase of workflow.phases.phases()) for (const task of phase.tasks.tasks()) if (task.
|
|
1139
|
+
for (const phase of workflow.phases.phases()) for (const task of phase.tasks.tasks()) if (task.behavior !== void 0 && !(0, _orkestrel_contract.isFunction)(task.handler)) return false;
|
|
805
1140
|
return true;
|
|
806
1141
|
}
|
|
807
|
-
const
|
|
1142
|
+
const behaviors = /* @__PURE__ */ new Set();
|
|
808
1143
|
for (const phase of workflow.phases) for (const task of phase.tasks) {
|
|
809
|
-
if (task.
|
|
810
|
-
|
|
811
|
-
if (!(0, _orkestrel_contract.isFunction)(functions?.[task.
|
|
1144
|
+
if (task.behavior === void 0 || behaviors.has(task.behavior)) continue;
|
|
1145
|
+
behaviors.add(task.behavior);
|
|
1146
|
+
if (!(0, _orkestrel_contract.isFunction)(functions?.[task.behavior])) return false;
|
|
812
1147
|
}
|
|
813
1148
|
return true;
|
|
814
1149
|
}
|
|
815
|
-
/**
|
|
816
|
-
|
|
1150
|
+
/**
|
|
1151
|
+
* Locates the nearest identifiable node for an inconsistent owned snapshot.
|
|
1152
|
+
*
|
|
1153
|
+
* @remarks
|
|
1154
|
+
* The walk stops at the first phase or task whose persisted fields are inconsistent and returns
|
|
1155
|
+
* the identifiers it could read there, so a diagnostic can name the offending node even when part
|
|
1156
|
+
* of its identity is unreadable.
|
|
1157
|
+
*
|
|
1158
|
+
* @param value - The candidate snapshot, which may be any unknown value
|
|
1159
|
+
* @returns The nearest identifying record naming the offending `phase` and `task`, or `undefined`
|
|
1160
|
+
* when no inconsistent node is identifiable
|
|
1161
|
+
*/
|
|
1162
|
+
function scanSnapshotContext(value) {
|
|
817
1163
|
if (!(0, _orkestrel_contract.isRecord)(value) || !(0, _orkestrel_contract.isArray)(value.phases)) return void 0;
|
|
818
1164
|
for (const phase of value.phases) {
|
|
819
1165
|
if (!(0, _orkestrel_contract.isRecord)(phase)) continue;
|
|
@@ -821,7 +1167,7 @@ function workflowSnapshotContext(value) {
|
|
|
821
1167
|
if (!(0, _orkestrel_contract.isBoolean)(phase.bail) || phase.concurrency !== void 0 && (!(0, _orkestrel_contract.isInteger)(phase.concurrency) || phase.concurrency < 1) || !(0, _orkestrel_contract.isArray)(phase.tasks)) return phaseContext;
|
|
822
1168
|
for (const task of phase.tasks) {
|
|
823
1169
|
if (!(0, _orkestrel_contract.isRecord)(task)) continue;
|
|
824
|
-
if (task.
|
|
1170
|
+
if (task.behavior !== void 0 && !(0, _orkestrel_contract.isNonEmptyString)(task.behavior) || task.retries !== void 0 && (!(0, _orkestrel_contract.isInteger)(task.retries) || task.retries < 0) || task.timeout !== void 0 && (!(0, _orkestrel_contract.isInteger)(task.timeout) || task.timeout < 0 || task.timeout > 2147483647) || !(0, _orkestrel_contract.isInteger)(task.attempts) || task.attempts < 0) return {
|
|
825
1171
|
...phaseContext ?? {},
|
|
826
1172
|
...(0, _orkestrel_contract.isNonEmptyString)(task.id) ? { task: task.id } : {}
|
|
827
1173
|
};
|
|
@@ -829,7 +1175,7 @@ function workflowSnapshotContext(value) {
|
|
|
829
1175
|
}
|
|
830
1176
|
}
|
|
831
1177
|
/**
|
|
832
|
-
*
|
|
1178
|
+
* Flattens a nested list of per-phase {@link TaskResult} lists into one positional list
|
|
833
1179
|
* — the workflow tier of the result tree, built from each phase's `results()`.
|
|
834
1180
|
*
|
|
835
1181
|
* @remarks
|
|
@@ -844,16 +1190,15 @@ function collectResults(phases) {
|
|
|
844
1190
|
return phases.flat();
|
|
845
1191
|
}
|
|
846
1192
|
/**
|
|
847
|
-
*
|
|
1193
|
+
* Inserts one `[key, value]` entry at a positional index into a readonly entries array —
|
|
848
1194
|
* the pure splice-in step behind an insertion-ordered registry's `add`.
|
|
849
1195
|
*
|
|
850
1196
|
* @remarks
|
|
851
|
-
*
|
|
852
|
-
*
|
|
853
|
-
*
|
|
854
|
-
*
|
|
855
|
-
*
|
|
856
|
-
* mutate `entries`; returns a new array.
|
|
1197
|
+
* Used by the shared {@link import('./Collection.js').Collection} store both managers hold: it
|
|
1198
|
+
* converts its insertion-ordered `Map` to `[...map.entries()]`, calls this to splice the new entry
|
|
1199
|
+
* in at the target index, then rebuilds the `Map` from the result (a stateful step that stays a
|
|
1200
|
+
* `#` private method — this helper does no `Map` construction). Does not mutate `entries`;
|
|
1201
|
+
* returns a new array.
|
|
857
1202
|
*
|
|
858
1203
|
* @typeParam T - The entry's value type
|
|
859
1204
|
* @param entries - The current positional entries, in order
|
|
@@ -873,16 +1218,17 @@ function insertEntry(entries, index, key, value) {
|
|
|
873
1218
|
return next;
|
|
874
1219
|
}
|
|
875
1220
|
/**
|
|
876
|
-
*
|
|
1221
|
+
* Repositions the entry keyed `key` to a new positional index in a readonly entries
|
|
877
1222
|
* array — the pure remove-then-reinsert step behind an insertion-ordered registry's
|
|
878
1223
|
* `move`.
|
|
879
1224
|
*
|
|
880
1225
|
* @remarks
|
|
881
1226
|
* The move counterpart of {@link insertEntry}: finds the entry by `key`, splices it
|
|
882
1227
|
* out, then splices it back in at `index`. An absent `key` is a no-op (returns a copy
|
|
883
|
-
* of `entries` unchanged) — the caller
|
|
884
|
-
* already gates on the target's
|
|
885
|
-
*
|
|
1228
|
+
* of `entries` unchanged) — the caller, the shared
|
|
1229
|
+
* {@link import('./Collection.js').Collection} store's `move`, already gates on the target's
|
|
1230
|
+
* existence before calling this, so the no-op branch is defensive, never reached in practice.
|
|
1231
|
+
* Does not mutate `entries`; returns a new array.
|
|
886
1232
|
*
|
|
887
1233
|
* @typeParam T - The entry's value type
|
|
888
1234
|
* @param entries - The current positional entries, in order
|
|
@@ -904,17 +1250,7 @@ function moveEntry(entries, key, index) {
|
|
|
904
1250
|
return next;
|
|
905
1251
|
}
|
|
906
1252
|
/**
|
|
907
|
-
*
|
|
908
|
-
* externally, so a caller can resolve/reject it from outside the executor.
|
|
909
|
-
*
|
|
910
|
-
* @typeParam T - The value the deferred promise resolves
|
|
911
|
-
* @returns A deferred `promise` plus its `resolve` / `reject`
|
|
912
|
-
*/
|
|
913
|
-
function createDeferred() {
|
|
914
|
-
return Promise.withResolvers();
|
|
915
|
-
}
|
|
916
|
-
/**
|
|
917
|
-
* Schedule one cancellable host operation behind an owned settlement signal.
|
|
1253
|
+
* Schedules one cancellable host operation behind an owned settlement signal.
|
|
918
1254
|
*
|
|
919
1255
|
* @remarks
|
|
920
1256
|
* A defined `signal` that is not a native `AbortSignal` is refused before anything is armed, as a
|
|
@@ -981,17 +1317,46 @@ function scheduleHost(start, signal) {
|
|
|
981
1317
|
});
|
|
982
1318
|
}
|
|
983
1319
|
/**
|
|
984
|
-
*
|
|
985
|
-
* busy-loop, that NEVER rejects.
|
|
1320
|
+
* Schedules the shared host timer boundary every scheduler backend resumes from.
|
|
986
1321
|
*
|
|
987
1322
|
* @remarks
|
|
988
|
-
*
|
|
1323
|
+
* The one `setTimeout` / `clearTimeout` boundary in the package: the cross-environment
|
|
1324
|
+
* {@link import('./Scheduler.js').Scheduler}, both Node primitives, and every browser backend's
|
|
1325
|
+
* `delay` and macrotask fallback route here, so the timer is armed and cleared in one place. It
|
|
1326
|
+
* composes {@link scheduleHost}, which owns listener safety, the cancellation race, the exact
|
|
1327
|
+
* caller reason, and once-only settlement. It does not validate `ms`: the value passes straight to
|
|
1328
|
+
* the host `setTimeout`, which clamps a negative value or `NaN` to about zero, so an
|
|
1329
|
+
* out-of-domain `ms` resumes on the next host turn rather than throwing. Pass a non-negative
|
|
1330
|
+
* finite `ms`.
|
|
1331
|
+
*
|
|
1332
|
+
* @param ms - The milliseconds to wait before resuming
|
|
1333
|
+
* @param signal - Optional caller cancellation signal
|
|
1334
|
+
* @returns A promise that resolves after `ms`, or rejects with the caller's exact abort reason
|
|
1335
|
+
*
|
|
1336
|
+
* @example
|
|
1337
|
+
* ```ts
|
|
1338
|
+
* const controller = new AbortController()
|
|
1339
|
+
* await delayHost(0, controller.signal) // a real macrotask host turn
|
|
1340
|
+
* ```
|
|
1341
|
+
*/
|
|
1342
|
+
function delayHost(ms, signal) {
|
|
1343
|
+
return scheduleHost((complete) => {
|
|
1344
|
+
const handle = setTimeout(complete, ms);
|
|
1345
|
+
return () => clearTimeout(handle);
|
|
1346
|
+
}, signal);
|
|
1347
|
+
}
|
|
1348
|
+
/**
|
|
1349
|
+
* Parks until `signal` aborts — a promise-parked wait, never a timer or
|
|
1350
|
+
* busy-loop, that resolves on the abort event and never rejects.
|
|
1351
|
+
*
|
|
1352
|
+
* @remarks
|
|
1353
|
+
* Resolves immediately when `signal` is already aborted; otherwise attaches a one-shot
|
|
989
1354
|
* `abort` listener and resolves when it fires, removing the listener either way. The
|
|
990
1355
|
* shared leaf behind the duplicate abort-wiring an execution engine otherwise hand-rolls
|
|
991
1356
|
* at every fold point.
|
|
992
1357
|
*
|
|
993
1358
|
* @param signal - The signal to park on
|
|
994
|
-
* @returns A promise that resolves
|
|
1359
|
+
* @returns A promise that resolves after `signal` has aborted
|
|
995
1360
|
*
|
|
996
1361
|
* @example
|
|
997
1362
|
* ```ts
|
|
@@ -1008,25 +1373,125 @@ function parkSignal(signal) {
|
|
|
1008
1373
|
});
|
|
1009
1374
|
}
|
|
1010
1375
|
//#endregion
|
|
1376
|
+
//#region src/core/Collection.ts
|
|
1377
|
+
/**
|
|
1378
|
+
* Implements the insertion-ordered gated store both lean managers hold — entities keyed by `id`,
|
|
1379
|
+
* positional order preserved across an interior `skip` or `remove`.
|
|
1380
|
+
*
|
|
1381
|
+
* @remarks
|
|
1382
|
+
* - **One engine, two managers.** {@link import('./tasks/TaskManager.js').TaskManager} and
|
|
1383
|
+
* {@link import('./phases/PhaseManager.js').PhaseManager} differ only in the entity noun and the
|
|
1384
|
+
* patch shape they validate, so both hold one of these and add only their domain accessors
|
|
1385
|
+
* (`task` / `tasks`, `phase` / `phases`). The `Map`'s insertion order is the single source of
|
|
1386
|
+
* positional truth; `add` and `move` rebuild it through the pure
|
|
1387
|
+
* {@link import('./helpers.js').insertEntry} / {@link import('./helpers.js').moveEntry} leaves.
|
|
1388
|
+
* - **Gated mutation API.** `append` is the build-time wiring path and throws on a
|
|
1389
|
+
* duplicate id; `add` / `remove` / `move` / `update` return a graceful `MUTATION`
|
|
1390
|
+
* {@link WorkflowError} failure instead. Gating reads only the target's own existence, `pending`
|
|
1391
|
+
* status, id, and bounds — a container's own status is the owning entity's gate, applied before
|
|
1392
|
+
* it delegates here.
|
|
1393
|
+
* - **Event-free.** A purely structural container; the entity that owns it emits on success.
|
|
1394
|
+
*
|
|
1395
|
+
* @typeParam TEntry - The stored entity
|
|
1396
|
+
* @typeParam TPatch - The declarative partial update `update` validates and applies
|
|
1397
|
+
*
|
|
1398
|
+
* @example
|
|
1399
|
+
* ```ts
|
|
1400
|
+
* import { compileGuard } from '@orkestrel/contract'
|
|
1401
|
+
* import { Collection, taskUpdateShape } from '@orkestrel/workflow'
|
|
1402
|
+
* import type { TaskInterface, TaskUpdate } from '@orkestrel/workflow'
|
|
1403
|
+
*
|
|
1404
|
+
* const tasks = new Collection<TaskInterface, TaskUpdate>('task', compileGuard(taskUpdateShape))
|
|
1405
|
+
* tasks.append(task) // a live Task
|
|
1406
|
+
* tasks.entry(task.id) // the same task
|
|
1407
|
+
* tasks.entries() // [task]
|
|
1408
|
+
* tasks.count // 1
|
|
1409
|
+
* tasks.add(other, 0) // Result — inserted first
|
|
1410
|
+
* tasks.move(other.id, 1) // Result — repositioned
|
|
1411
|
+
* tasks.update(task.id, { name: 'Renamed task' }) // Result — patched
|
|
1412
|
+
* tasks.remove(other.id) // Result — dropped
|
|
1413
|
+
* ```
|
|
1414
|
+
*/
|
|
1415
|
+
var Collection = class {
|
|
1416
|
+
#entries = /* @__PURE__ */ new Map();
|
|
1417
|
+
#noun;
|
|
1418
|
+
#isPatch;
|
|
1419
|
+
constructor(noun, patch) {
|
|
1420
|
+
this.#noun = noun;
|
|
1421
|
+
this.#isPatch = patch;
|
|
1422
|
+
}
|
|
1423
|
+
get count() {
|
|
1424
|
+
return this.#entries.size;
|
|
1425
|
+
}
|
|
1426
|
+
append(entry) {
|
|
1427
|
+
if (this.#entries.has(entry.id)) throw new WorkflowError("MUTATION", `duplicate ${this.#noun} id '${entry.id}'`, { id: entry.id });
|
|
1428
|
+
this.#entries.set(entry.id, entry);
|
|
1429
|
+
}
|
|
1430
|
+
add(entry, index) {
|
|
1431
|
+
if (this.#entries.has(entry.id)) return failure(new WorkflowError("MUTATION", `duplicate ${this.#noun} id '${entry.id}'`, { id: entry.id }));
|
|
1432
|
+
const at = index ?? this.#entries.size;
|
|
1433
|
+
if (at < 0 || at > this.#entries.size) return failure(new WorkflowError("MUTATION", `index '${at}' out of bounds`, { index: at }));
|
|
1434
|
+
this.#reorder(insertEntry([...this.#entries.entries()], at, entry.id, entry));
|
|
1435
|
+
return success(entry);
|
|
1436
|
+
}
|
|
1437
|
+
remove(id) {
|
|
1438
|
+
const target = this.#pending(id);
|
|
1439
|
+
if (target === void 0) return this.#refuse(id);
|
|
1440
|
+
this.#entries.delete(id);
|
|
1441
|
+
return success(target);
|
|
1442
|
+
}
|
|
1443
|
+
move(id, index) {
|
|
1444
|
+
const target = this.#pending(id);
|
|
1445
|
+
if (target === void 0) return this.#refuse(id);
|
|
1446
|
+
if (index < 0 || index >= this.#entries.size) return failure(new WorkflowError("MUTATION", `index '${index}' out of bounds`, { index }));
|
|
1447
|
+
this.#reorder(moveEntry([...this.#entries.entries()], id, index));
|
|
1448
|
+
return success(target);
|
|
1449
|
+
}
|
|
1450
|
+
update(id, patch) {
|
|
1451
|
+
const target = this.#pending(id);
|
|
1452
|
+
if (target === void 0) return this.#refuse(id);
|
|
1453
|
+
if (!this.#isPatch(patch)) return failure(new WorkflowError("MUTATION", `invalid patch for ${this.#noun} '${id}'`, { id }));
|
|
1454
|
+
target.patch(patch);
|
|
1455
|
+
return success(target);
|
|
1456
|
+
}
|
|
1457
|
+
entry(id) {
|
|
1458
|
+
return this.#entries.get(id);
|
|
1459
|
+
}
|
|
1460
|
+
entries() {
|
|
1461
|
+
return [...this.#entries.values()];
|
|
1462
|
+
}
|
|
1463
|
+
#pending(id) {
|
|
1464
|
+
const target = this.#entries.get(id);
|
|
1465
|
+
return target === void 0 || target.status !== "pending" ? void 0 : target;
|
|
1466
|
+
}
|
|
1467
|
+
#refuse(id) {
|
|
1468
|
+
return failure(new WorkflowError("MUTATION", `${this.#noun} '${id}' is not a pending ${this.#noun}`, { id }));
|
|
1469
|
+
}
|
|
1470
|
+
#reorder(entries) {
|
|
1471
|
+
this.#entries.clear();
|
|
1472
|
+
for (const [key, value] of entries) this.#entries.set(key, value);
|
|
1473
|
+
}
|
|
1474
|
+
};
|
|
1475
|
+
//#endregion
|
|
1011
1476
|
//#region src/core/Scheduler.ts
|
|
1012
1477
|
/**
|
|
1013
|
-
*
|
|
1478
|
+
* Implements the safe cross-environment cooperative-yield default — a {@link SchedulerInterface}
|
|
1014
1479
|
* built on `setTimeout` / `clearTimeout` alone, so it runs unchanged in both the
|
|
1015
1480
|
* browser and Node.
|
|
1016
1481
|
*
|
|
1017
1482
|
* @remarks
|
|
1018
|
-
* - **Cross-environment.** Uses
|
|
1483
|
+
* - **Cross-environment.** Uses only `setTimeout` / `clearTimeout` — universally
|
|
1019
1484
|
* available. It deliberately avoids env-specific fast paths (`setImmediate`,
|
|
1020
1485
|
* `scheduler.yield`, `requestAnimationFrame`, `node:timers/promises`,
|
|
1021
1486
|
* `MessageChannel`); those belong to the environment backends, built with the
|
|
1022
1487
|
* agent loop that consumes them.
|
|
1023
1488
|
* - **`yield` is a macrotask host-turn, not a microtask.** `yield()` waits on a
|
|
1024
|
-
* `setTimeout(0)`,
|
|
1489
|
+
* `setTimeout(0)`, not `queueMicrotask`. A microtask drains before the host
|
|
1025
1490
|
* regains control, so it would not actually let pending I/O, timers, or
|
|
1026
1491
|
* rendering run — it only defers within the current task. A zero-delay timer is
|
|
1027
1492
|
* the correct cross-environment "give the host a turn".
|
|
1028
1493
|
* - **Abort-aware.** A pending `yield` / `delay` rejects with `signal.reason` exactly.
|
|
1029
|
-
* {@link
|
|
1494
|
+
* {@link delayHost} links an owned settlement composite to the caller before arming
|
|
1030
1495
|
* the timer, so pre-abort schedules nothing, caller signal method mutation is harmless,
|
|
1031
1496
|
* cancellation clears the handle, and native first-settlement wins exactly once.
|
|
1032
1497
|
* - **Priority is accepted but uniform.** `options.priority` is part of the
|
|
@@ -1045,36 +1510,30 @@ function parkSignal(signal) {
|
|
|
1045
1510
|
*/
|
|
1046
1511
|
var Scheduler = class {
|
|
1047
1512
|
/**
|
|
1048
|
-
*
|
|
1049
|
-
* run, then
|
|
1513
|
+
* Yields control back to the host so other tasks (I/O, timers, rendering) can
|
|
1514
|
+
* run, then resumes — a macrotask turn through `setTimeout(0)` (not a microtask,
|
|
1050
1515
|
* which would resume before the host regains control).
|
|
1051
1516
|
*/
|
|
1052
1517
|
yield(options) {
|
|
1053
|
-
return
|
|
1518
|
+
return delayHost(0, options?.signal);
|
|
1054
1519
|
}
|
|
1055
1520
|
/**
|
|
1056
|
-
*
|
|
1521
|
+
* Resumes after at least `ms` milliseconds; abort rejects with `signal.reason`.
|
|
1057
1522
|
*
|
|
1058
1523
|
* @remarks
|
|
1059
|
-
*
|
|
1060
|
-
*
|
|
1061
|
-
*
|
|
1062
|
-
*
|
|
1524
|
+
* Pass a non-negative finite `ms`. The primitive stays minimal and does no
|
|
1525
|
+
* validation: it passes `ms` straight to the host `setTimeout`, which clamps a
|
|
1526
|
+
* negative value or `NaN` to ~0 — so an out-of-domain `ms` resolves on the next
|
|
1527
|
+
* host turn rather than throwing.
|
|
1063
1528
|
*/
|
|
1064
1529
|
delay(ms, options) {
|
|
1065
|
-
return
|
|
1066
|
-
}
|
|
1067
|
-
#sleep(ms, signal) {
|
|
1068
|
-
return scheduleHost((complete) => {
|
|
1069
|
-
const handle = setTimeout(complete, ms);
|
|
1070
|
-
return () => clearTimeout(handle);
|
|
1071
|
-
}, signal);
|
|
1530
|
+
return delayHost(ms, options?.signal);
|
|
1072
1531
|
}
|
|
1073
1532
|
};
|
|
1074
1533
|
//#endregion
|
|
1075
1534
|
//#region src/core/cloners.ts
|
|
1076
1535
|
/**
|
|
1077
|
-
*
|
|
1536
|
+
* Validates and owns a workflow snapshot before live construction.
|
|
1078
1537
|
*
|
|
1079
1538
|
* @param input - The hostile snapshot boundary
|
|
1080
1539
|
* @param id - The optional storage key the owned snapshot must match
|
|
@@ -1090,7 +1549,7 @@ function cloneWorkflowSnapshot(input, id) {
|
|
|
1090
1549
|
if ((0, _orkestrel_contract.isContractError)(error)) throw new WorkflowError("RESTORE", `workflow snapshot could not be read safely: ${error.message}`);
|
|
1091
1550
|
throw new WorkflowError("RESTORE", "workflow snapshot could not be read safely");
|
|
1092
1551
|
}
|
|
1093
|
-
if (!isOwnedWorkflowSnapshot(cloned)) throw new WorkflowError("RESTORE", "workflow snapshot is inconsistent",
|
|
1552
|
+
if (!isOwnedWorkflowSnapshot(cloned)) throw new WorkflowError("RESTORE", "workflow snapshot is inconsistent", scanSnapshotContext(cloned));
|
|
1094
1553
|
if (id !== void 0 && cloned.id !== id) throw new WorkflowError("RESTORE", `workflow snapshot '${cloned.id}' does not match storage key '${id}'`, {
|
|
1095
1554
|
requested: id,
|
|
1096
1555
|
payload: cloned.id
|
|
@@ -1098,7 +1557,49 @@ function cloneWorkflowSnapshot(input, id) {
|
|
|
1098
1557
|
return cloned;
|
|
1099
1558
|
}
|
|
1100
1559
|
/**
|
|
1101
|
-
*
|
|
1560
|
+
* Validates and owns one list of task activity claims.
|
|
1561
|
+
*
|
|
1562
|
+
* @remarks
|
|
1563
|
+
* The one cloner behind both claim lists of a task activity frame — its `operations` and its
|
|
1564
|
+
* `constraints` — because {@link import('./types.js').TaskOperation} and
|
|
1565
|
+
* {@link import('./types.js').TaskConstraint} are the same {@link import('./types.js').TaskClaim}
|
|
1566
|
+
* shape. An omitted
|
|
1567
|
+
* list is an empty one. Each member is read exactly once inside the caller's protected boundary
|
|
1568
|
+
* and returned frozen; the semantic pass over the copied values is
|
|
1569
|
+
* {@link import('./validators.js').isTaskClaimList}, so this cloner refuses only what it cannot
|
|
1570
|
+
* read: a non-array list, a non-record member, a hostile prototype, or an unexpected key.
|
|
1571
|
+
*
|
|
1572
|
+
* @param input - The untrusted claim list
|
|
1573
|
+
* @param noun - The singular claim noun the refusal message names, pluralized by adding `s`
|
|
1574
|
+
* @returns The owned frozen claims, in input order
|
|
1575
|
+
* @throws {WorkflowError} With `MUTATION` when the list or one of its members cannot be read
|
|
1576
|
+
*
|
|
1577
|
+
* @example
|
|
1578
|
+
* ```ts
|
|
1579
|
+
* cloneTaskClaims([{ id: 'fetch', name: 'Fetch', started: 1 }], 'operation')
|
|
1580
|
+
* ```
|
|
1581
|
+
*/
|
|
1582
|
+
function cloneTaskClaims(input, noun) {
|
|
1583
|
+
const inputs = input === void 0 ? [] : (0, _orkestrel_contract.isArray)(input) ? [...input] : void 0;
|
|
1584
|
+
if (inputs === void 0) throw new WorkflowError("MUTATION", `task activity ${noun}s must be an array`);
|
|
1585
|
+
const claims = [];
|
|
1586
|
+
for (const claim of inputs) {
|
|
1587
|
+
if (!(0, _orkestrel_contract.isRecord)(claim)) throw new WorkflowError("MUTATION", `task activity contains an invalid ${noun}`);
|
|
1588
|
+
const prototype = Object.getPrototypeOf(claim);
|
|
1589
|
+
if (prototype !== Object.prototype && prototype !== null || !Object.keys(claim).every((key) => key === "id" || key === "name" || key === "started")) throw new WorkflowError("MUTATION", `task activity contains an invalid ${noun}`);
|
|
1590
|
+
const id = claim.id;
|
|
1591
|
+
const name = claim.name;
|
|
1592
|
+
const started = claim.started;
|
|
1593
|
+
claims.push(Object.freeze({
|
|
1594
|
+
id,
|
|
1595
|
+
name,
|
|
1596
|
+
started
|
|
1597
|
+
}));
|
|
1598
|
+
}
|
|
1599
|
+
return claims;
|
|
1600
|
+
}
|
|
1601
|
+
/**
|
|
1602
|
+
* Validates and clones one complete task activity frame.
|
|
1102
1603
|
*
|
|
1103
1604
|
* @remarks
|
|
1104
1605
|
* This is the hostile boundary behind task reports and snapshot hydration. Supplying
|
|
@@ -1122,22 +1623,7 @@ function cloneTaskActivity(input, updated) {
|
|
|
1122
1623
|
const operationsInput = input.operations;
|
|
1123
1624
|
const constraintsInput = input.constraints;
|
|
1124
1625
|
const accepted = updated === void 0 ? input.updated : updated;
|
|
1125
|
-
const
|
|
1126
|
-
if (operationInputs === void 0) throw new WorkflowError("MUTATION", "task activity operations must be an array");
|
|
1127
|
-
const operations = [];
|
|
1128
|
-
for (const operation of operationInputs) {
|
|
1129
|
-
if (!(0, _orkestrel_contract.isRecord)(operation)) throw new WorkflowError("MUTATION", "task activity contains an invalid operation");
|
|
1130
|
-
const operationPrototype = Object.getPrototypeOf(operation);
|
|
1131
|
-
if (operationPrototype !== Object.prototype && operationPrototype !== null || !Object.keys(operation).every((key) => key === "id" || key === "name" || key === "started")) throw new WorkflowError("MUTATION", "task activity contains an invalid operation");
|
|
1132
|
-
const id = operation.id;
|
|
1133
|
-
const name = operation.name;
|
|
1134
|
-
const started = operation.started;
|
|
1135
|
-
operations.push(Object.freeze({
|
|
1136
|
-
id,
|
|
1137
|
-
name,
|
|
1138
|
-
started
|
|
1139
|
-
}));
|
|
1140
|
-
}
|
|
1626
|
+
const operations = cloneTaskClaims(operationsInput, "operation");
|
|
1141
1627
|
let progress;
|
|
1142
1628
|
if (progressInput !== void 0) {
|
|
1143
1629
|
if (!(0, _orkestrel_contract.isRecord)(progressInput)) throw new WorkflowError("MUTATION", "task activity contains invalid progress");
|
|
@@ -1152,22 +1638,7 @@ function cloneTaskActivity(input, updated) {
|
|
|
1152
1638
|
...message === void 0 ? {} : { message }
|
|
1153
1639
|
});
|
|
1154
1640
|
}
|
|
1155
|
-
const
|
|
1156
|
-
if (constraintInputs === void 0) throw new WorkflowError("MUTATION", "task activity constraints must be an array");
|
|
1157
|
-
const constraints = [];
|
|
1158
|
-
for (const constraint of constraintInputs) {
|
|
1159
|
-
if (!(0, _orkestrel_contract.isRecord)(constraint)) throw new WorkflowError("MUTATION", "task activity contains an invalid constraint");
|
|
1160
|
-
const constraintPrototype = Object.getPrototypeOf(constraint);
|
|
1161
|
-
if (constraintPrototype !== Object.prototype && constraintPrototype !== null || !Object.keys(constraint).every((key) => key === "id" || key === "name" || key === "started")) throw new WorkflowError("MUTATION", "task activity contains an invalid constraint");
|
|
1162
|
-
const id = constraint.id;
|
|
1163
|
-
const name = constraint.name;
|
|
1164
|
-
const started = constraint.started;
|
|
1165
|
-
constraints.push(Object.freeze({
|
|
1166
|
-
id,
|
|
1167
|
-
name,
|
|
1168
|
-
started
|
|
1169
|
-
}));
|
|
1170
|
-
}
|
|
1641
|
+
const constraints = cloneTaskClaims(constraintsInput, "constraint");
|
|
1171
1642
|
const activity = Object.freeze({
|
|
1172
1643
|
...note === void 0 ? {} : { note },
|
|
1173
1644
|
...progress === void 0 ? {} : { progress },
|
|
@@ -1185,8 +1656,8 @@ function cloneTaskActivity(input, updated) {
|
|
|
1185
1656
|
//#endregion
|
|
1186
1657
|
//#region src/core/shapers.ts
|
|
1187
1658
|
/**
|
|
1188
|
-
*
|
|
1189
|
-
* `
|
|
1659
|
+
* Describes the shape of a {@link import('./types.js').TaskDefinition} — identity plus an optional
|
|
1660
|
+
* `behavior` behavior reference (a plain registry-key string, min length 1). `description` is
|
|
1190
1661
|
* optional prose.
|
|
1191
1662
|
*/
|
|
1192
1663
|
var taskShape = (0, _orkestrel_contract.objectShape)({
|
|
@@ -1199,7 +1670,7 @@ var taskShape = (0, _orkestrel_contract.objectShape)({
|
|
|
1199
1670
|
description: "Human-readable task name."
|
|
1200
1671
|
}),
|
|
1201
1672
|
description: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.stringShape)({ description: "Optional task description." })),
|
|
1202
|
-
|
|
1673
|
+
behavior: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.stringShape)({
|
|
1203
1674
|
min: 1,
|
|
1204
1675
|
description: "The registered behavior name to invoke (a registry key, not a label); omitted has no handler."
|
|
1205
1676
|
})),
|
|
@@ -1214,7 +1685,7 @@ var taskShape = (0, _orkestrel_contract.objectShape)({
|
|
|
1214
1685
|
}))
|
|
1215
1686
|
});
|
|
1216
1687
|
/**
|
|
1217
|
-
*
|
|
1688
|
+
* Describes the shape of a {@link import('./types.js').PhaseDefinition} — identity, its ordered
|
|
1218
1689
|
* {@link taskShape} tasks, and an optional positive-integer `concurrency` throttle
|
|
1219
1690
|
* (max tasks in flight; omitted ⇒ unbounded).
|
|
1220
1691
|
*/
|
|
@@ -1236,7 +1707,7 @@ var phaseShape = (0, _orkestrel_contract.objectShape)({
|
|
|
1236
1707
|
bail: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.literalShape)([true, false], { description: "Per-phase failure-policy override; omitted inherits the workflow bail." }))
|
|
1237
1708
|
});
|
|
1238
1709
|
/**
|
|
1239
|
-
*
|
|
1710
|
+
* Describes the shape of a {@link import('./types.js').WorkflowDefinition} — the contract root:
|
|
1240
1711
|
* identity, its ordered {@link phaseShape} phases, and the optional `bail` boolean
|
|
1241
1712
|
* failure policy (the literal pair `true`/`false`, the runtime mirror of the boolean
|
|
1242
1713
|
* toggle; omitted ⇒ the graceful default).
|
|
@@ -1255,13 +1726,13 @@ var workflowShape = (0, _orkestrel_contract.objectShape)({
|
|
|
1255
1726
|
bail: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.literalShape)([true, false], { description: "Failure policy: false (default) continues gracefully, true halts on the first failure." }))
|
|
1256
1727
|
});
|
|
1257
1728
|
/**
|
|
1258
|
-
*
|
|
1729
|
+
* Describes the shape of a {@link import('./types.js').TaskUpdate} — a partial edit to a
|
|
1259
1730
|
* `pending` task's `name` / `description`, both optional.
|
|
1260
1731
|
*
|
|
1261
1732
|
* @remarks
|
|
1262
1733
|
* Mirrors {@link taskShape}'s `name` / `description` constraints exactly (a provided
|
|
1263
|
-
* `name` still has `minLength: 1`); never `id` / `
|
|
1264
|
-
* are not patchable fields
|
|
1734
|
+
* `name` still has `minLength: 1`); never `id` / `behavior` / `retries` / `timeout` (those
|
|
1735
|
+
* are not patchable fields).
|
|
1265
1736
|
*/
|
|
1266
1737
|
var taskUpdateShape = (0, _orkestrel_contract.objectShape)({
|
|
1267
1738
|
name: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.stringShape)({
|
|
@@ -1271,13 +1742,13 @@ var taskUpdateShape = (0, _orkestrel_contract.objectShape)({
|
|
|
1271
1742
|
description: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.stringShape)({ description: "New task description." }))
|
|
1272
1743
|
});
|
|
1273
1744
|
/**
|
|
1274
|
-
*
|
|
1745
|
+
* Describes the shape of a {@link import('./types.js').PhaseUpdate} — a partial edit to a
|
|
1275
1746
|
* `pending` phase's `name` / `description` / `concurrency` / `bail`, all optional.
|
|
1276
1747
|
*
|
|
1277
1748
|
* @remarks
|
|
1278
1749
|
* Mirrors {@link phaseShape}'s corresponding field constraints exactly; never `id` /
|
|
1279
1750
|
* `tasks` (structural children change through the phase's own `add` / `remove` /
|
|
1280
|
-
* `move`, not a patch
|
|
1751
|
+
* `move`, not a patch).
|
|
1281
1752
|
*/
|
|
1282
1753
|
var phaseUpdateShape = (0, _orkestrel_contract.objectShape)({
|
|
1283
1754
|
name: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.stringShape)({
|
|
@@ -1294,40 +1765,42 @@ var phaseUpdateShape = (0, _orkestrel_contract.objectShape)({
|
|
|
1294
1765
|
//#endregion
|
|
1295
1766
|
//#region src/core/stores/DatabaseWorkflowStore.ts
|
|
1296
1767
|
/**
|
|
1297
|
-
*
|
|
1298
|
-
* workflow's durable run
|
|
1768
|
+
* Implements a {@link WorkflowStoreInterface} backed by one table of the `databases` layer — a
|
|
1769
|
+
* workflow's durable run state is a row, so persistence reduces to keyed point-access
|
|
1299
1770
|
* (`get` / `set` / `delete`) over a `TableInterface`, the driver-pluggable twin of the
|
|
1300
1771
|
* plain-`Map` {@link import('./MemoryWorkflowStore.js').MemoryWorkflowStore}.
|
|
1301
1772
|
*
|
|
1302
1773
|
* @remarks
|
|
1303
1774
|
* The store is driver-agnostic: it holds a single {@link TableInterface} whose backend
|
|
1304
1775
|
* (memory, JSON, SQLite, IndexedDB) is chosen by whoever builds it (the factories), so a
|
|
1305
|
-
* JSON / SQLite / IndexedDB backend swaps in
|
|
1776
|
+
* JSON / SQLite / IndexedDB backend swaps in without touching the runner or the entity tree
|
|
1306
1777
|
* — the same seam as `@orkestrel/queue`'s `DatabaseQueueStore`.
|
|
1307
1778
|
* The driver defaults to memory ({@link import('../factories.js').createDatabaseWorkflowStore}
|
|
1308
|
-
* passes `createMemoryDriver()`), so it
|
|
1779
|
+
* passes `createMemoryDriver()`), so it also works in memory out of the box; you opt into the
|
|
1309
1780
|
* durable plumbing by passing a JSON / SQLite / IndexedDB driver.
|
|
1310
1781
|
*
|
|
1311
|
-
* The {@link WorkflowSnapshot} is stored as
|
|
1782
|
+
* The {@link WorkflowSnapshot} is stored as one opaque JSON column — the table is a row of
|
|
1312
1783
|
* `{ id; snapshot }` ({@link WorkflowSnapshotRow}), the snapshot the whole JSON blob (a `rawShape`
|
|
1313
1784
|
* column the factory builds) — exactly as `DatabaseQueueStore` stores its `input`. The snapshot is
|
|
1314
|
-
* already a
|
|
1785
|
+
* already a complete, self-contained, pure-JSON payload, so storing it whole is lossless and
|
|
1315
1786
|
* sidesteps a TS2589 instantiation-depth blow-up: a structured multi-column table would force the
|
|
1316
1787
|
* contract to `Infer` the deeply-nested snapshot shape (workflow → phases → tasks → results),
|
|
1317
1788
|
* tripping the compiler — one JSON column keeps the row type flat (`snapshot` reads back as `unknown`).
|
|
1318
1789
|
*
|
|
1319
|
-
* - **`set(snapshot)` upserts under the snapshot's
|
|
1790
|
+
* - **`set(snapshot)` upserts under the snapshot's own `id`** (no separate id param) — it writes
|
|
1320
1791
|
* the row `{ id: snapshot.id, snapshot }`.
|
|
1321
|
-
* - **`get(id)` resolves the stored snapshot for an id**, narrowing the opaque JSON
|
|
1322
|
-
* a {@link WorkflowSnapshot}
|
|
1323
|
-
*
|
|
1324
|
-
*
|
|
1792
|
+
* - **`get(id)` resolves the stored snapshot for an id**, owning and narrowing the opaque JSON
|
|
1793
|
+
* column back to a {@link WorkflowSnapshot} through
|
|
1794
|
+
* {@link import('../cloners.js').cloneWorkflowSnapshot}, whose semantic pass is
|
|
1795
|
+
* {@link import('../validators.js').isOwnedWorkflowSnapshot} — the boundary narrow for
|
|
1796
|
+
* an untrusted storage read — or `undefined` if none is stored. A present snapshot whose own id
|
|
1797
|
+
* differs from the requested key rejects with normalized `RESTORE` evidence.
|
|
1325
1798
|
* - **`delete(id)` drops a snapshot by id**; an absent id is a no-op (no throw).
|
|
1326
1799
|
*
|
|
1327
|
-
*
|
|
1800
|
+
* unlike the server package's `SessionStoreInterface` there is no
|
|
1328
1801
|
* idle-TTL / eviction — a persisted run-state is durable orchestration state that lives until an
|
|
1329
|
-
* explicit `delete`. The public surface is
|
|
1330
|
-
*
|
|
1802
|
+
* explicit `delete`. The public surface is exactly `get` / `set` / `delete` — no extra members (the
|
|
1803
|
+
* guide's method bijection with {@link WorkflowStoreInterface}). Restore stays a caller concern: read a
|
|
1331
1804
|
* snapshot back and rebuild the live tree with {@link import('../factories.js').createRestoredWorkflow}.
|
|
1332
1805
|
*
|
|
1333
1806
|
* @example
|
|
@@ -1346,7 +1819,7 @@ var phaseUpdateShape = (0, _orkestrel_contract.objectShape)({
|
|
|
1346
1819
|
var DatabaseWorkflowStore = class {
|
|
1347
1820
|
#table;
|
|
1348
1821
|
/**
|
|
1349
|
-
*
|
|
1822
|
+
* Wraps a table as a workflow store.
|
|
1350
1823
|
*
|
|
1351
1824
|
* @param table - The {@link TableInterface} holding the snapshots — its row is the
|
|
1352
1825
|
* {@link WorkflowSnapshotRow} `{ id; snapshot }` shape (the snapshot one opaque JSON column)
|
|
@@ -1354,13 +1827,13 @@ var DatabaseWorkflowStore = class {
|
|
|
1354
1827
|
constructor(table) {
|
|
1355
1828
|
this.#table = table;
|
|
1356
1829
|
}
|
|
1357
|
-
/**
|
|
1830
|
+
/** Resolves and key-checks the snapshot for `id`, narrowing the opaque column to `WorkflowSnapshot`. */
|
|
1358
1831
|
async get(id) {
|
|
1359
1832
|
const row = await this.#table.get(id);
|
|
1360
1833
|
if (row === void 0) return void 0;
|
|
1361
1834
|
return cloneWorkflowSnapshot(row.snapshot, id);
|
|
1362
1835
|
}
|
|
1363
|
-
/**
|
|
1836
|
+
/** Inserts or replaces under the snapshot's own `id` (no separate id param) — the row is `{ id, snapshot }`. */
|
|
1364
1837
|
async set(snapshot) {
|
|
1365
1838
|
const owned = cloneWorkflowSnapshot(snapshot);
|
|
1366
1839
|
await this.#table.set({
|
|
@@ -1368,7 +1841,7 @@ var DatabaseWorkflowStore = class {
|
|
|
1368
1841
|
snapshot: owned
|
|
1369
1842
|
});
|
|
1370
1843
|
}
|
|
1371
|
-
/**
|
|
1844
|
+
/** Drops a snapshot by id; an absent id is a no-op (no throw). */
|
|
1372
1845
|
async delete(id) {
|
|
1373
1846
|
await this.#table.remove(id);
|
|
1374
1847
|
}
|
|
@@ -1376,28 +1849,25 @@ var DatabaseWorkflowStore = class {
|
|
|
1376
1849
|
//#endregion
|
|
1377
1850
|
//#region src/core/stores/MemoryWorkflowStore.ts
|
|
1378
1851
|
/**
|
|
1379
|
-
*
|
|
1380
|
-
* {@link WorkflowSnapshot}s keyed by workflow id, the
|
|
1381
|
-
* {@link import('../factories.js').createMemoryWorkflowStore} builds.
|
|
1852
|
+
* Implements the in-memory {@link WorkflowStoreInterface} — a process-lifetime `Map` of
|
|
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`.
|
|
1382
1856
|
*
|
|
1383
1857
|
* @remarks
|
|
1384
|
-
* A plain `Map<string, WorkflowSnapshot>` (
|
|
1385
|
-
* self-contained JSON, so no encoding is needed for the memory tier).
|
|
1386
|
-
*
|
|
1387
|
-
* NO idle-TTL and NO eviction: a persisted workflow run-state is durable orchestration state
|
|
1388
|
-
* that lives until an explicit `delete`, never silently aging out (a run that vanished
|
|
1389
|
-
* mid-flight would be a silent data loss, not a freed session). A durable backend (JSON /
|
|
1390
|
-
* SQLite / IndexedDB) swaps in through the SAME interface without touching the runner or the
|
|
1858
|
+
* A plain `Map<string, WorkflowSnapshot>` (the snapshot is already pure,
|
|
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
|
|
1391
1861
|
* entity tree — its driver-pluggable twin is
|
|
1392
1862
|
* {@link import('./DatabaseWorkflowStore.js').DatabaseWorkflowStore} (the snapshot as one opaque
|
|
1393
1863
|
* JSON column), exactly as `@orkestrel/queue`'s `MemoryQueueStore`
|
|
1394
1864
|
* twins `DatabaseQueueStore`.
|
|
1395
1865
|
*
|
|
1396
1866
|
* - **`get` resolves the persisted snapshot for an id**, or `undefined` if none is stored.
|
|
1397
|
-
* - **`set` inserts / replaces under the snapshot's
|
|
1867
|
+
* - **`set` inserts / replaces under the snapshot's own `id`** (no separate id param).
|
|
1398
1868
|
* - **`delete` drops a snapshot by id**; an absent id is a no-op (no throw).
|
|
1399
1869
|
*
|
|
1400
|
-
* The public surface is
|
|
1870
|
+
* The public surface is exactly `get` / `set` / `delete` — no extra members (the guide's method
|
|
1401
1871
|
* bijection with {@link WorkflowStoreInterface}). Restore is a caller concern: read a snapshot
|
|
1402
1872
|
* back and rebuild the live tree with {@link import('../factories.js').createRestoredWorkflow}.
|
|
1403
1873
|
*
|
|
@@ -1432,35 +1902,35 @@ var MemoryWorkflowStore = class {
|
|
|
1432
1902
|
//#endregion
|
|
1433
1903
|
//#region src/core/tasks/Task.ts
|
|
1434
1904
|
/**
|
|
1435
|
-
*
|
|
1436
|
-
* synchronous task whose explicit {@link
|
|
1905
|
+
* Implements the live leaf state machine (W-b) for one task — an observable, guarded
|
|
1906
|
+
* synchronous task whose explicit {@link LifecycleStatus} advances through the declared
|
|
1437
1907
|
* transitions, recording a {@link TaskResult} on a terminal outcome.
|
|
1438
1908
|
*
|
|
1439
1909
|
* @remarks
|
|
1440
|
-
* - **Guarded transitions
|
|
1910
|
+
* - **Guarded transitions.** `start` (→ `running`), then `complete(value)`
|
|
1441
1911
|
* (→ `completed`, records a {@link import('@orkestrel/contract').Success}), `fail(error)`
|
|
1442
1912
|
* (→ `failed`, records a {@link import('@orkestrel/contract').Failure}), `skip` (→ `skipped`),
|
|
1443
|
-
* `stop` (→ `stopped`). Each consults {@link canTransitionTask}
|
|
1444
|
-
* `TRANSITION` {@link WorkflowError} on an illegal move (
|
|
1445
|
-
* task) — the legal graph is the single source of truth, so the leaf can never
|
|
1446
|
-
* impossible state.
|
|
1913
|
+
* `stop` (→ `stopped`). Each consults {@link canTransitionTask} first and throws a
|
|
1914
|
+
* `TRANSITION` {@link WorkflowError} on an illegal move (for example, completing a
|
|
1915
|
+
* non-`running` task) — the legal graph is the single source of truth, so the leaf can never
|
|
1916
|
+
* reach an impossible state.
|
|
1447
1917
|
* - **Snapshot fidelity.** A leaf needs no override: `skipped` / `stopped` are explicit terminal
|
|
1448
1918
|
* statuses, and restore reinstates the leaf directly from {@link TaskSnapshot.status}.
|
|
1449
1919
|
* - **The cascade.** Every status change records its boxed result (when any), fires the leaf's
|
|
1450
|
-
*
|
|
1451
|
-
* transition propagates
|
|
1452
|
-
* 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
|
|
1453
1923
|
* re-derive) — the project precedent (`Runner.#settle` emits its own `fail` before propagating).
|
|
1454
|
-
* - **Observable
|
|
1455
|
-
* matching event strictly
|
|
1924
|
+
* - **Observable.** The owned {@link emitter} ({@link TaskEventMap}) fires the
|
|
1925
|
+
* matching event strictly after the state change, before the cascade; the emitter isolates
|
|
1456
1926
|
* a listener throw and routes it to its `error` handler (the `error` option), so a buggy
|
|
1457
1927
|
* observer can never corrupt a transition.
|
|
1458
|
-
* - **Declarative config
|
|
1928
|
+
* - **Declarative config.** `behavior` / `retries` / `timeout` persist in a
|
|
1459
1929
|
* {@link TaskSnapshot} (like a phase's `bail` / `concurrency`), carried verbatim from the
|
|
1460
1930
|
* matching {@link import('../types.js').TaskDefinition} / {@link TaskSnapshot} field. `handler`
|
|
1461
|
-
* is the
|
|
1931
|
+
* is the runtime-only counterpart — `behavior` resolved once at construction against the
|
|
1462
1932
|
* workflow-level {@link import('../types.js').WorkflowOptions.functions} registry — and is
|
|
1463
|
-
*
|
|
1933
|
+
* never persisted; `undefined` when `behavior` is omitted or unregistered. Only omission is a
|
|
1464
1934
|
* deliberate no-op; unresolved named work is rejected before dispatch.
|
|
1465
1935
|
*/
|
|
1466
1936
|
var Task = class {
|
|
@@ -1473,7 +1943,8 @@ var Task = class {
|
|
|
1473
1943
|
#status;
|
|
1474
1944
|
#result;
|
|
1475
1945
|
#name;
|
|
1476
|
-
#
|
|
1946
|
+
#description;
|
|
1947
|
+
#behavior;
|
|
1477
1948
|
#retries;
|
|
1478
1949
|
#timeout;
|
|
1479
1950
|
#attempts;
|
|
@@ -1486,7 +1957,7 @@ var Task = class {
|
|
|
1486
1957
|
#paused;
|
|
1487
1958
|
#gate;
|
|
1488
1959
|
#timerSignal;
|
|
1489
|
-
constructor(context, phase, workflow, recompute, options, status = "pending", result,
|
|
1960
|
+
constructor(context, phase, workflow, recompute, options, status = "pending", result, behavior, retries, timeout, metadata = {}, attempts = 0, activity, handler, silence) {
|
|
1490
1961
|
this.#context = buildTaskContext(context.phase, context);
|
|
1491
1962
|
this.#phase = phase;
|
|
1492
1963
|
this.#workflow = workflow;
|
|
@@ -1508,11 +1979,8 @@ var Task = class {
|
|
|
1508
1979
|
this.#status = status;
|
|
1509
1980
|
this.#result = result;
|
|
1510
1981
|
this.#name = context.name;
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
value: context.description
|
|
1514
|
-
});
|
|
1515
|
-
this.#run = run;
|
|
1982
|
+
this.#description = context.description;
|
|
1983
|
+
this.#behavior = behavior;
|
|
1516
1984
|
this.#retries = retries;
|
|
1517
1985
|
this.#timeout = timeout;
|
|
1518
1986
|
this.#attempts = attempts;
|
|
@@ -1538,6 +2006,9 @@ var Task = class {
|
|
|
1538
2006
|
get name() {
|
|
1539
2007
|
return this.#name;
|
|
1540
2008
|
}
|
|
2009
|
+
get description() {
|
|
2010
|
+
return this.#description;
|
|
2011
|
+
}
|
|
1541
2012
|
get context() {
|
|
1542
2013
|
return this.#context;
|
|
1543
2014
|
}
|
|
@@ -1556,8 +2027,8 @@ var Task = class {
|
|
|
1556
2027
|
get attempts() {
|
|
1557
2028
|
return this.#attempts;
|
|
1558
2029
|
}
|
|
1559
|
-
get
|
|
1560
|
-
return this.#
|
|
2030
|
+
get behavior() {
|
|
2031
|
+
return this.#behavior;
|
|
1561
2032
|
}
|
|
1562
2033
|
get handler() {
|
|
1563
2034
|
return this.#handler;
|
|
@@ -1596,7 +2067,7 @@ var Task = class {
|
|
|
1596
2067
|
this.#activity = cloneTaskActivity({}, this.#stamp());
|
|
1597
2068
|
this.#arm();
|
|
1598
2069
|
this.#emitter.emit("start", this.id);
|
|
1599
|
-
this.#
|
|
2070
|
+
this.#recompute();
|
|
1600
2071
|
}
|
|
1601
2072
|
complete(value) {
|
|
1602
2073
|
let owned;
|
|
@@ -1613,7 +2084,7 @@ var Task = class {
|
|
|
1613
2084
|
value: owned
|
|
1614
2085
|
}));
|
|
1615
2086
|
this.#emitter.emit("complete", result);
|
|
1616
|
-
this.#
|
|
2087
|
+
this.#recompute();
|
|
1617
2088
|
}
|
|
1618
2089
|
fail(error) {
|
|
1619
2090
|
const origin = error.origin === "handler" || error.origin === "timeout" || error.origin === "recovery" ? error.origin : "handler";
|
|
@@ -1628,21 +2099,21 @@ var Task = class {
|
|
|
1628
2099
|
})
|
|
1629
2100
|
}));
|
|
1630
2101
|
this.#emitter.emit("fail", result);
|
|
1631
|
-
this.#
|
|
2102
|
+
this.#recompute();
|
|
1632
2103
|
}
|
|
1633
2104
|
skip() {
|
|
1634
2105
|
this.#transition("skipped");
|
|
1635
2106
|
this.#finish();
|
|
1636
2107
|
this.#abort.abort();
|
|
1637
2108
|
this.#emitter.emit("skip");
|
|
1638
|
-
this.#
|
|
2109
|
+
this.#recompute();
|
|
1639
2110
|
}
|
|
1640
2111
|
stop() {
|
|
1641
2112
|
this.#transition("stopped");
|
|
1642
2113
|
this.#finish();
|
|
1643
2114
|
this.#abort.abort();
|
|
1644
2115
|
this.#emitter.emit("stop");
|
|
1645
|
-
this.#
|
|
2116
|
+
this.#recompute();
|
|
1646
2117
|
}
|
|
1647
2118
|
report(input) {
|
|
1648
2119
|
if (this.#status !== "running") return failure(new WorkflowError("TRANSITION", `task '${this.id}' cannot report while '${this.#status}'`, {
|
|
@@ -1670,7 +2141,7 @@ var Task = class {
|
|
|
1670
2141
|
pause() {
|
|
1671
2142
|
if (this.#paused || this.#status !== "pending" && this.#status !== "running") return;
|
|
1672
2143
|
this.#paused = true;
|
|
1673
|
-
this.#gate =
|
|
2144
|
+
this.#gate = Promise.withResolvers();
|
|
1674
2145
|
this.#emitter.emit("pause");
|
|
1675
2146
|
}
|
|
1676
2147
|
resume() {
|
|
@@ -1683,12 +2154,12 @@ var Task = class {
|
|
|
1683
2154
|
return this.#paused && this.#gate !== void 0 ? this.#gate.promise : Promise.resolve();
|
|
1684
2155
|
}
|
|
1685
2156
|
/**
|
|
1686
|
-
*
|
|
2157
|
+
* Applies a validated declarative patch to self (`name` / `description`).
|
|
1687
2158
|
*
|
|
1688
2159
|
* @remarks
|
|
1689
|
-
* Defense-in-depth
|
|
1690
|
-
* {@link import('../types.js').TaskManagerInterface.update} gates
|
|
1691
|
-
* exists + `pending`), so this is the second, redundant check — it
|
|
2160
|
+
* Defense-in-depth: the owning
|
|
2161
|
+
* {@link import('../types.js').TaskManagerInterface.update} gates first (target
|
|
2162
|
+
* exists + `pending`), so this is the second, redundant check — it throws a
|
|
1692
2163
|
* `MUTATION` {@link WorkflowError} unless this task's own `status` is `pending`.
|
|
1693
2164
|
*
|
|
1694
2165
|
* @param value - The {@link TaskUpdate} fields to apply
|
|
@@ -1703,21 +2174,18 @@ var Task = class {
|
|
|
1703
2174
|
status: this.#status
|
|
1704
2175
|
});
|
|
1705
2176
|
if (value.name !== void 0) this.#name = value.name;
|
|
1706
|
-
if (value.description !== void 0)
|
|
1707
|
-
configurable: true,
|
|
1708
|
-
value: value.description
|
|
1709
|
-
});
|
|
2177
|
+
if (value.description !== void 0) this.#description = value.description;
|
|
1710
2178
|
}
|
|
1711
2179
|
snapshot() {
|
|
1712
2180
|
return {
|
|
1713
2181
|
id: this.id,
|
|
1714
2182
|
name: this.name,
|
|
1715
|
-
...this
|
|
2183
|
+
...this.#description === void 0 ? {} : { description: this.#description },
|
|
1716
2184
|
status: this.#status,
|
|
1717
2185
|
...this.#result === void 0 ? {} : { result: this.#result },
|
|
1718
2186
|
metadata: this.#metadata,
|
|
1719
2187
|
attempts: this.#attempts,
|
|
1720
|
-
...this.#
|
|
2188
|
+
...this.#behavior === void 0 ? {} : { behavior: this.#behavior },
|
|
1721
2189
|
...this.#retries === void 0 ? {} : { retries: this.#retries },
|
|
1722
2190
|
...this.#timeout === void 0 ? {} : { timeout: this.#timeout },
|
|
1723
2191
|
...this.#activity === void 0 ? {} : { activity: this.#activity }
|
|
@@ -1744,9 +2212,6 @@ var Task = class {
|
|
|
1744
2212
|
this.#result = frozen;
|
|
1745
2213
|
return frozen;
|
|
1746
2214
|
}
|
|
1747
|
-
#escalate() {
|
|
1748
|
-
this.#recompute();
|
|
1749
|
-
}
|
|
1750
2215
|
#touch() {
|
|
1751
2216
|
if (this.#activity === void 0) return;
|
|
1752
2217
|
this.#activity = Object.freeze({
|
|
@@ -1791,25 +2256,30 @@ var Task = class {
|
|
|
1791
2256
|
//#endregion
|
|
1792
2257
|
//#region src/core/tasks/TaskManager.ts
|
|
1793
2258
|
/**
|
|
1794
|
-
*
|
|
1795
|
-
* tasks —
|
|
1796
|
-
* preserved across an interior `skip` / `remove`.
|
|
2259
|
+
* Implements the lean child manager of a {@link import('../phases/Phase.js').Phase}'s live
|
|
2260
|
+
* tasks — the task vocabulary over one insertion-ordered {@link Collection}, so positional order
|
|
2261
|
+
* is preserved across an interior `skip` / `remove`.
|
|
1797
2262
|
*
|
|
1798
2263
|
* @remarks
|
|
1799
|
-
* - **
|
|
1800
|
-
* `
|
|
1801
|
-
* `
|
|
1802
|
-
*
|
|
1803
|
-
*
|
|
1804
|
-
* - **
|
|
1805
|
-
*
|
|
1806
|
-
*
|
|
1807
|
-
*
|
|
1808
|
-
*
|
|
1809
|
-
*
|
|
1810
|
-
*
|
|
1811
|
-
*
|
|
1812
|
-
*
|
|
2264
|
+
* - **One shared store.** The insertion-ordered `Map`, the reorder step, the bounds checks, and
|
|
2265
|
+
* the gated `add` / `remove` / `move` / `update` all live in {@link Collection}, built with the
|
|
2266
|
+
* `task` noun its refusals name and the compiled {@link taskUpdateShape} guard. This class adds
|
|
2267
|
+
* the domain accessors `task` / `tasks` and nothing else, so the task and phase managers cannot
|
|
2268
|
+
* drift apart.
|
|
2269
|
+
* - **Positional store.** `append` adds one live {@link TaskInterface} at the end (the build-time
|
|
2270
|
+
* wiring path), `task(id)` looks one up, `tasks()` lists them in positional order, `count` is
|
|
2271
|
+
* the tally. A `skip` is a status change on a stored task (never a removal), so order survives
|
|
2272
|
+
* it; a snapshot RESTORE re-`append`s in the snapshot's order, reproducing it exactly.
|
|
2273
|
+
* - **Gated mutation API.** `add` / `remove` / `move` / `update` are the graceful
|
|
2274
|
+
* `Result` counterparts to `append`, gating only on the target's own existence/status/id/bounds
|
|
2275
|
+
* — a duplicate id, an absent/non-`pending` target, an out-of-bounds `index`, or a patch that
|
|
2276
|
+
* fails {@link taskUpdateShape} validation all fail gracefully with a `MUTATION`
|
|
2277
|
+
* {@link WorkflowError} instead of throwing.
|
|
2278
|
+
* - **No batch matrix.** A phase's tasks are a fixed positional set, so
|
|
2279
|
+
* `.claude/rules/patterns.md` § Batch operations (the bulk verb
|
|
2280
|
+
* overloads) is deliberately omitted — no `remove` family lives here.
|
|
2281
|
+
* - **Event-free.** A purely structural container — the live {@link TaskInterface}s own their own
|
|
2282
|
+
* emitters; the manager observes nothing.
|
|
1813
2283
|
*
|
|
1814
2284
|
* @example
|
|
1815
2285
|
* ```ts
|
|
@@ -1820,105 +2290,84 @@ var Task = class {
|
|
|
1820
2290
|
* ```
|
|
1821
2291
|
*/
|
|
1822
2292
|
var TaskManager = class {
|
|
1823
|
-
#tasks =
|
|
1824
|
-
#isUpdate = (0, _orkestrel_contract.compileGuard)(taskUpdateShape);
|
|
2293
|
+
#tasks = new Collection("task", (0, _orkestrel_contract.compileGuard)(taskUpdateShape));
|
|
1825
2294
|
get count() {
|
|
1826
|
-
return this.#tasks.
|
|
2295
|
+
return this.#tasks.count;
|
|
1827
2296
|
}
|
|
1828
2297
|
append(task) {
|
|
1829
|
-
|
|
1830
|
-
this.#tasks.set(task.id, task);
|
|
2298
|
+
this.#tasks.append(task);
|
|
1831
2299
|
}
|
|
1832
2300
|
add(task, index) {
|
|
1833
|
-
|
|
1834
|
-
const at = index ?? this.#tasks.size;
|
|
1835
|
-
if (at < 0 || at > this.#tasks.size) return failure(new WorkflowError("MUTATION", `index '${at}' out of bounds`, { index: at }));
|
|
1836
|
-
this.#reorder(insertEntry([...this.#tasks.entries()], at, task.id, task));
|
|
1837
|
-
return success(task);
|
|
2301
|
+
return this.#tasks.add(task, index);
|
|
1838
2302
|
}
|
|
1839
2303
|
remove(id) {
|
|
1840
|
-
|
|
1841
|
-
if (target === void 0 || target.status !== "pending") return failure(new WorkflowError("MUTATION", `task '${id}' is not a pending task`, { id }));
|
|
1842
|
-
this.#tasks.delete(id);
|
|
1843
|
-
return success(target);
|
|
2304
|
+
return this.#tasks.remove(id);
|
|
1844
2305
|
}
|
|
1845
2306
|
move(id, index) {
|
|
1846
|
-
|
|
1847
|
-
if (target === void 0 || target.status !== "pending") return failure(new WorkflowError("MUTATION", `task '${id}' is not a pending task`, { id }));
|
|
1848
|
-
if (index < 0 || index >= this.#tasks.size) return failure(new WorkflowError("MUTATION", `index '${index}' out of bounds`, { index }));
|
|
1849
|
-
this.#reorder(moveEntry([...this.#tasks.entries()], id, index));
|
|
1850
|
-
return success(target);
|
|
2307
|
+
return this.#tasks.move(id, index);
|
|
1851
2308
|
}
|
|
1852
2309
|
update(id, patch) {
|
|
1853
|
-
|
|
1854
|
-
if (target === void 0 || target.status !== "pending") return failure(new WorkflowError("MUTATION", `task '${id}' is not a pending task`, { id }));
|
|
1855
|
-
if (!this.#isUpdate(patch)) return failure(new WorkflowError("MUTATION", `invalid patch for task '${id}'`, { id }));
|
|
1856
|
-
target.patch(patch);
|
|
1857
|
-
return success(target);
|
|
2310
|
+
return this.#tasks.update(id, patch);
|
|
1858
2311
|
}
|
|
1859
2312
|
task(id) {
|
|
1860
|
-
return this.#tasks.
|
|
2313
|
+
return this.#tasks.entry(id);
|
|
1861
2314
|
}
|
|
1862
2315
|
tasks() {
|
|
1863
|
-
return
|
|
1864
|
-
}
|
|
1865
|
-
#reorder(entries) {
|
|
1866
|
-
this.#tasks.clear();
|
|
1867
|
-
for (const [key, value] of entries) this.#tasks.set(key, value);
|
|
2316
|
+
return this.#tasks.entries();
|
|
1868
2317
|
}
|
|
1869
2318
|
};
|
|
1870
2319
|
//#endregion
|
|
1871
2320
|
//#region src/core/phases/Phase.ts
|
|
1872
2321
|
/**
|
|
1873
|
-
*
|
|
1874
|
-
* {@link
|
|
2322
|
+
* Implements the live derived state machine (W-b) for one phase — an observable whose
|
|
2323
|
+
* {@link LifecycleStatus} is computed from its tasks (never set directly) and recomputed
|
|
1875
2324
|
* reactively as a task transitions (the middle tier of the cascade).
|
|
1876
2325
|
*
|
|
1877
2326
|
* @remarks
|
|
1878
2327
|
* - **Derived status.** `status` is `#override` when one is in force, else
|
|
1879
2328
|
* {@link derivePhaseStatus} over the live tasks' statuses. `#recompute` (passed to
|
|
1880
|
-
* each child {@link Task}) re-derives on every child transition; a
|
|
1881
|
-
* event
|
|
1882
|
-
* - **Override
|
|
1883
|
-
* phase), overriding the derived value; the override is
|
|
1884
|
-
* `override` field and restored
|
|
1885
|
-
* - **Children
|
|
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.
|
|
2334
|
+
* - **Children.** `tasks` is the lean {@link TaskManager} (an accessor + `count`,
|
|
1886
2335
|
* no batch matrix); built positionally from the snapshot so order survives an interior `skip`.
|
|
1887
2336
|
* `results()` collects the settled tasks' {@link TaskResult}s (the phase tier of the result
|
|
1888
|
-
* tree); `workflow` navigates
|
|
1889
|
-
* - **Observable
|
|
2337
|
+
* tree); `workflow` navigates up to the live parent.
|
|
2338
|
+
* - **Observable.** The owned {@link emitter} ({@link PhaseEventMap}) fires
|
|
1890
2339
|
* `start` / `complete` / `fail` / `pause` / `resume` / `skip` / `stop` after the
|
|
1891
2340
|
* corresponding status or runtime-gate change. Status events fire after the phase recomputes
|
|
1892
2341
|
* and before it escalates to the workflow, preserving child/phase cause before parent effect.
|
|
1893
2342
|
* The emitter isolates a listener throw and routes it to its `error` handler (the `error`
|
|
1894
2343
|
* option); `fail` carries the failing task's {@link TaskResult}.
|
|
1895
|
-
* - **Structural API
|
|
2344
|
+
* - **Structural API.** `add` / `remove` / `move` / `update` gate before
|
|
1896
2345
|
* delegating to {@link tasks} (the manager gates the target's own existence/status/id/
|
|
1897
|
-
* bounds), then emit the matching {@link PhaseEventMap} event on success only.
|
|
2346
|
+
* bounds), then emit the matching {@link PhaseEventMap} event on success only. Native
|
|
1898
2347
|
* gating, purely from this phase's own derived `status` (no runner-installed hook): while
|
|
1899
|
-
* `pending`, any valid `index` is accepted; while `running`, `add` accepts
|
|
2348
|
+
* `pending`, any valid `index` is accepted; while `running`, `add` accepts only a pure
|
|
1900
2349
|
* append (a live runner subscribed to the `add` event picks it up), and `remove` / `move` /
|
|
1901
2350
|
* `update` always fail gracefully (the tasks are already handed to the execution
|
|
1902
2351
|
* substrate); while terminal, everything is refused.
|
|
1903
|
-
* - **Patch
|
|
2352
|
+
* - **Patch.** `patch` applies a validated {@link PhaseUpdate} to self
|
|
1904
2353
|
* (`name` / `description` / `concurrency` / `bail`) — defense-in-depth: it throws a
|
|
1905
2354
|
* `MUTATION` {@link WorkflowError} unless this phase's own `status` is `pending`, mirroring
|
|
1906
2355
|
* the owning {@link WorkflowInterface.update}'s gate.
|
|
1907
|
-
* - **Minting
|
|
1908
|
-
* (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
|
|
1909
2358
|
* construction path {@link #append} uses at build time, so a live mint and a restored/built
|
|
1910
|
-
* task are wired
|
|
1911
|
-
* {@link import('../types.js').
|
|
1912
|
-
* {@link import('../types.js').WorkflowOptions.functions}) resolves every unique initial `
|
|
1913
|
-
* name
|
|
2359
|
+
* task are wired identically. At construction, the workflow-level
|
|
2360
|
+
* {@link import('../types.js').WorkflowRegistry} registry (threaded from
|
|
2361
|
+
* {@link import('../types.js').WorkflowOptions.functions}) resolves every unique initial `behavior`
|
|
2362
|
+
* name once before any task is built; siblings sharing a name receive the exact same captured
|
|
1914
2363
|
* runtime {@link import('../types.js').TaskInterface.handler}. A later live {@link add} reads
|
|
1915
2364
|
* that name once from the retained registry at its own mint moment. An omitted or unregistered
|
|
1916
|
-
* `
|
|
2365
|
+
* `behavior` resolves to no handler; only omission is a no-op, while an unresolved present name makes
|
|
1917
2366
|
* the containing tree non-drivable.
|
|
1918
|
-
* - **Runtime lifecycle
|
|
2367
|
+
* - **Runtime lifecycle.** `pause` / `resume` / `wait` mirror the workflow's own
|
|
1919
2368
|
* quartet, scoped to this phase — a driving
|
|
1920
2369
|
* {@link import('../types.js').WorkflowRunnerInterface.execute} gates a task's own
|
|
1921
|
-
* pre-dispatch on the workflow's gate
|
|
2370
|
+
* pre-dispatch on the workflow's gate first, then this phase's gate, without touching
|
|
1922
2371
|
* {@link status} — `paused` is runtime-only, never persisted. `skip` / `stop` (this phase's
|
|
1923
2372
|
* own terminal forcing) always release a parked {@link wait} waiter, mirroring
|
|
1924
2373
|
* {@link import('../Workflow.js').Workflow.destroy}'s cascade — a permanently-ended phase
|
|
@@ -1927,6 +2376,7 @@ var TaskManager = class {
|
|
|
1927
2376
|
var Phase = class {
|
|
1928
2377
|
#id;
|
|
1929
2378
|
#name;
|
|
2379
|
+
#description;
|
|
1930
2380
|
#workflow;
|
|
1931
2381
|
#escalateUp;
|
|
1932
2382
|
#tasks = new TaskManager();
|
|
@@ -1945,16 +2395,13 @@ var Phase = class {
|
|
|
1945
2395
|
const tasks = options?.tasks;
|
|
1946
2396
|
this.#id = snapshot.id;
|
|
1947
2397
|
this.#name = snapshot.name;
|
|
1948
|
-
|
|
1949
|
-
configurable: true,
|
|
1950
|
-
value: snapshot.description
|
|
1951
|
-
});
|
|
2398
|
+
this.#description = snapshot.description;
|
|
1952
2399
|
this.#workflow = workflow;
|
|
1953
2400
|
this.#escalateUp = escalate;
|
|
1954
2401
|
this.#functions = functions;
|
|
1955
2402
|
this.#silence = silence;
|
|
1956
2403
|
const handlers = /* @__PURE__ */ new Map();
|
|
1957
|
-
for (const task of snapshot.tasks) if (task.
|
|
2404
|
+
for (const task of snapshot.tasks) if (task.behavior !== void 0 && !handlers.has(task.behavior)) handlers.set(task.behavior, functions?.[task.behavior]);
|
|
1958
2405
|
this.#bail = bail ?? snapshot.bail;
|
|
1959
2406
|
this.#concurrency = snapshot.concurrency;
|
|
1960
2407
|
this.#emitter = new _orkestrel_emitter.Emitter({
|
|
@@ -1963,7 +2410,7 @@ var Phase = class {
|
|
|
1963
2410
|
});
|
|
1964
2411
|
for (const task of snapshot.tasks) {
|
|
1965
2412
|
const taskOptions = tasks?.[task.id];
|
|
1966
|
-
const handler = task.
|
|
2413
|
+
const handler = task.behavior === void 0 ? void 0 : handlers.get(task.behavior);
|
|
1967
2414
|
this.#append(task, taskOptions, handler);
|
|
1968
2415
|
}
|
|
1969
2416
|
this.#override = snapshot.override;
|
|
@@ -1980,11 +2427,14 @@ var Phase = class {
|
|
|
1980
2427
|
get name() {
|
|
1981
2428
|
return this.#name;
|
|
1982
2429
|
}
|
|
2430
|
+
get description() {
|
|
2431
|
+
return this.#description;
|
|
2432
|
+
}
|
|
1983
2433
|
get context() {
|
|
1984
2434
|
return buildPhaseContext(this.#workflow.context, {
|
|
1985
2435
|
id: this.#id,
|
|
1986
2436
|
name: this.#name,
|
|
1987
|
-
...this
|
|
2437
|
+
...this.#description === void 0 ? {} : { description: this.#description }
|
|
1988
2438
|
});
|
|
1989
2439
|
}
|
|
1990
2440
|
get workflow() {
|
|
@@ -2026,7 +2476,7 @@ var Phase = class {
|
|
|
2026
2476
|
pause() {
|
|
2027
2477
|
if (this.#paused || isTerminalStatus(this.status)) return;
|
|
2028
2478
|
this.#paused = true;
|
|
2029
|
-
this.#gate =
|
|
2479
|
+
this.#gate = Promise.withResolvers();
|
|
2030
2480
|
this.#emitter.emit("pause");
|
|
2031
2481
|
}
|
|
2032
2482
|
resume() {
|
|
@@ -2088,10 +2538,7 @@ var Phase = class {
|
|
|
2088
2538
|
status: this.status
|
|
2089
2539
|
});
|
|
2090
2540
|
if (value.name !== void 0) this.#name = value.name;
|
|
2091
|
-
if (value.description !== void 0)
|
|
2092
|
-
configurable: true,
|
|
2093
|
-
value: value.description
|
|
2094
|
-
});
|
|
2541
|
+
if (value.description !== void 0) this.#description = value.description;
|
|
2095
2542
|
if (value.concurrency !== void 0) this.#concurrency = value.concurrency;
|
|
2096
2543
|
if (value.bail !== void 0) this.#bail = value.bail;
|
|
2097
2544
|
}
|
|
@@ -2099,7 +2546,7 @@ var Phase = class {
|
|
|
2099
2546
|
return {
|
|
2100
2547
|
id: this.id,
|
|
2101
2548
|
name: this.name,
|
|
2102
|
-
...this
|
|
2549
|
+
...this.#description === void 0 ? {} : { description: this.#description },
|
|
2103
2550
|
status: this.status,
|
|
2104
2551
|
...this.#override === void 0 ? {} : { override: this.#override },
|
|
2105
2552
|
bail: this.#bail,
|
|
@@ -2134,7 +2581,7 @@ var Phase = class {
|
|
|
2134
2581
|
}
|
|
2135
2582
|
#failure() {
|
|
2136
2583
|
const found = findFailure(this.results());
|
|
2137
|
-
if (found === void 0) throw new
|
|
2584
|
+
if (found === void 0) throw new WorkflowError("INVARIANT", `phase '${this.id}' derived failed with no failing task result`, { phase: this.id });
|
|
2138
2585
|
return found;
|
|
2139
2586
|
}
|
|
2140
2587
|
#release() {
|
|
@@ -2152,11 +2599,11 @@ var Phase = class {
|
|
|
2152
2599
|
this.#tasks.append(created);
|
|
2153
2600
|
}
|
|
2154
2601
|
#create(snapshot, options, handler) {
|
|
2155
|
-
return new Task(buildTaskContext(this.context, snapshot), this, this.#workflow, () => this.#recompute(), options, snapshot.status, snapshot.result, snapshot.
|
|
2602
|
+
return new Task(buildTaskContext(this.context, snapshot), this, this.#workflow, () => this.#recompute(), options, snapshot.status, snapshot.result, snapshot.behavior, snapshot.retries, snapshot.timeout, snapshot.metadata, snapshot.attempts, snapshot.activity, handler, this.#silence);
|
|
2156
2603
|
}
|
|
2157
2604
|
#mint(definition) {
|
|
2158
2605
|
const snapshot = taskDefinitionToSnapshot(definition);
|
|
2159
|
-
const handler = snapshot.
|
|
2606
|
+
const handler = snapshot.behavior === void 0 ? void 0 : this.#functions?.[snapshot.behavior];
|
|
2160
2607
|
return this.#create(snapshot, void 0, handler);
|
|
2161
2608
|
}
|
|
2162
2609
|
#statuses() {
|
|
@@ -2166,24 +2613,28 @@ var Phase = class {
|
|
|
2166
2613
|
//#endregion
|
|
2167
2614
|
//#region src/core/phases/PhaseManager.ts
|
|
2168
2615
|
/**
|
|
2169
|
-
*
|
|
2170
|
-
*
|
|
2616
|
+
* Implements the lean child manager of a {@link import('../Workflow.js').Workflow}'s live
|
|
2617
|
+
* phases — the phase vocabulary over one insertion-ordered {@link Collection}, the phase analogue
|
|
2171
2618
|
* of {@link import('../tasks/TaskManager.js').TaskManager}.
|
|
2172
2619
|
*
|
|
2173
2620
|
* @remarks
|
|
2174
|
-
* - **
|
|
2175
|
-
* `
|
|
2176
|
-
*
|
|
2177
|
-
*
|
|
2178
|
-
* - **
|
|
2179
|
-
*
|
|
2180
|
-
*
|
|
2181
|
-
*
|
|
2182
|
-
*
|
|
2183
|
-
*
|
|
2184
|
-
*
|
|
2185
|
-
*
|
|
2186
|
-
*
|
|
2621
|
+
* - **One shared store.** The insertion-ordered `Map`, the reorder step, the bounds checks, and
|
|
2622
|
+
* the gated `add` / `remove` / `move` / `update` all live in {@link Collection}, built with the
|
|
2623
|
+
* `phase` noun its refusals name and the compiled {@link phaseUpdateShape} guard. This class
|
|
2624
|
+
* adds the domain accessors `phase` / `phases` and nothing else.
|
|
2625
|
+
* - **Positional store.** `append` adds one live {@link PhaseInterface} at the end, `phase(id)`
|
|
2626
|
+
* looks one up, `phases()` lists them in positional order, `count` is the tally. A snapshot
|
|
2627
|
+
* RESTORE re-`append`s in the snapshot's order, reproducing it exactly.
|
|
2628
|
+
* - **Gated mutation API.** `add` / `remove` / `move` / `update` are the graceful
|
|
2629
|
+
* `Result` counterparts to `append`, gating only on the target's own existence/status/id/bounds
|
|
2630
|
+
* — a duplicate id, an absent/non-`pending` target, an out-of-bounds `index`, or a patch that
|
|
2631
|
+
* fails {@link phaseUpdateShape} validation all fail gracefully with a `MUTATION`
|
|
2632
|
+
* {@link WorkflowError} instead of throwing.
|
|
2633
|
+
* - **No batch matrix.** A workflow's phases are a fixed positional set, so the batch verbs of
|
|
2634
|
+
* `.claude/rules/patterns.md` § Batch operations are
|
|
2635
|
+
* deliberately omitted.
|
|
2636
|
+
* - **Event-free.** A purely structural container — the live {@link PhaseInterface}s own their own
|
|
2637
|
+
* emitters.
|
|
2187
2638
|
*
|
|
2188
2639
|
* @example
|
|
2189
2640
|
* ```ts
|
|
@@ -2194,58 +2645,37 @@ var Phase = class {
|
|
|
2194
2645
|
* ```
|
|
2195
2646
|
*/
|
|
2196
2647
|
var PhaseManager = class {
|
|
2197
|
-
#phases =
|
|
2198
|
-
#isUpdate = (0, _orkestrel_contract.compileGuard)(phaseUpdateShape);
|
|
2648
|
+
#phases = new Collection("phase", (0, _orkestrel_contract.compileGuard)(phaseUpdateShape));
|
|
2199
2649
|
get count() {
|
|
2200
|
-
return this.#phases.
|
|
2650
|
+
return this.#phases.count;
|
|
2201
2651
|
}
|
|
2202
2652
|
append(phase) {
|
|
2203
|
-
|
|
2204
|
-
this.#phases.set(phase.id, phase);
|
|
2653
|
+
this.#phases.append(phase);
|
|
2205
2654
|
}
|
|
2206
2655
|
add(phase, index) {
|
|
2207
|
-
|
|
2208
|
-
const at = index ?? this.#phases.size;
|
|
2209
|
-
if (at < 0 || at > this.#phases.size) return failure(new WorkflowError("MUTATION", `index '${at}' out of bounds`, { index: at }));
|
|
2210
|
-
this.#reorder(insertEntry([...this.#phases.entries()], at, phase.id, phase));
|
|
2211
|
-
return success(phase);
|
|
2656
|
+
return this.#phases.add(phase, index);
|
|
2212
2657
|
}
|
|
2213
2658
|
remove(id) {
|
|
2214
|
-
|
|
2215
|
-
if (target === void 0 || target.status !== "pending") return failure(new WorkflowError("MUTATION", `phase '${id}' is not a pending phase`, { id }));
|
|
2216
|
-
this.#phases.delete(id);
|
|
2217
|
-
return success(target);
|
|
2659
|
+
return this.#phases.remove(id);
|
|
2218
2660
|
}
|
|
2219
2661
|
move(id, index) {
|
|
2220
|
-
|
|
2221
|
-
if (target === void 0 || target.status !== "pending") return failure(new WorkflowError("MUTATION", `phase '${id}' is not a pending phase`, { id }));
|
|
2222
|
-
if (index < 0 || index >= this.#phases.size) return failure(new WorkflowError("MUTATION", `index '${index}' out of bounds`, { index }));
|
|
2223
|
-
this.#reorder(moveEntry([...this.#phases.entries()], id, index));
|
|
2224
|
-
return success(target);
|
|
2662
|
+
return this.#phases.move(id, index);
|
|
2225
2663
|
}
|
|
2226
2664
|
update(id, patch) {
|
|
2227
|
-
|
|
2228
|
-
if (target === void 0 || target.status !== "pending") return failure(new WorkflowError("MUTATION", `phase '${id}' is not a pending phase`, { id }));
|
|
2229
|
-
if (!this.#isUpdate(patch)) return failure(new WorkflowError("MUTATION", `invalid patch for phase '${id}'`, { id }));
|
|
2230
|
-
target.patch(patch);
|
|
2231
|
-
return success(target);
|
|
2665
|
+
return this.#phases.update(id, patch);
|
|
2232
2666
|
}
|
|
2233
2667
|
phase(id) {
|
|
2234
|
-
return this.#phases.
|
|
2668
|
+
return this.#phases.entry(id);
|
|
2235
2669
|
}
|
|
2236
2670
|
phases() {
|
|
2237
|
-
return
|
|
2238
|
-
}
|
|
2239
|
-
#reorder(entries) {
|
|
2240
|
-
this.#phases.clear();
|
|
2241
|
-
for (const [key, value] of entries) this.#phases.set(key, value);
|
|
2671
|
+
return this.#phases.entries();
|
|
2242
2672
|
}
|
|
2243
2673
|
};
|
|
2244
2674
|
//#endregion
|
|
2245
2675
|
//#region src/core/Workflow.ts
|
|
2246
2676
|
/**
|
|
2247
|
-
*
|
|
2248
|
-
*
|
|
2677
|
+
* Implements the live derived state machine (W-b) for a whole workflow — the observable root
|
|
2678
|
+
* whose {@link LifecycleStatus} is computed from its phases under the `bail` policy and
|
|
2249
2679
|
* recomputed reactively as the cascade propagates up from a task transition.
|
|
2250
2680
|
*
|
|
2251
2681
|
* @remarks
|
|
@@ -2255,38 +2685,53 @@ var PhaseManager = class {
|
|
|
2255
2685
|
* passes a persisted one). Each child {@link Phase} is wired to escalate to `#recompute`.
|
|
2256
2686
|
* - **Derived status.** `status` is `#override` when forced, else
|
|
2257
2687
|
* {@link deriveWorkflowStatus} over the live phases' statuses feeding `bail`. `failed` is
|
|
2258
|
-
* reachable
|
|
2688
|
+
* reachable only under `bail: true` (a single failed task halts the workflow); under
|
|
2259
2689
|
* `bail: false` a failed phase folds into `completed`. `#recompute` diffs on each phase
|
|
2260
|
-
* change; a
|
|
2261
|
-
* - **Override
|
|
2262
|
-
* may also be force-completed vacuously. The override is
|
|
2263
|
-
* `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
|
|
2264
2694
|
* `bail`, so a restore re-derives status identically without a silent policy default.
|
|
2265
2695
|
* - **Result tree.** `results()` flattens every phase's `results()` ({@link collectResults}) — the
|
|
2266
|
-
* workflow tier; `phase(id)` + each `phase.task(id)` navigate
|
|
2267
|
-
* navigate
|
|
2696
|
+
* workflow tier; `phase(id)` + each `phase.task(id)` navigate down, a task's `phase` / `workflow`
|
|
2697
|
+
* navigate up.
|
|
2268
2698
|
* - **Snapshot.** `snapshot()` serializes the whole live tree to a {@link WorkflowSnapshot} (pure
|
|
2269
2699
|
* JSON); {@link import('./factories.js').createRestoredWorkflow} rebuilds an equivalent live tree.
|
|
2270
|
-
* - **Observable
|
|
2700
|
+
* - **Observable.** The owned {@link emitter} ({@link WorkflowEventMap}) fires
|
|
2271
2701
|
* `start` / `complete` / `fail` / `pause` / `resume` / `skip` / `stop` after the
|
|
2272
2702
|
* corresponding status or runtime-gate change; the emitter isolates a listener throw and
|
|
2273
2703
|
* routes it to its `error` handler (the `error` option); `fail` carries the failing task's
|
|
2274
2704
|
* {@link TaskResult}.
|
|
2275
|
-
* - **Structural API
|
|
2705
|
+
* - **Structural API.** `add` / `remove` / `move` / `update` gate before
|
|
2276
2706
|
* delegating to {@link phases} (the manager gates the target's own existence/status/id/
|
|
2277
|
-
* bounds), then emit the matching {@link WorkflowEventMap} event on success only.
|
|
2707
|
+
* bounds), then emit the matching {@link WorkflowEventMap} event on success only. Native,
|
|
2278
2708
|
* bottom-up gating (no runner-installed hook): refused outright while this workflow's own
|
|
2279
|
-
* `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 —
|
|
2280
2710
|
* the contiguous trailing run of `pending` phases — whose boundary is
|
|
2281
2711
|
* {@link import('./helpers.js').deriveBoundary} over the live phases' statuses. A `pending`
|
|
2282
2712
|
* workflow's phases are all `pending`, so the boundary is `0` and every position is
|
|
2283
2713
|
* naturally accepted.
|
|
2284
|
-
* - **Runtime lifecycle
|
|
2285
|
-
* phase/task boundaries
|
|
2714
|
+
* - **Runtime lifecycle.** `pause` / `resume` / `wait` gate execution at the runner's
|
|
2715
|
+
* phase/task boundaries without touching {@link status} — `paused` is runtime-only, never
|
|
2286
2716
|
* persisted. `destroy` is a terminal teardown: it `stop`s every non-terminal task and
|
|
2287
2717
|
* phase (releasing their gates and liveness resources), aborts {@link signal}, forces the
|
|
2288
2718
|
* workflow `stop` override when needed, releases its parked waiter, and marks
|
|
2289
2719
|
* {@link destroyed} — all idempotent.
|
|
2720
|
+
*
|
|
2721
|
+
* @example
|
|
2722
|
+
* ```ts
|
|
2723
|
+
* import { definitionToSnapshot, Workflow } from '@orkestrel/workflow'
|
|
2724
|
+
*
|
|
2725
|
+
* const definition = {
|
|
2726
|
+
* id: 'release',
|
|
2727
|
+
* name: 'Release',
|
|
2728
|
+
* phases: [{ id: 'build', name: 'Build', tasks: [{ id: 'compile', name: 'Compile' }] }],
|
|
2729
|
+
* }
|
|
2730
|
+
* const workflow = new Workflow(definitionToSnapshot(definition))
|
|
2731
|
+
* workflow.status // 'pending'
|
|
2732
|
+
* workflow.phase('build')?.task('compile')?.status // 'pending'
|
|
2733
|
+
* workflow.snapshot().id // 'release'
|
|
2734
|
+
* ```
|
|
2290
2735
|
*/
|
|
2291
2736
|
var Workflow = class {
|
|
2292
2737
|
#context;
|
|
@@ -2313,7 +2758,6 @@ var Workflow = class {
|
|
|
2313
2758
|
const functions = captured.functions;
|
|
2314
2759
|
const silence = captured.silence;
|
|
2315
2760
|
this.#context = buildWorkflowContext(snapshot);
|
|
2316
|
-
if (snapshot.description !== void 0) Object.defineProperty(this, "description", { value: snapshot.description });
|
|
2317
2761
|
this.#bail = bail ?? snapshot.bail;
|
|
2318
2762
|
this.#bailOverride = bail;
|
|
2319
2763
|
this.#functions = functions;
|
|
@@ -2344,6 +2788,9 @@ var Workflow = class {
|
|
|
2344
2788
|
get name() {
|
|
2345
2789
|
return this.#context.name;
|
|
2346
2790
|
}
|
|
2791
|
+
get description() {
|
|
2792
|
+
return this.#context.description;
|
|
2793
|
+
}
|
|
2347
2794
|
get context() {
|
|
2348
2795
|
return this.#context;
|
|
2349
2796
|
}
|
|
@@ -2387,7 +2834,7 @@ var Workflow = class {
|
|
|
2387
2834
|
pause() {
|
|
2388
2835
|
if (this.#paused || isTerminalStatus(this.status) || this.#destroyed) return;
|
|
2389
2836
|
this.#paused = true;
|
|
2390
|
-
this.#gate =
|
|
2837
|
+
this.#gate = Promise.withResolvers();
|
|
2391
2838
|
this.#emitter.emit("pause");
|
|
2392
2839
|
}
|
|
2393
2840
|
resume() {
|
|
@@ -2475,7 +2922,7 @@ var Workflow = class {
|
|
|
2475
2922
|
return cloneWorkflowSnapshot({
|
|
2476
2923
|
id: this.id,
|
|
2477
2924
|
name: this.name,
|
|
2478
|
-
...this.description === void 0 ? {} : { description: this.description },
|
|
2925
|
+
...this.#context.description === void 0 ? {} : { description: this.#context.description },
|
|
2479
2926
|
status: this.status,
|
|
2480
2927
|
...this.#override === void 0 ? {} : { override: this.#override },
|
|
2481
2928
|
bail: this.#bail,
|
|
@@ -2519,7 +2966,7 @@ var Workflow = class {
|
|
|
2519
2966
|
}
|
|
2520
2967
|
#failure() {
|
|
2521
2968
|
const found = findFailure(this.results());
|
|
2522
|
-
if (found === void 0) throw new
|
|
2969
|
+
if (found === void 0) throw new WorkflowError("INVARIANT", `workflow '${this.id}' derived failed with no failing task result`, { workflow: this.id });
|
|
2523
2970
|
return found;
|
|
2524
2971
|
}
|
|
2525
2972
|
#append(phase, options) {
|
|
@@ -2544,24 +2991,25 @@ var Workflow = class {
|
|
|
2544
2991
|
//#endregion
|
|
2545
2992
|
//#region src/core/WorkflowManager.ts
|
|
2546
2993
|
/**
|
|
2547
|
-
*
|
|
2994
|
+
* Implements the store-backed registry of {@link WorkflowInterface}s keyed by `id`, in insertion order —
|
|
2548
2995
|
* the additive manager tier mirroring the `@orkestrel/agent` line's `ConversationManager` /
|
|
2549
2996
|
* `WorkspaceManager`. Event-free (a registry, like its twins); the observability lives on each
|
|
2550
2997
|
* {@link WorkflowInterface}.
|
|
2551
2998
|
*
|
|
2552
2999
|
* @remarks
|
|
2553
3000
|
* - **Registry.** Workflows live in an insertion-ordered `Map` keyed by `id`. `add(definition)`
|
|
2554
|
-
* mints a live {@link WorkflowInterface} through
|
|
3001
|
+
* mints a live {@link WorkflowInterface} through the same construction path
|
|
3002
|
+
* {@link import('./factories.js').createWorkflow} takes (flowing the manager's
|
|
2555
3003
|
* `functions` registry in) and stores it under `definition.id` — an already-present id
|
|
2556
|
-
*
|
|
3004
|
+
* overwrites (last write wins). `count` is the map size, `workflow(id)` looks one up,
|
|
2557
3005
|
* `workflows()` lists them in insertion order.
|
|
2558
3006
|
* - **Durable open / save.** `open(id)` returns an already-registered workflow directly; same-id
|
|
2559
3007
|
* misses share one hydration. A concurrent `add` wins, while `remove` / `clear` invalidate
|
|
2560
3008
|
* earlier reads; wrong-key payloads reject with `RESTORE`. `save(id)` captures a registered
|
|
2561
3009
|
* workflow's snapshot at invocation and serializes same-id writes without coupling other ids.
|
|
2562
3010
|
* Both remain lenient without a store or registered id.
|
|
2563
|
-
* - **Removal.** `remove` drops one by id, or a batch (
|
|
2564
|
-
*
|
|
3011
|
+
* - **Removal.** `remove` drops one by id, or a batch (array overload first) — `true` only when
|
|
3012
|
+
* every id was removed. `clear` empties the registry.
|
|
2565
3013
|
* - **No active pointer.** Unlike its `ConversationManager` / `WorkspaceManager` twins, there is
|
|
2566
3014
|
* no `active` / `switch` — nothing in the workflow domain renders "the current workflow".
|
|
2567
3015
|
*
|
|
@@ -2570,7 +3018,7 @@ var Workflow = class {
|
|
|
2570
3018
|
* const manager = new WorkflowManager({
|
|
2571
3019
|
* functions: { compile: async (controller) => `built ${controller.task.id}` },
|
|
2572
3020
|
* })
|
|
2573
|
-
* const workflow = manager.add(definition) // minted, registered,
|
|
3021
|
+
* const workflow = manager.add(definition) // minted, registered, runnable
|
|
2574
3022
|
* manager.workflow(workflow.id) // the same workflow
|
|
2575
3023
|
* manager.count // 1
|
|
2576
3024
|
* ```
|
|
@@ -2599,7 +3047,7 @@ var WorkflowManager = class {
|
|
|
2599
3047
|
return [...this.#workflows.values()];
|
|
2600
3048
|
}
|
|
2601
3049
|
add(definition) {
|
|
2602
|
-
const workflow =
|
|
3050
|
+
const workflow = this.#build(definition);
|
|
2603
3051
|
const mutation = this.#invalidate(workflow.id);
|
|
2604
3052
|
if (mutation === void 0) this.#additions.delete(workflow.id);
|
|
2605
3053
|
else this.#additions.set(workflow.id, mutation);
|
|
@@ -2636,11 +3084,11 @@ var WorkflowManager = class {
|
|
|
2636
3084
|
}
|
|
2637
3085
|
remove(ids) {
|
|
2638
3086
|
if ((0, _orkestrel_contract.isArray)(ids)) {
|
|
2639
|
-
let removed =
|
|
3087
|
+
let removed = true;
|
|
2640
3088
|
for (const id of ids) {
|
|
2641
3089
|
this.#invalidate(id);
|
|
2642
3090
|
this.#additions.delete(id);
|
|
2643
|
-
if (this.#workflows.delete(id)) removed =
|
|
3091
|
+
if (!this.#workflows.delete(id)) removed = false;
|
|
2644
3092
|
}
|
|
2645
3093
|
return removed;
|
|
2646
3094
|
}
|
|
@@ -2670,7 +3118,7 @@ var WorkflowManager = class {
|
|
|
2670
3118
|
if (!this.#owns(id, mutation, generation)) return this.#resolve(id, generation);
|
|
2671
3119
|
let workflow;
|
|
2672
3120
|
try {
|
|
2673
|
-
workflow =
|
|
3121
|
+
workflow = this.#restore(owned);
|
|
2674
3122
|
} catch (error) {
|
|
2675
3123
|
if (!this.#owns(id, mutation, generation)) return this.#resolve(id, generation);
|
|
2676
3124
|
throw error;
|
|
@@ -2681,6 +3129,15 @@ var WorkflowManager = class {
|
|
|
2681
3129
|
this.#releaseHydration(id, lease);
|
|
2682
3130
|
}
|
|
2683
3131
|
}
|
|
3132
|
+
#build(definition) {
|
|
3133
|
+
return createWorkflowTree(definition, this.#captured());
|
|
3134
|
+
}
|
|
3135
|
+
#restore(snapshot) {
|
|
3136
|
+
return new Workflow(cloneWorkflowSnapshot(snapshot), this.#captured());
|
|
3137
|
+
}
|
|
3138
|
+
#captured() {
|
|
3139
|
+
return captureWorkflowOptions(this.#functions === void 0 ? {} : { functions: this.#functions });
|
|
3140
|
+
}
|
|
2684
3141
|
#owns(id, mutation, generation) {
|
|
2685
3142
|
return this.#generation === generation && this.#mutations.get(id) === mutation;
|
|
2686
3143
|
}
|
|
@@ -2736,9 +3193,50 @@ var WorkflowManager = class {
|
|
|
2736
3193
|
}
|
|
2737
3194
|
};
|
|
2738
3195
|
//#endregion
|
|
3196
|
+
//#region src/core/RunHolder.ts
|
|
3197
|
+
/**
|
|
3198
|
+
* Holds the active phase {@link RunnerInterface} for one
|
|
3199
|
+
* {@link import('./types.js').WorkflowRunnerInterface.execute} call, for the lifetime of that run.
|
|
3200
|
+
*
|
|
3201
|
+
* @remarks
|
|
3202
|
+
* - **One holder per run.** The engine mints a holder as a run begins and threads that one
|
|
3203
|
+
* instance through every phase of the run, so a nested `execute` reached through application
|
|
3204
|
+
* composition gets its own holder and can never clobber the suspended outer run's.
|
|
3205
|
+
* - **`hold` is the only mutation.** A phase takes the substrate runner with `hold(runner)` as it
|
|
3206
|
+
* starts and releases it with `hold()` as it settles; `runner` reads the held value back and is
|
|
3207
|
+
* `undefined` between phases and after the last one.
|
|
3208
|
+
* - **A cancel closes over the holder.** The run-level abort listener reads `runner` when it
|
|
3209
|
+
* fires, so it reaches whichever phase runner is live at that moment rather than the one that
|
|
3210
|
+
* was live when the listener was armed.
|
|
3211
|
+
* - **Event-free.** A plain cell — no emitter, no lifecycle of its own.
|
|
3212
|
+
*/
|
|
3213
|
+
var RunHolder = class {
|
|
3214
|
+
#runner;
|
|
3215
|
+
get runner() {
|
|
3216
|
+
return this.#runner;
|
|
3217
|
+
}
|
|
3218
|
+
/**
|
|
3219
|
+
* Takes the phase runner a starting phase hands this run, or releases the held one.
|
|
3220
|
+
*
|
|
3221
|
+
* @param runner - The phase runner to hold; omitted releases the held runner
|
|
3222
|
+
* @example
|
|
3223
|
+
* ```ts
|
|
3224
|
+
* import type { TaskInterface } from '@orkestrel/workflow'
|
|
3225
|
+
* import { createRunner, RunHolder } from '@orkestrel/workflow'
|
|
3226
|
+
*
|
|
3227
|
+
* const holder = new RunHolder()
|
|
3228
|
+
* holder.hold(createRunner<TaskInterface, void>({ handler: () => undefined }))
|
|
3229
|
+
* holder.hold() // released — `runner` reads `undefined` again
|
|
3230
|
+
* ```
|
|
3231
|
+
*/
|
|
3232
|
+
hold(runner) {
|
|
3233
|
+
this.#runner = runner;
|
|
3234
|
+
}
|
|
3235
|
+
};
|
|
3236
|
+
//#endregion
|
|
2739
3237
|
//#region src/core/Controller.ts
|
|
2740
3238
|
/**
|
|
2741
|
-
*
|
|
3239
|
+
* Implements the per-unit handle a runner handler receives — wraps the unit's identity,
|
|
2742
3240
|
* input, cancellation, and the run controls (`wait` / `spawn` / `abort`).
|
|
2743
3241
|
*
|
|
2744
3242
|
* @remarks
|
|
@@ -2746,14 +3244,14 @@ var WorkflowManager = class {
|
|
|
2746
3244
|
* unit it dispatches, handing it the unit's `id`, `input`, the unit's `Abort`
|
|
2747
3245
|
* handle, the queue attempt's `signal`, and a `spawn` callback that launches a
|
|
2748
3246
|
* sibling through the same queue.
|
|
2749
|
-
* - **Signal.** `signal` is the queue attempt's signal, which
|
|
3247
|
+
* - **Signal.** `signal` is the queue attempt's signal, which any-combines the
|
|
2750
3248
|
* unit's own abort, the runner-level abort (the runner aborts every unit), and
|
|
2751
3249
|
* the per-attempt timeout — so it fires on any of the three. `aborted` and
|
|
2752
3250
|
* `abort(reason)` delegate to the unit's `Abort` (the cancellation source of
|
|
2753
|
-
* truth);
|
|
3251
|
+
* truth); because the attempt signal any-includes that abort, `abort()` fires
|
|
2754
3252
|
* `signal` too.
|
|
2755
3253
|
* - **`wait` promise-parks (never a timer).** It resolves the instant the unit's
|
|
2756
|
-
* `signal` fires (immediately if already aborted)
|
|
3254
|
+
* `signal` fires (immediately if already aborted) through a one-shot listener — no
|
|
2757
3255
|
* `setTimeout`, no polling, no busy-yield — so a parked unit costs no CPU.
|
|
2758
3256
|
* - **`spawn` is fire-and-track.** It delegates to the runner's launch-a-sibling
|
|
2759
3257
|
* callback, which routes the sibling through the queue; the runner's `execute`
|
|
@@ -2764,23 +3262,32 @@ var WorkflowManager = class {
|
|
|
2764
3262
|
* {@link RunnerInterface.emitter} instead (`unit` / `spawn` / `settle` / `fail` carry the id).
|
|
2765
3263
|
*/
|
|
2766
3264
|
var Controller = class {
|
|
2767
|
-
id;
|
|
2768
|
-
input;
|
|
2769
|
-
signal;
|
|
3265
|
+
#id;
|
|
3266
|
+
#input;
|
|
3267
|
+
#signal;
|
|
2770
3268
|
#abort;
|
|
2771
3269
|
#spawn;
|
|
2772
3270
|
constructor(id, input, abort, signal, spawn) {
|
|
2773
|
-
this
|
|
2774
|
-
this
|
|
3271
|
+
this.#id = id;
|
|
3272
|
+
this.#input = input;
|
|
2775
3273
|
this.#abort = abort;
|
|
2776
|
-
this
|
|
3274
|
+
this.#signal = signal;
|
|
2777
3275
|
this.#spawn = spawn;
|
|
2778
3276
|
}
|
|
3277
|
+
get id() {
|
|
3278
|
+
return this.#id;
|
|
3279
|
+
}
|
|
3280
|
+
get input() {
|
|
3281
|
+
return this.#input;
|
|
3282
|
+
}
|
|
3283
|
+
get signal() {
|
|
3284
|
+
return this.#signal;
|
|
3285
|
+
}
|
|
2779
3286
|
get aborted() {
|
|
2780
3287
|
return this.#abort.aborted;
|
|
2781
3288
|
}
|
|
2782
3289
|
wait() {
|
|
2783
|
-
return parkSignal(this
|
|
3290
|
+
return parkSignal(this.#signal);
|
|
2784
3291
|
}
|
|
2785
3292
|
spawn(input) {
|
|
2786
3293
|
return this.#spawn(input);
|
|
@@ -2792,7 +3299,7 @@ var Controller = class {
|
|
|
2792
3299
|
//#endregion
|
|
2793
3300
|
//#region src/core/Runner.ts
|
|
2794
3301
|
/**
|
|
2795
|
-
*
|
|
3302
|
+
* Implements a thin generic orchestrator that drives declared units — and any they `spawn` —
|
|
2796
3303
|
* through a bounded-concurrency {@link createQueue}, collecting ordered results.
|
|
2797
3304
|
*
|
|
2798
3305
|
* @remarks
|
|
@@ -2801,17 +3308,17 @@ var Controller = class {
|
|
|
2801
3308
|
* bounded concurrency, retries, and the per-attempt timeout are all the Queue's —
|
|
2802
3309
|
* the Runner adds only orchestration (launching, ordering, draining, fail-fast).
|
|
2803
3310
|
* - **Spawns actually run, results stay ordered (the B2 fix).** Declared inputs and
|
|
2804
|
-
* `spawn`ed siblings flow through the
|
|
3311
|
+
* `spawn`ed siblings flow through the same `#launch`, which appends the unit's `id`
|
|
2805
3312
|
* to an ordered `#order` list and records its settled value into `#values` by `id`.
|
|
2806
3313
|
* Results are read back as `#order.map(id => #values.get(id))` — declared first (in
|
|
2807
3314
|
* input order), then spawns (in spawn order). There is no one-time task snapshot,
|
|
2808
3315
|
* so a unit spawned mid-handler is run and ordered like any other.
|
|
2809
|
-
* - **`execute` awaits the full spawn closure
|
|
2810
|
-
* an outstanding-unit `#count`
|
|
3316
|
+
* - **`execute` awaits the full spawn closure through a count gate.** `#launch` increments
|
|
3317
|
+
* an outstanding-unit `#count` before enqueuing and every settle decrements it,
|
|
2811
3318
|
* resolving the `#drained` deferred at zero. Because `spawn` calls `#launch` (so
|
|
2812
3319
|
* `#count += 1`) before the parent handler returns, the count never reaches zero
|
|
2813
3320
|
* mid-run — `execute` parks on `#drained` and so awaits the entire transitive
|
|
2814
|
-
* closure, not
|
|
3321
|
+
* closure, not only the declared units.
|
|
2815
3322
|
* - **`spawn` is fire-and-track.** A spawned unit runs through the queue regardless of
|
|
2816
3323
|
* whether its promise is awaited; the Runner never awaits a spawned promise from
|
|
2817
3324
|
* within a handler's slot (it awaits the count gate instead), so a slot-holding
|
|
@@ -2819,26 +3326,26 @@ var Controller = class {
|
|
|
2819
3326
|
* spawn by a bounded handler can still deadlock — that caveat is the caller's.)
|
|
2820
3327
|
* - **Per-unit Controller + signal.** Each unit gets a `Controller` carrying its `id`,
|
|
2821
3328
|
* `input`, the unit's `Abort` (so `aborted` / `abort` delegate to it), and the queue
|
|
2822
|
-
* attempt's `signal` (which
|
|
3329
|
+
* attempt's `signal` (which any-combines the unit abort + runner abort + timeout). A
|
|
2823
3330
|
* `spawn` callback is injected so `controller.spawn(input)` delegates to `#launch`.
|
|
2824
3331
|
* - **One-shot + fail-fast.** `execute` runs once (a second call throws). The first
|
|
2825
3332
|
* unit failure (after its retries) records the error and `abort()`s the run, so every
|
|
2826
3333
|
* sibling's signal fires; later failures are ignored and `execute` rejects with the
|
|
2827
3334
|
* first error. A user `abort(reason)` likewise rejects a running `execute`.
|
|
2828
|
-
* - **`pause` / `resume` / `stop`
|
|
2829
|
-
* delegate straight to the Queue's own pause/resume (holding/releasing the
|
|
3335
|
+
* - **`pause` / `resume` / `stop` ride the backing Queue.** `pause` / `resume`
|
|
3336
|
+
* delegate straight to the Queue's own pause/resume (holding/releasing the next
|
|
2830
3337
|
* dispatch while an in-flight unit finishes); `paused` mirrors the Queue's. `stop` is a
|
|
2831
|
-
*
|
|
2832
|
-
* 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
|
|
2833
3340
|
* `#settle` reads that fact (`#dispatched`) to treat the rejection as a stop artifact —
|
|
2834
3341
|
* not a failure, never tripping fail-fast — while an in-flight unit still runs to
|
|
2835
|
-
* completion and settles normally. `execute`
|
|
3342
|
+
* completion and settles normally. `execute` resolves (never rejects) after every unit
|
|
2836
3343
|
* has settled, with whatever results actually completed.
|
|
2837
|
-
* - **Observable
|
|
3344
|
+
* - **Observable.** The owned {@link emitter} ({@link RunnerEventMap}) carries the run
|
|
2838
3345
|
* lifecycle — `start` / `unit` / `spawn` / `settle` / `fail` / `finish` / `abort` — for
|
|
2839
|
-
* fire-and-forget observers. Every event is emitted directly, strictly
|
|
3346
|
+
* fire-and-forget observers. Every event is emitted directly, strictly after the relevant
|
|
2840
3347
|
* launch / settle / drain transition; the emitter isolates a listener throw and routes it
|
|
2841
|
-
* 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
|
|
2842
3349
|
* into, or corrupt the one-shot / fail-fast / spawn-tracking engine: the outstanding-unit
|
|
2843
3350
|
* count gate stays balanced and fail-fast still fires regardless of what a listener does.
|
|
2844
3351
|
* Observation is purely a side-channel.
|
|
@@ -2897,20 +3404,20 @@ var Runner = class {
|
|
|
2897
3404
|
return this.#queue.paused;
|
|
2898
3405
|
}
|
|
2899
3406
|
/**
|
|
2900
|
-
*
|
|
2901
|
-
* `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.
|
|
2902
3409
|
*
|
|
2903
3410
|
* @remarks
|
|
2904
|
-
* Returns `undefined` synchronously (graceful, non-throwing
|
|
2905
|
-
* runner is
|
|
3411
|
+
* Returns `undefined` synchronously (graceful, non-throwing) unless the
|
|
3412
|
+
* runner is mid-`execute` and not yet stopped — covering "never started",
|
|
2906
3413
|
* "already drained", "aborted", and "destroyed". Otherwise the unit is routed through
|
|
2907
|
-
* the
|
|
2908
|
-
* 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`
|
|
2909
3416
|
* keeps awaiting it (the drain race: `#running` flips to `false` as the very first
|
|
2910
3417
|
* step after `execute`'s `await drained.promise` settles, so a `spawn` reaching this
|
|
2911
3418
|
* method after the run has fully drained is cleanly rejected with `undefined` —
|
|
2912
3419
|
* never silently dropped, never hangs `execute`). Emits {@link RunnerEventMap.spawn}
|
|
2913
|
-
* with a `parent` of `undefined` (this call has no spawning unit)
|
|
3420
|
+
* with a `parent` of `undefined` (this call has no spawning unit) after acceptance.
|
|
2914
3421
|
*
|
|
2915
3422
|
* @param input - The unit's work payload
|
|
2916
3423
|
* @returns The unit's result promise, or `undefined` when no in-flight run can accept it
|
|
@@ -2927,11 +3434,11 @@ var Runner = class {
|
|
|
2927
3434
|
return this.#launch(input, void 0, true);
|
|
2928
3435
|
}
|
|
2929
3436
|
async execute(inputs) {
|
|
2930
|
-
if (this.#started) throw new
|
|
2931
|
-
if (this.#stopped) throw new
|
|
3437
|
+
if (this.#started) throw new WorkflowError("TRANSITION", "runner has already executed", { started: true });
|
|
3438
|
+
if (this.#stopped) throw new WorkflowError("TRANSITION", "runner is stopped", { stopped: true });
|
|
2932
3439
|
this.#started = true;
|
|
2933
3440
|
this.#running = true;
|
|
2934
|
-
const drained =
|
|
3441
|
+
const drained = Promise.withResolvers();
|
|
2935
3442
|
this.#drained = drained;
|
|
2936
3443
|
for (const input of inputs) {
|
|
2937
3444
|
if (!this.#accepts()) break;
|
|
@@ -2952,23 +3459,23 @@ var Runner = class {
|
|
|
2952
3459
|
}
|
|
2953
3460
|
abort(reason) {
|
|
2954
3461
|
if (this.#abortPromise !== void 0) return this.#abortPromise;
|
|
2955
|
-
const barrier =
|
|
3462
|
+
const barrier = Promise.withResolvers();
|
|
2956
3463
|
this.#abortPromise = barrier.promise;
|
|
2957
3464
|
barrier.promise.catch(() => {});
|
|
2958
|
-
if (this.#running && this.#failure === void 0) this.#failure =
|
|
3465
|
+
if (this.#running && this.#failure === void 0) this.#failure = failure(reason === void 0 ? /* @__PURE__ */ new Error("runner aborted") : reason);
|
|
2959
3466
|
this.#cancel(reason);
|
|
2960
3467
|
this.#stopped = true;
|
|
2961
3468
|
const cleanup = this.#queue.abort(reason);
|
|
2962
|
-
this.#
|
|
3469
|
+
this.#settleBarrier(barrier, cleanup, false);
|
|
2963
3470
|
this.#emitter.emit("abort", reason);
|
|
2964
3471
|
return barrier.promise;
|
|
2965
3472
|
}
|
|
2966
3473
|
/**
|
|
2967
|
-
*
|
|
2968
|
-
* `pause`, which holds the
|
|
3474
|
+
* Suspends dispatch (resumable): delegates to the backing queue's own
|
|
3475
|
+
* `pause`, which holds the next dispatch while any in-flight unit finishes.
|
|
2969
3476
|
*
|
|
2970
3477
|
* @remarks
|
|
2971
|
-
* A no-op
|
|
3478
|
+
* A no-op after the runner is `stopped` — a stopped runner has no dispatch left to
|
|
2972
3479
|
* suspend, mirroring the guard `stop()` itself applies. Also a no-op when already
|
|
2973
3480
|
* `paused` (the queue's own `pause` is idempotent), so calling it repeatedly is safe.
|
|
2974
3481
|
*/
|
|
@@ -2977,11 +3484,11 @@ var Runner = class {
|
|
|
2977
3484
|
this.#queue.pause();
|
|
2978
3485
|
}
|
|
2979
3486
|
/**
|
|
2980
|
-
*
|
|
3487
|
+
* Continues a paused runner; delegates to the backing queue's `resume`.
|
|
2981
3488
|
*
|
|
2982
3489
|
* @remarks
|
|
2983
|
-
* A no-op
|
|
2984
|
-
* runner is not
|
|
3490
|
+
* A no-op after the runner is `stopped` (nothing left to resume) and a no-op when the
|
|
3491
|
+
* runner is not `paused`, so calling it repeatedly or on a never-paused
|
|
2985
3492
|
* runner is safe.
|
|
2986
3493
|
*/
|
|
2987
3494
|
resume() {
|
|
@@ -2989,10 +3496,10 @@ var Runner = class {
|
|
|
2989
3496
|
this.#queue.resume();
|
|
2990
3497
|
}
|
|
2991
3498
|
/**
|
|
2992
|
-
*
|
|
3499
|
+
* Ends the runner permanently — a graceful stop, distinct from `abort`.
|
|
2993
3500
|
* Marks the runner `stopping` + `stopped`, then stops the backing queue: every
|
|
2994
|
-
* still-
|
|
2995
|
-
* "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
|
|
2996
3503
|
* keeps running to completion and settles normally. `#settle` reads `#stopping` to
|
|
2997
3504
|
* classify a never-dispatched unit's rejection as a stop artifact (decrement the count
|
|
2998
3505
|
* gate, no recorded failure, no fail-fast trip) rather than a genuine failure — a
|
|
@@ -3002,24 +3509,24 @@ var Runner = class {
|
|
|
3002
3509
|
if (this.#destroyPromise !== void 0) return this.#destroyPromise;
|
|
3003
3510
|
if (this.#abortPromise !== void 0) return this.#abortPromise;
|
|
3004
3511
|
if (this.#stopPromise !== void 0) return this.#stopPromise;
|
|
3005
|
-
const barrier =
|
|
3512
|
+
const barrier = Promise.withResolvers();
|
|
3006
3513
|
this.#stopPromise = barrier.promise;
|
|
3007
3514
|
barrier.promise.catch(() => {});
|
|
3008
3515
|
this.#stopping = true;
|
|
3009
3516
|
this.#stopped = true;
|
|
3010
3517
|
const cleanup = this.#queue.stop();
|
|
3011
|
-
this.#
|
|
3518
|
+
this.#settleBarrier(barrier, cleanup, false);
|
|
3012
3519
|
return barrier.promise;
|
|
3013
3520
|
}
|
|
3014
3521
|
destroy() {
|
|
3015
3522
|
if (this.#destroyPromise !== void 0) return this.#destroyPromise;
|
|
3016
|
-
const barrier =
|
|
3523
|
+
const barrier = Promise.withResolvers();
|
|
3017
3524
|
this.#destroyPromise = barrier.promise;
|
|
3018
3525
|
barrier.promise.catch(() => {});
|
|
3019
3526
|
this.#stopped = true;
|
|
3020
3527
|
this.abort();
|
|
3021
3528
|
const cleanup = this.#queue.destroy();
|
|
3022
|
-
this.#
|
|
3529
|
+
this.#settleBarrier(barrier, cleanup, true);
|
|
3023
3530
|
return barrier.promise;
|
|
3024
3531
|
}
|
|
3025
3532
|
#launch(input, parent, announce = parent !== void 0) {
|
|
@@ -3047,33 +3554,27 @@ var Runner = class {
|
|
|
3047
3554
|
} catch (error) {
|
|
3048
3555
|
promise = Promise.reject(error);
|
|
3049
3556
|
}
|
|
3050
|
-
promise.then((value) => this.#settle(id,
|
|
3051
|
-
ok: true,
|
|
3052
|
-
value
|
|
3053
|
-
}), (error) => this.#settle(id, {
|
|
3054
|
-
ok: false,
|
|
3055
|
-
error
|
|
3056
|
-
}));
|
|
3557
|
+
promise.then((value) => this.#settle(id, success(value)), (error) => this.#settle(id, failure(error)));
|
|
3057
3558
|
return promise;
|
|
3058
3559
|
}
|
|
3059
|
-
#dispatch(unit,
|
|
3560
|
+
#dispatch(unit, context) {
|
|
3060
3561
|
const abort = this.#aborts.get(unit.id);
|
|
3061
|
-
if (abort === void 0) throw new
|
|
3562
|
+
if (abort === void 0) throw new WorkflowError("INVARIANT", "unit abort missing", { unit: unit.id });
|
|
3062
3563
|
this.#dispatched.add(unit.id);
|
|
3063
|
-
const controller = new Controller(unit.id, unit.input, abort,
|
|
3564
|
+
const controller = new Controller(unit.id, unit.input, abort, context.signal, (input) => this.#spawn(input, unit.id));
|
|
3064
3565
|
this.#emitter.emit("unit", unit.id);
|
|
3065
3566
|
return this.#handler(controller);
|
|
3066
3567
|
}
|
|
3067
3568
|
#spawn(input, parent) {
|
|
3068
|
-
if (!this.#accepts()) throw new
|
|
3569
|
+
if (!this.#accepts()) throw new WorkflowError("TRANSITION", "spawn is unavailable outside an active run", { parent });
|
|
3069
3570
|
return this.#launch(input, parent);
|
|
3070
3571
|
}
|
|
3071
3572
|
#settle(id, outcome) {
|
|
3072
|
-
if (outcome.
|
|
3073
|
-
this.#values.set(id,
|
|
3573
|
+
if (outcome.success) {
|
|
3574
|
+
this.#values.set(id, outcome);
|
|
3074
3575
|
this.#emitter.emit("settle", id);
|
|
3075
3576
|
} else if (this.#stopping && this.#queued.has(id) && !this.#dispatched.has(id)) {} else if (this.#failure === void 0) {
|
|
3076
|
-
this.#failure =
|
|
3577
|
+
this.#failure = failure(outcome.error);
|
|
3077
3578
|
this.#emitter.emit("fail", id, outcome.error);
|
|
3078
3579
|
this.abort(outcome.error);
|
|
3079
3580
|
}
|
|
@@ -3098,31 +3599,20 @@ var Runner = class {
|
|
|
3098
3599
|
await cleanup;
|
|
3099
3600
|
return;
|
|
3100
3601
|
} catch (error) {
|
|
3101
|
-
return
|
|
3602
|
+
return failure(error);
|
|
3102
3603
|
}
|
|
3103
3604
|
}
|
|
3104
|
-
async #
|
|
3105
|
-
let
|
|
3605
|
+
async #settleBarrier(barrier, cleanup, teardown) {
|
|
3606
|
+
let cleanupFailure;
|
|
3106
3607
|
try {
|
|
3107
3608
|
await cleanup;
|
|
3108
3609
|
} catch (error) {
|
|
3109
|
-
|
|
3610
|
+
cleanupFailure = failure(error);
|
|
3110
3611
|
}
|
|
3111
3612
|
await this.#waitDrain();
|
|
3112
|
-
if (
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
async #settleDestroy(barrier, cleanup) {
|
|
3116
|
-
let failure;
|
|
3117
|
-
try {
|
|
3118
|
-
await cleanup;
|
|
3119
|
-
} catch (error) {
|
|
3120
|
-
failure = { error };
|
|
3121
|
-
}
|
|
3122
|
-
await this.#waitDrain();
|
|
3123
|
-
this.#emitter.destroy();
|
|
3124
|
-
if (failure === void 0) barrier.resolve();
|
|
3125
|
-
else barrier.reject(failure.error);
|
|
3613
|
+
if (teardown) this.#emitter.destroy();
|
|
3614
|
+
if (cleanupFailure === void 0) barrier.resolve();
|
|
3615
|
+
else barrier.reject(cleanupFailure.error);
|
|
3126
3616
|
}
|
|
3127
3617
|
async #waitDrain() {
|
|
3128
3618
|
if (this.#count === 0) return;
|
|
@@ -3135,49 +3625,61 @@ var Runner = class {
|
|
|
3135
3625
|
//#endregion
|
|
3136
3626
|
//#region src/core/tasks/TaskController.ts
|
|
3137
3627
|
/**
|
|
3138
|
-
*
|
|
3628
|
+
* Implements the attempt-scoped handle a {@link import('./types.js').WorkflowFunction} receives.
|
|
3139
3629
|
*
|
|
3140
3630
|
* @remarks
|
|
3141
|
-
* - **A leaf handle,
|
|
3631
|
+
* - **A leaf handle, not the runner `Controller`.** A workflow task is a leaf of the
|
|
3142
3632
|
* declarative W-b tree, not a fan-out unit, so it has no `spawn`; its `wait` instead
|
|
3143
3633
|
* checkpoints the workflow, phase, and task cooperative gates.
|
|
3144
|
-
* - **Folded signal.** `signal` is the cancellation folded for
|
|
3634
|
+
* - **Folded signal.** `signal` is the cancellation folded for this attempt: its per-attempt
|
|
3145
3635
|
* deadline, task stop/skip, workflow abort/timeout/budget/destroy, or a sibling fail-fast.
|
|
3146
3636
|
* A handler races its work against it; `aborted` reads it.
|
|
3147
3637
|
* - **Attempt ownership.** `report` / `pulse` are closures supplied by the runner and refuse
|
|
3148
3638
|
* after this signal aborts or a retry token supersedes this handle.
|
|
3149
3639
|
* - **Input + lineage.** `input` is the task's open `metadata` bag (its
|
|
3150
3640
|
* {@link import('./types.js').TaskInput} payload, `{}` when none); `task` is the full
|
|
3151
|
-
* {@link TaskContext}, so `task.phase` / `task.phase.workflow` navigate
|
|
3641
|
+
* {@link TaskContext}, so `task.phase` / `task.phase.workflow` navigate up the lineage.
|
|
3152
3642
|
* - **Read-up results.** `results()` returns every settled task's {@link TaskResult} across
|
|
3153
3643
|
* the phases that have already finished (a closure over the live
|
|
3154
3644
|
* {@link import('./types.js').WorkflowInterface}), so a `function` task can read an earlier
|
|
3155
|
-
* 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
|
|
3156
3646
|
* by mutating the tree.
|
|
3157
3647
|
* - **Event-free.** Like the runner `Controller`, the per-task handle carries no Emitter;
|
|
3158
3648
|
* observe the W-b entities' own emitters (`task.emitter` / `phase.emitter`) instead.
|
|
3159
3649
|
*/
|
|
3160
3650
|
var TaskController = class {
|
|
3161
|
-
signal;
|
|
3162
|
-
input;
|
|
3163
|
-
task;
|
|
3164
|
-
attempt;
|
|
3651
|
+
#signal;
|
|
3652
|
+
#input;
|
|
3653
|
+
#task;
|
|
3654
|
+
#attempt;
|
|
3165
3655
|
#entity;
|
|
3166
3656
|
#report;
|
|
3167
3657
|
#pulse;
|
|
3168
3658
|
#results;
|
|
3169
3659
|
constructor(signal, input, task, attempt, results, report, pulse) {
|
|
3170
|
-
this
|
|
3171
|
-
this
|
|
3172
|
-
this
|
|
3173
|
-
this
|
|
3660
|
+
this.#signal = signal;
|
|
3661
|
+
this.#input = input;
|
|
3662
|
+
this.#task = task.context;
|
|
3663
|
+
this.#attempt = attempt;
|
|
3174
3664
|
this.#entity = task;
|
|
3175
3665
|
this.#results = results;
|
|
3176
3666
|
this.#report = report;
|
|
3177
3667
|
this.#pulse = pulse;
|
|
3178
3668
|
}
|
|
3669
|
+
get signal() {
|
|
3670
|
+
return this.#signal;
|
|
3671
|
+
}
|
|
3672
|
+
get input() {
|
|
3673
|
+
return this.#input;
|
|
3674
|
+
}
|
|
3675
|
+
get task() {
|
|
3676
|
+
return this.#task;
|
|
3677
|
+
}
|
|
3678
|
+
get attempt() {
|
|
3679
|
+
return this.#attempt;
|
|
3680
|
+
}
|
|
3179
3681
|
get aborted() {
|
|
3180
|
-
return this
|
|
3682
|
+
return this.#signal.aborted;
|
|
3181
3683
|
}
|
|
3182
3684
|
get paused() {
|
|
3183
3685
|
if (this.#ancestorTerminal()) return false;
|
|
@@ -3190,7 +3692,7 @@ var TaskController = class {
|
|
|
3190
3692
|
return this.#pulse();
|
|
3191
3693
|
}
|
|
3192
3694
|
async wait() {
|
|
3193
|
-
while (this.paused && !this
|
|
3695
|
+
while (this.paused && !this.#signal.aborted) await this.#race(this.#gates());
|
|
3194
3696
|
}
|
|
3195
3697
|
results() {
|
|
3196
3698
|
return this.#results();
|
|
@@ -3204,11 +3706,11 @@ var TaskController = class {
|
|
|
3204
3706
|
return gates;
|
|
3205
3707
|
}
|
|
3206
3708
|
async #race(gates) {
|
|
3207
|
-
if (this
|
|
3709
|
+
if (this.#signal.aborted || gates.length === 0) return;
|
|
3208
3710
|
const deferred = Promise.withResolvers();
|
|
3209
3711
|
const onAbort = this.#resolve.bind(this, deferred);
|
|
3210
3712
|
const onTerminal = this.#resolve.bind(this, deferred);
|
|
3211
|
-
this
|
|
3713
|
+
this.#signal.addEventListener("abort", onAbort, { once: true });
|
|
3212
3714
|
this.#entity.workflow.emitter.on("skip", onTerminal);
|
|
3213
3715
|
this.#entity.workflow.emitter.on("stop", onTerminal);
|
|
3214
3716
|
this.#entity.phase.emitter.on("skip", onTerminal);
|
|
@@ -3217,7 +3719,7 @@ var TaskController = class {
|
|
|
3217
3719
|
if (this.#ancestorTerminal()) deferred.resolve();
|
|
3218
3720
|
await Promise.race([Promise.all(gates), deferred.promise]);
|
|
3219
3721
|
} finally {
|
|
3220
|
-
this
|
|
3722
|
+
this.#signal.removeEventListener("abort", onAbort);
|
|
3221
3723
|
this.#entity.workflow.emitter.off("skip", onTerminal);
|
|
3222
3724
|
this.#entity.workflow.emitter.off("stop", onTerminal);
|
|
3223
3725
|
this.#entity.phase.emitter.off("skip", onTerminal);
|
|
@@ -3234,24 +3736,34 @@ var TaskController = class {
|
|
|
3234
3736
|
//#endregion
|
|
3235
3737
|
//#region src/core/WorkflowPersistence.ts
|
|
3236
3738
|
/**
|
|
3237
|
-
*
|
|
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.
|
|
3238
3741
|
*
|
|
3239
3742
|
* @remarks
|
|
3240
|
-
*
|
|
3241
|
-
*
|
|
3743
|
+
* Exported for hosts that need to coordinate the same required boundaries around their own runner
|
|
3744
|
+
* integration.
|
|
3745
|
+
*
|
|
3746
|
+
* @example
|
|
3747
|
+
* ```ts
|
|
3748
|
+
* import { WorkflowPersistence, createMemoryWorkflowStore, createWorkflow } from '@orkestrel/workflow'
|
|
3749
|
+
*
|
|
3750
|
+
* const workflow = createWorkflow({ id: 'durable', name: 'Durable', phases: [] })
|
|
3751
|
+
* const persistence = new WorkflowPersistence(workflow, createMemoryWorkflowStore())
|
|
3752
|
+
* await persistence.checkpoint('initial')
|
|
3753
|
+
* const durable = await persistence.finalize()
|
|
3754
|
+
* persistence.detach() // idempotent after finalize
|
|
3755
|
+
* ```
|
|
3242
3756
|
*/
|
|
3243
3757
|
var WorkflowPersistence = class {
|
|
3244
3758
|
#workflow;
|
|
3245
3759
|
#store;
|
|
3246
3760
|
#phases = /* @__PURE__ */ new Set();
|
|
3247
3761
|
#tasks = /* @__PURE__ */ new Set();
|
|
3248
|
-
#
|
|
3762
|
+
#onChange;
|
|
3249
3763
|
#onWorkflowAdd;
|
|
3250
3764
|
#onWorkflowRemove;
|
|
3251
|
-
#onPhaseChange;
|
|
3252
3765
|
#onPhaseAdd;
|
|
3253
3766
|
#onPhaseRemove;
|
|
3254
|
-
#onTaskChange;
|
|
3255
3767
|
#writing;
|
|
3256
3768
|
#error;
|
|
3257
3769
|
#fault;
|
|
@@ -3261,32 +3773,29 @@ var WorkflowPersistence = class {
|
|
|
3261
3773
|
constructor(workflow, store) {
|
|
3262
3774
|
this.#workflow = workflow;
|
|
3263
3775
|
this.#store = store;
|
|
3264
|
-
this.#
|
|
3776
|
+
this.#onChange = this.#change.bind(this);
|
|
3265
3777
|
this.#onWorkflowAdd = this.#addPhase.bind(this);
|
|
3266
3778
|
this.#onWorkflowRemove = this.#removePhase.bind(this);
|
|
3267
|
-
this.#onPhaseChange = this.#change.bind(this);
|
|
3268
3779
|
this.#onPhaseAdd = this.#addTask.bind(this);
|
|
3269
3780
|
this.#onPhaseRemove = this.#removeTask.bind(this);
|
|
3270
|
-
this.#onTaskChange = this.#change.bind(this);
|
|
3271
3781
|
this.#attachWorkflow();
|
|
3272
3782
|
}
|
|
3273
3783
|
get fault() {
|
|
3274
3784
|
return this.#fault;
|
|
3275
3785
|
}
|
|
3276
3786
|
/**
|
|
3277
|
-
*
|
|
3787
|
+
* Persists every change through this required boundary.
|
|
3278
3788
|
*
|
|
3279
3789
|
* @param checkpoint - The boundary being made durable
|
|
3280
3790
|
* @param task - The task owning an attempt or settlement
|
|
3281
3791
|
* @param attempt - The persisted attempt number
|
|
3282
|
-
* @returns
|
|
3792
|
+
* @returns True if the most recent state reached the store; false otherwise
|
|
3283
3793
|
*/
|
|
3284
3794
|
async checkpoint(checkpoint, task, attempt) {
|
|
3285
3795
|
const revision = this.#mark();
|
|
3286
3796
|
while (this.#stored < revision) await this.#flush();
|
|
3287
3797
|
if (this.#error === void 0) return true;
|
|
3288
3798
|
if (this.#fault === void 0) this.#fault = Object.freeze({
|
|
3289
|
-
origin: "persistence",
|
|
3290
3799
|
checkpoint,
|
|
3291
3800
|
message: this.#error,
|
|
3292
3801
|
...task === void 0 ? {} : { task: task.id },
|
|
@@ -3295,37 +3804,25 @@ var WorkflowPersistence = class {
|
|
|
3295
3804
|
return false;
|
|
3296
3805
|
}
|
|
3297
3806
|
/**
|
|
3298
|
-
*
|
|
3807
|
+
* Stops observing the live tree and persists its final state.
|
|
3299
3808
|
*
|
|
3300
|
-
* @returns
|
|
3809
|
+
* @returns True if the final snapshot reached the store; false otherwise
|
|
3301
3810
|
*/
|
|
3302
3811
|
async finalize() {
|
|
3303
3812
|
this.detach();
|
|
3304
3813
|
return this.checkpoint("final");
|
|
3305
3814
|
}
|
|
3306
|
-
/**
|
|
3815
|
+
/** Stops observing the live tree. */
|
|
3307
3816
|
detach() {
|
|
3308
3817
|
if (!this.#attached) return;
|
|
3309
3818
|
this.#attached = false;
|
|
3310
|
-
this.#workflow.emitter.off(
|
|
3311
|
-
this.#workflow.emitter.off("complete", this.#onWorkflowChange);
|
|
3312
|
-
this.#workflow.emitter.off("fail", this.#onWorkflowChange);
|
|
3313
|
-
this.#workflow.emitter.off("skip", this.#onWorkflowChange);
|
|
3314
|
-
this.#workflow.emitter.off("stop", this.#onWorkflowChange);
|
|
3315
|
-
this.#workflow.emitter.off("move", this.#onWorkflowChange);
|
|
3316
|
-
this.#workflow.emitter.off("update", this.#onWorkflowChange);
|
|
3819
|
+
for (const event of PERSISTED_NODE_EVENTS) this.#workflow.emitter.off(event, this.#onChange);
|
|
3317
3820
|
this.#workflow.emitter.off("add", this.#onWorkflowAdd);
|
|
3318
3821
|
this.#workflow.emitter.off("remove", this.#onWorkflowRemove);
|
|
3319
3822
|
for (const phase of this.#phases) this.#detachPhase(phase);
|
|
3320
3823
|
}
|
|
3321
3824
|
#attachWorkflow() {
|
|
3322
|
-
this.#workflow.emitter.on(
|
|
3323
|
-
this.#workflow.emitter.on("complete", this.#onWorkflowChange);
|
|
3324
|
-
this.#workflow.emitter.on("fail", this.#onWorkflowChange);
|
|
3325
|
-
this.#workflow.emitter.on("skip", this.#onWorkflowChange);
|
|
3326
|
-
this.#workflow.emitter.on("stop", this.#onWorkflowChange);
|
|
3327
|
-
this.#workflow.emitter.on("move", this.#onWorkflowChange);
|
|
3328
|
-
this.#workflow.emitter.on("update", this.#onWorkflowChange);
|
|
3825
|
+
for (const event of PERSISTED_NODE_EVENTS) this.#workflow.emitter.on(event, this.#onChange);
|
|
3329
3826
|
this.#workflow.emitter.on("add", this.#onWorkflowAdd);
|
|
3330
3827
|
this.#workflow.emitter.on("remove", this.#onWorkflowRemove);
|
|
3331
3828
|
for (const phase of this.#workflow.phases.phases()) this.#attachPhase(phase);
|
|
@@ -3333,26 +3830,14 @@ var WorkflowPersistence = class {
|
|
|
3333
3830
|
#attachPhase(phase) {
|
|
3334
3831
|
if (this.#phases.has(phase)) return;
|
|
3335
3832
|
this.#phases.add(phase);
|
|
3336
|
-
phase.emitter.on(
|
|
3337
|
-
phase.emitter.on("complete", this.#onPhaseChange);
|
|
3338
|
-
phase.emitter.on("fail", this.#onPhaseChange);
|
|
3339
|
-
phase.emitter.on("skip", this.#onPhaseChange);
|
|
3340
|
-
phase.emitter.on("stop", this.#onPhaseChange);
|
|
3341
|
-
phase.emitter.on("move", this.#onPhaseChange);
|
|
3342
|
-
phase.emitter.on("update", this.#onPhaseChange);
|
|
3833
|
+
for (const event of PERSISTED_NODE_EVENTS) phase.emitter.on(event, this.#onChange);
|
|
3343
3834
|
phase.emitter.on("add", this.#onPhaseAdd);
|
|
3344
3835
|
phase.emitter.on("remove", this.#onPhaseRemove);
|
|
3345
3836
|
for (const task of phase.tasks.tasks()) this.#attachTask(task);
|
|
3346
3837
|
}
|
|
3347
3838
|
#detachPhase(phase) {
|
|
3348
3839
|
if (!this.#phases.delete(phase)) return;
|
|
3349
|
-
phase.emitter.off(
|
|
3350
|
-
phase.emitter.off("complete", this.#onPhaseChange);
|
|
3351
|
-
phase.emitter.off("fail", this.#onPhaseChange);
|
|
3352
|
-
phase.emitter.off("skip", this.#onPhaseChange);
|
|
3353
|
-
phase.emitter.off("stop", this.#onPhaseChange);
|
|
3354
|
-
phase.emitter.off("move", this.#onPhaseChange);
|
|
3355
|
-
phase.emitter.off("update", this.#onPhaseChange);
|
|
3840
|
+
for (const event of PERSISTED_NODE_EVENTS) phase.emitter.off(event, this.#onChange);
|
|
3356
3841
|
phase.emitter.off("add", this.#onPhaseAdd);
|
|
3357
3842
|
phase.emitter.off("remove", this.#onPhaseRemove);
|
|
3358
3843
|
for (const task of phase.tasks.tasks()) this.#detachTask(task);
|
|
@@ -3360,23 +3845,11 @@ var WorkflowPersistence = class {
|
|
|
3360
3845
|
#attachTask(task) {
|
|
3361
3846
|
if (this.#tasks.has(task)) return;
|
|
3362
3847
|
this.#tasks.add(task);
|
|
3363
|
-
task.emitter.on(
|
|
3364
|
-
task.emitter.on("complete", this.#onTaskChange);
|
|
3365
|
-
task.emitter.on("fail", this.#onTaskChange);
|
|
3366
|
-
task.emitter.on("skip", this.#onTaskChange);
|
|
3367
|
-
task.emitter.on("stop", this.#onTaskChange);
|
|
3368
|
-
task.emitter.on("report", this.#onTaskChange);
|
|
3369
|
-
task.emitter.on("pulse", this.#onTaskChange);
|
|
3848
|
+
for (const event of PERSISTED_TASK_EVENTS) task.emitter.on(event, this.#onChange);
|
|
3370
3849
|
}
|
|
3371
3850
|
#detachTask(task) {
|
|
3372
3851
|
if (!this.#tasks.delete(task)) return;
|
|
3373
|
-
task.emitter.off(
|
|
3374
|
-
task.emitter.off("complete", this.#onTaskChange);
|
|
3375
|
-
task.emitter.off("fail", this.#onTaskChange);
|
|
3376
|
-
task.emitter.off("skip", this.#onTaskChange);
|
|
3377
|
-
task.emitter.off("stop", this.#onTaskChange);
|
|
3378
|
-
task.emitter.off("report", this.#onTaskChange);
|
|
3379
|
-
task.emitter.off("pulse", this.#onTaskChange);
|
|
3852
|
+
for (const event of PERSISTED_TASK_EVENTS) task.emitter.off(event, this.#onChange);
|
|
3380
3853
|
}
|
|
3381
3854
|
#addPhase(phase) {
|
|
3382
3855
|
this.#attachPhase(phase);
|
|
@@ -3434,82 +3907,83 @@ var WorkflowPersistence = class {
|
|
|
3434
3907
|
//#endregion
|
|
3435
3908
|
//#region src/core/WorkflowRunner.ts
|
|
3436
3909
|
/**
|
|
3437
|
-
*
|
|
3438
|
-
* 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
|
|
3439
3912
|
* resolved handler under the `bail` policy.
|
|
3440
3913
|
*
|
|
3441
3914
|
* @remarks
|
|
3442
3915
|
* - **Composes, never re-implements.** Per-phase bounded concurrency is one
|
|
3443
|
-
* {@link createRunner} per phase (the substrate {@link RunnerInterface}
|
|
3916
|
+
* {@link createRunner} per phase (the substrate {@link import('./types.js').RunnerInterface}
|
|
3917
|
+
* over the workers
|
|
3444
3918
|
* `Queue`); `bail` maps onto that Runner's fail-fast vs settle-all; the run-level abort /
|
|
3445
3919
|
* timeout / budget / entity `signal` fold through the `@orkestrel/abort` signal contract,
|
|
3446
3920
|
* {@link createTimeout}, and `AbortSignal.any` (exactly as the agent runtime folds its bounds);
|
|
3447
3921
|
* pacing is the shipped
|
|
3448
|
-
* {@link SchedulerInterface}. The runner writes
|
|
3922
|
+
* {@link SchedulerInterface}. The runner writes zero concurrency / retry / abort logic of
|
|
3449
3923
|
* its own — it only sequences phases, dispatches a task's own handler, and drives the live
|
|
3450
3924
|
* entity. The workflow layer owns per-task deadlines because timeout settlement must
|
|
3451
3925
|
* update the live leaf under the phase's `bail` policy before the substrate unit settles.
|
|
3452
3926
|
* - **Pure engine — no integration registry.** The runner carries no behavior or provider
|
|
3453
3927
|
* registry: each live {@link TaskInterface} already
|
|
3454
3928
|
* resolved its own {@link import('./types.js').WorkflowFunction} into
|
|
3455
|
-
* {@link import('./types.js').TaskInterface.handler}
|
|
3929
|
+
* {@link import('./types.js').TaskInterface.handler} once at construction (build, restore,
|
|
3456
3930
|
* or a live mint all resolve it identically, from {@link WorkflowOptions.functions}), so
|
|
3457
|
-
* dispatch is
|
|
3931
|
+
* dispatch is "invoke the task's own handler". Provider, protocol, and tool
|
|
3458
3932
|
* integrations remain application-owned {@link import('./types.js').WorkflowFunction}s
|
|
3459
3933
|
* composed into {@link WorkflowOptions.functions}. This module imports none of them.
|
|
3460
|
-
* - **Two `execute` forms, one engine.** `execute(definition, options)`
|
|
3461
|
-
* from a {@link WorkflowDefinition} (single source of truth for the `
|
|
3462
|
-
* metadata); `execute(workflow, options)`
|
|
3463
|
-
* {@link WorkflowInterface} instead — the entity-native control surface
|
|
3464
|
-
* `pause` / `resume` / `add` / `stop` / `destroy` live on the entity itself). Both forms
|
|
3465
|
-
* converge on the
|
|
3466
|
-
* exists — `#runTask` reads each task's
|
|
3467
|
-
* / `retries` / `timeout`, and `#runPhase` reads each phase's
|
|
3934
|
+
* - **Two `execute` forms, one engine.** `execute(definition, options)` builds the live tree
|
|
3935
|
+
* from a {@link WorkflowDefinition} (single source of truth for the `behavior` / `concurrency`
|
|
3936
|
+
* metadata); `execute(workflow, options)` drives a caller-owned, already-built
|
|
3937
|
+
* {@link WorkflowInterface} instead — the entity-native control surface
|
|
3938
|
+
* (`pause` / `resume` / `add` / `stop` / `destroy` live on the entity itself). Both forms
|
|
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
|
|
3468
3942
|
* {@link PhaseInterface.concurrency} / `bail`, so a live `add`-minted phase or task (V5)
|
|
3469
|
-
* runs under
|
|
3470
|
-
* - **Phases sequential, tasks concurrent —
|
|
3471
|
-
* 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
|
|
3472
3946
|
* manager, not a one-time snapshot) so a caller's `workflow.add(phaseDefinition)` mid-run is
|
|
3473
|
-
* picked up. Within a phase, `#runPhase` subscribes to that phase's `add` event
|
|
3474
|
-
* 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
|
|
3475
3949
|
* Runner (so it is actually dispatched, under the same `concurrency`); a task added too late
|
|
3476
3950
|
* for `spawn` to accept (the runner already drained) is swept `skip`ped afterward so the
|
|
3477
3951
|
* phase always reaches a coherent terminal state.
|
|
3478
3952
|
* - **Dispatch by handler.** `#runTask` invokes the live task's own
|
|
3479
|
-
* {@link import('./types.js').TaskInterface.handler} directly. An omitted `
|
|
3953
|
+
* {@link import('./types.js').TaskInterface.handler} directly. An omitted `behavior` deliberately
|
|
3480
3954
|
* auto-completes with JSON `null`; a present unresolved name is rejected by the synchronous
|
|
3481
3955
|
* execution claim and never false-completes.
|
|
3482
3956
|
* - **`bail` → substrate.** Under `bail: true` (halt) a genuine task failure `fail`s the leaf
|
|
3483
|
-
*
|
|
3957
|
+
* then re-throws, so the substrate Runner fail-fasts — it aborts the in-flight siblings
|
|
3484
3958
|
* (their `controller.signal` fires; a mid-flight sibling `skip`s) and rejects the phase run;
|
|
3485
3959
|
* `#execute` then `skip`s the remaining tasks / phases (the workflow derives `failed`).
|
|
3486
|
-
* 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
|
|
3487
3961
|
* the Runner settles every unit (allSettled) and the run finishes (the workflow derives
|
|
3488
3962
|
* `completed`, the failure recorded in the result tree).
|
|
3489
3963
|
* - **Pause / stop / destroy gates.** Workflow, phase, and task gates are checked before
|
|
3490
3964
|
* dispatch, and a running handler can checkpoint their folded state through
|
|
3491
3965
|
* {@link import('./types.js').TaskControllerInterface.wait}. Because the substrate acquires
|
|
3492
3966
|
* concurrency before this handler gate, a paused task occupies one phase slot until resume;
|
|
3493
|
-
* 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
|
|
3494
3968
|
* `workflow.stop()` (no signal involved) is caught at
|
|
3495
3969
|
* those same gates: not-yet-started work is `skip`ped, in-flight work finishes naturally. A
|
|
3496
|
-
*
|
|
3970
|
+
* hard `workflow.destroy()` aborts {@link WorkflowInterface.signal}, which `#fold` has folded
|
|
3497
3971
|
* into the run's composed signal — so it cancels the active phase Runner (and every
|
|
3498
|
-
* in-flight task) exactly like an external abort / timeout / budget fire.
|
|
3499
|
-
* `wait()` gate is
|
|
3500
|
-
*
|
|
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
|
|
3501
3975
|
* halt / abort re-checks after the gate then decide the outcome.
|
|
3502
3976
|
* - **Abort / Timeout / Budget / entity-signal fold.** `#execute` folds the live workflow's
|
|
3503
3977
|
* own {@link WorkflowInterface.signal}, the run's external `signal`, a
|
|
3504
3978
|
* {@link TimeoutInterface}, and the `@orkestrel/budget` package's `BudgetInterface`'s
|
|
3505
3979
|
* `signal` into one `runSignal` (`AbortSignal.any`); a fire aborts the active phase's Runner
|
|
3506
|
-
* (cancelling every in-flight task) and
|
|
3980
|
+
* (cancelling every in-flight task) and halts the run — the remaining tasks / phases `skip`
|
|
3507
3981
|
* and the workflow is force-`stop`ped (settles `stopped`). Each task's
|
|
3508
3982
|
* {@link TaskController} signal `AbortSignal.any`-combines the substrate per-unit signal with
|
|
3509
3983
|
* `runSignal`, so a handler observes either cause directly.
|
|
3510
|
-
* - **Re-entrant-safe.** No shared per-run mutable field:
|
|
3511
|
-
*
|
|
3512
|
-
* state.
|
|
3984
|
+
* - **Re-entrant-safe.** No shared per-run mutable field: each `#execute` mints its own
|
|
3985
|
+
* {@link import('./RunHolder.js').RunHolder}, so a nested application-level `execute` cannot
|
|
3986
|
+
* clobber the outer run's state.
|
|
3513
3987
|
*/
|
|
3514
3988
|
var WorkflowRunner = class WorkflowRunner {
|
|
3515
3989
|
static #executions = /* @__PURE__ */ new WeakSet();
|
|
@@ -3518,7 +3992,7 @@ var WorkflowRunner = class WorkflowRunner {
|
|
|
3518
3992
|
this.#scheduler = scheduler;
|
|
3519
3993
|
}
|
|
3520
3994
|
execute(target, options) {
|
|
3521
|
-
if (
|
|
3995
|
+
if (isWorkflowInterface(target)) {
|
|
3522
3996
|
const signal = options?.signal;
|
|
3523
3997
|
const timeout = options?.timeout;
|
|
3524
3998
|
const budget = options?.budget;
|
|
@@ -3531,7 +4005,7 @@ var WorkflowRunner = class WorkflowRunner {
|
|
|
3531
4005
|
const timeout = options?.timeout;
|
|
3532
4006
|
const budget = options?.budget;
|
|
3533
4007
|
const store = options?.store;
|
|
3534
|
-
const workflow =
|
|
4008
|
+
const workflow = createWorkflowTree(target, captured);
|
|
3535
4009
|
this.#acquire(workflow);
|
|
3536
4010
|
return this.#execute(workflow, signal, timeout, budget, store);
|
|
3537
4011
|
}
|
|
@@ -3545,7 +4019,7 @@ var WorkflowRunner = class WorkflowRunner {
|
|
|
3545
4019
|
WorkflowRunner.#executions.add(workflow);
|
|
3546
4020
|
}
|
|
3547
4021
|
async #execute(workflow, signal, ms, budget, store) {
|
|
3548
|
-
const holder =
|
|
4022
|
+
const holder = new RunHolder();
|
|
3549
4023
|
let timeout;
|
|
3550
4024
|
let persistence;
|
|
3551
4025
|
let runSignal;
|
|
@@ -3560,7 +4034,7 @@ var WorkflowRunner = class WorkflowRunner {
|
|
|
3560
4034
|
if (runSignal.aborted) onCancel();
|
|
3561
4035
|
else runSignal.addEventListener("abort", onCancel, { once: true });
|
|
3562
4036
|
if (persistence !== void 0 && !await persistence.checkpoint("initial")) {
|
|
3563
|
-
if (
|
|
4037
|
+
if (isStoppable(workflow)) workflow.stop();
|
|
3564
4038
|
this.#skipFrom(workflow.phases.phases(), 0);
|
|
3565
4039
|
}
|
|
3566
4040
|
let index = 0;
|
|
@@ -3572,12 +4046,12 @@ var WorkflowRunner = class WorkflowRunner {
|
|
|
3572
4046
|
index += 1;
|
|
3573
4047
|
continue;
|
|
3574
4048
|
}
|
|
3575
|
-
if (
|
|
4049
|
+
if (runSignal.aborted || isHalted(workflow)) {
|
|
3576
4050
|
this.#haltFrom(phases, index, workflow, runSignal);
|
|
3577
4051
|
break;
|
|
3578
4052
|
}
|
|
3579
4053
|
if (workflow.paused) await this.#raceWait(workflow.wait(), runSignal, void 0, workflow);
|
|
3580
|
-
if (
|
|
4054
|
+
if (runSignal.aborted || isHalted(workflow)) {
|
|
3581
4055
|
this.#haltFrom(workflow.phases.phases(), index, workflow, runSignal);
|
|
3582
4056
|
break;
|
|
3583
4057
|
}
|
|
@@ -3592,10 +4066,10 @@ var WorkflowRunner = class WorkflowRunner {
|
|
|
3592
4066
|
}
|
|
3593
4067
|
index += 1;
|
|
3594
4068
|
const remaining = workflow.phases.phases();
|
|
3595
|
-
if (index < remaining.length && !
|
|
4069
|
+
if (index < remaining.length && !runSignal.aborted) await this.#pace(runSignal);
|
|
3596
4070
|
}
|
|
3597
|
-
if (
|
|
3598
|
-
else if (
|
|
4071
|
+
if (runSignal.aborted) this.#haltFrom(workflow.phases.phases(), 0, workflow, runSignal);
|
|
4072
|
+
else if (isCompletable(workflow)) workflow.complete();
|
|
3599
4073
|
const durable = await persistence?.finalize();
|
|
3600
4074
|
return {
|
|
3601
4075
|
workflow,
|
|
@@ -3605,7 +4079,7 @@ var WorkflowRunner = class WorkflowRunner {
|
|
|
3605
4079
|
...persistence?.fault === void 0 ? {} : { fault: persistence.fault }
|
|
3606
4080
|
};
|
|
3607
4081
|
} catch (error) {
|
|
3608
|
-
if (
|
|
4082
|
+
if (isStoppable(workflow)) workflow.stop();
|
|
3609
4083
|
this.#skipFrom(workflow.phases.phases(), 0);
|
|
3610
4084
|
await persistence?.finalize();
|
|
3611
4085
|
throw error;
|
|
@@ -3640,22 +4114,22 @@ var WorkflowRunner = class WorkflowRunner {
|
|
|
3640
4114
|
entries: this.#entry.bind(this),
|
|
3641
4115
|
handler: this.#runUnit.bind(this, workflow, runSignal, bail, attempts, owners, persistence)
|
|
3642
4116
|
});
|
|
3643
|
-
holder.
|
|
4117
|
+
holder.hold(created);
|
|
3644
4118
|
try {
|
|
3645
4119
|
await created.execute(tasks);
|
|
3646
4120
|
return false;
|
|
3647
4121
|
} catch {
|
|
3648
|
-
return !
|
|
4122
|
+
return !runSignal.aborted;
|
|
3649
4123
|
} finally {
|
|
3650
4124
|
try {
|
|
3651
4125
|
await created.destroy();
|
|
3652
4126
|
} finally {
|
|
3653
|
-
holder.
|
|
4127
|
+
holder.hold();
|
|
3654
4128
|
}
|
|
3655
4129
|
}
|
|
3656
4130
|
} finally {
|
|
3657
4131
|
phase.emitter.off("add", onAdd);
|
|
3658
|
-
if (
|
|
4132
|
+
if (runSignal.aborted && isStoppable(workflow)) workflow.stop();
|
|
3659
4133
|
for (const task of phase.tasks.tasks()) this.#skip(task);
|
|
3660
4134
|
}
|
|
3661
4135
|
}
|
|
@@ -3679,7 +4153,7 @@ var WorkflowRunner = class WorkflowRunner {
|
|
|
3679
4153
|
attempts.set(task.id, attempt);
|
|
3680
4154
|
const last = attempt > Math.max(0, task.retries ?? 0);
|
|
3681
4155
|
if (task.status !== "pending" && task.status !== "running") return;
|
|
3682
|
-
if (
|
|
4156
|
+
if (isSkipping(task, controller, runSignal) || isHalted(workflow, task.phase)) {
|
|
3683
4157
|
this.#settleCancelled(task, workflow, runSignal);
|
|
3684
4158
|
return;
|
|
3685
4159
|
}
|
|
@@ -3692,15 +4166,15 @@ var WorkflowRunner = class WorkflowRunner {
|
|
|
3692
4166
|
owners.set(task.id, attempt);
|
|
3693
4167
|
deadline?.start();
|
|
3694
4168
|
const durable = persistence === void 0 ? true : await persistence.checkpoint("attempt", task, attempt);
|
|
3695
|
-
if (!
|
|
4169
|
+
if (!ownsAttempt(owners, task, attempt)) return;
|
|
3696
4170
|
if (!durable) {
|
|
3697
|
-
if (
|
|
4171
|
+
if (isStoppable(workflow)) workflow.stop();
|
|
3698
4172
|
return;
|
|
3699
4173
|
}
|
|
3700
|
-
if (task.
|
|
3701
|
-
const error = new WorkflowError("TRANSITION", `task '${task.id}' has an unresolved
|
|
4174
|
+
if (task.behavior !== void 0 && task.handler === void 0) {
|
|
4175
|
+
const error = new WorkflowError("TRANSITION", `task '${task.id}' has an unresolved behavior '${task.behavior}'`, {
|
|
3702
4176
|
task: task.id,
|
|
3703
|
-
|
|
4177
|
+
behavior: task.behavior
|
|
3704
4178
|
});
|
|
3705
4179
|
task.fail({
|
|
3706
4180
|
origin: "handler",
|
|
@@ -3712,21 +4186,21 @@ var WorkflowRunner = class WorkflowRunner {
|
|
|
3712
4186
|
if (await this.#gate(workflow.paused ? workflow.wait() : void 0, task, workflow, controller, runSignal, signal, attempts, owners, attempt, last, bail)) return;
|
|
3713
4187
|
if (await this.#gate(task.phase.paused ? task.phase.wait() : void 0, task, workflow, controller, runSignal, signal, attempts, owners, attempt, last, bail)) return;
|
|
3714
4188
|
if (await this.#gate(task.paused ? task.wait() : void 0, task, workflow, controller, runSignal, signal, attempts, owners, attempt, last, bail)) return;
|
|
3715
|
-
if (
|
|
4189
|
+
if (isSkipping(task, controller, runSignal) || isHalted(workflow, task.phase)) {
|
|
3716
4190
|
this.#settleCancelled(task, workflow, runSignal);
|
|
3717
4191
|
return;
|
|
3718
4192
|
}
|
|
3719
4193
|
if (task.status !== "running") return;
|
|
3720
|
-
const handle = new TaskController(signal, task.snapshot().metadata, task, attempt, () => workflow.results(), (input) =>
|
|
4194
|
+
const handle = new TaskController(signal, task.snapshot().metadata, task, attempt, () => workflow.results(), (input) => ownsAttempt(owners, task, attempt) && !signal.aborted ? task.report(input) : failure(new WorkflowError("TRANSITION", `task '${task.id}' attempt '${attempt}' no longer owns activity`, {
|
|
3721
4195
|
task: task.id,
|
|
3722
4196
|
attempt
|
|
3723
|
-
})), () =>
|
|
4197
|
+
})), () => ownsAttempt(owners, task, attempt) && !signal.aborted && task.pulse());
|
|
3724
4198
|
let outcome;
|
|
3725
4199
|
try {
|
|
3726
|
-
outcome = task.handler === void 0 ? [true, null] : await this.#raceHandler(Promise.resolve(task.handler(handle)), signal,
|
|
4200
|
+
outcome = task.handler === void 0 ? [true, null] : await this.#raceHandler(Promise.resolve(task.handler(handle)), signal, () => isSkipping(task, controller, runSignal));
|
|
3727
4201
|
} catch (error) {
|
|
3728
|
-
if (!
|
|
3729
|
-
if (task.status !== "running" ||
|
|
4202
|
+
if (!ownsAttempt(owners, task, attempt)) return;
|
|
4203
|
+
if (task.status !== "running" || isSkipping(task, controller, runSignal)) {
|
|
3730
4204
|
this.#settleCancelled(task, workflow, runSignal);
|
|
3731
4205
|
return;
|
|
3732
4206
|
}
|
|
@@ -3737,13 +4211,13 @@ var WorkflowRunner = class WorkflowRunner {
|
|
|
3737
4211
|
this.#failed(owners, task, attempt, error, last, bail);
|
|
3738
4212
|
return;
|
|
3739
4213
|
}
|
|
3740
|
-
if (!
|
|
4214
|
+
if (!ownsAttempt(owners, task, attempt)) return;
|
|
3741
4215
|
if (!outcome[0]) {
|
|
3742
4216
|
this.#settleAttempt(task, workflow, controller, runSignal, signal, attempts, owners, attempt, last, bail, outcome[2]);
|
|
3743
4217
|
return;
|
|
3744
4218
|
}
|
|
3745
4219
|
if (task.status !== "running") return;
|
|
3746
|
-
if (
|
|
4220
|
+
if (isSkipping(task, controller, runSignal)) {
|
|
3747
4221
|
this.#settleCancelled(task, workflow, runSignal);
|
|
3748
4222
|
return;
|
|
3749
4223
|
}
|
|
@@ -3751,32 +4225,32 @@ var WorkflowRunner = class WorkflowRunner {
|
|
|
3751
4225
|
this.#timedOut(owners, task, attempt, last, bail);
|
|
3752
4226
|
return;
|
|
3753
4227
|
}
|
|
3754
|
-
if (!
|
|
4228
|
+
if (!ownsAttempt(owners, task, attempt)) return;
|
|
3755
4229
|
try {
|
|
3756
4230
|
task.complete(outcome[1]);
|
|
3757
4231
|
} catch (error) {
|
|
3758
|
-
if (!
|
|
4232
|
+
if (!ownsAttempt(owners, task, attempt)) return;
|
|
3759
4233
|
if (task.status !== "running") throw error;
|
|
3760
4234
|
this.#failed(owners, task, attempt, error, last, bail);
|
|
3761
4235
|
}
|
|
3762
4236
|
} finally {
|
|
3763
4237
|
deadline?.clear();
|
|
3764
|
-
if (persistence !== void 0 &&
|
|
4238
|
+
if (persistence !== void 0 && ownsAttempt(owners, task, attempt) && isTerminalStatus(task.status) && !await persistence.checkpoint("settlement", task, attempt) && isStoppable(workflow)) workflow.stop();
|
|
3765
4239
|
this.#revoke(owners, task.id, attempt);
|
|
3766
4240
|
}
|
|
3767
4241
|
}
|
|
3768
4242
|
async #gate(wait, task, workflow, controller, runSignal, signal, attempts, owners, attempt, last, bail) {
|
|
3769
|
-
const genuine = wait === void 0 ? void 0 : await this.#raceWait(wait, signal,
|
|
4243
|
+
const genuine = wait === void 0 ? void 0 : await this.#raceWait(wait, signal, () => isSkipping(task, controller, runSignal), workflow, task.phase);
|
|
3770
4244
|
return this.#settleAttempt(task, workflow, controller, runSignal, signal, attempts, owners, attempt, last, bail, genuine);
|
|
3771
4245
|
}
|
|
3772
4246
|
#settleAttempt(task, workflow, controller, runSignal, signal, attempts, owners, attempt, last, bail, genuine) {
|
|
3773
|
-
if (attempts.get(task.id) !== attempt || !
|
|
4247
|
+
if (attempts.get(task.id) !== attempt || !ownsAttempt(owners, task, attempt)) return true;
|
|
3774
4248
|
if (signal.aborted) {
|
|
3775
|
-
if (genuine ??
|
|
4249
|
+
if (genuine ?? isSkipping(task, controller, runSignal)) this.#settleCancelled(task, workflow, runSignal);
|
|
3776
4250
|
else this.#timedOut(owners, task, attempt, last, bail);
|
|
3777
4251
|
return true;
|
|
3778
4252
|
}
|
|
3779
|
-
if (
|
|
4253
|
+
if (isSkipping(task, controller, runSignal) || isHalted(workflow, task.phase)) {
|
|
3780
4254
|
this.#settleCancelled(task, workflow, runSignal);
|
|
3781
4255
|
return true;
|
|
3782
4256
|
}
|
|
@@ -3805,7 +4279,7 @@ var WorkflowRunner = class WorkflowRunner {
|
|
|
3805
4279
|
]);
|
|
3806
4280
|
}
|
|
3807
4281
|
#timedOut(owners, task, attempt, last, bail) {
|
|
3808
|
-
if (!
|
|
4282
|
+
if (!ownsAttempt(owners, task, attempt)) return;
|
|
3809
4283
|
const error = /* @__PURE__ */ new Error(`task '${task.id}' timed out`);
|
|
3810
4284
|
if (last) task.fail({
|
|
3811
4285
|
origin: "timeout",
|
|
@@ -3814,7 +4288,7 @@ var WorkflowRunner = class WorkflowRunner {
|
|
|
3814
4288
|
if (!last || bail) throw error;
|
|
3815
4289
|
}
|
|
3816
4290
|
#failed(owners, task, attempt, error, last, bail) {
|
|
3817
|
-
if (!
|
|
4291
|
+
if (!ownsAttempt(owners, task, attempt)) return;
|
|
3818
4292
|
if (!last) throw error;
|
|
3819
4293
|
task.fail({
|
|
3820
4294
|
origin: "handler",
|
|
@@ -3822,9 +4296,6 @@ var WorkflowRunner = class WorkflowRunner {
|
|
|
3822
4296
|
});
|
|
3823
4297
|
if (bail) throw error;
|
|
3824
4298
|
}
|
|
3825
|
-
#owns(owners, task, attempt) {
|
|
3826
|
-
return owners.get(task.id) === attempt && task.attempts === attempt;
|
|
3827
|
-
}
|
|
3828
4299
|
#revoke(owners, id, attempt) {
|
|
3829
4300
|
if (owners.get(id) === attempt) owners.delete(id);
|
|
3830
4301
|
}
|
|
@@ -3839,7 +4310,7 @@ var WorkflowRunner = class WorkflowRunner {
|
|
|
3839
4310
|
phase?.emitter.on("skip", onTerminal);
|
|
3840
4311
|
phase?.emitter.on("stop", onTerminal);
|
|
3841
4312
|
try {
|
|
3842
|
-
if (workflow !== void 0 &&
|
|
4313
|
+
if (workflow !== void 0 && isHalted(workflow, phase)) deferred.resolve(void 0);
|
|
3843
4314
|
const outcome = await Promise.race([wait, deferred.promise]);
|
|
3844
4315
|
return typeof outcome === "boolean" ? outcome : void 0;
|
|
3845
4316
|
} finally {
|
|
@@ -3870,7 +4341,7 @@ var WorkflowRunner = class WorkflowRunner {
|
|
|
3870
4341
|
return signals.length === 1 ? workflow.signal : AbortSignal.any(signals);
|
|
3871
4342
|
}
|
|
3872
4343
|
#haltFrom(phases, index, workflow, runSignal) {
|
|
3873
|
-
if (
|
|
4344
|
+
if (runSignal.aborted && isStoppable(workflow)) workflow.stop();
|
|
3874
4345
|
this.#skipFrom(phases, index);
|
|
3875
4346
|
}
|
|
3876
4347
|
#skipFrom(phases, index) {
|
|
@@ -3881,37 +4352,17 @@ var WorkflowRunner = class WorkflowRunner {
|
|
|
3881
4352
|
}
|
|
3882
4353
|
}
|
|
3883
4354
|
#settleCancelled(task, workflow, runSignal) {
|
|
3884
|
-
if (
|
|
4355
|
+
if (runSignal.aborted && isStoppable(workflow)) workflow.stop();
|
|
3885
4356
|
this.#skip(task);
|
|
3886
4357
|
}
|
|
3887
4358
|
#skip(task) {
|
|
3888
4359
|
if (task.status === "pending" || task.status === "running") task.skip();
|
|
3889
4360
|
}
|
|
3890
|
-
#skipping(task, controller, runSignal) {
|
|
3891
|
-
return task.signal.aborted || controller.aborted || runSignal.aborted;
|
|
3892
|
-
}
|
|
3893
|
-
#cancelled(runSignal) {
|
|
3894
|
-
return runSignal.aborted;
|
|
3895
|
-
}
|
|
3896
|
-
#halted(workflow, phase) {
|
|
3897
|
-
const status = workflow.status;
|
|
3898
|
-
return status === "failed" || status === "skipped" || status === "stopped" || phase?.status === "skipped" || phase?.status === "stopped";
|
|
3899
|
-
}
|
|
3900
|
-
#stoppable(workflow) {
|
|
3901
|
-
const status = workflow.status;
|
|
3902
|
-
return status !== "failed" && status !== "stopped";
|
|
3903
|
-
}
|
|
3904
|
-
#completable(workflow) {
|
|
3905
|
-
return workflow.status === "pending";
|
|
3906
|
-
}
|
|
3907
|
-
#isWorkflow(target) {
|
|
3908
|
-
return "destroyed" in target && "snapshot" in target && typeof target.snapshot === "function";
|
|
3909
|
-
}
|
|
3910
4361
|
};
|
|
3911
4362
|
//#endregion
|
|
3912
4363
|
//#region src/core/factories.ts
|
|
3913
4364
|
/**
|
|
3914
|
-
*
|
|
4365
|
+
* Compiles the workflow definition contract — the JSON Schema, guard, parser, and
|
|
3915
4366
|
* seeded generator for a {@link WorkflowDefinition}, all derived from one shape and
|
|
3916
4367
|
* kept in lockstep.
|
|
3917
4368
|
*
|
|
@@ -3941,23 +4392,23 @@ function createWorkflowContract() {
|
|
|
3941
4392
|
return (0, _orkestrel_contract.createContract)(workflowShape);
|
|
3942
4393
|
}
|
|
3943
4394
|
/**
|
|
3944
|
-
*
|
|
4395
|
+
* Builds the live W-b entity tree from a {@link WorkflowDefinition} — the whole
|
|
3945
4396
|
* {@link WorkflowInterface} → {@link import('./types.js').PhaseInterface} →
|
|
3946
4397
|
* {@link import('./types.js').TaskInterface} tree, each level wired with its lineage
|
|
3947
|
-
* context, its emitter, and the cascade
|
|
4398
|
+
* context, its emitter, and the cascade, and every node born `pending`.
|
|
3948
4399
|
*
|
|
3949
4400
|
* @remarks
|
|
3950
|
-
* The definition is the
|
|
4401
|
+
* The definition is the declarative blueprint; this seeds an initial all-`pending`
|
|
3951
4402
|
* {@link WorkflowSnapshot} from it ({@link definitionToSnapshot}) and constructs the live
|
|
3952
4403
|
* tree over that one path. The `bail` failure policy resolves to `options.bail`, else the
|
|
3953
4404
|
* definition's `bail`, else the graceful {@link import('./constants.js').DEFAULT_BAIL}; it
|
|
3954
4405
|
* feeds {@link import('./helpers.js').deriveWorkflowStatus}. Per-phase / per-task initial
|
|
3955
4406
|
* listeners + metadata travel through `options.phases[id].on` /
|
|
3956
|
-
* `options.phases[id].tasks[id]` (the
|
|
3957
|
-
* state machine
|
|
4407
|
+
* `options.phases[id].tasks[id]` (the nested-by-id bag). The W-b tree is the
|
|
4408
|
+
* state machine only — it does not execute tasks (W-c drives the transitions).
|
|
3958
4409
|
*
|
|
3959
|
-
* `options.functions` is the {@link import('./types.js').
|
|
3960
|
-
* task's `
|
|
4410
|
+
* `options.functions` is the {@link import('./types.js').WorkflowRegistry} registry each live
|
|
4411
|
+
* task's `behavior` name resolves against once at construction into its runtime
|
|
3961
4412
|
* {@link import('./types.js').TaskInterface.handler}. An omitted name is the deliberate no-op;
|
|
3962
4413
|
* an unresolved present name remains inspectable but is rejected if execution is attempted.
|
|
3963
4414
|
*
|
|
@@ -3975,26 +4426,62 @@ function createWorkflowContract() {
|
|
|
3975
4426
|
* ```
|
|
3976
4427
|
*/
|
|
3977
4428
|
function createWorkflow(definition, options) {
|
|
3978
|
-
|
|
3979
|
-
|
|
4429
|
+
return createWorkflowTree(definition, captureWorkflowOptions(options));
|
|
4430
|
+
}
|
|
4431
|
+
/**
|
|
4432
|
+
* Builds the live entity tree one definition and one owned options bag describe — the shared
|
|
4433
|
+
* construction path behind every definition-driven mint.
|
|
4434
|
+
*
|
|
4435
|
+
* @remarks
|
|
4436
|
+
* Seeds an initial all-`pending` {@link WorkflowSnapshot} from the definition and constructs the
|
|
4437
|
+
* live {@link WorkflowInterface} over it. `bail` is the caller's own override, forwarded to
|
|
4438
|
+
* {@link definitionToSnapshot} so it reaches both tiers: the workflow snapshot and the inheritance
|
|
4439
|
+
* default of every phase that declares no `bail` of its own, while a phase declaring one still
|
|
4440
|
+
* wins. Omitted, the definition's own `bail` governs, defaulting to the graceful
|
|
4441
|
+
* {@link import('./constants.js').DEFAULT_BAIL}.
|
|
4442
|
+
*
|
|
4443
|
+
* `captured` is forwarded to the entity unchanged — its own `bail` is deliberately not replaced
|
|
4444
|
+
* with the resolved policy, because the snapshot already carries the resolved value at both tiers
|
|
4445
|
+
* and an injected one would make `Workflow` read it as an explicit uniform override and clobber
|
|
4446
|
+
* the per-phase overrides. Each task's `behavior` / `retries` / `timeout` travel onto the snapshot
|
|
4447
|
+
* too, so `captured.functions` resolves every handler identically whether the tree is built fresh
|
|
4448
|
+
* or restored. Pass a bag {@link captureWorkflowOptions} already owns: this constructs over it
|
|
4449
|
+
* without re-capturing.
|
|
4450
|
+
*
|
|
4451
|
+
* @param definition - The workflow definition to bring to life
|
|
4452
|
+
* @param captured - The already-owned {@link WorkflowOptions} bag the entity is constructed with,
|
|
4453
|
+
* whose `bail` is the caller's failure-policy override, or `undefined` to take the definition's
|
|
4454
|
+
* @returns The live {@link WorkflowInterface} root
|
|
4455
|
+
*
|
|
4456
|
+
* @example
|
|
4457
|
+
* ```ts
|
|
4458
|
+
* import { captureWorkflowOptions, createWorkflowTree } from '@orkestrel/workflow'
|
|
4459
|
+
*
|
|
4460
|
+
* const captured = captureWorkflowOptions({ bail: true })
|
|
4461
|
+
* const workflow = createWorkflowTree(definition, captured)
|
|
4462
|
+
* workflow.bail // true — the override reached the workflow and every inheriting phase
|
|
4463
|
+
* ```
|
|
4464
|
+
*/
|
|
4465
|
+
function createWorkflowTree(definition, captured) {
|
|
4466
|
+
return new Workflow(definitionToSnapshot(definition, captured.bail), captured);
|
|
3980
4467
|
}
|
|
3981
4468
|
/**
|
|
3982
|
-
*
|
|
4469
|
+
* Builds an equivalent live W-b entity tree from a {@link WorkflowSnapshot} — the
|
|
3983
4470
|
* inverse of {@link WorkflowInterface.snapshot}, restoring structure + each node's status
|
|
3984
4471
|
* + recorded results + positional order + the persisted `#override`.
|
|
3985
4472
|
*
|
|
3986
4473
|
* @remarks
|
|
3987
4474
|
* Round-trip fidelity is paramount: a `snapshot()` → `createRestoredWorkflow()` reproduces the
|
|
3988
|
-
* same status at every node (each `#override` restored
|
|
4475
|
+
* same status at every node (each `#override` restored directly from the snapshot's own
|
|
3989
4476
|
* `override` field, not guessed from a status divergence), the same recorded
|
|
3990
4477
|
* {@link import('./types.js').TaskResult}s, and the same positional order (an interior
|
|
3991
|
-
* `skip` / `remove` survives). The snapshot is
|
|
3992
|
-
* 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
|
|
3993
4480
|
* default; the snapshot's `bail` is the source of truth, while an explicit `options.bail`
|
|
3994
4481
|
* still wins when supplied (to deliberately re-run under a different policy). A structurally
|
|
3995
4482
|
* invalid snapshot (a status — or override — outside the lifecycle vocabulary, or a
|
|
3996
4483
|
* non-boolean `bail`) throws a `RESTORE` {@link WorkflowError}.
|
|
3997
|
-
* Runtime handlers are optional: without a matching `functions` entry, a persisted `
|
|
4484
|
+
* Runtime handlers are optional: without a matching `functions` entry, a persisted `behavior`
|
|
3998
4485
|
* remains visible with an undefined `handler` so the exact state is inspectable. The runner
|
|
3999
4486
|
* rejects that unresolved tree if execution is attempted.
|
|
4000
4487
|
*
|
|
@@ -4015,10 +4502,11 @@ function createRestoredWorkflow(snapshot, options) {
|
|
|
4015
4502
|
return new Workflow(cloneWorkflowSnapshot(snapshot), captured);
|
|
4016
4503
|
}
|
|
4017
4504
|
/**
|
|
4018
|
-
*
|
|
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.
|
|
4019
4507
|
*
|
|
4020
4508
|
* @remarks
|
|
4021
|
-
* Each phase captures every unique initial `
|
|
4509
|
+
* Each phase captures every unique initial `behavior` binding once before constructing tasks. Recovery
|
|
4022
4510
|
* validates those live tasks' captured callable handlers without rereading the registry, while the
|
|
4023
4511
|
* retained registry identity remains available to resolve future live additions at their mint time.
|
|
4024
4512
|
*
|
|
@@ -4039,22 +4527,22 @@ function createRecoveredWorkflow(snapshot, options) {
|
|
|
4039
4527
|
const owned = cloneWorkflowSnapshot(snapshot);
|
|
4040
4528
|
if (owned.override !== void 0 || owned.phases.some((phase) => phase.override !== void 0)) throw new WorkflowError("RESTORE", `workflow '${owned.id}' has a terminal override`, { workflow: owned.id });
|
|
4041
4529
|
const workflow = new Workflow(cloneWorkflowSnapshot(recoverWorkflowSnapshot(owned)), captured);
|
|
4042
|
-
if (!hasWorkflowHandlers(workflow)) throw new WorkflowError("RESTORE", `workflow '${owned.id}' has an unresolved
|
|
4530
|
+
if (!hasWorkflowHandlers(workflow)) throw new WorkflowError("RESTORE", `workflow '${owned.id}' has an unresolved behavior`, { workflow: owned.id });
|
|
4043
4531
|
return workflow;
|
|
4044
4532
|
}
|
|
4045
4533
|
/**
|
|
4046
|
-
*
|
|
4047
|
-
* {@link MemoryWorkflowStore} persisting {@link WorkflowSnapshot}s by workflow id, the
|
|
4048
|
-
* backend behind the W-d persistence seam.
|
|
4534
|
+
* Creates the in-memory durable {@link WorkflowStoreInterface} — a process-lifetime
|
|
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`.
|
|
4049
4538
|
*
|
|
4050
4539
|
* @remarks
|
|
4051
4540
|
* The snapshot analogue of the server package's `createMemorySessionStore`
|
|
4052
|
-
* (and the `createMemoryQueueStore` family)
|
|
4053
|
-
*
|
|
4054
|
-
* 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
|
|
4055
4543
|
* {@link createDatabaseWorkflowStore} (the snapshot as one opaque JSON column over a `databases`
|
|
4056
|
-
* table) — for a
|
|
4057
|
-
* 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
|
|
4058
4546
|
* concern: read a snapshot back and rebuild the live tree with {@link createRestoredWorkflow}.
|
|
4059
4547
|
*
|
|
4060
4548
|
* @returns A memory-backed {@link WorkflowStoreInterface}
|
|
@@ -4074,23 +4562,24 @@ function createMemoryWorkflowStore() {
|
|
|
4074
4562
|
return new MemoryWorkflowStore();
|
|
4075
4563
|
}
|
|
4076
4564
|
/**
|
|
4077
|
-
*
|
|
4565
|
+
* Creates a {@link DatabaseWorkflowStore} over any {@link DriverInterface} — the durable,
|
|
4078
4566
|
* driver-pluggable backing for the W-d persistence seam, the opt-in twin of
|
|
4079
|
-
* {@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.
|
|
4080
4569
|
*
|
|
4081
4570
|
* @remarks
|
|
4082
|
-
* Builds a one-table database (`snapshots`, keyed by `id`) over the supplied driver
|
|
4083
|
-
*
|
|
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
|
|
4084
4573
|
* `rawShape` (a JSON blob), exactly as `createDatabaseQueueStore` stores its `input`. The
|
|
4085
|
-
* snapshot is already a
|
|
4086
|
-
*
|
|
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
|
|
4087
4576
|
* `Infer` the deeply-nested snapshot shape (workflow → phases → tasks → results) and trip TS2589;
|
|
4088
|
-
* the opaque column sidesteps it (the column reads back as `unknown`, narrowed on `get` by
|
|
4089
|
-
* {@link
|
|
4090
|
-
* {@link
|
|
4091
|
-
* `
|
|
4577
|
+
* the opaque column sidesteps it (the column reads back as `unknown`, owned and narrowed on `get` by
|
|
4578
|
+
* {@link cloneWorkflowSnapshot}, whose semantic pass is
|
|
4579
|
+
* {@link import('./validators.js').isOwnedWorkflowSnapshot}). Pass a server `createJSONDriver` /
|
|
4580
|
+
* `createSQLiteDriver` (or a browser IndexedDB driver) for a persistent one —
|
|
4092
4581
|
* the durability is the driver's job, the store engine is shared. It swaps in behind
|
|
4093
|
-
* {@link WorkflowStoreInterface}
|
|
4582
|
+
* {@link WorkflowStoreInterface} without touching the runner or the entity tree.
|
|
4094
4583
|
*
|
|
4095
4584
|
* @param driver - The storage backend the snapshots persist to (defaults to {@link createMemoryDriver})
|
|
4096
4585
|
* @returns A {@link WorkflowStoreInterface} over the driver
|
|
@@ -4118,65 +4607,91 @@ function createDatabaseWorkflowStore(driver = (0, _orkestrel_database.createMemo
|
|
|
4118
4607
|
}).table("snapshots"));
|
|
4119
4608
|
}
|
|
4120
4609
|
/**
|
|
4121
|
-
*
|
|
4122
|
-
* workflow tree by
|
|
4123
|
-
*
|
|
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.
|
|
4124
4615
|
*
|
|
4125
4616
|
* @remarks
|
|
4126
|
-
*
|
|
4127
|
-
*
|
|
4128
|
-
* resolved its own {@link import('./types.js').WorkflowFunction} into
|
|
4129
|
-
* {@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
|
|
4130
4619
|
* {@link WorkflowOptions.functions} registry supplied to `execute` / {@link createWorkflow}.
|
|
4131
4620
|
* Per-phase bounded concurrency is one {@link createRunner} per phase; `bail` maps onto that
|
|
4132
4621
|
* Runner's fail-fast (`true` — the first failure aborts the in-flight siblings + skips the
|
|
4133
4622
|
* rest) vs settle-all (`false` — failures are recorded, the run finishes); the run-level abort
|
|
4134
4623
|
* / timeout / budget ({@link import('./types.js').WorkflowRunOptions}) fold through
|
|
4135
4624
|
* `AbortSignal.any` (the agent runtime's pattern); pacing is the shipped scheduler.
|
|
4136
|
-
* `execute(definition, options?)`
|
|
4625
|
+
* `execute(definition, options?)` builds the live tree from the definition itself (through
|
|
4137
4626
|
* {@link createWorkflow} — one source of truth, returned in `WorkflowResult.workflow`), drives
|
|
4138
4627
|
* the live entity (`start` → `complete` / `fail`), and resolves a
|
|
4139
4628
|
* {@link import('./types.js').WorkflowResult}.
|
|
4140
4629
|
*
|
|
4141
4630
|
* External integrations remain application-owned: a caller wires an ordinary
|
|
4142
4631
|
* {@link import('./types.js').WorkflowFunction} into its own {@link WorkflowOptions.functions}
|
|
4143
|
-
* registry. Only a task that omits `
|
|
4632
|
+
* registry. Only a task that omits `behavior` auto-completes; unresolved named work is rejected
|
|
4144
4633
|
* before dispatch.
|
|
4145
4634
|
*
|
|
4146
4635
|
* @param options - An optional pacing `scheduler` (default the shipped cross-environment one).
|
|
4147
4636
|
* See {@link WorkflowRunnerOptions}.
|
|
4148
4637
|
* @returns A working {@link WorkflowRunnerInterface}
|
|
4149
4638
|
*
|
|
4150
|
-
* @example
|
|
4639
|
+
* @example Author a definition and run it
|
|
4151
4640
|
* ```ts
|
|
4152
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
|
|
4153
4665
|
*
|
|
4154
|
-
* const runner = createWorkflowRunner()
|
|
4155
|
-
* const definition = { id: 'w', name: 'W', phases: [{ id: 'p', name: 'P', tasks: [
|
|
4156
|
-
* { id: 't', name: 'T', run: 'compile' },
|
|
4157
|
-
* ] }] }
|
|
4158
4666
|
* const result = await runner.execute(definition, {
|
|
4159
|
-
* functions: {
|
|
4667
|
+
* functions: {
|
|
4668
|
+
* compile: async (controller) => `built ${controller.task.id}`,
|
|
4669
|
+
* lint: async () => 'clean',
|
|
4670
|
+
* publish: async () => 'published',
|
|
4671
|
+
* },
|
|
4160
4672
|
* })
|
|
4161
4673
|
* result.status // 'completed'
|
|
4162
|
-
* result.workflow.phase('
|
|
4674
|
+
* result.workflow.phase('build')?.task('compile')?.status // 'completed'
|
|
4675
|
+
* result.results // every settled task's TaskResult, in positional order
|
|
4163
4676
|
* ```
|
|
4164
4677
|
*/
|
|
4165
4678
|
function createWorkflowRunner(options) {
|
|
4166
4679
|
return new WorkflowRunner(options?.scheduler ?? createScheduler());
|
|
4167
4680
|
}
|
|
4168
4681
|
/**
|
|
4169
|
-
*
|
|
4682
|
+
* Creates a {@link WorkflowManagerInterface} — the store-backed registry of
|
|
4170
4683
|
* {@link WorkflowInterface}s, the additive manager tier mirroring the `@orkestrel/agent`
|
|
4171
|
-
* 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.
|
|
4172
4687
|
*
|
|
4173
4688
|
* @remarks
|
|
4174
|
-
* `options.functions` flows into every workflow the manager mints (`add`,
|
|
4175
|
-
* {@link createWorkflow}) or hydrates (`open`'s registry-miss path,
|
|
4176
|
-
* {@link createRestoredWorkflow}), so a hydrated workflow is
|
|
4177
|
-
* mirror. `options.store` is the
|
|
4689
|
+
* `options.functions` flows into every workflow the manager mints (`add`, through
|
|
4690
|
+
* {@link createWorkflow}) or hydrates (`open`'s registry-miss path, through
|
|
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
|
|
4178
4693
|
* manager is registry-only (`open` resolves only what is registered, `save` is a no-op). This
|
|
4179
|
-
* is
|
|
4694
|
+
* is purely additive: direct {@link WorkflowStoreInterface} use and
|
|
4180
4695
|
* {@link createRestoredWorkflow} remain valid — the manager is one more caller-driven persistence
|
|
4181
4696
|
* seam, not a replacement.
|
|
4182
4697
|
*
|
|
@@ -4191,7 +4706,7 @@ function createWorkflowRunner(options) {
|
|
|
4191
4706
|
* store: createMemoryWorkflowStore(),
|
|
4192
4707
|
* functions: { compile: async (controller) => `built ${controller.task.id}` },
|
|
4193
4708
|
* })
|
|
4194
|
-
* const workflow = manager.add(definition) // minted, registered,
|
|
4709
|
+
* const workflow = manager.add(definition) // minted, registered, runnable
|
|
4195
4710
|
* await manager.save(workflow.id) // persisted to the store
|
|
4196
4711
|
* const reopened = await manager.open(workflow.id) // already registered — no store hit
|
|
4197
4712
|
* ```
|
|
@@ -4200,12 +4715,12 @@ function createWorkflowManager(options) {
|
|
|
4200
4715
|
return new WorkflowManager(options);
|
|
4201
4716
|
}
|
|
4202
4717
|
/**
|
|
4203
|
-
*
|
|
4718
|
+
* Creates the safe cross-environment cooperative-yield default — a
|
|
4204
4719
|
* {@link SchedulerInterface} built on `setTimeout` / `clearTimeout` alone, so it
|
|
4205
4720
|
* runs unchanged in both the browser and Node.
|
|
4206
4721
|
*
|
|
4207
4722
|
* @remarks
|
|
4208
|
-
* `yield()` gives the host a turn
|
|
4723
|
+
* `yield()` gives the host a turn through a zero-delay macrotask (so pending I/O,
|
|
4209
4724
|
* timers, and rendering actually run — a microtask would not); `delay(ms)` resumes
|
|
4210
4725
|
* after at least `ms`. Pass `options.signal` to make a pending yield/delay reject
|
|
4211
4726
|
* with the signal's exact `reason`; the shared owned-signal lifecycle clears the timer
|
|
@@ -4246,25 +4761,26 @@ function createScheduler() {
|
|
|
4246
4761
|
return new Scheduler();
|
|
4247
4762
|
}
|
|
4248
4763
|
/**
|
|
4249
|
-
*
|
|
4250
|
-
* `spawn` — through a bounded-concurrency queue, collecting their results in order
|
|
4764
|
+
* Creates a thin generic orchestrator that drives declared units — and any they
|
|
4765
|
+
* `spawn` — through a bounded-concurrency queue, collecting their results in order and
|
|
4766
|
+
* failing the run fast on the first genuine unit failure.
|
|
4251
4767
|
*
|
|
4252
4768
|
* @remarks
|
|
4253
4769
|
* The Runner composes the workers `Queue` for backpressure, FIFO ordering, bounded
|
|
4254
4770
|
* concurrency, retries, and the per-attempt timeout — it adds only orchestration, not
|
|
4255
|
-
* 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
|
|
4256
4772
|
* throws) and resolves the units' results in order: the declared inputs first, then
|
|
4257
4773
|
* any `spawn`ed siblings in spawn order. Each unit's handler gets a `Controller` — its
|
|
4258
4774
|
* `id` / `input`, a `signal` that fires on the unit's `abort`, a runner-level `abort`,
|
|
4259
4775
|
* or the attempt's timeout, a promise-parked `wait()`, and `spawn(input)` to fan out
|
|
4260
4776
|
* sibling units. The run is **fail-fast**: the first unit failure (after retries)
|
|
4261
|
-
* aborts every other unit and rejects `execute` with that error. **Observable
|
|
4777
|
+
* aborts every other unit and rejects `execute` with that error. **Observable:** a
|
|
4262
4778
|
* typed `emitter` surfaces `start` / `unit` / `spawn` / `settle` / `fail` / `finish` / `abort`.
|
|
4263
4779
|
*
|
|
4264
|
-
* Because `spawn` is fire-and-track (the runner awaits the whole spawn closure
|
|
4265
|
-
* outstanding-unit count, not a one-time snapshot), a handler need
|
|
4266
|
-
* for them to run — and on a bounded runner
|
|
4267
|
-
*
|
|
4780
|
+
* Because `spawn` is fire-and-track (the runner awaits the whole spawn closure through an
|
|
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
|
|
4783
|
+
* handler awaiting its own spawn can deadlock); fan out and return instead.
|
|
4268
4784
|
*
|
|
4269
4785
|
* @typeParam TInput - The work input each unit carries
|
|
4270
4786
|
* @typeParam TResult - The value a unit's handler resolves
|
|
@@ -4293,24 +4809,22 @@ function createRunner(options) {
|
|
|
4293
4809
|
return new Runner(options);
|
|
4294
4810
|
}
|
|
4295
4811
|
//#endregion
|
|
4296
|
-
exports.
|
|
4812
|
+
exports.Collection = Collection;
|
|
4297
4813
|
exports.DEFAULT_BAIL = DEFAULT_BAIL;
|
|
4298
4814
|
exports.DEFAULT_PHASE_CONCURRENCY = DEFAULT_PHASE_CONCURRENCY;
|
|
4299
4815
|
exports.DatabaseWorkflowStore = DatabaseWorkflowStore;
|
|
4816
|
+
exports.LIFECYCLE_STATUSES = LIFECYCLE_STATUSES;
|
|
4300
4817
|
exports.MAX_TIMER_MS = MAX_TIMER_MS;
|
|
4301
4818
|
exports.MemoryWorkflowStore = MemoryWorkflowStore;
|
|
4302
|
-
exports.
|
|
4303
|
-
exports.
|
|
4819
|
+
exports.PERSISTED_NODE_EVENTS = PERSISTED_NODE_EVENTS;
|
|
4820
|
+
exports.PERSISTED_TASK_EVENTS = PERSISTED_TASK_EVENTS;
|
|
4304
4821
|
exports.PhaseManager = PhaseManager;
|
|
4822
|
+
exports.RunHolder = RunHolder;
|
|
4305
4823
|
exports.Runner = Runner;
|
|
4306
4824
|
exports.Scheduler = Scheduler;
|
|
4307
|
-
exports.TASK_STATUSES = TASK_STATUSES;
|
|
4308
4825
|
exports.TASK_TRANSITIONS = TASK_TRANSITIONS;
|
|
4309
|
-
exports.
|
|
4310
|
-
exports.Task = Task;
|
|
4311
|
-
exports.TaskController = TaskController;
|
|
4826
|
+
exports.TERMINAL_STATUSES = TERMINAL_STATUSES;
|
|
4312
4827
|
exports.TaskManager = TaskManager;
|
|
4313
|
-
exports.WORKFLOW_STATUSES = WORKFLOW_STATUSES;
|
|
4314
4828
|
exports.Workflow = Workflow;
|
|
4315
4829
|
exports.WorkflowError = WorkflowError;
|
|
4316
4830
|
exports.WorkflowManager = WorkflowManager;
|
|
@@ -4322,10 +4836,10 @@ exports.buildWorkflowContext = buildWorkflowContext;
|
|
|
4322
4836
|
exports.canTransitionTask = canTransitionTask;
|
|
4323
4837
|
exports.captureWorkflowOptions = captureWorkflowOptions;
|
|
4324
4838
|
exports.cloneTaskActivity = cloneTaskActivity;
|
|
4839
|
+
exports.cloneTaskClaims = cloneTaskClaims;
|
|
4325
4840
|
exports.cloneWorkflowSnapshot = cloneWorkflowSnapshot;
|
|
4326
4841
|
exports.collectResults = collectResults;
|
|
4327
4842
|
exports.createDatabaseWorkflowStore = createDatabaseWorkflowStore;
|
|
4328
|
-
exports.createDeferred = createDeferred;
|
|
4329
4843
|
exports.createMemoryWorkflowStore = createMemoryWorkflowStore;
|
|
4330
4844
|
exports.createRecoveredWorkflow = createRecoveredWorkflow;
|
|
4331
4845
|
exports.createRestoredWorkflow = createRestoredWorkflow;
|
|
@@ -4335,7 +4849,9 @@ exports.createWorkflow = createWorkflow;
|
|
|
4335
4849
|
exports.createWorkflowContract = createWorkflowContract;
|
|
4336
4850
|
exports.createWorkflowManager = createWorkflowManager;
|
|
4337
4851
|
exports.createWorkflowRunner = createWorkflowRunner;
|
|
4852
|
+
exports.createWorkflowTree = createWorkflowTree;
|
|
4338
4853
|
exports.definitionToSnapshot = definitionToSnapshot;
|
|
4854
|
+
exports.delayHost = delayHost;
|
|
4339
4855
|
exports.deriveBoundary = deriveBoundary;
|
|
4340
4856
|
exports.derivePhaseStatus = derivePhaseStatus;
|
|
4341
4857
|
exports.deriveWorkflowStatus = deriveWorkflowStatus;
|
|
@@ -4344,29 +4860,36 @@ exports.failure = failure;
|
|
|
4344
4860
|
exports.findFailure = findFailure;
|
|
4345
4861
|
exports.hasWorkflowHandlers = hasWorkflowHandlers;
|
|
4346
4862
|
exports.insertEntry = insertEntry;
|
|
4863
|
+
exports.isCompletable = isCompletable;
|
|
4864
|
+
exports.isHalted = isHalted;
|
|
4347
4865
|
exports.isLifecycleStatus = isLifecycleStatus;
|
|
4348
4866
|
exports.isOwnedWorkflowSnapshot = isOwnedWorkflowSnapshot;
|
|
4867
|
+
exports.isSkipping = isSkipping;
|
|
4868
|
+
exports.isStoppable = isStoppable;
|
|
4349
4869
|
exports.isTaskActivity = isTaskActivity;
|
|
4350
4870
|
exports.isTaskActivityInput = isTaskActivityInput;
|
|
4871
|
+
exports.isTaskClaimList = isTaskClaimList;
|
|
4351
4872
|
exports.isTaskFailure = isTaskFailure;
|
|
4352
4873
|
exports.isTaskResult = isTaskResult;
|
|
4353
4874
|
exports.isTerminalStatus = isTerminalStatus;
|
|
4354
4875
|
exports.isWorkflowError = isWorkflowError;
|
|
4876
|
+
exports.isWorkflowInterface = isWorkflowInterface;
|
|
4355
4877
|
exports.isWorkflowSnapshot = isWorkflowSnapshot;
|
|
4356
4878
|
exports.matchesDescription = matchesDescription;
|
|
4357
4879
|
exports.moveEntry = moveEntry;
|
|
4880
|
+
exports.ownsAttempt = ownsAttempt;
|
|
4358
4881
|
exports.parkSignal = parkSignal;
|
|
4359
4882
|
exports.phaseDefinitionToSnapshot = phaseDefinitionToSnapshot;
|
|
4360
4883
|
exports.phaseShape = phaseShape;
|
|
4361
4884
|
exports.phaseUpdateShape = phaseUpdateShape;
|
|
4362
4885
|
exports.recoverWorkflowSnapshot = recoverWorkflowSnapshot;
|
|
4363
4886
|
exports.resolveTaskSilence = resolveTaskSilence;
|
|
4887
|
+
exports.scanSnapshotContext = scanSnapshotContext;
|
|
4364
4888
|
exports.scheduleHost = scheduleHost;
|
|
4365
4889
|
exports.success = success;
|
|
4366
4890
|
exports.taskDefinitionToSnapshot = taskDefinitionToSnapshot;
|
|
4367
4891
|
exports.taskShape = taskShape;
|
|
4368
4892
|
exports.taskUpdateShape = taskUpdateShape;
|
|
4369
4893
|
exports.workflowShape = workflowShape;
|
|
4370
|
-
exports.workflowSnapshotContext = workflowSnapshotContext;
|
|
4371
4894
|
|
|
4372
4895
|
//# sourceMappingURL=index.cjs.map
|