@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.cjs
CHANGED
|
@@ -110,6 +110,204 @@ var DEFAULT_PHASE_CONCURRENCY = 1024;
|
|
|
110
110
|
*/
|
|
111
111
|
var MAX_TIMER_MS = 2147483647;
|
|
112
112
|
//#endregion
|
|
113
|
+
//#region src/core/errors.ts
|
|
114
|
+
/**
|
|
115
|
+
* An error raised by the workflow runtime.
|
|
116
|
+
*
|
|
117
|
+
* @remarks
|
|
118
|
+
* Carries a {@link WorkflowErrorCode} and an optional `context` bag naming the
|
|
119
|
+
* offending node id / status / parameter. Raised for an illegal lifecycle transition
|
|
120
|
+
* (`TRANSITION`), a structurally invalid {@link import('./types.js').WorkflowSnapshot}
|
|
121
|
+
* boundary (`RESTORE`), a refused structural/activity edit (`MUTATION`), or a host
|
|
122
|
+
* schedule refused before arming because the caller's `signal` is not a native
|
|
123
|
+
* `AbortSignal` (`SCHEDULE`, delivered as a rejected promise).
|
|
124
|
+
*/
|
|
125
|
+
var WorkflowError = class extends Error {
|
|
126
|
+
code;
|
|
127
|
+
context;
|
|
128
|
+
constructor(code, message, context) {
|
|
129
|
+
super(message);
|
|
130
|
+
this.name = "WorkflowError";
|
|
131
|
+
this.code = code;
|
|
132
|
+
if (context !== void 0) this.context = context;
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* Narrow an unknown caught value to a {@link WorkflowError}.
|
|
137
|
+
*
|
|
138
|
+
* @param value - The value to test (typically a `catch` binding)
|
|
139
|
+
* @returns `true` when `value` is a {@link WorkflowError}
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* ```ts
|
|
143
|
+
* try {
|
|
144
|
+
* task.complete('done')
|
|
145
|
+
* } catch (error) {
|
|
146
|
+
* if (isWorkflowError(error) && error.code === 'TRANSITION') retry()
|
|
147
|
+
* }
|
|
148
|
+
* ```
|
|
149
|
+
*/
|
|
150
|
+
function isWorkflowError(value) {
|
|
151
|
+
try {
|
|
152
|
+
return value instanceof WorkflowError;
|
|
153
|
+
} catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
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
|
|
113
311
|
//#region src/core/helpers.ts
|
|
114
312
|
/**
|
|
115
313
|
* Capture every top-level {@link WorkflowOptions} value exactly once into an owned plain bag.
|
|
@@ -583,6 +781,53 @@ function recoverWorkflowSnapshot(snapshot) {
|
|
|
583
781
|
updated: now
|
|
584
782
|
};
|
|
585
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
|
+
}
|
|
586
831
|
/**
|
|
587
832
|
* Flatten a nested list of per-phase {@link TaskResult} lists into one positional list
|
|
588
833
|
* — the workflow tier of the result tree, built from each phase's `results()`.
|
|
@@ -672,6 +917,16 @@ function createDeferred() {
|
|
|
672
917
|
* Schedule one cancellable host operation behind an owned settlement signal.
|
|
673
918
|
*
|
|
674
919
|
* @remarks
|
|
920
|
+
* A defined `signal` that is not a native `AbortSignal` is refused before anything is armed, as a
|
|
921
|
+
* rejected promise carrying a {@link import('./errors.js').WorkflowError} with the `SCHEDULE` code.
|
|
922
|
+
* Rejecting rather than throwing keeps every caller on one settlement path, so a backend never has
|
|
923
|
+
* to guard the call itself.
|
|
924
|
+
*
|
|
925
|
+
* The guard is necessary but not sufficient, so linking stays contained. A `Proxy` over a native
|
|
926
|
+
* signal passes the guard and can still make linking throw from a trap, and that escape would be
|
|
927
|
+
* synchronous — the one shape every caller here is built not to expect. Containment turns it into
|
|
928
|
+
* the same `SCHEDULE` rejection, so setup has exactly one failure shape however hostile the input.
|
|
929
|
+
*
|
|
675
930
|
* The completion and failure paths each own an {@link AbortController}; their native composite is
|
|
676
931
|
* linked to the optional caller signal before `start` can arm host work. Scheduler backends attach
|
|
677
932
|
* only to that safe composite, so caller mutation of `addEventListener` or `removeEventListener`
|
|
@@ -683,16 +938,18 @@ function createDeferred() {
|
|
|
683
938
|
*
|
|
684
939
|
* @param start - Arm host work and return its cancellation closure
|
|
685
940
|
* @param signal - Optional caller cancellation signal
|
|
686
|
-
* @returns A promise settled exactly once by completion, host failure,
|
|
941
|
+
* @returns A promise settled exactly once by an invalid-signal refusal, completion, host failure,
|
|
942
|
+
* or caller abort
|
|
687
943
|
*/
|
|
688
944
|
function scheduleHost(start, signal) {
|
|
945
|
+
if (signal !== void 0 && !(0, _orkestrel_abort.isAbortSignal)(signal)) return Promise.reject(new WorkflowError("SCHEDULE", "scheduleHost signal must be an AbortSignal", { signal: typeof signal }));
|
|
689
946
|
const completion = new AbortController();
|
|
690
947
|
const failed = new AbortController();
|
|
691
948
|
let settled;
|
|
692
949
|
try {
|
|
693
950
|
settled = (0, _orkestrel_abort.linkSignal)(AbortSignal.any([completion.signal, failed.signal]), signal);
|
|
694
|
-
} catch
|
|
695
|
-
return Promise.reject(
|
|
951
|
+
} catch {
|
|
952
|
+
return Promise.reject(new WorkflowError("SCHEDULE", "scheduleHost could not link the caller signal", { signal: typeof signal }));
|
|
696
953
|
}
|
|
697
954
|
if (settled.aborted) return Promise.reject(settled.reason);
|
|
698
955
|
return new Promise((resolve, reject) => {
|
|
@@ -815,249 +1072,6 @@ var Scheduler = class {
|
|
|
815
1072
|
}
|
|
816
1073
|
};
|
|
817
1074
|
//#endregion
|
|
818
|
-
//#region src/core/errors.ts
|
|
819
|
-
/**
|
|
820
|
-
* An error raised by the workflow runtime.
|
|
821
|
-
*
|
|
822
|
-
* @remarks
|
|
823
|
-
* Carries a {@link WorkflowErrorCode} and an optional `context` bag naming the
|
|
824
|
-
* offending node id / status. Raised for an illegal lifecycle transition
|
|
825
|
-
* (`TRANSITION`), a structurally invalid {@link import('./types.js').WorkflowSnapshot}
|
|
826
|
-
* boundary (`RESTORE`), or a refused structural/activity edit (`MUTATION`).
|
|
827
|
-
*/
|
|
828
|
-
var WorkflowError = class extends Error {
|
|
829
|
-
code;
|
|
830
|
-
context;
|
|
831
|
-
constructor(code, message, context) {
|
|
832
|
-
super(message);
|
|
833
|
-
this.name = "WorkflowError";
|
|
834
|
-
this.code = code;
|
|
835
|
-
if (context !== void 0) this.context = context;
|
|
836
|
-
}
|
|
837
|
-
};
|
|
838
|
-
/**
|
|
839
|
-
* Narrow an unknown caught value to a {@link WorkflowError}.
|
|
840
|
-
*
|
|
841
|
-
* @param value - The value to test (typically a `catch` binding)
|
|
842
|
-
* @returns `true` when `value` is a {@link WorkflowError}
|
|
843
|
-
*
|
|
844
|
-
* @example
|
|
845
|
-
* ```ts
|
|
846
|
-
* try {
|
|
847
|
-
* task.complete('done')
|
|
848
|
-
* } catch (error) {
|
|
849
|
-
* if (isWorkflowError(error) && error.code === 'TRANSITION') retry()
|
|
850
|
-
* }
|
|
851
|
-
* ```
|
|
852
|
-
*/
|
|
853
|
-
function isWorkflowError(value) {
|
|
854
|
-
try {
|
|
855
|
-
return value instanceof WorkflowError;
|
|
856
|
-
} catch {
|
|
857
|
-
return false;
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
//#endregion
|
|
861
|
-
//#region src/core/validators.ts
|
|
862
|
-
/** Test the workflow lifecycle vocabulary. */
|
|
863
|
-
function isLifecycleStatus(value) {
|
|
864
|
-
return value === "pending" || value === "running" || value === "completed" || value === "failed" || value === "skipped" || value === "stopped";
|
|
865
|
-
}
|
|
866
|
-
/** Test a normalized persisted task failure. */
|
|
867
|
-
function isTaskFailure(value) {
|
|
868
|
-
try {
|
|
869
|
-
return (0, _orkestrel_contract.isRecord)(value) && Object.keys(value).every((key) => key === "origin" || key === "message") && (value.origin === "handler" || value.origin === "timeout" || value.origin === "recovery") && (0, _orkestrel_contract.isNonEmptyString)(value.message);
|
|
870
|
-
} catch {
|
|
871
|
-
return false;
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
/** Compare two optional description values. */
|
|
875
|
-
function matchesDescription(left, right) {
|
|
876
|
-
return left === right && (left === void 0 || typeof left === "string");
|
|
877
|
-
}
|
|
878
|
-
/** Test a result's lineage against its containing snapshot nodes. */
|
|
879
|
-
function isTaskResult(value, workflow, phase, task) {
|
|
880
|
-
try {
|
|
881
|
-
if (!(0, _orkestrel_contract.isRecord)(value) || !(0, _orkestrel_contract.isRecord)(workflow) || !(0, _orkestrel_contract.isRecord)(phase) || !(0, _orkestrel_contract.isRecord)(task) || !Object.keys(value).every((key) => key === "task" || key === "phase" || key === "workflow" || key === "status" || key === "result" || key === "timestamp") || !isLifecycleStatus(value.status) || value.status !== task.status || !(0, _orkestrel_contract.isFiniteNumber)(value.timestamp) || value.timestamp < 0 || !(0, _orkestrel_contract.isRecord)(value.task) || !(0, _orkestrel_contract.isRecord)(value.phase) || !(0, _orkestrel_contract.isRecord)(value.workflow) || !Object.keys(value.workflow).every((key) => key === "id" || key === "name" || key === "description") || !Object.keys(value.phase).every((key) => key === "id" || key === "name" || key === "description" || key === "workflow") || !Object.keys(value.task).every((key) => key === "id" || key === "name" || key === "description" || key === "phase")) return false;
|
|
882
|
-
if (value.task.id !== task.id || value.task.name !== task.name || !matchesDescription(value.task.description, task.description) || value.phase.id !== phase.id || value.phase.name !== phase.name || !matchesDescription(value.phase.description, phase.description) || value.workflow.id !== workflow.id || value.workflow.name !== workflow.name || !matchesDescription(value.workflow.description, workflow.description)) return false;
|
|
883
|
-
if (!(0, _orkestrel_contract.isRecord)(value.task.phase) || !(0, _orkestrel_contract.isRecord)(value.task.phase.workflow) || !(0, _orkestrel_contract.isRecord)(value.phase.workflow) || !Object.keys(value.task.phase).every((key) => key === "id" || key === "name" || key === "description" || key === "workflow") || !Object.keys(value.task.phase.workflow).every((key) => key === "id" || key === "name" || key === "description") || !Object.keys(value.phase.workflow).every((key) => key === "id" || key === "name" || key === "description")) return false;
|
|
884
|
-
if (value.task.phase.id !== phase.id || value.task.phase.name !== phase.name || !matchesDescription(value.task.phase.description, phase.description) || value.phase.workflow.id !== workflow.id || value.phase.workflow.name !== workflow.name || !matchesDescription(value.phase.workflow.description, workflow.description) || value.task.phase.workflow.id !== workflow.id || value.task.phase.workflow.name !== workflow.name || !matchesDescription(value.task.phase.workflow.description, workflow.description)) return false;
|
|
885
|
-
if (value.status === "completed") return (0, _orkestrel_contract.isRecord)(value.result) && value.result.success === true && Object.keys(value.result).every((key) => key === "success" || key === "value") && (0, _orkestrel_contract.isJSONValue)(value.result.value);
|
|
886
|
-
if (value.status === "failed") return (0, _orkestrel_contract.isRecord)(value.result) && value.result.success === false && Object.keys(value.result).every((key) => key === "success" || key === "error") && isTaskFailure(value.result.error);
|
|
887
|
-
return false;
|
|
888
|
-
} catch {
|
|
889
|
-
return false;
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
/**
|
|
893
|
-
* Validate a safe owned JSON graph as a coherent workflow snapshot.
|
|
894
|
-
*
|
|
895
|
-
* @remarks
|
|
896
|
-
* Callers at hostile boundaries use {@link isWorkflowSnapshot}, which owns the
|
|
897
|
-
* graph first so this semantic pass never observes accessors or prototypes.
|
|
898
|
-
*/
|
|
899
|
-
function isOwnedWorkflowSnapshot(value) {
|
|
900
|
-
try {
|
|
901
|
-
if (!(0, _orkestrel_contract.isRecord)(value) || !Object.keys(value).every((key) => key === "id" || key === "name" || key === "description" || key === "status" || key === "override" || key === "bail" || key === "phases" || key === "created" || key === "updated") || !(0, _orkestrel_contract.isNonEmptyString)(value.id) || !(0, _orkestrel_contract.isNonEmptyString)(value.name) || value.description !== void 0 && typeof value.description !== "string" || !isLifecycleStatus(value.status) || value.override !== void 0 && value.override !== "completed" && value.override !== "skipped" && value.override !== "stopped" || !(0, _orkestrel_contract.isBoolean)(value.bail) || !(0, _orkestrel_contract.isArray)(value.phases) || !(0, _orkestrel_contract.isFiniteNumber)(value.created) || value.created < 0 || !(0, _orkestrel_contract.isFiniteNumber)(value.updated) || value.updated < value.created) return false;
|
|
902
|
-
const phaseIds = /* @__PURE__ */ new Set();
|
|
903
|
-
const derivations = [];
|
|
904
|
-
let frontier = false;
|
|
905
|
-
let running = false;
|
|
906
|
-
let vacuous = true;
|
|
907
|
-
for (const phase of value.phases) {
|
|
908
|
-
if (!(0, _orkestrel_contract.isRecord)(phase) || !Object.keys(phase).every((key) => key === "id" || key === "name" || key === "description" || key === "status" || key === "override" || key === "bail" || key === "concurrency" || key === "tasks") || !(0, _orkestrel_contract.isNonEmptyString)(phase.id) || phaseIds.has(phase.id) || !(0, _orkestrel_contract.isNonEmptyString)(phase.name) || phase.description !== void 0 && typeof phase.description !== "string" || !isLifecycleStatus(phase.status) || phase.override !== void 0 && phase.override !== "skipped" && phase.override !== "stopped" || !(0, _orkestrel_contract.isBoolean)(phase.bail) || phase.concurrency !== void 0 && (!(0, _orkestrel_contract.isInteger)(phase.concurrency) || phase.concurrency < 1) || !(0, _orkestrel_contract.isArray)(phase.tasks)) return false;
|
|
909
|
-
const forced = phase.override === "skipped" || phase.override === "stopped";
|
|
910
|
-
const started = phase.status === "running" || phase.status === "completed" || phase.status === "failed";
|
|
911
|
-
if (!forced && frontier && started || phase.status === "running" && running) return false;
|
|
912
|
-
if (phase.status === "running") running = true;
|
|
913
|
-
if (!forced && (phase.status === "pending" || phase.status === "running" || phase.status === "failed" && phase.bail)) frontier = true;
|
|
914
|
-
phaseIds.add(phase.id);
|
|
915
|
-
const taskIds = /* @__PURE__ */ new Set();
|
|
916
|
-
const statuses = [];
|
|
917
|
-
if (phase.tasks.length > 0) vacuous = false;
|
|
918
|
-
for (const task of phase.tasks) {
|
|
919
|
-
if (!(0, _orkestrel_contract.isRecord)(task) || !Object.keys(task).every((key) => key === "id" || key === "name" || key === "description" || key === "status" || key === "result" || key === "metadata" || key === "attempts" || key === "run" || key === "retries" || key === "timeout" || key === "activity") || !(0, _orkestrel_contract.isNonEmptyString)(task.id) || taskIds.has(task.id) || !(0, _orkestrel_contract.isNonEmptyString)(task.name) || task.description !== void 0 && typeof task.description !== "string" || !isLifecycleStatus(task.status) || !(0, _orkestrel_contract.isRecord)(task.metadata) || !(0, _orkestrel_contract.isJSONValue)(task.metadata) || !(0, _orkestrel_contract.isInteger)(task.attempts) || task.attempts < 0 || task.run !== void 0 && !(0, _orkestrel_contract.isNonEmptyString)(task.run) || task.retries !== void 0 && (!(0, _orkestrel_contract.isInteger)(task.retries) || task.retries < 0) || task.timeout !== void 0 && (!(0, _orkestrel_contract.isInteger)(task.timeout) || task.timeout < 0 || task.timeout > 2147483647)) return false;
|
|
920
|
-
const budget = (task.retries ?? 0) + 1;
|
|
921
|
-
if (task.attempts > budget || task.status === "pending" && task.attempts >= budget) return false;
|
|
922
|
-
if (!(task.activity === void 0 || isTaskActivity(task.activity))) return false;
|
|
923
|
-
if (task.status === "running" || task.status === "completed" || task.status === "failed") {
|
|
924
|
-
if (task.attempts < 1 || task.activity === void 0) return false;
|
|
925
|
-
}
|
|
926
|
-
if (task.status === "pending" && task.activity !== void 0) return false;
|
|
927
|
-
if (task.status === "completed" || task.status === "failed") {
|
|
928
|
-
if (!isTaskResult(task.result, value, phase, task)) return false;
|
|
929
|
-
} else if (task.result !== void 0) return false;
|
|
930
|
-
taskIds.add(task.id);
|
|
931
|
-
statuses.push(task.status);
|
|
932
|
-
}
|
|
933
|
-
const derived = derivePhaseStatus(statuses);
|
|
934
|
-
if (phase.status !== (phase.override ?? derived) || phase.override !== void 0 && phase.status !== phase.override) return false;
|
|
935
|
-
derivations.push({
|
|
936
|
-
status: phase.status,
|
|
937
|
-
bail: phase.bail
|
|
938
|
-
});
|
|
939
|
-
}
|
|
940
|
-
const derived = deriveWorkflowStatus(derivations);
|
|
941
|
-
if (value.override === "completed") return value.status === "completed" && derived === "pending" && vacuous;
|
|
942
|
-
return value.status === (value.override ?? derived);
|
|
943
|
-
} catch {
|
|
944
|
-
return false;
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
/** Total hostile-boundary workflow snapshot guard. */
|
|
948
|
-
function isWorkflowSnapshot(value) {
|
|
949
|
-
const cloned = (0, _orkestrel_contract.attempt)(() => (0, _orkestrel_contract.cloneJSONValue)(value));
|
|
950
|
-
return cloned.success && isOwnedWorkflowSnapshot(cloned.value);
|
|
951
|
-
}
|
|
952
|
-
function hasWorkflowHandlers(workflow, functions) {
|
|
953
|
-
if ("destroyed" in workflow) {
|
|
954
|
-
for (const phase of workflow.phases.phases()) for (const task of phase.tasks.tasks()) if (task.run !== void 0 && !(0, _orkestrel_contract.isFunction)(task.handler)) return false;
|
|
955
|
-
return true;
|
|
956
|
-
}
|
|
957
|
-
const runs = /* @__PURE__ */ new Set();
|
|
958
|
-
for (const phase of workflow.phases) for (const task of phase.tasks) {
|
|
959
|
-
if (task.run === void 0 || runs.has(task.run)) continue;
|
|
960
|
-
runs.add(task.run);
|
|
961
|
-
if (!(0, _orkestrel_contract.isFunction)(functions?.[task.run])) return false;
|
|
962
|
-
}
|
|
963
|
-
return true;
|
|
964
|
-
}
|
|
965
|
-
/** Locate the nearest identifiable node for an inconsistent owned snapshot. */
|
|
966
|
-
function workflowSnapshotContext(value) {
|
|
967
|
-
if (!(0, _orkestrel_contract.isRecord)(value) || !(0, _orkestrel_contract.isArray)(value.phases)) return void 0;
|
|
968
|
-
for (const phase of value.phases) {
|
|
969
|
-
if (!(0, _orkestrel_contract.isRecord)(phase)) continue;
|
|
970
|
-
const phaseContext = (0, _orkestrel_contract.isNonEmptyString)(phase.id) ? { phase: phase.id } : void 0;
|
|
971
|
-
if (!(0, _orkestrel_contract.isBoolean)(phase.bail) || phase.concurrency !== void 0 && (!(0, _orkestrel_contract.isInteger)(phase.concurrency) || phase.concurrency < 1) || !(0, _orkestrel_contract.isArray)(phase.tasks)) return phaseContext;
|
|
972
|
-
for (const task of phase.tasks) {
|
|
973
|
-
if (!(0, _orkestrel_contract.isRecord)(task)) continue;
|
|
974
|
-
if (task.run !== void 0 && !(0, _orkestrel_contract.isNonEmptyString)(task.run) || task.retries !== void 0 && (!(0, _orkestrel_contract.isInteger)(task.retries) || task.retries < 0) || task.timeout !== void 0 && (!(0, _orkestrel_contract.isInteger)(task.timeout) || task.timeout < 0 || task.timeout > 2147483647) || !(0, _orkestrel_contract.isInteger)(task.attempts) || task.attempts < 0) return {
|
|
975
|
-
...phaseContext ?? {},
|
|
976
|
-
...(0, _orkestrel_contract.isNonEmptyString)(task.id) ? { task: task.id } : {}
|
|
977
|
-
};
|
|
978
|
-
}
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
/**
|
|
982
|
-
* Test whether an unknown value is a valid whole-frame activity report.
|
|
983
|
-
*/
|
|
984
|
-
function isTaskActivityInput(value) {
|
|
985
|
-
try {
|
|
986
|
-
if (!(0, _orkestrel_contract.isRecord)(value)) return false;
|
|
987
|
-
const prototype = Object.getPrototypeOf(value);
|
|
988
|
-
if (prototype !== Object.prototype && prototype !== null || !Object.keys(value).every((key) => key === "note" || key === "progress" || key === "operations" || key === "constraints")) return false;
|
|
989
|
-
const note = value.note;
|
|
990
|
-
const progress = value.progress;
|
|
991
|
-
const operations = value.operations;
|
|
992
|
-
const constraints = value.constraints;
|
|
993
|
-
if (note !== void 0 && !(0, _orkestrel_contract.isNonEmptyString)(note)) return false;
|
|
994
|
-
if (progress !== void 0) {
|
|
995
|
-
if (!(0, _orkestrel_contract.isRecord)(progress)) return false;
|
|
996
|
-
const progressPrototype = Object.getPrototypeOf(progress);
|
|
997
|
-
if (progressPrototype !== Object.prototype && progressPrototype !== null || !Object.keys(progress).every((key) => key === "current" || key === "total" || key === "unit")) return false;
|
|
998
|
-
const current = progress.current;
|
|
999
|
-
const total = progress.total;
|
|
1000
|
-
const unit = progress.unit;
|
|
1001
|
-
if (!(0, _orkestrel_contract.isFiniteNumber)(current) || current < 0 || total !== void 0 && (!(0, _orkestrel_contract.isFiniteNumber)(total) || total < current) || unit !== void 0 && !(0, _orkestrel_contract.isNonEmptyString)(unit)) return false;
|
|
1002
|
-
}
|
|
1003
|
-
if (operations !== void 0) {
|
|
1004
|
-
if (!(0, _orkestrel_contract.isArray)(operations)) return false;
|
|
1005
|
-
const ids = /* @__PURE__ */ new Set();
|
|
1006
|
-
for (const operation of operations) {
|
|
1007
|
-
if (!(0, _orkestrel_contract.isRecord)(operation)) return false;
|
|
1008
|
-
const operationPrototype = Object.getPrototypeOf(operation);
|
|
1009
|
-
if (operationPrototype !== Object.prototype && operationPrototype !== null || !Object.keys(operation).every((key) => key === "id" || key === "name" || key === "started")) return false;
|
|
1010
|
-
const id = operation.id;
|
|
1011
|
-
const name = operation.name;
|
|
1012
|
-
const started = operation.started;
|
|
1013
|
-
if (!(0, _orkestrel_contract.isNonEmptyString)(id) || !(0, _orkestrel_contract.isNonEmptyString)(name) || !(0, _orkestrel_contract.isFiniteNumber)(started) || started < 0 || ids.has(id)) return false;
|
|
1014
|
-
ids.add(id);
|
|
1015
|
-
}
|
|
1016
|
-
}
|
|
1017
|
-
if (constraints !== void 0) {
|
|
1018
|
-
if (!(0, _orkestrel_contract.isArray)(constraints)) return false;
|
|
1019
|
-
const ids = /* @__PURE__ */ new Set();
|
|
1020
|
-
for (const constraint of constraints) {
|
|
1021
|
-
if (!(0, _orkestrel_contract.isRecord)(constraint)) return false;
|
|
1022
|
-
const constraintPrototype = Object.getPrototypeOf(constraint);
|
|
1023
|
-
if (constraintPrototype !== Object.prototype && constraintPrototype !== null || !Object.keys(constraint).every((key) => key === "id" || key === "name" || key === "started")) return false;
|
|
1024
|
-
const id = constraint.id;
|
|
1025
|
-
const name = constraint.name;
|
|
1026
|
-
const started = constraint.started;
|
|
1027
|
-
if (!(0, _orkestrel_contract.isNonEmptyString)(id) || !(0, _orkestrel_contract.isNonEmptyString)(name) || !(0, _orkestrel_contract.isFiniteNumber)(started) || started < 0 || ids.has(id)) return false;
|
|
1028
|
-
ids.add(id);
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
return true;
|
|
1032
|
-
} catch {
|
|
1033
|
-
return false;
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
/**
|
|
1037
|
-
* Test whether an unknown value is valid persisted task activity.
|
|
1038
|
-
*/
|
|
1039
|
-
function isTaskActivity(value) {
|
|
1040
|
-
try {
|
|
1041
|
-
if (!(0, _orkestrel_contract.isRecord)(value)) return false;
|
|
1042
|
-
const prototype = Object.getPrototypeOf(value);
|
|
1043
|
-
if (prototype !== Object.prototype && prototype !== null || !Object.keys(value).every((key) => key === "note" || key === "progress" || key === "operations" || key === "constraints" || key === "updated")) return false;
|
|
1044
|
-
const note = value.note;
|
|
1045
|
-
const progress = value.progress;
|
|
1046
|
-
const operations = value.operations;
|
|
1047
|
-
const constraints = value.constraints;
|
|
1048
|
-
const updated = value.updated;
|
|
1049
|
-
if (operations === void 0 || constraints === void 0 || !(0, _orkestrel_contract.isFiniteNumber)(updated) || updated < 0) return false;
|
|
1050
|
-
return isTaskActivityInput({
|
|
1051
|
-
...note === void 0 ? {} : { note },
|
|
1052
|
-
...progress === void 0 ? {} : { progress },
|
|
1053
|
-
operations,
|
|
1054
|
-
constraints
|
|
1055
|
-
});
|
|
1056
|
-
} catch {
|
|
1057
|
-
return false;
|
|
1058
|
-
}
|
|
1059
|
-
}
|
|
1060
|
-
//#endregion
|
|
1061
1075
|
//#region src/core/cloners.ts
|
|
1062
1076
|
/**
|
|
1063
1077
|
* Validate and own a workflow snapshot before live construction.
|
|
@@ -1314,18 +1328,18 @@ var phaseUpdateShape = (0, _orkestrel_contract.objectShape)({
|
|
|
1314
1328
|
* idle-TTL / eviction — a persisted run-state is durable orchestration state that lives until an
|
|
1315
1329
|
* explicit `delete`. The public surface is EXACTLY `get` / `set` / `delete` — no extra members (the
|
|
1316
1330
|
* §22 method bijection with {@link WorkflowStoreInterface}). Restore stays a caller concern: read a
|
|
1317
|
-
* 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}.
|
|
1318
1332
|
*
|
|
1319
1333
|
* @example
|
|
1320
1334
|
* ```ts
|
|
1321
1335
|
* import { createMemoryDriver } from '@orkestrel/database'
|
|
1322
|
-
* import { createDatabaseWorkflowStore, createWorkflow,
|
|
1336
|
+
* import { createDatabaseWorkflowStore, createWorkflow, createRestoredWorkflow } from '@orkestrel/workflow'
|
|
1323
1337
|
*
|
|
1324
1338
|
* const store = createDatabaseWorkflowStore(createMemoryDriver()) // a durable driver swaps in here
|
|
1325
1339
|
* const workflow = createWorkflow(definition)
|
|
1326
1340
|
* await store.set(workflow.snapshot()) // persist the run state (one JSON column)
|
|
1327
1341
|
* const snapshot = await store.get(definition.id)
|
|
1328
|
-
* const restored = snapshot &&
|
|
1342
|
+
* const restored = snapshot && createRestoredWorkflow(snapshot) // an identical live tree
|
|
1329
1343
|
* await store.delete(definition.id) // drop it
|
|
1330
1344
|
* ```
|
|
1331
1345
|
*/
|
|
@@ -1385,17 +1399,17 @@ var DatabaseWorkflowStore = class {
|
|
|
1385
1399
|
*
|
|
1386
1400
|
* The public surface is EXACTLY `get` / `set` / `delete` — no extra members (the §22 method
|
|
1387
1401
|
* bijection with {@link WorkflowStoreInterface}). Restore is a caller concern: read a snapshot
|
|
1388
|
-
* back and rebuild the live tree with {@link import('../factories.js').
|
|
1402
|
+
* back and rebuild the live tree with {@link import('../factories.js').createRestoredWorkflow}.
|
|
1389
1403
|
*
|
|
1390
1404
|
* @example
|
|
1391
1405
|
* ```ts
|
|
1392
|
-
* import { createMemoryWorkflowStore, createWorkflow,
|
|
1406
|
+
* import { createMemoryWorkflowStore, createWorkflow, createRestoredWorkflow } from '@orkestrel/workflow'
|
|
1393
1407
|
*
|
|
1394
1408
|
* const store = createMemoryWorkflowStore()
|
|
1395
1409
|
* const workflow = createWorkflow(definition)
|
|
1396
1410
|
* await store.set(workflow.snapshot()) // persist the run state
|
|
1397
1411
|
* const snapshot = await store.get(definition.id)
|
|
1398
|
-
* const restored = snapshot &&
|
|
1412
|
+
* const restored = snapshot && createRestoredWorkflow(snapshot) // an identical live tree
|
|
1399
1413
|
* await store.delete(definition.id) // drop it
|
|
1400
1414
|
* ```
|
|
1401
1415
|
*/
|
|
@@ -2237,7 +2251,7 @@ var PhaseManager = class {
|
|
|
2237
2251
|
* @remarks
|
|
2238
2252
|
* - **Construction.** Built from a {@link WorkflowSnapshot} (the unified input —
|
|
2239
2253
|
* {@link import('./factories.js').createWorkflow} seeds an initial snapshot from a
|
|
2240
|
-
* {@link import('./types.js').WorkflowDefinition}, {@link import('./factories.js').
|
|
2254
|
+
* {@link import('./types.js').WorkflowDefinition}, {@link import('./factories.js').createRestoredWorkflow}
|
|
2241
2255
|
* passes a persisted one). Each child {@link Phase} is wired to escalate to `#recompute`.
|
|
2242
2256
|
* - **Derived status.** `status` is `#override` when forced, else
|
|
2243
2257
|
* {@link deriveWorkflowStatus} over the live phases' statuses feeding `bail`. `failed` is
|
|
@@ -2252,7 +2266,7 @@ var PhaseManager = class {
|
|
|
2252
2266
|
* workflow tier; `phase(id)` + each `phase.task(id)` navigate DOWN, a task's `phase` / `workflow`
|
|
2253
2267
|
* navigate UP.
|
|
2254
2268
|
* - **Snapshot.** `snapshot()` serializes the whole live tree to a {@link WorkflowSnapshot} (pure
|
|
2255
|
-
* JSON); {@link import('./factories.js').
|
|
2269
|
+
* JSON); {@link import('./factories.js').createRestoredWorkflow} rebuilds an equivalent live tree.
|
|
2256
2270
|
* - **Observable (AGENTS §13).** The owned {@link emitter} ({@link WorkflowEventMap}) fires
|
|
2257
2271
|
* `start` / `complete` / `fail` / `pause` / `resume` / `skip` / `stop` after the
|
|
2258
2272
|
* corresponding status or runtime-gate change; the emitter isolates a listener throw and
|
|
@@ -2656,7 +2670,7 @@ var WorkflowManager = class {
|
|
|
2656
2670
|
if (!this.#owns(id, mutation, generation)) return this.#resolve(id, generation);
|
|
2657
2671
|
let workflow;
|
|
2658
2672
|
try {
|
|
2659
|
-
workflow =
|
|
2673
|
+
workflow = createRestoredWorkflow(owned, { ...this.#functions === void 0 ? {} : { functions: this.#functions } });
|
|
2660
2674
|
} catch (error) {
|
|
2661
2675
|
if (!this.#owns(id, mutation, generation)) return this.#resolve(id, generation);
|
|
2662
2676
|
throw error;
|
|
@@ -3965,12 +3979,12 @@ function createWorkflow(definition, options) {
|
|
|
3965
3979
|
return new Workflow(definitionToSnapshot(definition, captured.bail ?? definition.bail ?? false), captured);
|
|
3966
3980
|
}
|
|
3967
3981
|
/**
|
|
3968
|
-
*
|
|
3982
|
+
* Build an equivalent live W-b entity tree from a {@link WorkflowSnapshot} — the
|
|
3969
3983
|
* inverse of {@link WorkflowInterface.snapshot}, restoring structure + each node's status
|
|
3970
3984
|
* + recorded results + positional order + the persisted `#override`.
|
|
3971
3985
|
*
|
|
3972
3986
|
* @remarks
|
|
3973
|
-
* Round-trip fidelity is paramount: a `snapshot()` → `
|
|
3987
|
+
* Round-trip fidelity is paramount: a `snapshot()` → `createRestoredWorkflow()` reproduces the
|
|
3974
3988
|
* same status at every node (each `#override` restored DIRECTLY from the snapshot's own
|
|
3975
3989
|
* `override` field, not guessed from a status divergence), the same recorded
|
|
3976
3990
|
* {@link import('./types.js').TaskResult}s, and the same positional order (an interior
|
|
@@ -3990,18 +4004,18 @@ function createWorkflow(definition, options) {
|
|
|
3990
4004
|
*
|
|
3991
4005
|
* @example
|
|
3992
4006
|
* ```ts
|
|
3993
|
-
* import {
|
|
4007
|
+
* import { createRestoredWorkflow } from '@orkestrel/workflow'
|
|
3994
4008
|
*
|
|
3995
|
-
* const restored =
|
|
4009
|
+
* const restored = createRestoredWorkflow(workflow.snapshot()) // bail comes from the snapshot
|
|
3996
4010
|
* restored.status === workflow.status // true
|
|
3997
4011
|
* ```
|
|
3998
4012
|
*/
|
|
3999
|
-
function
|
|
4013
|
+
function createRestoredWorkflow(snapshot, options) {
|
|
4000
4014
|
const captured = captureWorkflowOptions(options);
|
|
4001
4015
|
return new Workflow(cloneWorkflowSnapshot(snapshot), captured);
|
|
4002
4016
|
}
|
|
4003
4017
|
/**
|
|
4004
|
-
*
|
|
4018
|
+
* Build an interrupted workflow back to life at its remaining retry budget.
|
|
4005
4019
|
*
|
|
4006
4020
|
* @remarks
|
|
4007
4021
|
* Each phase captures every unique initial `run` binding once before constructing tasks. Recovery
|
|
@@ -4010,9 +4024,17 @@ function restoreWorkflow(snapshot, options) {
|
|
|
4010
4024
|
*
|
|
4011
4025
|
* @param snapshot - The hostile persisted snapshot
|
|
4012
4026
|
* @param options - Runtime handlers and entity options
|
|
4013
|
-
* @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
|
+
* ```
|
|
4014
4036
|
*/
|
|
4015
|
-
function
|
|
4037
|
+
function createRecoveredWorkflow(snapshot, options) {
|
|
4016
4038
|
const captured = captureWorkflowOptions(options);
|
|
4017
4039
|
const owned = cloneWorkflowSnapshot(snapshot);
|
|
4018
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 });
|
|
@@ -4021,29 +4043,6 @@ function recoverWorkflow(snapshot, options) {
|
|
|
4021
4043
|
return workflow;
|
|
4022
4044
|
}
|
|
4023
4045
|
/**
|
|
4024
|
-
* Assert that a {@link WorkflowSnapshot} carries a `boolean` `bail` — at the workflow tier AND
|
|
4025
|
-
* on every phase — and that its every node's status (and its `override`, when present) is drawn
|
|
4026
|
-
* from the lifecycle vocabulary, throwing a `RESTORE` {@link WorkflowError} otherwise.
|
|
4027
|
-
*
|
|
4028
|
-
* @remarks
|
|
4029
|
-
* The boundary-narrowing guard (AGENTS §14) for {@link restoreWorkflow}: a snapshot is
|
|
4030
|
-
* untrusted JSON, so a status (or an override) outside
|
|
4031
|
-
* {@link import('./constants.js').WORKFLOW_STATUSES} /
|
|
4032
|
-
* {@link import('./constants.js').PHASE_STATUSES} / {@link import('./constants.js').TASK_STATUSES},
|
|
4033
|
-
* a non-boolean `bail` (the workflow's OR any phase's — both are REQUIRED persisted policy), a
|
|
4034
|
-
* present-but-invalid phase `concurrency` (not a positive integer), or a present-but-invalid task
|
|
4035
|
-
* `run` (an empty string) / `retries` / `timeout` (not a non-negative integer),
|
|
4036
|
-
* is rejected loudly (naming the offending node) rather than silently producing a broken tree.
|
|
4037
|
-
* The `override` / `concurrency` / `run` / `retries` / `timeout` are optional, so each is only
|
|
4038
|
-
* checked WHEN present. Structural shape beyond these fields is the contract's concern; this
|
|
4039
|
-
* guards exactly the fields the live state machine reads back.
|
|
4040
|
-
*
|
|
4041
|
-
* @param snapshot - The snapshot to validate
|
|
4042
|
-
*/
|
|
4043
|
-
function assertSnapshot(snapshot) {
|
|
4044
|
-
cloneWorkflowSnapshot(snapshot);
|
|
4045
|
-
}
|
|
4046
|
-
/**
|
|
4047
4046
|
* Create the in-memory durable {@link WorkflowStoreInterface} — a process-lifetime
|
|
4048
4047
|
* {@link MemoryWorkflowStore} persisting {@link WorkflowSnapshot}s by workflow id, the DEFAULT
|
|
4049
4048
|
* backend behind the W-d persistence seam.
|
|
@@ -4056,19 +4055,19 @@ function assertSnapshot(snapshot) {
|
|
|
4056
4055
|
* {@link createDatabaseWorkflowStore} (the snapshot as one opaque JSON column over a `databases`
|
|
4057
4056
|
* table) — for a DURABLE store (run-state surviving a restart) pass it a JSON / SQLite / IndexedDB
|
|
4058
4057
|
* driver, and it swaps in WITHOUT touching the runner or the entity tree. Restore stays a caller
|
|
4059
|
-
* 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}.
|
|
4060
4059
|
*
|
|
4061
4060
|
* @returns A memory-backed {@link WorkflowStoreInterface}
|
|
4062
4061
|
*
|
|
4063
4062
|
* @example
|
|
4064
4063
|
* ```ts
|
|
4065
|
-
* import { createMemoryWorkflowStore, createWorkflow,
|
|
4064
|
+
* import { createMemoryWorkflowStore, createWorkflow, createRestoredWorkflow } from '@orkestrel/workflow'
|
|
4066
4065
|
*
|
|
4067
4066
|
* const store = createMemoryWorkflowStore()
|
|
4068
4067
|
* const workflow = createWorkflow(definition)
|
|
4069
4068
|
* await store.set(workflow.snapshot()) // persist the run state
|
|
4070
4069
|
* const snapshot = await store.get(definition.id)
|
|
4071
|
-
* const restored = snapshot &&
|
|
4070
|
+
* const restored = snapshot && createRestoredWorkflow(snapshot) // an identical live tree
|
|
4072
4071
|
* ```
|
|
4073
4072
|
*/
|
|
4074
4073
|
function createMemoryWorkflowStore() {
|
|
@@ -4099,13 +4098,13 @@ function createMemoryWorkflowStore() {
|
|
|
4099
4098
|
* @example
|
|
4100
4099
|
* ```ts
|
|
4101
4100
|
* import { createMemoryDriver } from '@orkestrel/database'
|
|
4102
|
-
* import { createDatabaseWorkflowStore, createWorkflow,
|
|
4101
|
+
* import { createDatabaseWorkflowStore, createWorkflow, createRestoredWorkflow } from '@orkestrel/workflow'
|
|
4103
4102
|
*
|
|
4104
4103
|
* const store = createDatabaseWorkflowStore(createMemoryDriver()) // a durable driver swaps in here
|
|
4105
4104
|
* const workflow = createWorkflow(definition)
|
|
4106
4105
|
* await store.set(workflow.snapshot()) // persist the run state (one JSON column)
|
|
4107
4106
|
* const snapshot = await store.get(definition.id)
|
|
4108
|
-
* const restored = snapshot &&
|
|
4107
|
+
* const restored = snapshot && createRestoredWorkflow(snapshot) // an identical live tree
|
|
4109
4108
|
* ```
|
|
4110
4109
|
*/
|
|
4111
4110
|
function createDatabaseWorkflowStore(driver = (0, _orkestrel_database.createMemoryDriver)()) {
|
|
@@ -4174,11 +4173,11 @@ function createWorkflowRunner(options) {
|
|
|
4174
4173
|
* @remarks
|
|
4175
4174
|
* `options.functions` flows into every workflow the manager mints (`add`, via
|
|
4176
4175
|
* {@link createWorkflow}) or hydrates (`open`'s registry-miss path, via
|
|
4177
|
-
* {@link
|
|
4176
|
+
* {@link createRestoredWorkflow}), so a hydrated workflow is RUNNABLE rather than a dead snapshot
|
|
4178
4177
|
* mirror. `options.store` is the EXACT analogue of the twins' `store` seam — omitted ⇒ the
|
|
4179
4178
|
* manager is registry-only (`open` resolves only what is registered, `save` is a no-op). This
|
|
4180
4179
|
* is PURELY ADDITIVE: direct {@link WorkflowStoreInterface} use and
|
|
4181
|
-
* {@link
|
|
4180
|
+
* {@link createRestoredWorkflow} remain valid — the manager is one more caller-driven persistence
|
|
4182
4181
|
* seam, not a replacement.
|
|
4183
4182
|
*
|
|
4184
4183
|
* @param options - The optional `store` seam and the `functions` registry threaded into every mint/hydrate
|
|
@@ -4317,7 +4316,6 @@ exports.WorkflowError = WorkflowError;
|
|
|
4317
4316
|
exports.WorkflowManager = WorkflowManager;
|
|
4318
4317
|
exports.WorkflowPersistence = WorkflowPersistence;
|
|
4319
4318
|
exports.WorkflowRunner = WorkflowRunner;
|
|
4320
|
-
exports.assertSnapshot = assertSnapshot;
|
|
4321
4319
|
exports.buildPhaseContext = buildPhaseContext;
|
|
4322
4320
|
exports.buildTaskContext = buildTaskContext;
|
|
4323
4321
|
exports.buildWorkflowContext = buildWorkflowContext;
|
|
@@ -4329,6 +4327,8 @@ exports.collectResults = collectResults;
|
|
|
4329
4327
|
exports.createDatabaseWorkflowStore = createDatabaseWorkflowStore;
|
|
4330
4328
|
exports.createDeferred = createDeferred;
|
|
4331
4329
|
exports.createMemoryWorkflowStore = createMemoryWorkflowStore;
|
|
4330
|
+
exports.createRecoveredWorkflow = createRecoveredWorkflow;
|
|
4331
|
+
exports.createRestoredWorkflow = createRestoredWorkflow;
|
|
4332
4332
|
exports.createRunner = createRunner;
|
|
4333
4333
|
exports.createScheduler = createScheduler;
|
|
4334
4334
|
exports.createWorkflow = createWorkflow;
|
|
@@ -4359,10 +4359,8 @@ exports.parkSignal = parkSignal;
|
|
|
4359
4359
|
exports.phaseDefinitionToSnapshot = phaseDefinitionToSnapshot;
|
|
4360
4360
|
exports.phaseShape = phaseShape;
|
|
4361
4361
|
exports.phaseUpdateShape = phaseUpdateShape;
|
|
4362
|
-
exports.recoverWorkflow = recoverWorkflow;
|
|
4363
4362
|
exports.recoverWorkflowSnapshot = recoverWorkflowSnapshot;
|
|
4364
4363
|
exports.resolveTaskSilence = resolveTaskSilence;
|
|
4365
|
-
exports.restoreWorkflow = restoreWorkflow;
|
|
4366
4364
|
exports.scheduleHost = scheduleHost;
|
|
4367
4365
|
exports.success = success;
|
|
4368
4366
|
exports.taskDefinitionToSnapshot = taskDefinitionToSnapshot;
|