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