@orkestrel/workflow 0.0.16 → 0.0.17
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 +3 -3
- package/dist/src/browser/index.d.ts +41 -44
- package/dist/src/browser/index.js +46 -67
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/core/index.cjs +1160 -674
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +1464 -1030
- package/dist/src/core/index.d.ts +1464 -1030
- package/dist/src/core/index.js +1145 -666
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +12 -19
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +11 -12
- package/dist/src/server/index.d.ts +11 -12
- package/dist/src/server/index.js +13 -20
- package/dist/src/server/index.js.map +1 -1
- package/package.json +16 -16
package/dist/src/core/index.js
CHANGED
|
@@ -1,38 +1,67 @@
|
|
|
1
1
|
import { createAbort, isAbortSignal, linkSignal } from "@orkestrel/abort";
|
|
2
|
-
import { arrayShape, attempt, cloneJSONRecord, cloneJSONValue, compileGuard, createContract, integerShape, isArray, isBoolean, isContractError, isFiniteNumber, isFunction, isInteger, isJSONValue, isNonEmptyString, isRecord, literalShape, objectShape, optionalShape, rawShape, stringShape } from "@orkestrel/contract";
|
|
2
|
+
import { arrayShape, attempt, cloneJSONRecord, cloneJSONValue, compileGuard, createContract, integerShape, isArray, isBoolean, isContractError, isFiniteNumber, isFunction, isInteger, isJSONValue, isNonEmptyString, isObject, isRecord, literalShape, objectShape, optionalShape, rawShape, stringShape } from "@orkestrel/contract";
|
|
3
3
|
import { createDatabase, createMemoryDriver } from "@orkestrel/database";
|
|
4
4
|
import { Emitter } from "@orkestrel/emitter";
|
|
5
5
|
import { createTimeout } from "@orkestrel/timeout";
|
|
6
6
|
import { createQueue } from "@orkestrel/queue";
|
|
7
|
+
//#region src/core/errors.ts
|
|
8
|
+
/**
|
|
9
|
+
* Represents an error raised by the workflow runtime.
|
|
10
|
+
*
|
|
11
|
+
* @remarks
|
|
12
|
+
* Carries a {@link WorkflowErrorCode} and an optional `context` bag naming the
|
|
13
|
+
* offending node id / status / parameter. Raised for an illegal lifecycle transition
|
|
14
|
+
* (`TRANSITION`), a structurally invalid {@link import('./types.js').WorkflowSnapshot}
|
|
15
|
+
* boundary (`RESTORE`), a refused structural/activity edit (`MUTATION`), a host
|
|
16
|
+
* schedule refused before arming because the caller's `signal` is not a native
|
|
17
|
+
* `AbortSignal` (`SCHEDULE`, delivered as a rejected promise), or a broken internal
|
|
18
|
+
* invariant (`INVARIANT`).
|
|
19
|
+
*/
|
|
20
|
+
var WorkflowError = class extends Error {
|
|
21
|
+
code;
|
|
22
|
+
context;
|
|
23
|
+
constructor(code, message, context) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = "WorkflowError";
|
|
26
|
+
this.code = code;
|
|
27
|
+
if (context !== void 0) this.context = context;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Narrows an unknown caught value to a {@link WorkflowError}.
|
|
32
|
+
*
|
|
33
|
+
* @param value - The value to test (typically a `catch` binding)
|
|
34
|
+
* @returns True if `value` is a {@link WorkflowError}; false otherwise
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```ts
|
|
38
|
+
* try {
|
|
39
|
+
* task.complete('done')
|
|
40
|
+
* } catch (error) {
|
|
41
|
+
* if (isWorkflowError(error) && error.code === 'TRANSITION') retry()
|
|
42
|
+
* }
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
function isWorkflowError(value) {
|
|
46
|
+
try {
|
|
47
|
+
return value instanceof WorkflowError;
|
|
48
|
+
} catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
//#endregion
|
|
7
53
|
//#region src/core/constants.ts
|
|
8
|
-
/**
|
|
54
|
+
/** Names the default {@link import('./types.js').WorkflowDefinition.bail} — graceful (continue on a leaf failure). */
|
|
9
55
|
var DEFAULT_BAIL = false;
|
|
10
56
|
/**
|
|
11
|
-
*
|
|
57
|
+
* Lists every {@link LifecycleStatus} value, frozen — the vocabulary every tier draws from.
|
|
12
58
|
*
|
|
13
59
|
* @remarks
|
|
14
60
|
* Ordered pending → running → terminal (`completed` / `failed` / `skipped` /
|
|
15
|
-
* `stopped`). The source of truth for the union
|
|
61
|
+
* `stopped`). The runtime source of truth for the union:
|
|
62
|
+
* {@link import('./validators.js').isLifecycleStatus} reads this array.
|
|
16
63
|
*/
|
|
17
|
-
var
|
|
18
|
-
"pending",
|
|
19
|
-
"running",
|
|
20
|
-
"completed",
|
|
21
|
-
"failed",
|
|
22
|
-
"skipped",
|
|
23
|
-
"stopped"
|
|
24
|
-
]);
|
|
25
|
-
/** Every {@link PhaseStatus} value, frozen — the lifecycle vocabulary of a phase. */
|
|
26
|
-
var PHASE_STATUSES = Object.freeze([
|
|
27
|
-
"pending",
|
|
28
|
-
"running",
|
|
29
|
-
"completed",
|
|
30
|
-
"failed",
|
|
31
|
-
"skipped",
|
|
32
|
-
"stopped"
|
|
33
|
-
]);
|
|
34
|
-
/** Every {@link WorkflowStatus} value, frozen — the lifecycle vocabulary of a workflow. */
|
|
35
|
-
var WORKFLOW_STATUSES = Object.freeze([
|
|
64
|
+
var LIFECYCLE_STATUSES = Object.freeze([
|
|
36
65
|
"pending",
|
|
37
66
|
"running",
|
|
38
67
|
"completed",
|
|
@@ -41,21 +70,21 @@ var WORKFLOW_STATUSES = Object.freeze([
|
|
|
41
70
|
"stopped"
|
|
42
71
|
]);
|
|
43
72
|
/**
|
|
44
|
-
*
|
|
73
|
+
* Lists the {@link LifecycleStatus} values that are TERMINAL — a node in one of these will
|
|
45
74
|
* not transition further, frozen.
|
|
46
75
|
*
|
|
47
76
|
* @remarks
|
|
48
77
|
* The source of truth behind {@link import('./helpers.js').isTerminalStatus}.
|
|
49
78
|
* `pending` and `running` are the only non-terminal members.
|
|
50
79
|
*/
|
|
51
|
-
var
|
|
80
|
+
var TERMINAL_STATUSES = Object.freeze([
|
|
52
81
|
"completed",
|
|
53
82
|
"failed",
|
|
54
83
|
"skipped",
|
|
55
84
|
"stopped"
|
|
56
85
|
]);
|
|
57
86
|
/**
|
|
58
|
-
*
|
|
87
|
+
* Declares the legal {@link LifecycleStatus} transition graph of the live W-b task state machine —
|
|
59
88
|
* each current status mapped to the statuses it may move to directly, frozen.
|
|
60
89
|
*
|
|
61
90
|
* @remarks
|
|
@@ -85,7 +114,7 @@ var TASK_TRANSITIONS = Object.freeze({
|
|
|
85
114
|
stopped: []
|
|
86
115
|
});
|
|
87
116
|
/**
|
|
88
|
-
*
|
|
117
|
+
* Names the default per-phase task concurrency the {@link import('./factories.js').createWorkflowRunner}
|
|
89
118
|
* runner applies when a {@link import('./types.js').PhaseDefinition} omits its `concurrency`
|
|
90
119
|
* throttle — a cap that is effectively unbounded for any realistic phase.
|
|
91
120
|
*
|
|
@@ -105,74 +134,138 @@ var TASK_TRANSITIONS = Object.freeze({
|
|
|
105
134
|
*/
|
|
106
135
|
var DEFAULT_PHASE_CONCURRENCY = 1024;
|
|
107
136
|
/**
|
|
108
|
-
*
|
|
137
|
+
* Names the largest delay representable by the host timer APIs without overflow or clamping.
|
|
109
138
|
*/
|
|
110
139
|
var MAX_TIMER_MS = 2147483647;
|
|
140
|
+
/**
|
|
141
|
+
* Lists the {@link WorkflowEventMap} / {@link PhaseEventMap} events that make a durable observer
|
|
142
|
+
* re-persist the live tree, frozen.
|
|
143
|
+
*
|
|
144
|
+
* @remarks
|
|
145
|
+
* The two maps carry the same event names, so one list serves both tiers. It is the source of
|
|
146
|
+
* truth behind {@link import('./WorkflowPersistence.js').WorkflowPersistence}'s attach and detach
|
|
147
|
+
* passes: subscribing and unsubscribing loop over these names, so an added event reaches both
|
|
148
|
+
* passes from one edit. `add` and `remove` are deliberately absent — they carry the new or dropped
|
|
149
|
+
* child, so the persistence layer binds its own attaching handler to them instead.
|
|
150
|
+
*/
|
|
151
|
+
var PERSISTED_NODE_EVENTS = Object.freeze([
|
|
152
|
+
"start",
|
|
153
|
+
"complete",
|
|
154
|
+
"fail",
|
|
155
|
+
"skip",
|
|
156
|
+
"stop",
|
|
157
|
+
"move",
|
|
158
|
+
"update"
|
|
159
|
+
]);
|
|
160
|
+
/**
|
|
161
|
+
* Lists the {@link TaskEventMap} events that make a durable observer re-persist the live tree, frozen.
|
|
162
|
+
*
|
|
163
|
+
* @remarks
|
|
164
|
+
* The leaf counterpart of {@link PERSISTED_NODE_EVENTS}, and the source of truth behind the task
|
|
165
|
+
* attach and detach passes of
|
|
166
|
+
* {@link import('./WorkflowPersistence.js').WorkflowPersistence}. `report` and `pulse` join the
|
|
167
|
+
* lifecycle events because an accepted activity frame changes the persisted snapshot; a leaf has
|
|
168
|
+
* no children, so there is no structural event to bind separately.
|
|
169
|
+
*/
|
|
170
|
+
var PERSISTED_TASK_EVENTS = Object.freeze([
|
|
171
|
+
"start",
|
|
172
|
+
"complete",
|
|
173
|
+
"fail",
|
|
174
|
+
"skip",
|
|
175
|
+
"stop",
|
|
176
|
+
"report",
|
|
177
|
+
"pulse"
|
|
178
|
+
]);
|
|
111
179
|
//#endregion
|
|
112
|
-
//#region src/core/
|
|
180
|
+
//#region src/core/validators.ts
|
|
113
181
|
/**
|
|
114
|
-
*
|
|
182
|
+
* Checks whether an unknown value belongs to the workflow lifecycle vocabulary.
|
|
115
183
|
*
|
|
116
184
|
* @remarks
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
185
|
+
* Reads {@link import('./constants.js').LIFECYCLE_STATUSES}, the runtime array every tier draws
|
|
186
|
+
* from, so the vocabulary has one definition rather than a hard-coded copy per guard.
|
|
187
|
+
*
|
|
188
|
+
* @param value - The value to test
|
|
189
|
+
* @returns True if `value` is a {@link LifecycleStatus}; false otherwise
|
|
190
|
+
*
|
|
191
|
+
* @example
|
|
192
|
+
* ```ts
|
|
193
|
+
* isLifecycleStatus('running') // true
|
|
194
|
+
* isLifecycleStatus('paused') // false
|
|
195
|
+
* ```
|
|
123
196
|
*/
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
constructor(code, message, context) {
|
|
128
|
-
super(message);
|
|
129
|
-
this.name = "WorkflowError";
|
|
130
|
-
this.code = code;
|
|
131
|
-
if (context !== void 0) this.context = context;
|
|
132
|
-
}
|
|
133
|
-
};
|
|
197
|
+
function isLifecycleStatus(value) {
|
|
198
|
+
return LIFECYCLE_STATUSES.some((status) => status === value);
|
|
199
|
+
}
|
|
134
200
|
/**
|
|
135
|
-
*
|
|
201
|
+
* Tests a normalized persisted task failure.
|
|
136
202
|
*
|
|
137
|
-
* @
|
|
138
|
-
*
|
|
203
|
+
* @remarks
|
|
204
|
+
* The exact-record guard behind a persisted {@link TaskFailure}: exactly `origin` and `message`,
|
|
205
|
+
* an `origin` drawn from the {@link import('./types.js').TaskFailureOrigin} vocabulary, and a
|
|
206
|
+
* non-empty `message`. Total — a hostile prototype or accessor answers `false` rather than
|
|
207
|
+
* throwing.
|
|
208
|
+
*
|
|
209
|
+
* @param value - The value to test
|
|
210
|
+
* @returns True if `value` is a persisted {@link TaskFailure}; false otherwise
|
|
139
211
|
*
|
|
140
212
|
* @example
|
|
141
213
|
* ```ts
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
* } catch (error) {
|
|
145
|
-
* if (isWorkflowError(error) && error.code === 'TRANSITION') retry()
|
|
146
|
-
* }
|
|
214
|
+
* isTaskFailure({ origin: 'handler', message: 'boom' }) // true
|
|
215
|
+
* isTaskFailure({ origin: 'handler' }) // false
|
|
147
216
|
* ```
|
|
148
217
|
*/
|
|
149
|
-
function
|
|
218
|
+
function isTaskFailure(value) {
|
|
150
219
|
try {
|
|
151
|
-
return value
|
|
220
|
+
return isRecord(value) && Object.keys(value).every((key) => key === "origin" || key === "message") && (value.origin === "handler" || value.origin === "timeout" || value.origin === "recovery") && isNonEmptyString(value.message);
|
|
152
221
|
} catch {
|
|
153
222
|
return false;
|
|
154
223
|
}
|
|
155
224
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
|
|
225
|
+
/**
|
|
226
|
+
* Checks whether an unknown value is a live workflow entity rather than a definition.
|
|
227
|
+
*
|
|
228
|
+
* @remarks
|
|
229
|
+
* The discriminator behind the overloaded
|
|
230
|
+
* {@link import('./types.js').WorkflowRunnerInterface.execute}: a
|
|
231
|
+
* {@link import('./types.js').WorkflowInterface} is the only one of the two carrying `destroyed`
|
|
232
|
+
* (RUNTIME-ONLY, never a field on the pure-JSON
|
|
233
|
+
* {@link import('./types.js').WorkflowDefinition}) AND a callable `snapshot`. Requiring both is
|
|
234
|
+
* sturdier than `destroyed` alone — a definition could coincidentally carry a `destroyed` field as
|
|
235
|
+
* arbitrary data, and pairing it with a function-typed `snapshot` narrows to the actual entity
|
|
236
|
+
* shape without an `as`. It reads a live class instance, so it tests object identity rather than a
|
|
237
|
+
* plain-record brand, and it is total: any other value answers `false`.
|
|
238
|
+
*
|
|
239
|
+
* @param value - The value to test
|
|
240
|
+
* @returns True if `value` is a live {@link WorkflowInterface}; false otherwise
|
|
241
|
+
*
|
|
242
|
+
* @example
|
|
243
|
+
* ```ts
|
|
244
|
+
* isWorkflowInterface(createWorkflow(definition)) // true
|
|
245
|
+
* isWorkflowInterface(definition) // false
|
|
246
|
+
* ```
|
|
247
|
+
*/
|
|
248
|
+
function isWorkflowInterface(value) {
|
|
164
249
|
try {
|
|
165
|
-
return
|
|
250
|
+
return isObject(value) && "destroyed" in value && "snapshot" in value && isFunction(value.snapshot);
|
|
166
251
|
} catch {
|
|
167
252
|
return false;
|
|
168
253
|
}
|
|
169
254
|
}
|
|
170
255
|
/**
|
|
171
|
-
*
|
|
256
|
+
* Validates a safe owned JSON graph as a coherent workflow snapshot.
|
|
172
257
|
*
|
|
173
258
|
* @remarks
|
|
174
259
|
* Callers at hostile boundaries use {@link isWorkflowSnapshot}, which owns the
|
|
175
260
|
* graph first so this semantic pass never observes accessors or prototypes.
|
|
261
|
+
*
|
|
262
|
+
* @param value - The already-owned JSON graph to validate
|
|
263
|
+
* @returns True if `value` is a coherent {@link WorkflowSnapshot}; false otherwise
|
|
264
|
+
*
|
|
265
|
+
* @example
|
|
266
|
+
* ```ts
|
|
267
|
+
* isOwnedWorkflowSnapshot(workflow.snapshot()) // true
|
|
268
|
+
* ```
|
|
176
269
|
*/
|
|
177
270
|
function isOwnedWorkflowSnapshot(value) {
|
|
178
271
|
try {
|
|
@@ -194,7 +287,7 @@ function isOwnedWorkflowSnapshot(value) {
|
|
|
194
287
|
const statuses = [];
|
|
195
288
|
if (phase.tasks.length > 0) vacuous = false;
|
|
196
289
|
for (const task of phase.tasks) {
|
|
197
|
-
if (!isRecord(task) || !Object.keys(task).every((key) => key === "id" || key === "name" || key === "description" || key === "status" || key === "result" || key === "metadata" || key === "attempts" || key === "
|
|
290
|
+
if (!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") || !isNonEmptyString(task.id) || taskIds.has(task.id) || !isNonEmptyString(task.name) || task.description !== void 0 && typeof task.description !== "string" || !isLifecycleStatus(task.status) || !isRecord(task.metadata) || !isJSONValue(task.metadata) || !isInteger(task.attempts) || task.attempts < 0 || task.behavior !== void 0 && !isNonEmptyString(task.behavior) || task.retries !== void 0 && (!isInteger(task.retries) || task.retries < 0) || task.timeout !== void 0 && (!isInteger(task.timeout) || task.timeout < 0 || task.timeout > 2147483647)) return false;
|
|
198
291
|
const budget = (task.retries ?? 0) + 1;
|
|
199
292
|
if (task.attempts > budget || task.status === "pending" && task.attempts >= budget) return false;
|
|
200
293
|
if (!(task.activity === void 0 || isTaskActivity(task.activity))) return false;
|
|
@@ -222,13 +315,82 @@ function isOwnedWorkflowSnapshot(value) {
|
|
|
222
315
|
return false;
|
|
223
316
|
}
|
|
224
317
|
}
|
|
225
|
-
/**
|
|
318
|
+
/**
|
|
319
|
+
* Guards the hostile boundary totally for a workflow snapshot.
|
|
320
|
+
*
|
|
321
|
+
* @remarks
|
|
322
|
+
* Owns the value first through the exact-JSON clone of `@orkestrel/contract`, then runs the
|
|
323
|
+
* semantic pass {@link isOwnedWorkflowSnapshot} over the owned copy — so no accessor, prototype,
|
|
324
|
+
* or cycle in the caller's graph is ever observed by the semantic pass. Total: an unclonable
|
|
325
|
+
* value answers `false` rather than throwing.
|
|
326
|
+
*
|
|
327
|
+
* @param value - The untrusted value to test
|
|
328
|
+
* @returns True if `value` is a coherent {@link WorkflowSnapshot}; false otherwise
|
|
329
|
+
*
|
|
330
|
+
* @example
|
|
331
|
+
* ```ts
|
|
332
|
+
* isWorkflowSnapshot(JSON.parse(payload)) // true only for a coherent snapshot
|
|
333
|
+
* ```
|
|
334
|
+
*/
|
|
226
335
|
function isWorkflowSnapshot(value) {
|
|
227
336
|
const cloned = attempt(() => cloneJSONValue(value));
|
|
228
337
|
return cloned.success && isOwnedWorkflowSnapshot(cloned.value);
|
|
229
338
|
}
|
|
230
339
|
/**
|
|
231
|
-
*
|
|
340
|
+
* Checks whether an unknown value is a valid list of task activity claims.
|
|
341
|
+
*
|
|
342
|
+
* @remarks
|
|
343
|
+
* The one guard behind both claim lists of a {@link TaskActivityInput} — its `operations` and its
|
|
344
|
+
* `constraints` — because {@link import('./types.js').TaskOperation} and
|
|
345
|
+
* {@link import('./types.js').TaskConstraint} are the same {@link TaskClaim} shape. Every member must be a plain record carrying exactly `id`, `name`, and
|
|
346
|
+
* `started`, with non-empty string `id` and `name`, a finite non-negative `started`, and an `id`
|
|
347
|
+
* unique within the list. Total: a hostile prototype, an accessor, or a cycle returns `false`
|
|
348
|
+
* rather than throwing.
|
|
349
|
+
*
|
|
350
|
+
* @param value - The value to test
|
|
351
|
+
* @returns True if `value` is a list of valid, uniquely identified claims; false otherwise
|
|
352
|
+
*
|
|
353
|
+
* @example
|
|
354
|
+
* ```ts
|
|
355
|
+
* isTaskClaimList([{ id: 'fetch', name: 'Fetch', started: 1 }]) // true
|
|
356
|
+
* isTaskClaimList([{ id: 'fetch', name: 'Fetch' }]) // false
|
|
357
|
+
* ```
|
|
358
|
+
*/
|
|
359
|
+
function isTaskClaimList(value) {
|
|
360
|
+
try {
|
|
361
|
+
if (!isArray(value)) return false;
|
|
362
|
+
const ids = /* @__PURE__ */ new Set();
|
|
363
|
+
for (const claim of value) {
|
|
364
|
+
if (!isRecord(claim)) return false;
|
|
365
|
+
const prototype = Object.getPrototypeOf(claim);
|
|
366
|
+
if (prototype !== Object.prototype && prototype !== null || !Object.keys(claim).every((key) => key === "id" || key === "name" || key === "started")) return false;
|
|
367
|
+
const id = claim.id;
|
|
368
|
+
const name = claim.name;
|
|
369
|
+
const started = claim.started;
|
|
370
|
+
if (!isNonEmptyString(id) || !isNonEmptyString(name) || !isFiniteNumber(started) || started < 0 || ids.has(id)) return false;
|
|
371
|
+
ids.add(id);
|
|
372
|
+
}
|
|
373
|
+
return true;
|
|
374
|
+
} catch {
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Tests whether an unknown value is a valid whole-frame activity report.
|
|
380
|
+
*
|
|
381
|
+
* @remarks
|
|
382
|
+
* The guard behind {@link import('./types.js').TaskInterface.report}: exactly the optional `note`,
|
|
383
|
+
* `progress`, `operations`, and `constraints` keys, with the two claim lists checked by
|
|
384
|
+
* {@link isTaskClaimList} and `progress` a finite non-negative value under an optional `total` at
|
|
385
|
+
* least as large. Total — a hostile prototype or accessor answers `false` rather than throwing.
|
|
386
|
+
*
|
|
387
|
+
* @param value - The value to test
|
|
388
|
+
* @returns True if `value` is a valid {@link TaskActivityInput}; false otherwise
|
|
389
|
+
*
|
|
390
|
+
* @example
|
|
391
|
+
* ```ts
|
|
392
|
+
* isTaskActivityInput({ note: 'compiling', progress: { progress: 2, total: 5 } }) // true
|
|
393
|
+
* ```
|
|
232
394
|
*/
|
|
233
395
|
function isTaskActivityInput(value) {
|
|
234
396
|
try {
|
|
@@ -249,41 +411,29 @@ function isTaskActivityInput(value) {
|
|
|
249
411
|
const message = progress.message;
|
|
250
412
|
if (!isFiniteNumber(reported) || reported < 0 || total !== void 0 && (!isFiniteNumber(total) || total < reported) || message !== void 0 && !isNonEmptyString(message)) return false;
|
|
251
413
|
}
|
|
252
|
-
if (operations !== void 0)
|
|
253
|
-
|
|
254
|
-
const ids = /* @__PURE__ */ new Set();
|
|
255
|
-
for (const operation of operations) {
|
|
256
|
-
if (!isRecord(operation)) return false;
|
|
257
|
-
const operationPrototype = Object.getPrototypeOf(operation);
|
|
258
|
-
if (operationPrototype !== Object.prototype && operationPrototype !== null || !Object.keys(operation).every((key) => key === "id" || key === "name" || key === "started")) return false;
|
|
259
|
-
const id = operation.id;
|
|
260
|
-
const name = operation.name;
|
|
261
|
-
const started = operation.started;
|
|
262
|
-
if (!isNonEmptyString(id) || !isNonEmptyString(name) || !isFiniteNumber(started) || started < 0 || ids.has(id)) return false;
|
|
263
|
-
ids.add(id);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
if (constraints !== void 0) {
|
|
267
|
-
if (!isArray(constraints)) return false;
|
|
268
|
-
const ids = /* @__PURE__ */ new Set();
|
|
269
|
-
for (const constraint of constraints) {
|
|
270
|
-
if (!isRecord(constraint)) return false;
|
|
271
|
-
const constraintPrototype = Object.getPrototypeOf(constraint);
|
|
272
|
-
if (constraintPrototype !== Object.prototype && constraintPrototype !== null || !Object.keys(constraint).every((key) => key === "id" || key === "name" || key === "started")) return false;
|
|
273
|
-
const id = constraint.id;
|
|
274
|
-
const name = constraint.name;
|
|
275
|
-
const started = constraint.started;
|
|
276
|
-
if (!isNonEmptyString(id) || !isNonEmptyString(name) || !isFiniteNumber(started) || started < 0 || ids.has(id)) return false;
|
|
277
|
-
ids.add(id);
|
|
278
|
-
}
|
|
279
|
-
}
|
|
414
|
+
if (operations !== void 0 && !isTaskClaimList(operations)) return false;
|
|
415
|
+
if (constraints !== void 0 && !isTaskClaimList(constraints)) return false;
|
|
280
416
|
return true;
|
|
281
417
|
} catch {
|
|
282
418
|
return false;
|
|
283
419
|
}
|
|
284
420
|
}
|
|
285
421
|
/**
|
|
286
|
-
*
|
|
422
|
+
* Tests whether an unknown value is valid persisted task activity.
|
|
423
|
+
*
|
|
424
|
+
* @remarks
|
|
425
|
+
* The persisted counterpart of {@link isTaskActivityInput}: the same frame plus the REQUIRED
|
|
426
|
+
* `operations`, `constraints`, and a finite non-negative `updated` stamp, because a stored frame
|
|
427
|
+
* has already been accepted and normalized. Total — a hostile prototype or accessor answers
|
|
428
|
+
* `false` rather than throwing.
|
|
429
|
+
*
|
|
430
|
+
* @param value - The value to test
|
|
431
|
+
* @returns True if `value` is a persisted {@link TaskActivity}; false otherwise
|
|
432
|
+
*
|
|
433
|
+
* @example
|
|
434
|
+
* ```ts
|
|
435
|
+
* isTaskActivity({ operations: [], constraints: [], updated: 1 }) // true
|
|
436
|
+
* ```
|
|
287
437
|
*/
|
|
288
438
|
function isTaskActivity(value) {
|
|
289
439
|
try {
|
|
@@ -309,7 +459,7 @@ function isTaskActivity(value) {
|
|
|
309
459
|
//#endregion
|
|
310
460
|
//#region src/core/helpers.ts
|
|
311
461
|
/**
|
|
312
|
-
*
|
|
462
|
+
* Captures every top-level {@link WorkflowOptions} value exactly once into an owned plain bag.
|
|
313
463
|
*
|
|
314
464
|
* @remarks
|
|
315
465
|
* Direct property reads preserve inherited and non-enumerable option values while preventing
|
|
@@ -344,25 +494,151 @@ function captureWorkflowOptions(options) {
|
|
|
344
494
|
});
|
|
345
495
|
}
|
|
346
496
|
/**
|
|
347
|
-
*
|
|
497
|
+
* Tests whether a {@link LifecycleStatus} is TERMINAL — a node in this state will not
|
|
348
498
|
* transition further.
|
|
349
499
|
*
|
|
350
500
|
* @remarks
|
|
351
|
-
* The ONE terminal check across
|
|
501
|
+
* The ONE terminal check across every tier (AGENTS.md § Design laws, "one concept, one term"):
|
|
352
502
|
* a task, a phase, and a workflow share the same {@link LifecycleStatus} vocabulary, so a
|
|
353
503
|
* single predicate covers them — {@link derivePhaseStatus} and {@link deriveWorkflowStatus}
|
|
354
|
-
* both consult it to tell a settled node from an in-flight one.
|
|
355
|
-
* `
|
|
356
|
-
* `
|
|
504
|
+
* both consult it to tell a settled node from an in-flight one. It reads the terminal set from
|
|
505
|
+
* {@link import('./constants.js').TERMINAL_STATUSES} (`completed` / `failed` / `skipped` /
|
|
506
|
+
* `stopped`), so that constant is the one definition; the only non-terminal states are `pending`
|
|
507
|
+
* and `running`.
|
|
357
508
|
*
|
|
358
509
|
* @param status - The lifecycle status to test (a task / phase / workflow status)
|
|
359
|
-
* @returns
|
|
510
|
+
* @returns True if the status is terminal; false otherwise
|
|
360
511
|
*/
|
|
361
512
|
function isTerminalStatus(status) {
|
|
362
|
-
return status
|
|
513
|
+
return TERMINAL_STATUSES.includes(status);
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Tests whether a driving run must stop giving a workflow more work.
|
|
517
|
+
*
|
|
518
|
+
* @remarks
|
|
519
|
+
* The halt gate a {@link import('./WorkflowRunner.js').WorkflowRunner} consults before starting a
|
|
520
|
+
* phase, before dispatching a task, and after every cooperative gate. A workflow is halted after
|
|
521
|
+
* its derived status is terminal but NOT `completed` — a `bail: true` failure, a caller's own
|
|
522
|
+
* graceful `stop()`, or a forced `skip`. `completed` is excluded deliberately: a workflow that
|
|
523
|
+
* completed vacuously is settled, not halted, and the distinction is what keeps the run from
|
|
524
|
+
* sweeping a finished tree. When a `phase` is supplied, its own forced `skipped` / `stopped` halts
|
|
525
|
+
* that phase's work too; a `failed` phase does not, because the workflow's own `bail` policy
|
|
526
|
+
* decides whether a failed phase ends the run.
|
|
527
|
+
*
|
|
528
|
+
* @param workflow - The live workflow the run is driving
|
|
529
|
+
* @param phase - The phase whose own forced terminal status also halts its tasks
|
|
530
|
+
* @returns True if the run must stop giving this workflow (or phase) more work; false otherwise
|
|
531
|
+
*
|
|
532
|
+
* @example
|
|
533
|
+
* ```ts
|
|
534
|
+
* isHalted(workflow) // false while pending or running
|
|
535
|
+
* workflow.stop()
|
|
536
|
+
* isHalted(workflow) // true
|
|
537
|
+
* ```
|
|
538
|
+
*/
|
|
539
|
+
function isHalted(workflow, phase) {
|
|
540
|
+
const status = workflow.status;
|
|
541
|
+
return isTerminalStatus(status) && status !== "completed" || phase?.status === "skipped" || phase?.status === "stopped";
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Tests whether forcing a workflow `stopped` would still record something.
|
|
545
|
+
*
|
|
546
|
+
* @remarks
|
|
547
|
+
* `stop()` is a no-op after a workflow's status becomes terminal, so a run that must record a
|
|
548
|
+
* cancellation forces it only while this holds. It is NOT the negation of
|
|
549
|
+
* {@link isTerminalStatus}: `completed` and `skipped` both pass, because a run-level cancel that
|
|
550
|
+
* lands on a vacuously-completed or fully-skipped tree still records `stopped` as the outcome the
|
|
551
|
+
* caller asked for. Only an already-`failed` or already-`stopped` workflow has a terminal state
|
|
552
|
+
* worth keeping.
|
|
553
|
+
*
|
|
554
|
+
* @param workflow - The live workflow a run-level cancel would force
|
|
555
|
+
* @returns True if forcing `stopped` would change the recorded outcome; false otherwise
|
|
556
|
+
*
|
|
557
|
+
* @example
|
|
558
|
+
* ```ts
|
|
559
|
+
* isStoppable(workflow) // true while pending, running, completed, or skipped
|
|
560
|
+
* workflow.stop()
|
|
561
|
+
* isStoppable(workflow) // false
|
|
562
|
+
* ```
|
|
563
|
+
*/
|
|
564
|
+
function isStoppable(workflow) {
|
|
565
|
+
const status = workflow.status;
|
|
566
|
+
return status !== "failed" && status !== "stopped";
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Tests whether a naturally-finished run may force its workflow `completed`.
|
|
570
|
+
*
|
|
571
|
+
* @remarks
|
|
572
|
+
* A run that walked every phase and still derives `pending` executed nothing — zero phases, or
|
|
573
|
+
* every phase empty — so it is vacuously done and the run settles it `completed`. Gated on
|
|
574
|
+
* EXACTLY `pending` so a real `completed`, a `bail: true` `failed`, a `stopped`, or a derived
|
|
575
|
+
* `skipped` is never overridden. The tree-is-empty half of the rule is
|
|
576
|
+
* {@link WorkflowInterface.complete}'s own guard, which refuses a pending tree that still holds
|
|
577
|
+
* tasks.
|
|
578
|
+
*
|
|
579
|
+
* @param workflow - The live workflow the run has finished walking
|
|
580
|
+
* @returns True if the run may force the vacuous completion; false otherwise
|
|
581
|
+
*
|
|
582
|
+
* @example
|
|
583
|
+
* ```ts
|
|
584
|
+
* isCompletable(createWorkflow({ id: 'w', name: 'W', phases: [] })) // true
|
|
585
|
+
* ```
|
|
586
|
+
*/
|
|
587
|
+
function isCompletable(workflow) {
|
|
588
|
+
return workflow.status === "pending";
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Tests whether a task attempt is being genuinely cancelled rather than merely timed out.
|
|
592
|
+
*
|
|
593
|
+
* @remarks
|
|
594
|
+
* The discriminator that keeps a per-attempt deadline off the skip path. Three causes fire a
|
|
595
|
+
* running task's folded signal, and only two of them mean "skip this task": the task's own
|
|
596
|
+
* `signal` (its `stop` / `skip`), and the unit or run signal (a sibling fail-fast under
|
|
597
|
+
* `bail: true`, or a run-level abort / timeout / budget / `destroy`). A bare per-attempt timeout
|
|
598
|
+
* fires NEITHER — it aborts only the deadline portion of the attempt signal — so it stays a
|
|
599
|
+
* retryable failure of that attempt instead of skipping the leaf and losing the recorded fault.
|
|
600
|
+
* Read fresh at each call so a cancel that lands mid-dispatch is seen.
|
|
601
|
+
*
|
|
602
|
+
* @param task - The live task the attempt is driving
|
|
603
|
+
* @param controller - The substrate unit handle carrying the unit-level abort
|
|
604
|
+
* @param runSignal - The run's folded cancellation signal
|
|
605
|
+
* @returns True if the attempt is being genuinely cancelled; false otherwise
|
|
606
|
+
*
|
|
607
|
+
* @example
|
|
608
|
+
* ```ts
|
|
609
|
+
* isSkipping(task, controller, runSignal) // false until a cancel fires
|
|
610
|
+
* ```
|
|
611
|
+
*/
|
|
612
|
+
function isSkipping(task, controller, runSignal) {
|
|
613
|
+
return task.signal.aborted || controller.aborted || runSignal.aborted;
|
|
363
614
|
}
|
|
364
615
|
/**
|
|
365
|
-
*
|
|
616
|
+
* Tests whether one attempt still owns the task it launched.
|
|
617
|
+
*
|
|
618
|
+
* @remarks
|
|
619
|
+
* A retried task is re-dispatched while an earlier attempt's handler may still be resolving, so
|
|
620
|
+
* every settlement path re-checks ownership before touching the leaf. Ownership needs BOTH
|
|
621
|
+
* halves: the run-local `owners` ledger must still name this attempt, and the live task's own
|
|
622
|
+
* `attempts` tally must still match it. A superseded attempt reads `false` and returns without
|
|
623
|
+
* recording anything, so a late resolution can never overwrite the newer attempt's outcome.
|
|
624
|
+
*
|
|
625
|
+
* @param owners - The run-local ledger of the attempt owning each task id
|
|
626
|
+
* @param task - The live task the attempt launched
|
|
627
|
+
* @param attempt - The one-based attempt number to test
|
|
628
|
+
* @returns True if `attempt` still owns `task`; false otherwise
|
|
629
|
+
*
|
|
630
|
+
* @example
|
|
631
|
+
* ```ts
|
|
632
|
+
* const owners = new Map([[task.id, 1]])
|
|
633
|
+
* ownsAttempt(owners, task, 1) // true while the task's own `attempts` is 1
|
|
634
|
+
* ownsAttempt(owners, task, 2) // false
|
|
635
|
+
* ```
|
|
636
|
+
*/
|
|
637
|
+
function ownsAttempt(owners, task, attempt) {
|
|
638
|
+
return owners.get(task.id) === attempt && task.attempts === attempt;
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Derives a phase's status from its tasks' statuses (tasks are concurrent, so this
|
|
366
642
|
* is an order-insensitive reduction).
|
|
367
643
|
*
|
|
368
644
|
* @remarks
|
|
@@ -380,7 +656,7 @@ function isTerminalStatus(status) {
|
|
|
380
656
|
* task makes the phase `failed`.
|
|
381
657
|
*
|
|
382
658
|
* @param tasks - The phase's task statuses, in any order
|
|
383
|
-
* @returns The derived {@link
|
|
659
|
+
* @returns The derived phase {@link LifecycleStatus}
|
|
384
660
|
*/
|
|
385
661
|
function derivePhaseStatus(tasks) {
|
|
386
662
|
if (tasks.length === 0) return "pending";
|
|
@@ -392,13 +668,13 @@ function derivePhaseStatus(tasks) {
|
|
|
392
668
|
return "skipped";
|
|
393
669
|
}
|
|
394
670
|
/**
|
|
395
|
-
*
|
|
671
|
+
* Derives a workflow's status from its phases' {@link PhaseDerivation}s — each phase's status
|
|
396
672
|
* paired with the EFFECTIVE `bail` it ran under (`phase.bail ?? workflow.bail`) — so the
|
|
397
673
|
* failure outcome is PER-PHASE-bail-aware (phases are sequential, but the derivation is an
|
|
398
674
|
* order-insensitive reduction over the settled set).
|
|
399
675
|
*
|
|
400
676
|
* @remarks
|
|
401
|
-
* `bail` is
|
|
677
|
+
* `bail` is a per-phase override, so it is carried on each
|
|
402
678
|
* {@link PhaseDerivation} rather than passed as one scalar. It is the ONLY axis that changes
|
|
403
679
|
* the failure outcome, decided per phase:
|
|
404
680
|
* - **A `failed` phase whose effective `bail` is `true` (halt)** propagates ⇒ the workflow is
|
|
@@ -417,7 +693,7 @@ function derivePhaseStatus(tasks) {
|
|
|
417
693
|
* else (all `skipped`) ⇒ `skipped`.
|
|
418
694
|
*
|
|
419
695
|
* @param phases - The workflow's per-phase {@link PhaseDerivation}s (status + effective bail), in any order
|
|
420
|
-
* @returns The derived {@link
|
|
696
|
+
* @returns The derived workflow {@link LifecycleStatus}
|
|
421
697
|
*/
|
|
422
698
|
function deriveWorkflowStatus(phases) {
|
|
423
699
|
if (phases.length === 0) return "pending";
|
|
@@ -429,16 +705,16 @@ function deriveWorkflowStatus(phases) {
|
|
|
429
705
|
return "skipped";
|
|
430
706
|
}
|
|
431
707
|
/**
|
|
432
|
-
*
|
|
708
|
+
* Derives the PENDING SUFFIX boundary of a positional list of {@link LifecycleStatus}es —
|
|
433
709
|
* the index of the first entry in the contiguous trailing run of `pending` entries.
|
|
434
710
|
*
|
|
435
711
|
* @remarks
|
|
436
|
-
* The native, hook-free replacement for a runner-installed cursor
|
|
712
|
+
* The native, hook-free replacement for a runner-installed cursor: a
|
|
437
713
|
* {@link import('./types.js').WorkflowInterface}'s `add` / `remove` / `move` / `update`
|
|
438
714
|
* reads this over its live phases' statuses to decide which positions are safe to edit.
|
|
439
715
|
* Because entries run SEQUENTIALLY (phases sequential, AGENTS determinism), every
|
|
440
716
|
* already-started entry forms a contiguous LEADING prefix and every still-`pending`
|
|
441
|
-
* entry forms the trailing suffix — so the boundary is
|
|
717
|
+
* entry forms the trailing suffix — so the boundary is the count of leading
|
|
442
718
|
* non-`pending` entries: the index of the first `pending` entry, or the full length when
|
|
443
719
|
* none is `pending` (nothing is safely editable). A `pending` container's entries are ALL
|
|
444
720
|
* `pending`, so the boundary is `0` and every position is naturally accepted — callers
|
|
@@ -459,8 +735,8 @@ function deriveBoundary(statuses) {
|
|
|
459
735
|
return index === -1 ? statuses.length : index;
|
|
460
736
|
}
|
|
461
737
|
/**
|
|
462
|
-
*
|
|
463
|
-
* {@link
|
|
738
|
+
* Tests whether the live W-b task state machine may move directly from one
|
|
739
|
+
* {@link LifecycleStatus} to another — the legal-transition guard.
|
|
464
740
|
*
|
|
465
741
|
* @remarks
|
|
466
742
|
* Reads the {@link import('./constants.js').TASK_TRANSITIONS} graph: `true` only when
|
|
@@ -470,13 +746,13 @@ function deriveBoundary(statuses) {
|
|
|
470
746
|
*
|
|
471
747
|
* @param from - The task's current status
|
|
472
748
|
* @param to - The status the transition would move it to
|
|
473
|
-
* @returns
|
|
749
|
+
* @returns True if the move is legal; false otherwise
|
|
474
750
|
*/
|
|
475
751
|
function canTransitionTask(from, to) {
|
|
476
752
|
return TASK_TRANSITIONS[from].includes(to);
|
|
477
753
|
}
|
|
478
754
|
/**
|
|
479
|
-
*
|
|
755
|
+
* Resolves a task's runtime silence window against its workflow default.
|
|
480
756
|
*
|
|
481
757
|
* @param value - The task-level override; any present non-positive or non-finite value disables
|
|
482
758
|
* @param fallback - The workflow-level default
|
|
@@ -487,7 +763,7 @@ function resolveTaskSilence(value, fallback) {
|
|
|
487
763
|
return fallback !== void 0 && Number.isFinite(fallback) && fallback > 0 && fallback <= 2147483647 ? fallback : void 0;
|
|
488
764
|
}
|
|
489
765
|
/**
|
|
490
|
-
*
|
|
766
|
+
* Boxes a value as a {@link Success} — the graceful outcome half of a {@link Result}.
|
|
491
767
|
*
|
|
492
768
|
* @typeParam T - The boxed value's type
|
|
493
769
|
* @param value - The value to box
|
|
@@ -505,7 +781,7 @@ function success(value) {
|
|
|
505
781
|
};
|
|
506
782
|
}
|
|
507
783
|
/**
|
|
508
|
-
*
|
|
784
|
+
* Boxes an error as a {@link Failure} — the graceful outcome half of a {@link Result}.
|
|
509
785
|
*
|
|
510
786
|
* @typeParam E - The boxed error's type
|
|
511
787
|
* @param error - The error to box
|
|
@@ -523,7 +799,7 @@ function failure(error) {
|
|
|
523
799
|
};
|
|
524
800
|
}
|
|
525
801
|
/**
|
|
526
|
-
*
|
|
802
|
+
* Normalizes an unknown thrown value to a non-empty persistence-safe message.
|
|
527
803
|
*
|
|
528
804
|
* @param error - The caught value
|
|
529
805
|
* @returns A non-empty message without stack or cause data
|
|
@@ -537,7 +813,7 @@ function errorToMessage(error) {
|
|
|
537
813
|
}
|
|
538
814
|
}
|
|
539
815
|
/**
|
|
540
|
-
*
|
|
816
|
+
* Finds the first {@link TaskResult} in a positional list whose boxed outcome is a
|
|
541
817
|
* `Failure` — the pure scan shared by a phase's and a workflow's derived-`failed`
|
|
542
818
|
* `fail`-event lookup.
|
|
543
819
|
*
|
|
@@ -545,8 +821,8 @@ function errorToMessage(error) {
|
|
|
545
821
|
* The shared leaf behind {@link import('./phases/Phase.js').Phase} and
|
|
546
822
|
* {@link import('./Workflow.js').Workflow}'s own `#failure` — each gathers ITS tier's
|
|
547
823
|
* results (a phase's own settled tasks, a workflow's flattened `results()`) and feeds
|
|
548
|
-
* them here; the tier-local method keeps the
|
|
549
|
-
* status
|
|
824
|
+
* them here; the tier-local method keeps the invariant throw (a derived `failed`
|
|
825
|
+
* status means a failing result exists) because throwing on `undefined` is
|
|
550
826
|
* orchestration, not a leaf concern.
|
|
551
827
|
*
|
|
552
828
|
* @param results - The results to scan, in any order
|
|
@@ -561,7 +837,7 @@ function findFailure(results) {
|
|
|
561
837
|
return results.find((result) => result.result?.success === false);
|
|
562
838
|
}
|
|
563
839
|
/**
|
|
564
|
-
*
|
|
840
|
+
* Builds a {@link WorkflowContext} — the identity every level inherits — from a node's
|
|
565
841
|
* `id` / `name` / optional `description`.
|
|
566
842
|
*
|
|
567
843
|
* @remarks
|
|
@@ -580,7 +856,7 @@ function buildWorkflowContext(node) {
|
|
|
580
856
|
});
|
|
581
857
|
}
|
|
582
858
|
/**
|
|
583
|
-
*
|
|
859
|
+
* Builds a {@link PhaseContext} — a phase's own identity plus a back-reference to its
|
|
584
860
|
* workflow — from the parent {@link WorkflowContext} and the phase node's identity.
|
|
585
861
|
*
|
|
586
862
|
* @param workflow - The parent workflow context (the lineage pointer UP the tree)
|
|
@@ -594,7 +870,7 @@ function buildPhaseContext(workflow, node) {
|
|
|
594
870
|
});
|
|
595
871
|
}
|
|
596
872
|
/**
|
|
597
|
-
*
|
|
873
|
+
* Builds a {@link TaskContext} — a task's own identity plus a back-reference to its phase
|
|
598
874
|
* (and, transitively, its workflow) — from the parent {@link PhaseContext} and the task
|
|
599
875
|
* node's identity.
|
|
600
876
|
*
|
|
@@ -609,20 +885,20 @@ function buildTaskContext(phase, node) {
|
|
|
609
885
|
});
|
|
610
886
|
}
|
|
611
887
|
/**
|
|
612
|
-
*
|
|
888
|
+
* Converts a {@link WorkflowDefinition} into an INITIAL {@link WorkflowSnapshot} — every
|
|
613
889
|
* node `pending`, no results, empty metadata — so the live W-b tree has ONE construction
|
|
614
890
|
* path (snapshot-driven) for both a fresh build and a restore.
|
|
615
891
|
*
|
|
616
892
|
* @remarks
|
|
617
893
|
* The structural fields (`id` / `name` / `description` + the ordered phases / tasks)
|
|
618
894
|
* carry over verbatim, as does each phase's `concurrency` (persisted on the
|
|
619
|
-
* {@link PhaseSnapshot} so a restore reinstates the same throttle) and each task's `
|
|
895
|
+
* {@link PhaseSnapshot} so a restore reinstates the same throttle) and each task's `behavior` /
|
|
620
896
|
* `retries` / `timeout` (persisted on the {@link TaskSnapshot}, like `bail` / `concurrency`,
|
|
621
897
|
* so a restore + a {@link import('./types.js').WorkflowOptions.functions} registry resumes
|
|
622
898
|
* real work). The `bail` policy carries over — at the
|
|
623
899
|
* workflow tier AND, per phase, the
|
|
624
900
|
* EFFECTIVE policy (`phase.bail ?? workflowBail`) on each {@link PhaseSnapshot} — so the seeded
|
|
625
|
-
* snapshot is self-contained; a fresh seed has no `override`. `created` / `updated` are stamped
|
|
901
|
+
* snapshot is self-contained; a fresh seed has no `override`. `created` / `updated` are stamped at that point.
|
|
626
902
|
* {@link import('./factories.js').createWorkflow} builds from this.
|
|
627
903
|
*
|
|
628
904
|
* The optional `bail` override is the EFFECTIVE workflow policy the tree will run under
|
|
@@ -651,7 +927,7 @@ function definitionToSnapshot(definition, bail) {
|
|
|
651
927
|
};
|
|
652
928
|
}
|
|
653
929
|
/**
|
|
654
|
-
*
|
|
930
|
+
* Converts one {@link import('./types.js').PhaseDefinition} into an initial, all-`pending`
|
|
655
931
|
* {@link PhaseSnapshot} — the per-phase step of {@link definitionToSnapshot}.
|
|
656
932
|
*
|
|
657
933
|
* @remarks
|
|
@@ -676,14 +952,14 @@ function phaseDefinitionToSnapshot(phase, workflowBail) {
|
|
|
676
952
|
};
|
|
677
953
|
}
|
|
678
954
|
/**
|
|
679
|
-
*
|
|
955
|
+
* Converts one {@link import('./types.js').TaskDefinition} into an initial, `pending`
|
|
680
956
|
* {@link TaskSnapshot} — the per-task leaf step of {@link definitionToSnapshot} (no
|
|
681
957
|
* result yet, empty metadata).
|
|
682
958
|
*
|
|
683
959
|
* @remarks
|
|
684
|
-
* `
|
|
960
|
+
* `behavior` / `retries` / `timeout` carry over verbatim (persisted declarative config, like a
|
|
685
961
|
* phase's `bail` / `concurrency`) — a restore reinstates the same behavior reference and
|
|
686
|
-
* reliability overrides
|
|
962
|
+
* reliability overrides after pairing with a {@link import('./types.js').WorkflowOptions.functions}
|
|
687
963
|
* registry.
|
|
688
964
|
*
|
|
689
965
|
* @param task - The task definition to seed from
|
|
@@ -697,13 +973,13 @@ function taskDefinitionToSnapshot(task) {
|
|
|
697
973
|
status: "pending",
|
|
698
974
|
metadata: {},
|
|
699
975
|
attempts: 0,
|
|
700
|
-
...task.
|
|
976
|
+
...task.behavior === void 0 ? {} : { behavior: task.behavior },
|
|
701
977
|
...task.retries === void 0 ? {} : { retries: task.retries },
|
|
702
978
|
...task.timeout === void 0 ? {} : { timeout: task.timeout }
|
|
703
979
|
};
|
|
704
980
|
}
|
|
705
981
|
/**
|
|
706
|
-
*
|
|
982
|
+
* Converts interrupted running work into a recoverable pending suffix or an
|
|
707
983
|
* exhausted recovery failure without replenishing attempts.
|
|
708
984
|
*
|
|
709
985
|
* @param snapshot - A fully validated owned snapshot with no terminal overrides
|
|
@@ -780,11 +1056,58 @@ function recoverWorkflowSnapshot(snapshot) {
|
|
|
780
1056
|
updated: now
|
|
781
1057
|
};
|
|
782
1058
|
}
|
|
783
|
-
/**
|
|
1059
|
+
/**
|
|
1060
|
+
* Compares two optional description values.
|
|
1061
|
+
*
|
|
1062
|
+
* @remarks
|
|
1063
|
+
* The equality rule a lineage check needs: two descriptions match when they are the same value
|
|
1064
|
+
* AND that value is either a string or genuine absence. Anything else — a number, an object, a
|
|
1065
|
+
* `null` — never matches, even against itself, so a lineage stamped with a non-string description
|
|
1066
|
+
* is rejected rather than silently accepted.
|
|
1067
|
+
*
|
|
1068
|
+
* @param left - The first description value
|
|
1069
|
+
* @param right - The second description value
|
|
1070
|
+
* @returns True if both are the same string or both absent; false otherwise
|
|
1071
|
+
*
|
|
1072
|
+
* @example
|
|
1073
|
+
* ```ts
|
|
1074
|
+
* matchesDescription('build', 'build') // true
|
|
1075
|
+
* matchesDescription(undefined, undefined) // true
|
|
1076
|
+
* matchesDescription('build', undefined) // false
|
|
1077
|
+
* ```
|
|
1078
|
+
*/
|
|
784
1079
|
function matchesDescription(left, right) {
|
|
785
1080
|
return left === right && (left === void 0 || typeof left === "string");
|
|
786
1081
|
}
|
|
787
|
-
/**
|
|
1082
|
+
/**
|
|
1083
|
+
* Tests a result's lineage against its containing snapshot nodes.
|
|
1084
|
+
*
|
|
1085
|
+
* @remarks
|
|
1086
|
+
* The four arguments are the result and the three snapshot nodes it claims to belong to, read
|
|
1087
|
+
* from the OUTSIDE in: a {@link TaskResult} is self-describing, so restoring one is only safe
|
|
1088
|
+
* when every identity it carries agrees with the tree it was found in. It checks the exact key
|
|
1089
|
+
* set at each level, that `status` equals the owning task's, and that the `task` / `phase` /
|
|
1090
|
+
* `workflow` contexts — including the nested `task.phase.workflow` lineage — carry the same `id`,
|
|
1091
|
+
* `name`, and `description` as the nodes containing them. It then requires the boxed outcome to
|
|
1092
|
+
* match the status: a `Success` holding JSON for `completed`, a `Failure` holding a
|
|
1093
|
+
* {@link TaskFailure} for `failed`, and nothing for any other status. Total — a hostile
|
|
1094
|
+
* prototype, accessor, or cycle answers `false` rather than throwing.
|
|
1095
|
+
*
|
|
1096
|
+
* @param value - The candidate {@link TaskResult}
|
|
1097
|
+
* @param workflow - The workflow snapshot node containing it
|
|
1098
|
+
* @param phase - The phase snapshot node containing it
|
|
1099
|
+
* @param task - The task snapshot node the result belongs to
|
|
1100
|
+
* @returns True if `value` is a {@link TaskResult} whose lineage and outcome match
|
|
1101
|
+
* those nodes; false otherwise
|
|
1102
|
+
*
|
|
1103
|
+
* @example
|
|
1104
|
+
* ```ts
|
|
1105
|
+
* const snapshot = workflow.snapshot()
|
|
1106
|
+
* const phase = snapshot.phases[0]
|
|
1107
|
+
* const task = phase?.tasks[0]
|
|
1108
|
+
* isTaskResult(task?.result, snapshot, phase, task) // true for a settled task
|
|
1109
|
+
* ```
|
|
1110
|
+
*/
|
|
788
1111
|
function isTaskResult(value, workflow, phase, task) {
|
|
789
1112
|
try {
|
|
790
1113
|
if (!isRecord(value) || !isRecord(workflow) || !isRecord(phase) || !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 || !isFiniteNumber(value.timestamp) || value.timestamp < 0 || !isRecord(value.task) || !isRecord(value.phase) || !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;
|
|
@@ -800,19 +1123,30 @@ function isTaskResult(value, workflow, phase, task) {
|
|
|
800
1123
|
}
|
|
801
1124
|
function hasWorkflowHandlers(workflow, functions) {
|
|
802
1125
|
if ("destroyed" in workflow) {
|
|
803
|
-
for (const phase of workflow.phases.phases()) for (const task of phase.tasks.tasks()) if (task.
|
|
1126
|
+
for (const phase of workflow.phases.phases()) for (const task of phase.tasks.tasks()) if (task.behavior !== void 0 && !isFunction(task.handler)) return false;
|
|
804
1127
|
return true;
|
|
805
1128
|
}
|
|
806
|
-
const
|
|
1129
|
+
const behaviors = /* @__PURE__ */ new Set();
|
|
807
1130
|
for (const phase of workflow.phases) for (const task of phase.tasks) {
|
|
808
|
-
if (task.
|
|
809
|
-
|
|
810
|
-
if (!isFunction(functions?.[task.
|
|
1131
|
+
if (task.behavior === void 0 || behaviors.has(task.behavior)) continue;
|
|
1132
|
+
behaviors.add(task.behavior);
|
|
1133
|
+
if (!isFunction(functions?.[task.behavior])) return false;
|
|
811
1134
|
}
|
|
812
1135
|
return true;
|
|
813
1136
|
}
|
|
814
|
-
/**
|
|
815
|
-
|
|
1137
|
+
/**
|
|
1138
|
+
* Locates the nearest identifiable node for an inconsistent owned snapshot.
|
|
1139
|
+
*
|
|
1140
|
+
* @remarks
|
|
1141
|
+
* The walk stops at the first phase or task whose persisted fields are inconsistent and returns
|
|
1142
|
+
* the identifiers it could read there, so a diagnostic can name the offending node even when part
|
|
1143
|
+
* of its identity is unreadable.
|
|
1144
|
+
*
|
|
1145
|
+
* @param value - The candidate snapshot, which may be any unknown value
|
|
1146
|
+
* @returns The nearest identifying record naming the offending `phase` and `task`, or `undefined`
|
|
1147
|
+
* when no inconsistent node is identifiable
|
|
1148
|
+
*/
|
|
1149
|
+
function scanSnapshotContext(value) {
|
|
816
1150
|
if (!isRecord(value) || !isArray(value.phases)) return void 0;
|
|
817
1151
|
for (const phase of value.phases) {
|
|
818
1152
|
if (!isRecord(phase)) continue;
|
|
@@ -820,7 +1154,7 @@ function workflowSnapshotContext(value) {
|
|
|
820
1154
|
if (!isBoolean(phase.bail) || phase.concurrency !== void 0 && (!isInteger(phase.concurrency) || phase.concurrency < 1) || !isArray(phase.tasks)) return phaseContext;
|
|
821
1155
|
for (const task of phase.tasks) {
|
|
822
1156
|
if (!isRecord(task)) continue;
|
|
823
|
-
if (task.
|
|
1157
|
+
if (task.behavior !== void 0 && !isNonEmptyString(task.behavior) || task.retries !== void 0 && (!isInteger(task.retries) || task.retries < 0) || task.timeout !== void 0 && (!isInteger(task.timeout) || task.timeout < 0 || task.timeout > 2147483647) || !isInteger(task.attempts) || task.attempts < 0) return {
|
|
824
1158
|
...phaseContext ?? {},
|
|
825
1159
|
...isNonEmptyString(task.id) ? { task: task.id } : {}
|
|
826
1160
|
};
|
|
@@ -828,7 +1162,7 @@ function workflowSnapshotContext(value) {
|
|
|
828
1162
|
}
|
|
829
1163
|
}
|
|
830
1164
|
/**
|
|
831
|
-
*
|
|
1165
|
+
* Flattens a nested list of per-phase {@link TaskResult} lists into one positional list
|
|
832
1166
|
* — the workflow tier of the result tree, built from each phase's `results()`.
|
|
833
1167
|
*
|
|
834
1168
|
* @remarks
|
|
@@ -843,16 +1177,15 @@ function collectResults(phases) {
|
|
|
843
1177
|
return phases.flat();
|
|
844
1178
|
}
|
|
845
1179
|
/**
|
|
846
|
-
*
|
|
1180
|
+
* Inserts one `[key, value]` entry at a positional index into a readonly entries array —
|
|
847
1181
|
* the pure splice-in step behind an insertion-ordered registry's `add`.
|
|
848
1182
|
*
|
|
849
1183
|
* @remarks
|
|
850
|
-
*
|
|
851
|
-
*
|
|
852
|
-
*
|
|
853
|
-
*
|
|
854
|
-
*
|
|
855
|
-
* mutate `entries`; returns a new array.
|
|
1184
|
+
* Used by the shared {@link import('./Collection.js').Collection} store both managers hold: it
|
|
1185
|
+
* converts its insertion-ordered `Map` to `[...map.entries()]`, calls this to splice the new entry
|
|
1186
|
+
* in at the target index, then rebuilds the `Map` from the result (a stateful step that stays a
|
|
1187
|
+
* `#` private method — this helper does no `Map` construction). Does not mutate `entries`;
|
|
1188
|
+
* returns a new array.
|
|
856
1189
|
*
|
|
857
1190
|
* @typeParam T - The entry's value type
|
|
858
1191
|
* @param entries - The current positional entries, in order
|
|
@@ -872,16 +1205,17 @@ function insertEntry(entries, index, key, value) {
|
|
|
872
1205
|
return next;
|
|
873
1206
|
}
|
|
874
1207
|
/**
|
|
875
|
-
*
|
|
1208
|
+
* Repositions the entry keyed `key` to a new positional index in a readonly entries
|
|
876
1209
|
* array — the pure remove-then-reinsert step behind an insertion-ordered registry's
|
|
877
1210
|
* `move`.
|
|
878
1211
|
*
|
|
879
1212
|
* @remarks
|
|
880
1213
|
* The move counterpart of {@link insertEntry}: finds the entry by `key`, splices it
|
|
881
1214
|
* out, then splices it back in at `index`. An absent `key` is a no-op (returns a copy
|
|
882
|
-
* of `entries` unchanged) — the caller
|
|
883
|
-
* already gates on the target's
|
|
884
|
-
*
|
|
1215
|
+
* of `entries` unchanged) — the caller, the shared
|
|
1216
|
+
* {@link import('./Collection.js').Collection} store's `move`, already gates on the target's
|
|
1217
|
+
* existence before calling this, so the no-op branch is defensive, never reached in practice.
|
|
1218
|
+
* Does not mutate `entries`; returns a new array.
|
|
885
1219
|
*
|
|
886
1220
|
* @typeParam T - The entry's value type
|
|
887
1221
|
* @param entries - The current positional entries, in order
|
|
@@ -903,17 +1237,7 @@ function moveEntry(entries, key, index) {
|
|
|
903
1237
|
return next;
|
|
904
1238
|
}
|
|
905
1239
|
/**
|
|
906
|
-
*
|
|
907
|
-
* externally, so a caller can resolve/reject it from outside the executor.
|
|
908
|
-
*
|
|
909
|
-
* @typeParam T - The value the deferred promise resolves
|
|
910
|
-
* @returns A deferred `promise` plus its `resolve` / `reject`
|
|
911
|
-
*/
|
|
912
|
-
function createDeferred() {
|
|
913
|
-
return Promise.withResolvers();
|
|
914
|
-
}
|
|
915
|
-
/**
|
|
916
|
-
* Schedule one cancellable host operation behind an owned settlement signal.
|
|
1240
|
+
* Schedules one cancellable host operation behind an owned settlement signal.
|
|
917
1241
|
*
|
|
918
1242
|
* @remarks
|
|
919
1243
|
* A defined `signal` that is not a native `AbortSignal` is refused before anything is armed, as a
|
|
@@ -980,7 +1304,36 @@ function scheduleHost(start, signal) {
|
|
|
980
1304
|
});
|
|
981
1305
|
}
|
|
982
1306
|
/**
|
|
983
|
-
*
|
|
1307
|
+
* Schedules the shared host timer boundary every scheduler backend resumes from.
|
|
1308
|
+
*
|
|
1309
|
+
* @remarks
|
|
1310
|
+
* The one `setTimeout` / `clearTimeout` boundary in the package: the cross-environment
|
|
1311
|
+
* {@link import('./Scheduler.js').Scheduler}, both Node primitives, and every browser backend's
|
|
1312
|
+
* `delay` and macrotask fallback route here, so the timer is armed and cleared in one place. It
|
|
1313
|
+
* composes {@link scheduleHost}, which owns listener safety, the cancellation race, the exact
|
|
1314
|
+
* caller reason, and once-only settlement. It does NOT validate `ms`: the value passes straight to
|
|
1315
|
+
* the host `setTimeout`, which clamps a negative value or `NaN` to about zero, so an
|
|
1316
|
+
* out-of-domain `ms` resumes on the next host turn rather than throwing. Pass a non-negative
|
|
1317
|
+
* finite `ms`.
|
|
1318
|
+
*
|
|
1319
|
+
* @param ms - The milliseconds to wait before resuming
|
|
1320
|
+
* @param signal - Optional caller cancellation signal
|
|
1321
|
+
* @returns A promise that resolves after `ms`, or rejects with the caller's exact abort reason
|
|
1322
|
+
*
|
|
1323
|
+
* @example
|
|
1324
|
+
* ```ts
|
|
1325
|
+
* const controller = new AbortController()
|
|
1326
|
+
* await delayHost(0, controller.signal) // a real macrotask host turn
|
|
1327
|
+
* ```
|
|
1328
|
+
*/
|
|
1329
|
+
function delayHost(ms, signal) {
|
|
1330
|
+
return scheduleHost((complete) => {
|
|
1331
|
+
const handle = setTimeout(complete, ms);
|
|
1332
|
+
return () => clearTimeout(handle);
|
|
1333
|
+
}, signal);
|
|
1334
|
+
}
|
|
1335
|
+
/**
|
|
1336
|
+
* Parks until `signal` aborts — a promise-parked wait, never a timer or
|
|
984
1337
|
* busy-loop, that NEVER rejects.
|
|
985
1338
|
*
|
|
986
1339
|
* @remarks
|
|
@@ -990,7 +1343,7 @@ function scheduleHost(start, signal) {
|
|
|
990
1343
|
* at every fold point.
|
|
991
1344
|
*
|
|
992
1345
|
* @param signal - The signal to park on
|
|
993
|
-
* @returns A promise that resolves
|
|
1346
|
+
* @returns A promise that resolves after `signal` has aborted
|
|
994
1347
|
*
|
|
995
1348
|
* @example
|
|
996
1349
|
* ```ts
|
|
@@ -1007,9 +1360,109 @@ function parkSignal(signal) {
|
|
|
1007
1360
|
});
|
|
1008
1361
|
}
|
|
1009
1362
|
//#endregion
|
|
1363
|
+
//#region src/core/Collection.ts
|
|
1364
|
+
/**
|
|
1365
|
+
* Implements the insertion-ordered gated store both lean managers hold — entities keyed by `id`,
|
|
1366
|
+
* positional order preserved across an interior `skip` or `remove`.
|
|
1367
|
+
*
|
|
1368
|
+
* @remarks
|
|
1369
|
+
* - **One engine, two managers.** {@link import('./tasks/TaskManager.js').TaskManager} and
|
|
1370
|
+
* {@link import('./phases/PhaseManager.js').PhaseManager} differ only in the entity noun and the
|
|
1371
|
+
* patch shape they validate, so both hold one of these and add only their domain accessors
|
|
1372
|
+
* (`task` / `tasks`, `phase` / `phases`). The `Map`'s insertion order is the single source of
|
|
1373
|
+
* positional truth; `add` and `move` rebuild it through the pure
|
|
1374
|
+
* {@link import('./helpers.js').insertEntry} / {@link import('./helpers.js').moveEntry} leaves.
|
|
1375
|
+
* - **Gated mutation API.** `append` is the build-time wiring path and THROWS on a
|
|
1376
|
+
* duplicate id; `add` / `remove` / `move` / `update` return a graceful `MUTATION`
|
|
1377
|
+
* {@link WorkflowError} failure instead. Gating reads ONLY the target's own existence, `pending`
|
|
1378
|
+
* status, id, and bounds — a container's own status is the owning entity's gate, applied before
|
|
1379
|
+
* it delegates here.
|
|
1380
|
+
* - **Event-free.** A purely structural container; the entity that owns it emits on success.
|
|
1381
|
+
*
|
|
1382
|
+
* @typeParam TEntry - The stored entity
|
|
1383
|
+
* @typeParam TPatch - The declarative partial update `update` validates and applies
|
|
1384
|
+
*
|
|
1385
|
+
* @example
|
|
1386
|
+
* ```ts
|
|
1387
|
+
* import { compileGuard } from '@orkestrel/contract'
|
|
1388
|
+
* import { Collection, taskUpdateShape } from '@orkestrel/workflow'
|
|
1389
|
+
* import type { TaskInterface, TaskUpdate } from '@orkestrel/workflow'
|
|
1390
|
+
*
|
|
1391
|
+
* const tasks = new Collection<TaskInterface, TaskUpdate>('task', compileGuard(taskUpdateShape))
|
|
1392
|
+
* tasks.append(task) // a live Task
|
|
1393
|
+
* tasks.entry(task.id) // the same task
|
|
1394
|
+
* tasks.entries() // [task]
|
|
1395
|
+
* tasks.count // 1
|
|
1396
|
+
* tasks.add(other, 0) // Result — inserted first
|
|
1397
|
+
* tasks.move(other.id, 1) // Result — repositioned
|
|
1398
|
+
* tasks.update(task.id, { name: 'Renamed task' }) // Result — patched
|
|
1399
|
+
* tasks.remove(other.id) // Result — dropped
|
|
1400
|
+
* ```
|
|
1401
|
+
*/
|
|
1402
|
+
var Collection = class {
|
|
1403
|
+
#entries = /* @__PURE__ */ new Map();
|
|
1404
|
+
#noun;
|
|
1405
|
+
#isPatch;
|
|
1406
|
+
constructor(noun, patch) {
|
|
1407
|
+
this.#noun = noun;
|
|
1408
|
+
this.#isPatch = patch;
|
|
1409
|
+
}
|
|
1410
|
+
get count() {
|
|
1411
|
+
return this.#entries.size;
|
|
1412
|
+
}
|
|
1413
|
+
append(entry) {
|
|
1414
|
+
if (this.#entries.has(entry.id)) throw new WorkflowError("MUTATION", `duplicate ${this.#noun} id '${entry.id}'`, { id: entry.id });
|
|
1415
|
+
this.#entries.set(entry.id, entry);
|
|
1416
|
+
}
|
|
1417
|
+
add(entry, index) {
|
|
1418
|
+
if (this.#entries.has(entry.id)) return failure(new WorkflowError("MUTATION", `duplicate ${this.#noun} id '${entry.id}'`, { id: entry.id }));
|
|
1419
|
+
const at = index ?? this.#entries.size;
|
|
1420
|
+
if (at < 0 || at > this.#entries.size) return failure(new WorkflowError("MUTATION", `index '${at}' out of bounds`, { index: at }));
|
|
1421
|
+
this.#reorder(insertEntry([...this.#entries.entries()], at, entry.id, entry));
|
|
1422
|
+
return success(entry);
|
|
1423
|
+
}
|
|
1424
|
+
remove(id) {
|
|
1425
|
+
const target = this.#pending(id);
|
|
1426
|
+
if (target === void 0) return this.#refuse(id);
|
|
1427
|
+
this.#entries.delete(id);
|
|
1428
|
+
return success(target);
|
|
1429
|
+
}
|
|
1430
|
+
move(id, index) {
|
|
1431
|
+
const target = this.#pending(id);
|
|
1432
|
+
if (target === void 0) return this.#refuse(id);
|
|
1433
|
+
if (index < 0 || index >= this.#entries.size) return failure(new WorkflowError("MUTATION", `index '${index}' out of bounds`, { index }));
|
|
1434
|
+
this.#reorder(moveEntry([...this.#entries.entries()], id, index));
|
|
1435
|
+
return success(target);
|
|
1436
|
+
}
|
|
1437
|
+
update(id, patch) {
|
|
1438
|
+
const target = this.#pending(id);
|
|
1439
|
+
if (target === void 0) return this.#refuse(id);
|
|
1440
|
+
if (!this.#isPatch(patch)) return failure(new WorkflowError("MUTATION", `invalid patch for ${this.#noun} '${id}'`, { id }));
|
|
1441
|
+
target.patch(patch);
|
|
1442
|
+
return success(target);
|
|
1443
|
+
}
|
|
1444
|
+
entry(id) {
|
|
1445
|
+
return this.#entries.get(id);
|
|
1446
|
+
}
|
|
1447
|
+
entries() {
|
|
1448
|
+
return [...this.#entries.values()];
|
|
1449
|
+
}
|
|
1450
|
+
#pending(id) {
|
|
1451
|
+
const target = this.#entries.get(id);
|
|
1452
|
+
return target === void 0 || target.status !== "pending" ? void 0 : target;
|
|
1453
|
+
}
|
|
1454
|
+
#refuse(id) {
|
|
1455
|
+
return failure(new WorkflowError("MUTATION", `${this.#noun} '${id}' is not a pending ${this.#noun}`, { id }));
|
|
1456
|
+
}
|
|
1457
|
+
#reorder(entries) {
|
|
1458
|
+
this.#entries.clear();
|
|
1459
|
+
for (const [key, value] of entries) this.#entries.set(key, value);
|
|
1460
|
+
}
|
|
1461
|
+
};
|
|
1462
|
+
//#endregion
|
|
1010
1463
|
//#region src/core/Scheduler.ts
|
|
1011
1464
|
/**
|
|
1012
|
-
*
|
|
1465
|
+
* Implements the safe cross-environment cooperative-yield default — a {@link SchedulerInterface}
|
|
1013
1466
|
* built on `setTimeout` / `clearTimeout` alone, so it runs unchanged in both the
|
|
1014
1467
|
* browser and Node.
|
|
1015
1468
|
*
|
|
@@ -1025,7 +1478,7 @@ function parkSignal(signal) {
|
|
|
1025
1478
|
* rendering run — it only defers within the current task. A zero-delay timer is
|
|
1026
1479
|
* the correct cross-environment "give the host a turn".
|
|
1027
1480
|
* - **Abort-aware.** A pending `yield` / `delay` rejects with `signal.reason` exactly.
|
|
1028
|
-
* {@link
|
|
1481
|
+
* {@link delayHost} links an owned settlement composite to the caller before arming
|
|
1029
1482
|
* the timer, so pre-abort schedules nothing, caller signal method mutation is harmless,
|
|
1030
1483
|
* cancellation clears the handle, and native first-settlement wins exactly once.
|
|
1031
1484
|
* - **Priority is accepted but uniform.** `options.priority` is part of the
|
|
@@ -1044,36 +1497,30 @@ function parkSignal(signal) {
|
|
|
1044
1497
|
*/
|
|
1045
1498
|
var Scheduler = class {
|
|
1046
1499
|
/**
|
|
1047
|
-
*
|
|
1048
|
-
* run, then
|
|
1500
|
+
* Yields control back to the host so other tasks (I/O, timers, rendering) can
|
|
1501
|
+
* run, then resumes — a macrotask turn through `setTimeout(0)` (NOT a microtask,
|
|
1049
1502
|
* which would resume before the host regains control).
|
|
1050
1503
|
*/
|
|
1051
1504
|
yield(options) {
|
|
1052
|
-
return
|
|
1505
|
+
return delayHost(0, options?.signal);
|
|
1053
1506
|
}
|
|
1054
1507
|
/**
|
|
1055
|
-
*
|
|
1508
|
+
* Resumes after at least `ms` milliseconds; abort rejects with `signal.reason`.
|
|
1056
1509
|
*
|
|
1057
1510
|
* @remarks
|
|
1058
|
-
*
|
|
1059
|
-
*
|
|
1060
|
-
*
|
|
1061
|
-
*
|
|
1511
|
+
* Pass a non-negative finite `ms`. The primitive stays minimal and does no
|
|
1512
|
+
* validation: it passes `ms` straight to the host `setTimeout`, which clamps a
|
|
1513
|
+
* negative value or `NaN` to ~0 — so an out-of-domain `ms` resolves on the next
|
|
1514
|
+
* host turn rather than throwing.
|
|
1062
1515
|
*/
|
|
1063
1516
|
delay(ms, options) {
|
|
1064
|
-
return
|
|
1065
|
-
}
|
|
1066
|
-
#sleep(ms, signal) {
|
|
1067
|
-
return scheduleHost((complete) => {
|
|
1068
|
-
const handle = setTimeout(complete, ms);
|
|
1069
|
-
return () => clearTimeout(handle);
|
|
1070
|
-
}, signal);
|
|
1517
|
+
return delayHost(ms, options?.signal);
|
|
1071
1518
|
}
|
|
1072
1519
|
};
|
|
1073
1520
|
//#endregion
|
|
1074
1521
|
//#region src/core/cloners.ts
|
|
1075
1522
|
/**
|
|
1076
|
-
*
|
|
1523
|
+
* Validates and owns a workflow snapshot before live construction.
|
|
1077
1524
|
*
|
|
1078
1525
|
* @param input - The hostile snapshot boundary
|
|
1079
1526
|
* @param id - The optional storage key the owned snapshot must match
|
|
@@ -1089,7 +1536,7 @@ function cloneWorkflowSnapshot(input, id) {
|
|
|
1089
1536
|
if (isContractError(error)) throw new WorkflowError("RESTORE", `workflow snapshot could not be read safely: ${error.message}`);
|
|
1090
1537
|
throw new WorkflowError("RESTORE", "workflow snapshot could not be read safely");
|
|
1091
1538
|
}
|
|
1092
|
-
if (!isOwnedWorkflowSnapshot(cloned)) throw new WorkflowError("RESTORE", "workflow snapshot is inconsistent",
|
|
1539
|
+
if (!isOwnedWorkflowSnapshot(cloned)) throw new WorkflowError("RESTORE", "workflow snapshot is inconsistent", scanSnapshotContext(cloned));
|
|
1093
1540
|
if (id !== void 0 && cloned.id !== id) throw new WorkflowError("RESTORE", `workflow snapshot '${cloned.id}' does not match storage key '${id}'`, {
|
|
1094
1541
|
requested: id,
|
|
1095
1542
|
payload: cloned.id
|
|
@@ -1097,7 +1544,49 @@ function cloneWorkflowSnapshot(input, id) {
|
|
|
1097
1544
|
return cloned;
|
|
1098
1545
|
}
|
|
1099
1546
|
/**
|
|
1100
|
-
*
|
|
1547
|
+
* Validates and owns one list of task activity claims.
|
|
1548
|
+
*
|
|
1549
|
+
* @remarks
|
|
1550
|
+
* The one cloner behind both claim lists of a task activity frame — its `operations` and its
|
|
1551
|
+
* `constraints` — because {@link import('./types.js').TaskOperation} and
|
|
1552
|
+
* {@link import('./types.js').TaskConstraint} are the same {@link import('./types.js').TaskClaim}
|
|
1553
|
+
* shape. An omitted
|
|
1554
|
+
* list is an empty one. Each member is read exactly once inside the caller's protected boundary
|
|
1555
|
+
* and returned frozen; the semantic pass over the copied values is
|
|
1556
|
+
* {@link import('./validators.js').isTaskClaimList}, so this cloner refuses only what it cannot
|
|
1557
|
+
* read: a non-array list, a non-record member, a hostile prototype, or an unexpected key.
|
|
1558
|
+
*
|
|
1559
|
+
* @param input - The untrusted claim list
|
|
1560
|
+
* @param noun - The singular claim noun the refusal message names, pluralized by adding `s`
|
|
1561
|
+
* @returns The owned frozen claims, in input order
|
|
1562
|
+
* @throws {WorkflowError} With `MUTATION` when the list or one of its members cannot be read
|
|
1563
|
+
*
|
|
1564
|
+
* @example
|
|
1565
|
+
* ```ts
|
|
1566
|
+
* cloneTaskClaims([{ id: 'fetch', name: 'Fetch', started: 1 }], 'operation')
|
|
1567
|
+
* ```
|
|
1568
|
+
*/
|
|
1569
|
+
function cloneTaskClaims(input, noun) {
|
|
1570
|
+
const inputs = input === void 0 ? [] : isArray(input) ? [...input] : void 0;
|
|
1571
|
+
if (inputs === void 0) throw new WorkflowError("MUTATION", `task activity ${noun}s must be an array`);
|
|
1572
|
+
const claims = [];
|
|
1573
|
+
for (const claim of inputs) {
|
|
1574
|
+
if (!isRecord(claim)) throw new WorkflowError("MUTATION", `task activity contains an invalid ${noun}`);
|
|
1575
|
+
const prototype = Object.getPrototypeOf(claim);
|
|
1576
|
+
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}`);
|
|
1577
|
+
const id = claim.id;
|
|
1578
|
+
const name = claim.name;
|
|
1579
|
+
const started = claim.started;
|
|
1580
|
+
claims.push(Object.freeze({
|
|
1581
|
+
id,
|
|
1582
|
+
name,
|
|
1583
|
+
started
|
|
1584
|
+
}));
|
|
1585
|
+
}
|
|
1586
|
+
return claims;
|
|
1587
|
+
}
|
|
1588
|
+
/**
|
|
1589
|
+
* Validates and clones one complete task activity frame.
|
|
1101
1590
|
*
|
|
1102
1591
|
* @remarks
|
|
1103
1592
|
* This is the hostile boundary behind task reports and snapshot hydration. Supplying
|
|
@@ -1121,22 +1610,7 @@ function cloneTaskActivity(input, updated) {
|
|
|
1121
1610
|
const operationsInput = input.operations;
|
|
1122
1611
|
const constraintsInput = input.constraints;
|
|
1123
1612
|
const accepted = updated === void 0 ? input.updated : updated;
|
|
1124
|
-
const
|
|
1125
|
-
if (operationInputs === void 0) throw new WorkflowError("MUTATION", "task activity operations must be an array");
|
|
1126
|
-
const operations = [];
|
|
1127
|
-
for (const operation of operationInputs) {
|
|
1128
|
-
if (!isRecord(operation)) throw new WorkflowError("MUTATION", "task activity contains an invalid operation");
|
|
1129
|
-
const operationPrototype = Object.getPrototypeOf(operation);
|
|
1130
|
-
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");
|
|
1131
|
-
const id = operation.id;
|
|
1132
|
-
const name = operation.name;
|
|
1133
|
-
const started = operation.started;
|
|
1134
|
-
operations.push(Object.freeze({
|
|
1135
|
-
id,
|
|
1136
|
-
name,
|
|
1137
|
-
started
|
|
1138
|
-
}));
|
|
1139
|
-
}
|
|
1613
|
+
const operations = cloneTaskClaims(operationsInput, "operation");
|
|
1140
1614
|
let progress;
|
|
1141
1615
|
if (progressInput !== void 0) {
|
|
1142
1616
|
if (!isRecord(progressInput)) throw new WorkflowError("MUTATION", "task activity contains invalid progress");
|
|
@@ -1151,22 +1625,7 @@ function cloneTaskActivity(input, updated) {
|
|
|
1151
1625
|
...message === void 0 ? {} : { message }
|
|
1152
1626
|
});
|
|
1153
1627
|
}
|
|
1154
|
-
const
|
|
1155
|
-
if (constraintInputs === void 0) throw new WorkflowError("MUTATION", "task activity constraints must be an array");
|
|
1156
|
-
const constraints = [];
|
|
1157
|
-
for (const constraint of constraintInputs) {
|
|
1158
|
-
if (!isRecord(constraint)) throw new WorkflowError("MUTATION", "task activity contains an invalid constraint");
|
|
1159
|
-
const constraintPrototype = Object.getPrototypeOf(constraint);
|
|
1160
|
-
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");
|
|
1161
|
-
const id = constraint.id;
|
|
1162
|
-
const name = constraint.name;
|
|
1163
|
-
const started = constraint.started;
|
|
1164
|
-
constraints.push(Object.freeze({
|
|
1165
|
-
id,
|
|
1166
|
-
name,
|
|
1167
|
-
started
|
|
1168
|
-
}));
|
|
1169
|
-
}
|
|
1628
|
+
const constraints = cloneTaskClaims(constraintsInput, "constraint");
|
|
1170
1629
|
const activity = Object.freeze({
|
|
1171
1630
|
...note === void 0 ? {} : { note },
|
|
1172
1631
|
...progress === void 0 ? {} : { progress },
|
|
@@ -1184,8 +1643,8 @@ function cloneTaskActivity(input, updated) {
|
|
|
1184
1643
|
//#endregion
|
|
1185
1644
|
//#region src/core/shapers.ts
|
|
1186
1645
|
/**
|
|
1187
|
-
*
|
|
1188
|
-
* `
|
|
1646
|
+
* Describes the shape of a {@link import('./types.js').TaskDefinition} — identity plus an optional
|
|
1647
|
+
* `behavior` behavior reference (a plain registry-key string, min length 1). `description` is
|
|
1189
1648
|
* optional prose.
|
|
1190
1649
|
*/
|
|
1191
1650
|
var taskShape = objectShape({
|
|
@@ -1198,7 +1657,7 @@ var taskShape = objectShape({
|
|
|
1198
1657
|
description: "Human-readable task name."
|
|
1199
1658
|
}),
|
|
1200
1659
|
description: optionalShape(stringShape({ description: "Optional task description." })),
|
|
1201
|
-
|
|
1660
|
+
behavior: optionalShape(stringShape({
|
|
1202
1661
|
min: 1,
|
|
1203
1662
|
description: "The registered behavior name to invoke (a registry key, not a label); omitted has no handler."
|
|
1204
1663
|
})),
|
|
@@ -1213,7 +1672,7 @@ var taskShape = objectShape({
|
|
|
1213
1672
|
}))
|
|
1214
1673
|
});
|
|
1215
1674
|
/**
|
|
1216
|
-
*
|
|
1675
|
+
* Describes the shape of a {@link import('./types.js').PhaseDefinition} — identity, its ordered
|
|
1217
1676
|
* {@link taskShape} tasks, and an optional positive-integer `concurrency` throttle
|
|
1218
1677
|
* (max tasks in flight; omitted ⇒ unbounded).
|
|
1219
1678
|
*/
|
|
@@ -1235,7 +1694,7 @@ var phaseShape = objectShape({
|
|
|
1235
1694
|
bail: optionalShape(literalShape([true, false], { description: "Per-phase failure-policy override; omitted inherits the workflow bail." }))
|
|
1236
1695
|
});
|
|
1237
1696
|
/**
|
|
1238
|
-
*
|
|
1697
|
+
* Describes the shape of a {@link import('./types.js').WorkflowDefinition} — the contract root:
|
|
1239
1698
|
* identity, its ordered {@link phaseShape} phases, and the optional `bail` boolean
|
|
1240
1699
|
* failure policy (the literal pair `true`/`false`, the runtime mirror of the boolean
|
|
1241
1700
|
* toggle; omitted ⇒ the graceful default).
|
|
@@ -1254,13 +1713,13 @@ var workflowShape = objectShape({
|
|
|
1254
1713
|
bail: optionalShape(literalShape([true, false], { description: "Failure policy: false (default) continues gracefully, true halts on the first failure." }))
|
|
1255
1714
|
});
|
|
1256
1715
|
/**
|
|
1257
|
-
*
|
|
1716
|
+
* Describes the shape of a {@link import('./types.js').TaskUpdate} — a partial edit to a
|
|
1258
1717
|
* `pending` task's `name` / `description`, both optional.
|
|
1259
1718
|
*
|
|
1260
1719
|
* @remarks
|
|
1261
1720
|
* Mirrors {@link taskShape}'s `name` / `description` constraints exactly (a provided
|
|
1262
|
-
* `name` still has `minLength: 1`); never `id` / `
|
|
1263
|
-
* are not patchable fields
|
|
1721
|
+
* `name` still has `minLength: 1`); never `id` / `behavior` / `retries` / `timeout` (those
|
|
1722
|
+
* are not patchable fields).
|
|
1264
1723
|
*/
|
|
1265
1724
|
var taskUpdateShape = objectShape({
|
|
1266
1725
|
name: optionalShape(stringShape({
|
|
@@ -1270,13 +1729,13 @@ var taskUpdateShape = objectShape({
|
|
|
1270
1729
|
description: optionalShape(stringShape({ description: "New task description." }))
|
|
1271
1730
|
});
|
|
1272
1731
|
/**
|
|
1273
|
-
*
|
|
1732
|
+
* Describes the shape of a {@link import('./types.js').PhaseUpdate} — a partial edit to a
|
|
1274
1733
|
* `pending` phase's `name` / `description` / `concurrency` / `bail`, all optional.
|
|
1275
1734
|
*
|
|
1276
1735
|
* @remarks
|
|
1277
1736
|
* Mirrors {@link phaseShape}'s corresponding field constraints exactly; never `id` /
|
|
1278
1737
|
* `tasks` (structural children change through the phase's own `add` / `remove` /
|
|
1279
|
-
* `move`, not a patch
|
|
1738
|
+
* `move`, not a patch).
|
|
1280
1739
|
*/
|
|
1281
1740
|
var phaseUpdateShape = objectShape({
|
|
1282
1741
|
name: optionalShape(stringShape({
|
|
@@ -1293,7 +1752,7 @@ var phaseUpdateShape = objectShape({
|
|
|
1293
1752
|
//#endregion
|
|
1294
1753
|
//#region src/core/stores/DatabaseWorkflowStore.ts
|
|
1295
1754
|
/**
|
|
1296
|
-
*
|
|
1755
|
+
* Implements a {@link WorkflowStoreInterface} backed by one table of the `databases` layer — a
|
|
1297
1756
|
* workflow's durable run-state IS a row, so persistence reduces to keyed point-access
|
|
1298
1757
|
* (`get` / `set` / `delete`) over a `TableInterface`, the driver-pluggable twin of the
|
|
1299
1758
|
* plain-`Map` {@link import('./MemoryWorkflowStore.js').MemoryWorkflowStore}.
|
|
@@ -1317,16 +1776,18 @@ var phaseUpdateShape = objectShape({
|
|
|
1317
1776
|
*
|
|
1318
1777
|
* - **`set(snapshot)` upserts under the snapshot's OWN `id`** (no separate id param) — it writes
|
|
1319
1778
|
* the row `{ id: snapshot.id, snapshot }`.
|
|
1320
|
-
* - **`get(id)` resolves the stored snapshot for an id**, narrowing the opaque JSON
|
|
1321
|
-
* a {@link WorkflowSnapshot}
|
|
1322
|
-
*
|
|
1323
|
-
*
|
|
1779
|
+
* - **`get(id)` resolves the stored snapshot for an id**, owning and narrowing the opaque JSON
|
|
1780
|
+
* column back to a {@link WorkflowSnapshot} through
|
|
1781
|
+
* {@link import('../cloners.js').cloneWorkflowSnapshot}, whose semantic pass is
|
|
1782
|
+
* {@link import('../validators.js').isOwnedWorkflowSnapshot} — the boundary narrow for
|
|
1783
|
+
* an untrusted storage read — or `undefined` if none is stored. A present snapshot whose own id
|
|
1784
|
+
* differs from the requested key rejects with normalized `RESTORE` evidence.
|
|
1324
1785
|
* - **`delete(id)` drops a snapshot by id**; an absent id is a no-op (no throw).
|
|
1325
1786
|
*
|
|
1326
1787
|
* UNLIKE the server package's `SessionStoreInterface` there is NO
|
|
1327
1788
|
* idle-TTL / eviction — a persisted run-state is durable orchestration state that lives until an
|
|
1328
1789
|
* explicit `delete`. The public surface is EXACTLY `get` / `set` / `delete` — no extra members (the
|
|
1329
|
-
*
|
|
1790
|
+
* guide's method bijection with {@link WorkflowStoreInterface}). Restore stays a caller concern: read a
|
|
1330
1791
|
* snapshot back and rebuild the live tree with {@link import('../factories.js').createRestoredWorkflow}.
|
|
1331
1792
|
*
|
|
1332
1793
|
* @example
|
|
@@ -1345,7 +1806,7 @@ var phaseUpdateShape = objectShape({
|
|
|
1345
1806
|
var DatabaseWorkflowStore = class {
|
|
1346
1807
|
#table;
|
|
1347
1808
|
/**
|
|
1348
|
-
*
|
|
1809
|
+
* Wraps a table as a workflow store.
|
|
1349
1810
|
*
|
|
1350
1811
|
* @param table - The {@link TableInterface} holding the snapshots — its row is the
|
|
1351
1812
|
* {@link WorkflowSnapshotRow} `{ id; snapshot }` shape (the snapshot one opaque JSON column)
|
|
@@ -1353,13 +1814,13 @@ var DatabaseWorkflowStore = class {
|
|
|
1353
1814
|
constructor(table) {
|
|
1354
1815
|
this.#table = table;
|
|
1355
1816
|
}
|
|
1356
|
-
/**
|
|
1817
|
+
/** Resolves and key-checks the snapshot for `id`, narrowing the opaque column to `WorkflowSnapshot`. */
|
|
1357
1818
|
async get(id) {
|
|
1358
1819
|
const row = await this.#table.get(id);
|
|
1359
1820
|
if (row === void 0) return void 0;
|
|
1360
1821
|
return cloneWorkflowSnapshot(row.snapshot, id);
|
|
1361
1822
|
}
|
|
1362
|
-
/**
|
|
1823
|
+
/** Inserts or replaces under the snapshot's OWN `id` (no separate id param) — the row is `{ id, snapshot }`. */
|
|
1363
1824
|
async set(snapshot) {
|
|
1364
1825
|
const owned = cloneWorkflowSnapshot(snapshot);
|
|
1365
1826
|
await this.#table.set({
|
|
@@ -1367,7 +1828,7 @@ var DatabaseWorkflowStore = class {
|
|
|
1367
1828
|
snapshot: owned
|
|
1368
1829
|
});
|
|
1369
1830
|
}
|
|
1370
|
-
/**
|
|
1831
|
+
/** Drops a snapshot by id; an absent id is a no-op (no throw). */
|
|
1371
1832
|
async delete(id) {
|
|
1372
1833
|
await this.#table.remove(id);
|
|
1373
1834
|
}
|
|
@@ -1375,12 +1836,12 @@ var DatabaseWorkflowStore = class {
|
|
|
1375
1836
|
//#endregion
|
|
1376
1837
|
//#region src/core/stores/MemoryWorkflowStore.ts
|
|
1377
1838
|
/**
|
|
1378
|
-
*
|
|
1839
|
+
* Implements the in-memory {@link WorkflowStoreInterface} — a process-lifetime `Map` of
|
|
1379
1840
|
* {@link WorkflowSnapshot}s keyed by workflow id, the DEFAULT store
|
|
1380
1841
|
* {@link import('../factories.js').createMemoryWorkflowStore} builds.
|
|
1381
1842
|
*
|
|
1382
1843
|
* @remarks
|
|
1383
|
-
* A plain `Map<string, WorkflowSnapshot>` (
|
|
1844
|
+
* A plain `Map<string, WorkflowSnapshot>` (the snapshot is already pure,
|
|
1384
1845
|
* self-contained JSON, so no encoding is needed for the memory tier). UNLIKE the server
|
|
1385
1846
|
* package's `SessionStoreInterface`'s memory store there is
|
|
1386
1847
|
* NO idle-TTL and NO eviction: a persisted workflow run-state is durable orchestration state
|
|
@@ -1396,7 +1857,7 @@ var DatabaseWorkflowStore = class {
|
|
|
1396
1857
|
* - **`set` inserts / replaces under the snapshot's OWN `id`** (no separate id param).
|
|
1397
1858
|
* - **`delete` drops a snapshot by id**; an absent id is a no-op (no throw).
|
|
1398
1859
|
*
|
|
1399
|
-
* The public surface is EXACTLY `get` / `set` / `delete` — no extra members (the
|
|
1860
|
+
* The public surface is EXACTLY `get` / `set` / `delete` — no extra members (the guide's method
|
|
1400
1861
|
* bijection with {@link WorkflowStoreInterface}). Restore is a caller concern: read a snapshot
|
|
1401
1862
|
* back and rebuild the live tree with {@link import('../factories.js').createRestoredWorkflow}.
|
|
1402
1863
|
*
|
|
@@ -1431,18 +1892,18 @@ var MemoryWorkflowStore = class {
|
|
|
1431
1892
|
//#endregion
|
|
1432
1893
|
//#region src/core/tasks/Task.ts
|
|
1433
1894
|
/**
|
|
1434
|
-
*
|
|
1435
|
-
* synchronous task whose explicit {@link
|
|
1895
|
+
* Implements the live leaf state machine (W-b) for one task — an observable, guarded
|
|
1896
|
+
* synchronous task whose explicit {@link LifecycleStatus} advances through the declared
|
|
1436
1897
|
* transitions, recording a {@link TaskResult} on a terminal outcome.
|
|
1437
1898
|
*
|
|
1438
1899
|
* @remarks
|
|
1439
|
-
* - **Guarded transitions
|
|
1900
|
+
* - **Guarded transitions.** `start` (→ `running`), then `complete(value)`
|
|
1440
1901
|
* (→ `completed`, records a {@link import('@orkestrel/contract').Success}), `fail(error)`
|
|
1441
1902
|
* (→ `failed`, records a {@link import('@orkestrel/contract').Failure}), `skip` (→ `skipped`),
|
|
1442
1903
|
* `stop` (→ `stopped`). Each consults {@link canTransitionTask} FIRST and throws a
|
|
1443
|
-
* `TRANSITION` {@link WorkflowError} on an illegal move (
|
|
1444
|
-
* task) — the legal graph is the single source of truth, so the leaf can never
|
|
1445
|
-
* impossible state.
|
|
1904
|
+
* `TRANSITION` {@link WorkflowError} on an illegal move (for example, completing a
|
|
1905
|
+
* non-`running` task) — the legal graph is the single source of truth, so the leaf can never
|
|
1906
|
+
* reach an impossible state.
|
|
1446
1907
|
* - **Snapshot fidelity.** A leaf needs no override: `skipped` / `stopped` are explicit terminal
|
|
1447
1908
|
* statuses, and restore reinstates the leaf directly from {@link TaskSnapshot.status}.
|
|
1448
1909
|
* - **The cascade.** Every status change records its boxed result (when any), fires the leaf's
|
|
@@ -1450,16 +1911,16 @@ var MemoryWorkflowStore = class {
|
|
|
1450
1911
|
* transition propagates UP (Task → Phase → Workflow re-derive). The own-event-before-cascade
|
|
1451
1912
|
* order means an observer sees the CAUSE (this leaf changed) before the EFFECT (the parents
|
|
1452
1913
|
* re-derive) — the project precedent (`Runner.#settle` emits its own `fail` before propagating).
|
|
1453
|
-
* - **Observable
|
|
1914
|
+
* - **Observable.** The owned {@link emitter} ({@link TaskEventMap}) fires the
|
|
1454
1915
|
* matching event strictly AFTER the state change, BEFORE the cascade; the emitter isolates
|
|
1455
1916
|
* a listener throw and routes it to its `error` handler (the `error` option), so a buggy
|
|
1456
1917
|
* observer can never corrupt a transition.
|
|
1457
|
-
* - **Declarative config
|
|
1918
|
+
* - **Declarative config.** `behavior` / `retries` / `timeout` PERSIST in a
|
|
1458
1919
|
* {@link TaskSnapshot} (like a phase's `bail` / `concurrency`), carried verbatim from the
|
|
1459
1920
|
* matching {@link import('../types.js').TaskDefinition} / {@link TaskSnapshot} field. `handler`
|
|
1460
|
-
* is the RUNTIME-ONLY counterpart — `
|
|
1921
|
+
* is the RUNTIME-ONLY counterpart — `behavior` resolved ONCE at construction against the
|
|
1461
1922
|
* workflow-level {@link import('../types.js').WorkflowOptions.functions} registry — and is
|
|
1462
|
-
* NEVER persisted; `undefined` when `
|
|
1923
|
+
* NEVER persisted; `undefined` when `behavior` is omitted or unregistered. Only omission is a
|
|
1463
1924
|
* deliberate no-op; unresolved named work is rejected before dispatch.
|
|
1464
1925
|
*/
|
|
1465
1926
|
var Task = class {
|
|
@@ -1472,7 +1933,8 @@ var Task = class {
|
|
|
1472
1933
|
#status;
|
|
1473
1934
|
#result;
|
|
1474
1935
|
#name;
|
|
1475
|
-
#
|
|
1936
|
+
#description;
|
|
1937
|
+
#behavior;
|
|
1476
1938
|
#retries;
|
|
1477
1939
|
#timeout;
|
|
1478
1940
|
#attempts;
|
|
@@ -1485,7 +1947,7 @@ var Task = class {
|
|
|
1485
1947
|
#paused;
|
|
1486
1948
|
#gate;
|
|
1487
1949
|
#timerSignal;
|
|
1488
|
-
constructor(context, phase, workflow, recompute, options, status = "pending", result,
|
|
1950
|
+
constructor(context, phase, workflow, recompute, options, status = "pending", result, behavior, retries, timeout, metadata = {}, attempts = 0, activity, handler, silence) {
|
|
1489
1951
|
this.#context = buildTaskContext(context.phase, context);
|
|
1490
1952
|
this.#phase = phase;
|
|
1491
1953
|
this.#workflow = workflow;
|
|
@@ -1507,11 +1969,8 @@ var Task = class {
|
|
|
1507
1969
|
this.#status = status;
|
|
1508
1970
|
this.#result = result;
|
|
1509
1971
|
this.#name = context.name;
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
value: context.description
|
|
1513
|
-
});
|
|
1514
|
-
this.#run = run;
|
|
1972
|
+
this.#description = context.description;
|
|
1973
|
+
this.#behavior = behavior;
|
|
1515
1974
|
this.#retries = retries;
|
|
1516
1975
|
this.#timeout = timeout;
|
|
1517
1976
|
this.#attempts = attempts;
|
|
@@ -1537,6 +1996,9 @@ var Task = class {
|
|
|
1537
1996
|
get name() {
|
|
1538
1997
|
return this.#name;
|
|
1539
1998
|
}
|
|
1999
|
+
get description() {
|
|
2000
|
+
return this.#description;
|
|
2001
|
+
}
|
|
1540
2002
|
get context() {
|
|
1541
2003
|
return this.#context;
|
|
1542
2004
|
}
|
|
@@ -1555,8 +2017,8 @@ var Task = class {
|
|
|
1555
2017
|
get attempts() {
|
|
1556
2018
|
return this.#attempts;
|
|
1557
2019
|
}
|
|
1558
|
-
get
|
|
1559
|
-
return this.#
|
|
2020
|
+
get behavior() {
|
|
2021
|
+
return this.#behavior;
|
|
1560
2022
|
}
|
|
1561
2023
|
get handler() {
|
|
1562
2024
|
return this.#handler;
|
|
@@ -1595,7 +2057,7 @@ var Task = class {
|
|
|
1595
2057
|
this.#activity = cloneTaskActivity({}, this.#stamp());
|
|
1596
2058
|
this.#arm();
|
|
1597
2059
|
this.#emitter.emit("start", this.id);
|
|
1598
|
-
this.#
|
|
2060
|
+
this.#recompute();
|
|
1599
2061
|
}
|
|
1600
2062
|
complete(value) {
|
|
1601
2063
|
let owned;
|
|
@@ -1612,7 +2074,7 @@ var Task = class {
|
|
|
1612
2074
|
value: owned
|
|
1613
2075
|
}));
|
|
1614
2076
|
this.#emitter.emit("complete", result);
|
|
1615
|
-
this.#
|
|
2077
|
+
this.#recompute();
|
|
1616
2078
|
}
|
|
1617
2079
|
fail(error) {
|
|
1618
2080
|
const origin = error.origin === "handler" || error.origin === "timeout" || error.origin === "recovery" ? error.origin : "handler";
|
|
@@ -1627,21 +2089,21 @@ var Task = class {
|
|
|
1627
2089
|
})
|
|
1628
2090
|
}));
|
|
1629
2091
|
this.#emitter.emit("fail", result);
|
|
1630
|
-
this.#
|
|
2092
|
+
this.#recompute();
|
|
1631
2093
|
}
|
|
1632
2094
|
skip() {
|
|
1633
2095
|
this.#transition("skipped");
|
|
1634
2096
|
this.#finish();
|
|
1635
2097
|
this.#abort.abort();
|
|
1636
2098
|
this.#emitter.emit("skip");
|
|
1637
|
-
this.#
|
|
2099
|
+
this.#recompute();
|
|
1638
2100
|
}
|
|
1639
2101
|
stop() {
|
|
1640
2102
|
this.#transition("stopped");
|
|
1641
2103
|
this.#finish();
|
|
1642
2104
|
this.#abort.abort();
|
|
1643
2105
|
this.#emitter.emit("stop");
|
|
1644
|
-
this.#
|
|
2106
|
+
this.#recompute();
|
|
1645
2107
|
}
|
|
1646
2108
|
report(input) {
|
|
1647
2109
|
if (this.#status !== "running") return failure(new WorkflowError("TRANSITION", `task '${this.id}' cannot report while '${this.#status}'`, {
|
|
@@ -1669,7 +2131,7 @@ var Task = class {
|
|
|
1669
2131
|
pause() {
|
|
1670
2132
|
if (this.#paused || this.#status !== "pending" && this.#status !== "running") return;
|
|
1671
2133
|
this.#paused = true;
|
|
1672
|
-
this.#gate =
|
|
2134
|
+
this.#gate = Promise.withResolvers();
|
|
1673
2135
|
this.#emitter.emit("pause");
|
|
1674
2136
|
}
|
|
1675
2137
|
resume() {
|
|
@@ -1682,10 +2144,10 @@ var Task = class {
|
|
|
1682
2144
|
return this.#paused && this.#gate !== void 0 ? this.#gate.promise : Promise.resolve();
|
|
1683
2145
|
}
|
|
1684
2146
|
/**
|
|
1685
|
-
*
|
|
2147
|
+
* Applies a validated declarative patch to SELF (`name` / `description`).
|
|
1686
2148
|
*
|
|
1687
2149
|
* @remarks
|
|
1688
|
-
* Defense-in-depth
|
|
2150
|
+
* Defense-in-depth: the owning
|
|
1689
2151
|
* {@link import('../types.js').TaskManagerInterface.update} gates FIRST (target
|
|
1690
2152
|
* exists + `pending`), so this is the second, redundant check — it THROWS a
|
|
1691
2153
|
* `MUTATION` {@link WorkflowError} unless this task's own `status` is `pending`.
|
|
@@ -1702,21 +2164,18 @@ var Task = class {
|
|
|
1702
2164
|
status: this.#status
|
|
1703
2165
|
});
|
|
1704
2166
|
if (value.name !== void 0) this.#name = value.name;
|
|
1705
|
-
if (value.description !== void 0)
|
|
1706
|
-
configurable: true,
|
|
1707
|
-
value: value.description
|
|
1708
|
-
});
|
|
2167
|
+
if (value.description !== void 0) this.#description = value.description;
|
|
1709
2168
|
}
|
|
1710
2169
|
snapshot() {
|
|
1711
2170
|
return {
|
|
1712
2171
|
id: this.id,
|
|
1713
2172
|
name: this.name,
|
|
1714
|
-
...this
|
|
2173
|
+
...this.#description === void 0 ? {} : { description: this.#description },
|
|
1715
2174
|
status: this.#status,
|
|
1716
2175
|
...this.#result === void 0 ? {} : { result: this.#result },
|
|
1717
2176
|
metadata: this.#metadata,
|
|
1718
2177
|
attempts: this.#attempts,
|
|
1719
|
-
...this.#
|
|
2178
|
+
...this.#behavior === void 0 ? {} : { behavior: this.#behavior },
|
|
1720
2179
|
...this.#retries === void 0 ? {} : { retries: this.#retries },
|
|
1721
2180
|
...this.#timeout === void 0 ? {} : { timeout: this.#timeout },
|
|
1722
2181
|
...this.#activity === void 0 ? {} : { activity: this.#activity }
|
|
@@ -1743,9 +2202,6 @@ var Task = class {
|
|
|
1743
2202
|
this.#result = frozen;
|
|
1744
2203
|
return frozen;
|
|
1745
2204
|
}
|
|
1746
|
-
#escalate() {
|
|
1747
|
-
this.#recompute();
|
|
1748
|
-
}
|
|
1749
2205
|
#touch() {
|
|
1750
2206
|
if (this.#activity === void 0) return;
|
|
1751
2207
|
this.#activity = Object.freeze({
|
|
@@ -1790,25 +2246,30 @@ var Task = class {
|
|
|
1790
2246
|
//#endregion
|
|
1791
2247
|
//#region src/core/tasks/TaskManager.ts
|
|
1792
2248
|
/**
|
|
1793
|
-
*
|
|
1794
|
-
* tasks —
|
|
1795
|
-
* preserved across an interior `skip` / `remove`.
|
|
2249
|
+
* Implements the lean child manager of a {@link import('../phases/Phase.js').Phase}'s live
|
|
2250
|
+
* tasks — the task vocabulary over one insertion-ordered {@link Collection}, so positional order
|
|
2251
|
+
* is preserved across an interior `skip` / `remove`.
|
|
1796
2252
|
*
|
|
1797
2253
|
* @remarks
|
|
1798
|
-
* - **
|
|
1799
|
-
* `
|
|
1800
|
-
* `
|
|
1801
|
-
*
|
|
1802
|
-
*
|
|
1803
|
-
* - **
|
|
1804
|
-
*
|
|
1805
|
-
*
|
|
1806
|
-
*
|
|
1807
|
-
*
|
|
1808
|
-
*
|
|
1809
|
-
*
|
|
1810
|
-
*
|
|
1811
|
-
*
|
|
2254
|
+
* - **One shared store.** The insertion-ordered `Map`, the reorder step, the bounds checks, and
|
|
2255
|
+
* the gated `add` / `remove` / `move` / `update` all live in {@link Collection}, built with the
|
|
2256
|
+
* `task` noun its refusals name and the compiled {@link taskUpdateShape} guard. This class adds
|
|
2257
|
+
* the domain accessors `task` / `tasks` and nothing else, so the task and phase managers cannot
|
|
2258
|
+
* drift apart.
|
|
2259
|
+
* - **Positional store.** `append` adds one live {@link TaskInterface} at the end (the build-time
|
|
2260
|
+
* wiring path), `task(id)` looks one up, `tasks()` lists them in positional order, `count` is
|
|
2261
|
+
* the tally. A `skip` is a STATUS change on a stored task (never a removal), so order survives
|
|
2262
|
+
* it; a snapshot RESTORE re-`append`s in the snapshot's order, reproducing it exactly.
|
|
2263
|
+
* - **Gated mutation API.** `add` / `remove` / `move` / `update` are the graceful
|
|
2264
|
+
* `Result` counterparts to `append`, gating ONLY on the target's OWN existence/status/id/bounds
|
|
2265
|
+
* — a duplicate id, an absent/non-`pending` target, an out-of-bounds `index`, or a patch that
|
|
2266
|
+
* fails {@link taskUpdateShape} validation all fail gracefully with a `MUTATION`
|
|
2267
|
+
* {@link WorkflowError} instead of throwing.
|
|
2268
|
+
* - **No batch matrix.** A phase's tasks are a fixed positional set, so
|
|
2269
|
+
* `.claude/rules/patterns.md` § Batch operations (the bulk verb
|
|
2270
|
+
* overloads) is deliberately omitted — no `remove` family lives here.
|
|
2271
|
+
* - **Event-free.** A purely structural container — the live {@link TaskInterface}s own their own
|
|
2272
|
+
* emitters; the manager observes nothing.
|
|
1812
2273
|
*
|
|
1813
2274
|
* @example
|
|
1814
2275
|
* ```ts
|
|
@@ -1819,58 +2280,37 @@ var Task = class {
|
|
|
1819
2280
|
* ```
|
|
1820
2281
|
*/
|
|
1821
2282
|
var TaskManager = class {
|
|
1822
|
-
#tasks =
|
|
1823
|
-
#isUpdate = compileGuard(taskUpdateShape);
|
|
2283
|
+
#tasks = new Collection("task", compileGuard(taskUpdateShape));
|
|
1824
2284
|
get count() {
|
|
1825
|
-
return this.#tasks.
|
|
2285
|
+
return this.#tasks.count;
|
|
1826
2286
|
}
|
|
1827
2287
|
append(task) {
|
|
1828
|
-
|
|
1829
|
-
this.#tasks.set(task.id, task);
|
|
2288
|
+
this.#tasks.append(task);
|
|
1830
2289
|
}
|
|
1831
2290
|
add(task, index) {
|
|
1832
|
-
|
|
1833
|
-
const at = index ?? this.#tasks.size;
|
|
1834
|
-
if (at < 0 || at > this.#tasks.size) return failure(new WorkflowError("MUTATION", `index '${at}' out of bounds`, { index: at }));
|
|
1835
|
-
this.#reorder(insertEntry([...this.#tasks.entries()], at, task.id, task));
|
|
1836
|
-
return success(task);
|
|
2291
|
+
return this.#tasks.add(task, index);
|
|
1837
2292
|
}
|
|
1838
2293
|
remove(id) {
|
|
1839
|
-
|
|
1840
|
-
if (target === void 0 || target.status !== "pending") return failure(new WorkflowError("MUTATION", `task '${id}' is not a pending task`, { id }));
|
|
1841
|
-
this.#tasks.delete(id);
|
|
1842
|
-
return success(target);
|
|
2294
|
+
return this.#tasks.remove(id);
|
|
1843
2295
|
}
|
|
1844
2296
|
move(id, index) {
|
|
1845
|
-
|
|
1846
|
-
if (target === void 0 || target.status !== "pending") return failure(new WorkflowError("MUTATION", `task '${id}' is not a pending task`, { id }));
|
|
1847
|
-
if (index < 0 || index >= this.#tasks.size) return failure(new WorkflowError("MUTATION", `index '${index}' out of bounds`, { index }));
|
|
1848
|
-
this.#reorder(moveEntry([...this.#tasks.entries()], id, index));
|
|
1849
|
-
return success(target);
|
|
2297
|
+
return this.#tasks.move(id, index);
|
|
1850
2298
|
}
|
|
1851
2299
|
update(id, patch) {
|
|
1852
|
-
|
|
1853
|
-
if (target === void 0 || target.status !== "pending") return failure(new WorkflowError("MUTATION", `task '${id}' is not a pending task`, { id }));
|
|
1854
|
-
if (!this.#isUpdate(patch)) return failure(new WorkflowError("MUTATION", `invalid patch for task '${id}'`, { id }));
|
|
1855
|
-
target.patch(patch);
|
|
1856
|
-
return success(target);
|
|
2300
|
+
return this.#tasks.update(id, patch);
|
|
1857
2301
|
}
|
|
1858
2302
|
task(id) {
|
|
1859
|
-
return this.#tasks.
|
|
2303
|
+
return this.#tasks.entry(id);
|
|
1860
2304
|
}
|
|
1861
2305
|
tasks() {
|
|
1862
|
-
return
|
|
1863
|
-
}
|
|
1864
|
-
#reorder(entries) {
|
|
1865
|
-
this.#tasks.clear();
|
|
1866
|
-
for (const [key, value] of entries) this.#tasks.set(key, value);
|
|
2306
|
+
return this.#tasks.entries();
|
|
1867
2307
|
}
|
|
1868
2308
|
};
|
|
1869
2309
|
//#endregion
|
|
1870
2310
|
//#region src/core/phases/Phase.ts
|
|
1871
2311
|
/**
|
|
1872
|
-
*
|
|
1873
|
-
* {@link
|
|
2312
|
+
* Implements the live DERIVED state machine (W-b) for one phase — an observable whose
|
|
2313
|
+
* {@link LifecycleStatus} is computed from its tasks (never set directly) and recomputed
|
|
1874
2314
|
* reactively as a task transitions (the middle tier of the cascade).
|
|
1875
2315
|
*
|
|
1876
2316
|
* @remarks
|
|
@@ -1878,20 +2318,20 @@ var TaskManager = class {
|
|
|
1878
2318
|
* {@link derivePhaseStatus} over the live tasks' statuses. `#recompute` (passed to
|
|
1879
2319
|
* each child {@link Task}) re-derives on every child transition; a CHANGE emits the matching
|
|
1880
2320
|
* event AND escalates to the workflow (`#escalate`, the upward step of the cascade).
|
|
1881
|
-
* - **Override
|
|
2321
|
+
* - **Override.** `skip` / `stop` FORCE the phase's status (for example, skipping a whole
|
|
1882
2322
|
* phase), overriding the derived value; the override is PERSISTED in the snapshot's own
|
|
1883
2323
|
* `override` field and restored DIRECTLY (no divergence guess), so a forced phase round-trips.
|
|
1884
|
-
* - **Children
|
|
2324
|
+
* - **Children.** `tasks` is the lean {@link TaskManager} (an accessor + `count`,
|
|
1885
2325
|
* no batch matrix); built positionally from the snapshot so order survives an interior `skip`.
|
|
1886
2326
|
* `results()` collects the settled tasks' {@link TaskResult}s (the phase tier of the result
|
|
1887
2327
|
* tree); `workflow` navigates UP to the live parent.
|
|
1888
|
-
* - **Observable
|
|
2328
|
+
* - **Observable.** The owned {@link emitter} ({@link PhaseEventMap}) fires
|
|
1889
2329
|
* `start` / `complete` / `fail` / `pause` / `resume` / `skip` / `stop` after the
|
|
1890
2330
|
* corresponding status or runtime-gate change. Status events fire after the phase recomputes
|
|
1891
2331
|
* and before it escalates to the workflow, preserving child/phase cause before parent effect.
|
|
1892
2332
|
* The emitter isolates a listener throw and routes it to its `error` handler (the `error`
|
|
1893
2333
|
* option); `fail` carries the failing task's {@link TaskResult}.
|
|
1894
|
-
* - **Structural API
|
|
2334
|
+
* - **Structural API.** `add` / `remove` / `move` / `update` gate BEFORE
|
|
1895
2335
|
* delegating to {@link tasks} (the manager gates the target's own existence/status/id/
|
|
1896
2336
|
* bounds), then emit the matching {@link PhaseEventMap} event on success only. NATIVE
|
|
1897
2337
|
* gating, purely from this phase's own derived `status` (no runner-installed hook): while
|
|
@@ -1899,22 +2339,22 @@ var TaskManager = class {
|
|
|
1899
2339
|
* append (a live runner subscribed to the `add` event picks it up), and `remove` / `move` /
|
|
1900
2340
|
* `update` always fail gracefully (the tasks are already handed to the execution
|
|
1901
2341
|
* substrate); while terminal, everything is refused.
|
|
1902
|
-
* - **Patch
|
|
2342
|
+
* - **Patch.** `patch` applies a validated {@link PhaseUpdate} to SELF
|
|
1903
2343
|
* (`name` / `description` / `concurrency` / `bail`) — defense-in-depth: it throws a
|
|
1904
2344
|
* `MUTATION` {@link WorkflowError} unless this phase's own `status` is `pending`, mirroring
|
|
1905
2345
|
* the owning {@link WorkflowInterface.update}'s gate.
|
|
1906
|
-
* - **Minting
|
|
2346
|
+
* - **Minting.** {@link add} MINTS a live {@link Task} from a {@link TaskDefinition}
|
|
1907
2347
|
* (converts it to a {@link TaskSnapshot}, builds the task wired to THIS phase) — the same
|
|
1908
2348
|
* construction path {@link #append} uses at build time, so a live mint and a restored/built
|
|
1909
2349
|
* task are wired IDENTICALLY. At construction, the workflow-level
|
|
1910
|
-
* {@link import('../types.js').
|
|
1911
|
-
* {@link import('../types.js').WorkflowOptions.functions}) resolves every unique initial `
|
|
2350
|
+
* {@link import('../types.js').WorkflowRegistry} registry (threaded from
|
|
2351
|
+
* {@link import('../types.js').WorkflowOptions.functions}) resolves every unique initial `behavior`
|
|
1912
2352
|
* name ONCE before any task is built; siblings sharing a name receive the exact same captured
|
|
1913
2353
|
* runtime {@link import('../types.js').TaskInterface.handler}. A later live {@link add} reads
|
|
1914
2354
|
* that name once from the retained registry at its own mint moment. An omitted or unregistered
|
|
1915
|
-
* `
|
|
2355
|
+
* `behavior` resolves to no handler; only omission is a no-op, while an unresolved present name makes
|
|
1916
2356
|
* the containing tree non-drivable.
|
|
1917
|
-
* - **Runtime lifecycle
|
|
2357
|
+
* - **Runtime lifecycle.** `pause` / `resume` / `wait` mirror the workflow's own
|
|
1918
2358
|
* quartet, scoped to this phase — a driving
|
|
1919
2359
|
* {@link import('../types.js').WorkflowRunnerInterface.execute} gates a task's own
|
|
1920
2360
|
* pre-dispatch on the workflow's gate FIRST, then this phase's gate, WITHOUT touching
|
|
@@ -1926,6 +2366,7 @@ var TaskManager = class {
|
|
|
1926
2366
|
var Phase = class {
|
|
1927
2367
|
#id;
|
|
1928
2368
|
#name;
|
|
2369
|
+
#description;
|
|
1929
2370
|
#workflow;
|
|
1930
2371
|
#escalateUp;
|
|
1931
2372
|
#tasks = new TaskManager();
|
|
@@ -1944,16 +2385,13 @@ var Phase = class {
|
|
|
1944
2385
|
const tasks = options?.tasks;
|
|
1945
2386
|
this.#id = snapshot.id;
|
|
1946
2387
|
this.#name = snapshot.name;
|
|
1947
|
-
|
|
1948
|
-
configurable: true,
|
|
1949
|
-
value: snapshot.description
|
|
1950
|
-
});
|
|
2388
|
+
this.#description = snapshot.description;
|
|
1951
2389
|
this.#workflow = workflow;
|
|
1952
2390
|
this.#escalateUp = escalate;
|
|
1953
2391
|
this.#functions = functions;
|
|
1954
2392
|
this.#silence = silence;
|
|
1955
2393
|
const handlers = /* @__PURE__ */ new Map();
|
|
1956
|
-
for (const task of snapshot.tasks) if (task.
|
|
2394
|
+
for (const task of snapshot.tasks) if (task.behavior !== void 0 && !handlers.has(task.behavior)) handlers.set(task.behavior, functions?.[task.behavior]);
|
|
1957
2395
|
this.#bail = bail ?? snapshot.bail;
|
|
1958
2396
|
this.#concurrency = snapshot.concurrency;
|
|
1959
2397
|
this.#emitter = new Emitter({
|
|
@@ -1962,7 +2400,7 @@ var Phase = class {
|
|
|
1962
2400
|
});
|
|
1963
2401
|
for (const task of snapshot.tasks) {
|
|
1964
2402
|
const taskOptions = tasks?.[task.id];
|
|
1965
|
-
const handler = task.
|
|
2403
|
+
const handler = task.behavior === void 0 ? void 0 : handlers.get(task.behavior);
|
|
1966
2404
|
this.#append(task, taskOptions, handler);
|
|
1967
2405
|
}
|
|
1968
2406
|
this.#override = snapshot.override;
|
|
@@ -1979,11 +2417,14 @@ var Phase = class {
|
|
|
1979
2417
|
get name() {
|
|
1980
2418
|
return this.#name;
|
|
1981
2419
|
}
|
|
2420
|
+
get description() {
|
|
2421
|
+
return this.#description;
|
|
2422
|
+
}
|
|
1982
2423
|
get context() {
|
|
1983
2424
|
return buildPhaseContext(this.#workflow.context, {
|
|
1984
2425
|
id: this.#id,
|
|
1985
2426
|
name: this.#name,
|
|
1986
|
-
...this
|
|
2427
|
+
...this.#description === void 0 ? {} : { description: this.#description }
|
|
1987
2428
|
});
|
|
1988
2429
|
}
|
|
1989
2430
|
get workflow() {
|
|
@@ -2025,7 +2466,7 @@ var Phase = class {
|
|
|
2025
2466
|
pause() {
|
|
2026
2467
|
if (this.#paused || isTerminalStatus(this.status)) return;
|
|
2027
2468
|
this.#paused = true;
|
|
2028
|
-
this.#gate =
|
|
2469
|
+
this.#gate = Promise.withResolvers();
|
|
2029
2470
|
this.#emitter.emit("pause");
|
|
2030
2471
|
}
|
|
2031
2472
|
resume() {
|
|
@@ -2087,10 +2528,7 @@ var Phase = class {
|
|
|
2087
2528
|
status: this.status
|
|
2088
2529
|
});
|
|
2089
2530
|
if (value.name !== void 0) this.#name = value.name;
|
|
2090
|
-
if (value.description !== void 0)
|
|
2091
|
-
configurable: true,
|
|
2092
|
-
value: value.description
|
|
2093
|
-
});
|
|
2531
|
+
if (value.description !== void 0) this.#description = value.description;
|
|
2094
2532
|
if (value.concurrency !== void 0) this.#concurrency = value.concurrency;
|
|
2095
2533
|
if (value.bail !== void 0) this.#bail = value.bail;
|
|
2096
2534
|
}
|
|
@@ -2098,7 +2536,7 @@ var Phase = class {
|
|
|
2098
2536
|
return {
|
|
2099
2537
|
id: this.id,
|
|
2100
2538
|
name: this.name,
|
|
2101
|
-
...this
|
|
2539
|
+
...this.#description === void 0 ? {} : { description: this.#description },
|
|
2102
2540
|
status: this.status,
|
|
2103
2541
|
...this.#override === void 0 ? {} : { override: this.#override },
|
|
2104
2542
|
bail: this.#bail,
|
|
@@ -2133,7 +2571,7 @@ var Phase = class {
|
|
|
2133
2571
|
}
|
|
2134
2572
|
#failure() {
|
|
2135
2573
|
const found = findFailure(this.results());
|
|
2136
|
-
if (found === void 0) throw new
|
|
2574
|
+
if (found === void 0) throw new WorkflowError("INVARIANT", `phase '${this.id}' derived failed with no failing task result`, { phase: this.id });
|
|
2137
2575
|
return found;
|
|
2138
2576
|
}
|
|
2139
2577
|
#release() {
|
|
@@ -2151,11 +2589,11 @@ var Phase = class {
|
|
|
2151
2589
|
this.#tasks.append(created);
|
|
2152
2590
|
}
|
|
2153
2591
|
#create(snapshot, options, handler) {
|
|
2154
|
-
return new Task(buildTaskContext(this.context, snapshot), this, this.#workflow, () => this.#recompute(), options, snapshot.status, snapshot.result, snapshot.
|
|
2592
|
+
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);
|
|
2155
2593
|
}
|
|
2156
2594
|
#mint(definition) {
|
|
2157
2595
|
const snapshot = taskDefinitionToSnapshot(definition);
|
|
2158
|
-
const handler = snapshot.
|
|
2596
|
+
const handler = snapshot.behavior === void 0 ? void 0 : this.#functions?.[snapshot.behavior];
|
|
2159
2597
|
return this.#create(snapshot, void 0, handler);
|
|
2160
2598
|
}
|
|
2161
2599
|
#statuses() {
|
|
@@ -2165,24 +2603,28 @@ var Phase = class {
|
|
|
2165
2603
|
//#endregion
|
|
2166
2604
|
//#region src/core/phases/PhaseManager.ts
|
|
2167
2605
|
/**
|
|
2168
|
-
*
|
|
2169
|
-
*
|
|
2606
|
+
* Implements the lean child manager of a {@link import('../Workflow.js').Workflow}'s live
|
|
2607
|
+
* phases — the phase vocabulary over one insertion-ordered {@link Collection}, the phase analogue
|
|
2170
2608
|
* of {@link import('../tasks/TaskManager.js').TaskManager}.
|
|
2171
2609
|
*
|
|
2172
2610
|
* @remarks
|
|
2173
|
-
* - **
|
|
2174
|
-
* `
|
|
2175
|
-
*
|
|
2176
|
-
*
|
|
2177
|
-
* - **
|
|
2178
|
-
*
|
|
2179
|
-
*
|
|
2180
|
-
*
|
|
2181
|
-
*
|
|
2182
|
-
*
|
|
2183
|
-
*
|
|
2184
|
-
*
|
|
2185
|
-
*
|
|
2611
|
+
* - **One shared store.** The insertion-ordered `Map`, the reorder step, the bounds checks, and
|
|
2612
|
+
* the gated `add` / `remove` / `move` / `update` all live in {@link Collection}, built with the
|
|
2613
|
+
* `phase` noun its refusals name and the compiled {@link phaseUpdateShape} guard. This class
|
|
2614
|
+
* adds the domain accessors `phase` / `phases` and nothing else.
|
|
2615
|
+
* - **Positional store.** `append` adds one live {@link PhaseInterface} at the end, `phase(id)`
|
|
2616
|
+
* looks one up, `phases()` lists them in positional order, `count` is the tally. A snapshot
|
|
2617
|
+
* RESTORE re-`append`s in the snapshot's order, reproducing it exactly.
|
|
2618
|
+
* - **Gated mutation API.** `add` / `remove` / `move` / `update` are the graceful
|
|
2619
|
+
* `Result` counterparts to `append`, gating ONLY on the target's OWN existence/status/id/bounds
|
|
2620
|
+
* — a duplicate id, an absent/non-`pending` target, an out-of-bounds `index`, or a patch that
|
|
2621
|
+
* fails {@link phaseUpdateShape} validation all fail gracefully with a `MUTATION`
|
|
2622
|
+
* {@link WorkflowError} instead of throwing.
|
|
2623
|
+
* - **No batch matrix.** A workflow's phases are a fixed positional set, so the batch verbs of
|
|
2624
|
+
* `.claude/rules/patterns.md` § Batch operations are
|
|
2625
|
+
* deliberately omitted.
|
|
2626
|
+
* - **Event-free.** A purely structural container — the live {@link PhaseInterface}s own their own
|
|
2627
|
+
* emitters.
|
|
2186
2628
|
*
|
|
2187
2629
|
* @example
|
|
2188
2630
|
* ```ts
|
|
@@ -2193,58 +2635,37 @@ var Phase = class {
|
|
|
2193
2635
|
* ```
|
|
2194
2636
|
*/
|
|
2195
2637
|
var PhaseManager = class {
|
|
2196
|
-
#phases =
|
|
2197
|
-
#isUpdate = compileGuard(phaseUpdateShape);
|
|
2638
|
+
#phases = new Collection("phase", compileGuard(phaseUpdateShape));
|
|
2198
2639
|
get count() {
|
|
2199
|
-
return this.#phases.
|
|
2640
|
+
return this.#phases.count;
|
|
2200
2641
|
}
|
|
2201
2642
|
append(phase) {
|
|
2202
|
-
|
|
2203
|
-
this.#phases.set(phase.id, phase);
|
|
2643
|
+
this.#phases.append(phase);
|
|
2204
2644
|
}
|
|
2205
2645
|
add(phase, index) {
|
|
2206
|
-
|
|
2207
|
-
const at = index ?? this.#phases.size;
|
|
2208
|
-
if (at < 0 || at > this.#phases.size) return failure(new WorkflowError("MUTATION", `index '${at}' out of bounds`, { index: at }));
|
|
2209
|
-
this.#reorder(insertEntry([...this.#phases.entries()], at, phase.id, phase));
|
|
2210
|
-
return success(phase);
|
|
2646
|
+
return this.#phases.add(phase, index);
|
|
2211
2647
|
}
|
|
2212
2648
|
remove(id) {
|
|
2213
|
-
|
|
2214
|
-
if (target === void 0 || target.status !== "pending") return failure(new WorkflowError("MUTATION", `phase '${id}' is not a pending phase`, { id }));
|
|
2215
|
-
this.#phases.delete(id);
|
|
2216
|
-
return success(target);
|
|
2649
|
+
return this.#phases.remove(id);
|
|
2217
2650
|
}
|
|
2218
2651
|
move(id, index) {
|
|
2219
|
-
|
|
2220
|
-
if (target === void 0 || target.status !== "pending") return failure(new WorkflowError("MUTATION", `phase '${id}' is not a pending phase`, { id }));
|
|
2221
|
-
if (index < 0 || index >= this.#phases.size) return failure(new WorkflowError("MUTATION", `index '${index}' out of bounds`, { index }));
|
|
2222
|
-
this.#reorder(moveEntry([...this.#phases.entries()], id, index));
|
|
2223
|
-
return success(target);
|
|
2652
|
+
return this.#phases.move(id, index);
|
|
2224
2653
|
}
|
|
2225
2654
|
update(id, patch) {
|
|
2226
|
-
|
|
2227
|
-
if (target === void 0 || target.status !== "pending") return failure(new WorkflowError("MUTATION", `phase '${id}' is not a pending phase`, { id }));
|
|
2228
|
-
if (!this.#isUpdate(patch)) return failure(new WorkflowError("MUTATION", `invalid patch for phase '${id}'`, { id }));
|
|
2229
|
-
target.patch(patch);
|
|
2230
|
-
return success(target);
|
|
2655
|
+
return this.#phases.update(id, patch);
|
|
2231
2656
|
}
|
|
2232
2657
|
phase(id) {
|
|
2233
|
-
return this.#phases.
|
|
2658
|
+
return this.#phases.entry(id);
|
|
2234
2659
|
}
|
|
2235
2660
|
phases() {
|
|
2236
|
-
return
|
|
2237
|
-
}
|
|
2238
|
-
#reorder(entries) {
|
|
2239
|
-
this.#phases.clear();
|
|
2240
|
-
for (const [key, value] of entries) this.#phases.set(key, value);
|
|
2661
|
+
return this.#phases.entries();
|
|
2241
2662
|
}
|
|
2242
2663
|
};
|
|
2243
2664
|
//#endregion
|
|
2244
2665
|
//#region src/core/Workflow.ts
|
|
2245
2666
|
/**
|
|
2246
|
-
*
|
|
2247
|
-
*
|
|
2667
|
+
* Implements the live DERIVED state machine (W-b) for a whole workflow — the observable ROOT
|
|
2668
|
+
* whose {@link LifecycleStatus} is computed from its phases under the `bail` policy and
|
|
2248
2669
|
* recomputed reactively as the cascade propagates up from a task transition.
|
|
2249
2670
|
*
|
|
2250
2671
|
* @remarks
|
|
@@ -2257,7 +2678,7 @@ var PhaseManager = class {
|
|
|
2257
2678
|
* reachable ONLY under `bail: true` (a single failed task halts the workflow); under
|
|
2258
2679
|
* `bail: false` a failed phase folds into `completed`. `#recompute` diffs on each phase
|
|
2259
2680
|
* change; a CHANGE emits.
|
|
2260
|
-
* - **Override
|
|
2681
|
+
* - **Override.** `skip` / `stop` FORCE the status; an executed task-free pending tree
|
|
2261
2682
|
* may also be force-completed vacuously. The override is PERSISTED in the snapshot's own
|
|
2262
2683
|
* `override` field and restored DIRECTLY (no divergence guess). The snapshot also persists
|
|
2263
2684
|
* `bail`, so a restore re-derives status identically without a silent policy default.
|
|
@@ -2266,12 +2687,12 @@ var PhaseManager = class {
|
|
|
2266
2687
|
* navigate UP.
|
|
2267
2688
|
* - **Snapshot.** `snapshot()` serializes the whole live tree to a {@link WorkflowSnapshot} (pure
|
|
2268
2689
|
* JSON); {@link import('./factories.js').createRestoredWorkflow} rebuilds an equivalent live tree.
|
|
2269
|
-
* - **Observable
|
|
2690
|
+
* - **Observable.** The owned {@link emitter} ({@link WorkflowEventMap}) fires
|
|
2270
2691
|
* `start` / `complete` / `fail` / `pause` / `resume` / `skip` / `stop` after the
|
|
2271
2692
|
* corresponding status or runtime-gate change; the emitter isolates a listener throw and
|
|
2272
2693
|
* routes it to its `error` handler (the `error` option); `fail` carries the failing task's
|
|
2273
2694
|
* {@link TaskResult}.
|
|
2274
|
-
* - **Structural API
|
|
2695
|
+
* - **Structural API.** `add` / `remove` / `move` / `update` gate BEFORE
|
|
2275
2696
|
* delegating to {@link phases} (the manager gates the target's own existence/status/id/
|
|
2276
2697
|
* bounds), then emit the matching {@link WorkflowEventMap} event on success only. NATIVE,
|
|
2277
2698
|
* bottom-up gating (no runner-installed hook): refused outright while this workflow's own
|
|
@@ -2280,12 +2701,27 @@ var PhaseManager = class {
|
|
|
2280
2701
|
* {@link import('./helpers.js').deriveBoundary} over the live phases' statuses. A `pending`
|
|
2281
2702
|
* workflow's phases are all `pending`, so the boundary is `0` and every position is
|
|
2282
2703
|
* naturally accepted.
|
|
2283
|
-
* - **Runtime lifecycle
|
|
2704
|
+
* - **Runtime lifecycle.** `pause` / `resume` / `wait` gate execution at the runner's
|
|
2284
2705
|
* phase/task boundaries WITHOUT touching {@link status} — `paused` is runtime-only, never
|
|
2285
2706
|
* persisted. `destroy` is a terminal teardown: it `stop`s every non-terminal task and
|
|
2286
2707
|
* phase (releasing their gates and liveness resources), aborts {@link signal}, forces the
|
|
2287
2708
|
* workflow `stop` override when needed, releases its parked waiter, and marks
|
|
2288
2709
|
* {@link destroyed} — all idempotent.
|
|
2710
|
+
*
|
|
2711
|
+
* @example
|
|
2712
|
+
* ```ts
|
|
2713
|
+
* import { definitionToSnapshot, Workflow } from '@orkestrel/workflow'
|
|
2714
|
+
*
|
|
2715
|
+
* const definition = {
|
|
2716
|
+
* id: 'release',
|
|
2717
|
+
* name: 'Release',
|
|
2718
|
+
* phases: [{ id: 'build', name: 'Build', tasks: [{ id: 'compile', name: 'Compile' }] }],
|
|
2719
|
+
* }
|
|
2720
|
+
* const workflow = new Workflow(definitionToSnapshot(definition))
|
|
2721
|
+
* workflow.status // 'pending'
|
|
2722
|
+
* workflow.phase('build')?.task('compile')?.status // 'pending'
|
|
2723
|
+
* workflow.snapshot().id // 'release'
|
|
2724
|
+
* ```
|
|
2289
2725
|
*/
|
|
2290
2726
|
var Workflow = class {
|
|
2291
2727
|
#context;
|
|
@@ -2312,7 +2748,6 @@ var Workflow = class {
|
|
|
2312
2748
|
const functions = captured.functions;
|
|
2313
2749
|
const silence = captured.silence;
|
|
2314
2750
|
this.#context = buildWorkflowContext(snapshot);
|
|
2315
|
-
if (snapshot.description !== void 0) Object.defineProperty(this, "description", { value: snapshot.description });
|
|
2316
2751
|
this.#bail = bail ?? snapshot.bail;
|
|
2317
2752
|
this.#bailOverride = bail;
|
|
2318
2753
|
this.#functions = functions;
|
|
@@ -2343,6 +2778,9 @@ var Workflow = class {
|
|
|
2343
2778
|
get name() {
|
|
2344
2779
|
return this.#context.name;
|
|
2345
2780
|
}
|
|
2781
|
+
get description() {
|
|
2782
|
+
return this.#context.description;
|
|
2783
|
+
}
|
|
2346
2784
|
get context() {
|
|
2347
2785
|
return this.#context;
|
|
2348
2786
|
}
|
|
@@ -2386,7 +2824,7 @@ var Workflow = class {
|
|
|
2386
2824
|
pause() {
|
|
2387
2825
|
if (this.#paused || isTerminalStatus(this.status) || this.#destroyed) return;
|
|
2388
2826
|
this.#paused = true;
|
|
2389
|
-
this.#gate =
|
|
2827
|
+
this.#gate = Promise.withResolvers();
|
|
2390
2828
|
this.#emitter.emit("pause");
|
|
2391
2829
|
}
|
|
2392
2830
|
resume() {
|
|
@@ -2474,7 +2912,7 @@ var Workflow = class {
|
|
|
2474
2912
|
return cloneWorkflowSnapshot({
|
|
2475
2913
|
id: this.id,
|
|
2476
2914
|
name: this.name,
|
|
2477
|
-
...this.description === void 0 ? {} : { description: this.description },
|
|
2915
|
+
...this.#context.description === void 0 ? {} : { description: this.#context.description },
|
|
2478
2916
|
status: this.status,
|
|
2479
2917
|
...this.#override === void 0 ? {} : { override: this.#override },
|
|
2480
2918
|
bail: this.#bail,
|
|
@@ -2518,7 +2956,7 @@ var Workflow = class {
|
|
|
2518
2956
|
}
|
|
2519
2957
|
#failure() {
|
|
2520
2958
|
const found = findFailure(this.results());
|
|
2521
|
-
if (found === void 0) throw new
|
|
2959
|
+
if (found === void 0) throw new WorkflowError("INVARIANT", `workflow '${this.id}' derived failed with no failing task result`, { workflow: this.id });
|
|
2522
2960
|
return found;
|
|
2523
2961
|
}
|
|
2524
2962
|
#append(phase, options) {
|
|
@@ -2543,14 +2981,15 @@ var Workflow = class {
|
|
|
2543
2981
|
//#endregion
|
|
2544
2982
|
//#region src/core/WorkflowManager.ts
|
|
2545
2983
|
/**
|
|
2546
|
-
*
|
|
2984
|
+
* Implements the store-backed registry of {@link WorkflowInterface}s keyed by `id`, in insertion order —
|
|
2547
2985
|
* the additive manager tier mirroring the `@orkestrel/agent` line's `ConversationManager` /
|
|
2548
2986
|
* `WorkspaceManager`. Event-free (a registry, like its twins); the observability lives on each
|
|
2549
2987
|
* {@link WorkflowInterface}.
|
|
2550
2988
|
*
|
|
2551
2989
|
* @remarks
|
|
2552
2990
|
* - **Registry.** Workflows live in an insertion-ordered `Map` keyed by `id`. `add(definition)`
|
|
2553
|
-
* mints a live {@link WorkflowInterface} through
|
|
2991
|
+
* mints a live {@link WorkflowInterface} through the same construction path
|
|
2992
|
+
* {@link import('./factories.js').createWorkflow} takes (flowing the manager's
|
|
2554
2993
|
* `functions` registry in) and stores it under `definition.id` — an already-present id
|
|
2555
2994
|
* OVERWRITES (last write wins). `count` is the map size, `workflow(id)` looks one up,
|
|
2556
2995
|
* `workflows()` lists them in insertion order.
|
|
@@ -2559,8 +2998,8 @@ var Workflow = class {
|
|
|
2559
2998
|
* earlier reads; wrong-key payloads reject with `RESTORE`. `save(id)` captures a registered
|
|
2560
2999
|
* workflow's snapshot at invocation and serializes same-id writes without coupling other ids.
|
|
2561
3000
|
* Both remain lenient without a store or registered id.
|
|
2562
|
-
* - **Removal.** `remove` drops one by id, or a batch (
|
|
2563
|
-
*
|
|
3001
|
+
* - **Removal.** `remove` drops one by id, or a batch (array overload FIRST) — `true` only when
|
|
3002
|
+
* every id was removed. `clear` empties the registry.
|
|
2564
3003
|
* - **No active pointer.** Unlike its `ConversationManager` / `WorkspaceManager` twins, there is
|
|
2565
3004
|
* no `active` / `switch` — nothing in the workflow domain renders "the current workflow".
|
|
2566
3005
|
*
|
|
@@ -2598,7 +3037,7 @@ var WorkflowManager = class {
|
|
|
2598
3037
|
return [...this.#workflows.values()];
|
|
2599
3038
|
}
|
|
2600
3039
|
add(definition) {
|
|
2601
|
-
const workflow =
|
|
3040
|
+
const workflow = this.#build(definition);
|
|
2602
3041
|
const mutation = this.#invalidate(workflow.id);
|
|
2603
3042
|
if (mutation === void 0) this.#additions.delete(workflow.id);
|
|
2604
3043
|
else this.#additions.set(workflow.id, mutation);
|
|
@@ -2635,11 +3074,11 @@ var WorkflowManager = class {
|
|
|
2635
3074
|
}
|
|
2636
3075
|
remove(ids) {
|
|
2637
3076
|
if (isArray(ids)) {
|
|
2638
|
-
let removed =
|
|
3077
|
+
let removed = true;
|
|
2639
3078
|
for (const id of ids) {
|
|
2640
3079
|
this.#invalidate(id);
|
|
2641
3080
|
this.#additions.delete(id);
|
|
2642
|
-
if (this.#workflows.delete(id)) removed =
|
|
3081
|
+
if (!this.#workflows.delete(id)) removed = false;
|
|
2643
3082
|
}
|
|
2644
3083
|
return removed;
|
|
2645
3084
|
}
|
|
@@ -2669,7 +3108,7 @@ var WorkflowManager = class {
|
|
|
2669
3108
|
if (!this.#owns(id, mutation, generation)) return this.#resolve(id, generation);
|
|
2670
3109
|
let workflow;
|
|
2671
3110
|
try {
|
|
2672
|
-
workflow =
|
|
3111
|
+
workflow = this.#restore(owned);
|
|
2673
3112
|
} catch (error) {
|
|
2674
3113
|
if (!this.#owns(id, mutation, generation)) return this.#resolve(id, generation);
|
|
2675
3114
|
throw error;
|
|
@@ -2680,6 +3119,15 @@ var WorkflowManager = class {
|
|
|
2680
3119
|
this.#releaseHydration(id, lease);
|
|
2681
3120
|
}
|
|
2682
3121
|
}
|
|
3122
|
+
#build(definition) {
|
|
3123
|
+
return createWorkflowTree(definition, this.#captured());
|
|
3124
|
+
}
|
|
3125
|
+
#restore(snapshot) {
|
|
3126
|
+
return new Workflow(cloneWorkflowSnapshot(snapshot), this.#captured());
|
|
3127
|
+
}
|
|
3128
|
+
#captured() {
|
|
3129
|
+
return captureWorkflowOptions(this.#functions === void 0 ? {} : { functions: this.#functions });
|
|
3130
|
+
}
|
|
2683
3131
|
#owns(id, mutation, generation) {
|
|
2684
3132
|
return this.#generation === generation && this.#mutations.get(id) === mutation;
|
|
2685
3133
|
}
|
|
@@ -2735,9 +3183,50 @@ var WorkflowManager = class {
|
|
|
2735
3183
|
}
|
|
2736
3184
|
};
|
|
2737
3185
|
//#endregion
|
|
3186
|
+
//#region src/core/RunHolder.ts
|
|
3187
|
+
/**
|
|
3188
|
+
* Holds the active phase {@link RunnerInterface} for one
|
|
3189
|
+
* {@link import('./types.js').WorkflowRunnerInterface.execute} call, for the lifetime of that run.
|
|
3190
|
+
*
|
|
3191
|
+
* @remarks
|
|
3192
|
+
* - **One holder per run.** The engine mints a holder as a run begins and threads that one
|
|
3193
|
+
* instance through every phase of the run, so a nested `execute` reached through application
|
|
3194
|
+
* composition gets its own holder and can never clobber the suspended outer run's.
|
|
3195
|
+
* - **`hold` is the only mutation.** A phase takes the substrate runner with `hold(runner)` as it
|
|
3196
|
+
* starts and releases it with `hold()` as it settles; `runner` reads the held value back and is
|
|
3197
|
+
* `undefined` between phases and after the last one.
|
|
3198
|
+
* - **A cancel closes over the holder.** The run-level abort listener reads `runner` when it
|
|
3199
|
+
* fires, so it reaches whichever phase runner is live at that moment rather than the one that
|
|
3200
|
+
* was live when the listener was armed.
|
|
3201
|
+
* - **Event-free.** A plain cell — no emitter, no lifecycle of its own.
|
|
3202
|
+
*/
|
|
3203
|
+
var RunHolder = class {
|
|
3204
|
+
#runner;
|
|
3205
|
+
get runner() {
|
|
3206
|
+
return this.#runner;
|
|
3207
|
+
}
|
|
3208
|
+
/**
|
|
3209
|
+
* Takes the phase runner a starting phase hands this run, or releases the held one.
|
|
3210
|
+
*
|
|
3211
|
+
* @param runner - The phase runner to hold; omitted releases the held runner
|
|
3212
|
+
* @example
|
|
3213
|
+
* ```ts
|
|
3214
|
+
* import type { TaskInterface } from '@orkestrel/workflow'
|
|
3215
|
+
* import { createRunner, RunHolder } from '@orkestrel/workflow'
|
|
3216
|
+
*
|
|
3217
|
+
* const holder = new RunHolder()
|
|
3218
|
+
* holder.hold(createRunner<TaskInterface, void>({ handler: () => undefined }))
|
|
3219
|
+
* holder.hold() // released — `runner` reads `undefined` again
|
|
3220
|
+
* ```
|
|
3221
|
+
*/
|
|
3222
|
+
hold(runner) {
|
|
3223
|
+
this.#runner = runner;
|
|
3224
|
+
}
|
|
3225
|
+
};
|
|
3226
|
+
//#endregion
|
|
2738
3227
|
//#region src/core/Controller.ts
|
|
2739
3228
|
/**
|
|
2740
|
-
*
|
|
3229
|
+
* Implements the per-unit handle a runner handler receives — wraps the unit's identity,
|
|
2741
3230
|
* input, cancellation, and the run controls (`wait` / `spawn` / `abort`).
|
|
2742
3231
|
*
|
|
2743
3232
|
* @remarks
|
|
@@ -2749,10 +3238,10 @@ var WorkflowManager = class {
|
|
|
2749
3238
|
* unit's own abort, the runner-level abort (the runner aborts every unit), and
|
|
2750
3239
|
* the per-attempt timeout — so it fires on any of the three. `aborted` and
|
|
2751
3240
|
* `abort(reason)` delegate to the unit's `Abort` (the cancellation source of
|
|
2752
|
-
* truth);
|
|
3241
|
+
* truth); because the attempt signal ANY-includes that abort, `abort()` fires
|
|
2753
3242
|
* `signal` too.
|
|
2754
3243
|
* - **`wait` promise-parks (never a timer).** It resolves the instant the unit's
|
|
2755
|
-
* `signal` fires (immediately if already aborted)
|
|
3244
|
+
* `signal` fires (immediately if already aborted) through a one-shot listener — no
|
|
2756
3245
|
* `setTimeout`, no polling, no busy-yield — so a parked unit costs no CPU.
|
|
2757
3246
|
* - **`spawn` is fire-and-track.** It delegates to the runner's launch-a-sibling
|
|
2758
3247
|
* callback, which routes the sibling through the queue; the runner's `execute`
|
|
@@ -2763,23 +3252,32 @@ var WorkflowManager = class {
|
|
|
2763
3252
|
* {@link RunnerInterface.emitter} instead (`unit` / `spawn` / `settle` / `fail` carry the id).
|
|
2764
3253
|
*/
|
|
2765
3254
|
var Controller = class {
|
|
2766
|
-
id;
|
|
2767
|
-
input;
|
|
2768
|
-
signal;
|
|
3255
|
+
#id;
|
|
3256
|
+
#input;
|
|
3257
|
+
#signal;
|
|
2769
3258
|
#abort;
|
|
2770
3259
|
#spawn;
|
|
2771
3260
|
constructor(id, input, abort, signal, spawn) {
|
|
2772
|
-
this
|
|
2773
|
-
this
|
|
3261
|
+
this.#id = id;
|
|
3262
|
+
this.#input = input;
|
|
2774
3263
|
this.#abort = abort;
|
|
2775
|
-
this
|
|
3264
|
+
this.#signal = signal;
|
|
2776
3265
|
this.#spawn = spawn;
|
|
2777
3266
|
}
|
|
3267
|
+
get id() {
|
|
3268
|
+
return this.#id;
|
|
3269
|
+
}
|
|
3270
|
+
get input() {
|
|
3271
|
+
return this.#input;
|
|
3272
|
+
}
|
|
3273
|
+
get signal() {
|
|
3274
|
+
return this.#signal;
|
|
3275
|
+
}
|
|
2778
3276
|
get aborted() {
|
|
2779
3277
|
return this.#abort.aborted;
|
|
2780
3278
|
}
|
|
2781
3279
|
wait() {
|
|
2782
|
-
return parkSignal(this
|
|
3280
|
+
return parkSignal(this.#signal);
|
|
2783
3281
|
}
|
|
2784
3282
|
spawn(input) {
|
|
2785
3283
|
return this.#spawn(input);
|
|
@@ -2791,7 +3289,7 @@ var Controller = class {
|
|
|
2791
3289
|
//#endregion
|
|
2792
3290
|
//#region src/core/Runner.ts
|
|
2793
3291
|
/**
|
|
2794
|
-
*
|
|
3292
|
+
* Implements a thin generic orchestrator that drives declared units — and any they `spawn` —
|
|
2795
3293
|
* through a bounded-concurrency {@link createQueue}, collecting ordered results.
|
|
2796
3294
|
*
|
|
2797
3295
|
* @remarks
|
|
@@ -2805,12 +3303,12 @@ var Controller = class {
|
|
|
2805
3303
|
* Results are read back as `#order.map(id => #values.get(id))` — declared first (in
|
|
2806
3304
|
* input order), then spawns (in spawn order). There is no one-time task snapshot,
|
|
2807
3305
|
* so a unit spawned mid-handler is run and ordered like any other.
|
|
2808
|
-
* - **`execute` awaits the full spawn closure
|
|
3306
|
+
* - **`execute` awaits the full spawn closure through a count gate.** `#launch` increments
|
|
2809
3307
|
* an outstanding-unit `#count` BEFORE enqueuing and every settle decrements it,
|
|
2810
3308
|
* resolving the `#drained` deferred at zero. Because `spawn` calls `#launch` (so
|
|
2811
3309
|
* `#count += 1`) before the parent handler returns, the count never reaches zero
|
|
2812
3310
|
* mid-run — `execute` parks on `#drained` and so awaits the entire transitive
|
|
2813
|
-
* closure, not
|
|
3311
|
+
* closure, not only the declared units.
|
|
2814
3312
|
* - **`spawn` is fire-and-track.** A spawned unit runs through the queue regardless of
|
|
2815
3313
|
* whether its promise is awaited; the Runner never awaits a spawned promise from
|
|
2816
3314
|
* within a handler's slot (it awaits the count gate instead), so a slot-holding
|
|
@@ -2824,16 +3322,16 @@ var Controller = class {
|
|
|
2824
3322
|
* unit failure (after its retries) records the error and `abort()`s the run, so every
|
|
2825
3323
|
* sibling's signal fires; later failures are ignored and `execute` rejects with the
|
|
2826
3324
|
* first error. A user `abort(reason)` likewise rejects a running `execute`.
|
|
2827
|
-
* - **`pause` / `resume` / `stop`
|
|
3325
|
+
* - **`pause` / `resume` / `stop` ride the backing Queue.** `pause` / `resume`
|
|
2828
3326
|
* delegate straight to the Queue's own pause/resume (holding/releasing the NEXT
|
|
2829
3327
|
* dispatch while an in-flight unit finishes); `paused` mirrors the Queue's. `stop` is a
|
|
2830
3328
|
* GRACEFUL permanent end, distinct from `abort`: still-pending (never-dispatched)
|
|
2831
3329
|
* units are rejected by the Queue's own stop WITHOUT their handler ever running, and
|
|
2832
3330
|
* `#settle` reads that fact (`#dispatched`) to treat the rejection as a stop artifact —
|
|
2833
3331
|
* not a failure, never tripping fail-fast — while an in-flight unit still runs to
|
|
2834
|
-
* completion and settles normally. `execute` RESOLVES (never rejects)
|
|
3332
|
+
* completion and settles normally. `execute` RESOLVES (never rejects) after every unit
|
|
2835
3333
|
* has settled, with whatever results actually completed.
|
|
2836
|
-
* - **Observable
|
|
3334
|
+
* - **Observable.** The owned {@link emitter} ({@link RunnerEventMap}) carries the run
|
|
2837
3335
|
* lifecycle — `start` / `unit` / `spawn` / `settle` / `fail` / `finish` / `abort` — for
|
|
2838
3336
|
* fire-and-forget observers. Every event is emitted directly, strictly AFTER the relevant
|
|
2839
3337
|
* launch / settle / drain transition; the emitter isolates a listener throw and routes it
|
|
@@ -2896,20 +3394,20 @@ var Runner = class {
|
|
|
2896
3394
|
return this.#queue.paused;
|
|
2897
3395
|
}
|
|
2898
3396
|
/**
|
|
2899
|
-
*
|
|
3397
|
+
* Injects one more unit into an IN-FLIGHT `execute` run — a LIVE counterpart to a
|
|
2900
3398
|
* `Controller.spawn`, called from OUTSIDE any unit's handler.
|
|
2901
3399
|
*
|
|
2902
3400
|
* @remarks
|
|
2903
|
-
* Returns `undefined` synchronously (graceful, non-throwing
|
|
2904
|
-
* runner is
|
|
3401
|
+
* Returns `undefined` synchronously (graceful, non-throwing) unless the
|
|
3402
|
+
* runner is mid-`execute` and not yet stopped — covering "never started",
|
|
2905
3403
|
* "already drained", "aborted", and "destroyed". Otherwise the unit is routed through
|
|
2906
|
-
* the SAME backing queue as a declared/`spawn`ed unit
|
|
3404
|
+
* the SAME backing queue as a declared/`spawn`ed unit through `#launch` — the outstanding-
|
|
2907
3405
|
* unit count gate increments BEFORE this call returns, so an in-flight `execute`
|
|
2908
3406
|
* keeps awaiting it (the drain race: `#running` flips to `false` as the very first
|
|
2909
3407
|
* step after `execute`'s `await drained.promise` settles, so a `spawn` reaching this
|
|
2910
3408
|
* method after the run has fully drained is cleanly rejected with `undefined` —
|
|
2911
3409
|
* never silently dropped, never hangs `execute`). Emits {@link RunnerEventMap.spawn}
|
|
2912
|
-
* with a `parent` of `undefined` (this call has no spawning unit)
|
|
3410
|
+
* with a `parent` of `undefined` (this call has no spawning unit) after acceptance.
|
|
2913
3411
|
*
|
|
2914
3412
|
* @param input - The unit's work payload
|
|
2915
3413
|
* @returns The unit's result promise, or `undefined` when no in-flight run can accept it
|
|
@@ -2926,11 +3424,11 @@ var Runner = class {
|
|
|
2926
3424
|
return this.#launch(input, void 0, true);
|
|
2927
3425
|
}
|
|
2928
3426
|
async execute(inputs) {
|
|
2929
|
-
if (this.#started) throw new
|
|
2930
|
-
if (this.#stopped) throw new
|
|
3427
|
+
if (this.#started) throw new WorkflowError("TRANSITION", "runner has already executed", { started: true });
|
|
3428
|
+
if (this.#stopped) throw new WorkflowError("TRANSITION", "runner is stopped", { stopped: true });
|
|
2931
3429
|
this.#started = true;
|
|
2932
3430
|
this.#running = true;
|
|
2933
|
-
const drained =
|
|
3431
|
+
const drained = Promise.withResolvers();
|
|
2934
3432
|
this.#drained = drained;
|
|
2935
3433
|
for (const input of inputs) {
|
|
2936
3434
|
if (!this.#accepts()) break;
|
|
@@ -2951,23 +3449,23 @@ var Runner = class {
|
|
|
2951
3449
|
}
|
|
2952
3450
|
abort(reason) {
|
|
2953
3451
|
if (this.#abortPromise !== void 0) return this.#abortPromise;
|
|
2954
|
-
const barrier =
|
|
3452
|
+
const barrier = Promise.withResolvers();
|
|
2955
3453
|
this.#abortPromise = barrier.promise;
|
|
2956
3454
|
barrier.promise.catch(() => {});
|
|
2957
|
-
if (this.#running && this.#failure === void 0) this.#failure =
|
|
3455
|
+
if (this.#running && this.#failure === void 0) this.#failure = failure(reason === void 0 ? /* @__PURE__ */ new Error("runner aborted") : reason);
|
|
2958
3456
|
this.#cancel(reason);
|
|
2959
3457
|
this.#stopped = true;
|
|
2960
3458
|
const cleanup = this.#queue.abort(reason);
|
|
2961
|
-
this.#
|
|
3459
|
+
this.#settleBarrier(barrier, cleanup, false);
|
|
2962
3460
|
this.#emitter.emit("abort", reason);
|
|
2963
3461
|
return barrier.promise;
|
|
2964
3462
|
}
|
|
2965
3463
|
/**
|
|
2966
|
-
*
|
|
3464
|
+
* Suspends dispatch (resumable): delegates to the backing queue's own
|
|
2967
3465
|
* `pause`, which holds the NEXT dispatch while any in-flight unit finishes.
|
|
2968
3466
|
*
|
|
2969
3467
|
* @remarks
|
|
2970
|
-
* A no-op
|
|
3468
|
+
* A no-op after the runner is `stopped` — a stopped runner has no dispatch left to
|
|
2971
3469
|
* suspend, mirroring the guard `stop()` itself applies. Also a no-op when already
|
|
2972
3470
|
* `paused` (the queue's own `pause` is idempotent), so calling it repeatedly is safe.
|
|
2973
3471
|
*/
|
|
@@ -2976,11 +3474,11 @@ var Runner = class {
|
|
|
2976
3474
|
this.#queue.pause();
|
|
2977
3475
|
}
|
|
2978
3476
|
/**
|
|
2979
|
-
*
|
|
3477
|
+
* Continues a paused runner; delegates to the backing queue's `resume`.
|
|
2980
3478
|
*
|
|
2981
3479
|
* @remarks
|
|
2982
|
-
* A no-op
|
|
2983
|
-
* runner is not
|
|
3480
|
+
* A no-op after the runner is `stopped` (nothing left to resume) and a no-op when the
|
|
3481
|
+
* runner is not `paused`, so calling it repeatedly or on a never-paused
|
|
2984
3482
|
* runner is safe.
|
|
2985
3483
|
*/
|
|
2986
3484
|
resume() {
|
|
@@ -2988,7 +3486,7 @@ var Runner = class {
|
|
|
2988
3486
|
this.#queue.resume();
|
|
2989
3487
|
}
|
|
2990
3488
|
/**
|
|
2991
|
-
*
|
|
3489
|
+
* Ends the runner permanently — a GRACEFUL stop, distinct from `abort`.
|
|
2992
3490
|
* Marks the runner `stopping` + `stopped`, then stops the backing queue: every
|
|
2993
3491
|
* still-PENDING (never-dispatched) unit is rejected by the queue with its own
|
|
2994
3492
|
* "queue is stopped" error, WITHOUT running its handler; every already-in-flight unit
|
|
@@ -3001,24 +3499,24 @@ var Runner = class {
|
|
|
3001
3499
|
if (this.#destroyPromise !== void 0) return this.#destroyPromise;
|
|
3002
3500
|
if (this.#abortPromise !== void 0) return this.#abortPromise;
|
|
3003
3501
|
if (this.#stopPromise !== void 0) return this.#stopPromise;
|
|
3004
|
-
const barrier =
|
|
3502
|
+
const barrier = Promise.withResolvers();
|
|
3005
3503
|
this.#stopPromise = barrier.promise;
|
|
3006
3504
|
barrier.promise.catch(() => {});
|
|
3007
3505
|
this.#stopping = true;
|
|
3008
3506
|
this.#stopped = true;
|
|
3009
3507
|
const cleanup = this.#queue.stop();
|
|
3010
|
-
this.#
|
|
3508
|
+
this.#settleBarrier(barrier, cleanup, false);
|
|
3011
3509
|
return barrier.promise;
|
|
3012
3510
|
}
|
|
3013
3511
|
destroy() {
|
|
3014
3512
|
if (this.#destroyPromise !== void 0) return this.#destroyPromise;
|
|
3015
|
-
const barrier =
|
|
3513
|
+
const barrier = Promise.withResolvers();
|
|
3016
3514
|
this.#destroyPromise = barrier.promise;
|
|
3017
3515
|
barrier.promise.catch(() => {});
|
|
3018
3516
|
this.#stopped = true;
|
|
3019
3517
|
this.abort();
|
|
3020
3518
|
const cleanup = this.#queue.destroy();
|
|
3021
|
-
this.#
|
|
3519
|
+
this.#settleBarrier(barrier, cleanup, true);
|
|
3022
3520
|
return barrier.promise;
|
|
3023
3521
|
}
|
|
3024
3522
|
#launch(input, parent, announce = parent !== void 0) {
|
|
@@ -3046,33 +3544,27 @@ var Runner = class {
|
|
|
3046
3544
|
} catch (error) {
|
|
3047
3545
|
promise = Promise.reject(error);
|
|
3048
3546
|
}
|
|
3049
|
-
promise.then((value) => this.#settle(id,
|
|
3050
|
-
ok: true,
|
|
3051
|
-
value
|
|
3052
|
-
}), (error) => this.#settle(id, {
|
|
3053
|
-
ok: false,
|
|
3054
|
-
error
|
|
3055
|
-
}));
|
|
3547
|
+
promise.then((value) => this.#settle(id, success(value)), (error) => this.#settle(id, failure(error)));
|
|
3056
3548
|
return promise;
|
|
3057
3549
|
}
|
|
3058
|
-
#dispatch(unit,
|
|
3550
|
+
#dispatch(unit, context) {
|
|
3059
3551
|
const abort = this.#aborts.get(unit.id);
|
|
3060
|
-
if (abort === void 0) throw new
|
|
3552
|
+
if (abort === void 0) throw new WorkflowError("INVARIANT", "unit abort missing", { unit: unit.id });
|
|
3061
3553
|
this.#dispatched.add(unit.id);
|
|
3062
|
-
const controller = new Controller(unit.id, unit.input, abort,
|
|
3554
|
+
const controller = new Controller(unit.id, unit.input, abort, context.signal, (input) => this.#spawn(input, unit.id));
|
|
3063
3555
|
this.#emitter.emit("unit", unit.id);
|
|
3064
3556
|
return this.#handler(controller);
|
|
3065
3557
|
}
|
|
3066
3558
|
#spawn(input, parent) {
|
|
3067
|
-
if (!this.#accepts()) throw new
|
|
3559
|
+
if (!this.#accepts()) throw new WorkflowError("TRANSITION", "spawn is unavailable outside an active run", { parent });
|
|
3068
3560
|
return this.#launch(input, parent);
|
|
3069
3561
|
}
|
|
3070
3562
|
#settle(id, outcome) {
|
|
3071
|
-
if (outcome.
|
|
3072
|
-
this.#values.set(id,
|
|
3563
|
+
if (outcome.success) {
|
|
3564
|
+
this.#values.set(id, outcome);
|
|
3073
3565
|
this.#emitter.emit("settle", id);
|
|
3074
3566
|
} else if (this.#stopping && this.#queued.has(id) && !this.#dispatched.has(id)) {} else if (this.#failure === void 0) {
|
|
3075
|
-
this.#failure =
|
|
3567
|
+
this.#failure = failure(outcome.error);
|
|
3076
3568
|
this.#emitter.emit("fail", id, outcome.error);
|
|
3077
3569
|
this.abort(outcome.error);
|
|
3078
3570
|
}
|
|
@@ -3097,31 +3589,20 @@ var Runner = class {
|
|
|
3097
3589
|
await cleanup;
|
|
3098
3590
|
return;
|
|
3099
3591
|
} catch (error) {
|
|
3100
|
-
return
|
|
3592
|
+
return failure(error);
|
|
3101
3593
|
}
|
|
3102
3594
|
}
|
|
3103
|
-
async #
|
|
3104
|
-
let
|
|
3595
|
+
async #settleBarrier(barrier, cleanup, teardown) {
|
|
3596
|
+
let cleanupFailure;
|
|
3105
3597
|
try {
|
|
3106
3598
|
await cleanup;
|
|
3107
3599
|
} catch (error) {
|
|
3108
|
-
|
|
3600
|
+
cleanupFailure = failure(error);
|
|
3109
3601
|
}
|
|
3110
3602
|
await this.#waitDrain();
|
|
3111
|
-
if (
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
async #settleDestroy(barrier, cleanup) {
|
|
3115
|
-
let failure;
|
|
3116
|
-
try {
|
|
3117
|
-
await cleanup;
|
|
3118
|
-
} catch (error) {
|
|
3119
|
-
failure = { error };
|
|
3120
|
-
}
|
|
3121
|
-
await this.#waitDrain();
|
|
3122
|
-
this.#emitter.destroy();
|
|
3123
|
-
if (failure === void 0) barrier.resolve();
|
|
3124
|
-
else barrier.reject(failure.error);
|
|
3603
|
+
if (teardown) this.#emitter.destroy();
|
|
3604
|
+
if (cleanupFailure === void 0) barrier.resolve();
|
|
3605
|
+
else barrier.reject(cleanupFailure.error);
|
|
3125
3606
|
}
|
|
3126
3607
|
async #waitDrain() {
|
|
3127
3608
|
if (this.#count === 0) return;
|
|
@@ -3134,7 +3615,7 @@ var Runner = class {
|
|
|
3134
3615
|
//#endregion
|
|
3135
3616
|
//#region src/core/tasks/TaskController.ts
|
|
3136
3617
|
/**
|
|
3137
|
-
*
|
|
3618
|
+
* Implements the attempt-scoped handle a {@link import('./types.js').WorkflowFunction} receives.
|
|
3138
3619
|
*
|
|
3139
3620
|
* @remarks
|
|
3140
3621
|
* - **A leaf handle, NOT the runner `Controller`.** A workflow task is a leaf of the
|
|
@@ -3157,26 +3638,38 @@ var Runner = class {
|
|
|
3157
3638
|
* observe the W-b entities' own emitters (`task.emitter` / `phase.emitter`) instead.
|
|
3158
3639
|
*/
|
|
3159
3640
|
var TaskController = class {
|
|
3160
|
-
signal;
|
|
3161
|
-
input;
|
|
3162
|
-
task;
|
|
3163
|
-
attempt;
|
|
3641
|
+
#signal;
|
|
3642
|
+
#input;
|
|
3643
|
+
#task;
|
|
3644
|
+
#attempt;
|
|
3164
3645
|
#entity;
|
|
3165
3646
|
#report;
|
|
3166
3647
|
#pulse;
|
|
3167
3648
|
#results;
|
|
3168
3649
|
constructor(signal, input, task, attempt, results, report, pulse) {
|
|
3169
|
-
this
|
|
3170
|
-
this
|
|
3171
|
-
this
|
|
3172
|
-
this
|
|
3650
|
+
this.#signal = signal;
|
|
3651
|
+
this.#input = input;
|
|
3652
|
+
this.#task = task.context;
|
|
3653
|
+
this.#attempt = attempt;
|
|
3173
3654
|
this.#entity = task;
|
|
3174
3655
|
this.#results = results;
|
|
3175
3656
|
this.#report = report;
|
|
3176
3657
|
this.#pulse = pulse;
|
|
3177
3658
|
}
|
|
3659
|
+
get signal() {
|
|
3660
|
+
return this.#signal;
|
|
3661
|
+
}
|
|
3662
|
+
get input() {
|
|
3663
|
+
return this.#input;
|
|
3664
|
+
}
|
|
3665
|
+
get task() {
|
|
3666
|
+
return this.#task;
|
|
3667
|
+
}
|
|
3668
|
+
get attempt() {
|
|
3669
|
+
return this.#attempt;
|
|
3670
|
+
}
|
|
3178
3671
|
get aborted() {
|
|
3179
|
-
return this
|
|
3672
|
+
return this.#signal.aborted;
|
|
3180
3673
|
}
|
|
3181
3674
|
get paused() {
|
|
3182
3675
|
if (this.#ancestorTerminal()) return false;
|
|
@@ -3189,7 +3682,7 @@ var TaskController = class {
|
|
|
3189
3682
|
return this.#pulse();
|
|
3190
3683
|
}
|
|
3191
3684
|
async wait() {
|
|
3192
|
-
while (this.paused && !this
|
|
3685
|
+
while (this.paused && !this.#signal.aborted) await this.#race(this.#gates());
|
|
3193
3686
|
}
|
|
3194
3687
|
results() {
|
|
3195
3688
|
return this.#results();
|
|
@@ -3203,11 +3696,11 @@ var TaskController = class {
|
|
|
3203
3696
|
return gates;
|
|
3204
3697
|
}
|
|
3205
3698
|
async #race(gates) {
|
|
3206
|
-
if (this
|
|
3699
|
+
if (this.#signal.aborted || gates.length === 0) return;
|
|
3207
3700
|
const deferred = Promise.withResolvers();
|
|
3208
3701
|
const onAbort = this.#resolve.bind(this, deferred);
|
|
3209
3702
|
const onTerminal = this.#resolve.bind(this, deferred);
|
|
3210
|
-
this
|
|
3703
|
+
this.#signal.addEventListener("abort", onAbort, { once: true });
|
|
3211
3704
|
this.#entity.workflow.emitter.on("skip", onTerminal);
|
|
3212
3705
|
this.#entity.workflow.emitter.on("stop", onTerminal);
|
|
3213
3706
|
this.#entity.phase.emitter.on("skip", onTerminal);
|
|
@@ -3216,7 +3709,7 @@ var TaskController = class {
|
|
|
3216
3709
|
if (this.#ancestorTerminal()) deferred.resolve();
|
|
3217
3710
|
await Promise.race([Promise.all(gates), deferred.promise]);
|
|
3218
3711
|
} finally {
|
|
3219
|
-
this
|
|
3712
|
+
this.#signal.removeEventListener("abort", onAbort);
|
|
3220
3713
|
this.#entity.workflow.emitter.off("skip", onTerminal);
|
|
3221
3714
|
this.#entity.workflow.emitter.off("stop", onTerminal);
|
|
3222
3715
|
this.#entity.phase.emitter.off("skip", onTerminal);
|
|
@@ -3233,24 +3726,33 @@ var TaskController = class {
|
|
|
3233
3726
|
//#endregion
|
|
3234
3727
|
//#region src/core/WorkflowPersistence.ts
|
|
3235
3728
|
/**
|
|
3236
|
-
*
|
|
3729
|
+
* Coordinates advanced run-local snapshot persistence with one writer and one coalesced most recent obligation.
|
|
3237
3730
|
*
|
|
3238
3731
|
* @remarks
|
|
3239
3732
|
* Normally composed by `WorkflowRunner.execute({ store })`; exported for hosts that need to
|
|
3240
3733
|
* coordinate the same required boundaries around their own runner integration.
|
|
3734
|
+
*
|
|
3735
|
+
* @example
|
|
3736
|
+
* ```ts
|
|
3737
|
+
* import { WorkflowPersistence, createMemoryWorkflowStore, createWorkflow } from '@orkestrel/workflow'
|
|
3738
|
+
*
|
|
3739
|
+
* const workflow = createWorkflow({ id: 'durable', name: 'Durable', phases: [] })
|
|
3740
|
+
* const persistence = new WorkflowPersistence(workflow, createMemoryWorkflowStore())
|
|
3741
|
+
* await persistence.checkpoint('initial')
|
|
3742
|
+
* const durable = await persistence.finalize()
|
|
3743
|
+
* persistence.detach() // idempotent after finalize
|
|
3744
|
+
* ```
|
|
3241
3745
|
*/
|
|
3242
3746
|
var WorkflowPersistence = class {
|
|
3243
3747
|
#workflow;
|
|
3244
3748
|
#store;
|
|
3245
3749
|
#phases = /* @__PURE__ */ new Set();
|
|
3246
3750
|
#tasks = /* @__PURE__ */ new Set();
|
|
3247
|
-
#
|
|
3751
|
+
#onChange;
|
|
3248
3752
|
#onWorkflowAdd;
|
|
3249
3753
|
#onWorkflowRemove;
|
|
3250
|
-
#onPhaseChange;
|
|
3251
3754
|
#onPhaseAdd;
|
|
3252
3755
|
#onPhaseRemove;
|
|
3253
|
-
#onTaskChange;
|
|
3254
3756
|
#writing;
|
|
3255
3757
|
#error;
|
|
3256
3758
|
#fault;
|
|
@@ -3260,32 +3762,29 @@ var WorkflowPersistence = class {
|
|
|
3260
3762
|
constructor(workflow, store) {
|
|
3261
3763
|
this.#workflow = workflow;
|
|
3262
3764
|
this.#store = store;
|
|
3263
|
-
this.#
|
|
3765
|
+
this.#onChange = this.#change.bind(this);
|
|
3264
3766
|
this.#onWorkflowAdd = this.#addPhase.bind(this);
|
|
3265
3767
|
this.#onWorkflowRemove = this.#removePhase.bind(this);
|
|
3266
|
-
this.#onPhaseChange = this.#change.bind(this);
|
|
3267
3768
|
this.#onPhaseAdd = this.#addTask.bind(this);
|
|
3268
3769
|
this.#onPhaseRemove = this.#removeTask.bind(this);
|
|
3269
|
-
this.#onTaskChange = this.#change.bind(this);
|
|
3270
3770
|
this.#attachWorkflow();
|
|
3271
3771
|
}
|
|
3272
3772
|
get fault() {
|
|
3273
3773
|
return this.#fault;
|
|
3274
3774
|
}
|
|
3275
3775
|
/**
|
|
3276
|
-
*
|
|
3776
|
+
* Persists every change through this required boundary.
|
|
3277
3777
|
*
|
|
3278
3778
|
* @param checkpoint - The boundary being made durable
|
|
3279
3779
|
* @param task - The task owning an attempt or settlement
|
|
3280
3780
|
* @param attempt - The persisted attempt number
|
|
3281
|
-
* @returns
|
|
3781
|
+
* @returns True if the most recent state reached the store; false otherwise
|
|
3282
3782
|
*/
|
|
3283
3783
|
async checkpoint(checkpoint, task, attempt) {
|
|
3284
3784
|
const revision = this.#mark();
|
|
3285
3785
|
while (this.#stored < revision) await this.#flush();
|
|
3286
3786
|
if (this.#error === void 0) return true;
|
|
3287
3787
|
if (this.#fault === void 0) this.#fault = Object.freeze({
|
|
3288
|
-
origin: "persistence",
|
|
3289
3788
|
checkpoint,
|
|
3290
3789
|
message: this.#error,
|
|
3291
3790
|
...task === void 0 ? {} : { task: task.id },
|
|
@@ -3294,37 +3793,25 @@ var WorkflowPersistence = class {
|
|
|
3294
3793
|
return false;
|
|
3295
3794
|
}
|
|
3296
3795
|
/**
|
|
3297
|
-
*
|
|
3796
|
+
* Stops observing the live tree and persists its final state.
|
|
3298
3797
|
*
|
|
3299
|
-
* @returns
|
|
3798
|
+
* @returns True if the final snapshot reached the store; false otherwise
|
|
3300
3799
|
*/
|
|
3301
3800
|
async finalize() {
|
|
3302
3801
|
this.detach();
|
|
3303
3802
|
return this.checkpoint("final");
|
|
3304
3803
|
}
|
|
3305
|
-
/**
|
|
3804
|
+
/** Stops observing the live tree. */
|
|
3306
3805
|
detach() {
|
|
3307
3806
|
if (!this.#attached) return;
|
|
3308
3807
|
this.#attached = false;
|
|
3309
|
-
this.#workflow.emitter.off(
|
|
3310
|
-
this.#workflow.emitter.off("complete", this.#onWorkflowChange);
|
|
3311
|
-
this.#workflow.emitter.off("fail", this.#onWorkflowChange);
|
|
3312
|
-
this.#workflow.emitter.off("skip", this.#onWorkflowChange);
|
|
3313
|
-
this.#workflow.emitter.off("stop", this.#onWorkflowChange);
|
|
3314
|
-
this.#workflow.emitter.off("move", this.#onWorkflowChange);
|
|
3315
|
-
this.#workflow.emitter.off("update", this.#onWorkflowChange);
|
|
3808
|
+
for (const event of PERSISTED_NODE_EVENTS) this.#workflow.emitter.off(event, this.#onChange);
|
|
3316
3809
|
this.#workflow.emitter.off("add", this.#onWorkflowAdd);
|
|
3317
3810
|
this.#workflow.emitter.off("remove", this.#onWorkflowRemove);
|
|
3318
3811
|
for (const phase of this.#phases) this.#detachPhase(phase);
|
|
3319
3812
|
}
|
|
3320
3813
|
#attachWorkflow() {
|
|
3321
|
-
this.#workflow.emitter.on(
|
|
3322
|
-
this.#workflow.emitter.on("complete", this.#onWorkflowChange);
|
|
3323
|
-
this.#workflow.emitter.on("fail", this.#onWorkflowChange);
|
|
3324
|
-
this.#workflow.emitter.on("skip", this.#onWorkflowChange);
|
|
3325
|
-
this.#workflow.emitter.on("stop", this.#onWorkflowChange);
|
|
3326
|
-
this.#workflow.emitter.on("move", this.#onWorkflowChange);
|
|
3327
|
-
this.#workflow.emitter.on("update", this.#onWorkflowChange);
|
|
3814
|
+
for (const event of PERSISTED_NODE_EVENTS) this.#workflow.emitter.on(event, this.#onChange);
|
|
3328
3815
|
this.#workflow.emitter.on("add", this.#onWorkflowAdd);
|
|
3329
3816
|
this.#workflow.emitter.on("remove", this.#onWorkflowRemove);
|
|
3330
3817
|
for (const phase of this.#workflow.phases.phases()) this.#attachPhase(phase);
|
|
@@ -3332,26 +3819,14 @@ var WorkflowPersistence = class {
|
|
|
3332
3819
|
#attachPhase(phase) {
|
|
3333
3820
|
if (this.#phases.has(phase)) return;
|
|
3334
3821
|
this.#phases.add(phase);
|
|
3335
|
-
phase.emitter.on(
|
|
3336
|
-
phase.emitter.on("complete", this.#onPhaseChange);
|
|
3337
|
-
phase.emitter.on("fail", this.#onPhaseChange);
|
|
3338
|
-
phase.emitter.on("skip", this.#onPhaseChange);
|
|
3339
|
-
phase.emitter.on("stop", this.#onPhaseChange);
|
|
3340
|
-
phase.emitter.on("move", this.#onPhaseChange);
|
|
3341
|
-
phase.emitter.on("update", this.#onPhaseChange);
|
|
3822
|
+
for (const event of PERSISTED_NODE_EVENTS) phase.emitter.on(event, this.#onChange);
|
|
3342
3823
|
phase.emitter.on("add", this.#onPhaseAdd);
|
|
3343
3824
|
phase.emitter.on("remove", this.#onPhaseRemove);
|
|
3344
3825
|
for (const task of phase.tasks.tasks()) this.#attachTask(task);
|
|
3345
3826
|
}
|
|
3346
3827
|
#detachPhase(phase) {
|
|
3347
3828
|
if (!this.#phases.delete(phase)) return;
|
|
3348
|
-
phase.emitter.off(
|
|
3349
|
-
phase.emitter.off("complete", this.#onPhaseChange);
|
|
3350
|
-
phase.emitter.off("fail", this.#onPhaseChange);
|
|
3351
|
-
phase.emitter.off("skip", this.#onPhaseChange);
|
|
3352
|
-
phase.emitter.off("stop", this.#onPhaseChange);
|
|
3353
|
-
phase.emitter.off("move", this.#onPhaseChange);
|
|
3354
|
-
phase.emitter.off("update", this.#onPhaseChange);
|
|
3829
|
+
for (const event of PERSISTED_NODE_EVENTS) phase.emitter.off(event, this.#onChange);
|
|
3355
3830
|
phase.emitter.off("add", this.#onPhaseAdd);
|
|
3356
3831
|
phase.emitter.off("remove", this.#onPhaseRemove);
|
|
3357
3832
|
for (const task of phase.tasks.tasks()) this.#detachTask(task);
|
|
@@ -3359,23 +3834,11 @@ var WorkflowPersistence = class {
|
|
|
3359
3834
|
#attachTask(task) {
|
|
3360
3835
|
if (this.#tasks.has(task)) return;
|
|
3361
3836
|
this.#tasks.add(task);
|
|
3362
|
-
task.emitter.on(
|
|
3363
|
-
task.emitter.on("complete", this.#onTaskChange);
|
|
3364
|
-
task.emitter.on("fail", this.#onTaskChange);
|
|
3365
|
-
task.emitter.on("skip", this.#onTaskChange);
|
|
3366
|
-
task.emitter.on("stop", this.#onTaskChange);
|
|
3367
|
-
task.emitter.on("report", this.#onTaskChange);
|
|
3368
|
-
task.emitter.on("pulse", this.#onTaskChange);
|
|
3837
|
+
for (const event of PERSISTED_TASK_EVENTS) task.emitter.on(event, this.#onChange);
|
|
3369
3838
|
}
|
|
3370
3839
|
#detachTask(task) {
|
|
3371
3840
|
if (!this.#tasks.delete(task)) return;
|
|
3372
|
-
task.emitter.off(
|
|
3373
|
-
task.emitter.off("complete", this.#onTaskChange);
|
|
3374
|
-
task.emitter.off("fail", this.#onTaskChange);
|
|
3375
|
-
task.emitter.off("skip", this.#onTaskChange);
|
|
3376
|
-
task.emitter.off("stop", this.#onTaskChange);
|
|
3377
|
-
task.emitter.off("report", this.#onTaskChange);
|
|
3378
|
-
task.emitter.off("pulse", this.#onTaskChange);
|
|
3841
|
+
for (const event of PERSISTED_TASK_EVENTS) task.emitter.off(event, this.#onChange);
|
|
3379
3842
|
}
|
|
3380
3843
|
#addPhase(phase) {
|
|
3381
3844
|
this.#attachPhase(phase);
|
|
@@ -3433,13 +3896,14 @@ var WorkflowPersistence = class {
|
|
|
3433
3896
|
//#endregion
|
|
3434
3897
|
//#region src/core/WorkflowRunner.ts
|
|
3435
3898
|
/**
|
|
3436
|
-
*
|
|
3899
|
+
* Implements the thin orchestrator that EXECUTES a live W-b workflow tree by COMPOSING the shipped
|
|
3437
3900
|
* substrate — phases sequential, tasks concurrent — dispatching each task through its OWN
|
|
3438
3901
|
* resolved handler under the `bail` policy.
|
|
3439
3902
|
*
|
|
3440
3903
|
* @remarks
|
|
3441
3904
|
* - **Composes, never re-implements.** Per-phase bounded concurrency is one
|
|
3442
|
-
* {@link createRunner} per phase (the substrate {@link RunnerInterface}
|
|
3905
|
+
* {@link createRunner} per phase (the substrate {@link import('./types.js').RunnerInterface}
|
|
3906
|
+
* over the workers
|
|
3443
3907
|
* `Queue`); `bail` maps onto that Runner's fail-fast vs settle-all; the run-level abort /
|
|
3444
3908
|
* timeout / budget / entity `signal` fold through the `@orkestrel/abort` signal contract,
|
|
3445
3909
|
* {@link createTimeout}, and `AbortSignal.any` (exactly as the agent runtime folds its bounds);
|
|
@@ -3453,15 +3917,15 @@ var WorkflowPersistence = class {
|
|
|
3453
3917
|
* resolved its own {@link import('./types.js').WorkflowFunction} into
|
|
3454
3918
|
* {@link import('./types.js').TaskInterface.handler} ONCE at construction (build, restore,
|
|
3455
3919
|
* or a live mint all resolve it identically, from {@link WorkflowOptions.functions}), so
|
|
3456
|
-
* dispatch is
|
|
3920
|
+
* dispatch is "invoke the task's own handler". Provider, protocol, and tool
|
|
3457
3921
|
* integrations remain application-owned {@link import('./types.js').WorkflowFunction}s
|
|
3458
3922
|
* composed into {@link WorkflowOptions.functions}. This module imports none of them.
|
|
3459
3923
|
* - **Two `execute` forms, one engine.** `execute(definition, options)` BUILDS the live tree
|
|
3460
|
-
* from a {@link WorkflowDefinition} (single source of truth for the `
|
|
3924
|
+
* from a {@link WorkflowDefinition} (single source of truth for the `behavior` / `concurrency`
|
|
3461
3925
|
* metadata); `execute(workflow, options)` DRIVES a caller-owned, ALREADY-BUILT
|
|
3462
|
-
* {@link WorkflowInterface} instead — the entity-native control surface
|
|
3463
|
-
* `pause` / `resume` / `add` / `stop` / `destroy` live on the entity itself). Both forms
|
|
3464
|
-
* converge on the SAME `#execute` engine: neither reads a `WorkflowDefinition`
|
|
3926
|
+
* {@link WorkflowInterface} instead — the entity-native control surface
|
|
3927
|
+
* (`pause` / `resume` / `add` / `stop` / `destroy` live on the entity itself). Both forms
|
|
3928
|
+
* converge on the SAME `#execute` engine: neither reads a `WorkflowDefinition` after the tree
|
|
3465
3929
|
* exists — `#runTask` reads each task's OWN {@link import('./types.js').TaskInterface.handler}
|
|
3466
3930
|
* / `retries` / `timeout`, and `#runPhase` reads each phase's OWN
|
|
3467
3931
|
* {@link PhaseInterface.concurrency} / `bail`, so a live `add`-minted phase or task (V5)
|
|
@@ -3475,7 +3939,7 @@ var WorkflowPersistence = class {
|
|
|
3475
3939
|
* for `spawn` to accept (the runner already drained) is swept `skip`ped afterward so the
|
|
3476
3940
|
* phase always reaches a coherent terminal state.
|
|
3477
3941
|
* - **Dispatch by handler.** `#runTask` invokes the live task's own
|
|
3478
|
-
* {@link import('./types.js').TaskInterface.handler} directly. An omitted `
|
|
3942
|
+
* {@link import('./types.js').TaskInterface.handler} directly. An omitted `behavior` deliberately
|
|
3479
3943
|
* auto-completes with JSON `null`; a present unresolved name is rejected by the synchronous
|
|
3480
3944
|
* execution claim and never false-completes.
|
|
3481
3945
|
* - **`bail` → substrate.** Under `bail: true` (halt) a genuine task failure `fail`s the leaf
|
|
@@ -3506,9 +3970,9 @@ var WorkflowPersistence = class {
|
|
|
3506
3970
|
* and the workflow is force-`stop`ped (settles `stopped`). Each task's
|
|
3507
3971
|
* {@link TaskController} signal `AbortSignal.any`-combines the substrate per-unit signal with
|
|
3508
3972
|
* `runSignal`, so a handler observes either cause directly.
|
|
3509
|
-
* - **Re-entrant-safe.** No shared per-run mutable field:
|
|
3510
|
-
*
|
|
3511
|
-
* state.
|
|
3973
|
+
* - **Re-entrant-safe.** No shared per-run mutable field: each `#execute` mints its own
|
|
3974
|
+
* {@link import('./RunHolder.js').RunHolder}, so a nested application-level `execute` cannot
|
|
3975
|
+
* clobber the outer run's state.
|
|
3512
3976
|
*/
|
|
3513
3977
|
var WorkflowRunner = class WorkflowRunner {
|
|
3514
3978
|
static #executions = /* @__PURE__ */ new WeakSet();
|
|
@@ -3517,7 +3981,7 @@ var WorkflowRunner = class WorkflowRunner {
|
|
|
3517
3981
|
this.#scheduler = scheduler;
|
|
3518
3982
|
}
|
|
3519
3983
|
execute(target, options) {
|
|
3520
|
-
if (
|
|
3984
|
+
if (isWorkflowInterface(target)) {
|
|
3521
3985
|
const signal = options?.signal;
|
|
3522
3986
|
const timeout = options?.timeout;
|
|
3523
3987
|
const budget = options?.budget;
|
|
@@ -3530,7 +3994,7 @@ var WorkflowRunner = class WorkflowRunner {
|
|
|
3530
3994
|
const timeout = options?.timeout;
|
|
3531
3995
|
const budget = options?.budget;
|
|
3532
3996
|
const store = options?.store;
|
|
3533
|
-
const workflow =
|
|
3997
|
+
const workflow = createWorkflowTree(target, captured);
|
|
3534
3998
|
this.#acquire(workflow);
|
|
3535
3999
|
return this.#execute(workflow, signal, timeout, budget, store);
|
|
3536
4000
|
}
|
|
@@ -3544,7 +4008,7 @@ var WorkflowRunner = class WorkflowRunner {
|
|
|
3544
4008
|
WorkflowRunner.#executions.add(workflow);
|
|
3545
4009
|
}
|
|
3546
4010
|
async #execute(workflow, signal, ms, budget, store) {
|
|
3547
|
-
const holder =
|
|
4011
|
+
const holder = new RunHolder();
|
|
3548
4012
|
let timeout;
|
|
3549
4013
|
let persistence;
|
|
3550
4014
|
let runSignal;
|
|
@@ -3559,7 +4023,7 @@ var WorkflowRunner = class WorkflowRunner {
|
|
|
3559
4023
|
if (runSignal.aborted) onCancel();
|
|
3560
4024
|
else runSignal.addEventListener("abort", onCancel, { once: true });
|
|
3561
4025
|
if (persistence !== void 0 && !await persistence.checkpoint("initial")) {
|
|
3562
|
-
if (
|
|
4026
|
+
if (isStoppable(workflow)) workflow.stop();
|
|
3563
4027
|
this.#skipFrom(workflow.phases.phases(), 0);
|
|
3564
4028
|
}
|
|
3565
4029
|
let index = 0;
|
|
@@ -3571,12 +4035,12 @@ var WorkflowRunner = class WorkflowRunner {
|
|
|
3571
4035
|
index += 1;
|
|
3572
4036
|
continue;
|
|
3573
4037
|
}
|
|
3574
|
-
if (
|
|
4038
|
+
if (runSignal.aborted || isHalted(workflow)) {
|
|
3575
4039
|
this.#haltFrom(phases, index, workflow, runSignal);
|
|
3576
4040
|
break;
|
|
3577
4041
|
}
|
|
3578
4042
|
if (workflow.paused) await this.#raceWait(workflow.wait(), runSignal, void 0, workflow);
|
|
3579
|
-
if (
|
|
4043
|
+
if (runSignal.aborted || isHalted(workflow)) {
|
|
3580
4044
|
this.#haltFrom(workflow.phases.phases(), index, workflow, runSignal);
|
|
3581
4045
|
break;
|
|
3582
4046
|
}
|
|
@@ -3591,10 +4055,10 @@ var WorkflowRunner = class WorkflowRunner {
|
|
|
3591
4055
|
}
|
|
3592
4056
|
index += 1;
|
|
3593
4057
|
const remaining = workflow.phases.phases();
|
|
3594
|
-
if (index < remaining.length && !
|
|
4058
|
+
if (index < remaining.length && !runSignal.aborted) await this.#pace(runSignal);
|
|
3595
4059
|
}
|
|
3596
|
-
if (
|
|
3597
|
-
else if (
|
|
4060
|
+
if (runSignal.aborted) this.#haltFrom(workflow.phases.phases(), 0, workflow, runSignal);
|
|
4061
|
+
else if (isCompletable(workflow)) workflow.complete();
|
|
3598
4062
|
const durable = await persistence?.finalize();
|
|
3599
4063
|
return {
|
|
3600
4064
|
workflow,
|
|
@@ -3604,7 +4068,7 @@ var WorkflowRunner = class WorkflowRunner {
|
|
|
3604
4068
|
...persistence?.fault === void 0 ? {} : { fault: persistence.fault }
|
|
3605
4069
|
};
|
|
3606
4070
|
} catch (error) {
|
|
3607
|
-
if (
|
|
4071
|
+
if (isStoppable(workflow)) workflow.stop();
|
|
3608
4072
|
this.#skipFrom(workflow.phases.phases(), 0);
|
|
3609
4073
|
await persistence?.finalize();
|
|
3610
4074
|
throw error;
|
|
@@ -3639,22 +4103,22 @@ var WorkflowRunner = class WorkflowRunner {
|
|
|
3639
4103
|
entries: this.#entry.bind(this),
|
|
3640
4104
|
handler: this.#runUnit.bind(this, workflow, runSignal, bail, attempts, owners, persistence)
|
|
3641
4105
|
});
|
|
3642
|
-
holder.
|
|
4106
|
+
holder.hold(created);
|
|
3643
4107
|
try {
|
|
3644
4108
|
await created.execute(tasks);
|
|
3645
4109
|
return false;
|
|
3646
4110
|
} catch {
|
|
3647
|
-
return !
|
|
4111
|
+
return !runSignal.aborted;
|
|
3648
4112
|
} finally {
|
|
3649
4113
|
try {
|
|
3650
4114
|
await created.destroy();
|
|
3651
4115
|
} finally {
|
|
3652
|
-
holder.
|
|
4116
|
+
holder.hold();
|
|
3653
4117
|
}
|
|
3654
4118
|
}
|
|
3655
4119
|
} finally {
|
|
3656
4120
|
phase.emitter.off("add", onAdd);
|
|
3657
|
-
if (
|
|
4121
|
+
if (runSignal.aborted && isStoppable(workflow)) workflow.stop();
|
|
3658
4122
|
for (const task of phase.tasks.tasks()) this.#skip(task);
|
|
3659
4123
|
}
|
|
3660
4124
|
}
|
|
@@ -3678,7 +4142,7 @@ var WorkflowRunner = class WorkflowRunner {
|
|
|
3678
4142
|
attempts.set(task.id, attempt);
|
|
3679
4143
|
const last = attempt > Math.max(0, task.retries ?? 0);
|
|
3680
4144
|
if (task.status !== "pending" && task.status !== "running") return;
|
|
3681
|
-
if (
|
|
4145
|
+
if (isSkipping(task, controller, runSignal) || isHalted(workflow, task.phase)) {
|
|
3682
4146
|
this.#settleCancelled(task, workflow, runSignal);
|
|
3683
4147
|
return;
|
|
3684
4148
|
}
|
|
@@ -3691,15 +4155,15 @@ var WorkflowRunner = class WorkflowRunner {
|
|
|
3691
4155
|
owners.set(task.id, attempt);
|
|
3692
4156
|
deadline?.start();
|
|
3693
4157
|
const durable = persistence === void 0 ? true : await persistence.checkpoint("attempt", task, attempt);
|
|
3694
|
-
if (!
|
|
4158
|
+
if (!ownsAttempt(owners, task, attempt)) return;
|
|
3695
4159
|
if (!durable) {
|
|
3696
|
-
if (
|
|
4160
|
+
if (isStoppable(workflow)) workflow.stop();
|
|
3697
4161
|
return;
|
|
3698
4162
|
}
|
|
3699
|
-
if (task.
|
|
3700
|
-
const error = new WorkflowError("TRANSITION", `task '${task.id}' has an unresolved
|
|
4163
|
+
if (task.behavior !== void 0 && task.handler === void 0) {
|
|
4164
|
+
const error = new WorkflowError("TRANSITION", `task '${task.id}' has an unresolved behavior '${task.behavior}'`, {
|
|
3701
4165
|
task: task.id,
|
|
3702
|
-
|
|
4166
|
+
behavior: task.behavior
|
|
3703
4167
|
});
|
|
3704
4168
|
task.fail({
|
|
3705
4169
|
origin: "handler",
|
|
@@ -3711,21 +4175,21 @@ var WorkflowRunner = class WorkflowRunner {
|
|
|
3711
4175
|
if (await this.#gate(workflow.paused ? workflow.wait() : void 0, task, workflow, controller, runSignal, signal, attempts, owners, attempt, last, bail)) return;
|
|
3712
4176
|
if (await this.#gate(task.phase.paused ? task.phase.wait() : void 0, task, workflow, controller, runSignal, signal, attempts, owners, attempt, last, bail)) return;
|
|
3713
4177
|
if (await this.#gate(task.paused ? task.wait() : void 0, task, workflow, controller, runSignal, signal, attempts, owners, attempt, last, bail)) return;
|
|
3714
|
-
if (
|
|
4178
|
+
if (isSkipping(task, controller, runSignal) || isHalted(workflow, task.phase)) {
|
|
3715
4179
|
this.#settleCancelled(task, workflow, runSignal);
|
|
3716
4180
|
return;
|
|
3717
4181
|
}
|
|
3718
4182
|
if (task.status !== "running") return;
|
|
3719
|
-
const handle = new TaskController(signal, task.snapshot().metadata, task, attempt, () => workflow.results(), (input) =>
|
|
4183
|
+
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`, {
|
|
3720
4184
|
task: task.id,
|
|
3721
4185
|
attempt
|
|
3722
|
-
})), () =>
|
|
4186
|
+
})), () => ownsAttempt(owners, task, attempt) && !signal.aborted && task.pulse());
|
|
3723
4187
|
let outcome;
|
|
3724
4188
|
try {
|
|
3725
|
-
outcome = task.handler === void 0 ? [true, null] : await this.#raceHandler(Promise.resolve(task.handler(handle)), signal,
|
|
4189
|
+
outcome = task.handler === void 0 ? [true, null] : await this.#raceHandler(Promise.resolve(task.handler(handle)), signal, () => isSkipping(task, controller, runSignal));
|
|
3726
4190
|
} catch (error) {
|
|
3727
|
-
if (!
|
|
3728
|
-
if (task.status !== "running" ||
|
|
4191
|
+
if (!ownsAttempt(owners, task, attempt)) return;
|
|
4192
|
+
if (task.status !== "running" || isSkipping(task, controller, runSignal)) {
|
|
3729
4193
|
this.#settleCancelled(task, workflow, runSignal);
|
|
3730
4194
|
return;
|
|
3731
4195
|
}
|
|
@@ -3736,13 +4200,13 @@ var WorkflowRunner = class WorkflowRunner {
|
|
|
3736
4200
|
this.#failed(owners, task, attempt, error, last, bail);
|
|
3737
4201
|
return;
|
|
3738
4202
|
}
|
|
3739
|
-
if (!
|
|
4203
|
+
if (!ownsAttempt(owners, task, attempt)) return;
|
|
3740
4204
|
if (!outcome[0]) {
|
|
3741
4205
|
this.#settleAttempt(task, workflow, controller, runSignal, signal, attempts, owners, attempt, last, bail, outcome[2]);
|
|
3742
4206
|
return;
|
|
3743
4207
|
}
|
|
3744
4208
|
if (task.status !== "running") return;
|
|
3745
|
-
if (
|
|
4209
|
+
if (isSkipping(task, controller, runSignal)) {
|
|
3746
4210
|
this.#settleCancelled(task, workflow, runSignal);
|
|
3747
4211
|
return;
|
|
3748
4212
|
}
|
|
@@ -3750,32 +4214,32 @@ var WorkflowRunner = class WorkflowRunner {
|
|
|
3750
4214
|
this.#timedOut(owners, task, attempt, last, bail);
|
|
3751
4215
|
return;
|
|
3752
4216
|
}
|
|
3753
|
-
if (!
|
|
4217
|
+
if (!ownsAttempt(owners, task, attempt)) return;
|
|
3754
4218
|
try {
|
|
3755
4219
|
task.complete(outcome[1]);
|
|
3756
4220
|
} catch (error) {
|
|
3757
|
-
if (!
|
|
4221
|
+
if (!ownsAttempt(owners, task, attempt)) return;
|
|
3758
4222
|
if (task.status !== "running") throw error;
|
|
3759
4223
|
this.#failed(owners, task, attempt, error, last, bail);
|
|
3760
4224
|
}
|
|
3761
4225
|
} finally {
|
|
3762
4226
|
deadline?.clear();
|
|
3763
|
-
if (persistence !== void 0 &&
|
|
4227
|
+
if (persistence !== void 0 && ownsAttempt(owners, task, attempt) && isTerminalStatus(task.status) && !await persistence.checkpoint("settlement", task, attempt) && isStoppable(workflow)) workflow.stop();
|
|
3764
4228
|
this.#revoke(owners, task.id, attempt);
|
|
3765
4229
|
}
|
|
3766
4230
|
}
|
|
3767
4231
|
async #gate(wait, task, workflow, controller, runSignal, signal, attempts, owners, attempt, last, bail) {
|
|
3768
|
-
const genuine = wait === void 0 ? void 0 : await this.#raceWait(wait, signal,
|
|
4232
|
+
const genuine = wait === void 0 ? void 0 : await this.#raceWait(wait, signal, () => isSkipping(task, controller, runSignal), workflow, task.phase);
|
|
3769
4233
|
return this.#settleAttempt(task, workflow, controller, runSignal, signal, attempts, owners, attempt, last, bail, genuine);
|
|
3770
4234
|
}
|
|
3771
4235
|
#settleAttempt(task, workflow, controller, runSignal, signal, attempts, owners, attempt, last, bail, genuine) {
|
|
3772
|
-
if (attempts.get(task.id) !== attempt || !
|
|
4236
|
+
if (attempts.get(task.id) !== attempt || !ownsAttempt(owners, task, attempt)) return true;
|
|
3773
4237
|
if (signal.aborted) {
|
|
3774
|
-
if (genuine ??
|
|
4238
|
+
if (genuine ?? isSkipping(task, controller, runSignal)) this.#settleCancelled(task, workflow, runSignal);
|
|
3775
4239
|
else this.#timedOut(owners, task, attempt, last, bail);
|
|
3776
4240
|
return true;
|
|
3777
4241
|
}
|
|
3778
|
-
if (
|
|
4242
|
+
if (isSkipping(task, controller, runSignal) || isHalted(workflow, task.phase)) {
|
|
3779
4243
|
this.#settleCancelled(task, workflow, runSignal);
|
|
3780
4244
|
return true;
|
|
3781
4245
|
}
|
|
@@ -3804,7 +4268,7 @@ var WorkflowRunner = class WorkflowRunner {
|
|
|
3804
4268
|
]);
|
|
3805
4269
|
}
|
|
3806
4270
|
#timedOut(owners, task, attempt, last, bail) {
|
|
3807
|
-
if (!
|
|
4271
|
+
if (!ownsAttempt(owners, task, attempt)) return;
|
|
3808
4272
|
const error = /* @__PURE__ */ new Error(`task '${task.id}' timed out`);
|
|
3809
4273
|
if (last) task.fail({
|
|
3810
4274
|
origin: "timeout",
|
|
@@ -3813,7 +4277,7 @@ var WorkflowRunner = class WorkflowRunner {
|
|
|
3813
4277
|
if (!last || bail) throw error;
|
|
3814
4278
|
}
|
|
3815
4279
|
#failed(owners, task, attempt, error, last, bail) {
|
|
3816
|
-
if (!
|
|
4280
|
+
if (!ownsAttempt(owners, task, attempt)) return;
|
|
3817
4281
|
if (!last) throw error;
|
|
3818
4282
|
task.fail({
|
|
3819
4283
|
origin: "handler",
|
|
@@ -3821,9 +4285,6 @@ var WorkflowRunner = class WorkflowRunner {
|
|
|
3821
4285
|
});
|
|
3822
4286
|
if (bail) throw error;
|
|
3823
4287
|
}
|
|
3824
|
-
#owns(owners, task, attempt) {
|
|
3825
|
-
return owners.get(task.id) === attempt && task.attempts === attempt;
|
|
3826
|
-
}
|
|
3827
4288
|
#revoke(owners, id, attempt) {
|
|
3828
4289
|
if (owners.get(id) === attempt) owners.delete(id);
|
|
3829
4290
|
}
|
|
@@ -3838,7 +4299,7 @@ var WorkflowRunner = class WorkflowRunner {
|
|
|
3838
4299
|
phase?.emitter.on("skip", onTerminal);
|
|
3839
4300
|
phase?.emitter.on("stop", onTerminal);
|
|
3840
4301
|
try {
|
|
3841
|
-
if (workflow !== void 0 &&
|
|
4302
|
+
if (workflow !== void 0 && isHalted(workflow, phase)) deferred.resolve(void 0);
|
|
3842
4303
|
const outcome = await Promise.race([wait, deferred.promise]);
|
|
3843
4304
|
return typeof outcome === "boolean" ? outcome : void 0;
|
|
3844
4305
|
} finally {
|
|
@@ -3869,7 +4330,7 @@ var WorkflowRunner = class WorkflowRunner {
|
|
|
3869
4330
|
return signals.length === 1 ? workflow.signal : AbortSignal.any(signals);
|
|
3870
4331
|
}
|
|
3871
4332
|
#haltFrom(phases, index, workflow, runSignal) {
|
|
3872
|
-
if (
|
|
4333
|
+
if (runSignal.aborted && isStoppable(workflow)) workflow.stop();
|
|
3873
4334
|
this.#skipFrom(phases, index);
|
|
3874
4335
|
}
|
|
3875
4336
|
#skipFrom(phases, index) {
|
|
@@ -3880,37 +4341,17 @@ var WorkflowRunner = class WorkflowRunner {
|
|
|
3880
4341
|
}
|
|
3881
4342
|
}
|
|
3882
4343
|
#settleCancelled(task, workflow, runSignal) {
|
|
3883
|
-
if (
|
|
4344
|
+
if (runSignal.aborted && isStoppable(workflow)) workflow.stop();
|
|
3884
4345
|
this.#skip(task);
|
|
3885
4346
|
}
|
|
3886
4347
|
#skip(task) {
|
|
3887
4348
|
if (task.status === "pending" || task.status === "running") task.skip();
|
|
3888
4349
|
}
|
|
3889
|
-
#skipping(task, controller, runSignal) {
|
|
3890
|
-
return task.signal.aborted || controller.aborted || runSignal.aborted;
|
|
3891
|
-
}
|
|
3892
|
-
#cancelled(runSignal) {
|
|
3893
|
-
return runSignal.aborted;
|
|
3894
|
-
}
|
|
3895
|
-
#halted(workflow, phase) {
|
|
3896
|
-
const status = workflow.status;
|
|
3897
|
-
return status === "failed" || status === "skipped" || status === "stopped" || phase?.status === "skipped" || phase?.status === "stopped";
|
|
3898
|
-
}
|
|
3899
|
-
#stoppable(workflow) {
|
|
3900
|
-
const status = workflow.status;
|
|
3901
|
-
return status !== "failed" && status !== "stopped";
|
|
3902
|
-
}
|
|
3903
|
-
#completable(workflow) {
|
|
3904
|
-
return workflow.status === "pending";
|
|
3905
|
-
}
|
|
3906
|
-
#isWorkflow(target) {
|
|
3907
|
-
return "destroyed" in target && "snapshot" in target && typeof target.snapshot === "function";
|
|
3908
|
-
}
|
|
3909
4350
|
};
|
|
3910
4351
|
//#endregion
|
|
3911
4352
|
//#region src/core/factories.ts
|
|
3912
4353
|
/**
|
|
3913
|
-
*
|
|
4354
|
+
* Compiles the workflow definition contract — the JSON Schema, guard, parser, and
|
|
3914
4355
|
* seeded generator for a {@link WorkflowDefinition}, all derived from one shape and
|
|
3915
4356
|
* kept in lockstep.
|
|
3916
4357
|
*
|
|
@@ -3940,7 +4381,7 @@ function createWorkflowContract() {
|
|
|
3940
4381
|
return createContract(workflowShape);
|
|
3941
4382
|
}
|
|
3942
4383
|
/**
|
|
3943
|
-
*
|
|
4384
|
+
* Builds the live W-b entity tree from a {@link WorkflowDefinition} — the whole
|
|
3944
4385
|
* {@link WorkflowInterface} → {@link import('./types.js').PhaseInterface} →
|
|
3945
4386
|
* {@link import('./types.js').TaskInterface} tree, each level wired with its lineage
|
|
3946
4387
|
* context, its emitter, and the cascade.
|
|
@@ -3952,11 +4393,11 @@ function createWorkflowContract() {
|
|
|
3952
4393
|
* definition's `bail`, else the graceful {@link import('./constants.js').DEFAULT_BAIL}; it
|
|
3953
4394
|
* feeds {@link import('./helpers.js').deriveWorkflowStatus}. Per-phase / per-task initial
|
|
3954
4395
|
* listeners + metadata travel through `options.phases[id].on` /
|
|
3955
|
-
* `options.phases[id].tasks[id]` (the
|
|
4396
|
+
* `options.phases[id].tasks[id]` (the nested-by-id bag). The W-b tree is the
|
|
3956
4397
|
* state machine ONLY — it does not execute tasks (W-c drives the transitions).
|
|
3957
4398
|
*
|
|
3958
|
-
* `options.functions` is the {@link import('./types.js').
|
|
3959
|
-
* task's `
|
|
4399
|
+
* `options.functions` is the {@link import('./types.js').WorkflowRegistry} registry each live
|
|
4400
|
+
* task's `behavior` name resolves against ONCE at construction into its runtime
|
|
3960
4401
|
* {@link import('./types.js').TaskInterface.handler}. An omitted name is the deliberate no-op;
|
|
3961
4402
|
* an unresolved present name remains inspectable but is rejected if execution is attempted.
|
|
3962
4403
|
*
|
|
@@ -3974,11 +4415,47 @@ function createWorkflowContract() {
|
|
|
3974
4415
|
* ```
|
|
3975
4416
|
*/
|
|
3976
4417
|
function createWorkflow(definition, options) {
|
|
3977
|
-
|
|
3978
|
-
|
|
4418
|
+
return createWorkflowTree(definition, captureWorkflowOptions(options));
|
|
4419
|
+
}
|
|
4420
|
+
/**
|
|
4421
|
+
* Builds the live entity tree one definition and one owned options bag describe — the shared
|
|
4422
|
+
* construction path behind every definition-driven mint.
|
|
4423
|
+
*
|
|
4424
|
+
* @remarks
|
|
4425
|
+
* Seeds an initial all-`pending` {@link WorkflowSnapshot} from the definition and constructs the
|
|
4426
|
+
* live {@link WorkflowInterface} over it. `bail` is the caller's own override, forwarded to
|
|
4427
|
+
* {@link definitionToSnapshot} so it reaches BOTH tiers: the workflow snapshot AND the inheritance
|
|
4428
|
+
* default of every phase that declares no `bail` of its own, while a phase declaring one still
|
|
4429
|
+
* wins. Omitted, the definition's own `bail` governs, defaulting to the graceful
|
|
4430
|
+
* {@link import('./constants.js').DEFAULT_BAIL}.
|
|
4431
|
+
*
|
|
4432
|
+
* `captured` is forwarded to the entity UNCHANGED — its own `bail` is deliberately not replaced
|
|
4433
|
+
* with the resolved policy, because the snapshot already carries the resolved value at both tiers
|
|
4434
|
+
* and an injected one would make `Workflow` read it as an EXPLICIT uniform override and clobber
|
|
4435
|
+
* the per-phase overrides. Each task's `behavior` / `retries` / `timeout` travel onto the snapshot
|
|
4436
|
+
* too, so `captured.functions` resolves every handler identically whether the tree is built fresh
|
|
4437
|
+
* or restored. Pass a bag {@link captureWorkflowOptions} already owns: this constructs over it
|
|
4438
|
+
* without re-capturing.
|
|
4439
|
+
*
|
|
4440
|
+
* @param definition - The workflow definition to bring to life
|
|
4441
|
+
* @param captured - The already-owned {@link WorkflowOptions} bag the entity is constructed with,
|
|
4442
|
+
* whose `bail` is the caller's failure-policy override, or `undefined` to take the definition's
|
|
4443
|
+
* @returns The live {@link WorkflowInterface} root
|
|
4444
|
+
*
|
|
4445
|
+
* @example
|
|
4446
|
+
* ```ts
|
|
4447
|
+
* import { captureWorkflowOptions, createWorkflowTree } from '@orkestrel/workflow'
|
|
4448
|
+
*
|
|
4449
|
+
* const captured = captureWorkflowOptions({ bail: true })
|
|
4450
|
+
* const workflow = createWorkflowTree(definition, captured)
|
|
4451
|
+
* workflow.bail // true — the override reached the workflow and every inheriting phase
|
|
4452
|
+
* ```
|
|
4453
|
+
*/
|
|
4454
|
+
function createWorkflowTree(definition, captured) {
|
|
4455
|
+
return new Workflow(definitionToSnapshot(definition, captured.bail), captured);
|
|
3979
4456
|
}
|
|
3980
4457
|
/**
|
|
3981
|
-
*
|
|
4458
|
+
* Builds an equivalent live W-b entity tree from a {@link WorkflowSnapshot} — the
|
|
3982
4459
|
* inverse of {@link WorkflowInterface.snapshot}, restoring structure + each node's status
|
|
3983
4460
|
* + recorded results + positional order + the persisted `#override`.
|
|
3984
4461
|
*
|
|
@@ -3993,7 +4470,7 @@ function createWorkflow(definition, options) {
|
|
|
3993
4470
|
* still wins when supplied (to deliberately re-run under a different policy). A structurally
|
|
3994
4471
|
* invalid snapshot (a status — or override — outside the lifecycle vocabulary, or a
|
|
3995
4472
|
* non-boolean `bail`) throws a `RESTORE` {@link WorkflowError}.
|
|
3996
|
-
* Runtime handlers are optional: without a matching `functions` entry, a persisted `
|
|
4473
|
+
* Runtime handlers are optional: without a matching `functions` entry, a persisted `behavior`
|
|
3997
4474
|
* remains visible with an undefined `handler` so the exact state is inspectable. The runner
|
|
3998
4475
|
* rejects that unresolved tree if execution is attempted.
|
|
3999
4476
|
*
|
|
@@ -4014,10 +4491,10 @@ function createRestoredWorkflow(snapshot, options) {
|
|
|
4014
4491
|
return new Workflow(cloneWorkflowSnapshot(snapshot), captured);
|
|
4015
4492
|
}
|
|
4016
4493
|
/**
|
|
4017
|
-
*
|
|
4494
|
+
* Builds an interrupted workflow back to life at its remaining retry budget.
|
|
4018
4495
|
*
|
|
4019
4496
|
* @remarks
|
|
4020
|
-
* Each phase captures every unique initial `
|
|
4497
|
+
* Each phase captures every unique initial `behavior` binding once before constructing tasks. Recovery
|
|
4021
4498
|
* validates those live tasks' captured callable handlers without rereading the registry, while the
|
|
4022
4499
|
* retained registry identity remains available to resolve future live additions at their mint time.
|
|
4023
4500
|
*
|
|
@@ -4038,18 +4515,19 @@ function createRecoveredWorkflow(snapshot, options) {
|
|
|
4038
4515
|
const owned = cloneWorkflowSnapshot(snapshot);
|
|
4039
4516
|
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 });
|
|
4040
4517
|
const workflow = new Workflow(cloneWorkflowSnapshot(recoverWorkflowSnapshot(owned)), captured);
|
|
4041
|
-
if (!hasWorkflowHandlers(workflow)) throw new WorkflowError("RESTORE", `workflow '${owned.id}' has an unresolved
|
|
4518
|
+
if (!hasWorkflowHandlers(workflow)) throw new WorkflowError("RESTORE", `workflow '${owned.id}' has an unresolved behavior`, { workflow: owned.id });
|
|
4042
4519
|
return workflow;
|
|
4043
4520
|
}
|
|
4044
4521
|
/**
|
|
4045
|
-
*
|
|
4522
|
+
* Creates the in-memory durable {@link WorkflowStoreInterface} — a process-lifetime
|
|
4046
4523
|
* {@link MemoryWorkflowStore} persisting {@link WorkflowSnapshot}s by workflow id, the DEFAULT
|
|
4047
4524
|
* backend behind the W-d persistence seam.
|
|
4048
4525
|
*
|
|
4049
4526
|
* @remarks
|
|
4050
4527
|
* The snapshot analogue of the server package's `createMemorySessionStore`
|
|
4051
4528
|
* (and the `createMemoryQueueStore` family), but LEANER — there is no idle-TTL, so no
|
|
4052
|
-
* options bag (
|
|
4529
|
+
* options bag (the smallest interface the capability requires): a persisted run-state lives until
|
|
4530
|
+
* an explicit `delete`. This is
|
|
4053
4531
|
* the zero-plumbing DEFAULT (a plain `Map`); its driver-pluggable twin is
|
|
4054
4532
|
* {@link createDatabaseWorkflowStore} (the snapshot as one opaque JSON column over a `databases`
|
|
4055
4533
|
* table) — for a DURABLE store (run-state surviving a restart) pass it a JSON / SQLite / IndexedDB
|
|
@@ -4073,7 +4551,7 @@ function createMemoryWorkflowStore() {
|
|
|
4073
4551
|
return new MemoryWorkflowStore();
|
|
4074
4552
|
}
|
|
4075
4553
|
/**
|
|
4076
|
-
*
|
|
4554
|
+
* Creates a {@link DatabaseWorkflowStore} over any {@link DriverInterface} — the durable,
|
|
4077
4555
|
* driver-pluggable backing for the W-d persistence seam, the opt-in twin of
|
|
4078
4556
|
* {@link createMemoryWorkflowStore}.
|
|
4079
4557
|
*
|
|
@@ -4084,8 +4562,9 @@ function createMemoryWorkflowStore() {
|
|
|
4084
4562
|
* snapshot is already a COMPLETE, self-contained, pure-JSON payload, so storing it whole is lossless
|
|
4085
4563
|
* AND keeps the row type FLAT — a structured multi-column snapshot table would force the contract to
|
|
4086
4564
|
* `Infer` the deeply-nested snapshot shape (workflow → phases → tasks → results) and trip TS2589;
|
|
4087
|
-
* the opaque column sidesteps it (the column reads back as `unknown`, narrowed on `get` by
|
|
4088
|
-
* {@link
|
|
4565
|
+
* the opaque column sidesteps it (the column reads back as `unknown`, owned and narrowed on `get` by
|
|
4566
|
+
* {@link cloneWorkflowSnapshot}, whose semantic pass is
|
|
4567
|
+
* {@link import('./validators.js').isOwnedWorkflowSnapshot}). The `driver` DEFAULTS to
|
|
4089
4568
|
* {@link createMemoryDriver}, so the store ALSO works in memory out of the box; pass a server
|
|
4090
4569
|
* `createJSONDriver` / `createSQLiteDriver` (or a browser IndexedDB driver) for a persistent one —
|
|
4091
4570
|
* the durability is the driver's job, the store engine is shared. It swaps in behind
|
|
@@ -4117,9 +4596,9 @@ function createDatabaseWorkflowStore(driver = createMemoryDriver()) {
|
|
|
4117
4596
|
}).table("snapshots"));
|
|
4118
4597
|
}
|
|
4119
4598
|
/**
|
|
4120
|
-
*
|
|
4121
|
-
* workflow tree by COMPOSING the shipped substrate: phases sequential, tasks concurrent,
|
|
4122
|
-
*
|
|
4599
|
+
* Creates the thin orchestrator — a {@link WorkflowRunnerInterface} — that EXECUTES a live W-b
|
|
4600
|
+
* workflow tree by COMPOSING the shipped substrate: phases sequential, tasks concurrent, each
|
|
4601
|
+
* task dispatched through its OWN resolved handler under the workflow's `bail` policy.
|
|
4123
4602
|
*
|
|
4124
4603
|
* @remarks
|
|
4125
4604
|
* The runner is a PURE engine — it re-implements no concurrency / retry / abort logic, AND it
|
|
@@ -4132,14 +4611,14 @@ function createDatabaseWorkflowStore(driver = createMemoryDriver()) {
|
|
|
4132
4611
|
* rest) vs settle-all (`false` — failures are recorded, the run finishes); the run-level abort
|
|
4133
4612
|
* / timeout / budget ({@link import('./types.js').WorkflowRunOptions}) fold through
|
|
4134
4613
|
* `AbortSignal.any` (the agent runtime's pattern); pacing is the shipped scheduler.
|
|
4135
|
-
* `execute(definition, options?)` BUILDS the live tree from the definition itself (
|
|
4614
|
+
* `execute(definition, options?)` BUILDS the live tree from the definition itself (through
|
|
4136
4615
|
* {@link createWorkflow} — one source of truth, returned in `WorkflowResult.workflow`), drives
|
|
4137
4616
|
* the live entity (`start` → `complete` / `fail`), and resolves a
|
|
4138
4617
|
* {@link import('./types.js').WorkflowResult}.
|
|
4139
4618
|
*
|
|
4140
4619
|
* External integrations remain application-owned: a caller wires an ordinary
|
|
4141
4620
|
* {@link import('./types.js').WorkflowFunction} into its own {@link WorkflowOptions.functions}
|
|
4142
|
-
* registry. Only a task that omits `
|
|
4621
|
+
* registry. Only a task that omits `behavior` auto-completes; unresolved named work is rejected
|
|
4143
4622
|
* before dispatch.
|
|
4144
4623
|
*
|
|
4145
4624
|
* @param options - An optional pacing `scheduler` (default the shipped cross-environment one).
|
|
@@ -4152,7 +4631,7 @@ function createDatabaseWorkflowStore(driver = createMemoryDriver()) {
|
|
|
4152
4631
|
*
|
|
4153
4632
|
* const runner = createWorkflowRunner()
|
|
4154
4633
|
* const definition = { id: 'w', name: 'W', phases: [{ id: 'p', name: 'P', tasks: [
|
|
4155
|
-
* { id: 't', name: 'T',
|
|
4634
|
+
* { id: 't', name: 'T', behavior: 'compile' },
|
|
4156
4635
|
* ] }] }
|
|
4157
4636
|
* const result = await runner.execute(definition, {
|
|
4158
4637
|
* functions: { compile: async (controller) => `built ${controller.task.id}` },
|
|
@@ -4165,13 +4644,13 @@ function createWorkflowRunner(options) {
|
|
|
4165
4644
|
return new WorkflowRunner(options?.scheduler ?? createScheduler());
|
|
4166
4645
|
}
|
|
4167
4646
|
/**
|
|
4168
|
-
*
|
|
4647
|
+
* Creates a {@link WorkflowManagerInterface} — the store-backed registry of
|
|
4169
4648
|
* {@link WorkflowInterface}s, the additive manager tier mirroring the `@orkestrel/agent`
|
|
4170
4649
|
* line's `createConversationManager` / `createWorkspaceManager`.
|
|
4171
4650
|
*
|
|
4172
4651
|
* @remarks
|
|
4173
|
-
* `options.functions` flows into every workflow the manager mints (`add`,
|
|
4174
|
-
* {@link createWorkflow}) or hydrates (`open`'s registry-miss path,
|
|
4652
|
+
* `options.functions` flows into every workflow the manager mints (`add`, through
|
|
4653
|
+
* {@link createWorkflow}) or hydrates (`open`'s registry-miss path, through
|
|
4175
4654
|
* {@link createRestoredWorkflow}), so a hydrated workflow is RUNNABLE rather than a dead snapshot
|
|
4176
4655
|
* mirror. `options.store` is the EXACT analogue of the twins' `store` seam — omitted ⇒ the
|
|
4177
4656
|
* manager is registry-only (`open` resolves only what is registered, `save` is a no-op). This
|
|
@@ -4199,12 +4678,12 @@ function createWorkflowManager(options) {
|
|
|
4199
4678
|
return new WorkflowManager(options);
|
|
4200
4679
|
}
|
|
4201
4680
|
/**
|
|
4202
|
-
*
|
|
4681
|
+
* Creates the safe cross-environment cooperative-yield default — a
|
|
4203
4682
|
* {@link SchedulerInterface} built on `setTimeout` / `clearTimeout` alone, so it
|
|
4204
4683
|
* runs unchanged in both the browser and Node.
|
|
4205
4684
|
*
|
|
4206
4685
|
* @remarks
|
|
4207
|
-
* `yield()` gives the host a turn
|
|
4686
|
+
* `yield()` gives the host a turn through a zero-delay macrotask (so pending I/O,
|
|
4208
4687
|
* timers, and rendering actually run — a microtask would not); `delay(ms)` resumes
|
|
4209
4688
|
* after at least `ms`. Pass `options.signal` to make a pending yield/delay reject
|
|
4210
4689
|
* with the signal's exact `reason`; the shared owned-signal lifecycle clears the timer
|
|
@@ -4245,7 +4724,7 @@ function createScheduler() {
|
|
|
4245
4724
|
return new Scheduler();
|
|
4246
4725
|
}
|
|
4247
4726
|
/**
|
|
4248
|
-
*
|
|
4727
|
+
* Creates a thin generic orchestrator that drives declared units — and any they
|
|
4249
4728
|
* `spawn` — through a bounded-concurrency queue, collecting their results in order.
|
|
4250
4729
|
*
|
|
4251
4730
|
* @remarks
|
|
@@ -4257,13 +4736,13 @@ function createScheduler() {
|
|
|
4257
4736
|
* `id` / `input`, a `signal` that fires on the unit's `abort`, a runner-level `abort`,
|
|
4258
4737
|
* or the attempt's timeout, a promise-parked `wait()`, and `spawn(input)` to fan out
|
|
4259
4738
|
* sibling units. The run is **fail-fast**: the first unit failure (after retries)
|
|
4260
|
-
* aborts every other unit and rejects `execute` with that error. **Observable
|
|
4739
|
+
* aborts every other unit and rejects `execute` with that error. **Observable:** a
|
|
4261
4740
|
* typed `emitter` surfaces `start` / `unit` / `spawn` / `settle` / `fail` / `finish` / `abort`.
|
|
4262
4741
|
*
|
|
4263
|
-
* Because `spawn` is fire-and-track (the runner awaits the whole spawn closure
|
|
4742
|
+
* Because `spawn` is fire-and-track (the runner awaits the whole spawn closure through an
|
|
4264
4743
|
* outstanding-unit count, not a one-time snapshot), a handler need NOT await its spawns
|
|
4265
|
-
* for them to run — and on a bounded runner
|
|
4266
|
-
*
|
|
4744
|
+
* for them to run — and on a bounded runner do NOT `await` a spawn inline (a slot-holding
|
|
4745
|
+
* handler awaiting its own spawn can deadlock); fan out and return instead.
|
|
4267
4746
|
*
|
|
4268
4747
|
* @typeParam TInput - The work input each unit carries
|
|
4269
4748
|
* @typeParam TResult - The value a unit's handler resolves
|
|
@@ -4292,6 +4771,6 @@ function createRunner(options) {
|
|
|
4292
4771
|
return new Runner(options);
|
|
4293
4772
|
}
|
|
4294
4773
|
//#endregion
|
|
4295
|
-
export {
|
|
4774
|
+
export { Collection, DEFAULT_BAIL, DEFAULT_PHASE_CONCURRENCY, DatabaseWorkflowStore, LIFECYCLE_STATUSES, MAX_TIMER_MS, MemoryWorkflowStore, PERSISTED_NODE_EVENTS, PERSISTED_TASK_EVENTS, PhaseManager, RunHolder, Runner, Scheduler, TASK_TRANSITIONS, TERMINAL_STATUSES, TaskManager, Workflow, WorkflowError, WorkflowManager, WorkflowPersistence, WorkflowRunner, buildPhaseContext, buildTaskContext, buildWorkflowContext, canTransitionTask, captureWorkflowOptions, cloneTaskActivity, cloneTaskClaims, cloneWorkflowSnapshot, collectResults, createDatabaseWorkflowStore, createMemoryWorkflowStore, createRecoveredWorkflow, createRestoredWorkflow, createRunner, createScheduler, createWorkflow, createWorkflowContract, createWorkflowManager, createWorkflowRunner, createWorkflowTree, definitionToSnapshot, delayHost, deriveBoundary, derivePhaseStatus, deriveWorkflowStatus, errorToMessage, failure, findFailure, hasWorkflowHandlers, insertEntry, isCompletable, isHalted, isLifecycleStatus, isOwnedWorkflowSnapshot, isSkipping, isStoppable, isTaskActivity, isTaskActivityInput, isTaskClaimList, isTaskFailure, isTaskResult, isTerminalStatus, isWorkflowError, isWorkflowInterface, isWorkflowSnapshot, matchesDescription, moveEntry, ownsAttempt, parkSignal, phaseDefinitionToSnapshot, phaseShape, phaseUpdateShape, recoverWorkflowSnapshot, resolveTaskSilence, scanSnapshotContext, scheduleHost, success, taskDefinitionToSnapshot, taskShape, taskUpdateShape, workflowShape };
|
|
4296
4775
|
|
|
4297
4776
|
//# sourceMappingURL=index.js.map
|