@orkestrel/workflow 0.0.10 → 0.0.12
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/dist/src/core/index.cjs +294 -296
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +114 -109
- package/dist/src/core/index.d.ts +114 -109
- package/dist/src/core/index.js +294 -295
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.d.cts +2 -2
- package/dist/src/server/index.d.ts +2 -2
- package/package.json +16 -15
package/dist/src/core/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createAbort, linkSignal } from "@orkestrel/abort";
|
|
1
|
+
import { createAbort, isAbortSignal, linkSignal } from "@orkestrel/abort";
|
|
2
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";
|
|
3
3
|
import { createDatabase, createMemoryDriver } from "@orkestrel/database";
|
|
4
4
|
import { Emitter } from "@orkestrel/emitter";
|
|
@@ -109,6 +109,204 @@ var DEFAULT_PHASE_CONCURRENCY = 1024;
|
|
|
109
109
|
*/
|
|
110
110
|
var MAX_TIMER_MS = 2147483647;
|
|
111
111
|
//#endregion
|
|
112
|
+
//#region src/core/errors.ts
|
|
113
|
+
/**
|
|
114
|
+
* An error raised by the workflow runtime.
|
|
115
|
+
*
|
|
116
|
+
* @remarks
|
|
117
|
+
* Carries a {@link WorkflowErrorCode} and an optional `context` bag naming the
|
|
118
|
+
* offending node id / status / parameter. Raised for an illegal lifecycle transition
|
|
119
|
+
* (`TRANSITION`), a structurally invalid {@link import('./types.js').WorkflowSnapshot}
|
|
120
|
+
* boundary (`RESTORE`), a refused structural/activity edit (`MUTATION`), or a host
|
|
121
|
+
* schedule refused before arming because the caller's `signal` is not a native
|
|
122
|
+
* `AbortSignal` (`SCHEDULE`, delivered as a rejected promise).
|
|
123
|
+
*/
|
|
124
|
+
var WorkflowError = class extends Error {
|
|
125
|
+
code;
|
|
126
|
+
context;
|
|
127
|
+
constructor(code, message, context) {
|
|
128
|
+
super(message);
|
|
129
|
+
this.name = "WorkflowError";
|
|
130
|
+
this.code = code;
|
|
131
|
+
if (context !== void 0) this.context = context;
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
/**
|
|
135
|
+
* Narrow an unknown caught value to a {@link WorkflowError}.
|
|
136
|
+
*
|
|
137
|
+
* @param value - The value to test (typically a `catch` binding)
|
|
138
|
+
* @returns `true` when `value` is a {@link WorkflowError}
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* ```ts
|
|
142
|
+
* try {
|
|
143
|
+
* task.complete('done')
|
|
144
|
+
* } catch (error) {
|
|
145
|
+
* if (isWorkflowError(error) && error.code === 'TRANSITION') retry()
|
|
146
|
+
* }
|
|
147
|
+
* ```
|
|
148
|
+
*/
|
|
149
|
+
function isWorkflowError(value) {
|
|
150
|
+
try {
|
|
151
|
+
return value instanceof WorkflowError;
|
|
152
|
+
} catch {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
//#endregion
|
|
157
|
+
//#region src/core/validators.ts
|
|
158
|
+
/** Test the workflow lifecycle vocabulary. */
|
|
159
|
+
function isLifecycleStatus(value) {
|
|
160
|
+
return value === "pending" || value === "running" || value === "completed" || value === "failed" || value === "skipped" || value === "stopped";
|
|
161
|
+
}
|
|
162
|
+
/** Test a normalized persisted task failure. */
|
|
163
|
+
function isTaskFailure(value) {
|
|
164
|
+
try {
|
|
165
|
+
return isRecord(value) && Object.keys(value).every((key) => key === "origin" || key === "message") && (value.origin === "handler" || value.origin === "timeout" || value.origin === "recovery") && isNonEmptyString(value.message);
|
|
166
|
+
} catch {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Validate a safe owned JSON graph as a coherent workflow snapshot.
|
|
172
|
+
*
|
|
173
|
+
* @remarks
|
|
174
|
+
* Callers at hostile boundaries use {@link isWorkflowSnapshot}, which owns the
|
|
175
|
+
* graph first so this semantic pass never observes accessors or prototypes.
|
|
176
|
+
*/
|
|
177
|
+
function isOwnedWorkflowSnapshot(value) {
|
|
178
|
+
try {
|
|
179
|
+
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;
|
|
180
|
+
const phaseIds = /* @__PURE__ */ new Set();
|
|
181
|
+
const derivations = [];
|
|
182
|
+
let frontier = false;
|
|
183
|
+
let running = false;
|
|
184
|
+
let vacuous = true;
|
|
185
|
+
for (const phase of value.phases) {
|
|
186
|
+
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;
|
|
187
|
+
const forced = phase.override === "skipped" || phase.override === "stopped";
|
|
188
|
+
const started = phase.status === "running" || phase.status === "completed" || phase.status === "failed";
|
|
189
|
+
if (!forced && frontier && started || phase.status === "running" && running) return false;
|
|
190
|
+
if (phase.status === "running") running = true;
|
|
191
|
+
if (!forced && (phase.status === "pending" || phase.status === "running" || phase.status === "failed" && phase.bail)) frontier = true;
|
|
192
|
+
phaseIds.add(phase.id);
|
|
193
|
+
const taskIds = /* @__PURE__ */ new Set();
|
|
194
|
+
const statuses = [];
|
|
195
|
+
if (phase.tasks.length > 0) vacuous = false;
|
|
196
|
+
for (const task of phase.tasks) {
|
|
197
|
+
if (!isRecord(task) || !Object.keys(task).every((key) => key === "id" || key === "name" || key === "description" || key === "status" || key === "result" || key === "metadata" || key === "attempts" || key === "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;
|
|
198
|
+
const budget = (task.retries ?? 0) + 1;
|
|
199
|
+
if (task.attempts > budget || task.status === "pending" && task.attempts >= budget) return false;
|
|
200
|
+
if (!(task.activity === void 0 || isTaskActivity(task.activity))) return false;
|
|
201
|
+
if (task.status === "running" || task.status === "completed" || task.status === "failed") {
|
|
202
|
+
if (task.attempts < 1 || task.activity === void 0) return false;
|
|
203
|
+
}
|
|
204
|
+
if (task.status === "pending" && task.activity !== void 0) return false;
|
|
205
|
+
if (task.status === "completed" || task.status === "failed") {
|
|
206
|
+
if (!isTaskResult(task.result, value, phase, task)) return false;
|
|
207
|
+
} else if (task.result !== void 0) return false;
|
|
208
|
+
taskIds.add(task.id);
|
|
209
|
+
statuses.push(task.status);
|
|
210
|
+
}
|
|
211
|
+
const derived = derivePhaseStatus(statuses);
|
|
212
|
+
if (phase.status !== (phase.override ?? derived) || phase.override !== void 0 && phase.status !== phase.override) return false;
|
|
213
|
+
derivations.push({
|
|
214
|
+
status: phase.status,
|
|
215
|
+
bail: phase.bail
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
const derived = deriveWorkflowStatus(derivations);
|
|
219
|
+
if (value.override === "completed") return value.status === "completed" && derived === "pending" && vacuous;
|
|
220
|
+
return value.status === (value.override ?? derived);
|
|
221
|
+
} catch {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/** Total hostile-boundary workflow snapshot guard. */
|
|
226
|
+
function isWorkflowSnapshot(value) {
|
|
227
|
+
const cloned = attempt(() => cloneJSONValue(value));
|
|
228
|
+
return cloned.success && isOwnedWorkflowSnapshot(cloned.value);
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Test whether an unknown value is a valid whole-frame activity report.
|
|
232
|
+
*/
|
|
233
|
+
function isTaskActivityInput(value) {
|
|
234
|
+
try {
|
|
235
|
+
if (!isRecord(value)) return false;
|
|
236
|
+
const prototype = Object.getPrototypeOf(value);
|
|
237
|
+
if (prototype !== Object.prototype && prototype !== null || !Object.keys(value).every((key) => key === "note" || key === "progress" || key === "operations" || key === "constraints")) return false;
|
|
238
|
+
const note = value.note;
|
|
239
|
+
const progress = value.progress;
|
|
240
|
+
const operations = value.operations;
|
|
241
|
+
const constraints = value.constraints;
|
|
242
|
+
if (note !== void 0 && !isNonEmptyString(note)) return false;
|
|
243
|
+
if (progress !== void 0) {
|
|
244
|
+
if (!isRecord(progress)) return false;
|
|
245
|
+
const progressPrototype = Object.getPrototypeOf(progress);
|
|
246
|
+
if (progressPrototype !== Object.prototype && progressPrototype !== null || !Object.keys(progress).every((key) => key === "current" || key === "total" || key === "unit")) return false;
|
|
247
|
+
const current = progress.current;
|
|
248
|
+
const total = progress.total;
|
|
249
|
+
const unit = progress.unit;
|
|
250
|
+
if (!isFiniteNumber(current) || current < 0 || total !== void 0 && (!isFiniteNumber(total) || total < current) || unit !== void 0 && !isNonEmptyString(unit)) return false;
|
|
251
|
+
}
|
|
252
|
+
if (operations !== void 0) {
|
|
253
|
+
if (!isArray(operations)) return false;
|
|
254
|
+
const ids = /* @__PURE__ */ new Set();
|
|
255
|
+
for (const operation of operations) {
|
|
256
|
+
if (!isRecord(operation)) return false;
|
|
257
|
+
const operationPrototype = Object.getPrototypeOf(operation);
|
|
258
|
+
if (operationPrototype !== Object.prototype && operationPrototype !== null || !Object.keys(operation).every((key) => key === "id" || key === "name" || key === "started")) return false;
|
|
259
|
+
const id = operation.id;
|
|
260
|
+
const name = operation.name;
|
|
261
|
+
const started = operation.started;
|
|
262
|
+
if (!isNonEmptyString(id) || !isNonEmptyString(name) || !isFiniteNumber(started) || started < 0 || ids.has(id)) return false;
|
|
263
|
+
ids.add(id);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (constraints !== void 0) {
|
|
267
|
+
if (!isArray(constraints)) return false;
|
|
268
|
+
const ids = /* @__PURE__ */ new Set();
|
|
269
|
+
for (const constraint of constraints) {
|
|
270
|
+
if (!isRecord(constraint)) return false;
|
|
271
|
+
const constraintPrototype = Object.getPrototypeOf(constraint);
|
|
272
|
+
if (constraintPrototype !== Object.prototype && constraintPrototype !== null || !Object.keys(constraint).every((key) => key === "id" || key === "name" || key === "started")) return false;
|
|
273
|
+
const id = constraint.id;
|
|
274
|
+
const name = constraint.name;
|
|
275
|
+
const started = constraint.started;
|
|
276
|
+
if (!isNonEmptyString(id) || !isNonEmptyString(name) || !isFiniteNumber(started) || started < 0 || ids.has(id)) return false;
|
|
277
|
+
ids.add(id);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return true;
|
|
281
|
+
} catch {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Test whether an unknown value is valid persisted task activity.
|
|
287
|
+
*/
|
|
288
|
+
function isTaskActivity(value) {
|
|
289
|
+
try {
|
|
290
|
+
if (!isRecord(value)) return false;
|
|
291
|
+
const prototype = Object.getPrototypeOf(value);
|
|
292
|
+
if (prototype !== Object.prototype && prototype !== null || !Object.keys(value).every((key) => key === "note" || key === "progress" || key === "operations" || key === "constraints" || key === "updated")) return false;
|
|
293
|
+
const note = value.note;
|
|
294
|
+
const progress = value.progress;
|
|
295
|
+
const operations = value.operations;
|
|
296
|
+
const constraints = value.constraints;
|
|
297
|
+
const updated = value.updated;
|
|
298
|
+
if (operations === void 0 || constraints === void 0 || !isFiniteNumber(updated) || updated < 0) return false;
|
|
299
|
+
return isTaskActivityInput({
|
|
300
|
+
...note === void 0 ? {} : { note },
|
|
301
|
+
...progress === void 0 ? {} : { progress },
|
|
302
|
+
operations,
|
|
303
|
+
constraints
|
|
304
|
+
});
|
|
305
|
+
} catch {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
//#endregion
|
|
112
310
|
//#region src/core/helpers.ts
|
|
113
311
|
/**
|
|
114
312
|
* Capture every top-level {@link WorkflowOptions} value exactly once into an owned plain bag.
|
|
@@ -582,6 +780,53 @@ function recoverWorkflowSnapshot(snapshot) {
|
|
|
582
780
|
updated: now
|
|
583
781
|
};
|
|
584
782
|
}
|
|
783
|
+
/** Compare two optional description values. */
|
|
784
|
+
function matchesDescription(left, right) {
|
|
785
|
+
return left === right && (left === void 0 || typeof left === "string");
|
|
786
|
+
}
|
|
787
|
+
/** Test a result's lineage against its containing snapshot nodes. */
|
|
788
|
+
function isTaskResult(value, workflow, phase, task) {
|
|
789
|
+
try {
|
|
790
|
+
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;
|
|
791
|
+
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;
|
|
792
|
+
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;
|
|
793
|
+
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;
|
|
794
|
+
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);
|
|
795
|
+
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);
|
|
796
|
+
return false;
|
|
797
|
+
} catch {
|
|
798
|
+
return false;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
function hasWorkflowHandlers(workflow, functions) {
|
|
802
|
+
if ("destroyed" in workflow) {
|
|
803
|
+
for (const phase of workflow.phases.phases()) for (const task of phase.tasks.tasks()) if (task.run !== void 0 && !isFunction(task.handler)) return false;
|
|
804
|
+
return true;
|
|
805
|
+
}
|
|
806
|
+
const runs = /* @__PURE__ */ new Set();
|
|
807
|
+
for (const phase of workflow.phases) for (const task of phase.tasks) {
|
|
808
|
+
if (task.run === void 0 || runs.has(task.run)) continue;
|
|
809
|
+
runs.add(task.run);
|
|
810
|
+
if (!isFunction(functions?.[task.run])) return false;
|
|
811
|
+
}
|
|
812
|
+
return true;
|
|
813
|
+
}
|
|
814
|
+
/** Locate the nearest identifiable node for an inconsistent owned snapshot. */
|
|
815
|
+
function workflowSnapshotContext(value) {
|
|
816
|
+
if (!isRecord(value) || !isArray(value.phases)) return void 0;
|
|
817
|
+
for (const phase of value.phases) {
|
|
818
|
+
if (!isRecord(phase)) continue;
|
|
819
|
+
const phaseContext = isNonEmptyString(phase.id) ? { phase: phase.id } : void 0;
|
|
820
|
+
if (!isBoolean(phase.bail) || phase.concurrency !== void 0 && (!isInteger(phase.concurrency) || phase.concurrency < 1) || !isArray(phase.tasks)) return phaseContext;
|
|
821
|
+
for (const task of phase.tasks) {
|
|
822
|
+
if (!isRecord(task)) continue;
|
|
823
|
+
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 {
|
|
824
|
+
...phaseContext ?? {},
|
|
825
|
+
...isNonEmptyString(task.id) ? { task: task.id } : {}
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
}
|
|
585
830
|
/**
|
|
586
831
|
* Flatten a nested list of per-phase {@link TaskResult} lists into one positional list
|
|
587
832
|
* — the workflow tier of the result tree, built from each phase's `results()`.
|
|
@@ -671,6 +916,16 @@ function createDeferred() {
|
|
|
671
916
|
* Schedule one cancellable host operation behind an owned settlement signal.
|
|
672
917
|
*
|
|
673
918
|
* @remarks
|
|
919
|
+
* A defined `signal` that is not a native `AbortSignal` is refused before anything is armed, as a
|
|
920
|
+
* rejected promise carrying a {@link import('./errors.js').WorkflowError} with the `SCHEDULE` code.
|
|
921
|
+
* Rejecting rather than throwing keeps every caller on one settlement path, so a backend never has
|
|
922
|
+
* to guard the call itself.
|
|
923
|
+
*
|
|
924
|
+
* The guard is necessary but not sufficient, so linking stays contained. A `Proxy` over a native
|
|
925
|
+
* signal passes the guard and can still make linking throw from a trap, and that escape would be
|
|
926
|
+
* synchronous — the one shape every caller here is built not to expect. Containment turns it into
|
|
927
|
+
* the same `SCHEDULE` rejection, so setup has exactly one failure shape however hostile the input.
|
|
928
|
+
*
|
|
674
929
|
* The completion and failure paths each own an {@link AbortController}; their native composite is
|
|
675
930
|
* linked to the optional caller signal before `start` can arm host work. Scheduler backends attach
|
|
676
931
|
* only to that safe composite, so caller mutation of `addEventListener` or `removeEventListener`
|
|
@@ -682,16 +937,18 @@ function createDeferred() {
|
|
|
682
937
|
*
|
|
683
938
|
* @param start - Arm host work and return its cancellation closure
|
|
684
939
|
* @param signal - Optional caller cancellation signal
|
|
685
|
-
* @returns A promise settled exactly once by completion, host failure,
|
|
940
|
+
* @returns A promise settled exactly once by an invalid-signal refusal, completion, host failure,
|
|
941
|
+
* or caller abort
|
|
686
942
|
*/
|
|
687
943
|
function scheduleHost(start, signal) {
|
|
944
|
+
if (signal !== void 0 && !isAbortSignal(signal)) return Promise.reject(new WorkflowError("SCHEDULE", "scheduleHost signal must be an AbortSignal", { signal: typeof signal }));
|
|
688
945
|
const completion = new AbortController();
|
|
689
946
|
const failed = new AbortController();
|
|
690
947
|
let settled;
|
|
691
948
|
try {
|
|
692
949
|
settled = linkSignal(AbortSignal.any([completion.signal, failed.signal]), signal);
|
|
693
|
-
} catch
|
|
694
|
-
return Promise.reject(
|
|
950
|
+
} catch {
|
|
951
|
+
return Promise.reject(new WorkflowError("SCHEDULE", "scheduleHost could not link the caller signal", { signal: typeof signal }));
|
|
695
952
|
}
|
|
696
953
|
if (settled.aborted) return Promise.reject(settled.reason);
|
|
697
954
|
return new Promise((resolve, reject) => {
|
|
@@ -814,249 +1071,6 @@ var Scheduler = class {
|
|
|
814
1071
|
}
|
|
815
1072
|
};
|
|
816
1073
|
//#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
1074
|
//#region src/core/cloners.ts
|
|
1061
1075
|
/**
|
|
1062
1076
|
* Validate and own a workflow snapshot before live construction.
|
|
@@ -1313,18 +1327,18 @@ var phaseUpdateShape = objectShape({
|
|
|
1313
1327
|
* idle-TTL / eviction — a persisted run-state is durable orchestration state that lives until an
|
|
1314
1328
|
* explicit `delete`. The public surface is EXACTLY `get` / `set` / `delete` — no extra members (the
|
|
1315
1329
|
* §22 method bijection with {@link WorkflowStoreInterface}). Restore stays a caller concern: read a
|
|
1316
|
-
* snapshot back and rebuild the live tree with {@link import('../factories.js').
|
|
1330
|
+
* snapshot back and rebuild the live tree with {@link import('../factories.js').createRestoredWorkflow}.
|
|
1317
1331
|
*
|
|
1318
1332
|
* @example
|
|
1319
1333
|
* ```ts
|
|
1320
1334
|
* import { createMemoryDriver } from '@orkestrel/database'
|
|
1321
|
-
* import { createDatabaseWorkflowStore, createWorkflow,
|
|
1335
|
+
* import { createDatabaseWorkflowStore, createWorkflow, createRestoredWorkflow } from '@orkestrel/workflow'
|
|
1322
1336
|
*
|
|
1323
1337
|
* const store = createDatabaseWorkflowStore(createMemoryDriver()) // a durable driver swaps in here
|
|
1324
1338
|
* const workflow = createWorkflow(definition)
|
|
1325
1339
|
* await store.set(workflow.snapshot()) // persist the run state (one JSON column)
|
|
1326
1340
|
* const snapshot = await store.get(definition.id)
|
|
1327
|
-
* const restored = snapshot &&
|
|
1341
|
+
* const restored = snapshot && createRestoredWorkflow(snapshot) // an identical live tree
|
|
1328
1342
|
* await store.delete(definition.id) // drop it
|
|
1329
1343
|
* ```
|
|
1330
1344
|
*/
|
|
@@ -1384,17 +1398,17 @@ var DatabaseWorkflowStore = class {
|
|
|
1384
1398
|
*
|
|
1385
1399
|
* The public surface is EXACTLY `get` / `set` / `delete` — no extra members (the §22 method
|
|
1386
1400
|
* bijection with {@link WorkflowStoreInterface}). Restore is a caller concern: read a snapshot
|
|
1387
|
-
* back and rebuild the live tree with {@link import('../factories.js').
|
|
1401
|
+
* back and rebuild the live tree with {@link import('../factories.js').createRestoredWorkflow}.
|
|
1388
1402
|
*
|
|
1389
1403
|
* @example
|
|
1390
1404
|
* ```ts
|
|
1391
|
-
* import { createMemoryWorkflowStore, createWorkflow,
|
|
1405
|
+
* import { createMemoryWorkflowStore, createWorkflow, createRestoredWorkflow } from '@orkestrel/workflow'
|
|
1392
1406
|
*
|
|
1393
1407
|
* const store = createMemoryWorkflowStore()
|
|
1394
1408
|
* const workflow = createWorkflow(definition)
|
|
1395
1409
|
* await store.set(workflow.snapshot()) // persist the run state
|
|
1396
1410
|
* const snapshot = await store.get(definition.id)
|
|
1397
|
-
* const restored = snapshot &&
|
|
1411
|
+
* const restored = snapshot && createRestoredWorkflow(snapshot) // an identical live tree
|
|
1398
1412
|
* await store.delete(definition.id) // drop it
|
|
1399
1413
|
* ```
|
|
1400
1414
|
*/
|
|
@@ -2236,7 +2250,7 @@ var PhaseManager = class {
|
|
|
2236
2250
|
* @remarks
|
|
2237
2251
|
* - **Construction.** Built from a {@link WorkflowSnapshot} (the unified input —
|
|
2238
2252
|
* {@link import('./factories.js').createWorkflow} seeds an initial snapshot from a
|
|
2239
|
-
* {@link import('./types.js').WorkflowDefinition}, {@link import('./factories.js').
|
|
2253
|
+
* {@link import('./types.js').WorkflowDefinition}, {@link import('./factories.js').createRestoredWorkflow}
|
|
2240
2254
|
* passes a persisted one). Each child {@link Phase} is wired to escalate to `#recompute`.
|
|
2241
2255
|
* - **Derived status.** `status` is `#override` when forced, else
|
|
2242
2256
|
* {@link deriveWorkflowStatus} over the live phases' statuses feeding `bail`. `failed` is
|
|
@@ -2251,7 +2265,7 @@ var PhaseManager = class {
|
|
|
2251
2265
|
* workflow tier; `phase(id)` + each `phase.task(id)` navigate DOWN, a task's `phase` / `workflow`
|
|
2252
2266
|
* navigate UP.
|
|
2253
2267
|
* - **Snapshot.** `snapshot()` serializes the whole live tree to a {@link WorkflowSnapshot} (pure
|
|
2254
|
-
* JSON); {@link import('./factories.js').
|
|
2268
|
+
* JSON); {@link import('./factories.js').createRestoredWorkflow} rebuilds an equivalent live tree.
|
|
2255
2269
|
* - **Observable (AGENTS §13).** The owned {@link emitter} ({@link WorkflowEventMap}) fires
|
|
2256
2270
|
* `start` / `complete` / `fail` / `pause` / `resume` / `skip` / `stop` after the
|
|
2257
2271
|
* corresponding status or runtime-gate change; the emitter isolates a listener throw and
|
|
@@ -2655,7 +2669,7 @@ var WorkflowManager = class {
|
|
|
2655
2669
|
if (!this.#owns(id, mutation, generation)) return this.#resolve(id, generation);
|
|
2656
2670
|
let workflow;
|
|
2657
2671
|
try {
|
|
2658
|
-
workflow =
|
|
2672
|
+
workflow = createRestoredWorkflow(owned, { ...this.#functions === void 0 ? {} : { functions: this.#functions } });
|
|
2659
2673
|
} catch (error) {
|
|
2660
2674
|
if (!this.#owns(id, mutation, generation)) return this.#resolve(id, generation);
|
|
2661
2675
|
throw error;
|
|
@@ -3964,12 +3978,12 @@ function createWorkflow(definition, options) {
|
|
|
3964
3978
|
return new Workflow(definitionToSnapshot(definition, captured.bail ?? definition.bail ?? false), captured);
|
|
3965
3979
|
}
|
|
3966
3980
|
/**
|
|
3967
|
-
*
|
|
3981
|
+
* Build an equivalent live W-b entity tree from a {@link WorkflowSnapshot} — the
|
|
3968
3982
|
* inverse of {@link WorkflowInterface.snapshot}, restoring structure + each node's status
|
|
3969
3983
|
* + recorded results + positional order + the persisted `#override`.
|
|
3970
3984
|
*
|
|
3971
3985
|
* @remarks
|
|
3972
|
-
* Round-trip fidelity is paramount: a `snapshot()` → `
|
|
3986
|
+
* Round-trip fidelity is paramount: a `snapshot()` → `createRestoredWorkflow()` reproduces the
|
|
3973
3987
|
* same status at every node (each `#override` restored DIRECTLY from the snapshot's own
|
|
3974
3988
|
* `override` field, not guessed from a status divergence), the same recorded
|
|
3975
3989
|
* {@link import('./types.js').TaskResult}s, and the same positional order (an interior
|
|
@@ -3989,18 +4003,18 @@ function createWorkflow(definition, options) {
|
|
|
3989
4003
|
*
|
|
3990
4004
|
* @example
|
|
3991
4005
|
* ```ts
|
|
3992
|
-
* import {
|
|
4006
|
+
* import { createRestoredWorkflow } from '@orkestrel/workflow'
|
|
3993
4007
|
*
|
|
3994
|
-
* const restored =
|
|
4008
|
+
* const restored = createRestoredWorkflow(workflow.snapshot()) // bail comes from the snapshot
|
|
3995
4009
|
* restored.status === workflow.status // true
|
|
3996
4010
|
* ```
|
|
3997
4011
|
*/
|
|
3998
|
-
function
|
|
4012
|
+
function createRestoredWorkflow(snapshot, options) {
|
|
3999
4013
|
const captured = captureWorkflowOptions(options);
|
|
4000
4014
|
return new Workflow(cloneWorkflowSnapshot(snapshot), captured);
|
|
4001
4015
|
}
|
|
4002
4016
|
/**
|
|
4003
|
-
*
|
|
4017
|
+
* Build an interrupted workflow back to life at its remaining retry budget.
|
|
4004
4018
|
*
|
|
4005
4019
|
* @remarks
|
|
4006
4020
|
* Each phase captures every unique initial `run` binding once before constructing tasks. Recovery
|
|
@@ -4009,9 +4023,17 @@ function restoreWorkflow(snapshot, options) {
|
|
|
4009
4023
|
*
|
|
4010
4024
|
* @param snapshot - The hostile persisted snapshot
|
|
4011
4025
|
* @param options - Runtime handlers and entity options
|
|
4012
|
-
* @returns A recoverable live
|
|
4026
|
+
* @returns A recoverable live {@link WorkflowInterface} root
|
|
4027
|
+
*
|
|
4028
|
+
* @example
|
|
4029
|
+
* ```ts
|
|
4030
|
+
* import { createRecoveredWorkflow } from '@orkestrel/workflow'
|
|
4031
|
+
*
|
|
4032
|
+
* const recovered = createRecoveredWorkflow(snapshot, { functions })
|
|
4033
|
+
* recovered.status // 'pending' — interrupted running work returned to its remaining budget
|
|
4034
|
+
* ```
|
|
4013
4035
|
*/
|
|
4014
|
-
function
|
|
4036
|
+
function createRecoveredWorkflow(snapshot, options) {
|
|
4015
4037
|
const captured = captureWorkflowOptions(options);
|
|
4016
4038
|
const owned = cloneWorkflowSnapshot(snapshot);
|
|
4017
4039
|
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 });
|
|
@@ -4020,29 +4042,6 @@ function recoverWorkflow(snapshot, options) {
|
|
|
4020
4042
|
return workflow;
|
|
4021
4043
|
}
|
|
4022
4044
|
/**
|
|
4023
|
-
* Assert that a {@link WorkflowSnapshot} carries a `boolean` `bail` — at the workflow tier AND
|
|
4024
|
-
* on every phase — and that its every node's status (and its `override`, when present) is drawn
|
|
4025
|
-
* from the lifecycle vocabulary, throwing a `RESTORE` {@link WorkflowError} otherwise.
|
|
4026
|
-
*
|
|
4027
|
-
* @remarks
|
|
4028
|
-
* The boundary-narrowing guard (AGENTS §14) for {@link restoreWorkflow}: a snapshot is
|
|
4029
|
-
* untrusted JSON, so a status (or an override) outside
|
|
4030
|
-
* {@link import('./constants.js').WORKFLOW_STATUSES} /
|
|
4031
|
-
* {@link import('./constants.js').PHASE_STATUSES} / {@link import('./constants.js').TASK_STATUSES},
|
|
4032
|
-
* a non-boolean `bail` (the workflow's OR any phase's — both are REQUIRED persisted policy), a
|
|
4033
|
-
* present-but-invalid phase `concurrency` (not a positive integer), or a present-but-invalid task
|
|
4034
|
-
* `run` (an empty string) / `retries` / `timeout` (not a non-negative integer),
|
|
4035
|
-
* is rejected loudly (naming the offending node) rather than silently producing a broken tree.
|
|
4036
|
-
* The `override` / `concurrency` / `run` / `retries` / `timeout` are optional, so each is only
|
|
4037
|
-
* checked WHEN present. Structural shape beyond these fields is the contract's concern; this
|
|
4038
|
-
* guards exactly the fields the live state machine reads back.
|
|
4039
|
-
*
|
|
4040
|
-
* @param snapshot - The snapshot to validate
|
|
4041
|
-
*/
|
|
4042
|
-
function assertSnapshot(snapshot) {
|
|
4043
|
-
cloneWorkflowSnapshot(snapshot);
|
|
4044
|
-
}
|
|
4045
|
-
/**
|
|
4046
4045
|
* Create the in-memory durable {@link WorkflowStoreInterface} — a process-lifetime
|
|
4047
4046
|
* {@link MemoryWorkflowStore} persisting {@link WorkflowSnapshot}s by workflow id, the DEFAULT
|
|
4048
4047
|
* backend behind the W-d persistence seam.
|
|
@@ -4055,19 +4054,19 @@ function assertSnapshot(snapshot) {
|
|
|
4055
4054
|
* {@link createDatabaseWorkflowStore} (the snapshot as one opaque JSON column over a `databases`
|
|
4056
4055
|
* table) — for a DURABLE store (run-state surviving a restart) pass it a JSON / SQLite / IndexedDB
|
|
4057
4056
|
* driver, and it swaps in WITHOUT touching the runner or the entity tree. Restore stays a caller
|
|
4058
|
-
* concern: read a snapshot back and rebuild the live tree with {@link
|
|
4057
|
+
* concern: read a snapshot back and rebuild the live tree with {@link createRestoredWorkflow}.
|
|
4059
4058
|
*
|
|
4060
4059
|
* @returns A memory-backed {@link WorkflowStoreInterface}
|
|
4061
4060
|
*
|
|
4062
4061
|
* @example
|
|
4063
4062
|
* ```ts
|
|
4064
|
-
* import { createMemoryWorkflowStore, createWorkflow,
|
|
4063
|
+
* import { createMemoryWorkflowStore, createWorkflow, createRestoredWorkflow } from '@orkestrel/workflow'
|
|
4065
4064
|
*
|
|
4066
4065
|
* const store = createMemoryWorkflowStore()
|
|
4067
4066
|
* const workflow = createWorkflow(definition)
|
|
4068
4067
|
* await store.set(workflow.snapshot()) // persist the run state
|
|
4069
4068
|
* const snapshot = await store.get(definition.id)
|
|
4070
|
-
* const restored = snapshot &&
|
|
4069
|
+
* const restored = snapshot && createRestoredWorkflow(snapshot) // an identical live tree
|
|
4071
4070
|
* ```
|
|
4072
4071
|
*/
|
|
4073
4072
|
function createMemoryWorkflowStore() {
|
|
@@ -4098,13 +4097,13 @@ function createMemoryWorkflowStore() {
|
|
|
4098
4097
|
* @example
|
|
4099
4098
|
* ```ts
|
|
4100
4099
|
* import { createMemoryDriver } from '@orkestrel/database'
|
|
4101
|
-
* import { createDatabaseWorkflowStore, createWorkflow,
|
|
4100
|
+
* import { createDatabaseWorkflowStore, createWorkflow, createRestoredWorkflow } from '@orkestrel/workflow'
|
|
4102
4101
|
*
|
|
4103
4102
|
* const store = createDatabaseWorkflowStore(createMemoryDriver()) // a durable driver swaps in here
|
|
4104
4103
|
* const workflow = createWorkflow(definition)
|
|
4105
4104
|
* await store.set(workflow.snapshot()) // persist the run state (one JSON column)
|
|
4106
4105
|
* const snapshot = await store.get(definition.id)
|
|
4107
|
-
* const restored = snapshot &&
|
|
4106
|
+
* const restored = snapshot && createRestoredWorkflow(snapshot) // an identical live tree
|
|
4108
4107
|
* ```
|
|
4109
4108
|
*/
|
|
4110
4109
|
function createDatabaseWorkflowStore(driver = createMemoryDriver()) {
|
|
@@ -4173,11 +4172,11 @@ function createWorkflowRunner(options) {
|
|
|
4173
4172
|
* @remarks
|
|
4174
4173
|
* `options.functions` flows into every workflow the manager mints (`add`, via
|
|
4175
4174
|
* {@link createWorkflow}) or hydrates (`open`'s registry-miss path, via
|
|
4176
|
-
* {@link
|
|
4175
|
+
* {@link createRestoredWorkflow}), so a hydrated workflow is RUNNABLE rather than a dead snapshot
|
|
4177
4176
|
* mirror. `options.store` is the EXACT analogue of the twins' `store` seam — omitted ⇒ the
|
|
4178
4177
|
* manager is registry-only (`open` resolves only what is registered, `save` is a no-op). This
|
|
4179
4178
|
* is PURELY ADDITIVE: direct {@link WorkflowStoreInterface} use and
|
|
4180
|
-
* {@link
|
|
4179
|
+
* {@link createRestoredWorkflow} remain valid — the manager is one more caller-driven persistence
|
|
4181
4180
|
* seam, not a replacement.
|
|
4182
4181
|
*
|
|
4183
4182
|
* @param options - The optional `store` seam and the `functions` registry threaded into every mint/hydrate
|
|
@@ -4293,6 +4292,6 @@ function createRunner(options) {
|
|
|
4293
4292
|
return new Runner(options);
|
|
4294
4293
|
}
|
|
4295
4294
|
//#endregion
|
|
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,
|
|
4295
|
+
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, buildPhaseContext, buildTaskContext, buildWorkflowContext, canTransitionTask, captureWorkflowOptions, cloneTaskActivity, cloneWorkflowSnapshot, collectResults, createDatabaseWorkflowStore, createDeferred, createMemoryWorkflowStore, createRecoveredWorkflow, createRestoredWorkflow, 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, recoverWorkflowSnapshot, resolveTaskSilence, scheduleHost, success, taskDefinitionToSnapshot, taskShape, taskUpdateShape, workflowShape, workflowSnapshotContext };
|
|
4297
4296
|
|
|
4298
4297
|
//# sourceMappingURL=index.js.map
|