@objectstack/service-automation 9.2.0 → 9.4.0
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/index.cjs +76 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +59 -2
- package/dist/index.d.ts +59 -2
- package/dist/index.js +76 -3
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -49,7 +49,7 @@ var FlowSuspendSignal = class {
|
|
|
49
49
|
function isSuspendSignal(err) {
|
|
50
50
|
return typeof err === "object" && err !== null && err.__flowSuspend === true;
|
|
51
51
|
}
|
|
52
|
-
var
|
|
52
|
+
var _AutomationEngine = class _AutomationEngine {
|
|
53
53
|
constructor(logger, store) {
|
|
54
54
|
this.flows = /* @__PURE__ */ new Map();
|
|
55
55
|
this.flowEnabled = /* @__PURE__ */ new Map();
|
|
@@ -244,7 +244,7 @@ var AutomationEngine = class {
|
|
|
244
244
|
}
|
|
245
245
|
/**
|
|
246
246
|
* Derive a flow's trigger binding from its `start` node, or `undefined` if
|
|
247
|
-
* the flow has no auto-trigger (manual / screen
|
|
247
|
+
* the flow has no auto-trigger (manual / screen). The convention —
|
|
248
248
|
* established by the showcase flows — is that the start node carries the
|
|
249
249
|
* trigger details in its `config`: `{ objectName, triggerType, condition }`
|
|
250
250
|
* for record-change, or a `schedule` descriptor for time-based flows.
|
|
@@ -273,6 +273,12 @@ var AutomationEngine = class {
|
|
|
273
273
|
binding: { flowName, schedule: config.schedule, condition: config.condition ?? void 0, config }
|
|
274
274
|
};
|
|
275
275
|
}
|
|
276
|
+
if (flow.type === "api" || triggerType === "api") {
|
|
277
|
+
return {
|
|
278
|
+
triggerType: "api",
|
|
279
|
+
binding: { flowName, condition: config.condition ?? void 0, config }
|
|
280
|
+
};
|
|
281
|
+
}
|
|
276
282
|
return void 0;
|
|
277
283
|
}
|
|
278
284
|
/**
|
|
@@ -856,6 +862,46 @@ var AutomationEngine = class {
|
|
|
856
862
|
error
|
|
857
863
|
});
|
|
858
864
|
}
|
|
865
|
+
/**
|
|
866
|
+
* Cancel a suspended run (ADR-0044): consume its continuation and record a
|
|
867
|
+
* terminal `cancelled` log so it stops surfacing as resumable. The
|
|
868
|
+
* engine-level primitive behind "the submitter abandoned the revision
|
|
869
|
+
* window" — recalling there leaves the run paused at a wait node with no
|
|
870
|
+
* reject edge to resume down, so the run must end, not continue. Returns
|
|
871
|
+
* `false` when no suspended run exists under the id (already terminal /
|
|
872
|
+
* unknown), which callers treat as idempotent success.
|
|
873
|
+
*/
|
|
874
|
+
async cancelRun(runId, reason) {
|
|
875
|
+
let run = this.suspendedRuns.get(runId) ?? null;
|
|
876
|
+
if (!run && this.store) {
|
|
877
|
+
try {
|
|
878
|
+
run = await this.store.load(runId);
|
|
879
|
+
} catch (err) {
|
|
880
|
+
this.logger.warn(
|
|
881
|
+
`[automation] cancelRun: failed to load suspended run '${runId}' from durable store: ${err.message}`
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
if (!run) return false;
|
|
886
|
+
await this.forgetSuspendedRun(runId);
|
|
887
|
+
this.recordLog({
|
|
888
|
+
id: run.runId,
|
|
889
|
+
flowName: run.flowName,
|
|
890
|
+
flowVersion: run.flowVersion,
|
|
891
|
+
status: "cancelled",
|
|
892
|
+
startedAt: run.startedAt,
|
|
893
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
894
|
+
durationMs: Date.now() - run.startTime,
|
|
895
|
+
trigger: {
|
|
896
|
+
type: run.context?.event ?? "manual",
|
|
897
|
+
userId: run.context?.userId,
|
|
898
|
+
object: run.context?.object
|
|
899
|
+
},
|
|
900
|
+
steps: run.steps,
|
|
901
|
+
error: reason
|
|
902
|
+
});
|
|
903
|
+
return true;
|
|
904
|
+
}
|
|
859
905
|
/**
|
|
860
906
|
* Walk a failed run's `$parentRunId` chain and fail each suspended
|
|
861
907
|
* ancestor (see {@link failSuspendedRun}). Bounded so a corrupt context
|
|
@@ -987,6 +1033,13 @@ ${failures.join("\n")}`
|
|
|
987
1033
|
* Detect cycles in the flow graph (DAG validation).
|
|
988
1034
|
* Uses DFS with coloring (white/gray/black) to detect back edges.
|
|
989
1035
|
* Throws an error with cycle details if a cycle is found.
|
|
1036
|
+
*
|
|
1037
|
+
* ADR-0044: edges explicitly typed `back` (declared back-edges — e.g. a
|
|
1038
|
+
* revise/rework loop re-entering an approval node) are excluded from the
|
|
1039
|
+
* analysis: the graph **minus `back` edges** must be a DAG. An unmarked
|
|
1040
|
+
* cycle is still rejected — authors opt in edge by edge. At run time a
|
|
1041
|
+
* `back` edge traverses like any default edge; the re-entry runaway guard
|
|
1042
|
+
* lives in {@link executeNode}.
|
|
990
1043
|
*/
|
|
991
1044
|
detectCycles(flow) {
|
|
992
1045
|
const WHITE = 0, GRAY = 1, BLACK = 2;
|
|
@@ -998,6 +1051,7 @@ ${failures.join("\n")}`
|
|
|
998
1051
|
adj.set(node.id, []);
|
|
999
1052
|
}
|
|
1000
1053
|
for (const edge of flow.edges) {
|
|
1054
|
+
if (edge.type === "back") continue;
|
|
1001
1055
|
const targets = adj.get(edge.source);
|
|
1002
1056
|
if (targets) targets.push(edge.target);
|
|
1003
1057
|
}
|
|
@@ -1027,7 +1081,9 @@ ${failures.join("\n")}`
|
|
|
1027
1081
|
if (color.get(node.id) === WHITE) {
|
|
1028
1082
|
const cycle = dfs(node.id);
|
|
1029
1083
|
if (cycle) {
|
|
1030
|
-
throw new Error(
|
|
1084
|
+
throw new Error(
|
|
1085
|
+
`Flow contains a cycle: ${cycle.join(" \u2192 ")}. Only DAG flows are allowed \u2014 to author an intentional rework loop, mark the cycle-closing edge with type: 'back' (ADR-0044).`
|
|
1086
|
+
);
|
|
1031
1087
|
}
|
|
1032
1088
|
}
|
|
1033
1089
|
}
|
|
@@ -1071,6 +1127,15 @@ ${failures.join("\n")}`
|
|
|
1071
1127
|
*/
|
|
1072
1128
|
async executeNode(node, flow, variables, context, steps) {
|
|
1073
1129
|
if (node.type === "end") return;
|
|
1130
|
+
const priorVisits = steps.reduce(
|
|
1131
|
+
(n, s) => s.nodeId === node.id && s.parentNodeId === void 0 ? n + 1 : n,
|
|
1132
|
+
0
|
|
1133
|
+
);
|
|
1134
|
+
if (priorVisits >= _AutomationEngine.MAX_NODE_REENTRIES) {
|
|
1135
|
+
throw new Error(
|
|
1136
|
+
`Node '${node.id}' was entered ${priorVisits} times in one run \u2014 aborting as a runaway loop (back-edge cycles must terminate; see ADR-0044)`
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
1074
1139
|
const stepStart = Date.now();
|
|
1075
1140
|
const stepStartedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1076
1141
|
const executor = this.nodeExecutors.get(node.type);
|
|
@@ -1496,6 +1561,14 @@ ${failures.join("\n")}`
|
|
|
1496
1561
|
}
|
|
1497
1562
|
}
|
|
1498
1563
|
};
|
|
1564
|
+
/**
|
|
1565
|
+
* ADR-0044: maximum times a single node may be (re-)entered at the top
|
|
1566
|
+
* level of one run before the engine aborts it as a runaway back-edge
|
|
1567
|
+
* loop. Generous on purpose — the product guard (`maxRevisions`) sits
|
|
1568
|
+
* orders of magnitude lower.
|
|
1569
|
+
*/
|
|
1570
|
+
_AutomationEngine.MAX_NODE_REENTRIES = 100;
|
|
1571
|
+
var AutomationEngine = _AutomationEngine;
|
|
1499
1572
|
|
|
1500
1573
|
// src/suspended-run-store.ts
|
|
1501
1574
|
var TABLE = "sys_automation_run";
|