@orkestrel/workflow 0.0.11 → 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 +234 -250
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +86 -100
- package/dist/src/core/index.d.ts +86 -100
- package/dist/src/core/index.js +233 -248
- 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 +4 -3
package/dist/src/core/index.cjs
CHANGED
|
@@ -155,6 +155,159 @@ function isWorkflowError(value) {
|
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
157
|
//#endregion
|
|
158
|
+
//#region src/core/validators.ts
|
|
159
|
+
/** Test the workflow lifecycle vocabulary. */
|
|
160
|
+
function isLifecycleStatus(value) {
|
|
161
|
+
return value === "pending" || value === "running" || value === "completed" || value === "failed" || value === "skipped" || value === "stopped";
|
|
162
|
+
}
|
|
163
|
+
/** Test a normalized persisted task failure. */
|
|
164
|
+
function isTaskFailure(value) {
|
|
165
|
+
try {
|
|
166
|
+
return (0, _orkestrel_contract.isRecord)(value) && Object.keys(value).every((key) => key === "origin" || key === "message") && (value.origin === "handler" || value.origin === "timeout" || value.origin === "recovery") && (0, _orkestrel_contract.isNonEmptyString)(value.message);
|
|
167
|
+
} catch {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Validate a safe owned JSON graph as a coherent workflow snapshot.
|
|
173
|
+
*
|
|
174
|
+
* @remarks
|
|
175
|
+
* Callers at hostile boundaries use {@link isWorkflowSnapshot}, which owns the
|
|
176
|
+
* graph first so this semantic pass never observes accessors or prototypes.
|
|
177
|
+
*/
|
|
178
|
+
function isOwnedWorkflowSnapshot(value) {
|
|
179
|
+
try {
|
|
180
|
+
if (!(0, _orkestrel_contract.isRecord)(value) || !Object.keys(value).every((key) => key === "id" || key === "name" || key === "description" || key === "status" || key === "override" || key === "bail" || key === "phases" || key === "created" || key === "updated") || !(0, _orkestrel_contract.isNonEmptyString)(value.id) || !(0, _orkestrel_contract.isNonEmptyString)(value.name) || value.description !== void 0 && typeof value.description !== "string" || !isLifecycleStatus(value.status) || value.override !== void 0 && value.override !== "completed" && value.override !== "skipped" && value.override !== "stopped" || !(0, _orkestrel_contract.isBoolean)(value.bail) || !(0, _orkestrel_contract.isArray)(value.phases) || !(0, _orkestrel_contract.isFiniteNumber)(value.created) || value.created < 0 || !(0, _orkestrel_contract.isFiniteNumber)(value.updated) || value.updated < value.created) return false;
|
|
181
|
+
const phaseIds = /* @__PURE__ */ new Set();
|
|
182
|
+
const derivations = [];
|
|
183
|
+
let frontier = false;
|
|
184
|
+
let running = false;
|
|
185
|
+
let vacuous = true;
|
|
186
|
+
for (const phase of value.phases) {
|
|
187
|
+
if (!(0, _orkestrel_contract.isRecord)(phase) || !Object.keys(phase).every((key) => key === "id" || key === "name" || key === "description" || key === "status" || key === "override" || key === "bail" || key === "concurrency" || key === "tasks") || !(0, _orkestrel_contract.isNonEmptyString)(phase.id) || phaseIds.has(phase.id) || !(0, _orkestrel_contract.isNonEmptyString)(phase.name) || phase.description !== void 0 && typeof phase.description !== "string" || !isLifecycleStatus(phase.status) || phase.override !== void 0 && phase.override !== "skipped" && phase.override !== "stopped" || !(0, _orkestrel_contract.isBoolean)(phase.bail) || phase.concurrency !== void 0 && (!(0, _orkestrel_contract.isInteger)(phase.concurrency) || phase.concurrency < 1) || !(0, _orkestrel_contract.isArray)(phase.tasks)) return false;
|
|
188
|
+
const forced = phase.override === "skipped" || phase.override === "stopped";
|
|
189
|
+
const started = phase.status === "running" || phase.status === "completed" || phase.status === "failed";
|
|
190
|
+
if (!forced && frontier && started || phase.status === "running" && running) return false;
|
|
191
|
+
if (phase.status === "running") running = true;
|
|
192
|
+
if (!forced && (phase.status === "pending" || phase.status === "running" || phase.status === "failed" && phase.bail)) frontier = true;
|
|
193
|
+
phaseIds.add(phase.id);
|
|
194
|
+
const taskIds = /* @__PURE__ */ new Set();
|
|
195
|
+
const statuses = [];
|
|
196
|
+
if (phase.tasks.length > 0) vacuous = false;
|
|
197
|
+
for (const task of phase.tasks) {
|
|
198
|
+
if (!(0, _orkestrel_contract.isRecord)(task) || !Object.keys(task).every((key) => key === "id" || key === "name" || key === "description" || key === "status" || key === "result" || key === "metadata" || key === "attempts" || key === "run" || key === "retries" || key === "timeout" || key === "activity") || !(0, _orkestrel_contract.isNonEmptyString)(task.id) || taskIds.has(task.id) || !(0, _orkestrel_contract.isNonEmptyString)(task.name) || task.description !== void 0 && typeof task.description !== "string" || !isLifecycleStatus(task.status) || !(0, _orkestrel_contract.isRecord)(task.metadata) || !(0, _orkestrel_contract.isJSONValue)(task.metadata) || !(0, _orkestrel_contract.isInteger)(task.attempts) || task.attempts < 0 || task.run !== void 0 && !(0, _orkestrel_contract.isNonEmptyString)(task.run) || task.retries !== void 0 && (!(0, _orkestrel_contract.isInteger)(task.retries) || task.retries < 0) || task.timeout !== void 0 && (!(0, _orkestrel_contract.isInteger)(task.timeout) || task.timeout < 0 || task.timeout > 2147483647)) return false;
|
|
199
|
+
const budget = (task.retries ?? 0) + 1;
|
|
200
|
+
if (task.attempts > budget || task.status === "pending" && task.attempts >= budget) return false;
|
|
201
|
+
if (!(task.activity === void 0 || isTaskActivity(task.activity))) return false;
|
|
202
|
+
if (task.status === "running" || task.status === "completed" || task.status === "failed") {
|
|
203
|
+
if (task.attempts < 1 || task.activity === void 0) return false;
|
|
204
|
+
}
|
|
205
|
+
if (task.status === "pending" && task.activity !== void 0) return false;
|
|
206
|
+
if (task.status === "completed" || task.status === "failed") {
|
|
207
|
+
if (!isTaskResult(task.result, value, phase, task)) return false;
|
|
208
|
+
} else if (task.result !== void 0) return false;
|
|
209
|
+
taskIds.add(task.id);
|
|
210
|
+
statuses.push(task.status);
|
|
211
|
+
}
|
|
212
|
+
const derived = derivePhaseStatus(statuses);
|
|
213
|
+
if (phase.status !== (phase.override ?? derived) || phase.override !== void 0 && phase.status !== phase.override) return false;
|
|
214
|
+
derivations.push({
|
|
215
|
+
status: phase.status,
|
|
216
|
+
bail: phase.bail
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
const derived = deriveWorkflowStatus(derivations);
|
|
220
|
+
if (value.override === "completed") return value.status === "completed" && derived === "pending" && vacuous;
|
|
221
|
+
return value.status === (value.override ?? derived);
|
|
222
|
+
} catch {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
/** Total hostile-boundary workflow snapshot guard. */
|
|
227
|
+
function isWorkflowSnapshot(value) {
|
|
228
|
+
const cloned = (0, _orkestrel_contract.attempt)(() => (0, _orkestrel_contract.cloneJSONValue)(value));
|
|
229
|
+
return cloned.success && isOwnedWorkflowSnapshot(cloned.value);
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Test whether an unknown value is a valid whole-frame activity report.
|
|
233
|
+
*/
|
|
234
|
+
function isTaskActivityInput(value) {
|
|
235
|
+
try {
|
|
236
|
+
if (!(0, _orkestrel_contract.isRecord)(value)) return false;
|
|
237
|
+
const prototype = Object.getPrototypeOf(value);
|
|
238
|
+
if (prototype !== Object.prototype && prototype !== null || !Object.keys(value).every((key) => key === "note" || key === "progress" || key === "operations" || key === "constraints")) return false;
|
|
239
|
+
const note = value.note;
|
|
240
|
+
const progress = value.progress;
|
|
241
|
+
const operations = value.operations;
|
|
242
|
+
const constraints = value.constraints;
|
|
243
|
+
if (note !== void 0 && !(0, _orkestrel_contract.isNonEmptyString)(note)) return false;
|
|
244
|
+
if (progress !== void 0) {
|
|
245
|
+
if (!(0, _orkestrel_contract.isRecord)(progress)) return false;
|
|
246
|
+
const progressPrototype = Object.getPrototypeOf(progress);
|
|
247
|
+
if (progressPrototype !== Object.prototype && progressPrototype !== null || !Object.keys(progress).every((key) => key === "current" || key === "total" || key === "unit")) return false;
|
|
248
|
+
const current = progress.current;
|
|
249
|
+
const total = progress.total;
|
|
250
|
+
const unit = progress.unit;
|
|
251
|
+
if (!(0, _orkestrel_contract.isFiniteNumber)(current) || current < 0 || total !== void 0 && (!(0, _orkestrel_contract.isFiniteNumber)(total) || total < current) || unit !== void 0 && !(0, _orkestrel_contract.isNonEmptyString)(unit)) return false;
|
|
252
|
+
}
|
|
253
|
+
if (operations !== void 0) {
|
|
254
|
+
if (!(0, _orkestrel_contract.isArray)(operations)) return false;
|
|
255
|
+
const ids = /* @__PURE__ */ new Set();
|
|
256
|
+
for (const operation of operations) {
|
|
257
|
+
if (!(0, _orkestrel_contract.isRecord)(operation)) return false;
|
|
258
|
+
const operationPrototype = Object.getPrototypeOf(operation);
|
|
259
|
+
if (operationPrototype !== Object.prototype && operationPrototype !== null || !Object.keys(operation).every((key) => key === "id" || key === "name" || key === "started")) return false;
|
|
260
|
+
const id = operation.id;
|
|
261
|
+
const name = operation.name;
|
|
262
|
+
const started = operation.started;
|
|
263
|
+
if (!(0, _orkestrel_contract.isNonEmptyString)(id) || !(0, _orkestrel_contract.isNonEmptyString)(name) || !(0, _orkestrel_contract.isFiniteNumber)(started) || started < 0 || ids.has(id)) return false;
|
|
264
|
+
ids.add(id);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (constraints !== void 0) {
|
|
268
|
+
if (!(0, _orkestrel_contract.isArray)(constraints)) return false;
|
|
269
|
+
const ids = /* @__PURE__ */ new Set();
|
|
270
|
+
for (const constraint of constraints) {
|
|
271
|
+
if (!(0, _orkestrel_contract.isRecord)(constraint)) return false;
|
|
272
|
+
const constraintPrototype = Object.getPrototypeOf(constraint);
|
|
273
|
+
if (constraintPrototype !== Object.prototype && constraintPrototype !== null || !Object.keys(constraint).every((key) => key === "id" || key === "name" || key === "started")) return false;
|
|
274
|
+
const id = constraint.id;
|
|
275
|
+
const name = constraint.name;
|
|
276
|
+
const started = constraint.started;
|
|
277
|
+
if (!(0, _orkestrel_contract.isNonEmptyString)(id) || !(0, _orkestrel_contract.isNonEmptyString)(name) || !(0, _orkestrel_contract.isFiniteNumber)(started) || started < 0 || ids.has(id)) return false;
|
|
278
|
+
ids.add(id);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return true;
|
|
282
|
+
} catch {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Test whether an unknown value is valid persisted task activity.
|
|
288
|
+
*/
|
|
289
|
+
function isTaskActivity(value) {
|
|
290
|
+
try {
|
|
291
|
+
if (!(0, _orkestrel_contract.isRecord)(value)) return false;
|
|
292
|
+
const prototype = Object.getPrototypeOf(value);
|
|
293
|
+
if (prototype !== Object.prototype && prototype !== null || !Object.keys(value).every((key) => key === "note" || key === "progress" || key === "operations" || key === "constraints" || key === "updated")) return false;
|
|
294
|
+
const note = value.note;
|
|
295
|
+
const progress = value.progress;
|
|
296
|
+
const operations = value.operations;
|
|
297
|
+
const constraints = value.constraints;
|
|
298
|
+
const updated = value.updated;
|
|
299
|
+
if (operations === void 0 || constraints === void 0 || !(0, _orkestrel_contract.isFiniteNumber)(updated) || updated < 0) return false;
|
|
300
|
+
return isTaskActivityInput({
|
|
301
|
+
...note === void 0 ? {} : { note },
|
|
302
|
+
...progress === void 0 ? {} : { progress },
|
|
303
|
+
operations,
|
|
304
|
+
constraints
|
|
305
|
+
});
|
|
306
|
+
} catch {
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
//#endregion
|
|
158
311
|
//#region src/core/helpers.ts
|
|
159
312
|
/**
|
|
160
313
|
* Capture every top-level {@link WorkflowOptions} value exactly once into an owned plain bag.
|
|
@@ -628,6 +781,53 @@ function recoverWorkflowSnapshot(snapshot) {
|
|
|
628
781
|
updated: now
|
|
629
782
|
};
|
|
630
783
|
}
|
|
784
|
+
/** Compare two optional description values. */
|
|
785
|
+
function matchesDescription(left, right) {
|
|
786
|
+
return left === right && (left === void 0 || typeof left === "string");
|
|
787
|
+
}
|
|
788
|
+
/** Test a result's lineage against its containing snapshot nodes. */
|
|
789
|
+
function isTaskResult(value, workflow, phase, task) {
|
|
790
|
+
try {
|
|
791
|
+
if (!(0, _orkestrel_contract.isRecord)(value) || !(0, _orkestrel_contract.isRecord)(workflow) || !(0, _orkestrel_contract.isRecord)(phase) || !(0, _orkestrel_contract.isRecord)(task) || !Object.keys(value).every((key) => key === "task" || key === "phase" || key === "workflow" || key === "status" || key === "result" || key === "timestamp") || !isLifecycleStatus(value.status) || value.status !== task.status || !(0, _orkestrel_contract.isFiniteNumber)(value.timestamp) || value.timestamp < 0 || !(0, _orkestrel_contract.isRecord)(value.task) || !(0, _orkestrel_contract.isRecord)(value.phase) || !(0, _orkestrel_contract.isRecord)(value.workflow) || !Object.keys(value.workflow).every((key) => key === "id" || key === "name" || key === "description") || !Object.keys(value.phase).every((key) => key === "id" || key === "name" || key === "description" || key === "workflow") || !Object.keys(value.task).every((key) => key === "id" || key === "name" || key === "description" || key === "phase")) return false;
|
|
792
|
+
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;
|
|
793
|
+
if (!(0, _orkestrel_contract.isRecord)(value.task.phase) || !(0, _orkestrel_contract.isRecord)(value.task.phase.workflow) || !(0, _orkestrel_contract.isRecord)(value.phase.workflow) || !Object.keys(value.task.phase).every((key) => key === "id" || key === "name" || key === "description" || key === "workflow") || !Object.keys(value.task.phase.workflow).every((key) => key === "id" || key === "name" || key === "description") || !Object.keys(value.phase.workflow).every((key) => key === "id" || key === "name" || key === "description")) return false;
|
|
794
|
+
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;
|
|
795
|
+
if (value.status === "completed") return (0, _orkestrel_contract.isRecord)(value.result) && value.result.success === true && Object.keys(value.result).every((key) => key === "success" || key === "value") && (0, _orkestrel_contract.isJSONValue)(value.result.value);
|
|
796
|
+
if (value.status === "failed") return (0, _orkestrel_contract.isRecord)(value.result) && value.result.success === false && Object.keys(value.result).every((key) => key === "success" || key === "error") && isTaskFailure(value.result.error);
|
|
797
|
+
return false;
|
|
798
|
+
} catch {
|
|
799
|
+
return false;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
function hasWorkflowHandlers(workflow, functions) {
|
|
803
|
+
if ("destroyed" in workflow) {
|
|
804
|
+
for (const phase of workflow.phases.phases()) for (const task of phase.tasks.tasks()) if (task.run !== void 0 && !(0, _orkestrel_contract.isFunction)(task.handler)) return false;
|
|
805
|
+
return true;
|
|
806
|
+
}
|
|
807
|
+
const runs = /* @__PURE__ */ new Set();
|
|
808
|
+
for (const phase of workflow.phases) for (const task of phase.tasks) {
|
|
809
|
+
if (task.run === void 0 || runs.has(task.run)) continue;
|
|
810
|
+
runs.add(task.run);
|
|
811
|
+
if (!(0, _orkestrel_contract.isFunction)(functions?.[task.run])) return false;
|
|
812
|
+
}
|
|
813
|
+
return true;
|
|
814
|
+
}
|
|
815
|
+
/** Locate the nearest identifiable node for an inconsistent owned snapshot. */
|
|
816
|
+
function workflowSnapshotContext(value) {
|
|
817
|
+
if (!(0, _orkestrel_contract.isRecord)(value) || !(0, _orkestrel_contract.isArray)(value.phases)) return void 0;
|
|
818
|
+
for (const phase of value.phases) {
|
|
819
|
+
if (!(0, _orkestrel_contract.isRecord)(phase)) continue;
|
|
820
|
+
const phaseContext = (0, _orkestrel_contract.isNonEmptyString)(phase.id) ? { phase: phase.id } : void 0;
|
|
821
|
+
if (!(0, _orkestrel_contract.isBoolean)(phase.bail) || phase.concurrency !== void 0 && (!(0, _orkestrel_contract.isInteger)(phase.concurrency) || phase.concurrency < 1) || !(0, _orkestrel_contract.isArray)(phase.tasks)) return phaseContext;
|
|
822
|
+
for (const task of phase.tasks) {
|
|
823
|
+
if (!(0, _orkestrel_contract.isRecord)(task)) continue;
|
|
824
|
+
if (task.run !== void 0 && !(0, _orkestrel_contract.isNonEmptyString)(task.run) || task.retries !== void 0 && (!(0, _orkestrel_contract.isInteger)(task.retries) || task.retries < 0) || task.timeout !== void 0 && (!(0, _orkestrel_contract.isInteger)(task.timeout) || task.timeout < 0 || task.timeout > 2147483647) || !(0, _orkestrel_contract.isInteger)(task.attempts) || task.attempts < 0) return {
|
|
825
|
+
...phaseContext ?? {},
|
|
826
|
+
...(0, _orkestrel_contract.isNonEmptyString)(task.id) ? { task: task.id } : {}
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
631
831
|
/**
|
|
632
832
|
* Flatten a nested list of per-phase {@link TaskResult} lists into one positional list
|
|
633
833
|
* — the workflow tier of the result tree, built from each phase's `results()`.
|
|
@@ -872,206 +1072,6 @@ var Scheduler = class {
|
|
|
872
1072
|
}
|
|
873
1073
|
};
|
|
874
1074
|
//#endregion
|
|
875
|
-
//#region src/core/validators.ts
|
|
876
|
-
/** Test the workflow lifecycle vocabulary. */
|
|
877
|
-
function isLifecycleStatus(value) {
|
|
878
|
-
return value === "pending" || value === "running" || value === "completed" || value === "failed" || value === "skipped" || value === "stopped";
|
|
879
|
-
}
|
|
880
|
-
/** Test a normalized persisted task failure. */
|
|
881
|
-
function isTaskFailure(value) {
|
|
882
|
-
try {
|
|
883
|
-
return (0, _orkestrel_contract.isRecord)(value) && Object.keys(value).every((key) => key === "origin" || key === "message") && (value.origin === "handler" || value.origin === "timeout" || value.origin === "recovery") && (0, _orkestrel_contract.isNonEmptyString)(value.message);
|
|
884
|
-
} catch {
|
|
885
|
-
return false;
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
/** Compare two optional description values. */
|
|
889
|
-
function matchesDescription(left, right) {
|
|
890
|
-
return left === right && (left === void 0 || typeof left === "string");
|
|
891
|
-
}
|
|
892
|
-
/** Test a result's lineage against its containing snapshot nodes. */
|
|
893
|
-
function isTaskResult(value, workflow, phase, task) {
|
|
894
|
-
try {
|
|
895
|
-
if (!(0, _orkestrel_contract.isRecord)(value) || !(0, _orkestrel_contract.isRecord)(workflow) || !(0, _orkestrel_contract.isRecord)(phase) || !(0, _orkestrel_contract.isRecord)(task) || !Object.keys(value).every((key) => key === "task" || key === "phase" || key === "workflow" || key === "status" || key === "result" || key === "timestamp") || !isLifecycleStatus(value.status) || value.status !== task.status || !(0, _orkestrel_contract.isFiniteNumber)(value.timestamp) || value.timestamp < 0 || !(0, _orkestrel_contract.isRecord)(value.task) || !(0, _orkestrel_contract.isRecord)(value.phase) || !(0, _orkestrel_contract.isRecord)(value.workflow) || !Object.keys(value.workflow).every((key) => key === "id" || key === "name" || key === "description") || !Object.keys(value.phase).every((key) => key === "id" || key === "name" || key === "description" || key === "workflow") || !Object.keys(value.task).every((key) => key === "id" || key === "name" || key === "description" || key === "phase")) return false;
|
|
896
|
-
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;
|
|
897
|
-
if (!(0, _orkestrel_contract.isRecord)(value.task.phase) || !(0, _orkestrel_contract.isRecord)(value.task.phase.workflow) || !(0, _orkestrel_contract.isRecord)(value.phase.workflow) || !Object.keys(value.task.phase).every((key) => key === "id" || key === "name" || key === "description" || key === "workflow") || !Object.keys(value.task.phase.workflow).every((key) => key === "id" || key === "name" || key === "description") || !Object.keys(value.phase.workflow).every((key) => key === "id" || key === "name" || key === "description")) return false;
|
|
898
|
-
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;
|
|
899
|
-
if (value.status === "completed") return (0, _orkestrel_contract.isRecord)(value.result) && value.result.success === true && Object.keys(value.result).every((key) => key === "success" || key === "value") && (0, _orkestrel_contract.isJSONValue)(value.result.value);
|
|
900
|
-
if (value.status === "failed") return (0, _orkestrel_contract.isRecord)(value.result) && value.result.success === false && Object.keys(value.result).every((key) => key === "success" || key === "error") && isTaskFailure(value.result.error);
|
|
901
|
-
return false;
|
|
902
|
-
} catch {
|
|
903
|
-
return false;
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
/**
|
|
907
|
-
* Validate a safe owned JSON graph as a coherent workflow snapshot.
|
|
908
|
-
*
|
|
909
|
-
* @remarks
|
|
910
|
-
* Callers at hostile boundaries use {@link isWorkflowSnapshot}, which owns the
|
|
911
|
-
* graph first so this semantic pass never observes accessors or prototypes.
|
|
912
|
-
*/
|
|
913
|
-
function isOwnedWorkflowSnapshot(value) {
|
|
914
|
-
try {
|
|
915
|
-
if (!(0, _orkestrel_contract.isRecord)(value) || !Object.keys(value).every((key) => key === "id" || key === "name" || key === "description" || key === "status" || key === "override" || key === "bail" || key === "phases" || key === "created" || key === "updated") || !(0, _orkestrel_contract.isNonEmptyString)(value.id) || !(0, _orkestrel_contract.isNonEmptyString)(value.name) || value.description !== void 0 && typeof value.description !== "string" || !isLifecycleStatus(value.status) || value.override !== void 0 && value.override !== "completed" && value.override !== "skipped" && value.override !== "stopped" || !(0, _orkestrel_contract.isBoolean)(value.bail) || !(0, _orkestrel_contract.isArray)(value.phases) || !(0, _orkestrel_contract.isFiniteNumber)(value.created) || value.created < 0 || !(0, _orkestrel_contract.isFiniteNumber)(value.updated) || value.updated < value.created) return false;
|
|
916
|
-
const phaseIds = /* @__PURE__ */ new Set();
|
|
917
|
-
const derivations = [];
|
|
918
|
-
let frontier = false;
|
|
919
|
-
let running = false;
|
|
920
|
-
let vacuous = true;
|
|
921
|
-
for (const phase of value.phases) {
|
|
922
|
-
if (!(0, _orkestrel_contract.isRecord)(phase) || !Object.keys(phase).every((key) => key === "id" || key === "name" || key === "description" || key === "status" || key === "override" || key === "bail" || key === "concurrency" || key === "tasks") || !(0, _orkestrel_contract.isNonEmptyString)(phase.id) || phaseIds.has(phase.id) || !(0, _orkestrel_contract.isNonEmptyString)(phase.name) || phase.description !== void 0 && typeof phase.description !== "string" || !isLifecycleStatus(phase.status) || phase.override !== void 0 && phase.override !== "skipped" && phase.override !== "stopped" || !(0, _orkestrel_contract.isBoolean)(phase.bail) || phase.concurrency !== void 0 && (!(0, _orkestrel_contract.isInteger)(phase.concurrency) || phase.concurrency < 1) || !(0, _orkestrel_contract.isArray)(phase.tasks)) return false;
|
|
923
|
-
const forced = phase.override === "skipped" || phase.override === "stopped";
|
|
924
|
-
const started = phase.status === "running" || phase.status === "completed" || phase.status === "failed";
|
|
925
|
-
if (!forced && frontier && started || phase.status === "running" && running) return false;
|
|
926
|
-
if (phase.status === "running") running = true;
|
|
927
|
-
if (!forced && (phase.status === "pending" || phase.status === "running" || phase.status === "failed" && phase.bail)) frontier = true;
|
|
928
|
-
phaseIds.add(phase.id);
|
|
929
|
-
const taskIds = /* @__PURE__ */ new Set();
|
|
930
|
-
const statuses = [];
|
|
931
|
-
if (phase.tasks.length > 0) vacuous = false;
|
|
932
|
-
for (const task of phase.tasks) {
|
|
933
|
-
if (!(0, _orkestrel_contract.isRecord)(task) || !Object.keys(task).every((key) => key === "id" || key === "name" || key === "description" || key === "status" || key === "result" || key === "metadata" || key === "attempts" || key === "run" || key === "retries" || key === "timeout" || key === "activity") || !(0, _orkestrel_contract.isNonEmptyString)(task.id) || taskIds.has(task.id) || !(0, _orkestrel_contract.isNonEmptyString)(task.name) || task.description !== void 0 && typeof task.description !== "string" || !isLifecycleStatus(task.status) || !(0, _orkestrel_contract.isRecord)(task.metadata) || !(0, _orkestrel_contract.isJSONValue)(task.metadata) || !(0, _orkestrel_contract.isInteger)(task.attempts) || task.attempts < 0 || task.run !== void 0 && !(0, _orkestrel_contract.isNonEmptyString)(task.run) || task.retries !== void 0 && (!(0, _orkestrel_contract.isInteger)(task.retries) || task.retries < 0) || task.timeout !== void 0 && (!(0, _orkestrel_contract.isInteger)(task.timeout) || task.timeout < 0 || task.timeout > 2147483647)) return false;
|
|
934
|
-
const budget = (task.retries ?? 0) + 1;
|
|
935
|
-
if (task.attempts > budget || task.status === "pending" && task.attempts >= budget) return false;
|
|
936
|
-
if (!(task.activity === void 0 || isTaskActivity(task.activity))) return false;
|
|
937
|
-
if (task.status === "running" || task.status === "completed" || task.status === "failed") {
|
|
938
|
-
if (task.attempts < 1 || task.activity === void 0) return false;
|
|
939
|
-
}
|
|
940
|
-
if (task.status === "pending" && task.activity !== void 0) return false;
|
|
941
|
-
if (task.status === "completed" || task.status === "failed") {
|
|
942
|
-
if (!isTaskResult(task.result, value, phase, task)) return false;
|
|
943
|
-
} else if (task.result !== void 0) return false;
|
|
944
|
-
taskIds.add(task.id);
|
|
945
|
-
statuses.push(task.status);
|
|
946
|
-
}
|
|
947
|
-
const derived = derivePhaseStatus(statuses);
|
|
948
|
-
if (phase.status !== (phase.override ?? derived) || phase.override !== void 0 && phase.status !== phase.override) return false;
|
|
949
|
-
derivations.push({
|
|
950
|
-
status: phase.status,
|
|
951
|
-
bail: phase.bail
|
|
952
|
-
});
|
|
953
|
-
}
|
|
954
|
-
const derived = deriveWorkflowStatus(derivations);
|
|
955
|
-
if (value.override === "completed") return value.status === "completed" && derived === "pending" && vacuous;
|
|
956
|
-
return value.status === (value.override ?? derived);
|
|
957
|
-
} catch {
|
|
958
|
-
return false;
|
|
959
|
-
}
|
|
960
|
-
}
|
|
961
|
-
/** Total hostile-boundary workflow snapshot guard. */
|
|
962
|
-
function isWorkflowSnapshot(value) {
|
|
963
|
-
const cloned = (0, _orkestrel_contract.attempt)(() => (0, _orkestrel_contract.cloneJSONValue)(value));
|
|
964
|
-
return cloned.success && isOwnedWorkflowSnapshot(cloned.value);
|
|
965
|
-
}
|
|
966
|
-
function hasWorkflowHandlers(workflow, functions) {
|
|
967
|
-
if ("destroyed" in workflow) {
|
|
968
|
-
for (const phase of workflow.phases.phases()) for (const task of phase.tasks.tasks()) if (task.run !== void 0 && !(0, _orkestrel_contract.isFunction)(task.handler)) return false;
|
|
969
|
-
return true;
|
|
970
|
-
}
|
|
971
|
-
const runs = /* @__PURE__ */ new Set();
|
|
972
|
-
for (const phase of workflow.phases) for (const task of phase.tasks) {
|
|
973
|
-
if (task.run === void 0 || runs.has(task.run)) continue;
|
|
974
|
-
runs.add(task.run);
|
|
975
|
-
if (!(0, _orkestrel_contract.isFunction)(functions?.[task.run])) return false;
|
|
976
|
-
}
|
|
977
|
-
return true;
|
|
978
|
-
}
|
|
979
|
-
/** Locate the nearest identifiable node for an inconsistent owned snapshot. */
|
|
980
|
-
function workflowSnapshotContext(value) {
|
|
981
|
-
if (!(0, _orkestrel_contract.isRecord)(value) || !(0, _orkestrel_contract.isArray)(value.phases)) return void 0;
|
|
982
|
-
for (const phase of value.phases) {
|
|
983
|
-
if (!(0, _orkestrel_contract.isRecord)(phase)) continue;
|
|
984
|
-
const phaseContext = (0, _orkestrel_contract.isNonEmptyString)(phase.id) ? { phase: phase.id } : void 0;
|
|
985
|
-
if (!(0, _orkestrel_contract.isBoolean)(phase.bail) || phase.concurrency !== void 0 && (!(0, _orkestrel_contract.isInteger)(phase.concurrency) || phase.concurrency < 1) || !(0, _orkestrel_contract.isArray)(phase.tasks)) return phaseContext;
|
|
986
|
-
for (const task of phase.tasks) {
|
|
987
|
-
if (!(0, _orkestrel_contract.isRecord)(task)) continue;
|
|
988
|
-
if (task.run !== void 0 && !(0, _orkestrel_contract.isNonEmptyString)(task.run) || task.retries !== void 0 && (!(0, _orkestrel_contract.isInteger)(task.retries) || task.retries < 0) || task.timeout !== void 0 && (!(0, _orkestrel_contract.isInteger)(task.timeout) || task.timeout < 0 || task.timeout > 2147483647) || !(0, _orkestrel_contract.isInteger)(task.attempts) || task.attempts < 0) return {
|
|
989
|
-
...phaseContext ?? {},
|
|
990
|
-
...(0, _orkestrel_contract.isNonEmptyString)(task.id) ? { task: task.id } : {}
|
|
991
|
-
};
|
|
992
|
-
}
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
/**
|
|
996
|
-
* Test whether an unknown value is a valid whole-frame activity report.
|
|
997
|
-
*/
|
|
998
|
-
function isTaskActivityInput(value) {
|
|
999
|
-
try {
|
|
1000
|
-
if (!(0, _orkestrel_contract.isRecord)(value)) return false;
|
|
1001
|
-
const prototype = Object.getPrototypeOf(value);
|
|
1002
|
-
if (prototype !== Object.prototype && prototype !== null || !Object.keys(value).every((key) => key === "note" || key === "progress" || key === "operations" || key === "constraints")) return false;
|
|
1003
|
-
const note = value.note;
|
|
1004
|
-
const progress = value.progress;
|
|
1005
|
-
const operations = value.operations;
|
|
1006
|
-
const constraints = value.constraints;
|
|
1007
|
-
if (note !== void 0 && !(0, _orkestrel_contract.isNonEmptyString)(note)) return false;
|
|
1008
|
-
if (progress !== void 0) {
|
|
1009
|
-
if (!(0, _orkestrel_contract.isRecord)(progress)) return false;
|
|
1010
|
-
const progressPrototype = Object.getPrototypeOf(progress);
|
|
1011
|
-
if (progressPrototype !== Object.prototype && progressPrototype !== null || !Object.keys(progress).every((key) => key === "current" || key === "total" || key === "unit")) return false;
|
|
1012
|
-
const current = progress.current;
|
|
1013
|
-
const total = progress.total;
|
|
1014
|
-
const unit = progress.unit;
|
|
1015
|
-
if (!(0, _orkestrel_contract.isFiniteNumber)(current) || current < 0 || total !== void 0 && (!(0, _orkestrel_contract.isFiniteNumber)(total) || total < current) || unit !== void 0 && !(0, _orkestrel_contract.isNonEmptyString)(unit)) return false;
|
|
1016
|
-
}
|
|
1017
|
-
if (operations !== void 0) {
|
|
1018
|
-
if (!(0, _orkestrel_contract.isArray)(operations)) return false;
|
|
1019
|
-
const ids = /* @__PURE__ */ new Set();
|
|
1020
|
-
for (const operation of operations) {
|
|
1021
|
-
if (!(0, _orkestrel_contract.isRecord)(operation)) return false;
|
|
1022
|
-
const operationPrototype = Object.getPrototypeOf(operation);
|
|
1023
|
-
if (operationPrototype !== Object.prototype && operationPrototype !== null || !Object.keys(operation).every((key) => key === "id" || key === "name" || key === "started")) return false;
|
|
1024
|
-
const id = operation.id;
|
|
1025
|
-
const name = operation.name;
|
|
1026
|
-
const started = operation.started;
|
|
1027
|
-
if (!(0, _orkestrel_contract.isNonEmptyString)(id) || !(0, _orkestrel_contract.isNonEmptyString)(name) || !(0, _orkestrel_contract.isFiniteNumber)(started) || started < 0 || ids.has(id)) return false;
|
|
1028
|
-
ids.add(id);
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
if (constraints !== void 0) {
|
|
1032
|
-
if (!(0, _orkestrel_contract.isArray)(constraints)) return false;
|
|
1033
|
-
const ids = /* @__PURE__ */ new Set();
|
|
1034
|
-
for (const constraint of constraints) {
|
|
1035
|
-
if (!(0, _orkestrel_contract.isRecord)(constraint)) return false;
|
|
1036
|
-
const constraintPrototype = Object.getPrototypeOf(constraint);
|
|
1037
|
-
if (constraintPrototype !== Object.prototype && constraintPrototype !== null || !Object.keys(constraint).every((key) => key === "id" || key === "name" || key === "started")) return false;
|
|
1038
|
-
const id = constraint.id;
|
|
1039
|
-
const name = constraint.name;
|
|
1040
|
-
const started = constraint.started;
|
|
1041
|
-
if (!(0, _orkestrel_contract.isNonEmptyString)(id) || !(0, _orkestrel_contract.isNonEmptyString)(name) || !(0, _orkestrel_contract.isFiniteNumber)(started) || started < 0 || ids.has(id)) return false;
|
|
1042
|
-
ids.add(id);
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
return true;
|
|
1046
|
-
} catch {
|
|
1047
|
-
return false;
|
|
1048
|
-
}
|
|
1049
|
-
}
|
|
1050
|
-
/**
|
|
1051
|
-
* Test whether an unknown value is valid persisted task activity.
|
|
1052
|
-
*/
|
|
1053
|
-
function isTaskActivity(value) {
|
|
1054
|
-
try {
|
|
1055
|
-
if (!(0, _orkestrel_contract.isRecord)(value)) return false;
|
|
1056
|
-
const prototype = Object.getPrototypeOf(value);
|
|
1057
|
-
if (prototype !== Object.prototype && prototype !== null || !Object.keys(value).every((key) => key === "note" || key === "progress" || key === "operations" || key === "constraints" || key === "updated")) return false;
|
|
1058
|
-
const note = value.note;
|
|
1059
|
-
const progress = value.progress;
|
|
1060
|
-
const operations = value.operations;
|
|
1061
|
-
const constraints = value.constraints;
|
|
1062
|
-
const updated = value.updated;
|
|
1063
|
-
if (operations === void 0 || constraints === void 0 || !(0, _orkestrel_contract.isFiniteNumber)(updated) || updated < 0) return false;
|
|
1064
|
-
return isTaskActivityInput({
|
|
1065
|
-
...note === void 0 ? {} : { note },
|
|
1066
|
-
...progress === void 0 ? {} : { progress },
|
|
1067
|
-
operations,
|
|
1068
|
-
constraints
|
|
1069
|
-
});
|
|
1070
|
-
} catch {
|
|
1071
|
-
return false;
|
|
1072
|
-
}
|
|
1073
|
-
}
|
|
1074
|
-
//#endregion
|
|
1075
1075
|
//#region src/core/cloners.ts
|
|
1076
1076
|
/**
|
|
1077
1077
|
* Validate and own a workflow snapshot before live construction.
|
|
@@ -1328,18 +1328,18 @@ var phaseUpdateShape = (0, _orkestrel_contract.objectShape)({
|
|
|
1328
1328
|
* idle-TTL / eviction — a persisted run-state is durable orchestration state that lives until an
|
|
1329
1329
|
* explicit `delete`. The public surface is EXACTLY `get` / `set` / `delete` — no extra members (the
|
|
1330
1330
|
* §22 method bijection with {@link WorkflowStoreInterface}). Restore stays a caller concern: read a
|
|
1331
|
-
* snapshot back and rebuild the live tree with {@link import('../factories.js').
|
|
1331
|
+
* snapshot back and rebuild the live tree with {@link import('../factories.js').createRestoredWorkflow}.
|
|
1332
1332
|
*
|
|
1333
1333
|
* @example
|
|
1334
1334
|
* ```ts
|
|
1335
1335
|
* import { createMemoryDriver } from '@orkestrel/database'
|
|
1336
|
-
* import { createDatabaseWorkflowStore, createWorkflow,
|
|
1336
|
+
* import { createDatabaseWorkflowStore, createWorkflow, createRestoredWorkflow } from '@orkestrel/workflow'
|
|
1337
1337
|
*
|
|
1338
1338
|
* const store = createDatabaseWorkflowStore(createMemoryDriver()) // a durable driver swaps in here
|
|
1339
1339
|
* const workflow = createWorkflow(definition)
|
|
1340
1340
|
* await store.set(workflow.snapshot()) // persist the run state (one JSON column)
|
|
1341
1341
|
* const snapshot = await store.get(definition.id)
|
|
1342
|
-
* const restored = snapshot &&
|
|
1342
|
+
* const restored = snapshot && createRestoredWorkflow(snapshot) // an identical live tree
|
|
1343
1343
|
* await store.delete(definition.id) // drop it
|
|
1344
1344
|
* ```
|
|
1345
1345
|
*/
|
|
@@ -1399,17 +1399,17 @@ var DatabaseWorkflowStore = class {
|
|
|
1399
1399
|
*
|
|
1400
1400
|
* The public surface is EXACTLY `get` / `set` / `delete` — no extra members (the §22 method
|
|
1401
1401
|
* bijection with {@link WorkflowStoreInterface}). Restore is a caller concern: read a snapshot
|
|
1402
|
-
* back and rebuild the live tree with {@link import('../factories.js').
|
|
1402
|
+
* back and rebuild the live tree with {@link import('../factories.js').createRestoredWorkflow}.
|
|
1403
1403
|
*
|
|
1404
1404
|
* @example
|
|
1405
1405
|
* ```ts
|
|
1406
|
-
* import { createMemoryWorkflowStore, createWorkflow,
|
|
1406
|
+
* import { createMemoryWorkflowStore, createWorkflow, createRestoredWorkflow } from '@orkestrel/workflow'
|
|
1407
1407
|
*
|
|
1408
1408
|
* const store = createMemoryWorkflowStore()
|
|
1409
1409
|
* const workflow = createWorkflow(definition)
|
|
1410
1410
|
* await store.set(workflow.snapshot()) // persist the run state
|
|
1411
1411
|
* const snapshot = await store.get(definition.id)
|
|
1412
|
-
* const restored = snapshot &&
|
|
1412
|
+
* const restored = snapshot && createRestoredWorkflow(snapshot) // an identical live tree
|
|
1413
1413
|
* await store.delete(definition.id) // drop it
|
|
1414
1414
|
* ```
|
|
1415
1415
|
*/
|
|
@@ -2251,7 +2251,7 @@ var PhaseManager = class {
|
|
|
2251
2251
|
* @remarks
|
|
2252
2252
|
* - **Construction.** Built from a {@link WorkflowSnapshot} (the unified input —
|
|
2253
2253
|
* {@link import('./factories.js').createWorkflow} seeds an initial snapshot from a
|
|
2254
|
-
* {@link import('./types.js').WorkflowDefinition}, {@link import('./factories.js').
|
|
2254
|
+
* {@link import('./types.js').WorkflowDefinition}, {@link import('./factories.js').createRestoredWorkflow}
|
|
2255
2255
|
* passes a persisted one). Each child {@link Phase} is wired to escalate to `#recompute`.
|
|
2256
2256
|
* - **Derived status.** `status` is `#override` when forced, else
|
|
2257
2257
|
* {@link deriveWorkflowStatus} over the live phases' statuses feeding `bail`. `failed` is
|
|
@@ -2266,7 +2266,7 @@ var PhaseManager = class {
|
|
|
2266
2266
|
* workflow tier; `phase(id)` + each `phase.task(id)` navigate DOWN, a task's `phase` / `workflow`
|
|
2267
2267
|
* navigate UP.
|
|
2268
2268
|
* - **Snapshot.** `snapshot()` serializes the whole live tree to a {@link WorkflowSnapshot} (pure
|
|
2269
|
-
* JSON); {@link import('./factories.js').
|
|
2269
|
+
* JSON); {@link import('./factories.js').createRestoredWorkflow} rebuilds an equivalent live tree.
|
|
2270
2270
|
* - **Observable (AGENTS §13).** The owned {@link emitter} ({@link WorkflowEventMap}) fires
|
|
2271
2271
|
* `start` / `complete` / `fail` / `pause` / `resume` / `skip` / `stop` after the
|
|
2272
2272
|
* corresponding status or runtime-gate change; the emitter isolates a listener throw and
|
|
@@ -2670,7 +2670,7 @@ var WorkflowManager = class {
|
|
|
2670
2670
|
if (!this.#owns(id, mutation, generation)) return this.#resolve(id, generation);
|
|
2671
2671
|
let workflow;
|
|
2672
2672
|
try {
|
|
2673
|
-
workflow =
|
|
2673
|
+
workflow = createRestoredWorkflow(owned, { ...this.#functions === void 0 ? {} : { functions: this.#functions } });
|
|
2674
2674
|
} catch (error) {
|
|
2675
2675
|
if (!this.#owns(id, mutation, generation)) return this.#resolve(id, generation);
|
|
2676
2676
|
throw error;
|
|
@@ -3979,12 +3979,12 @@ function createWorkflow(definition, options) {
|
|
|
3979
3979
|
return new Workflow(definitionToSnapshot(definition, captured.bail ?? definition.bail ?? false), captured);
|
|
3980
3980
|
}
|
|
3981
3981
|
/**
|
|
3982
|
-
*
|
|
3982
|
+
* Build an equivalent live W-b entity tree from a {@link WorkflowSnapshot} — the
|
|
3983
3983
|
* inverse of {@link WorkflowInterface.snapshot}, restoring structure + each node's status
|
|
3984
3984
|
* + recorded results + positional order + the persisted `#override`.
|
|
3985
3985
|
*
|
|
3986
3986
|
* @remarks
|
|
3987
|
-
* Round-trip fidelity is paramount: a `snapshot()` → `
|
|
3987
|
+
* Round-trip fidelity is paramount: a `snapshot()` → `createRestoredWorkflow()` reproduces the
|
|
3988
3988
|
* same status at every node (each `#override` restored DIRECTLY from the snapshot's own
|
|
3989
3989
|
* `override` field, not guessed from a status divergence), the same recorded
|
|
3990
3990
|
* {@link import('./types.js').TaskResult}s, and the same positional order (an interior
|
|
@@ -4004,18 +4004,18 @@ function createWorkflow(definition, options) {
|
|
|
4004
4004
|
*
|
|
4005
4005
|
* @example
|
|
4006
4006
|
* ```ts
|
|
4007
|
-
* import {
|
|
4007
|
+
* import { createRestoredWorkflow } from '@orkestrel/workflow'
|
|
4008
4008
|
*
|
|
4009
|
-
* const restored =
|
|
4009
|
+
* const restored = createRestoredWorkflow(workflow.snapshot()) // bail comes from the snapshot
|
|
4010
4010
|
* restored.status === workflow.status // true
|
|
4011
4011
|
* ```
|
|
4012
4012
|
*/
|
|
4013
|
-
function
|
|
4013
|
+
function createRestoredWorkflow(snapshot, options) {
|
|
4014
4014
|
const captured = captureWorkflowOptions(options);
|
|
4015
4015
|
return new Workflow(cloneWorkflowSnapshot(snapshot), captured);
|
|
4016
4016
|
}
|
|
4017
4017
|
/**
|
|
4018
|
-
*
|
|
4018
|
+
* Build an interrupted workflow back to life at its remaining retry budget.
|
|
4019
4019
|
*
|
|
4020
4020
|
* @remarks
|
|
4021
4021
|
* Each phase captures every unique initial `run` binding once before constructing tasks. Recovery
|
|
@@ -4024,9 +4024,17 @@ function restoreWorkflow(snapshot, options) {
|
|
|
4024
4024
|
*
|
|
4025
4025
|
* @param snapshot - The hostile persisted snapshot
|
|
4026
4026
|
* @param options - Runtime handlers and entity options
|
|
4027
|
-
* @returns A recoverable live
|
|
4027
|
+
* @returns A recoverable live {@link WorkflowInterface} root
|
|
4028
|
+
*
|
|
4029
|
+
* @example
|
|
4030
|
+
* ```ts
|
|
4031
|
+
* import { createRecoveredWorkflow } from '@orkestrel/workflow'
|
|
4032
|
+
*
|
|
4033
|
+
* const recovered = createRecoveredWorkflow(snapshot, { functions })
|
|
4034
|
+
* recovered.status // 'pending' — interrupted running work returned to its remaining budget
|
|
4035
|
+
* ```
|
|
4028
4036
|
*/
|
|
4029
|
-
function
|
|
4037
|
+
function createRecoveredWorkflow(snapshot, options) {
|
|
4030
4038
|
const captured = captureWorkflowOptions(options);
|
|
4031
4039
|
const owned = cloneWorkflowSnapshot(snapshot);
|
|
4032
4040
|
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 });
|
|
@@ -4035,29 +4043,6 @@ function recoverWorkflow(snapshot, options) {
|
|
|
4035
4043
|
return workflow;
|
|
4036
4044
|
}
|
|
4037
4045
|
/**
|
|
4038
|
-
* Assert that a {@link WorkflowSnapshot} carries a `boolean` `bail` — at the workflow tier AND
|
|
4039
|
-
* on every phase — and that its every node's status (and its `override`, when present) is drawn
|
|
4040
|
-
* from the lifecycle vocabulary, throwing a `RESTORE` {@link WorkflowError} otherwise.
|
|
4041
|
-
*
|
|
4042
|
-
* @remarks
|
|
4043
|
-
* The boundary-narrowing guard (AGENTS §14) for {@link restoreWorkflow}: a snapshot is
|
|
4044
|
-
* untrusted JSON, so a status (or an override) outside
|
|
4045
|
-
* {@link import('./constants.js').WORKFLOW_STATUSES} /
|
|
4046
|
-
* {@link import('./constants.js').PHASE_STATUSES} / {@link import('./constants.js').TASK_STATUSES},
|
|
4047
|
-
* a non-boolean `bail` (the workflow's OR any phase's — both are REQUIRED persisted policy), a
|
|
4048
|
-
* present-but-invalid phase `concurrency` (not a positive integer), or a present-but-invalid task
|
|
4049
|
-
* `run` (an empty string) / `retries` / `timeout` (not a non-negative integer),
|
|
4050
|
-
* is rejected loudly (naming the offending node) rather than silently producing a broken tree.
|
|
4051
|
-
* The `override` / `concurrency` / `run` / `retries` / `timeout` are optional, so each is only
|
|
4052
|
-
* checked WHEN present. Structural shape beyond these fields is the contract's concern; this
|
|
4053
|
-
* guards exactly the fields the live state machine reads back.
|
|
4054
|
-
*
|
|
4055
|
-
* @param snapshot - The snapshot to validate
|
|
4056
|
-
*/
|
|
4057
|
-
function assertSnapshot(snapshot) {
|
|
4058
|
-
cloneWorkflowSnapshot(snapshot);
|
|
4059
|
-
}
|
|
4060
|
-
/**
|
|
4061
4046
|
* Create the in-memory durable {@link WorkflowStoreInterface} — a process-lifetime
|
|
4062
4047
|
* {@link MemoryWorkflowStore} persisting {@link WorkflowSnapshot}s by workflow id, the DEFAULT
|
|
4063
4048
|
* backend behind the W-d persistence seam.
|
|
@@ -4070,19 +4055,19 @@ function assertSnapshot(snapshot) {
|
|
|
4070
4055
|
* {@link createDatabaseWorkflowStore} (the snapshot as one opaque JSON column over a `databases`
|
|
4071
4056
|
* table) — for a DURABLE store (run-state surviving a restart) pass it a JSON / SQLite / IndexedDB
|
|
4072
4057
|
* driver, and it swaps in WITHOUT touching the runner or the entity tree. Restore stays a caller
|
|
4073
|
-
* concern: read a snapshot back and rebuild the live tree with {@link
|
|
4058
|
+
* concern: read a snapshot back and rebuild the live tree with {@link createRestoredWorkflow}.
|
|
4074
4059
|
*
|
|
4075
4060
|
* @returns A memory-backed {@link WorkflowStoreInterface}
|
|
4076
4061
|
*
|
|
4077
4062
|
* @example
|
|
4078
4063
|
* ```ts
|
|
4079
|
-
* import { createMemoryWorkflowStore, createWorkflow,
|
|
4064
|
+
* import { createMemoryWorkflowStore, createWorkflow, createRestoredWorkflow } from '@orkestrel/workflow'
|
|
4080
4065
|
*
|
|
4081
4066
|
* const store = createMemoryWorkflowStore()
|
|
4082
4067
|
* const workflow = createWorkflow(definition)
|
|
4083
4068
|
* await store.set(workflow.snapshot()) // persist the run state
|
|
4084
4069
|
* const snapshot = await store.get(definition.id)
|
|
4085
|
-
* const restored = snapshot &&
|
|
4070
|
+
* const restored = snapshot && createRestoredWorkflow(snapshot) // an identical live tree
|
|
4086
4071
|
* ```
|
|
4087
4072
|
*/
|
|
4088
4073
|
function createMemoryWorkflowStore() {
|
|
@@ -4113,13 +4098,13 @@ function createMemoryWorkflowStore() {
|
|
|
4113
4098
|
* @example
|
|
4114
4099
|
* ```ts
|
|
4115
4100
|
* import { createMemoryDriver } from '@orkestrel/database'
|
|
4116
|
-
* import { createDatabaseWorkflowStore, createWorkflow,
|
|
4101
|
+
* import { createDatabaseWorkflowStore, createWorkflow, createRestoredWorkflow } from '@orkestrel/workflow'
|
|
4117
4102
|
*
|
|
4118
4103
|
* const store = createDatabaseWorkflowStore(createMemoryDriver()) // a durable driver swaps in here
|
|
4119
4104
|
* const workflow = createWorkflow(definition)
|
|
4120
4105
|
* await store.set(workflow.snapshot()) // persist the run state (one JSON column)
|
|
4121
4106
|
* const snapshot = await store.get(definition.id)
|
|
4122
|
-
* const restored = snapshot &&
|
|
4107
|
+
* const restored = snapshot && createRestoredWorkflow(snapshot) // an identical live tree
|
|
4123
4108
|
* ```
|
|
4124
4109
|
*/
|
|
4125
4110
|
function createDatabaseWorkflowStore(driver = (0, _orkestrel_database.createMemoryDriver)()) {
|
|
@@ -4188,11 +4173,11 @@ function createWorkflowRunner(options) {
|
|
|
4188
4173
|
* @remarks
|
|
4189
4174
|
* `options.functions` flows into every workflow the manager mints (`add`, via
|
|
4190
4175
|
* {@link createWorkflow}) or hydrates (`open`'s registry-miss path, via
|
|
4191
|
-
* {@link
|
|
4176
|
+
* {@link createRestoredWorkflow}), so a hydrated workflow is RUNNABLE rather than a dead snapshot
|
|
4192
4177
|
* mirror. `options.store` is the EXACT analogue of the twins' `store` seam — omitted ⇒ the
|
|
4193
4178
|
* manager is registry-only (`open` resolves only what is registered, `save` is a no-op). This
|
|
4194
4179
|
* is PURELY ADDITIVE: direct {@link WorkflowStoreInterface} use and
|
|
4195
|
-
* {@link
|
|
4180
|
+
* {@link createRestoredWorkflow} remain valid — the manager is one more caller-driven persistence
|
|
4196
4181
|
* seam, not a replacement.
|
|
4197
4182
|
*
|
|
4198
4183
|
* @param options - The optional `store` seam and the `functions` registry threaded into every mint/hydrate
|
|
@@ -4331,7 +4316,6 @@ exports.WorkflowError = WorkflowError;
|
|
|
4331
4316
|
exports.WorkflowManager = WorkflowManager;
|
|
4332
4317
|
exports.WorkflowPersistence = WorkflowPersistence;
|
|
4333
4318
|
exports.WorkflowRunner = WorkflowRunner;
|
|
4334
|
-
exports.assertSnapshot = assertSnapshot;
|
|
4335
4319
|
exports.buildPhaseContext = buildPhaseContext;
|
|
4336
4320
|
exports.buildTaskContext = buildTaskContext;
|
|
4337
4321
|
exports.buildWorkflowContext = buildWorkflowContext;
|
|
@@ -4343,6 +4327,8 @@ exports.collectResults = collectResults;
|
|
|
4343
4327
|
exports.createDatabaseWorkflowStore = createDatabaseWorkflowStore;
|
|
4344
4328
|
exports.createDeferred = createDeferred;
|
|
4345
4329
|
exports.createMemoryWorkflowStore = createMemoryWorkflowStore;
|
|
4330
|
+
exports.createRecoveredWorkflow = createRecoveredWorkflow;
|
|
4331
|
+
exports.createRestoredWorkflow = createRestoredWorkflow;
|
|
4346
4332
|
exports.createRunner = createRunner;
|
|
4347
4333
|
exports.createScheduler = createScheduler;
|
|
4348
4334
|
exports.createWorkflow = createWorkflow;
|
|
@@ -4373,10 +4359,8 @@ exports.parkSignal = parkSignal;
|
|
|
4373
4359
|
exports.phaseDefinitionToSnapshot = phaseDefinitionToSnapshot;
|
|
4374
4360
|
exports.phaseShape = phaseShape;
|
|
4375
4361
|
exports.phaseUpdateShape = phaseUpdateShape;
|
|
4376
|
-
exports.recoverWorkflow = recoverWorkflow;
|
|
4377
4362
|
exports.recoverWorkflowSnapshot = recoverWorkflowSnapshot;
|
|
4378
4363
|
exports.resolveTaskSilence = resolveTaskSilence;
|
|
4379
|
-
exports.restoreWorkflow = restoreWorkflow;
|
|
4380
4364
|
exports.scheduleHost = scheduleHost;
|
|
4381
4365
|
exports.success = success;
|
|
4382
4366
|
exports.taskDefinitionToSnapshot = taskDefinitionToSnapshot;
|