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