@orkestrel/workflow 0.0.7 → 0.0.9
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 +23 -30
- package/dist/src/browser/index.d.ts +23 -38
- package/dist/src/browser/index.js +51 -122
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/core/index.cjs +1761 -459
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +686 -277
- package/dist/src/core/index.d.ts +686 -277
- package/dist/src/core/index.js +1744 -461
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +21 -43
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +12 -17
- package/dist/src/server/index.d.ts +12 -17
- package/dist/src/server/index.js +21 -43
- package/dist/src/server/index.js.map +1 -1
- package/package.json +9 -9
package/dist/src/core/index.cjs
CHANGED
|
@@ -1,84 +1,10 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let _orkestrel_abort = require("@orkestrel/abort");
|
|
2
3
|
let _orkestrel_contract = require("@orkestrel/contract");
|
|
3
4
|
let _orkestrel_database = require("@orkestrel/database");
|
|
4
|
-
let _orkestrel_abort = require("@orkestrel/abort");
|
|
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/Scheduler.ts
|
|
9
|
-
/**
|
|
10
|
-
* The safe cross-environment cooperative-yield default — a {@link SchedulerInterface}
|
|
11
|
-
* built on `setTimeout` / `clearTimeout` alone, so it runs unchanged in both the
|
|
12
|
-
* browser and Node.
|
|
13
|
-
*
|
|
14
|
-
* @remarks
|
|
15
|
-
* - **Cross-environment.** Uses ONLY `setTimeout` / `clearTimeout` — universally
|
|
16
|
-
* available. It deliberately avoids env-specific fast paths (`setImmediate`,
|
|
17
|
-
* `scheduler.yield`, `requestAnimationFrame`, `node:timers/promises`,
|
|
18
|
-
* `MessageChannel`); those belong to the environment backends, built with the
|
|
19
|
-
* agent loop that consumes them.
|
|
20
|
-
* - **`yield` is a macrotask host-turn, not a microtask.** `yield()` waits on a
|
|
21
|
-
* `setTimeout(0)`, NOT `queueMicrotask`. A microtask drains before the host
|
|
22
|
-
* regains control, so it would not actually let pending I/O, timers, or
|
|
23
|
-
* rendering run — it only defers within the current task. A zero-delay timer is
|
|
24
|
-
* the correct cross-environment "give the host a turn".
|
|
25
|
-
* - **Abort-aware.** A pending `yield` / `delay` rejects with `signal.reason` when
|
|
26
|
-
* the signal aborts (the standard `AbortSignal` convention). An already-aborted
|
|
27
|
-
* signal rejects immediately without arming a timer. Either settle path clears
|
|
28
|
-
* the timer and removes the abort listener — no leaked timer, no leaked
|
|
29
|
-
* listener, and no double-settle.
|
|
30
|
-
* - **Priority is accepted but uniform.** `options.priority` is part of the
|
|
31
|
-
* contract, but a `setTimeout`-based default cannot act on urgency, so it treats
|
|
32
|
-
* every priority the same. Environment backends honour it.
|
|
33
|
-
* - **Event-free.** A pure functional primitive — no Emitter, no events.
|
|
34
|
-
*
|
|
35
|
-
* @example
|
|
36
|
-
* ```ts
|
|
37
|
-
* const scheduler = new Scheduler()
|
|
38
|
-
* while (!signal.aborted) {
|
|
39
|
-
* doSomeWork()
|
|
40
|
-
* await scheduler.yield({ signal }) // let the host run between work units
|
|
41
|
-
* }
|
|
42
|
-
* ```
|
|
43
|
-
*/
|
|
44
|
-
var Scheduler = class {
|
|
45
|
-
/**
|
|
46
|
-
* Yield control back to the host so other tasks (I/O, timers, rendering) can
|
|
47
|
-
* run, then resume — a macrotask turn via `setTimeout(0)` (NOT a microtask,
|
|
48
|
-
* which would resume before the host regains control).
|
|
49
|
-
*/
|
|
50
|
-
yield(options) {
|
|
51
|
-
return this.#sleep(0, options?.signal);
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Resume after at least `ms` milliseconds; abort rejects with `signal.reason`.
|
|
55
|
-
*
|
|
56
|
-
* @remarks
|
|
57
|
-
* `ms` should be a non-negative finite number. The primitive stays minimal and
|
|
58
|
-
* does no validation: it passes `ms` straight to the host `setTimeout`, which
|
|
59
|
-
* clamps a negative value or `NaN` to ~0 — so an out-of-domain `ms` resolves on
|
|
60
|
-
* the next host turn rather than throwing.
|
|
61
|
-
*/
|
|
62
|
-
delay(ms, options) {
|
|
63
|
-
return this.#sleep(ms, options?.signal);
|
|
64
|
-
}
|
|
65
|
-
#sleep(ms, signal) {
|
|
66
|
-
if (signal?.aborted === true) return Promise.reject(signal.reason);
|
|
67
|
-
return new Promise((resolve, reject) => {
|
|
68
|
-
const handle = setTimeout(() => {
|
|
69
|
-
signal?.removeEventListener("abort", onAbort);
|
|
70
|
-
resolve();
|
|
71
|
-
}, ms);
|
|
72
|
-
const onAbort = this.#abort.bind(this, handle, reject, signal);
|
|
73
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
#abort(handle, reject, signal) {
|
|
77
|
-
clearTimeout(handle);
|
|
78
|
-
reject(signal?.reason);
|
|
79
|
-
}
|
|
80
|
-
};
|
|
81
|
-
//#endregion
|
|
82
8
|
//#region src/core/constants.ts
|
|
83
9
|
/** The default {@link import('./types.js').WorkflowDefinition.bail} — graceful (continue on a leaf failure). */
|
|
84
10
|
var DEFAULT_BAIL = false;
|
|
@@ -179,52 +105,47 @@ var TASK_TRANSITIONS = Object.freeze({
|
|
|
179
105
|
* phase may still DECLARE a larger explicit `concurrency` and pays that allocation knowingly.
|
|
180
106
|
*/
|
|
181
107
|
var DEFAULT_PHASE_CONCURRENCY = 1024;
|
|
108
|
+
/**
|
|
109
|
+
* The largest delay representable by the host timer APIs without overflow or clamping.
|
|
110
|
+
*/
|
|
111
|
+
var MAX_TIMER_MS = 2147483647;
|
|
182
112
|
//#endregion
|
|
183
|
-
//#region src/core/
|
|
113
|
+
//#region src/core/helpers.ts
|
|
184
114
|
/**
|
|
185
|
-
*
|
|
115
|
+
* Capture every top-level {@link WorkflowOptions} value exactly once into an owned plain bag.
|
|
186
116
|
*
|
|
187
117
|
* @remarks
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
* blob (`TOOL`). `DEPTH` and `TOOL` are public type surface constructed by the
|
|
194
|
-
* `@orkestrel/tool` package's workflow-tool / agent-function adapters; on that seam the
|
|
195
|
-
* throw is ISOLATED by its `ToolManager` into the tool result's top-level `error`
|
|
196
|
-
* (AGENTS §14 — the universal tool-handler contract).
|
|
197
|
-
*/
|
|
198
|
-
var WorkflowError = class extends Error {
|
|
199
|
-
code;
|
|
200
|
-
context;
|
|
201
|
-
constructor(code, message, context) {
|
|
202
|
-
super(message);
|
|
203
|
-
this.name = "WorkflowError";
|
|
204
|
-
this.code = code;
|
|
205
|
-
if (context !== void 0) this.context = context;
|
|
206
|
-
}
|
|
207
|
-
};
|
|
208
|
-
/**
|
|
209
|
-
* Narrow an unknown caught value to a {@link WorkflowError}.
|
|
118
|
+
* Direct property reads preserve inherited and non-enumerable option values while preventing
|
|
119
|
+
* accessor-backed caller bags from shifting policy, handlers, hooks, or nested options between
|
|
120
|
+
* construction stages. Nested bags and the functions registry retain their original identities so
|
|
121
|
+
* entity constructors can snapshot keyed child options and live additions can resolve against the
|
|
122
|
+
* same registry.
|
|
210
123
|
*
|
|
211
|
-
* @param
|
|
212
|
-
* @returns
|
|
124
|
+
* @param options - The caller-owned workflow construction options
|
|
125
|
+
* @returns An owned top-level options bag containing the captured values
|
|
213
126
|
*
|
|
214
127
|
* @example
|
|
215
128
|
* ```ts
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
* } catch (error) {
|
|
219
|
-
* if (isWorkflowError(error) && error.code === 'TRANSITION') retry()
|
|
220
|
-
* }
|
|
129
|
+
* const captured = captureWorkflowOptions(options)
|
|
130
|
+
* const workflow = createWorkflow(definition, captured)
|
|
221
131
|
* ```
|
|
222
132
|
*/
|
|
223
|
-
function
|
|
224
|
-
|
|
133
|
+
function captureWorkflowOptions(options) {
|
|
134
|
+
const on = options?.on;
|
|
135
|
+
const bail = options?.bail;
|
|
136
|
+
const error = options?.error;
|
|
137
|
+
const phases = options?.phases;
|
|
138
|
+
const functions = options?.functions;
|
|
139
|
+
const silence = options?.silence;
|
|
140
|
+
return Object.freeze({
|
|
141
|
+
...on === void 0 ? {} : { on },
|
|
142
|
+
...bail === void 0 ? {} : { bail },
|
|
143
|
+
...error === void 0 ? {} : { error },
|
|
144
|
+
...phases === void 0 ? {} : { phases },
|
|
145
|
+
...functions === void 0 ? {} : { functions },
|
|
146
|
+
...silence === void 0 ? {} : { silence }
|
|
147
|
+
});
|
|
225
148
|
}
|
|
226
|
-
//#endregion
|
|
227
|
-
//#region src/core/helpers.ts
|
|
228
149
|
/**
|
|
229
150
|
* Test whether a {@link LifecycleStatus} is TERMINAL — a node in this state will not
|
|
230
151
|
* transition further.
|
|
@@ -358,6 +279,17 @@ function canTransitionTask(from, to) {
|
|
|
358
279
|
return TASK_TRANSITIONS[from].includes(to);
|
|
359
280
|
}
|
|
360
281
|
/**
|
|
282
|
+
* Resolve a task's runtime silence window against its workflow default.
|
|
283
|
+
*
|
|
284
|
+
* @param value - The task-level override; any present non-positive or non-finite value disables
|
|
285
|
+
* @param fallback - The workflow-level default
|
|
286
|
+
* @returns A host-safe effective window (`1..MAX_TIMER_MS`), or `undefined`
|
|
287
|
+
*/
|
|
288
|
+
function resolveTaskSilence(value, fallback) {
|
|
289
|
+
if (value !== void 0) return Number.isFinite(value) && value > 0 && value <= 2147483647 ? value : void 0;
|
|
290
|
+
return fallback !== void 0 && Number.isFinite(fallback) && fallback > 0 && fallback <= 2147483647 ? fallback : void 0;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
361
293
|
* Box a value as a {@link Success} — the graceful outcome half of a {@link Result}.
|
|
362
294
|
*
|
|
363
295
|
* @typeParam T - The boxed value's type
|
|
@@ -394,6 +326,20 @@ function failure(error) {
|
|
|
394
326
|
};
|
|
395
327
|
}
|
|
396
328
|
/**
|
|
329
|
+
* Normalize an unknown thrown value to a non-empty persistence-safe message.
|
|
330
|
+
*
|
|
331
|
+
* @param error - The caught value
|
|
332
|
+
* @returns A non-empty message without stack or cause data
|
|
333
|
+
*/
|
|
334
|
+
function errorToMessage(error) {
|
|
335
|
+
try {
|
|
336
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
337
|
+
return typeof message === "string" && message.length > 0 ? message : "unknown failure";
|
|
338
|
+
} catch {
|
|
339
|
+
return "unknown failure";
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
397
343
|
* Find the first {@link TaskResult} in a positional list whose boxed outcome is a
|
|
398
344
|
* `Failure` — the pure scan shared by a phase's and a workflow's derived-`failed`
|
|
399
345
|
* `fail`-event lookup.
|
|
@@ -430,11 +376,11 @@ function findFailure(results) {
|
|
|
430
376
|
* @returns The {@link WorkflowContext}
|
|
431
377
|
*/
|
|
432
378
|
function buildWorkflowContext(node) {
|
|
433
|
-
return {
|
|
379
|
+
return Object.freeze({
|
|
434
380
|
id: node.id,
|
|
435
381
|
name: node.name,
|
|
436
382
|
...node.description === void 0 ? {} : { description: node.description }
|
|
437
|
-
};
|
|
383
|
+
});
|
|
438
384
|
}
|
|
439
385
|
/**
|
|
440
386
|
* Build a {@link PhaseContext} — a phase's own identity plus a back-reference to its
|
|
@@ -445,10 +391,10 @@ function buildWorkflowContext(node) {
|
|
|
445
391
|
* @returns The {@link PhaseContext}
|
|
446
392
|
*/
|
|
447
393
|
function buildPhaseContext(workflow, node) {
|
|
448
|
-
return {
|
|
394
|
+
return Object.freeze({
|
|
449
395
|
...buildWorkflowContext(node),
|
|
450
|
-
workflow
|
|
451
|
-
};
|
|
396
|
+
workflow: buildWorkflowContext(workflow)
|
|
397
|
+
});
|
|
452
398
|
}
|
|
453
399
|
/**
|
|
454
400
|
* Build a {@link TaskContext} — a task's own identity plus a back-reference to its phase
|
|
@@ -460,31 +406,10 @@ function buildPhaseContext(workflow, node) {
|
|
|
460
406
|
* @returns The {@link TaskContext}
|
|
461
407
|
*/
|
|
462
408
|
function buildTaskContext(phase, node) {
|
|
463
|
-
return {
|
|
409
|
+
return Object.freeze({
|
|
464
410
|
...buildWorkflowContext(node),
|
|
465
|
-
phase
|
|
466
|
-
};
|
|
467
|
-
}
|
|
468
|
-
/**
|
|
469
|
-
* Narrow an `unknown` to a {@link WorkflowSnapshot} — the AGENTS §14 boundary guard for an
|
|
470
|
-
* UNTRUSTED snapshot read (a storage row a {@link import('./stores/DatabaseWorkflowStore.js').DatabaseWorkflowStore}
|
|
471
|
-
* reads back from its opaque JSON column, a snapshot loaded from disk).
|
|
472
|
-
*
|
|
473
|
-
* @remarks
|
|
474
|
-
* A total guard (it NEVER throws — adversarial input returns `false`, AGENTS §14). It checks the
|
|
475
|
-
* snapshot's SHAPE — `id` / `name` / `status` strings, a `boolean` `bail`, an array of `phases`,
|
|
476
|
-
* `created` / `updated` numbers — enough to safely impose the {@link WorkflowSnapshot} type at a
|
|
477
|
-
* storage boundary WITHOUT a cast. It is complementary to
|
|
478
|
-
* {@link import('./factories.js').assertSnapshot}, which validates the DEEPER invariant (every
|
|
479
|
-
* node's status / override drawn from the lifecycle vocabulary) and THROWS a `RESTORE`
|
|
480
|
-
* {@link import('./errors.js').WorkflowError} — the deep gate a {@link import('./factories.js').restoreWorkflow}
|
|
481
|
-
* applies. A boundary read narrows shape with this guard; a restore validates vocabulary with `assertSnapshot`.
|
|
482
|
-
*
|
|
483
|
-
* @param value - The value to test (an opaque storage read)
|
|
484
|
-
* @returns `true` when `value` has the structural shape of a {@link WorkflowSnapshot}
|
|
485
|
-
*/
|
|
486
|
-
function isWorkflowSnapshot(value) {
|
|
487
|
-
return (0, _orkestrel_contract.isRecord)(value) && (0, _orkestrel_contract.isString)(value.id) && (0, _orkestrel_contract.isString)(value.name) && (0, _orkestrel_contract.isString)(value.status) && (0, _orkestrel_contract.isBoolean)(value.bail) && (0, _orkestrel_contract.isArray)(value.phases) && (0, _orkestrel_contract.isNumber)(value.created) && (0, _orkestrel_contract.isNumber)(value.updated);
|
|
411
|
+
phase: buildPhaseContext(phase.workflow, phase)
|
|
412
|
+
});
|
|
488
413
|
}
|
|
489
414
|
/**
|
|
490
415
|
* Convert a {@link WorkflowDefinition} into an INITIAL {@link WorkflowSnapshot} — every
|
|
@@ -574,12 +499,91 @@ function taskDefinitionToSnapshot(task) {
|
|
|
574
499
|
...task.description === void 0 ? {} : { description: task.description },
|
|
575
500
|
status: "pending",
|
|
576
501
|
metadata: {},
|
|
502
|
+
attempts: 0,
|
|
577
503
|
...task.run === void 0 ? {} : { run: task.run },
|
|
578
504
|
...task.retries === void 0 ? {} : { retries: task.retries },
|
|
579
505
|
...task.timeout === void 0 ? {} : { timeout: task.timeout }
|
|
580
506
|
};
|
|
581
507
|
}
|
|
582
508
|
/**
|
|
509
|
+
* Convert interrupted running work into a recoverable pending suffix or an
|
|
510
|
+
* exhausted recovery failure without replenishing attempts.
|
|
511
|
+
*
|
|
512
|
+
* @param snapshot - A fully validated owned snapshot with no terminal overrides
|
|
513
|
+
* @returns The recovery projection
|
|
514
|
+
*/
|
|
515
|
+
function recoverWorkflowSnapshot(snapshot) {
|
|
516
|
+
const phases = [];
|
|
517
|
+
let halted = false;
|
|
518
|
+
const now = Math.max(Date.now(), snapshot.updated);
|
|
519
|
+
const workflow = buildWorkflowContext(snapshot);
|
|
520
|
+
for (const phase of snapshot.phases) {
|
|
521
|
+
const exhausted = /* @__PURE__ */ new Set();
|
|
522
|
+
for (const task of phase.tasks) {
|
|
523
|
+
const budget = (task.retries ?? 0) + 1;
|
|
524
|
+
if (task.status === "running" && task.attempts >= budget) exhausted.add(task.id);
|
|
525
|
+
}
|
|
526
|
+
const strict = phase.bail && (exhausted.size > 0 || phase.tasks.some((task) => task.status === "failed"));
|
|
527
|
+
const tasks = [];
|
|
528
|
+
for (const task of phase.tasks) {
|
|
529
|
+
const eligible = task.status === "pending" || task.status === "running";
|
|
530
|
+
if ((halted || strict) && eligible && !exhausted.has(task.id)) {
|
|
531
|
+
tasks.push({
|
|
532
|
+
...task,
|
|
533
|
+
status: "skipped"
|
|
534
|
+
});
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
if (!exhausted.has(task.id)) {
|
|
538
|
+
if (task.status === "running") {
|
|
539
|
+
const { activity: _activity, ...pending } = task;
|
|
540
|
+
tasks.push({
|
|
541
|
+
...pending,
|
|
542
|
+
status: "pending"
|
|
543
|
+
});
|
|
544
|
+
} else tasks.push(task);
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
const phaseContext = buildPhaseContext(workflow, phase);
|
|
548
|
+
const result = {
|
|
549
|
+
task: buildTaskContext(phaseContext, task),
|
|
550
|
+
phase: phaseContext,
|
|
551
|
+
workflow,
|
|
552
|
+
status: "failed",
|
|
553
|
+
result: {
|
|
554
|
+
success: false,
|
|
555
|
+
error: {
|
|
556
|
+
origin: "recovery",
|
|
557
|
+
message: `task '${task.id}' exhausted its retry budget during recovery`
|
|
558
|
+
}
|
|
559
|
+
},
|
|
560
|
+
timestamp: now
|
|
561
|
+
};
|
|
562
|
+
tasks.push({
|
|
563
|
+
...task,
|
|
564
|
+
status: "failed",
|
|
565
|
+
result
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
const status = derivePhaseStatus(tasks.map((task) => task.status));
|
|
569
|
+
phases.push({
|
|
570
|
+
...phase,
|
|
571
|
+
status,
|
|
572
|
+
tasks
|
|
573
|
+
});
|
|
574
|
+
if (strict) halted = true;
|
|
575
|
+
}
|
|
576
|
+
return {
|
|
577
|
+
...snapshot,
|
|
578
|
+
status: deriveWorkflowStatus(phases.map((phase) => ({
|
|
579
|
+
status: phase.status,
|
|
580
|
+
bail: phase.bail
|
|
581
|
+
}))),
|
|
582
|
+
phases,
|
|
583
|
+
updated: now
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
583
587
|
* Flatten a nested list of per-phase {@link TaskResult} lists into one positional list
|
|
584
588
|
* — the workflow tier of the result tree, built from each phase's `results()`.
|
|
585
589
|
*
|
|
@@ -665,6 +669,61 @@ function createDeferred() {
|
|
|
665
669
|
return Promise.withResolvers();
|
|
666
670
|
}
|
|
667
671
|
/**
|
|
672
|
+
* Schedule one cancellable host operation behind an owned settlement signal.
|
|
673
|
+
*
|
|
674
|
+
* @remarks
|
|
675
|
+
* The completion and failure paths each own an {@link AbortController}; their native composite is
|
|
676
|
+
* linked to the optional caller signal before `start` can arm host work. Scheduler backends attach
|
|
677
|
+
* only to that safe composite, so caller mutation of `addEventListener` or `removeEventListener`
|
|
678
|
+
* cannot strand the operation. The first completion resolves, the first host failure rejects with
|
|
679
|
+
* its exact value, and caller abort rejects with its exact linked reason. Caller abort and host
|
|
680
|
+
* failure cancel an armed handle; synchronous settlement also cancels the handle immediately after
|
|
681
|
+
* `start` returns it. Cancellation is secondary cleanup: if its closure throws, the already-winning
|
|
682
|
+
* completion, exact host failure, or exact caller reason still settles without escape or replacement.
|
|
683
|
+
*
|
|
684
|
+
* @param start - Arm host work and return its cancellation closure
|
|
685
|
+
* @param signal - Optional caller cancellation signal
|
|
686
|
+
* @returns A promise settled exactly once by completion, host failure, or caller abort
|
|
687
|
+
*/
|
|
688
|
+
function scheduleHost(start, signal) {
|
|
689
|
+
const completion = new AbortController();
|
|
690
|
+
const failed = new AbortController();
|
|
691
|
+
let settled;
|
|
692
|
+
try {
|
|
693
|
+
settled = (0, _orkestrel_abort.linkSignal)(AbortSignal.any([completion.signal, failed.signal]), signal);
|
|
694
|
+
} catch (error) {
|
|
695
|
+
return Promise.reject(error);
|
|
696
|
+
}
|
|
697
|
+
if (settled.aborted) return Promise.reject(settled.reason);
|
|
698
|
+
return new Promise((resolve, reject) => {
|
|
699
|
+
let cancel;
|
|
700
|
+
let hostFailure;
|
|
701
|
+
settled.addEventListener("abort", () => {
|
|
702
|
+
if (completion.signal.aborted) {
|
|
703
|
+
resolve();
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
const reason = failed.signal.aborted ? hostFailure : settled.reason;
|
|
707
|
+
try {
|
|
708
|
+
cancel?.();
|
|
709
|
+
} catch {}
|
|
710
|
+
reject(reason);
|
|
711
|
+
}, { once: true });
|
|
712
|
+
try {
|
|
713
|
+
cancel = start(() => completion.abort(), (error) => {
|
|
714
|
+
hostFailure = error;
|
|
715
|
+
failed.abort();
|
|
716
|
+
});
|
|
717
|
+
} catch (error) {
|
|
718
|
+
hostFailure = error;
|
|
719
|
+
failed.abort();
|
|
720
|
+
}
|
|
721
|
+
if (settled.aborted) try {
|
|
722
|
+
cancel?.();
|
|
723
|
+
} catch {}
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
668
727
|
* Park until `signal` aborts — a promise-parked wait (AGENTS §21), never a timer or
|
|
669
728
|
* busy-loop, that NEVER rejects.
|
|
670
729
|
*
|
|
@@ -692,6 +751,424 @@ function parkSignal(signal) {
|
|
|
692
751
|
});
|
|
693
752
|
}
|
|
694
753
|
//#endregion
|
|
754
|
+
//#region src/core/Scheduler.ts
|
|
755
|
+
/**
|
|
756
|
+
* The safe cross-environment cooperative-yield default — a {@link SchedulerInterface}
|
|
757
|
+
* built on `setTimeout` / `clearTimeout` alone, so it runs unchanged in both the
|
|
758
|
+
* browser and Node.
|
|
759
|
+
*
|
|
760
|
+
* @remarks
|
|
761
|
+
* - **Cross-environment.** Uses ONLY `setTimeout` / `clearTimeout` — universally
|
|
762
|
+
* available. It deliberately avoids env-specific fast paths (`setImmediate`,
|
|
763
|
+
* `scheduler.yield`, `requestAnimationFrame`, `node:timers/promises`,
|
|
764
|
+
* `MessageChannel`); those belong to the environment backends, built with the
|
|
765
|
+
* agent loop that consumes them.
|
|
766
|
+
* - **`yield` is a macrotask host-turn, not a microtask.** `yield()` waits on a
|
|
767
|
+
* `setTimeout(0)`, NOT `queueMicrotask`. A microtask drains before the host
|
|
768
|
+
* regains control, so it would not actually let pending I/O, timers, or
|
|
769
|
+
* rendering run — it only defers within the current task. A zero-delay timer is
|
|
770
|
+
* the correct cross-environment "give the host a turn".
|
|
771
|
+
* - **Abort-aware.** A pending `yield` / `delay` rejects with `signal.reason` exactly.
|
|
772
|
+
* {@link scheduleHost} links an owned settlement composite to the caller before arming
|
|
773
|
+
* the timer, so pre-abort schedules nothing, caller signal method mutation is harmless,
|
|
774
|
+
* cancellation clears the handle, and native first-settlement wins exactly once.
|
|
775
|
+
* - **Priority is accepted but uniform.** `options.priority` is part of the
|
|
776
|
+
* contract, but a `setTimeout`-based default cannot act on urgency, so it treats
|
|
777
|
+
* every priority the same. Environment backends honour it.
|
|
778
|
+
* - **Event-free.** A pure functional primitive — no Emitter, no events.
|
|
779
|
+
*
|
|
780
|
+
* @example
|
|
781
|
+
* ```ts
|
|
782
|
+
* const scheduler = new Scheduler()
|
|
783
|
+
* while (!signal.aborted) {
|
|
784
|
+
* doSomeWork()
|
|
785
|
+
* await scheduler.yield({ signal }) // let the host run between work units
|
|
786
|
+
* }
|
|
787
|
+
* ```
|
|
788
|
+
*/
|
|
789
|
+
var Scheduler = class {
|
|
790
|
+
/**
|
|
791
|
+
* Yield control back to the host so other tasks (I/O, timers, rendering) can
|
|
792
|
+
* run, then resume — a macrotask turn via `setTimeout(0)` (NOT a microtask,
|
|
793
|
+
* which would resume before the host regains control).
|
|
794
|
+
*/
|
|
795
|
+
yield(options) {
|
|
796
|
+
return this.#sleep(0, options?.signal);
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* Resume after at least `ms` milliseconds; abort rejects with `signal.reason`.
|
|
800
|
+
*
|
|
801
|
+
* @remarks
|
|
802
|
+
* `ms` should be a non-negative finite number. The primitive stays minimal and
|
|
803
|
+
* does no validation: it passes `ms` straight to the host `setTimeout`, which
|
|
804
|
+
* clamps a negative value or `NaN` to ~0 — so an out-of-domain `ms` resolves on
|
|
805
|
+
* the next host turn rather than throwing.
|
|
806
|
+
*/
|
|
807
|
+
delay(ms, options) {
|
|
808
|
+
return this.#sleep(ms, options?.signal);
|
|
809
|
+
}
|
|
810
|
+
#sleep(ms, signal) {
|
|
811
|
+
return scheduleHost((complete) => {
|
|
812
|
+
const handle = setTimeout(complete, ms);
|
|
813
|
+
return () => clearTimeout(handle);
|
|
814
|
+
}, signal);
|
|
815
|
+
}
|
|
816
|
+
};
|
|
817
|
+
//#endregion
|
|
818
|
+
//#region src/core/errors.ts
|
|
819
|
+
/**
|
|
820
|
+
* An error raised by the workflow runtime.
|
|
821
|
+
*
|
|
822
|
+
* @remarks
|
|
823
|
+
* Carries a {@link WorkflowErrorCode} and an optional `context` bag naming the
|
|
824
|
+
* offending node id / status. Raised for an illegal lifecycle transition
|
|
825
|
+
* (`TRANSITION`), a structurally invalid {@link import('./types.js').WorkflowSnapshot}
|
|
826
|
+
* boundary (`RESTORE`), or a refused structural/activity edit (`MUTATION`).
|
|
827
|
+
*/
|
|
828
|
+
var WorkflowError = class extends Error {
|
|
829
|
+
code;
|
|
830
|
+
context;
|
|
831
|
+
constructor(code, message, context) {
|
|
832
|
+
super(message);
|
|
833
|
+
this.name = "WorkflowError";
|
|
834
|
+
this.code = code;
|
|
835
|
+
if (context !== void 0) this.context = context;
|
|
836
|
+
}
|
|
837
|
+
};
|
|
838
|
+
/**
|
|
839
|
+
* Narrow an unknown caught value to a {@link WorkflowError}.
|
|
840
|
+
*
|
|
841
|
+
* @param value - The value to test (typically a `catch` binding)
|
|
842
|
+
* @returns `true` when `value` is a {@link WorkflowError}
|
|
843
|
+
*
|
|
844
|
+
* @example
|
|
845
|
+
* ```ts
|
|
846
|
+
* try {
|
|
847
|
+
* task.complete('done')
|
|
848
|
+
* } catch (error) {
|
|
849
|
+
* if (isWorkflowError(error) && error.code === 'TRANSITION') retry()
|
|
850
|
+
* }
|
|
851
|
+
* ```
|
|
852
|
+
*/
|
|
853
|
+
function isWorkflowError(value) {
|
|
854
|
+
try {
|
|
855
|
+
return value instanceof WorkflowError;
|
|
856
|
+
} catch {
|
|
857
|
+
return false;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
//#endregion
|
|
861
|
+
//#region src/core/validators.ts
|
|
862
|
+
/** Test the workflow lifecycle vocabulary. */
|
|
863
|
+
function isLifecycleStatus(value) {
|
|
864
|
+
return value === "pending" || value === "running" || value === "completed" || value === "failed" || value === "skipped" || value === "stopped";
|
|
865
|
+
}
|
|
866
|
+
/** Test a normalized persisted task failure. */
|
|
867
|
+
function isTaskFailure(value) {
|
|
868
|
+
try {
|
|
869
|
+
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);
|
|
870
|
+
} catch {
|
|
871
|
+
return false;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
/** Compare two optional description values. */
|
|
875
|
+
function matchesDescription(left, right) {
|
|
876
|
+
return left === right && (left === void 0 || typeof left === "string");
|
|
877
|
+
}
|
|
878
|
+
/** Test a result's lineage against its containing snapshot nodes. */
|
|
879
|
+
function isTaskResult(value, workflow, phase, task) {
|
|
880
|
+
try {
|
|
881
|
+
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;
|
|
882
|
+
if (value.task.id !== task.id || value.task.name !== task.name || !matchesDescription(value.task.description, task.description) || value.phase.id !== phase.id || value.phase.name !== phase.name || !matchesDescription(value.phase.description, phase.description) || value.workflow.id !== workflow.id || value.workflow.name !== workflow.name || !matchesDescription(value.workflow.description, workflow.description)) return false;
|
|
883
|
+
if (!(0, _orkestrel_contract.isRecord)(value.task.phase) || !(0, _orkestrel_contract.isRecord)(value.task.phase.workflow) || !(0, _orkestrel_contract.isRecord)(value.phase.workflow) || !Object.keys(value.task.phase).every((key) => key === "id" || key === "name" || key === "description" || key === "workflow") || !Object.keys(value.task.phase.workflow).every((key) => key === "id" || key === "name" || key === "description") || !Object.keys(value.phase.workflow).every((key) => key === "id" || key === "name" || key === "description")) return false;
|
|
884
|
+
if (value.task.phase.id !== phase.id || value.task.phase.name !== phase.name || !matchesDescription(value.task.phase.description, phase.description) || value.phase.workflow.id !== workflow.id || value.phase.workflow.name !== workflow.name || !matchesDescription(value.phase.workflow.description, workflow.description) || value.task.phase.workflow.id !== workflow.id || value.task.phase.workflow.name !== workflow.name || !matchesDescription(value.task.phase.workflow.description, workflow.description)) return false;
|
|
885
|
+
if (value.status === "completed") return (0, _orkestrel_contract.isRecord)(value.result) && value.result.success === true && Object.keys(value.result).every((key) => key === "success" || key === "value") && (0, _orkestrel_contract.isJSONValue)(value.result.value);
|
|
886
|
+
if (value.status === "failed") return (0, _orkestrel_contract.isRecord)(value.result) && value.result.success === false && Object.keys(value.result).every((key) => key === "success" || key === "error") && isTaskFailure(value.result.error);
|
|
887
|
+
return false;
|
|
888
|
+
} catch {
|
|
889
|
+
return false;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* Validate a safe owned JSON graph as a coherent workflow snapshot.
|
|
894
|
+
*
|
|
895
|
+
* @remarks
|
|
896
|
+
* Callers at hostile boundaries use {@link isWorkflowSnapshot}, which owns the
|
|
897
|
+
* graph first so this semantic pass never observes accessors or prototypes.
|
|
898
|
+
*/
|
|
899
|
+
function isOwnedWorkflowSnapshot(value) {
|
|
900
|
+
try {
|
|
901
|
+
if (!(0, _orkestrel_contract.isRecord)(value) || !Object.keys(value).every((key) => key === "id" || key === "name" || key === "description" || key === "status" || key === "override" || key === "bail" || key === "phases" || key === "created" || key === "updated") || !(0, _orkestrel_contract.isNonEmptyString)(value.id) || !(0, _orkestrel_contract.isNonEmptyString)(value.name) || value.description !== void 0 && typeof value.description !== "string" || !isLifecycleStatus(value.status) || value.override !== void 0 && value.override !== "completed" && value.override !== "skipped" && value.override !== "stopped" || !(0, _orkestrel_contract.isBoolean)(value.bail) || !(0, _orkestrel_contract.isArray)(value.phases) || !(0, _orkestrel_contract.isFiniteNumber)(value.created) || value.created < 0 || !(0, _orkestrel_contract.isFiniteNumber)(value.updated) || value.updated < value.created) return false;
|
|
902
|
+
const phaseIds = /* @__PURE__ */ new Set();
|
|
903
|
+
const derivations = [];
|
|
904
|
+
let frontier = false;
|
|
905
|
+
let running = false;
|
|
906
|
+
let vacuous = true;
|
|
907
|
+
for (const phase of value.phases) {
|
|
908
|
+
if (!(0, _orkestrel_contract.isRecord)(phase) || !Object.keys(phase).every((key) => key === "id" || key === "name" || key === "description" || key === "status" || key === "override" || key === "bail" || key === "concurrency" || key === "tasks") || !(0, _orkestrel_contract.isNonEmptyString)(phase.id) || phaseIds.has(phase.id) || !(0, _orkestrel_contract.isNonEmptyString)(phase.name) || phase.description !== void 0 && typeof phase.description !== "string" || !isLifecycleStatus(phase.status) || phase.override !== void 0 && phase.override !== "skipped" && phase.override !== "stopped" || !(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 false;
|
|
909
|
+
const forced = phase.override === "skipped" || phase.override === "stopped";
|
|
910
|
+
const started = phase.status === "running" || phase.status === "completed" || phase.status === "failed";
|
|
911
|
+
if (!forced && frontier && started || phase.status === "running" && running) return false;
|
|
912
|
+
if (phase.status === "running") running = true;
|
|
913
|
+
if (!forced && (phase.status === "pending" || phase.status === "running" || phase.status === "failed" && phase.bail)) frontier = true;
|
|
914
|
+
phaseIds.add(phase.id);
|
|
915
|
+
const taskIds = /* @__PURE__ */ new Set();
|
|
916
|
+
const statuses = [];
|
|
917
|
+
if (phase.tasks.length > 0) vacuous = false;
|
|
918
|
+
for (const task of phase.tasks) {
|
|
919
|
+
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 === "run" || 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.run !== void 0 && !(0, _orkestrel_contract.isNonEmptyString)(task.run) || 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;
|
|
920
|
+
const budget = (task.retries ?? 0) + 1;
|
|
921
|
+
if (task.attempts > budget || task.status === "pending" && task.attempts >= budget) return false;
|
|
922
|
+
if (!(task.activity === void 0 || isTaskActivity(task.activity))) return false;
|
|
923
|
+
if (task.status === "running" || task.status === "completed" || task.status === "failed") {
|
|
924
|
+
if (task.attempts < 1 || task.activity === void 0) return false;
|
|
925
|
+
}
|
|
926
|
+
if (task.status === "pending" && task.activity !== void 0) return false;
|
|
927
|
+
if (task.status === "completed" || task.status === "failed") {
|
|
928
|
+
if (!isTaskResult(task.result, value, phase, task)) return false;
|
|
929
|
+
} else if (task.result !== void 0) return false;
|
|
930
|
+
taskIds.add(task.id);
|
|
931
|
+
statuses.push(task.status);
|
|
932
|
+
}
|
|
933
|
+
const derived = derivePhaseStatus(statuses);
|
|
934
|
+
if (phase.status !== (phase.override ?? derived) || phase.override !== void 0 && phase.status !== phase.override) return false;
|
|
935
|
+
derivations.push({
|
|
936
|
+
status: phase.status,
|
|
937
|
+
bail: phase.bail
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
const derived = deriveWorkflowStatus(derivations);
|
|
941
|
+
if (value.override === "completed") return value.status === "completed" && derived === "pending" && vacuous;
|
|
942
|
+
return value.status === (value.override ?? derived);
|
|
943
|
+
} catch {
|
|
944
|
+
return false;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
/** Total hostile-boundary workflow snapshot guard. */
|
|
948
|
+
function isWorkflowSnapshot(value) {
|
|
949
|
+
const cloned = (0, _orkestrel_contract.attempt)(() => (0, _orkestrel_contract.cloneJSONValue)(value));
|
|
950
|
+
return cloned.success && isOwnedWorkflowSnapshot(cloned.value);
|
|
951
|
+
}
|
|
952
|
+
function hasWorkflowHandlers(workflow, functions) {
|
|
953
|
+
if ("destroyed" in workflow) {
|
|
954
|
+
for (const phase of workflow.phases.phases()) for (const task of phase.tasks.tasks()) if (task.run !== void 0 && !(0, _orkestrel_contract.isFunction)(task.handler)) return false;
|
|
955
|
+
return true;
|
|
956
|
+
}
|
|
957
|
+
const runs = /* @__PURE__ */ new Set();
|
|
958
|
+
for (const phase of workflow.phases) for (const task of phase.tasks) {
|
|
959
|
+
if (task.run === void 0 || runs.has(task.run)) continue;
|
|
960
|
+
runs.add(task.run);
|
|
961
|
+
if (!(0, _orkestrel_contract.isFunction)(functions?.[task.run])) return false;
|
|
962
|
+
}
|
|
963
|
+
return true;
|
|
964
|
+
}
|
|
965
|
+
/** Locate the nearest identifiable node for an inconsistent owned snapshot. */
|
|
966
|
+
function workflowSnapshotContext(value) {
|
|
967
|
+
if (!(0, _orkestrel_contract.isRecord)(value) || !(0, _orkestrel_contract.isArray)(value.phases)) return void 0;
|
|
968
|
+
for (const phase of value.phases) {
|
|
969
|
+
if (!(0, _orkestrel_contract.isRecord)(phase)) continue;
|
|
970
|
+
const phaseContext = (0, _orkestrel_contract.isNonEmptyString)(phase.id) ? { phase: phase.id } : void 0;
|
|
971
|
+
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;
|
|
972
|
+
for (const task of phase.tasks) {
|
|
973
|
+
if (!(0, _orkestrel_contract.isRecord)(task)) continue;
|
|
974
|
+
if (task.run !== void 0 && !(0, _orkestrel_contract.isNonEmptyString)(task.run) || 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 {
|
|
975
|
+
...phaseContext ?? {},
|
|
976
|
+
...(0, _orkestrel_contract.isNonEmptyString)(task.id) ? { task: task.id } : {}
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
/**
|
|
982
|
+
* Test whether an unknown value is a valid whole-frame activity report.
|
|
983
|
+
*/
|
|
984
|
+
function isTaskActivityInput(value) {
|
|
985
|
+
try {
|
|
986
|
+
if (!(0, _orkestrel_contract.isRecord)(value)) return false;
|
|
987
|
+
const prototype = Object.getPrototypeOf(value);
|
|
988
|
+
if (prototype !== Object.prototype && prototype !== null || !Object.keys(value).every((key) => key === "note" || key === "progress" || key === "operations" || key === "constraints")) return false;
|
|
989
|
+
const note = value.note;
|
|
990
|
+
const progress = value.progress;
|
|
991
|
+
const operations = value.operations;
|
|
992
|
+
const constraints = value.constraints;
|
|
993
|
+
if (note !== void 0 && !(0, _orkestrel_contract.isNonEmptyString)(note)) return false;
|
|
994
|
+
if (progress !== void 0) {
|
|
995
|
+
if (!(0, _orkestrel_contract.isRecord)(progress)) return false;
|
|
996
|
+
const progressPrototype = Object.getPrototypeOf(progress);
|
|
997
|
+
if (progressPrototype !== Object.prototype && progressPrototype !== null || !Object.keys(progress).every((key) => key === "current" || key === "total" || key === "unit")) return false;
|
|
998
|
+
const current = progress.current;
|
|
999
|
+
const total = progress.total;
|
|
1000
|
+
const unit = progress.unit;
|
|
1001
|
+
if (!(0, _orkestrel_contract.isFiniteNumber)(current) || current < 0 || total !== void 0 && (!(0, _orkestrel_contract.isFiniteNumber)(total) || total < current) || unit !== void 0 && !(0, _orkestrel_contract.isNonEmptyString)(unit)) return false;
|
|
1002
|
+
}
|
|
1003
|
+
if (operations !== void 0) {
|
|
1004
|
+
if (!(0, _orkestrel_contract.isArray)(operations)) return false;
|
|
1005
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1006
|
+
for (const operation of operations) {
|
|
1007
|
+
if (!(0, _orkestrel_contract.isRecord)(operation)) return false;
|
|
1008
|
+
const operationPrototype = Object.getPrototypeOf(operation);
|
|
1009
|
+
if (operationPrototype !== Object.prototype && operationPrototype !== null || !Object.keys(operation).every((key) => key === "id" || key === "name" || key === "started")) return false;
|
|
1010
|
+
const id = operation.id;
|
|
1011
|
+
const name = operation.name;
|
|
1012
|
+
const started = operation.started;
|
|
1013
|
+
if (!(0, _orkestrel_contract.isNonEmptyString)(id) || !(0, _orkestrel_contract.isNonEmptyString)(name) || !(0, _orkestrel_contract.isFiniteNumber)(started) || started < 0 || ids.has(id)) return false;
|
|
1014
|
+
ids.add(id);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
if (constraints !== void 0) {
|
|
1018
|
+
if (!(0, _orkestrel_contract.isArray)(constraints)) return false;
|
|
1019
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1020
|
+
for (const constraint of constraints) {
|
|
1021
|
+
if (!(0, _orkestrel_contract.isRecord)(constraint)) return false;
|
|
1022
|
+
const constraintPrototype = Object.getPrototypeOf(constraint);
|
|
1023
|
+
if (constraintPrototype !== Object.prototype && constraintPrototype !== null || !Object.keys(constraint).every((key) => key === "id" || key === "name" || key === "started")) return false;
|
|
1024
|
+
const id = constraint.id;
|
|
1025
|
+
const name = constraint.name;
|
|
1026
|
+
const started = constraint.started;
|
|
1027
|
+
if (!(0, _orkestrel_contract.isNonEmptyString)(id) || !(0, _orkestrel_contract.isNonEmptyString)(name) || !(0, _orkestrel_contract.isFiniteNumber)(started) || started < 0 || ids.has(id)) return false;
|
|
1028
|
+
ids.add(id);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
return true;
|
|
1032
|
+
} catch {
|
|
1033
|
+
return false;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* Test whether an unknown value is valid persisted task activity.
|
|
1038
|
+
*/
|
|
1039
|
+
function isTaskActivity(value) {
|
|
1040
|
+
try {
|
|
1041
|
+
if (!(0, _orkestrel_contract.isRecord)(value)) return false;
|
|
1042
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1043
|
+
if (prototype !== Object.prototype && prototype !== null || !Object.keys(value).every((key) => key === "note" || key === "progress" || key === "operations" || key === "constraints" || key === "updated")) return false;
|
|
1044
|
+
const note = value.note;
|
|
1045
|
+
const progress = value.progress;
|
|
1046
|
+
const operations = value.operations;
|
|
1047
|
+
const constraints = value.constraints;
|
|
1048
|
+
const updated = value.updated;
|
|
1049
|
+
if (operations === void 0 || constraints === void 0 || !(0, _orkestrel_contract.isFiniteNumber)(updated) || updated < 0) return false;
|
|
1050
|
+
return isTaskActivityInput({
|
|
1051
|
+
...note === void 0 ? {} : { note },
|
|
1052
|
+
...progress === void 0 ? {} : { progress },
|
|
1053
|
+
operations,
|
|
1054
|
+
constraints
|
|
1055
|
+
});
|
|
1056
|
+
} catch {
|
|
1057
|
+
return false;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
//#endregion
|
|
1061
|
+
//#region src/core/cloners.ts
|
|
1062
|
+
/**
|
|
1063
|
+
* Validate and own a workflow snapshot before live construction.
|
|
1064
|
+
*
|
|
1065
|
+
* @param input - The hostile snapshot boundary
|
|
1066
|
+
* @param id - The optional storage key the owned snapshot must match
|
|
1067
|
+
* @returns A deeply owned frozen snapshot
|
|
1068
|
+
* @throws {WorkflowError} With `RESTORE` when the snapshot is invalid or does not match `id`
|
|
1069
|
+
*/
|
|
1070
|
+
function cloneWorkflowSnapshot(input, id) {
|
|
1071
|
+
let cloned;
|
|
1072
|
+
try {
|
|
1073
|
+
cloned = (0, _orkestrel_contract.cloneJSONValue)(input);
|
|
1074
|
+
} catch (error) {
|
|
1075
|
+
if (isWorkflowError(error)) throw error;
|
|
1076
|
+
if ((0, _orkestrel_contract.isContractError)(error)) throw new WorkflowError("RESTORE", `workflow snapshot could not be read safely: ${error.message}`);
|
|
1077
|
+
throw new WorkflowError("RESTORE", "workflow snapshot could not be read safely");
|
|
1078
|
+
}
|
|
1079
|
+
if (!isOwnedWorkflowSnapshot(cloned)) throw new WorkflowError("RESTORE", "workflow snapshot is inconsistent", workflowSnapshotContext(cloned));
|
|
1080
|
+
if (id !== void 0 && cloned.id !== id) throw new WorkflowError("RESTORE", `workflow snapshot '${cloned.id}' does not match storage key '${id}'`, {
|
|
1081
|
+
requested: id,
|
|
1082
|
+
payload: cloned.id
|
|
1083
|
+
});
|
|
1084
|
+
return cloned;
|
|
1085
|
+
}
|
|
1086
|
+
/**
|
|
1087
|
+
* Validate and clone one complete task activity frame.
|
|
1088
|
+
*
|
|
1089
|
+
* @remarks
|
|
1090
|
+
* This is the hostile boundary behind task reports and snapshot hydration. Supplying
|
|
1091
|
+
* `updated` stamps an input frame without reading an `updated` property from it; omitting
|
|
1092
|
+
* `updated` restores a stored frame and reads its persisted timestamp exactly once. Every
|
|
1093
|
+
* untrusted property is captured once inside one protected boundary. The returned frame,
|
|
1094
|
+
* collections, progress, operations, and constraints are copied and frozen.
|
|
1095
|
+
*
|
|
1096
|
+
* @param input - The untrusted complete activity frame
|
|
1097
|
+
* @param updated - An optional accepted timestamp used instead of a persisted `updated`
|
|
1098
|
+
* @returns An immutable cloned {@link TaskActivity}
|
|
1099
|
+
* @throws {WorkflowError} With `MUTATION` when the frame cannot be read or validated
|
|
1100
|
+
*/
|
|
1101
|
+
function cloneTaskActivity(input, updated) {
|
|
1102
|
+
try {
|
|
1103
|
+
if (!(0, _orkestrel_contract.isRecord)(input)) throw new WorkflowError("MUTATION", "task activity must be a record");
|
|
1104
|
+
const inputPrototype = Object.getPrototypeOf(input);
|
|
1105
|
+
if (inputPrototype !== Object.prototype && inputPrototype !== null || !Object.keys(input).every((key) => key === "note" || key === "progress" || key === "operations" || key === "constraints" || updated === void 0 && key === "updated")) throw new WorkflowError("MUTATION", "task activity must be a record");
|
|
1106
|
+
const note = input.note;
|
|
1107
|
+
const progressInput = input.progress;
|
|
1108
|
+
const operationsInput = input.operations;
|
|
1109
|
+
const constraintsInput = input.constraints;
|
|
1110
|
+
const accepted = updated === void 0 ? input.updated : updated;
|
|
1111
|
+
const operationInputs = operationsInput === void 0 ? [] : (0, _orkestrel_contract.isArray)(operationsInput) ? [...operationsInput] : void 0;
|
|
1112
|
+
if (operationInputs === void 0) throw new WorkflowError("MUTATION", "task activity operations must be an array");
|
|
1113
|
+
const operations = [];
|
|
1114
|
+
for (const operation of operationInputs) {
|
|
1115
|
+
if (!(0, _orkestrel_contract.isRecord)(operation)) throw new WorkflowError("MUTATION", "task activity contains an invalid operation");
|
|
1116
|
+
const operationPrototype = Object.getPrototypeOf(operation);
|
|
1117
|
+
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");
|
|
1118
|
+
const id = operation.id;
|
|
1119
|
+
const name = operation.name;
|
|
1120
|
+
const started = operation.started;
|
|
1121
|
+
operations.push(Object.freeze({
|
|
1122
|
+
id,
|
|
1123
|
+
name,
|
|
1124
|
+
started
|
|
1125
|
+
}));
|
|
1126
|
+
}
|
|
1127
|
+
let progress;
|
|
1128
|
+
if (progressInput !== void 0) {
|
|
1129
|
+
if (!(0, _orkestrel_contract.isRecord)(progressInput)) throw new WorkflowError("MUTATION", "task activity contains invalid progress");
|
|
1130
|
+
const progressPrototype = Object.getPrototypeOf(progressInput);
|
|
1131
|
+
if (progressPrototype !== Object.prototype && progressPrototype !== null || !Object.keys(progressInput).every((key) => key === "current" || key === "total" || key === "unit")) throw new WorkflowError("MUTATION", "task activity contains invalid progress");
|
|
1132
|
+
const current = progressInput.current;
|
|
1133
|
+
const total = progressInput.total;
|
|
1134
|
+
const unit = progressInput.unit;
|
|
1135
|
+
progress = Object.freeze({
|
|
1136
|
+
current,
|
|
1137
|
+
...total === void 0 ? {} : { total },
|
|
1138
|
+
...unit === void 0 ? {} : { unit }
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
const constraintInputs = constraintsInput === void 0 ? [] : (0, _orkestrel_contract.isArray)(constraintsInput) ? [...constraintsInput] : void 0;
|
|
1142
|
+
if (constraintInputs === void 0) throw new WorkflowError("MUTATION", "task activity constraints must be an array");
|
|
1143
|
+
const constraints = [];
|
|
1144
|
+
for (const constraint of constraintInputs) {
|
|
1145
|
+
if (!(0, _orkestrel_contract.isRecord)(constraint)) throw new WorkflowError("MUTATION", "task activity contains an invalid constraint");
|
|
1146
|
+
const constraintPrototype = Object.getPrototypeOf(constraint);
|
|
1147
|
+
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");
|
|
1148
|
+
const id = constraint.id;
|
|
1149
|
+
const name = constraint.name;
|
|
1150
|
+
const started = constraint.started;
|
|
1151
|
+
constraints.push(Object.freeze({
|
|
1152
|
+
id,
|
|
1153
|
+
name,
|
|
1154
|
+
started
|
|
1155
|
+
}));
|
|
1156
|
+
}
|
|
1157
|
+
const activity = Object.freeze({
|
|
1158
|
+
...note === void 0 ? {} : { note },
|
|
1159
|
+
...progress === void 0 ? {} : { progress },
|
|
1160
|
+
operations: Object.freeze(operations),
|
|
1161
|
+
constraints: Object.freeze(constraints),
|
|
1162
|
+
updated: accepted
|
|
1163
|
+
});
|
|
1164
|
+
if (!isTaskActivity(activity)) throw new WorkflowError("MUTATION", "task activity is invalid");
|
|
1165
|
+
return activity;
|
|
1166
|
+
} catch (error) {
|
|
1167
|
+
if (isWorkflowError(error)) throw error;
|
|
1168
|
+
throw new WorkflowError("MUTATION", "task activity could not be read safely");
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
//#endregion
|
|
695
1172
|
//#region src/core/shapers.ts
|
|
696
1173
|
/**
|
|
697
1174
|
* The shape of a {@link import('./types.js').TaskDefinition} — identity plus an optional
|
|
@@ -718,6 +1195,7 @@ var taskShape = (0, _orkestrel_contract.objectShape)({
|
|
|
718
1195
|
})),
|
|
719
1196
|
timeout: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.integerShape)({
|
|
720
1197
|
min: 0,
|
|
1198
|
+
max: MAX_TIMER_MS,
|
|
721
1199
|
description: "Per-attempt deadline in milliseconds; overrides the phase default. Omitted means no deadline."
|
|
722
1200
|
}))
|
|
723
1201
|
});
|
|
@@ -828,7 +1306,8 @@ var phaseUpdateShape = (0, _orkestrel_contract.objectShape)({
|
|
|
828
1306
|
* the row `{ id: snapshot.id, snapshot }`.
|
|
829
1307
|
* - **`get(id)` resolves the stored snapshot for an id**, narrowing the opaque JSON column back to
|
|
830
1308
|
* a {@link WorkflowSnapshot} ({@link import('../helpers.js').isWorkflowSnapshot} — the AGENTS §14
|
|
831
|
-
* boundary narrow for an untrusted storage read), or `undefined` if none is stored.
|
|
1309
|
+
* boundary narrow for an untrusted storage read), or `undefined` if none is stored. A present
|
|
1310
|
+
* snapshot whose own id differs from the requested key rejects with normalized `RESTORE` evidence.
|
|
832
1311
|
* - **`delete(id)` drops a snapshot by id**; an absent id is a no-op (no throw).
|
|
833
1312
|
*
|
|
834
1313
|
* UNLIKE the server package's `SessionStoreInterface` there is NO
|
|
@@ -839,7 +1318,8 @@ var phaseUpdateShape = (0, _orkestrel_contract.objectShape)({
|
|
|
839
1318
|
*
|
|
840
1319
|
* @example
|
|
841
1320
|
* ```ts
|
|
842
|
-
* import {
|
|
1321
|
+
* import { createMemoryDriver } from '@orkestrel/database'
|
|
1322
|
+
* import { createDatabaseWorkflowStore, createWorkflow, restoreWorkflow } from '@orkestrel/workflow'
|
|
843
1323
|
*
|
|
844
1324
|
* const store = createDatabaseWorkflowStore(createMemoryDriver()) // a durable driver swaps in here
|
|
845
1325
|
* const workflow = createWorkflow(definition)
|
|
@@ -860,17 +1340,18 @@ var DatabaseWorkflowStore = class {
|
|
|
860
1340
|
constructor(table) {
|
|
861
1341
|
this.#table = table;
|
|
862
1342
|
}
|
|
863
|
-
/** Resolve the
|
|
1343
|
+
/** Resolve and key-check the snapshot for `id`, narrowing the opaque column to `WorkflowSnapshot`. */
|
|
864
1344
|
async get(id) {
|
|
865
1345
|
const row = await this.#table.get(id);
|
|
866
1346
|
if (row === void 0) return void 0;
|
|
867
|
-
return
|
|
1347
|
+
return cloneWorkflowSnapshot(row.snapshot, id);
|
|
868
1348
|
}
|
|
869
1349
|
/** Insert or replace under the snapshot's OWN `id` (no separate id param) — the row is `{ id, snapshot }`. */
|
|
870
1350
|
async set(snapshot) {
|
|
1351
|
+
const owned = cloneWorkflowSnapshot(snapshot);
|
|
871
1352
|
await this.#table.set({
|
|
872
|
-
id:
|
|
873
|
-
snapshot
|
|
1353
|
+
id: owned.id,
|
|
1354
|
+
snapshot: owned
|
|
874
1355
|
});
|
|
875
1356
|
}
|
|
876
1357
|
/** Drop a snapshot by id; an absent id is a no-op (no throw). */
|
|
@@ -908,7 +1389,7 @@ var DatabaseWorkflowStore = class {
|
|
|
908
1389
|
*
|
|
909
1390
|
* @example
|
|
910
1391
|
* ```ts
|
|
911
|
-
* import { createMemoryWorkflowStore, createWorkflow, restoreWorkflow } from '@
|
|
1392
|
+
* import { createMemoryWorkflowStore, createWorkflow, restoreWorkflow } from '@orkestrel/workflow'
|
|
912
1393
|
*
|
|
913
1394
|
* const store = createMemoryWorkflowStore()
|
|
914
1395
|
* const workflow = createWorkflow(definition)
|
|
@@ -921,10 +1402,12 @@ var DatabaseWorkflowStore = class {
|
|
|
921
1402
|
var MemoryWorkflowStore = class {
|
|
922
1403
|
#snapshots = /* @__PURE__ */ new Map();
|
|
923
1404
|
get(id) {
|
|
924
|
-
|
|
1405
|
+
const snapshot = this.#snapshots.get(id);
|
|
1406
|
+
return Promise.resolve(snapshot === void 0 ? void 0 : cloneWorkflowSnapshot(snapshot));
|
|
925
1407
|
}
|
|
926
1408
|
set(snapshot) {
|
|
927
|
-
|
|
1409
|
+
const owned = cloneWorkflowSnapshot(snapshot);
|
|
1410
|
+
this.#snapshots.set(owned.id, owned);
|
|
928
1411
|
return Promise.resolve();
|
|
929
1412
|
}
|
|
930
1413
|
delete(id) {
|
|
@@ -947,9 +1430,8 @@ var MemoryWorkflowStore = class {
|
|
|
947
1430
|
* `TRANSITION` {@link WorkflowError} on an illegal move (e.g. completing a non-`running`
|
|
948
1431
|
* task) — the legal graph is the single source of truth, so the leaf can never reach an
|
|
949
1432
|
* impossible state.
|
|
950
|
-
* - **
|
|
951
|
-
*
|
|
952
|
-
* one and reinstate it AS an override — preserving the round-trip.
|
|
1433
|
+
* - **Snapshot fidelity.** A leaf needs no override: `skipped` / `stopped` are explicit terminal
|
|
1434
|
+
* statuses, and restore reinstates the leaf directly from {@link TaskSnapshot.status}.
|
|
953
1435
|
* - **The cascade.** Every status change records its boxed result (when any), fires the leaf's
|
|
954
1436
|
* OWN event, THEN calls the parent phase's `#recompute` (injected at construction) so the
|
|
955
1437
|
* transition propagates UP (Task → Phase → Workflow re-derive). The own-event-before-cascade
|
|
@@ -964,7 +1446,8 @@ var MemoryWorkflowStore = class {
|
|
|
964
1446
|
* matching {@link import('../types.js').TaskDefinition} / {@link TaskSnapshot} field. `handler`
|
|
965
1447
|
* is the RUNTIME-ONLY counterpart — `run` resolved ONCE at construction against the
|
|
966
1448
|
* workflow-level {@link import('../types.js').WorkflowOptions.functions} registry — and is
|
|
967
|
-
* NEVER persisted; `undefined` when `run` is omitted or unregistered
|
|
1449
|
+
* NEVER persisted; `undefined` when `run` is omitted or unregistered. Only omission is a
|
|
1450
|
+
* deliberate no-op; unresolved named work is rejected before dispatch.
|
|
968
1451
|
*/
|
|
969
1452
|
var Task = class {
|
|
970
1453
|
#context;
|
|
@@ -979,16 +1462,34 @@ var Task = class {
|
|
|
979
1462
|
#run;
|
|
980
1463
|
#retries;
|
|
981
1464
|
#timeout;
|
|
1465
|
+
#attempts;
|
|
982
1466
|
#handler;
|
|
983
|
-
|
|
984
|
-
|
|
1467
|
+
#abort;
|
|
1468
|
+
#silence;
|
|
1469
|
+
#onSilence;
|
|
1470
|
+
#liveness;
|
|
1471
|
+
#activity;
|
|
1472
|
+
#paused;
|
|
1473
|
+
#gate;
|
|
1474
|
+
#timerSignal;
|
|
1475
|
+
constructor(context, phase, workflow, recompute, options, status = "pending", result, run, retries, timeout, metadata = {}, attempts = 0, activity, handler, silence) {
|
|
1476
|
+
this.#context = buildTaskContext(context.phase, context);
|
|
985
1477
|
this.#phase = phase;
|
|
986
1478
|
this.#workflow = workflow;
|
|
987
1479
|
this.#recompute = recompute;
|
|
988
|
-
|
|
1480
|
+
try {
|
|
1481
|
+
const metadataOption = options?.metadata;
|
|
1482
|
+
this.#metadata = (0, _orkestrel_contract.cloneJSONRecord)(metadataOption ?? metadata);
|
|
1483
|
+
} catch (error) {
|
|
1484
|
+
if ((0, _orkestrel_contract.isContractError)(error)) throw new WorkflowError("RESTORE", `task '${context.id}' metadata could not be read safely: ${error.message}`, { task: context.id });
|
|
1485
|
+
throw new WorkflowError("RESTORE", `task '${context.id}' metadata could not be read safely`, { task: context.id });
|
|
1486
|
+
}
|
|
1487
|
+
const on = options?.on;
|
|
1488
|
+
const listenerError = options?.error;
|
|
1489
|
+
const silenceOption = options?.silence;
|
|
989
1490
|
this.#emitter = new _orkestrel_emitter.Emitter({
|
|
990
|
-
...
|
|
991
|
-
...
|
|
1491
|
+
...on === void 0 ? {} : { on },
|
|
1492
|
+
...listenerError === void 0 ? {} : { error: listenerError }
|
|
992
1493
|
});
|
|
993
1494
|
this.#status = status;
|
|
994
1495
|
this.#result = result;
|
|
@@ -1000,7 +1501,19 @@ var Task = class {
|
|
|
1000
1501
|
this.#run = run;
|
|
1001
1502
|
this.#retries = retries;
|
|
1002
1503
|
this.#timeout = timeout;
|
|
1504
|
+
this.#attempts = attempts;
|
|
1003
1505
|
this.#handler = handler;
|
|
1506
|
+
this.#abort = (0, _orkestrel_abort.createAbort)();
|
|
1507
|
+
this.#silence = resolveTaskSilence(silenceOption, silence);
|
|
1508
|
+
this.#onSilence = this.#expire.bind(this);
|
|
1509
|
+
this.#liveness = this.#silence === void 0 ? void 0 : (0, _orkestrel_timeout.createTimeout)({
|
|
1510
|
+
ms: this.#silence,
|
|
1511
|
+
signal: this.#abort.signal
|
|
1512
|
+
});
|
|
1513
|
+
this.#activity = activity === void 0 ? void 0 : cloneTaskActivity(activity);
|
|
1514
|
+
this.#paused = false;
|
|
1515
|
+
this.#gate = void 0;
|
|
1516
|
+
this.#timerSignal = void 0;
|
|
1004
1517
|
}
|
|
1005
1518
|
get emitter() {
|
|
1006
1519
|
return this.#emitter;
|
|
@@ -1026,6 +1539,9 @@ var Task = class {
|
|
|
1026
1539
|
get result() {
|
|
1027
1540
|
return this.#result;
|
|
1028
1541
|
}
|
|
1542
|
+
get attempts() {
|
|
1543
|
+
return this.#attempts;
|
|
1544
|
+
}
|
|
1029
1545
|
get run() {
|
|
1030
1546
|
return this.#run;
|
|
1031
1547
|
}
|
|
@@ -1038,40 +1554,120 @@ var Task = class {
|
|
|
1038
1554
|
get timeout() {
|
|
1039
1555
|
return this.#timeout;
|
|
1040
1556
|
}
|
|
1557
|
+
get activity() {
|
|
1558
|
+
return this.#activity;
|
|
1559
|
+
}
|
|
1560
|
+
get silence() {
|
|
1561
|
+
return this.#silence;
|
|
1562
|
+
}
|
|
1563
|
+
get silent() {
|
|
1564
|
+
return this.#status === "running" && this.#liveness?.expired === true;
|
|
1565
|
+
}
|
|
1566
|
+
get paused() {
|
|
1567
|
+
return this.#paused;
|
|
1568
|
+
}
|
|
1569
|
+
get signal() {
|
|
1570
|
+
return this.#abort.signal;
|
|
1571
|
+
}
|
|
1041
1572
|
start() {
|
|
1042
|
-
this.#
|
|
1573
|
+
const budget = Math.max(0, this.#retries ?? 0) + 1;
|
|
1574
|
+
if (this.#status !== "pending" && this.#status !== "running" || this.#attempts >= budget) throw new WorkflowError("TRANSITION", `task '${this.id}' cannot start another attempt`, {
|
|
1575
|
+
task: this.id,
|
|
1576
|
+
status: this.#status,
|
|
1577
|
+
attempts: this.#attempts,
|
|
1578
|
+
budget
|
|
1579
|
+
});
|
|
1580
|
+
if (this.#status === "pending") this.#transition("running");
|
|
1581
|
+
this.#attempts += 1;
|
|
1582
|
+
this.#activity = cloneTaskActivity({}, this.#stamp());
|
|
1583
|
+
this.#arm();
|
|
1043
1584
|
this.#emitter.emit("start", this.id);
|
|
1044
1585
|
this.#escalate();
|
|
1045
1586
|
}
|
|
1046
1587
|
complete(value) {
|
|
1588
|
+
let owned;
|
|
1589
|
+
try {
|
|
1590
|
+
owned = (0, _orkestrel_contract.cloneJSONValue)(value);
|
|
1591
|
+
} catch (error) {
|
|
1592
|
+
if ((0, _orkestrel_contract.isContractError)(error)) throw new WorkflowError("RESTORE", `task '${this.id}' result could not be read safely: ${error.message}`, { task: this.id });
|
|
1593
|
+
throw new WorkflowError("RESTORE", `task '${this.id}' result could not be read safely`, { task: this.id });
|
|
1594
|
+
}
|
|
1047
1595
|
this.#transition("completed");
|
|
1048
|
-
|
|
1596
|
+
this.#finish();
|
|
1597
|
+
const result = this.#record("completed", Object.freeze({
|
|
1049
1598
|
success: true,
|
|
1050
|
-
value
|
|
1051
|
-
});
|
|
1599
|
+
value: owned
|
|
1600
|
+
}));
|
|
1052
1601
|
this.#emitter.emit("complete", result);
|
|
1053
1602
|
this.#escalate();
|
|
1054
1603
|
}
|
|
1055
1604
|
fail(error) {
|
|
1605
|
+
const origin = error.origin === "handler" || error.origin === "timeout" || error.origin === "recovery" ? error.origin : "handler";
|
|
1606
|
+
const message = typeof error.message === "string" && error.message.length > 0 ? error.message : "unknown failure";
|
|
1056
1607
|
this.#transition("failed");
|
|
1057
|
-
|
|
1058
|
-
const result = this.#record("failed", {
|
|
1608
|
+
this.#finish();
|
|
1609
|
+
const result = this.#record("failed", Object.freeze({
|
|
1059
1610
|
success: false,
|
|
1060
|
-
error:
|
|
1061
|
-
|
|
1611
|
+
error: Object.freeze({
|
|
1612
|
+
origin,
|
|
1613
|
+
message
|
|
1614
|
+
})
|
|
1615
|
+
}));
|
|
1062
1616
|
this.#emitter.emit("fail", result);
|
|
1063
1617
|
this.#escalate();
|
|
1064
1618
|
}
|
|
1065
1619
|
skip() {
|
|
1066
1620
|
this.#transition("skipped");
|
|
1621
|
+
this.#finish();
|
|
1622
|
+
this.#abort.abort();
|
|
1067
1623
|
this.#emitter.emit("skip");
|
|
1068
1624
|
this.#escalate();
|
|
1069
1625
|
}
|
|
1070
1626
|
stop() {
|
|
1071
1627
|
this.#transition("stopped");
|
|
1628
|
+
this.#finish();
|
|
1629
|
+
this.#abort.abort();
|
|
1072
1630
|
this.#emitter.emit("stop");
|
|
1073
1631
|
this.#escalate();
|
|
1074
1632
|
}
|
|
1633
|
+
report(input) {
|
|
1634
|
+
if (this.#status !== "running") return failure(new WorkflowError("TRANSITION", `task '${this.id}' cannot report while '${this.#status}'`, {
|
|
1635
|
+
task: this.id,
|
|
1636
|
+
status: this.#status
|
|
1637
|
+
}));
|
|
1638
|
+
try {
|
|
1639
|
+
const activity = cloneTaskActivity(input, this.#stamp());
|
|
1640
|
+
this.#activity = activity;
|
|
1641
|
+
this.#arm();
|
|
1642
|
+
this.#emitter.emit("report", activity);
|
|
1643
|
+
return success(activity);
|
|
1644
|
+
} catch (error) {
|
|
1645
|
+
return failure(error instanceof WorkflowError ? error : new WorkflowError("MUTATION", "task activity report was refused", { task: this.id }));
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
pulse() {
|
|
1649
|
+
if (this.#status !== "running" || this.#activity === void 0) return false;
|
|
1650
|
+
this.#touch();
|
|
1651
|
+
this.#arm();
|
|
1652
|
+
const activity = this.#activity;
|
|
1653
|
+
this.#emitter.emit("pulse", activity);
|
|
1654
|
+
return true;
|
|
1655
|
+
}
|
|
1656
|
+
pause() {
|
|
1657
|
+
if (this.#paused || this.#status !== "pending" && this.#status !== "running") return;
|
|
1658
|
+
this.#paused = true;
|
|
1659
|
+
this.#gate = createDeferred();
|
|
1660
|
+
this.#emitter.emit("pause");
|
|
1661
|
+
}
|
|
1662
|
+
resume() {
|
|
1663
|
+
if (!this.#paused) return;
|
|
1664
|
+
this.#paused = false;
|
|
1665
|
+
this.#release();
|
|
1666
|
+
this.#emitter.emit("resume");
|
|
1667
|
+
}
|
|
1668
|
+
wait() {
|
|
1669
|
+
return this.#paused && this.#gate !== void 0 ? this.#gate.promise : Promise.resolve();
|
|
1670
|
+
}
|
|
1075
1671
|
/**
|
|
1076
1672
|
* Apply a validated declarative patch to SELF (`name` / `description`).
|
|
1077
1673
|
*
|
|
@@ -1106,9 +1702,11 @@ var Task = class {
|
|
|
1106
1702
|
status: this.#status,
|
|
1107
1703
|
...this.#result === void 0 ? {} : { result: this.#result },
|
|
1108
1704
|
metadata: this.#metadata,
|
|
1705
|
+
attempts: this.#attempts,
|
|
1109
1706
|
...this.#run === void 0 ? {} : { run: this.#run },
|
|
1110
1707
|
...this.#retries === void 0 ? {} : { retries: this.#retries },
|
|
1111
|
-
...this.#timeout === void 0 ? {} : { timeout: this.#timeout }
|
|
1708
|
+
...this.#timeout === void 0 ? {} : { timeout: this.#timeout },
|
|
1709
|
+
...this.#activity === void 0 ? {} : { activity: this.#activity }
|
|
1112
1710
|
};
|
|
1113
1711
|
}
|
|
1114
1712
|
#transition(to) {
|
|
@@ -1128,12 +1726,53 @@ var Task = class {
|
|
|
1128
1726
|
...result === void 0 ? {} : { result },
|
|
1129
1727
|
timestamp: Date.now()
|
|
1130
1728
|
};
|
|
1131
|
-
|
|
1132
|
-
|
|
1729
|
+
const frozen = Object.freeze(record);
|
|
1730
|
+
this.#result = frozen;
|
|
1731
|
+
return frozen;
|
|
1133
1732
|
}
|
|
1134
1733
|
#escalate() {
|
|
1135
1734
|
this.#recompute();
|
|
1136
1735
|
}
|
|
1736
|
+
#touch() {
|
|
1737
|
+
if (this.#activity === void 0) return;
|
|
1738
|
+
this.#activity = Object.freeze({
|
|
1739
|
+
...this.#activity,
|
|
1740
|
+
updated: this.#stamp()
|
|
1741
|
+
});
|
|
1742
|
+
}
|
|
1743
|
+
#stamp() {
|
|
1744
|
+
return Math.max(Date.now(), this.#activity?.updated ?? 0);
|
|
1745
|
+
}
|
|
1746
|
+
#finish() {
|
|
1747
|
+
this.#clear();
|
|
1748
|
+
this.#paused = false;
|
|
1749
|
+
this.#release();
|
|
1750
|
+
}
|
|
1751
|
+
#arm() {
|
|
1752
|
+
this.#clear();
|
|
1753
|
+
const liveness = this.#liveness;
|
|
1754
|
+
if (liveness === void 0 || this.#status !== "running") return;
|
|
1755
|
+
liveness.start();
|
|
1756
|
+
const signal = liveness.signal;
|
|
1757
|
+
this.#timerSignal = signal;
|
|
1758
|
+
signal.addEventListener("abort", this.#onSilence, { once: true });
|
|
1759
|
+
}
|
|
1760
|
+
#clear() {
|
|
1761
|
+
const signal = this.#timerSignal;
|
|
1762
|
+
if (signal !== void 0) signal.removeEventListener("abort", this.#onSilence);
|
|
1763
|
+
this.#timerSignal = void 0;
|
|
1764
|
+
this.#liveness?.clear();
|
|
1765
|
+
}
|
|
1766
|
+
#expire() {
|
|
1767
|
+
this.#timerSignal = void 0;
|
|
1768
|
+
if (this.#status !== "running" || this.#liveness?.expired !== true) return;
|
|
1769
|
+
this.#emitter.emit("silence");
|
|
1770
|
+
}
|
|
1771
|
+
#release() {
|
|
1772
|
+
if (this.#gate === void 0) return;
|
|
1773
|
+
this.#gate.resolve();
|
|
1774
|
+
this.#gate = void 0;
|
|
1775
|
+
}
|
|
1137
1776
|
};
|
|
1138
1777
|
//#endregion
|
|
1139
1778
|
//#region src/core/tasks/TaskManager.ts
|
|
@@ -1223,9 +1862,9 @@ var TaskManager = class {
|
|
|
1223
1862
|
*
|
|
1224
1863
|
* @remarks
|
|
1225
1864
|
* - **Derived status.** `status` is `#override` when one is in force, else
|
|
1226
|
-
* {@link derivePhaseStatus} over the live tasks' statuses.
|
|
1865
|
+
* {@link derivePhaseStatus} over the live tasks' statuses. `#recompute` (passed to
|
|
1227
1866
|
* each child {@link Task}) re-derives on every child transition; a CHANGE emits the matching
|
|
1228
|
-
* event AND escalates to the workflow (
|
|
1867
|
+
* event AND escalates to the workflow (`#escalate`, the upward step of the cascade).
|
|
1229
1868
|
* - **Override (AGENTS §10).** `skip` / `stop` FORCE the phase's status (e.g. skipping a whole
|
|
1230
1869
|
* phase), overriding the derived value; the override is PERSISTED in the snapshot's own
|
|
1231
1870
|
* `override` field and restored DIRECTLY (no divergence guess), so a forced phase round-trips.
|
|
@@ -1234,9 +1873,11 @@ var TaskManager = class {
|
|
|
1234
1873
|
* `results()` collects the settled tasks' {@link TaskResult}s (the phase tier of the result
|
|
1235
1874
|
* tree); `workflow` navigates UP to the live parent.
|
|
1236
1875
|
* - **Observable (AGENTS §13).** The owned {@link emitter} ({@link PhaseEventMap}) fires
|
|
1237
|
-
* `start` / `complete` / `fail` / `
|
|
1238
|
-
*
|
|
1239
|
-
*
|
|
1876
|
+
* `start` / `complete` / `fail` / `pause` / `resume` / `skip` / `stop` after the
|
|
1877
|
+
* corresponding status or runtime-gate change. Status events fire after the phase recomputes
|
|
1878
|
+
* and before it escalates to the workflow, preserving child/phase cause before parent effect.
|
|
1879
|
+
* The emitter isolates a listener throw and routes it to its `error` handler (the `error`
|
|
1880
|
+
* option); `fail` carries the failing task's {@link TaskResult}.
|
|
1240
1881
|
* - **Structural API (AGENTS §7).** `add` / `remove` / `move` / `update` gate BEFORE
|
|
1241
1882
|
* delegating to {@link tasks} (the manager gates the target's own existence/status/id/
|
|
1242
1883
|
* bounds), then emit the matching {@link PhaseEventMap} event on success only. NATIVE
|
|
@@ -1254,9 +1895,12 @@ var TaskManager = class {
|
|
|
1254
1895
|
* construction path {@link #append} uses at build time, so a live mint and a restored/built
|
|
1255
1896
|
* task are wired IDENTICALLY. At construction, the workflow-level
|
|
1256
1897
|
* {@link import('../types.js').WorkflowFunctions} registry (threaded from
|
|
1257
|
-
* {@link import('../types.js').WorkflowOptions.functions}) resolves
|
|
1258
|
-
*
|
|
1259
|
-
*
|
|
1898
|
+
* {@link import('../types.js').WorkflowOptions.functions}) resolves every unique initial `run`
|
|
1899
|
+
* name ONCE before any task is built; siblings sharing a name receive the exact same captured
|
|
1900
|
+
* runtime {@link import('../types.js').TaskInterface.handler}. A later live {@link add} reads
|
|
1901
|
+
* that name once from the retained registry at its own mint moment. An omitted or unregistered
|
|
1902
|
+
* `run` resolves to no handler; only omission is a no-op, while an unresolved present name makes
|
|
1903
|
+
* the containing tree non-drivable.
|
|
1260
1904
|
* - **Runtime lifecycle (AGENTS §10).** `pause` / `resume` / `wait` mirror the workflow's own
|
|
1261
1905
|
* quartet, scoped to this phase — a driving
|
|
1262
1906
|
* {@link import('../types.js').WorkflowRunnerInterface.execute} gates a task's own
|
|
@@ -1273,6 +1917,7 @@ var Phase = class {
|
|
|
1273
1917
|
#escalateUp;
|
|
1274
1918
|
#tasks = new TaskManager();
|
|
1275
1919
|
#functions;
|
|
1920
|
+
#silence;
|
|
1276
1921
|
#bail;
|
|
1277
1922
|
#concurrency;
|
|
1278
1923
|
#emitter;
|
|
@@ -1280,7 +1925,10 @@ var Phase = class {
|
|
|
1280
1925
|
#override;
|
|
1281
1926
|
#paused;
|
|
1282
1927
|
#gate;
|
|
1283
|
-
constructor(snapshot, workflow, escalate, options, bail, functions) {
|
|
1928
|
+
constructor(snapshot, workflow, escalate, options, bail, functions, silence) {
|
|
1929
|
+
const on = options?.on;
|
|
1930
|
+
const error = options?.error;
|
|
1931
|
+
const tasks = options?.tasks;
|
|
1284
1932
|
this.#id = snapshot.id;
|
|
1285
1933
|
this.#name = snapshot.name;
|
|
1286
1934
|
if (snapshot.description !== void 0) Object.defineProperty(this, "description", {
|
|
@@ -1290,13 +1938,20 @@ var Phase = class {
|
|
|
1290
1938
|
this.#workflow = workflow;
|
|
1291
1939
|
this.#escalateUp = escalate;
|
|
1292
1940
|
this.#functions = functions;
|
|
1941
|
+
this.#silence = silence;
|
|
1942
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
1943
|
+
for (const task of snapshot.tasks) if (task.run !== void 0 && !handlers.has(task.run)) handlers.set(task.run, functions?.[task.run]);
|
|
1293
1944
|
this.#bail = bail ?? snapshot.bail;
|
|
1294
1945
|
this.#concurrency = snapshot.concurrency;
|
|
1295
1946
|
this.#emitter = new _orkestrel_emitter.Emitter({
|
|
1296
|
-
...
|
|
1297
|
-
...
|
|
1947
|
+
...on === void 0 ? {} : { on },
|
|
1948
|
+
...error === void 0 ? {} : { error }
|
|
1298
1949
|
});
|
|
1299
|
-
for (const task of snapshot.tasks)
|
|
1950
|
+
for (const task of snapshot.tasks) {
|
|
1951
|
+
const taskOptions = tasks?.[task.id];
|
|
1952
|
+
const handler = task.run === void 0 ? void 0 : handlers.get(task.run);
|
|
1953
|
+
this.#append(task, taskOptions, handler);
|
|
1954
|
+
}
|
|
1300
1955
|
this.#override = snapshot.override;
|
|
1301
1956
|
this.#status = this.status;
|
|
1302
1957
|
this.#paused = false;
|
|
@@ -1358,11 +2013,13 @@ var Phase = class {
|
|
|
1358
2013
|
if (this.#paused || isTerminalStatus(this.status)) return;
|
|
1359
2014
|
this.#paused = true;
|
|
1360
2015
|
this.#gate = createDeferred();
|
|
2016
|
+
this.#emitter.emit("pause");
|
|
1361
2017
|
}
|
|
1362
2018
|
resume() {
|
|
1363
2019
|
if (!this.#paused) return;
|
|
1364
2020
|
this.#paused = false;
|
|
1365
2021
|
this.#release();
|
|
2022
|
+
this.#emitter.emit("resume");
|
|
1366
2023
|
}
|
|
1367
2024
|
wait() {
|
|
1368
2025
|
return this.#paused && this.#gate !== void 0 ? this.#gate.promise : Promise.resolve();
|
|
@@ -1443,6 +2100,10 @@ var Phase = class {
|
|
|
1443
2100
|
return;
|
|
1444
2101
|
}
|
|
1445
2102
|
this.#status = next;
|
|
2103
|
+
if (isTerminalStatus(next)) {
|
|
2104
|
+
this.#paused = false;
|
|
2105
|
+
this.#release();
|
|
2106
|
+
}
|
|
1446
2107
|
this.#emitFor(next);
|
|
1447
2108
|
this.#escalateUp();
|
|
1448
2109
|
}
|
|
@@ -1454,6 +2115,7 @@ var Phase = class {
|
|
|
1454
2115
|
if (status === "running") this.#emitter.emit("start", this.id);
|
|
1455
2116
|
else if (status === "completed") this.#emitter.emit("complete");
|
|
1456
2117
|
else if (status === "failed") this.#emitter.emit("fail", this.#failure());
|
|
2118
|
+
else if (status === "skipped") this.#emitter.emit("skip");
|
|
1457
2119
|
else if (status === "stopped") this.#emitter.emit("stop");
|
|
1458
2120
|
}
|
|
1459
2121
|
#failure() {
|
|
@@ -1471,17 +2133,17 @@ var Phase = class {
|
|
|
1471
2133
|
if (result.success) this.#emitter.emit("add", result.value, at);
|
|
1472
2134
|
return result;
|
|
1473
2135
|
}
|
|
1474
|
-
#append(task, options) {
|
|
1475
|
-
const created = this.#create(task, options
|
|
2136
|
+
#append(task, options, handler) {
|
|
2137
|
+
const created = this.#create(task, options, handler);
|
|
1476
2138
|
this.#tasks.append(created);
|
|
1477
2139
|
}
|
|
1478
|
-
#create(snapshot, options) {
|
|
1479
|
-
|
|
1480
|
-
const handler = snapshot.run === void 0 ? void 0 : this.#functions?.[snapshot.run];
|
|
1481
|
-
return new Task(context, this, this.#workflow, () => this.#recompute(), options, snapshot.status, snapshot.result, snapshot.run, snapshot.retries, snapshot.timeout, handler);
|
|
2140
|
+
#create(snapshot, options, handler) {
|
|
2141
|
+
return new Task(buildTaskContext(this.context, snapshot), this, this.#workflow, () => this.#recompute(), options, snapshot.status, snapshot.result, snapshot.run, snapshot.retries, snapshot.timeout, snapshot.metadata, snapshot.attempts, snapshot.activity, handler, this.#silence);
|
|
1482
2142
|
}
|
|
1483
2143
|
#mint(definition) {
|
|
1484
|
-
|
|
2144
|
+
const snapshot = taskDefinitionToSnapshot(definition);
|
|
2145
|
+
const handler = snapshot.run === void 0 ? void 0 : this.#functions?.[snapshot.run];
|
|
2146
|
+
return this.#create(snapshot, void 0, handler);
|
|
1485
2147
|
}
|
|
1486
2148
|
#statuses() {
|
|
1487
2149
|
return this.#tasks.tasks().map((task) => task.status);
|
|
@@ -1576,24 +2238,26 @@ var PhaseManager = class {
|
|
|
1576
2238
|
* - **Construction.** Built from a {@link WorkflowSnapshot} (the unified input —
|
|
1577
2239
|
* {@link import('./factories.js').createWorkflow} seeds an initial snapshot from a
|
|
1578
2240
|
* {@link import('./types.js').WorkflowDefinition}, {@link import('./factories.js').restoreWorkflow}
|
|
1579
|
-
* passes a persisted one). Each child {@link Phase} is wired to escalate to
|
|
2241
|
+
* passes a persisted one). Each child {@link Phase} is wired to escalate to `#recompute`.
|
|
1580
2242
|
* - **Derived status.** `status` is `#override` when forced, else
|
|
1581
2243
|
* {@link deriveWorkflowStatus} over the live phases' statuses feeding `bail`. `failed` is
|
|
1582
2244
|
* reachable ONLY under `bail: true` (a single failed task halts the workflow); under
|
|
1583
|
-
* `bail: false` a failed phase folds into `completed`.
|
|
2245
|
+
* `bail: false` a failed phase folds into `completed`. `#recompute` diffs on each phase
|
|
1584
2246
|
* change; a CHANGE emits.
|
|
1585
|
-
* - **Override (AGENTS §10).** `skip` / `stop` FORCE the status;
|
|
1586
|
-
*
|
|
1587
|
-
*
|
|
2247
|
+
* - **Override (AGENTS §10).** `skip` / `stop` FORCE the status; an executed task-free pending tree
|
|
2248
|
+
* may also be force-completed vacuously. The override is PERSISTED in the snapshot's own
|
|
2249
|
+
* `override` field and restored DIRECTLY (no divergence guess). The snapshot also persists
|
|
2250
|
+
* `bail`, so a restore re-derives status identically without a silent policy default.
|
|
1588
2251
|
* - **Result tree.** `results()` flattens every phase's `results()` ({@link collectResults}) — the
|
|
1589
2252
|
* workflow tier; `phase(id)` + each `phase.task(id)` navigate DOWN, a task's `phase` / `workflow`
|
|
1590
2253
|
* navigate UP.
|
|
1591
2254
|
* - **Snapshot.** `snapshot()` serializes the whole live tree to a {@link WorkflowSnapshot} (pure
|
|
1592
2255
|
* JSON); {@link import('./factories.js').restoreWorkflow} rebuilds an equivalent live tree.
|
|
1593
2256
|
* - **Observable (AGENTS §13).** The owned {@link emitter} ({@link WorkflowEventMap}) fires
|
|
1594
|
-
* `start` / `complete` / `fail` / `
|
|
1595
|
-
*
|
|
1596
|
-
* the failing task's
|
|
2257
|
+
* `start` / `complete` / `fail` / `pause` / `resume` / `skip` / `stop` after the
|
|
2258
|
+
* corresponding status or runtime-gate change; the emitter isolates a listener throw and
|
|
2259
|
+
* routes it to its `error` handler (the `error` option); `fail` carries the failing task's
|
|
2260
|
+
* {@link TaskResult}.
|
|
1597
2261
|
* - **Structural API (AGENTS §7).** `add` / `remove` / `move` / `update` gate BEFORE
|
|
1598
2262
|
* delegating to {@link phases} (the manager gates the target's own existence/status/id/
|
|
1599
2263
|
* bounds), then emit the matching {@link WorkflowEventMap} event on success only. NATIVE,
|
|
@@ -1605,16 +2269,17 @@ var PhaseManager = class {
|
|
|
1605
2269
|
* naturally accepted.
|
|
1606
2270
|
* - **Runtime lifecycle (AGENTS §10).** `pause` / `resume` / `wait` gate execution at the runner's
|
|
1607
2271
|
* phase/task boundaries WITHOUT touching {@link status} — `paused` is runtime-only, never
|
|
1608
|
-
* persisted. `destroy` is a terminal teardown: it
|
|
1609
|
-
*
|
|
1610
|
-
*
|
|
1611
|
-
*
|
|
2272
|
+
* persisted. `destroy` is a terminal teardown: it `stop`s every non-terminal task and
|
|
2273
|
+
* phase (releasing their gates and liveness resources), aborts {@link signal}, forces the
|
|
2274
|
+
* workflow `stop` override when needed, releases its parked waiter, and marks
|
|
2275
|
+
* {@link destroyed} — all idempotent.
|
|
1612
2276
|
*/
|
|
1613
2277
|
var Workflow = class {
|
|
1614
2278
|
#context;
|
|
1615
2279
|
#bail;
|
|
1616
2280
|
#bailOverride;
|
|
1617
2281
|
#functions;
|
|
2282
|
+
#silence;
|
|
1618
2283
|
#phases = new PhaseManager();
|
|
1619
2284
|
#emitter;
|
|
1620
2285
|
#created;
|
|
@@ -1626,14 +2291,22 @@ var Workflow = class {
|
|
|
1626
2291
|
#gate;
|
|
1627
2292
|
#destroyed;
|
|
1628
2293
|
constructor(snapshot, options) {
|
|
2294
|
+
const captured = captureWorkflowOptions(options);
|
|
2295
|
+
const on = captured.on;
|
|
2296
|
+
const bail = captured.bail;
|
|
2297
|
+
const error = captured.error;
|
|
2298
|
+
const phases = captured.phases;
|
|
2299
|
+
const functions = captured.functions;
|
|
2300
|
+
const silence = captured.silence;
|
|
1629
2301
|
this.#context = buildWorkflowContext(snapshot);
|
|
1630
2302
|
if (snapshot.description !== void 0) Object.defineProperty(this, "description", { value: snapshot.description });
|
|
1631
|
-
this.#bail =
|
|
1632
|
-
this.#bailOverride =
|
|
1633
|
-
this.#functions =
|
|
2303
|
+
this.#bail = bail ?? snapshot.bail;
|
|
2304
|
+
this.#bailOverride = bail;
|
|
2305
|
+
this.#functions = functions;
|
|
2306
|
+
this.#silence = silence;
|
|
1634
2307
|
this.#emitter = new _orkestrel_emitter.Emitter({
|
|
1635
|
-
...
|
|
1636
|
-
...
|
|
2308
|
+
...on === void 0 ? {} : { on },
|
|
2309
|
+
...error === void 0 ? {} : { error }
|
|
1637
2310
|
});
|
|
1638
2311
|
this.#created = snapshot.created;
|
|
1639
2312
|
this.#updated = snapshot.updated;
|
|
@@ -1641,7 +2314,10 @@ var Workflow = class {
|
|
|
1641
2314
|
this.#paused = false;
|
|
1642
2315
|
this.#gate = void 0;
|
|
1643
2316
|
this.#destroyed = false;
|
|
1644
|
-
for (const phase of snapshot.phases)
|
|
2317
|
+
for (const phase of snapshot.phases) {
|
|
2318
|
+
const phaseOptions = phases?.[phase.id];
|
|
2319
|
+
this.#append(phase, phaseOptions);
|
|
2320
|
+
}
|
|
1645
2321
|
this.#override = snapshot.override;
|
|
1646
2322
|
this.#status = this.status;
|
|
1647
2323
|
}
|
|
@@ -1692,26 +2368,35 @@ var Workflow = class {
|
|
|
1692
2368
|
this.#release();
|
|
1693
2369
|
}
|
|
1694
2370
|
complete() {
|
|
1695
|
-
if (this.status === "pending") this.#force("completed");
|
|
2371
|
+
if (this.status === "pending" && this.#phases.phases().every((phase) => phase.tasks.count === 0)) this.#force("completed");
|
|
1696
2372
|
}
|
|
1697
2373
|
pause() {
|
|
1698
2374
|
if (this.#paused || isTerminalStatus(this.status) || this.#destroyed) return;
|
|
1699
2375
|
this.#paused = true;
|
|
1700
2376
|
this.#gate = createDeferred();
|
|
2377
|
+
this.#emitter.emit("pause");
|
|
1701
2378
|
}
|
|
1702
2379
|
resume() {
|
|
1703
2380
|
if (!this.#paused) return;
|
|
1704
2381
|
this.#paused = false;
|
|
1705
2382
|
this.#release();
|
|
2383
|
+
this.#emitter.emit("resume");
|
|
1706
2384
|
}
|
|
1707
2385
|
destroy() {
|
|
1708
2386
|
if (this.#destroyed) return;
|
|
1709
2387
|
this.#destroyed = true;
|
|
1710
|
-
this.#
|
|
1711
|
-
for (const phase of this.#phases.phases()) if (!isTerminalStatus(phase.status)) phase.stop();
|
|
2388
|
+
const phases = this.#phases.phases();
|
|
1712
2389
|
if (!isTerminalStatus(this.status)) this.stop();
|
|
2390
|
+
for (const phase of phases) phase.stop();
|
|
2391
|
+
for (const phase of phases) for (const task of phase.tasks.tasks()) if (!isTerminalStatus(task.status)) task.stop();
|
|
1713
2392
|
this.#paused = false;
|
|
1714
2393
|
this.#release();
|
|
2394
|
+
this.#abort.abort();
|
|
2395
|
+
for (const phase of phases) {
|
|
2396
|
+
for (const task of phase.tasks.tasks()) task.emitter.destroy();
|
|
2397
|
+
phase.emitter.destroy();
|
|
2398
|
+
}
|
|
2399
|
+
this.#emitter.destroy();
|
|
1715
2400
|
}
|
|
1716
2401
|
wait() {
|
|
1717
2402
|
return this.#paused && this.#gate !== void 0 ? this.#gate.promise : Promise.resolve();
|
|
@@ -1773,7 +2458,7 @@ var Workflow = class {
|
|
|
1773
2458
|
return result;
|
|
1774
2459
|
}
|
|
1775
2460
|
snapshot() {
|
|
1776
|
-
return {
|
|
2461
|
+
return cloneWorkflowSnapshot({
|
|
1777
2462
|
id: this.id,
|
|
1778
2463
|
name: this.name,
|
|
1779
2464
|
...this.description === void 0 ? {} : { description: this.description },
|
|
@@ -1783,13 +2468,17 @@ var Workflow = class {
|
|
|
1783
2468
|
phases: this.#phases.phases().map((phase) => phase.snapshot()),
|
|
1784
2469
|
created: this.#created,
|
|
1785
2470
|
updated: this.#updated
|
|
1786
|
-
};
|
|
2471
|
+
});
|
|
1787
2472
|
}
|
|
1788
2473
|
#recompute() {
|
|
1789
2474
|
const next = this.status;
|
|
1790
2475
|
if (next === this.#status) return;
|
|
1791
2476
|
this.#status = next;
|
|
1792
|
-
|
|
2477
|
+
if (isTerminalStatus(next)) {
|
|
2478
|
+
this.#paused = false;
|
|
2479
|
+
this.#release();
|
|
2480
|
+
}
|
|
2481
|
+
this.#updated = Math.max(Date.now(), this.#updated);
|
|
1793
2482
|
this.#emitFor(next);
|
|
1794
2483
|
}
|
|
1795
2484
|
#force(status) {
|
|
@@ -1800,6 +2489,7 @@ var Workflow = class {
|
|
|
1800
2489
|
if (status === "running") this.#emitter.emit("start", this.id);
|
|
1801
2490
|
else if (status === "completed") this.#emitter.emit("complete");
|
|
1802
2491
|
else if (status === "failed") this.#emitter.emit("fail", this.#failure());
|
|
2492
|
+
else if (status === "skipped") this.#emitter.emit("skip");
|
|
1803
2493
|
else if (status === "stopped") this.#emitter.emit("stop");
|
|
1804
2494
|
}
|
|
1805
2495
|
#addTo(phase, index, at) {
|
|
@@ -1819,11 +2509,11 @@ var Workflow = class {
|
|
|
1819
2509
|
return found;
|
|
1820
2510
|
}
|
|
1821
2511
|
#append(phase, options) {
|
|
1822
|
-
const created = new Phase(phase, this, () => this.#recompute(), options
|
|
2512
|
+
const created = new Phase(phase, this, () => this.#recompute(), options, this.#bailOverride, this.#functions, this.#silence);
|
|
1823
2513
|
this.#phases.append(created);
|
|
1824
2514
|
}
|
|
1825
2515
|
#mint(definition) {
|
|
1826
|
-
return new Phase(phaseDefinitionToSnapshot(definition, this.#bail), this, () => this.#recompute(), void 0, this.#bailOverride, this.#functions);
|
|
2516
|
+
return new Phase(phaseDefinitionToSnapshot(definition, this.#bail), this, () => this.#recompute(), void 0, this.#bailOverride, this.#functions, this.#silence);
|
|
1827
2517
|
}
|
|
1828
2518
|
#release() {
|
|
1829
2519
|
if (this.#gate === void 0) return;
|
|
@@ -1851,12 +2541,11 @@ var Workflow = class {
|
|
|
1851
2541
|
* `functions` registry in) and stores it under `definition.id` — an already-present id
|
|
1852
2542
|
* OVERWRITES (last write wins). `count` is the map size, `workflow(id)` looks one up,
|
|
1853
2543
|
* `workflows()` lists them in insertion order.
|
|
1854
|
-
* - **Durable open / save.** `open(id)` returns an already-registered workflow directly;
|
|
1855
|
-
*
|
|
1856
|
-
*
|
|
1857
|
-
*
|
|
1858
|
-
*
|
|
1859
|
-
* unknown id.
|
|
2544
|
+
* - **Durable open / save.** `open(id)` returns an already-registered workflow directly; same-id
|
|
2545
|
+
* misses share one hydration. A concurrent `add` wins, while `remove` / `clear` invalidate
|
|
2546
|
+
* earlier reads; wrong-key payloads reject with `RESTORE`. `save(id)` captures a registered
|
|
2547
|
+
* workflow's snapshot at invocation and serializes same-id writes without coupling other ids.
|
|
2548
|
+
* Both remain lenient without a store or registered id.
|
|
1860
2549
|
* - **Removal.** `remove` drops one by id, or a batch (§9.2, array overload FIRST) — `true` when
|
|
1861
2550
|
* any was removed. `clear` empties the registry.
|
|
1862
2551
|
* - **No active pointer.** Unlike its `ConversationManager` / `WorkspaceManager` twins, there is
|
|
@@ -1874,6 +2563,12 @@ var Workflow = class {
|
|
|
1874
2563
|
*/
|
|
1875
2564
|
var WorkflowManager = class {
|
|
1876
2565
|
#workflows = /* @__PURE__ */ new Map();
|
|
2566
|
+
#opens = /* @__PURE__ */ new Map();
|
|
2567
|
+
#saves = /* @__PURE__ */ new Map();
|
|
2568
|
+
#mutations = /* @__PURE__ */ new Map();
|
|
2569
|
+
#additions = /* @__PURE__ */ new Map();
|
|
2570
|
+
#hydrations = /* @__PURE__ */ new Map();
|
|
2571
|
+
#generation = Symbol();
|
|
1877
2572
|
#functions;
|
|
1878
2573
|
#store;
|
|
1879
2574
|
constructor(options) {
|
|
@@ -1891,36 +2586,140 @@ var WorkflowManager = class {
|
|
|
1891
2586
|
}
|
|
1892
2587
|
add(definition) {
|
|
1893
2588
|
const workflow = createWorkflow(definition, { ...this.#functions === void 0 ? {} : { functions: this.#functions } });
|
|
2589
|
+
const mutation = this.#invalidate(workflow.id);
|
|
2590
|
+
if (mutation === void 0) this.#additions.delete(workflow.id);
|
|
2591
|
+
else this.#additions.set(workflow.id, mutation);
|
|
1894
2592
|
this.#workflows.set(workflow.id, workflow);
|
|
1895
2593
|
return workflow;
|
|
1896
2594
|
}
|
|
1897
|
-
|
|
2595
|
+
open(id) {
|
|
1898
2596
|
const existing = this.#workflows.get(id);
|
|
1899
|
-
if (existing !== void 0) return existing;
|
|
1900
|
-
if (this.#store === void 0) return void 0;
|
|
1901
|
-
const
|
|
1902
|
-
if (
|
|
1903
|
-
const
|
|
1904
|
-
this.#
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
2597
|
+
if (existing !== void 0) return Promise.resolve(existing);
|
|
2598
|
+
if (this.#store === void 0) return Promise.resolve(void 0);
|
|
2599
|
+
const pending = this.#opens.get(id);
|
|
2600
|
+
if (pending !== void 0) return pending;
|
|
2601
|
+
const mutation = this.#mutations.get(id);
|
|
2602
|
+
const generation = this.#generation;
|
|
2603
|
+
const lease = this.#retain(id);
|
|
2604
|
+
const reservation = Promise.withResolvers();
|
|
2605
|
+
const opening = reservation.promise;
|
|
2606
|
+
this.#opens.set(id, opening);
|
|
2607
|
+
this.#hydrate(id, mutation, generation, lease, this.#store).then(reservation.resolve, reservation.reject);
|
|
2608
|
+
opening.then(() => this.#releaseOpen(id, opening), () => this.#releaseOpen(id, opening));
|
|
2609
|
+
return opening;
|
|
2610
|
+
}
|
|
2611
|
+
save(id) {
|
|
1908
2612
|
const workflow = this.#workflows.get(id);
|
|
1909
|
-
if (this.#store === void 0 || workflow === void 0) return false;
|
|
1910
|
-
|
|
1911
|
-
|
|
2613
|
+
if (this.#store === void 0 || workflow === void 0) return Promise.resolve(false);
|
|
2614
|
+
const snapshot = workflow.snapshot();
|
|
2615
|
+
const previous = this.#saves.get(id);
|
|
2616
|
+
const reservation = Promise.withResolvers();
|
|
2617
|
+
const saving = reservation.promise;
|
|
2618
|
+
this.#saves.set(id, saving);
|
|
2619
|
+
this.#persist(this.#store, previous, snapshot).then(reservation.resolve, reservation.reject);
|
|
2620
|
+
saving.then(() => this.#settle(id, saving), () => this.#settle(id, saving));
|
|
2621
|
+
return saving.then(() => true);
|
|
1912
2622
|
}
|
|
1913
2623
|
remove(ids) {
|
|
1914
2624
|
if ((0, _orkestrel_contract.isArray)(ids)) {
|
|
1915
2625
|
let removed = false;
|
|
1916
|
-
for (const id of ids)
|
|
2626
|
+
for (const id of ids) {
|
|
2627
|
+
this.#invalidate(id);
|
|
2628
|
+
this.#additions.delete(id);
|
|
2629
|
+
if (this.#workflows.delete(id)) removed = true;
|
|
2630
|
+
}
|
|
1917
2631
|
return removed;
|
|
1918
2632
|
}
|
|
2633
|
+
this.#invalidate(ids);
|
|
2634
|
+
this.#additions.delete(ids);
|
|
1919
2635
|
return this.#workflows.delete(ids);
|
|
1920
2636
|
}
|
|
1921
2637
|
clear() {
|
|
2638
|
+
this.#generation = Symbol();
|
|
2639
|
+
this.#mutations.clear();
|
|
2640
|
+
this.#additions.clear();
|
|
2641
|
+
this.#opens.clear();
|
|
1922
2642
|
this.#workflows.clear();
|
|
1923
2643
|
}
|
|
2644
|
+
async #hydrate(id, mutation, generation, lease, store) {
|
|
2645
|
+
try {
|
|
2646
|
+
const snapshot = await store.get(id);
|
|
2647
|
+
if (!this.#owns(id, mutation, generation)) return this.#resolve(id, generation);
|
|
2648
|
+
if (snapshot === void 0) return void 0;
|
|
2649
|
+
let owned;
|
|
2650
|
+
try {
|
|
2651
|
+
owned = cloneWorkflowSnapshot(snapshot, id);
|
|
2652
|
+
} catch (error) {
|
|
2653
|
+
if (!this.#owns(id, mutation, generation)) return this.#resolve(id, generation);
|
|
2654
|
+
throw error;
|
|
2655
|
+
}
|
|
2656
|
+
if (!this.#owns(id, mutation, generation)) return this.#resolve(id, generation);
|
|
2657
|
+
let workflow;
|
|
2658
|
+
try {
|
|
2659
|
+
workflow = restoreWorkflow(owned, { ...this.#functions === void 0 ? {} : { functions: this.#functions } });
|
|
2660
|
+
} catch (error) {
|
|
2661
|
+
if (!this.#owns(id, mutation, generation)) return this.#resolve(id, generation);
|
|
2662
|
+
throw error;
|
|
2663
|
+
}
|
|
2664
|
+
if (!this.#owns(id, mutation, generation)) return this.#resolve(id, generation);
|
|
2665
|
+
return this.#register(id, workflow, mutation, generation);
|
|
2666
|
+
} finally {
|
|
2667
|
+
this.#releaseHydration(id, lease);
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
#owns(id, mutation, generation) {
|
|
2671
|
+
return this.#generation === generation && this.#mutations.get(id) === mutation;
|
|
2672
|
+
}
|
|
2673
|
+
#resolve(id, generation) {
|
|
2674
|
+
if (this.#generation !== generation) return void 0;
|
|
2675
|
+
const mutation = this.#mutations.get(id);
|
|
2676
|
+
const workflow = this.#workflows.get(id);
|
|
2677
|
+
return workflow !== void 0 && this.#additions.get(id) === mutation ? workflow : void 0;
|
|
2678
|
+
}
|
|
2679
|
+
#register(id, workflow, mutation, generation) {
|
|
2680
|
+
if (!this.#owns(id, mutation, generation)) return this.#resolve(id, generation);
|
|
2681
|
+
this.#workflows.set(id, workflow);
|
|
2682
|
+
return workflow;
|
|
2683
|
+
}
|
|
2684
|
+
async #persist(store, previous, snapshot) {
|
|
2685
|
+
if (previous !== void 0) try {
|
|
2686
|
+
await previous;
|
|
2687
|
+
} catch {}
|
|
2688
|
+
await store.set(snapshot);
|
|
2689
|
+
}
|
|
2690
|
+
#invalidate(id) {
|
|
2691
|
+
this.#opens.delete(id);
|
|
2692
|
+
if (!this.#hydrations.has(id)) {
|
|
2693
|
+
this.#mutations.delete(id);
|
|
2694
|
+
this.#additions.delete(id);
|
|
2695
|
+
return;
|
|
2696
|
+
}
|
|
2697
|
+
const mutation = Symbol();
|
|
2698
|
+
this.#mutations.set(id, mutation);
|
|
2699
|
+
return mutation;
|
|
2700
|
+
}
|
|
2701
|
+
#retain(id) {
|
|
2702
|
+
const lease = Symbol();
|
|
2703
|
+
const hydrations = this.#hydrations.get(id);
|
|
2704
|
+
if (hydrations === void 0) this.#hydrations.set(id, /* @__PURE__ */ new Set([lease]));
|
|
2705
|
+
else hydrations.add(lease);
|
|
2706
|
+
return lease;
|
|
2707
|
+
}
|
|
2708
|
+
#releaseOpen(id, opening) {
|
|
2709
|
+
if (this.#opens.get(id) === opening) this.#opens.delete(id);
|
|
2710
|
+
}
|
|
2711
|
+
#releaseHydration(id, lease) {
|
|
2712
|
+
const hydrations = this.#hydrations.get(id);
|
|
2713
|
+
if (hydrations === void 0) return;
|
|
2714
|
+
hydrations.delete(lease);
|
|
2715
|
+
if (hydrations.size !== 0) return;
|
|
2716
|
+
this.#hydrations.delete(id);
|
|
2717
|
+
this.#mutations.delete(id);
|
|
2718
|
+
this.#additions.delete(id);
|
|
2719
|
+
}
|
|
2720
|
+
#settle(id, saving) {
|
|
2721
|
+
if (this.#saves.get(id) === saving) this.#saves.delete(id);
|
|
2722
|
+
}
|
|
1924
2723
|
};
|
|
1925
2724
|
//#endregion
|
|
1926
2725
|
//#region src/core/Controller.ts
|
|
@@ -2039,6 +2838,7 @@ var Runner = class {
|
|
|
2039
2838
|
#order = [];
|
|
2040
2839
|
#values = /* @__PURE__ */ new Map();
|
|
2041
2840
|
#dispatched = /* @__PURE__ */ new Set();
|
|
2841
|
+
#queued = /* @__PURE__ */ new Set();
|
|
2042
2842
|
#count = 0;
|
|
2043
2843
|
#drained;
|
|
2044
2844
|
#started = false;
|
|
@@ -2046,18 +2846,28 @@ var Runner = class {
|
|
|
2046
2846
|
#stopped = false;
|
|
2047
2847
|
#stopping = false;
|
|
2048
2848
|
#failure;
|
|
2849
|
+
#stopPromise;
|
|
2850
|
+
#abortPromise;
|
|
2851
|
+
#destroyPromise;
|
|
2049
2852
|
constructor(options) {
|
|
2050
|
-
|
|
2051
|
-
|
|
2853
|
+
const handler = options.handler;
|
|
2854
|
+
const entries = options.entries;
|
|
2855
|
+
const on = options.on;
|
|
2856
|
+
const error = options.error;
|
|
2857
|
+
const concurrency = options.concurrency;
|
|
2858
|
+
const retries = options.retries;
|
|
2859
|
+
const timeout = options.timeout;
|
|
2860
|
+
this.#handler = handler;
|
|
2861
|
+
this.#entries = entries;
|
|
2052
2862
|
this.#emitter = new _orkestrel_emitter.Emitter({
|
|
2053
|
-
...
|
|
2054
|
-
...
|
|
2863
|
+
...on === void 0 ? {} : { on },
|
|
2864
|
+
...error === void 0 ? {} : { error }
|
|
2055
2865
|
});
|
|
2056
2866
|
this.#queue = (0, _orkestrel_queue.createQueue)({
|
|
2057
2867
|
handler: this.#dispatch.bind(this),
|
|
2058
|
-
...
|
|
2059
|
-
...
|
|
2060
|
-
...
|
|
2868
|
+
...concurrency === void 0 ? {} : { concurrency },
|
|
2869
|
+
...retries === void 0 ? {} : { retries },
|
|
2870
|
+
...timeout === void 0 ? {} : { timeout }
|
|
2061
2871
|
});
|
|
2062
2872
|
}
|
|
2063
2873
|
get emitter() {
|
|
@@ -2099,7 +2909,7 @@ var Runner = class {
|
|
|
2099
2909
|
* ```
|
|
2100
2910
|
*/
|
|
2101
2911
|
spawn(input) {
|
|
2102
|
-
if (
|
|
2912
|
+
if (!this.#accepts()) return void 0;
|
|
2103
2913
|
return this.#launch(input, void 0, true);
|
|
2104
2914
|
}
|
|
2105
2915
|
async execute(inputs) {
|
|
@@ -2107,29 +2917,37 @@ var Runner = class {
|
|
|
2107
2917
|
if (this.#stopped) throw new Error("runner is stopped");
|
|
2108
2918
|
this.#started = true;
|
|
2109
2919
|
this.#running = true;
|
|
2110
|
-
this.#emitter.emit("start");
|
|
2111
|
-
if (inputs.length === 0) {
|
|
2112
|
-
this.#running = false;
|
|
2113
|
-
this.#emitter.emit("finish", []);
|
|
2114
|
-
return [];
|
|
2115
|
-
}
|
|
2116
2920
|
const drained = createDeferred();
|
|
2117
2921
|
this.#drained = drained;
|
|
2118
|
-
for (const input of inputs)
|
|
2922
|
+
for (const input of inputs) {
|
|
2923
|
+
if (!this.#accepts()) break;
|
|
2924
|
+
this.#launch(input);
|
|
2925
|
+
}
|
|
2926
|
+
this.#emitter.emit("start");
|
|
2927
|
+
if (this.#count === 0) drained.resolve();
|
|
2119
2928
|
await drained.promise;
|
|
2120
2929
|
this.#running = false;
|
|
2930
|
+
const cleanup = await this.#cleanup();
|
|
2121
2931
|
if (this.#failure !== void 0) throw this.#failure.error;
|
|
2932
|
+
if (cleanup !== void 0) throw cleanup.error;
|
|
2122
2933
|
const results = this.#collect();
|
|
2123
2934
|
this.#emitter.emit("finish", results);
|
|
2935
|
+
const finishing = await this.#cleanup();
|
|
2936
|
+
if (finishing !== void 0) throw finishing.error;
|
|
2124
2937
|
return results;
|
|
2125
2938
|
}
|
|
2126
2939
|
abort(reason) {
|
|
2127
|
-
if (this.#
|
|
2940
|
+
if (this.#abortPromise !== void 0) return this.#abortPromise;
|
|
2941
|
+
const barrier = createDeferred();
|
|
2942
|
+
this.#abortPromise = barrier.promise;
|
|
2943
|
+
barrier.promise.catch(() => {});
|
|
2128
2944
|
if (this.#running && this.#failure === void 0) this.#failure = { error: reason === void 0 ? /* @__PURE__ */ new Error("runner aborted") : reason };
|
|
2129
2945
|
this.#cancel(reason);
|
|
2130
|
-
this.#queue.abort(reason);
|
|
2131
2946
|
this.#stopped = true;
|
|
2947
|
+
const cleanup = this.#queue.abort(reason);
|
|
2948
|
+
this.#settleLifecycle(barrier, cleanup);
|
|
2132
2949
|
this.#emitter.emit("abort", reason);
|
|
2950
|
+
return barrier.promise;
|
|
2133
2951
|
}
|
|
2134
2952
|
/**
|
|
2135
2953
|
* Suspend dispatch (AGENTS §10 — resumable): delegates to the backing queue's own
|
|
@@ -2167,18 +2985,28 @@ var Runner = class {
|
|
|
2167
2985
|
* dispatched unit's rejection while stopping is still a real failure. Idempotent.
|
|
2168
2986
|
*/
|
|
2169
2987
|
stop() {
|
|
2170
|
-
if (this.#
|
|
2988
|
+
if (this.#destroyPromise !== void 0) return this.#destroyPromise;
|
|
2989
|
+
if (this.#abortPromise !== void 0) return this.#abortPromise;
|
|
2990
|
+
if (this.#stopPromise !== void 0) return this.#stopPromise;
|
|
2991
|
+
const barrier = createDeferred();
|
|
2992
|
+
this.#stopPromise = barrier.promise;
|
|
2993
|
+
barrier.promise.catch(() => {});
|
|
2171
2994
|
this.#stopping = true;
|
|
2172
2995
|
this.#stopped = true;
|
|
2173
|
-
this.#queue.stop();
|
|
2996
|
+
const cleanup = this.#queue.stop();
|
|
2997
|
+
this.#settleLifecycle(barrier, cleanup);
|
|
2998
|
+
return barrier.promise;
|
|
2174
2999
|
}
|
|
2175
3000
|
destroy() {
|
|
2176
|
-
if (this.#
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
}
|
|
3001
|
+
if (this.#destroyPromise !== void 0) return this.#destroyPromise;
|
|
3002
|
+
const barrier = createDeferred();
|
|
3003
|
+
this.#destroyPromise = barrier.promise;
|
|
3004
|
+
barrier.promise.catch(() => {});
|
|
3005
|
+
this.#stopped = true;
|
|
2180
3006
|
this.abort();
|
|
2181
|
-
this.#queue.destroy();
|
|
3007
|
+
const cleanup = this.#queue.destroy();
|
|
3008
|
+
this.#settleDestroy(barrier, cleanup);
|
|
3009
|
+
return barrier.promise;
|
|
2182
3010
|
}
|
|
2183
3011
|
#launch(input, parent, announce = parent !== void 0) {
|
|
2184
3012
|
const id = crypto.randomUUID();
|
|
@@ -2187,14 +3015,24 @@ var Runner = class {
|
|
|
2187
3015
|
this.#order.push(id);
|
|
2188
3016
|
this.#count += 1;
|
|
2189
3017
|
if (announce) this.#emitter.emit("spawn", id, parent);
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
input
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
3018
|
+
let promise;
|
|
3019
|
+
try {
|
|
3020
|
+
const entry = this.#entries?.(input);
|
|
3021
|
+
const retries = entry?.retries;
|
|
3022
|
+
const timeout = entry?.timeout;
|
|
3023
|
+
this.#queued.add(id);
|
|
3024
|
+
promise = this.#queue.enqueue({
|
|
3025
|
+
id,
|
|
3026
|
+
input
|
|
3027
|
+
}, {
|
|
3028
|
+
id,
|
|
3029
|
+
signal: abort.signal,
|
|
3030
|
+
...retries === void 0 ? {} : { retries },
|
|
3031
|
+
...timeout === void 0 ? {} : { timeout }
|
|
3032
|
+
});
|
|
3033
|
+
} catch (error) {
|
|
3034
|
+
promise = Promise.reject(error);
|
|
3035
|
+
}
|
|
2198
3036
|
promise.then((value) => this.#settle(id, {
|
|
2199
3037
|
ok: true,
|
|
2200
3038
|
value
|
|
@@ -2213,14 +3051,14 @@ var Runner = class {
|
|
|
2213
3051
|
return this.#handler(controller);
|
|
2214
3052
|
}
|
|
2215
3053
|
#spawn(input, parent) {
|
|
2216
|
-
if (!this.#
|
|
3054
|
+
if (!this.#accepts()) throw new Error("spawn is unavailable outside an active run");
|
|
2217
3055
|
return this.#launch(input, parent);
|
|
2218
3056
|
}
|
|
2219
3057
|
#settle(id, outcome) {
|
|
2220
3058
|
if (outcome.ok) {
|
|
2221
3059
|
this.#values.set(id, { value: outcome.value });
|
|
2222
3060
|
this.#emitter.emit("settle", id);
|
|
2223
|
-
} else if (this.#stopping && !this.#dispatched.has(id)) {} else if (this.#failure === void 0) {
|
|
3061
|
+
} else if (this.#stopping && this.#queued.has(id) && !this.#dispatched.has(id)) {} else if (this.#failure === void 0) {
|
|
2224
3062
|
this.#failure = { error: outcome.error };
|
|
2225
3063
|
this.#emitter.emit("fail", id, outcome.error);
|
|
2226
3064
|
this.abort(outcome.error);
|
|
@@ -2236,6 +3074,46 @@ var Runner = class {
|
|
|
2236
3074
|
}
|
|
2237
3075
|
return results;
|
|
2238
3076
|
}
|
|
3077
|
+
#accepts() {
|
|
3078
|
+
return this.#running && !this.#stopped;
|
|
3079
|
+
}
|
|
3080
|
+
async #cleanup() {
|
|
3081
|
+
const cleanup = this.#destroyPromise ?? this.#abortPromise ?? this.#stopPromise;
|
|
3082
|
+
if (cleanup === void 0) return void 0;
|
|
3083
|
+
try {
|
|
3084
|
+
await cleanup;
|
|
3085
|
+
return;
|
|
3086
|
+
} catch (error) {
|
|
3087
|
+
return { error };
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
3090
|
+
async #settleLifecycle(barrier, cleanup) {
|
|
3091
|
+
let failure;
|
|
3092
|
+
try {
|
|
3093
|
+
await cleanup;
|
|
3094
|
+
} catch (error) {
|
|
3095
|
+
failure = { error };
|
|
3096
|
+
}
|
|
3097
|
+
await this.#waitDrain();
|
|
3098
|
+
if (failure === void 0) barrier.resolve();
|
|
3099
|
+
else barrier.reject(failure.error);
|
|
3100
|
+
}
|
|
3101
|
+
async #settleDestroy(barrier, cleanup) {
|
|
3102
|
+
let failure;
|
|
3103
|
+
try {
|
|
3104
|
+
await cleanup;
|
|
3105
|
+
} catch (error) {
|
|
3106
|
+
failure = { error };
|
|
3107
|
+
}
|
|
3108
|
+
await this.#waitDrain();
|
|
3109
|
+
this.#emitter.destroy();
|
|
3110
|
+
if (failure === void 0) barrier.resolve();
|
|
3111
|
+
else barrier.reject(failure.error);
|
|
3112
|
+
}
|
|
3113
|
+
async #waitDrain() {
|
|
3114
|
+
if (this.#count === 0) return;
|
|
3115
|
+
await this.#drained?.promise;
|
|
3116
|
+
}
|
|
2239
3117
|
#cancel(reason) {
|
|
2240
3118
|
for (const abort of this.#aborts.values()) abort.abort(reason);
|
|
2241
3119
|
}
|
|
@@ -2243,18 +3121,17 @@ var Runner = class {
|
|
|
2243
3121
|
//#endregion
|
|
2244
3122
|
//#region src/core/tasks/TaskController.ts
|
|
2245
3123
|
/**
|
|
2246
|
-
* The
|
|
2247
|
-
* running task's folded cancellation, its input, its lineage, and read-UP access to the
|
|
2248
|
-
* result tree.
|
|
3124
|
+
* The attempt-scoped handle a {@link import('./types.js').WorkflowFunction} receives.
|
|
2249
3125
|
*
|
|
2250
3126
|
* @remarks
|
|
2251
3127
|
* - **A leaf handle, NOT the runner `Controller`.** A workflow task is a leaf of the
|
|
2252
|
-
* declarative W-b tree, not a fan-out unit, so
|
|
2253
|
-
*
|
|
2254
|
-
* - **Folded signal.** `signal` is the cancellation
|
|
2255
|
-
*
|
|
2256
|
-
*
|
|
2257
|
-
*
|
|
3128
|
+
* declarative W-b tree, not a fan-out unit, so it has no `spawn`; its `wait` instead
|
|
3129
|
+
* checkpoints the workflow, phase, and task cooperative gates.
|
|
3130
|
+
* - **Folded signal.** `signal` is the cancellation folded for THIS attempt: its per-attempt
|
|
3131
|
+
* deadline, task stop/skip, workflow abort/timeout/budget/destroy, or a sibling fail-fast.
|
|
3132
|
+
* A handler races its work against it; `aborted` reads it.
|
|
3133
|
+
* - **Attempt ownership.** `report` / `pulse` are closures supplied by the runner and refuse
|
|
3134
|
+
* after this signal aborts or a retry token supersedes this handle.
|
|
2258
3135
|
* - **Input + lineage.** `input` is the task's open `metadata` bag (its
|
|
2259
3136
|
* {@link import('./types.js').TaskInput} payload, `{}` when none); `task` is the full
|
|
2260
3137
|
* {@link TaskContext}, so `task.phase` / `task.phase.workflow` navigate UP the lineage.
|
|
@@ -2270,19 +3147,275 @@ var TaskController = class {
|
|
|
2270
3147
|
signal;
|
|
2271
3148
|
input;
|
|
2272
3149
|
task;
|
|
3150
|
+
attempt;
|
|
3151
|
+
#entity;
|
|
3152
|
+
#report;
|
|
3153
|
+
#pulse;
|
|
2273
3154
|
#results;
|
|
2274
|
-
constructor(signal, input, task, results) {
|
|
3155
|
+
constructor(signal, input, task, attempt, results, report, pulse) {
|
|
2275
3156
|
this.signal = signal;
|
|
2276
3157
|
this.input = input;
|
|
2277
|
-
this.task = task;
|
|
3158
|
+
this.task = task.context;
|
|
3159
|
+
this.attempt = attempt;
|
|
3160
|
+
this.#entity = task;
|
|
2278
3161
|
this.#results = results;
|
|
3162
|
+
this.#report = report;
|
|
3163
|
+
this.#pulse = pulse;
|
|
2279
3164
|
}
|
|
2280
3165
|
get aborted() {
|
|
2281
3166
|
return this.signal.aborted;
|
|
2282
3167
|
}
|
|
3168
|
+
get paused() {
|
|
3169
|
+
if (this.#ancestorTerminal()) return false;
|
|
3170
|
+
return this.#entity.workflow.paused || this.#entity.phase.paused || !isTerminalStatus(this.#entity.status) && this.#entity.paused;
|
|
3171
|
+
}
|
|
3172
|
+
report(input) {
|
|
3173
|
+
return this.#report(input);
|
|
3174
|
+
}
|
|
3175
|
+
pulse() {
|
|
3176
|
+
return this.#pulse();
|
|
3177
|
+
}
|
|
3178
|
+
async wait() {
|
|
3179
|
+
while (this.paused && !this.signal.aborted) await this.#race(this.#gates());
|
|
3180
|
+
}
|
|
2283
3181
|
results() {
|
|
2284
3182
|
return this.#results();
|
|
2285
3183
|
}
|
|
3184
|
+
#gates() {
|
|
3185
|
+
if (this.#ancestorTerminal()) return [];
|
|
3186
|
+
const gates = [];
|
|
3187
|
+
if (this.#entity.workflow.paused) gates.push(this.#entity.workflow.wait());
|
|
3188
|
+
if (this.#entity.phase.paused) gates.push(this.#entity.phase.wait());
|
|
3189
|
+
if (!isTerminalStatus(this.#entity.status) && this.#entity.paused) gates.push(this.#entity.wait());
|
|
3190
|
+
return gates;
|
|
3191
|
+
}
|
|
3192
|
+
async #race(gates) {
|
|
3193
|
+
if (this.signal.aborted || gates.length === 0) return;
|
|
3194
|
+
const deferred = Promise.withResolvers();
|
|
3195
|
+
const onAbort = this.#resolve.bind(this, deferred);
|
|
3196
|
+
const onTerminal = this.#resolve.bind(this, deferred);
|
|
3197
|
+
this.signal.addEventListener("abort", onAbort, { once: true });
|
|
3198
|
+
this.#entity.workflow.emitter.on("skip", onTerminal);
|
|
3199
|
+
this.#entity.workflow.emitter.on("stop", onTerminal);
|
|
3200
|
+
this.#entity.phase.emitter.on("skip", onTerminal);
|
|
3201
|
+
this.#entity.phase.emitter.on("stop", onTerminal);
|
|
3202
|
+
try {
|
|
3203
|
+
if (this.#ancestorTerminal()) deferred.resolve();
|
|
3204
|
+
await Promise.race([Promise.all(gates), deferred.promise]);
|
|
3205
|
+
} finally {
|
|
3206
|
+
this.signal.removeEventListener("abort", onAbort);
|
|
3207
|
+
this.#entity.workflow.emitter.off("skip", onTerminal);
|
|
3208
|
+
this.#entity.workflow.emitter.off("stop", onTerminal);
|
|
3209
|
+
this.#entity.phase.emitter.off("skip", onTerminal);
|
|
3210
|
+
this.#entity.phase.emitter.off("stop", onTerminal);
|
|
3211
|
+
}
|
|
3212
|
+
}
|
|
3213
|
+
#ancestorTerminal() {
|
|
3214
|
+
return isTerminalStatus(this.#entity.workflow.status) || isTerminalStatus(this.#entity.phase.status);
|
|
3215
|
+
}
|
|
3216
|
+
#resolve(deferred) {
|
|
3217
|
+
deferred.resolve();
|
|
3218
|
+
}
|
|
3219
|
+
};
|
|
3220
|
+
//#endregion
|
|
3221
|
+
//#region src/core/WorkflowPersistence.ts
|
|
3222
|
+
/**
|
|
3223
|
+
* Advanced run-local snapshot persistence with one writer and one coalesced latest obligation.
|
|
3224
|
+
*
|
|
3225
|
+
* @remarks
|
|
3226
|
+
* Normally composed by `WorkflowRunner.execute({ store })`; exported for hosts that need to
|
|
3227
|
+
* coordinate the same required boundaries around their own runner integration.
|
|
3228
|
+
*/
|
|
3229
|
+
var WorkflowPersistence = class {
|
|
3230
|
+
#workflow;
|
|
3231
|
+
#store;
|
|
3232
|
+
#phases = /* @__PURE__ */ new Set();
|
|
3233
|
+
#tasks = /* @__PURE__ */ new Set();
|
|
3234
|
+
#onWorkflowChange;
|
|
3235
|
+
#onWorkflowAdd;
|
|
3236
|
+
#onWorkflowRemove;
|
|
3237
|
+
#onPhaseChange;
|
|
3238
|
+
#onPhaseAdd;
|
|
3239
|
+
#onPhaseRemove;
|
|
3240
|
+
#onTaskChange;
|
|
3241
|
+
#writing;
|
|
3242
|
+
#error;
|
|
3243
|
+
#fault;
|
|
3244
|
+
#attached = true;
|
|
3245
|
+
#revision = 0;
|
|
3246
|
+
#stored = 0;
|
|
3247
|
+
constructor(workflow, store) {
|
|
3248
|
+
this.#workflow = workflow;
|
|
3249
|
+
this.#store = store;
|
|
3250
|
+
this.#onWorkflowChange = this.#change.bind(this);
|
|
3251
|
+
this.#onWorkflowAdd = this.#addPhase.bind(this);
|
|
3252
|
+
this.#onWorkflowRemove = this.#removePhase.bind(this);
|
|
3253
|
+
this.#onPhaseChange = this.#change.bind(this);
|
|
3254
|
+
this.#onPhaseAdd = this.#addTask.bind(this);
|
|
3255
|
+
this.#onPhaseRemove = this.#removeTask.bind(this);
|
|
3256
|
+
this.#onTaskChange = this.#change.bind(this);
|
|
3257
|
+
this.#attachWorkflow();
|
|
3258
|
+
}
|
|
3259
|
+
get fault() {
|
|
3260
|
+
return this.#fault;
|
|
3261
|
+
}
|
|
3262
|
+
/**
|
|
3263
|
+
* Persist every change through this required boundary.
|
|
3264
|
+
*
|
|
3265
|
+
* @param checkpoint - The boundary being made durable
|
|
3266
|
+
* @param task - The task owning an attempt or settlement
|
|
3267
|
+
* @param attempt - The persisted attempt number
|
|
3268
|
+
* @returns Whether the latest state reached the store
|
|
3269
|
+
*/
|
|
3270
|
+
async checkpoint(checkpoint, task, attempt) {
|
|
3271
|
+
const revision = this.#mark();
|
|
3272
|
+
while (this.#stored < revision) await this.#flush();
|
|
3273
|
+
if (this.#error === void 0) return true;
|
|
3274
|
+
if (this.#fault === void 0) this.#fault = Object.freeze({
|
|
3275
|
+
origin: "persistence",
|
|
3276
|
+
checkpoint,
|
|
3277
|
+
message: this.#error,
|
|
3278
|
+
...task === void 0 ? {} : { task: task.id },
|
|
3279
|
+
...attempt === void 0 ? {} : { attempt }
|
|
3280
|
+
});
|
|
3281
|
+
return false;
|
|
3282
|
+
}
|
|
3283
|
+
/**
|
|
3284
|
+
* Stop observing the live tree and persist its final state.
|
|
3285
|
+
*
|
|
3286
|
+
* @returns Whether the final snapshot reached the store
|
|
3287
|
+
*/
|
|
3288
|
+
async finalize() {
|
|
3289
|
+
this.detach();
|
|
3290
|
+
return this.checkpoint("final");
|
|
3291
|
+
}
|
|
3292
|
+
/** Stop observing the live tree. */
|
|
3293
|
+
detach() {
|
|
3294
|
+
if (!this.#attached) return;
|
|
3295
|
+
this.#attached = false;
|
|
3296
|
+
this.#workflow.emitter.off("start", this.#onWorkflowChange);
|
|
3297
|
+
this.#workflow.emitter.off("complete", this.#onWorkflowChange);
|
|
3298
|
+
this.#workflow.emitter.off("fail", this.#onWorkflowChange);
|
|
3299
|
+
this.#workflow.emitter.off("skip", this.#onWorkflowChange);
|
|
3300
|
+
this.#workflow.emitter.off("stop", this.#onWorkflowChange);
|
|
3301
|
+
this.#workflow.emitter.off("move", this.#onWorkflowChange);
|
|
3302
|
+
this.#workflow.emitter.off("update", this.#onWorkflowChange);
|
|
3303
|
+
this.#workflow.emitter.off("add", this.#onWorkflowAdd);
|
|
3304
|
+
this.#workflow.emitter.off("remove", this.#onWorkflowRemove);
|
|
3305
|
+
for (const phase of this.#phases) this.#detachPhase(phase);
|
|
3306
|
+
}
|
|
3307
|
+
#attachWorkflow() {
|
|
3308
|
+
this.#workflow.emitter.on("start", this.#onWorkflowChange);
|
|
3309
|
+
this.#workflow.emitter.on("complete", this.#onWorkflowChange);
|
|
3310
|
+
this.#workflow.emitter.on("fail", this.#onWorkflowChange);
|
|
3311
|
+
this.#workflow.emitter.on("skip", this.#onWorkflowChange);
|
|
3312
|
+
this.#workflow.emitter.on("stop", this.#onWorkflowChange);
|
|
3313
|
+
this.#workflow.emitter.on("move", this.#onWorkflowChange);
|
|
3314
|
+
this.#workflow.emitter.on("update", this.#onWorkflowChange);
|
|
3315
|
+
this.#workflow.emitter.on("add", this.#onWorkflowAdd);
|
|
3316
|
+
this.#workflow.emitter.on("remove", this.#onWorkflowRemove);
|
|
3317
|
+
for (const phase of this.#workflow.phases.phases()) this.#attachPhase(phase);
|
|
3318
|
+
}
|
|
3319
|
+
#attachPhase(phase) {
|
|
3320
|
+
if (this.#phases.has(phase)) return;
|
|
3321
|
+
this.#phases.add(phase);
|
|
3322
|
+
phase.emitter.on("start", this.#onPhaseChange);
|
|
3323
|
+
phase.emitter.on("complete", this.#onPhaseChange);
|
|
3324
|
+
phase.emitter.on("fail", this.#onPhaseChange);
|
|
3325
|
+
phase.emitter.on("skip", this.#onPhaseChange);
|
|
3326
|
+
phase.emitter.on("stop", this.#onPhaseChange);
|
|
3327
|
+
phase.emitter.on("move", this.#onPhaseChange);
|
|
3328
|
+
phase.emitter.on("update", this.#onPhaseChange);
|
|
3329
|
+
phase.emitter.on("add", this.#onPhaseAdd);
|
|
3330
|
+
phase.emitter.on("remove", this.#onPhaseRemove);
|
|
3331
|
+
for (const task of phase.tasks.tasks()) this.#attachTask(task);
|
|
3332
|
+
}
|
|
3333
|
+
#detachPhase(phase) {
|
|
3334
|
+
if (!this.#phases.delete(phase)) return;
|
|
3335
|
+
phase.emitter.off("start", this.#onPhaseChange);
|
|
3336
|
+
phase.emitter.off("complete", this.#onPhaseChange);
|
|
3337
|
+
phase.emitter.off("fail", this.#onPhaseChange);
|
|
3338
|
+
phase.emitter.off("skip", this.#onPhaseChange);
|
|
3339
|
+
phase.emitter.off("stop", this.#onPhaseChange);
|
|
3340
|
+
phase.emitter.off("move", this.#onPhaseChange);
|
|
3341
|
+
phase.emitter.off("update", this.#onPhaseChange);
|
|
3342
|
+
phase.emitter.off("add", this.#onPhaseAdd);
|
|
3343
|
+
phase.emitter.off("remove", this.#onPhaseRemove);
|
|
3344
|
+
for (const task of phase.tasks.tasks()) this.#detachTask(task);
|
|
3345
|
+
}
|
|
3346
|
+
#attachTask(task) {
|
|
3347
|
+
if (this.#tasks.has(task)) return;
|
|
3348
|
+
this.#tasks.add(task);
|
|
3349
|
+
task.emitter.on("start", this.#onTaskChange);
|
|
3350
|
+
task.emitter.on("complete", this.#onTaskChange);
|
|
3351
|
+
task.emitter.on("fail", this.#onTaskChange);
|
|
3352
|
+
task.emitter.on("skip", this.#onTaskChange);
|
|
3353
|
+
task.emitter.on("stop", this.#onTaskChange);
|
|
3354
|
+
task.emitter.on("report", this.#onTaskChange);
|
|
3355
|
+
task.emitter.on("pulse", this.#onTaskChange);
|
|
3356
|
+
}
|
|
3357
|
+
#detachTask(task) {
|
|
3358
|
+
if (!this.#tasks.delete(task)) return;
|
|
3359
|
+
task.emitter.off("start", this.#onTaskChange);
|
|
3360
|
+
task.emitter.off("complete", this.#onTaskChange);
|
|
3361
|
+
task.emitter.off("fail", this.#onTaskChange);
|
|
3362
|
+
task.emitter.off("skip", this.#onTaskChange);
|
|
3363
|
+
task.emitter.off("stop", this.#onTaskChange);
|
|
3364
|
+
task.emitter.off("report", this.#onTaskChange);
|
|
3365
|
+
task.emitter.off("pulse", this.#onTaskChange);
|
|
3366
|
+
}
|
|
3367
|
+
#addPhase(phase) {
|
|
3368
|
+
this.#attachPhase(phase);
|
|
3369
|
+
this.#change();
|
|
3370
|
+
}
|
|
3371
|
+
#removePhase(phase) {
|
|
3372
|
+
this.#detachPhase(phase);
|
|
3373
|
+
this.#change();
|
|
3374
|
+
}
|
|
3375
|
+
#addTask(task) {
|
|
3376
|
+
this.#attachTask(task);
|
|
3377
|
+
this.#change();
|
|
3378
|
+
}
|
|
3379
|
+
#removeTask(task) {
|
|
3380
|
+
this.#detachTask(task);
|
|
3381
|
+
this.#change();
|
|
3382
|
+
}
|
|
3383
|
+
#change() {
|
|
3384
|
+
this.#mark();
|
|
3385
|
+
this.#flush();
|
|
3386
|
+
}
|
|
3387
|
+
async #flush() {
|
|
3388
|
+
if (this.#writing !== void 0) {
|
|
3389
|
+
await this.#writing;
|
|
3390
|
+
return;
|
|
3391
|
+
}
|
|
3392
|
+
const reservation = Promise.withResolvers();
|
|
3393
|
+
const writing = reservation.promise;
|
|
3394
|
+
this.#writing = writing;
|
|
3395
|
+
this.#drain().then(reservation.resolve, reservation.reject);
|
|
3396
|
+
try {
|
|
3397
|
+
await writing;
|
|
3398
|
+
} finally {
|
|
3399
|
+
if (this.#writing === writing) this.#writing = void 0;
|
|
3400
|
+
if (this.#stored < this.#revision) this.#flush();
|
|
3401
|
+
}
|
|
3402
|
+
}
|
|
3403
|
+
async #drain() {
|
|
3404
|
+
while (this.#stored < this.#revision) {
|
|
3405
|
+
const revision = this.#revision;
|
|
3406
|
+
try {
|
|
3407
|
+
await this.#store.set(this.#workflow.snapshot());
|
|
3408
|
+
this.#error = void 0;
|
|
3409
|
+
} catch (error) {
|
|
3410
|
+
this.#error = errorToMessage(error);
|
|
3411
|
+
}
|
|
3412
|
+
this.#stored = revision;
|
|
3413
|
+
}
|
|
3414
|
+
}
|
|
3415
|
+
#mark() {
|
|
3416
|
+
this.#revision += 1;
|
|
3417
|
+
return this.#revision;
|
|
3418
|
+
}
|
|
2286
3419
|
};
|
|
2287
3420
|
//#endregion
|
|
2288
3421
|
//#region src/core/WorkflowRunner.ts
|
|
@@ -2295,21 +3428,21 @@ var TaskController = class {
|
|
|
2295
3428
|
* - **Composes, never re-implements.** Per-phase bounded concurrency is one
|
|
2296
3429
|
* {@link createRunner} per phase (the substrate {@link RunnerInterface} over the workers
|
|
2297
3430
|
* `Queue`); `bail` maps onto that Runner's fail-fast vs settle-all; the run-level abort /
|
|
2298
|
-
* timeout / budget / entity `signal` fold through
|
|
2299
|
-
* `AbortSignal.any` (exactly as the agent runtime folds its bounds);
|
|
3431
|
+
* timeout / budget / entity `signal` fold through the `@orkestrel/abort` signal contract,
|
|
3432
|
+
* {@link createTimeout}, and `AbortSignal.any` (exactly as the agent runtime folds its bounds);
|
|
3433
|
+
* pacing is the shipped
|
|
2300
3434
|
* {@link SchedulerInterface}. The runner writes ZERO concurrency / retry / abort logic of
|
|
2301
3435
|
* its own — it only sequences phases, dispatches a task's own handler, and drives the live
|
|
2302
|
-
* entity.
|
|
2303
|
-
*
|
|
2304
|
-
*
|
|
3436
|
+
* entity. The workflow layer owns per-task deadlines because timeout settlement must
|
|
3437
|
+
* update the live leaf under the phase's `bail` policy before the substrate unit settles.
|
|
3438
|
+
* - **Pure engine — no integration registry.** The runner carries no behavior or provider
|
|
3439
|
+
* registry: each live {@link TaskInterface} already
|
|
2305
3440
|
* resolved its own {@link import('./types.js').WorkflowFunction} into
|
|
2306
3441
|
* {@link import('./types.js').TaskInterface.handler} ONCE at construction (build, restore,
|
|
2307
3442
|
* or a live mint all resolve it identically, from {@link WorkflowOptions.functions}), so
|
|
2308
|
-
* dispatch is simply "invoke the task's own handler".
|
|
2309
|
-
*
|
|
2310
|
-
* {@link
|
|
2311
|
-
* {@link WorkflowOptions.functions} like any other behavior. This module never imports
|
|
2312
|
-
* any tool/agent package.
|
|
3443
|
+
* dispatch is simply "invoke the task's own handler". Provider, protocol, and tool
|
|
3444
|
+
* integrations remain application-owned {@link import('./types.js').WorkflowFunction}s
|
|
3445
|
+
* composed into {@link WorkflowOptions.functions}. This module imports none of them.
|
|
2313
3446
|
* - **Two `execute` forms, one engine.** `execute(definition, options)` BUILDS the live tree
|
|
2314
3447
|
* from a {@link WorkflowDefinition} (single source of truth for the `run` / `concurrency`
|
|
2315
3448
|
* metadata); `execute(workflow, options)` DRIVES a caller-owned, ALREADY-BUILT
|
|
@@ -2329,10 +3462,9 @@ var TaskController = class {
|
|
|
2329
3462
|
* for `spawn` to accept (the runner already drained) is swept `skip`ped afterward so the
|
|
2330
3463
|
* phase always reaches a coherent terminal state.
|
|
2331
3464
|
* - **Dispatch by handler.** `#runTask` invokes the live task's own
|
|
2332
|
-
* {@link import('./types.js').TaskInterface.handler} directly
|
|
2333
|
-
*
|
|
2334
|
-
*
|
|
2335
|
-
* task's {@link import('./types.js').TaskControllerInterface} handle.
|
|
3465
|
+
* {@link import('./types.js').TaskInterface.handler} directly. An omitted `run` deliberately
|
|
3466
|
+
* auto-completes with JSON `null`; a present unresolved name is rejected by the synchronous
|
|
3467
|
+
* execution claim and never false-completes.
|
|
2336
3468
|
* - **`bail` → substrate.** Under `bail: true` (halt) a genuine task failure `fail`s the leaf
|
|
2337
3469
|
* THEN re-throws, so the substrate Runner fail-fasts — it aborts the in-flight siblings
|
|
2338
3470
|
* (their `controller.signal` fires; a mid-flight sibling `skip`s) and rejects the phase run;
|
|
@@ -2340,11 +3472,12 @@ var TaskController = class {
|
|
|
2340
3472
|
* Under `bail: false` (graceful) a failure `fail`s the leaf and RESOLVES (never throws), so
|
|
2341
3473
|
* the Runner settles every unit (allSettled) and the run finishes (the workflow derives
|
|
2342
3474
|
* `completed`, the failure recorded in the result tree).
|
|
2343
|
-
* - **Pause / stop / destroy gates.**
|
|
2344
|
-
*
|
|
2345
|
-
*
|
|
2346
|
-
*
|
|
2347
|
-
*
|
|
3475
|
+
* - **Pause / stop / destroy gates.** Workflow, phase, and task gates are checked before
|
|
3476
|
+
* dispatch, and a running handler can checkpoint their folded state through
|
|
3477
|
+
* {@link import('./types.js').TaskControllerInterface.wait}. Because the substrate acquires
|
|
3478
|
+
* concurrency before this handler gate, a paused task occupies one phase slot until resume;
|
|
3479
|
+
* already-running siblings continue and its per-attempt timeout keeps counting. A GRACEFUL
|
|
3480
|
+
* `workflow.stop()` (no signal involved) is caught at
|
|
2348
3481
|
* those same gates: not-yet-started work is `skip`ped, in-flight work finishes naturally. A
|
|
2349
3482
|
* HARD `workflow.destroy()` aborts {@link WorkflowInterface.signal}, which `#fold` has folded
|
|
2350
3483
|
* into the run's composed signal — so it cancels the active phase Runner (and every
|
|
@@ -2361,37 +3494,61 @@ var TaskController = class {
|
|
|
2361
3494
|
* {@link TaskController} signal `AbortSignal.any`-combines the substrate per-unit signal with
|
|
2362
3495
|
* `runSignal`, so a handler observes either cause directly.
|
|
2363
3496
|
* - **Re-entrant-safe.** No shared per-run mutable field: the active-Runner holder is LOCAL to
|
|
2364
|
-
* each `#execute`, so a nested `execute`
|
|
2365
|
-
*
|
|
3497
|
+
* each `#execute`, so a nested application-level `execute` cannot clobber the outer run's
|
|
3498
|
+
* state.
|
|
2366
3499
|
*/
|
|
2367
|
-
var WorkflowRunner = class {
|
|
3500
|
+
var WorkflowRunner = class WorkflowRunner {
|
|
3501
|
+
static #executions = /* @__PURE__ */ new WeakSet();
|
|
2368
3502
|
#scheduler;
|
|
2369
3503
|
constructor(scheduler) {
|
|
2370
3504
|
this.#scheduler = scheduler;
|
|
2371
3505
|
}
|
|
2372
3506
|
execute(target, options) {
|
|
2373
3507
|
if (this.#isWorkflow(target)) {
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
return this.#execute(target,
|
|
3508
|
+
const signal = options?.signal;
|
|
3509
|
+
const timeout = options?.timeout;
|
|
3510
|
+
const budget = options?.budget;
|
|
3511
|
+
const store = options?.store;
|
|
3512
|
+
this.#acquire(target);
|
|
3513
|
+
return this.#execute(target, signal, timeout, budget, store);
|
|
2380
3514
|
}
|
|
2381
|
-
const
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
const
|
|
2386
|
-
const
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
3515
|
+
const captured = captureWorkflowOptions(options);
|
|
3516
|
+
const signal = options?.signal;
|
|
3517
|
+
const timeout = options?.timeout;
|
|
3518
|
+
const budget = options?.budget;
|
|
3519
|
+
const store = options?.store;
|
|
3520
|
+
const workflow = new Workflow(definitionToSnapshot(target, captured.bail ?? target.bail ?? false), captured);
|
|
3521
|
+
this.#acquire(workflow);
|
|
3522
|
+
return this.#execute(workflow, signal, timeout, budget, store);
|
|
3523
|
+
}
|
|
3524
|
+
#acquire(workflow) {
|
|
3525
|
+
const tasks = workflow.phases.phases().flatMap((phase) => phase.tasks.tasks());
|
|
3526
|
+
if (!((workflow.status === "pending" || workflow.status === "running") && tasks.every((task) => task.status !== "running") && (tasks.length === 0 || tasks.some((task) => task.status === "pending")) && hasWorkflowHandlers(workflow)) || workflow.destroyed || WorkflowRunner.#executions.has(workflow)) throw new WorkflowError("TRANSITION", `workflow '${workflow.id}' is not drivable`, {
|
|
3527
|
+
id: workflow.id,
|
|
3528
|
+
status: workflow.status,
|
|
3529
|
+
destroyed: workflow.destroyed
|
|
3530
|
+
});
|
|
3531
|
+
WorkflowRunner.#executions.add(workflow);
|
|
3532
|
+
}
|
|
3533
|
+
async #execute(workflow, signal, ms, budget, store) {
|
|
2390
3534
|
const holder = { runner: void 0 };
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
3535
|
+
let timeout;
|
|
3536
|
+
let persistence;
|
|
3537
|
+
let runSignal;
|
|
3538
|
+
let onCancel;
|
|
2394
3539
|
try {
|
|
3540
|
+
timeout = ms !== void 0 && Number.isFinite(ms) && ms > 0 && ms <= 2147483647 ? (0, _orkestrel_timeout.createTimeout)({ ms }) : void 0;
|
|
3541
|
+
timeout?.start();
|
|
3542
|
+
budget?.start();
|
|
3543
|
+
runSignal = this.#fold(workflow, signal, budget, timeout);
|
|
3544
|
+
persistence = store === void 0 ? void 0 : new WorkflowPersistence(workflow, store);
|
|
3545
|
+
onCancel = this.#abortActive.bind(this, holder, runSignal);
|
|
3546
|
+
if (runSignal.aborted) onCancel();
|
|
3547
|
+
else runSignal.addEventListener("abort", onCancel, { once: true });
|
|
3548
|
+
if (persistence !== void 0 && !await persistence.checkpoint("initial")) {
|
|
3549
|
+
if (this.#stoppable(workflow)) workflow.stop();
|
|
3550
|
+
this.#skipFrom(workflow.phases.phases(), 0);
|
|
3551
|
+
}
|
|
2395
3552
|
let index = 0;
|
|
2396
3553
|
for (;;) {
|
|
2397
3554
|
const phases = workflow.phases.phases();
|
|
@@ -2405,36 +3562,53 @@ var WorkflowRunner = class {
|
|
|
2405
3562
|
this.#haltFrom(phases, index, workflow, runSignal);
|
|
2406
3563
|
break;
|
|
2407
3564
|
}
|
|
2408
|
-
if (workflow.paused) await this.#raceWait(
|
|
3565
|
+
if (workflow.paused) await this.#raceWait(workflow.wait(), runSignal, void 0, workflow);
|
|
2409
3566
|
if (this.#cancelled(runSignal) || this.#halted(workflow)) {
|
|
2410
3567
|
this.#haltFrom(workflow.phases.phases(), index, workflow, runSignal);
|
|
2411
3568
|
break;
|
|
2412
3569
|
}
|
|
2413
|
-
if (
|
|
3570
|
+
if (phase.status === "skipped" || phase.status === "stopped") {
|
|
3571
|
+
this.#skipFrom([phase], 0);
|
|
3572
|
+
index += 1;
|
|
3573
|
+
continue;
|
|
3574
|
+
}
|
|
3575
|
+
if (await this.#runPhase(workflow, phase, runSignal, holder, persistence)) {
|
|
2414
3576
|
this.#skipFrom(workflow.phases.phases(), index + 1);
|
|
2415
3577
|
break;
|
|
2416
3578
|
}
|
|
2417
3579
|
index += 1;
|
|
2418
3580
|
const remaining = workflow.phases.phases();
|
|
2419
|
-
if (index < remaining.length && !this.#cancelled(runSignal))
|
|
2420
|
-
await this.#scheduler.yield({ signal: runSignal });
|
|
2421
|
-
} catch (error) {
|
|
2422
|
-
if (!runSignal.aborted) throw error;
|
|
2423
|
-
}
|
|
3581
|
+
if (index < remaining.length && !this.#cancelled(runSignal)) await this.#pace(runSignal);
|
|
2424
3582
|
}
|
|
2425
3583
|
if (this.#cancelled(runSignal)) this.#haltFrom(workflow.phases.phases(), 0, workflow, runSignal);
|
|
2426
3584
|
else if (this.#completable(workflow)) workflow.complete();
|
|
3585
|
+
const durable = await persistence?.finalize();
|
|
2427
3586
|
return {
|
|
2428
3587
|
workflow,
|
|
2429
3588
|
status: workflow.status,
|
|
2430
|
-
results: workflow.results()
|
|
3589
|
+
results: workflow.results(),
|
|
3590
|
+
...durable === void 0 ? {} : { durable },
|
|
3591
|
+
...persistence?.fault === void 0 ? {} : { fault: persistence.fault }
|
|
2431
3592
|
};
|
|
3593
|
+
} catch (error) {
|
|
3594
|
+
if (this.#stoppable(workflow)) workflow.stop();
|
|
3595
|
+
this.#skipFrom(workflow.phases.phases(), 0);
|
|
3596
|
+
await persistence?.finalize();
|
|
3597
|
+
throw error;
|
|
2432
3598
|
} finally {
|
|
3599
|
+
persistence?.detach();
|
|
2433
3600
|
timeout?.clear();
|
|
2434
|
-
runSignal.removeEventListener("abort", onCancel);
|
|
3601
|
+
if (runSignal !== void 0 && onCancel !== void 0) runSignal.removeEventListener("abort", onCancel);
|
|
3602
|
+
}
|
|
3603
|
+
}
|
|
3604
|
+
async #pace(signal) {
|
|
3605
|
+
try {
|
|
3606
|
+
await this.#scheduler.yield({ signal });
|
|
3607
|
+
} catch (error) {
|
|
3608
|
+
if (!signal.aborted) throw error;
|
|
2435
3609
|
}
|
|
2436
3610
|
}
|
|
2437
|
-
async #runPhase(workflow, phase, runSignal, holder) {
|
|
3611
|
+
async #runPhase(workflow, phase, runSignal, holder, persistence) {
|
|
2438
3612
|
const launched = /* @__PURE__ */ new Set();
|
|
2439
3613
|
const onAdd = this.#spawnAdded.bind(this, launched, holder);
|
|
2440
3614
|
phase.emitter.on("add", onAdd);
|
|
@@ -2445,10 +3619,12 @@ var WorkflowRunner = class {
|
|
|
2445
3619
|
const bail = phase.bail;
|
|
2446
3620
|
const concurrency = phase.concurrency !== void 0 && phase.concurrency > 0 ? phase.concurrency : DEFAULT_PHASE_CONCURRENCY;
|
|
2447
3621
|
const attempts = /* @__PURE__ */ new Map();
|
|
3622
|
+
for (const task of tasks) attempts.set(task.id, task.attempts);
|
|
3623
|
+
const owners = /* @__PURE__ */ new Map();
|
|
2448
3624
|
const created = new Runner({
|
|
2449
3625
|
concurrency,
|
|
2450
3626
|
entries: this.#entry.bind(this),
|
|
2451
|
-
handler: this.#runUnit.bind(this, workflow, runSignal, bail, attempts)
|
|
3627
|
+
handler: this.#runUnit.bind(this, workflow, runSignal, bail, attempts, owners, persistence)
|
|
2452
3628
|
});
|
|
2453
3629
|
holder.runner = created;
|
|
2454
3630
|
try {
|
|
@@ -2457,8 +3633,11 @@ var WorkflowRunner = class {
|
|
|
2457
3633
|
} catch {
|
|
2458
3634
|
return !this.#cancelled(runSignal);
|
|
2459
3635
|
} finally {
|
|
2460
|
-
|
|
2461
|
-
|
|
3636
|
+
try {
|
|
3637
|
+
await created.destroy();
|
|
3638
|
+
} finally {
|
|
3639
|
+
holder.runner = void 0;
|
|
3640
|
+
}
|
|
2462
3641
|
}
|
|
2463
3642
|
} finally {
|
|
2464
3643
|
phase.emitter.off("add", onAdd);
|
|
@@ -2475,81 +3654,205 @@ var WorkflowRunner = class {
|
|
|
2475
3654
|
holder.runner?.spawn(task);
|
|
2476
3655
|
}
|
|
2477
3656
|
#entry(task) {
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
...task.timeout === void 0 ? {} : { timeout: task.timeout }
|
|
2481
|
-
};
|
|
3657
|
+
const retries = Math.max(0, (task.retries ?? 0) - task.attempts);
|
|
3658
|
+
return retries === 0 ? {} : { retries };
|
|
2482
3659
|
}
|
|
2483
|
-
#runUnit(workflow, runSignal, bail, attempts, controller) {
|
|
2484
|
-
return this.#runTask(workflow, controller.input, controller, runSignal, bail, attempts);
|
|
3660
|
+
#runUnit(workflow, runSignal, bail, attempts, owners, persistence, controller) {
|
|
3661
|
+
return this.#runTask(workflow, controller.input, controller, runSignal, bail, attempts, owners, persistence);
|
|
2485
3662
|
}
|
|
2486
|
-
async #runTask(workflow, task, controller, runSignal, bail, attempts) {
|
|
2487
|
-
const signal = this.#taskSignal(controller.signal, runSignal);
|
|
3663
|
+
async #runTask(workflow, task, controller, runSignal, bail, attempts, owners, persistence) {
|
|
2488
3664
|
const attempt = (attempts.get(task.id) ?? 0) + 1;
|
|
2489
3665
|
attempts.set(task.id, attempt);
|
|
2490
3666
|
const last = attempt > Math.max(0, task.retries ?? 0);
|
|
2491
|
-
if (
|
|
2492
|
-
if (task
|
|
2493
|
-
|
|
2494
|
-
this.#skipCancelled(task, workflow, runSignal);
|
|
2495
|
-
return;
|
|
2496
|
-
}
|
|
2497
|
-
if (task.status === "pending") task.start();
|
|
2498
|
-
if (this.#skipping(controller, runSignal) || this.#halted(workflow)) {
|
|
2499
|
-
this.#skipCancelled(task, workflow, runSignal);
|
|
3667
|
+
if (task.status !== "pending" && task.status !== "running") return;
|
|
3668
|
+
if (this.#skipping(task, controller, runSignal) || this.#halted(workflow, task.phase)) {
|
|
3669
|
+
this.#settleCancelled(task, workflow, runSignal);
|
|
2500
3670
|
return;
|
|
2501
3671
|
}
|
|
2502
|
-
const
|
|
3672
|
+
const ms = task.timeout;
|
|
3673
|
+
const deadline = ms !== void 0 && Number.isFinite(ms) && ms > 0 && ms <= 2147483647 ? (0, _orkestrel_timeout.createTimeout)({ ms }) : void 0;
|
|
3674
|
+
const signal = this.#taskSignal(task, controller.signal, runSignal, deadline);
|
|
2503
3675
|
try {
|
|
2504
|
-
|
|
2505
|
-
if (task.
|
|
2506
|
-
|
|
3676
|
+
task.start();
|
|
3677
|
+
if (task.attempts !== attempt) return;
|
|
3678
|
+
owners.set(task.id, attempt);
|
|
3679
|
+
deadline?.start();
|
|
3680
|
+
const durable = persistence === void 0 ? true : await persistence.checkpoint("attempt", task, attempt);
|
|
3681
|
+
if (!this.#owns(owners, task, attempt)) return;
|
|
3682
|
+
if (!durable) {
|
|
3683
|
+
if (this.#stoppable(workflow)) workflow.stop();
|
|
2507
3684
|
return;
|
|
2508
3685
|
}
|
|
2509
|
-
if (
|
|
2510
|
-
|
|
3686
|
+
if (task.run !== void 0 && task.handler === void 0) {
|
|
3687
|
+
const error = new WorkflowError("TRANSITION", `task '${task.id}' has an unresolved run '${task.run}'`, {
|
|
3688
|
+
task: task.id,
|
|
3689
|
+
run: task.run
|
|
3690
|
+
});
|
|
3691
|
+
task.fail({
|
|
3692
|
+
origin: "handler",
|
|
3693
|
+
message: error.message
|
|
3694
|
+
});
|
|
3695
|
+
if (bail) throw error;
|
|
2511
3696
|
return;
|
|
2512
3697
|
}
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
if (task.
|
|
2516
|
-
|
|
3698
|
+
if (await this.#gate(workflow.paused ? workflow.wait() : void 0, task, workflow, controller, runSignal, signal, attempts, owners, attempt, last, bail)) return;
|
|
3699
|
+
if (await this.#gate(task.phase.paused ? task.phase.wait() : void 0, task, workflow, controller, runSignal, signal, attempts, owners, attempt, last, bail)) return;
|
|
3700
|
+
if (await this.#gate(task.paused ? task.wait() : void 0, task, workflow, controller, runSignal, signal, attempts, owners, attempt, last, bail)) return;
|
|
3701
|
+
if (this.#skipping(task, controller, runSignal) || this.#halted(workflow, task.phase)) {
|
|
3702
|
+
this.#settleCancelled(task, workflow, runSignal);
|
|
3703
|
+
return;
|
|
3704
|
+
}
|
|
3705
|
+
if (task.status !== "running") return;
|
|
3706
|
+
const handle = new TaskController(signal, task.snapshot().metadata, task, attempt, () => workflow.results(), (input) => this.#owns(owners, task, attempt) && !signal.aborted ? task.report(input) : failure(new WorkflowError("TRANSITION", `task '${task.id}' attempt '${attempt}' no longer owns activity`, {
|
|
3707
|
+
task: task.id,
|
|
3708
|
+
attempt
|
|
3709
|
+
})), () => this.#owns(owners, task, attempt) && !signal.aborted && task.pulse());
|
|
3710
|
+
let outcome;
|
|
3711
|
+
try {
|
|
3712
|
+
outcome = task.handler === void 0 ? [true, null] : await this.#raceHandler(Promise.resolve(task.handler(handle)), signal, this.#skipping.bind(this, task, controller, runSignal));
|
|
3713
|
+
} catch (error) {
|
|
3714
|
+
if (!this.#owns(owners, task, attempt)) return;
|
|
3715
|
+
if (task.status !== "running" || this.#skipping(task, controller, runSignal)) {
|
|
3716
|
+
this.#settleCancelled(task, workflow, runSignal);
|
|
3717
|
+
return;
|
|
3718
|
+
}
|
|
3719
|
+
if (signal.aborted) {
|
|
3720
|
+
this.#timedOut(owners, task, attempt, last, bail);
|
|
3721
|
+
return;
|
|
3722
|
+
}
|
|
3723
|
+
this.#failed(owners, task, attempt, error, last, bail);
|
|
3724
|
+
return;
|
|
3725
|
+
}
|
|
3726
|
+
if (!this.#owns(owners, task, attempt)) return;
|
|
3727
|
+
if (!outcome[0]) {
|
|
3728
|
+
this.#settleAttempt(task, workflow, controller, runSignal, signal, attempts, owners, attempt, last, bail, outcome[2]);
|
|
3729
|
+
return;
|
|
3730
|
+
}
|
|
3731
|
+
if (task.status !== "running") return;
|
|
3732
|
+
if (this.#skipping(task, controller, runSignal)) {
|
|
3733
|
+
this.#settleCancelled(task, workflow, runSignal);
|
|
2517
3734
|
return;
|
|
2518
3735
|
}
|
|
2519
3736
|
if (signal.aborted) {
|
|
2520
|
-
this.#timedOut(task, last);
|
|
3737
|
+
this.#timedOut(owners, task, attempt, last, bail);
|
|
2521
3738
|
return;
|
|
2522
3739
|
}
|
|
2523
|
-
if (!
|
|
2524
|
-
|
|
2525
|
-
|
|
3740
|
+
if (!this.#owns(owners, task, attempt)) return;
|
|
3741
|
+
try {
|
|
3742
|
+
task.complete(outcome[1]);
|
|
3743
|
+
} catch (error) {
|
|
3744
|
+
if (!this.#owns(owners, task, attempt)) return;
|
|
3745
|
+
if (task.status !== "running") throw error;
|
|
3746
|
+
this.#failed(owners, task, attempt, error, last, bail);
|
|
3747
|
+
}
|
|
3748
|
+
} finally {
|
|
3749
|
+
deadline?.clear();
|
|
3750
|
+
if (persistence !== void 0 && this.#owns(owners, task, attempt) && isTerminalStatus(task.status) && !await persistence.checkpoint("settlement", task, attempt) && this.#stoppable(workflow)) workflow.stop();
|
|
3751
|
+
this.#revoke(owners, task.id, attempt);
|
|
2526
3752
|
}
|
|
2527
3753
|
}
|
|
2528
|
-
#
|
|
2529
|
-
|
|
2530
|
-
task
|
|
3754
|
+
async #gate(wait, task, workflow, controller, runSignal, signal, attempts, owners, attempt, last, bail) {
|
|
3755
|
+
const genuine = wait === void 0 ? void 0 : await this.#raceWait(wait, signal, this.#skipping.bind(this, task, controller, runSignal), workflow, task.phase);
|
|
3756
|
+
return this.#settleAttempt(task, workflow, controller, runSignal, signal, attempts, owners, attempt, last, bail, genuine);
|
|
3757
|
+
}
|
|
3758
|
+
#settleAttempt(task, workflow, controller, runSignal, signal, attempts, owners, attempt, last, bail, genuine) {
|
|
3759
|
+
if (attempts.get(task.id) !== attempt || !this.#owns(owners, task, attempt)) return true;
|
|
3760
|
+
if (signal.aborted) {
|
|
3761
|
+
if (genuine ?? this.#skipping(task, controller, runSignal)) this.#settleCancelled(task, workflow, runSignal);
|
|
3762
|
+
else this.#timedOut(owners, task, attempt, last, bail);
|
|
3763
|
+
return true;
|
|
3764
|
+
}
|
|
3765
|
+
if (this.#skipping(task, controller, runSignal) || this.#halted(workflow, task.phase)) {
|
|
3766
|
+
this.#settleCancelled(task, workflow, runSignal);
|
|
3767
|
+
return true;
|
|
3768
|
+
}
|
|
3769
|
+
return task.status !== "running";
|
|
3770
|
+
}
|
|
3771
|
+
async #raceHandler(handler, signal, cancelled) {
|
|
3772
|
+
if (signal.aborted) return [
|
|
3773
|
+
false,
|
|
3774
|
+
void 0,
|
|
3775
|
+
cancelled()
|
|
3776
|
+
];
|
|
3777
|
+
const deferred = Promise.withResolvers();
|
|
3778
|
+
const onAbort = this.#resolveHandlerAbort.bind(this, deferred, cancelled);
|
|
3779
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
3780
|
+
try {
|
|
3781
|
+
return await Promise.race([handler.then((value) => [true, value]), deferred.promise]);
|
|
3782
|
+
} finally {
|
|
3783
|
+
signal.removeEventListener("abort", onAbort);
|
|
3784
|
+
}
|
|
2531
3785
|
}
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
3786
|
+
#resolveHandlerAbort(deferred, cancelled) {
|
|
3787
|
+
deferred.resolve([
|
|
3788
|
+
false,
|
|
3789
|
+
void 0,
|
|
3790
|
+
cancelled()
|
|
3791
|
+
]);
|
|
3792
|
+
}
|
|
3793
|
+
#timedOut(owners, task, attempt, last, bail) {
|
|
3794
|
+
if (!this.#owns(owners, task, attempt)) return;
|
|
3795
|
+
const error = /* @__PURE__ */ new Error(`task '${task.id}' timed out`);
|
|
3796
|
+
if (last) task.fail({
|
|
3797
|
+
origin: "timeout",
|
|
3798
|
+
message: error.message
|
|
2538
3799
|
});
|
|
3800
|
+
if (!last || bail) throw error;
|
|
3801
|
+
}
|
|
3802
|
+
#failed(owners, task, attempt, error, last, bail) {
|
|
3803
|
+
if (!this.#owns(owners, task, attempt)) return;
|
|
3804
|
+
if (!last) throw error;
|
|
3805
|
+
task.fail({
|
|
3806
|
+
origin: "handler",
|
|
3807
|
+
message: errorToMessage(error)
|
|
3808
|
+
});
|
|
3809
|
+
if (bail) throw error;
|
|
3810
|
+
}
|
|
3811
|
+
#owns(owners, task, attempt) {
|
|
3812
|
+
return owners.get(task.id) === attempt && task.attempts === attempt;
|
|
3813
|
+
}
|
|
3814
|
+
#revoke(owners, id, attempt) {
|
|
3815
|
+
if (owners.get(id) === attempt) owners.delete(id);
|
|
3816
|
+
}
|
|
3817
|
+
async #raceWait(wait, signal, cancelled, workflow, phase) {
|
|
3818
|
+
if (signal.aborted) return cancelled?.();
|
|
3819
|
+
const deferred = Promise.withResolvers();
|
|
3820
|
+
const onAbort = this.#resolveWaitAbort.bind(this, deferred, cancelled);
|
|
3821
|
+
const onTerminal = this.#resolveWaitAbort.bind(this, deferred, void 0);
|
|
3822
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
3823
|
+
workflow?.emitter.on("skip", onTerminal);
|
|
3824
|
+
workflow?.emitter.on("stop", onTerminal);
|
|
3825
|
+
phase?.emitter.on("skip", onTerminal);
|
|
3826
|
+
phase?.emitter.on("stop", onTerminal);
|
|
2539
3827
|
try {
|
|
2540
|
-
|
|
3828
|
+
if (workflow !== void 0 && this.#halted(workflow, phase)) deferred.resolve(void 0);
|
|
3829
|
+
const outcome = await Promise.race([wait, deferred.promise]);
|
|
3830
|
+
return typeof outcome === "boolean" ? outcome : void 0;
|
|
2541
3831
|
} finally {
|
|
2542
|
-
|
|
3832
|
+
signal.removeEventListener("abort", onAbort);
|
|
3833
|
+
workflow?.emitter.off("skip", onTerminal);
|
|
3834
|
+
workflow?.emitter.off("stop", onTerminal);
|
|
3835
|
+
phase?.emitter.off("skip", onTerminal);
|
|
3836
|
+
phase?.emitter.off("stop", onTerminal);
|
|
2543
3837
|
}
|
|
2544
3838
|
}
|
|
2545
|
-
#
|
|
2546
|
-
|
|
3839
|
+
#resolveWaitAbort(deferred, cancelled) {
|
|
3840
|
+
deferred.resolve(cancelled?.());
|
|
3841
|
+
}
|
|
3842
|
+
#taskSignal(task, unitSignal, runSignal, timeout) {
|
|
3843
|
+
const signals = [
|
|
3844
|
+
task.signal,
|
|
3845
|
+
unitSignal,
|
|
3846
|
+
runSignal
|
|
3847
|
+
];
|
|
3848
|
+
if (timeout !== void 0) signals.push(timeout.signal);
|
|
3849
|
+
return AbortSignal.any(signals);
|
|
2547
3850
|
}
|
|
2548
|
-
#fold(workflow,
|
|
3851
|
+
#fold(workflow, signal, budget, timeout) {
|
|
2549
3852
|
const signals = [workflow.signal];
|
|
2550
|
-
if (
|
|
3853
|
+
if (signal !== void 0) signals.push(signal);
|
|
2551
3854
|
if (timeout !== void 0) signals.push(timeout.signal);
|
|
2552
|
-
if (
|
|
3855
|
+
if (budget !== void 0) signals.push(budget.signal);
|
|
2553
3856
|
return signals.length === 1 ? workflow.signal : AbortSignal.any(signals);
|
|
2554
3857
|
}
|
|
2555
3858
|
#haltFrom(phases, index, workflow, runSignal) {
|
|
@@ -2563,22 +3866,22 @@ var WorkflowRunner = class {
|
|
|
2563
3866
|
for (const task of phase.tasks.tasks()) this.#skip(task);
|
|
2564
3867
|
}
|
|
2565
3868
|
}
|
|
2566
|
-
#
|
|
3869
|
+
#settleCancelled(task, workflow, runSignal) {
|
|
2567
3870
|
if (this.#cancelled(runSignal) && this.#stoppable(workflow)) workflow.stop();
|
|
2568
3871
|
this.#skip(task);
|
|
2569
3872
|
}
|
|
2570
3873
|
#skip(task) {
|
|
2571
3874
|
if (task.status === "pending" || task.status === "running") task.skip();
|
|
2572
3875
|
}
|
|
2573
|
-
#skipping(controller, runSignal) {
|
|
2574
|
-
return controller.aborted || runSignal.aborted;
|
|
3876
|
+
#skipping(task, controller, runSignal) {
|
|
3877
|
+
return task.signal.aborted || controller.aborted || runSignal.aborted;
|
|
2575
3878
|
}
|
|
2576
3879
|
#cancelled(runSignal) {
|
|
2577
3880
|
return runSignal.aborted;
|
|
2578
3881
|
}
|
|
2579
|
-
#halted(workflow) {
|
|
3882
|
+
#halted(workflow, phase) {
|
|
2580
3883
|
const status = workflow.status;
|
|
2581
|
-
return status === "failed" || status === "skipped" || status === "stopped";
|
|
3884
|
+
return status === "failed" || status === "skipped" || status === "stopped" || phase?.status === "skipped" || phase?.status === "stopped";
|
|
2582
3885
|
}
|
|
2583
3886
|
#stoppable(workflow) {
|
|
2584
3887
|
const status = workflow.status;
|
|
@@ -2612,7 +3915,7 @@ var WorkflowRunner = class {
|
|
|
2612
3915
|
*
|
|
2613
3916
|
* @example
|
|
2614
3917
|
* ```ts
|
|
2615
|
-
* import { createWorkflowContract } from '@
|
|
3918
|
+
* import { createWorkflowContract } from '@orkestrel/workflow'
|
|
2616
3919
|
*
|
|
2617
3920
|
* const contract = createWorkflowContract()
|
|
2618
3921
|
* const definition = contract.generate() // a valid WorkflowDefinition
|
|
@@ -2641,8 +3944,8 @@ function createWorkflowContract() {
|
|
|
2641
3944
|
*
|
|
2642
3945
|
* `options.functions` is the {@link import('./types.js').WorkflowFunctions} registry each live
|
|
2643
3946
|
* task's `run` name resolves against ONCE at construction into its runtime
|
|
2644
|
-
* {@link import('./types.js').TaskInterface.handler}
|
|
2645
|
-
*
|
|
3947
|
+
* {@link import('./types.js').TaskInterface.handler}. An omitted name is the deliberate no-op;
|
|
3948
|
+
* an unresolved present name remains inspectable but is rejected if execution is attempted.
|
|
2646
3949
|
*
|
|
2647
3950
|
* @param definition - The workflow definition to bring to life
|
|
2648
3951
|
* @param options - Runtime options (initial listeners, `bail` override, per-node options)
|
|
@@ -2650,7 +3953,7 @@ function createWorkflowContract() {
|
|
|
2650
3953
|
*
|
|
2651
3954
|
* @example
|
|
2652
3955
|
* ```ts
|
|
2653
|
-
* import { createWorkflow } from '@
|
|
3956
|
+
* import { createWorkflow } from '@orkestrel/workflow'
|
|
2654
3957
|
*
|
|
2655
3958
|
* const workflow = createWorkflow(definition, { on: { complete: () => done() } })
|
|
2656
3959
|
* const phase = workflow.phase('phase-build')
|
|
@@ -2658,7 +3961,8 @@ function createWorkflowContract() {
|
|
|
2658
3961
|
* ```
|
|
2659
3962
|
*/
|
|
2660
3963
|
function createWorkflow(definition, options) {
|
|
2661
|
-
|
|
3964
|
+
const captured = captureWorkflowOptions(options);
|
|
3965
|
+
return new Workflow(definitionToSnapshot(definition, captured.bail ?? definition.bail ?? false), captured);
|
|
2662
3966
|
}
|
|
2663
3967
|
/**
|
|
2664
3968
|
* Rebuild an equivalent live W-b entity tree from a {@link WorkflowSnapshot} — the
|
|
@@ -2676,6 +3980,9 @@ function createWorkflow(definition, options) {
|
|
|
2676
3980
|
* still wins when supplied (to deliberately re-run under a different policy). A structurally
|
|
2677
3981
|
* invalid snapshot (a status — or override — outside the lifecycle vocabulary, or a
|
|
2678
3982
|
* non-boolean `bail`) throws a `RESTORE` {@link WorkflowError}.
|
|
3983
|
+
* Runtime handlers are optional: without a matching `functions` entry, a persisted `run`
|
|
3984
|
+
* remains visible with an undefined `handler` so the exact state is inspectable. The runner
|
|
3985
|
+
* rejects that unresolved tree if execution is attempted.
|
|
2679
3986
|
*
|
|
2680
3987
|
* @param snapshot - The snapshot to restore (carries its own `bail` + `override`)
|
|
2681
3988
|
* @param options - Runtime options (initial listeners, an optional `bail` override, per-node options)
|
|
@@ -2683,15 +3990,35 @@ function createWorkflow(definition, options) {
|
|
|
2683
3990
|
*
|
|
2684
3991
|
* @example
|
|
2685
3992
|
* ```ts
|
|
2686
|
-
* import { restoreWorkflow } from '@
|
|
3993
|
+
* import { restoreWorkflow } from '@orkestrel/workflow'
|
|
2687
3994
|
*
|
|
2688
3995
|
* const restored = restoreWorkflow(workflow.snapshot()) // bail comes from the snapshot
|
|
2689
3996
|
* restored.status === workflow.status // true
|
|
2690
3997
|
* ```
|
|
2691
3998
|
*/
|
|
2692
3999
|
function restoreWorkflow(snapshot, options) {
|
|
2693
|
-
|
|
2694
|
-
return new Workflow(snapshot,
|
|
4000
|
+
const captured = captureWorkflowOptions(options);
|
|
4001
|
+
return new Workflow(cloneWorkflowSnapshot(snapshot), captured);
|
|
4002
|
+
}
|
|
4003
|
+
/**
|
|
4004
|
+
* Rebuild an interrupted workflow at its remaining retry budget.
|
|
4005
|
+
*
|
|
4006
|
+
* @remarks
|
|
4007
|
+
* Each phase captures every unique initial `run` binding once before constructing tasks. Recovery
|
|
4008
|
+
* validates those live tasks' captured callable handlers without rereading the registry, while the
|
|
4009
|
+
* retained registry identity remains available to resolve future live additions at their mint time.
|
|
4010
|
+
*
|
|
4011
|
+
* @param snapshot - The hostile persisted snapshot
|
|
4012
|
+
* @param options - Runtime handlers and entity options
|
|
4013
|
+
* @returns A recoverable live workflow
|
|
4014
|
+
*/
|
|
4015
|
+
function recoverWorkflow(snapshot, options) {
|
|
4016
|
+
const captured = captureWorkflowOptions(options);
|
|
4017
|
+
const owned = cloneWorkflowSnapshot(snapshot);
|
|
4018
|
+
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 });
|
|
4019
|
+
const workflow = new Workflow(cloneWorkflowSnapshot(recoverWorkflowSnapshot(owned)), captured);
|
|
4020
|
+
if (!hasWorkflowHandlers(workflow)) throw new WorkflowError("RESTORE", `workflow '${owned.id}' has an unresolved run`, { workflow: owned.id });
|
|
4021
|
+
return workflow;
|
|
2695
4022
|
}
|
|
2696
4023
|
/**
|
|
2697
4024
|
* Assert that a {@link WorkflowSnapshot} carries a `boolean` `bail` — at the workflow tier AND
|
|
@@ -2714,54 +4041,7 @@ function restoreWorkflow(snapshot, options) {
|
|
|
2714
4041
|
* @param snapshot - The snapshot to validate
|
|
2715
4042
|
*/
|
|
2716
4043
|
function assertSnapshot(snapshot) {
|
|
2717
|
-
|
|
2718
|
-
workflow: snapshot.id,
|
|
2719
|
-
bail: snapshot.bail
|
|
2720
|
-
});
|
|
2721
|
-
if (!WORKFLOW_STATUSES.includes(snapshot.status)) throw new WorkflowError("RESTORE", `workflow '${snapshot.id}' has an invalid status`, {
|
|
2722
|
-
workflow: snapshot.id,
|
|
2723
|
-
status: snapshot.status
|
|
2724
|
-
});
|
|
2725
|
-
if (snapshot.override !== void 0 && !WORKFLOW_STATUSES.includes(snapshot.override)) throw new WorkflowError("RESTORE", `workflow '${snapshot.id}' has an invalid override`, {
|
|
2726
|
-
workflow: snapshot.id,
|
|
2727
|
-
override: snapshot.override
|
|
2728
|
-
});
|
|
2729
|
-
for (const phase of snapshot.phases) {
|
|
2730
|
-
if (typeof phase.bail !== "boolean") throw new WorkflowError("RESTORE", `phase '${phase.id}' has a non-boolean bail`, {
|
|
2731
|
-
phase: phase.id,
|
|
2732
|
-
bail: phase.bail
|
|
2733
|
-
});
|
|
2734
|
-
if (!PHASE_STATUSES.includes(phase.status)) throw new WorkflowError("RESTORE", `phase '${phase.id}' has an invalid status`, {
|
|
2735
|
-
phase: phase.id,
|
|
2736
|
-
status: phase.status
|
|
2737
|
-
});
|
|
2738
|
-
if (phase.override !== void 0 && !PHASE_STATUSES.includes(phase.override)) throw new WorkflowError("RESTORE", `phase '${phase.id}' has an invalid override`, {
|
|
2739
|
-
phase: phase.id,
|
|
2740
|
-
override: phase.override
|
|
2741
|
-
});
|
|
2742
|
-
if (phase.concurrency !== void 0 && (!Number.isInteger(phase.concurrency) || phase.concurrency < 1)) throw new WorkflowError("RESTORE", `phase '${phase.id}' has an invalid concurrency`, {
|
|
2743
|
-
phase: phase.id,
|
|
2744
|
-
concurrency: phase.concurrency
|
|
2745
|
-
});
|
|
2746
|
-
for (const task of phase.tasks) {
|
|
2747
|
-
if (!TASK_STATUSES.includes(task.status)) throw new WorkflowError("RESTORE", `task '${task.id}' has an invalid status`, {
|
|
2748
|
-
task: task.id,
|
|
2749
|
-
status: task.status
|
|
2750
|
-
});
|
|
2751
|
-
if (task.run !== void 0 && task.run.length < 1) throw new WorkflowError("RESTORE", `task '${task.id}' has an invalid run`, {
|
|
2752
|
-
task: task.id,
|
|
2753
|
-
run: task.run
|
|
2754
|
-
});
|
|
2755
|
-
if (task.retries !== void 0 && (!Number.isInteger(task.retries) || task.retries < 0)) throw new WorkflowError("RESTORE", `task '${task.id}' has an invalid retries`, {
|
|
2756
|
-
task: task.id,
|
|
2757
|
-
retries: task.retries
|
|
2758
|
-
});
|
|
2759
|
-
if (task.timeout !== void 0 && (!Number.isInteger(task.timeout) || task.timeout < 0)) throw new WorkflowError("RESTORE", `task '${task.id}' has an invalid timeout`, {
|
|
2760
|
-
task: task.id,
|
|
2761
|
-
timeout: task.timeout
|
|
2762
|
-
});
|
|
2763
|
-
}
|
|
2764
|
-
}
|
|
4044
|
+
cloneWorkflowSnapshot(snapshot);
|
|
2765
4045
|
}
|
|
2766
4046
|
/**
|
|
2767
4047
|
* Create the in-memory durable {@link WorkflowStoreInterface} — a process-lifetime
|
|
@@ -2770,7 +4050,7 @@ function assertSnapshot(snapshot) {
|
|
|
2770
4050
|
*
|
|
2771
4051
|
* @remarks
|
|
2772
4052
|
* The snapshot analogue of the server package's `createMemorySessionStore`
|
|
2773
|
-
* (and the
|
|
4053
|
+
* (and the `createMemoryQueueStore` family), but LEANER — there is no idle-TTL, so no
|
|
2774
4054
|
* options bag (AGENTS §21 minimal): a persisted run-state lives until an explicit `delete`. This is
|
|
2775
4055
|
* the zero-plumbing DEFAULT (a plain `Map`); its driver-pluggable twin is
|
|
2776
4056
|
* {@link createDatabaseWorkflowStore} (the snapshot as one opaque JSON column over a `databases`
|
|
@@ -2782,7 +4062,7 @@ function assertSnapshot(snapshot) {
|
|
|
2782
4062
|
*
|
|
2783
4063
|
* @example
|
|
2784
4064
|
* ```ts
|
|
2785
|
-
* import { createMemoryWorkflowStore, createWorkflow, restoreWorkflow } from '@
|
|
4065
|
+
* import { createMemoryWorkflowStore, createWorkflow, restoreWorkflow } from '@orkestrel/workflow'
|
|
2786
4066
|
*
|
|
2787
4067
|
* const store = createMemoryWorkflowStore()
|
|
2788
4068
|
* const workflow = createWorkflow(definition)
|
|
@@ -2802,7 +4082,7 @@ function createMemoryWorkflowStore() {
|
|
|
2802
4082
|
* @remarks
|
|
2803
4083
|
* Builds a one-table database (`snapshots`, keyed by `id`) over the supplied driver, the snapshot
|
|
2804
4084
|
* held as ONE OPAQUE JSON COLUMN — the column map is `{ id; snapshot }` where `snapshot` is a
|
|
2805
|
-
* `rawShape` (a JSON blob), exactly as
|
|
4085
|
+
* `rawShape` (a JSON blob), exactly as `createDatabaseQueueStore` stores its `input`. The
|
|
2806
4086
|
* snapshot is already a COMPLETE, self-contained, pure-JSON payload, so storing it whole is lossless
|
|
2807
4087
|
* AND keeps the row type FLAT — a structured multi-column snapshot table would force the contract to
|
|
2808
4088
|
* `Infer` the deeply-nested snapshot shape (workflow → phases → tasks → results) and trip TS2589;
|
|
@@ -2818,7 +4098,8 @@ function createMemoryWorkflowStore() {
|
|
|
2818
4098
|
*
|
|
2819
4099
|
* @example
|
|
2820
4100
|
* ```ts
|
|
2821
|
-
* import {
|
|
4101
|
+
* import { createMemoryDriver } from '@orkestrel/database'
|
|
4102
|
+
* import { createDatabaseWorkflowStore, createWorkflow, restoreWorkflow } from '@orkestrel/workflow'
|
|
2822
4103
|
*
|
|
2823
4104
|
* const store = createDatabaseWorkflowStore(createMemoryDriver()) // a durable driver swaps in here
|
|
2824
4105
|
* const workflow = createWorkflow(definition)
|
|
@@ -2828,12 +4109,13 @@ function createMemoryWorkflowStore() {
|
|
|
2828
4109
|
* ```
|
|
2829
4110
|
*/
|
|
2830
4111
|
function createDatabaseWorkflowStore(driver = (0, _orkestrel_database.createMemoryDriver)()) {
|
|
4112
|
+
const columns = {
|
|
4113
|
+
id: (0, _orkestrel_contract.stringShape)(),
|
|
4114
|
+
snapshot: (0, _orkestrel_contract.rawShape)({})
|
|
4115
|
+
};
|
|
2831
4116
|
return new DatabaseWorkflowStore((0, _orkestrel_database.createDatabase)({
|
|
2832
4117
|
driver,
|
|
2833
|
-
tables: { snapshots:
|
|
2834
|
-
id: (0, _orkestrel_contract.stringShape)(),
|
|
2835
|
-
snapshot: (0, _orkestrel_contract.rawShape)({})
|
|
2836
|
-
} }
|
|
4118
|
+
tables: { snapshots: columns }
|
|
2837
4119
|
}).table("snapshots"));
|
|
2838
4120
|
}
|
|
2839
4121
|
/**
|
|
@@ -2843,7 +4125,7 @@ function createDatabaseWorkflowStore(driver = (0, _orkestrel_database.createMemo
|
|
|
2843
4125
|
*
|
|
2844
4126
|
* @remarks
|
|
2845
4127
|
* The runner is a PURE engine — it re-implements no concurrency / retry / abort logic, AND it
|
|
2846
|
-
* carries no
|
|
4128
|
+
* carries no behavior or provider registry of its own: each live task already
|
|
2847
4129
|
* resolved its own {@link import('./types.js').WorkflowFunction} into
|
|
2848
4130
|
* {@link import('./types.js').TaskInterface.handler} ONCE at construction, from the
|
|
2849
4131
|
* {@link WorkflowOptions.functions} registry supplied to `execute` / {@link createWorkflow}.
|
|
@@ -2857,11 +4139,10 @@ function createDatabaseWorkflowStore(driver = (0, _orkestrel_database.createMemo
|
|
|
2857
4139
|
* the live entity (`start` → `complete` / `fail`), and resolves a
|
|
2858
4140
|
* {@link import('./types.js').WorkflowResult}.
|
|
2859
4141
|
*
|
|
2860
|
-
*
|
|
2861
|
-
* {@link import('./types.js').WorkflowFunction} into its
|
|
2862
|
-
* registry
|
|
2863
|
-
*
|
|
2864
|
-
* (the ROADMAP no-handler rule).
|
|
4142
|
+
* External integrations remain application-owned: a caller wires an ordinary
|
|
4143
|
+
* {@link import('./types.js').WorkflowFunction} into its own {@link WorkflowOptions.functions}
|
|
4144
|
+
* registry. Only a task that omits `run` auto-completes; unresolved named work is rejected
|
|
4145
|
+
* before dispatch.
|
|
2865
4146
|
*
|
|
2866
4147
|
* @param options - An optional pacing `scheduler` (default the shipped cross-environment one).
|
|
2867
4148
|
* See {@link WorkflowRunnerOptions}.
|
|
@@ -2869,7 +4150,7 @@ function createDatabaseWorkflowStore(driver = (0, _orkestrel_database.createMemo
|
|
|
2869
4150
|
*
|
|
2870
4151
|
* @example
|
|
2871
4152
|
* ```ts
|
|
2872
|
-
* import { createWorkflowRunner } from '@
|
|
4153
|
+
* import { createWorkflowRunner } from '@orkestrel/workflow'
|
|
2873
4154
|
*
|
|
2874
4155
|
* const runner = createWorkflowRunner()
|
|
2875
4156
|
* const definition = { id: 'w', name: 'W', phases: [{ id: 'p', name: 'P', tasks: [
|
|
@@ -2905,7 +4186,7 @@ function createWorkflowRunner(options) {
|
|
|
2905
4186
|
*
|
|
2906
4187
|
* @example
|
|
2907
4188
|
* ```ts
|
|
2908
|
-
* import { createMemoryWorkflowStore, createWorkflowManager } from '@
|
|
4189
|
+
* import { createMemoryWorkflowStore, createWorkflowManager } from '@orkestrel/workflow'
|
|
2909
4190
|
*
|
|
2910
4191
|
* const manager = createWorkflowManager({
|
|
2911
4192
|
* store: createMemoryWorkflowStore(),
|
|
@@ -2928,7 +4209,8 @@ function createWorkflowManager(options) {
|
|
|
2928
4209
|
* `yield()` gives the host a turn via a zero-delay macrotask (so pending I/O,
|
|
2929
4210
|
* timers, and rendering actually run — a microtask would not); `delay(ms)` resumes
|
|
2930
4211
|
* after at least `ms`. Pass `options.signal` to make a pending yield/delay reject
|
|
2931
|
-
* with the signal's `reason
|
|
4212
|
+
* with the signal's exact `reason`; the shared owned-signal lifecycle clears the timer
|
|
4213
|
+
* without invoking caller-owned listener methods.
|
|
2932
4214
|
* `options.priority` is accepted for contract compliance but treated uniformly by
|
|
2933
4215
|
* this default — environment backends honour it.
|
|
2934
4216
|
*
|
|
@@ -2936,7 +4218,8 @@ function createWorkflowManager(options) {
|
|
|
2936
4218
|
*
|
|
2937
4219
|
* @example
|
|
2938
4220
|
* ```ts
|
|
2939
|
-
* import { createAbort
|
|
4221
|
+
* import { createAbort } from '@orkestrel/abort'
|
|
4222
|
+
* import { createScheduler } from '@orkestrel/workflow'
|
|
2940
4223
|
*
|
|
2941
4224
|
* const abort = createAbort()
|
|
2942
4225
|
* const scheduler = createScheduler()
|
|
@@ -2950,7 +4233,7 @@ function createWorkflowManager(options) {
|
|
|
2950
4233
|
*
|
|
2951
4234
|
* @example
|
|
2952
4235
|
* ```ts
|
|
2953
|
-
* import { createScheduler } from '@
|
|
4236
|
+
* import { createScheduler } from '@orkestrel/workflow'
|
|
2954
4237
|
*
|
|
2955
4238
|
* // A backoff: wait a growing interval between retries.
|
|
2956
4239
|
* const scheduler = createScheduler()
|
|
@@ -2992,7 +4275,7 @@ function createScheduler() {
|
|
|
2992
4275
|
*
|
|
2993
4276
|
* @example
|
|
2994
4277
|
* ```ts
|
|
2995
|
-
* import { createRunner } from '@
|
|
4278
|
+
* import { createRunner } from '@orkestrel/workflow'
|
|
2996
4279
|
*
|
|
2997
4280
|
* // A handler that fans out one sibling per declared unit, then returns its own value.
|
|
2998
4281
|
* const runner = createRunner<number, number>({
|
|
@@ -3015,6 +4298,7 @@ exports.Controller = Controller;
|
|
|
3015
4298
|
exports.DEFAULT_BAIL = DEFAULT_BAIL;
|
|
3016
4299
|
exports.DEFAULT_PHASE_CONCURRENCY = DEFAULT_PHASE_CONCURRENCY;
|
|
3017
4300
|
exports.DatabaseWorkflowStore = DatabaseWorkflowStore;
|
|
4301
|
+
exports.MAX_TIMER_MS = MAX_TIMER_MS;
|
|
3018
4302
|
exports.MemoryWorkflowStore = MemoryWorkflowStore;
|
|
3019
4303
|
exports.PHASE_STATUSES = PHASE_STATUSES;
|
|
3020
4304
|
exports.Phase = Phase;
|
|
@@ -3031,12 +4315,16 @@ exports.WORKFLOW_STATUSES = WORKFLOW_STATUSES;
|
|
|
3031
4315
|
exports.Workflow = Workflow;
|
|
3032
4316
|
exports.WorkflowError = WorkflowError;
|
|
3033
4317
|
exports.WorkflowManager = WorkflowManager;
|
|
4318
|
+
exports.WorkflowPersistence = WorkflowPersistence;
|
|
3034
4319
|
exports.WorkflowRunner = WorkflowRunner;
|
|
3035
4320
|
exports.assertSnapshot = assertSnapshot;
|
|
3036
4321
|
exports.buildPhaseContext = buildPhaseContext;
|
|
3037
4322
|
exports.buildTaskContext = buildTaskContext;
|
|
3038
4323
|
exports.buildWorkflowContext = buildWorkflowContext;
|
|
3039
4324
|
exports.canTransitionTask = canTransitionTask;
|
|
4325
|
+
exports.captureWorkflowOptions = captureWorkflowOptions;
|
|
4326
|
+
exports.cloneTaskActivity = cloneTaskActivity;
|
|
4327
|
+
exports.cloneWorkflowSnapshot = cloneWorkflowSnapshot;
|
|
3040
4328
|
exports.collectResults = collectResults;
|
|
3041
4329
|
exports.createDatabaseWorkflowStore = createDatabaseWorkflowStore;
|
|
3042
4330
|
exports.createDeferred = createDeferred;
|
|
@@ -3051,22 +4339,36 @@ exports.definitionToSnapshot = definitionToSnapshot;
|
|
|
3051
4339
|
exports.deriveBoundary = deriveBoundary;
|
|
3052
4340
|
exports.derivePhaseStatus = derivePhaseStatus;
|
|
3053
4341
|
exports.deriveWorkflowStatus = deriveWorkflowStatus;
|
|
4342
|
+
exports.errorToMessage = errorToMessage;
|
|
3054
4343
|
exports.failure = failure;
|
|
3055
4344
|
exports.findFailure = findFailure;
|
|
4345
|
+
exports.hasWorkflowHandlers = hasWorkflowHandlers;
|
|
3056
4346
|
exports.insertEntry = insertEntry;
|
|
4347
|
+
exports.isLifecycleStatus = isLifecycleStatus;
|
|
4348
|
+
exports.isOwnedWorkflowSnapshot = isOwnedWorkflowSnapshot;
|
|
4349
|
+
exports.isTaskActivity = isTaskActivity;
|
|
4350
|
+
exports.isTaskActivityInput = isTaskActivityInput;
|
|
4351
|
+
exports.isTaskFailure = isTaskFailure;
|
|
4352
|
+
exports.isTaskResult = isTaskResult;
|
|
3057
4353
|
exports.isTerminalStatus = isTerminalStatus;
|
|
3058
4354
|
exports.isWorkflowError = isWorkflowError;
|
|
3059
4355
|
exports.isWorkflowSnapshot = isWorkflowSnapshot;
|
|
4356
|
+
exports.matchesDescription = matchesDescription;
|
|
3060
4357
|
exports.moveEntry = moveEntry;
|
|
3061
4358
|
exports.parkSignal = parkSignal;
|
|
3062
4359
|
exports.phaseDefinitionToSnapshot = phaseDefinitionToSnapshot;
|
|
3063
4360
|
exports.phaseShape = phaseShape;
|
|
3064
4361
|
exports.phaseUpdateShape = phaseUpdateShape;
|
|
4362
|
+
exports.recoverWorkflow = recoverWorkflow;
|
|
4363
|
+
exports.recoverWorkflowSnapshot = recoverWorkflowSnapshot;
|
|
4364
|
+
exports.resolveTaskSilence = resolveTaskSilence;
|
|
3065
4365
|
exports.restoreWorkflow = restoreWorkflow;
|
|
4366
|
+
exports.scheduleHost = scheduleHost;
|
|
3066
4367
|
exports.success = success;
|
|
3067
4368
|
exports.taskDefinitionToSnapshot = taskDefinitionToSnapshot;
|
|
3068
4369
|
exports.taskShape = taskShape;
|
|
3069
4370
|
exports.taskUpdateShape = taskUpdateShape;
|
|
3070
4371
|
exports.workflowShape = workflowShape;
|
|
4372
|
+
exports.workflowSnapshotContext = workflowSnapshotContext;
|
|
3071
4373
|
|
|
3072
4374
|
//# sourceMappingURL=index.cjs.map
|