nvent 0.4.5 → 0.5.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/module.d.mts +1 -1
- package/dist/module.mjs +433 -175
- package/dist/runtime/adapters/base/index.d.ts +6 -0
- package/dist/runtime/adapters/base/index.js +1 -0
- package/dist/runtime/adapters/base/store-validator.d.ts +48 -0
- package/dist/runtime/adapters/base/store-validator.js +147 -0
- package/dist/runtime/adapters/builtin/file-queue.d.ts +15 -1
- package/dist/runtime/adapters/builtin/file-queue.js +70 -6
- package/dist/runtime/adapters/builtin/file-store.d.ts +4 -18
- package/dist/runtime/adapters/builtin/file-store.js +90 -109
- package/dist/runtime/adapters/builtin/memory-queue.js +4 -0
- package/dist/runtime/adapters/builtin/memory-store.d.ts +42 -31
- package/dist/runtime/adapters/builtin/memory-store.js +253 -183
- package/dist/runtime/adapters/factory.d.ts +2 -2
- package/dist/runtime/adapters/factory.js +54 -20
- package/dist/runtime/adapters/interfaces/store.d.ts +177 -113
- package/dist/runtime/config/index.d.ts +2 -2
- package/dist/runtime/config/index.js +14 -6
- package/dist/runtime/config/types.d.ts +32 -2
- package/dist/runtime/events/eventBus.d.ts +1 -1
- package/dist/runtime/events/types.d.ts +31 -2
- package/dist/runtime/events/utils/scheduleTrigger.d.ts +8 -0
- package/dist/runtime/events/utils/scheduleTrigger.js +69 -0
- package/dist/runtime/events/utils/stallDetector.d.ts +44 -3
- package/dist/runtime/events/utils/stallDetector.js +288 -89
- package/dist/runtime/events/utils/triggerRuntime.d.ts +58 -0
- package/dist/runtime/events/utils/triggerRuntime.js +212 -0
- package/dist/runtime/events/wiring/flowWiring.d.ts +11 -5
- package/dist/runtime/events/wiring/flowWiring.js +620 -92
- package/dist/runtime/events/wiring/registry.d.ts +2 -2
- package/dist/runtime/events/wiring/registry.js +8 -6
- package/dist/runtime/events/wiring/streamWiring.d.ts +15 -11
- package/dist/runtime/events/wiring/streamWiring.js +88 -11
- package/dist/runtime/events/wiring/triggerWiring.d.ts +21 -0
- package/dist/runtime/events/wiring/triggerWiring.js +412 -0
- package/dist/runtime/{server → nitro}/plugins/00.adapters.js +8 -4
- package/dist/runtime/{server → nitro}/plugins/02.workers.js +21 -3
- package/dist/runtime/nitro/plugins/03.triggers.d.ts +12 -0
- package/dist/runtime/nitro/plugins/03.triggers.js +55 -0
- package/dist/runtime/nitro/routes/webhook.await.d.ts +23 -0
- package/dist/runtime/nitro/routes/webhook.await.js +90 -0
- package/dist/runtime/nitro/routes/webhook.trigger.d.ts +69 -0
- package/dist/runtime/nitro/routes/webhook.trigger.js +64 -0
- package/dist/runtime/{utils → nitro/utils}/adapters.d.ts +6 -6
- package/dist/runtime/nitro/utils/awaitPatterns/event.d.ts +15 -0
- package/dist/runtime/nitro/utils/awaitPatterns/event.js +120 -0
- package/dist/runtime/nitro/utils/awaitPatterns/index.d.ts +28 -0
- package/dist/runtime/nitro/utils/awaitPatterns/index.js +55 -0
- package/dist/runtime/nitro/utils/awaitPatterns/schedule.d.ts +16 -0
- package/dist/runtime/nitro/utils/awaitPatterns/schedule.js +78 -0
- package/dist/runtime/nitro/utils/awaitPatterns/time.d.ts +15 -0
- package/dist/runtime/nitro/utils/awaitPatterns/time.js +67 -0
- package/dist/runtime/nitro/utils/awaitPatterns/webhook.d.ts +15 -0
- package/dist/runtime/nitro/utils/awaitPatterns/webhook.js +120 -0
- package/dist/runtime/{utils → nitro/utils}/defineFunction.d.ts +2 -2
- package/dist/runtime/{utils → nitro/utils}/defineFunction.js +3 -3
- package/dist/runtime/{utils → nitro/utils}/defineFunctionConfig.d.ts +156 -0
- package/dist/runtime/{utils → nitro/utils}/defineFunctionConfig.js +1 -0
- package/dist/runtime/nitro/utils/defineHooks.d.ts +41 -0
- package/dist/runtime/nitro/utils/defineHooks.js +6 -0
- package/dist/runtime/{utils → nitro/utils}/registerAdapter.d.ts +3 -3
- package/dist/runtime/{utils → nitro/utils}/registerAdapter.js +1 -1
- package/dist/runtime/nitro/utils/useAwait.d.ts +71 -0
- package/dist/runtime/nitro/utils/useAwait.js +139 -0
- package/dist/runtime/{utils → nitro/utils}/useEventManager.d.ts +2 -2
- package/dist/runtime/{utils → nitro/utils}/useEventManager.js +1 -1
- package/dist/runtime/nitro/utils/useFlow.d.ts +68 -0
- package/dist/runtime/nitro/utils/useFlow.js +226 -0
- package/dist/runtime/nitro/utils/useHookRegistry.d.ts +34 -0
- package/dist/runtime/nitro/utils/useHookRegistry.js +25 -0
- package/dist/runtime/nitro/utils/useRunContext.d.ts +6 -0
- package/dist/runtime/nitro/utils/useRunContext.js +102 -0
- package/dist/runtime/nitro/utils/useStreamTopics.d.ts +83 -0
- package/dist/runtime/nitro/utils/useStreamTopics.js +94 -0
- package/dist/runtime/nitro/utils/useTrigger.d.ts +150 -0
- package/dist/runtime/nitro/utils/useTrigger.js +320 -0
- package/dist/runtime/scheduler/index.d.ts +33 -0
- package/dist/runtime/scheduler/index.js +38 -0
- package/dist/runtime/scheduler/scheduler.d.ts +113 -0
- package/dist/runtime/scheduler/scheduler.js +623 -0
- package/dist/runtime/scheduler/types.d.ts +116 -0
- package/dist/runtime/scheduler/types.js +0 -0
- package/dist/runtime/worker/node/runner.d.ts +12 -2
- package/dist/runtime/worker/node/runner.js +141 -37
- package/package.json +6 -6
- package/dist/runtime/server/api/_flows/[name]/clear-history.delete.d.ts +0 -10
- package/dist/runtime/server/api/_flows/[name]/clear-history.delete.js +0 -55
- package/dist/runtime/server/api/_flows/[name]/runs/[runId]/cancel.post.d.ts +0 -2
- package/dist/runtime/server/api/_flows/[name]/runs/[runId]/cancel.post.js +0 -21
- package/dist/runtime/server/api/_flows/[name]/runs.get.d.ts +0 -17
- package/dist/runtime/server/api/_flows/[name]/runs.get.js +0 -64
- package/dist/runtime/server/api/_flows/[name]/schedule.post.d.ts +0 -2
- package/dist/runtime/server/api/_flows/[name]/schedule.post.js +0 -66
- package/dist/runtime/server/api/_flows/[name]/schedules/[id].delete.d.ts +0 -2
- package/dist/runtime/server/api/_flows/[name]/schedules/[id].delete.js +0 -47
- package/dist/runtime/server/api/_flows/[name]/schedules.get.d.ts +0 -2
- package/dist/runtime/server/api/_flows/[name]/schedules.get.js +0 -50
- package/dist/runtime/server/api/_flows/[name]/start.post.d.ts +0 -2
- package/dist/runtime/server/api/_flows/[name]/start.post.js +0 -9
- package/dist/runtime/server/api/_flows/index.get.d.ts +0 -6
- package/dist/runtime/server/api/_flows/index.get.js +0 -5
- package/dist/runtime/server/api/_flows/ws.d.ts +0 -60
- package/dist/runtime/server/api/_flows/ws.js +0 -209
- package/dist/runtime/server/api/_queues/[name]/job/[id].get.d.ts +0 -2
- package/dist/runtime/server/api/_queues/[name]/job/[id].get.js +0 -14
- package/dist/runtime/server/api/_queues/[name]/job/index.get.d.ts +0 -2
- package/dist/runtime/server/api/_queues/[name]/job/index.get.js +0 -27
- package/dist/runtime/server/api/_queues/index.get.d.ts +0 -2
- package/dist/runtime/server/api/_queues/index.get.js +0 -106
- package/dist/runtime/server/api/_queues/ws.d.ts +0 -48
- package/dist/runtime/server/api/_queues/ws.js +0 -215
- package/dist/runtime/utils/useFlowEngine.d.ts +0 -19
- package/dist/runtime/utils/useFlowEngine.js +0 -108
- package/dist/runtime/utils/useStreamTopics.d.ts +0 -72
- package/dist/runtime/utils/useStreamTopics.js +0 -47
- /package/dist/runtime/{server → nitro}/plugins/00.adapters.d.ts +0 -0
- /package/dist/runtime/{server → nitro}/plugins/01.ws-lifecycle.d.ts +0 -0
- /package/dist/runtime/{server → nitro}/plugins/01.ws-lifecycle.js +0 -0
- /package/dist/runtime/{server → nitro}/plugins/02.workers.d.ts +0 -0
- /package/dist/runtime/{utils → nitro/utils}/adapters.js +0 -0
- /package/dist/runtime/{utils → nitro/utils}/useNventLogger.d.ts +0 -0
- /package/dist/runtime/{utils → nitro/utils}/useNventLogger.js +0 -0
- /package/dist/runtime/{utils → nitro/utils}/wsPeerManager.d.ts +0 -0
- /package/dist/runtime/{utils → nitro/utils}/wsPeerManager.js +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getEventBus } from "../eventBus.js";
|
|
2
|
-
import { useNventLogger, useStoreAdapter, useQueueAdapter, $useAnalyzedFlows, $
|
|
2
|
+
import { useNventLogger, useStoreAdapter, useQueueAdapter, $useAnalyzedFlows, $useFunctionRegistry, useStreamTopics, useRuntimeConfig, useScheduler } from "#imports";
|
|
3
3
|
import { createStallDetector } from "../utils/stallDetector.js";
|
|
4
4
|
export function checkPendingStepTriggers(step, emittedEvents, completedSteps) {
|
|
5
5
|
if (!step.subscribes || step.subscribes.length === 0) {
|
|
@@ -14,36 +14,132 @@ export function checkPendingStepTriggers(step, emittedEvents, completedSteps) {
|
|
|
14
14
|
return false;
|
|
15
15
|
});
|
|
16
16
|
}
|
|
17
|
-
async function checkAndTriggerPendingSteps(flowName, runId, store) {
|
|
17
|
+
export async function checkAndTriggerPendingSteps(flowName, runId, store) {
|
|
18
18
|
const logger = useNventLogger("flow-wiring");
|
|
19
19
|
try {
|
|
20
20
|
const analyzedFlows = $useAnalyzedFlows();
|
|
21
|
-
const registry = $
|
|
21
|
+
const registry = $useFunctionRegistry();
|
|
22
22
|
const queue = useQueueAdapter();
|
|
23
|
-
const {
|
|
23
|
+
const { StoreSubjects } = useStreamTopics();
|
|
24
24
|
const flowDef = analyzedFlows.find((f) => f.id === flowName);
|
|
25
|
-
if (!flowDef?.steps)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
25
|
+
if (!flowDef?.steps) {
|
|
26
|
+
logger.info("No flow definition or steps found", { flowName });
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const indexKey = StoreSubjects.flowRunIndex(flowName);
|
|
30
|
+
if (!store.index.get) {
|
|
31
|
+
logger.info("No indexGet method on store", { flowName });
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const flowEntry = await store.index.get(indexKey, runId);
|
|
35
|
+
if (!flowEntry?.metadata) {
|
|
36
|
+
logger.info("No flow entry or metadata found", { flowName, runId });
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const streamName = StoreSubjects.flowRun(runId);
|
|
40
|
+
const allEvents = await store.stream.read(streamName);
|
|
32
41
|
const isCanceled = allEvents.some((event) => event.type === "flow.cancel");
|
|
33
42
|
if (isCanceled) {
|
|
34
43
|
logger.debug("Flow is canceled, skipping pending step triggers", { flowName, runId });
|
|
35
44
|
return;
|
|
36
45
|
}
|
|
37
|
-
const
|
|
46
|
+
const emittedEventsObj = flowEntry.metadata.emittedEvents || {};
|
|
47
|
+
const flattenEmittedEvents = (obj, prefix = "") => {
|
|
48
|
+
const result = [];
|
|
49
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
50
|
+
if (key === "undefined" || key === "null") continue;
|
|
51
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
52
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
53
|
+
result.push(...flattenEmittedEvents(value, fullKey));
|
|
54
|
+
} else {
|
|
55
|
+
result.push(fullKey);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
};
|
|
60
|
+
const emittedEvents = new Set(
|
|
61
|
+
typeof emittedEventsObj === "object" && !Array.isArray(emittedEventsObj) ? flattenEmittedEvents(emittedEventsObj) : Array.isArray(emittedEventsObj) ? emittedEventsObj : []
|
|
62
|
+
);
|
|
38
63
|
const completedSteps = /* @__PURE__ */ new Set();
|
|
39
64
|
for (const event of allEvents) {
|
|
40
65
|
if (event.type === "step.completed" && "stepName" in event) {
|
|
41
66
|
completedSteps.add(event.stepName);
|
|
42
67
|
}
|
|
43
68
|
}
|
|
69
|
+
const awaitingSteps = flowEntry?.metadata?.awaitingSteps || {};
|
|
44
70
|
for (const [stepName, stepDef] of Object.entries(flowDef.steps)) {
|
|
45
71
|
const step = stepDef;
|
|
46
72
|
if (!step.subscribes || completedSteps.has(stepName)) continue;
|
|
73
|
+
const awaitState = flowEntry?.metadata?.awaitingSteps?.[stepName];
|
|
74
|
+
if (awaitState && awaitState.status === "awaiting") {
|
|
75
|
+
logger.debug("Step is awaiting, skipping trigger", {
|
|
76
|
+
flowName,
|
|
77
|
+
runId,
|
|
78
|
+
stepName,
|
|
79
|
+
awaitType: awaitState.awaitType,
|
|
80
|
+
position: awaitState.position,
|
|
81
|
+
status: awaitState.status
|
|
82
|
+
});
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (awaitState && awaitState.status === "timeout") {
|
|
86
|
+
logger.debug("Step await timed out, skipping trigger", {
|
|
87
|
+
flowName,
|
|
88
|
+
runId,
|
|
89
|
+
stepName,
|
|
90
|
+
awaitType: awaitState.awaitType
|
|
91
|
+
});
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (awaitState?.status === "resolved") {
|
|
95
|
+
logger.debug("Step await is resolved, will proceed", {
|
|
96
|
+
flowName,
|
|
97
|
+
runId,
|
|
98
|
+
stepName,
|
|
99
|
+
awaitType: awaitState.awaitType,
|
|
100
|
+
position: awaitState.position
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
const isDependencyAwaiting = step.subscribes.some((sub) => {
|
|
104
|
+
const emitEvent = allEvents.find(
|
|
105
|
+
(evt) => evt.type === "emit" && evt.data?.name === sub
|
|
106
|
+
);
|
|
107
|
+
if (!emitEvent) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
const emitStepName = emitEvent.stepName;
|
|
111
|
+
if (!emitStepName) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
const awaitState2 = awaitingSteps[emitStepName];
|
|
115
|
+
if (awaitState2?.position === "after" && awaitState2?.status === "awaiting") {
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
if (awaitState2?.status === "resolved") {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
let emittingStepMeta = flowDef.steps[emitStepName];
|
|
122
|
+
if (!emittingStepMeta && emitStepName === flowDef.entry?.step) {
|
|
123
|
+
emittingStepMeta = flowDef.entry;
|
|
124
|
+
}
|
|
125
|
+
if (emittingStepMeta?.awaitAfter) {
|
|
126
|
+
const stepCompleted = allEvents.some(
|
|
127
|
+
(evt) => evt.type === "step.completed" && evt.stepName === emitStepName
|
|
128
|
+
);
|
|
129
|
+
if (stepCompleted) {
|
|
130
|
+
const awaitResolved = allEvents.some(
|
|
131
|
+
(evt) => evt.type === "await.resolved" && evt.stepName === emitStepName
|
|
132
|
+
);
|
|
133
|
+
if (!awaitResolved) {
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return false;
|
|
139
|
+
});
|
|
140
|
+
if (isDependencyAwaiting) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
47
143
|
const canTrigger = checkPendingStepTriggers(step, emittedEvents, completedSteps);
|
|
48
144
|
if (canTrigger) {
|
|
49
145
|
const flowRegistry = (registry?.flows || {})[flowName];
|
|
@@ -65,6 +161,10 @@ async function checkAndTriggerPendingSteps(flowName, runId, store) {
|
|
|
65
161
|
input: emitData
|
|
66
162
|
// Keyed by event name
|
|
67
163
|
};
|
|
164
|
+
if (awaitState?.status === "resolved" && awaitState?.position === "before") {
|
|
165
|
+
payload.awaitResolved = true;
|
|
166
|
+
payload.awaitData = awaitState.triggerData;
|
|
167
|
+
}
|
|
68
168
|
const jobId = `${runId}__${stepName}`;
|
|
69
169
|
const worker = registry?.workers?.find(
|
|
70
170
|
(w) => w?.flow?.step === stepName && w?.queue?.name === stepMeta.queue
|
|
@@ -73,12 +173,6 @@ async function checkAndTriggerPendingSteps(flowName, runId, store) {
|
|
|
73
173
|
const opts = { ...defaultOpts, jobId };
|
|
74
174
|
try {
|
|
75
175
|
await queue.enqueue(stepMeta.queue, { name: stepName, data: payload, opts });
|
|
76
|
-
logger.debug("Triggered pending step", {
|
|
77
|
-
flowName,
|
|
78
|
-
runId,
|
|
79
|
-
step: stepName,
|
|
80
|
-
subscribes: step.subscribes
|
|
81
|
-
});
|
|
82
176
|
} catch {
|
|
83
177
|
}
|
|
84
178
|
}
|
|
@@ -92,7 +186,7 @@ async function checkAndTriggerPendingSteps(flowName, runId, store) {
|
|
|
92
186
|
});
|
|
93
187
|
}
|
|
94
188
|
}
|
|
95
|
-
export function analyzeFlowCompletion(flowSteps, entryStep, events) {
|
|
189
|
+
export function analyzeFlowCompletion(flowSteps, entryStep, events, entryStepDef) {
|
|
96
190
|
const isCanceled = events.some((event) => event.type === "flow.cancel");
|
|
97
191
|
const allSteps = entryStep ? [entryStep, ...Object.keys(flowSteps)] : Object.keys(flowSteps);
|
|
98
192
|
const completedSteps = /* @__PURE__ */ new Set();
|
|
@@ -139,29 +233,74 @@ export function analyzeFlowCompletion(flowSteps, entryStep, events) {
|
|
|
139
233
|
}
|
|
140
234
|
const totalSteps = allSteps.length;
|
|
141
235
|
const hasFinalFailures = finalFailedSteps.size > 0;
|
|
236
|
+
const stepDependencies = /* @__PURE__ */ new Map();
|
|
237
|
+
const stepDependents = /* @__PURE__ */ new Map();
|
|
238
|
+
for (const stepName of allSteps) {
|
|
239
|
+
stepDependencies.set(stepName, /* @__PURE__ */ new Set());
|
|
240
|
+
stepDependents.set(stepName, /* @__PURE__ */ new Set());
|
|
241
|
+
}
|
|
242
|
+
for (const [stepName, stepDef] of Object.entries(flowSteps)) {
|
|
243
|
+
const step = stepDef;
|
|
244
|
+
const subscribes = step.subscribes || [];
|
|
245
|
+
for (const sub of subscribes) {
|
|
246
|
+
for (const [emitStepName, emitStepDef] of Object.entries(flowSteps)) {
|
|
247
|
+
const emitStep = emitStepDef;
|
|
248
|
+
const emits = emitStep.emits || [];
|
|
249
|
+
if (emits.some((emit) => sub === `${emitStepName}.${emit}` || sub === emit)) {
|
|
250
|
+
stepDependencies.get(stepName)?.add(emitStepName);
|
|
251
|
+
stepDependents.get(emitStepName)?.add(stepName);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (entryStep && entryStepDef?.emits) {
|
|
255
|
+
const entryEmits = entryStepDef.emits || [];
|
|
256
|
+
if (entryEmits.some((emit) => sub === `${entryStep}.${emit}` || sub === emit)) {
|
|
257
|
+
stepDependencies.get(stepName)?.add(entryStep);
|
|
258
|
+
stepDependents.get(entryStep)?.add(stepName);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
142
263
|
let hasBlockingFailure = false;
|
|
264
|
+
let hasCriticalLayerFailure = false;
|
|
143
265
|
if (hasFinalFailures) {
|
|
144
266
|
for (const failedStepName of Array.from(finalFailedSteps)) {
|
|
145
|
-
const
|
|
146
|
-
if (
|
|
147
|
-
for (const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const dependsOnFailedStep = step.subscribes.some((sub) => {
|
|
152
|
-
return failedStepDef.emits.some(
|
|
153
|
-
(emit) => sub === `${failedStepName}.${emit}` || sub === emit
|
|
154
|
-
);
|
|
155
|
-
});
|
|
156
|
-
if (dependsOnFailedStep && !completedSteps.has(stepName)) {
|
|
157
|
-
hasBlockingFailure = true;
|
|
158
|
-
break;
|
|
159
|
-
}
|
|
267
|
+
const dependents = stepDependents.get(failedStepName);
|
|
268
|
+
if (dependents && dependents.size > 0) {
|
|
269
|
+
for (const dependentName of Array.from(dependents)) {
|
|
270
|
+
if (!completedSteps.has(dependentName)) {
|
|
271
|
+
hasBlockingFailure = true;
|
|
272
|
+
break;
|
|
160
273
|
}
|
|
161
274
|
}
|
|
162
275
|
}
|
|
163
276
|
if (hasBlockingFailure) break;
|
|
164
277
|
}
|
|
278
|
+
const layerGroups = /* @__PURE__ */ new Map();
|
|
279
|
+
for (const stepName of allSteps) {
|
|
280
|
+
const deps = stepDependencies.get(stepName);
|
|
281
|
+
const depsKey = Array.from(deps || []).sort().join(",");
|
|
282
|
+
if (!layerGroups.has(depsKey)) {
|
|
283
|
+
layerGroups.set(depsKey, /* @__PURE__ */ new Set());
|
|
284
|
+
}
|
|
285
|
+
layerGroups.get(depsKey)?.add(stepName);
|
|
286
|
+
}
|
|
287
|
+
for (const [_depsKey, layerSteps] of Array.from(layerGroups)) {
|
|
288
|
+
const layerHasFailures = Array.from(layerSteps).some((s) => finalFailedSteps.has(s));
|
|
289
|
+
if (!layerHasFailures) continue;
|
|
290
|
+
const allLayerStepsFailed = Array.from(layerSteps).every(
|
|
291
|
+
(s) => finalFailedSteps.has(s)
|
|
292
|
+
);
|
|
293
|
+
if (allLayerStepsFailed) {
|
|
294
|
+
const hasLeafNode = Array.from(layerSteps).some((s) => {
|
|
295
|
+
const deps = stepDependents.get(s);
|
|
296
|
+
return !deps || deps.size === 0;
|
|
297
|
+
});
|
|
298
|
+
if (hasLeafNode) {
|
|
299
|
+
hasCriticalLayerFailure = true;
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
165
304
|
}
|
|
166
305
|
if (isCanceled) {
|
|
167
306
|
return {
|
|
@@ -172,7 +311,7 @@ export function analyzeFlowCompletion(flowSteps, entryStep, events) {
|
|
|
172
311
|
completedAt
|
|
173
312
|
};
|
|
174
313
|
}
|
|
175
|
-
if (hasBlockingFailure) {
|
|
314
|
+
if (hasBlockingFailure || hasCriticalLayerFailure) {
|
|
176
315
|
return {
|
|
177
316
|
status: "failed",
|
|
178
317
|
totalSteps,
|
|
@@ -181,11 +320,11 @@ export function analyzeFlowCompletion(flowSteps, entryStep, events) {
|
|
|
181
320
|
completedAt: Date.now()
|
|
182
321
|
};
|
|
183
322
|
}
|
|
184
|
-
const
|
|
323
|
+
const allStepsTerminal = allSteps.every(
|
|
185
324
|
(step) => completedSteps.has(step) || finalFailedSteps.has(step)
|
|
186
325
|
);
|
|
187
326
|
let status = "running";
|
|
188
|
-
if (
|
|
327
|
+
if (allStepsTerminal) {
|
|
189
328
|
status = "completed";
|
|
190
329
|
completedAt = Date.now();
|
|
191
330
|
}
|
|
@@ -202,31 +341,34 @@ export function createFlowWiring() {
|
|
|
202
341
|
const unsubs = [];
|
|
203
342
|
let wired = false;
|
|
204
343
|
let stallDetector;
|
|
344
|
+
const publishingTerminalEvents = /* @__PURE__ */ new Set();
|
|
205
345
|
const indexFlowRun = async (flowName, flowId, timestamp, metadata) => {
|
|
206
346
|
const logger = useNventLogger("flow-wiring");
|
|
207
347
|
try {
|
|
208
348
|
const store = useStoreAdapter();
|
|
209
|
-
const {
|
|
210
|
-
const indexKey =
|
|
211
|
-
if (!store.
|
|
349
|
+
const { StoreSubjects } = useStreamTopics();
|
|
350
|
+
const indexKey = StoreSubjects.flowRunIndex(flowName);
|
|
351
|
+
if (!store.index.add) {
|
|
212
352
|
throw new Error("StoreAdapter does not support indexAdd");
|
|
213
353
|
}
|
|
214
|
-
await store.
|
|
215
|
-
logger.debug("Indexed run", { flowName, flowId, indexKey, timestamp, metadata });
|
|
354
|
+
await store.index.add(indexKey, flowId, timestamp, metadata);
|
|
216
355
|
} catch (err) {
|
|
217
356
|
logger.error("Failed to index run", { error: err });
|
|
218
357
|
}
|
|
219
358
|
};
|
|
220
|
-
|
|
359
|
+
const flowProcessingChain = /* @__PURE__ */ new Map();
|
|
360
|
+
const cleanupTimers = /* @__PURE__ */ new Map();
|
|
361
|
+
async function start() {
|
|
221
362
|
if (wired) return;
|
|
222
363
|
wired = true;
|
|
223
364
|
const logger = useNventLogger("flow-wiring");
|
|
224
|
-
const {
|
|
365
|
+
const { StoreSubjects } = useStreamTopics();
|
|
366
|
+
logger.info("Flow wiring starting");
|
|
225
367
|
const store = useStoreAdapter();
|
|
226
|
-
if (!store || !store.append) {
|
|
368
|
+
if (!store || !store.stream.append) {
|
|
227
369
|
logger.error("StoreAdapter not properly initialized or missing append method", {
|
|
228
370
|
hasStore: !!store,
|
|
229
|
-
hasAppend: !!(store && store.append)
|
|
371
|
+
hasAppend: !!(store && store.stream.append)
|
|
230
372
|
});
|
|
231
373
|
throw new Error("StoreAdapter not initialized");
|
|
232
374
|
}
|
|
@@ -243,7 +385,7 @@ export function createFlowWiring() {
|
|
|
243
385
|
if (!flowName) {
|
|
244
386
|
return;
|
|
245
387
|
}
|
|
246
|
-
const streamName =
|
|
388
|
+
const streamName = StoreSubjects.flowRun(runId);
|
|
247
389
|
if (!e.type) {
|
|
248
390
|
logger.error("Event missing type field", { event: e });
|
|
249
391
|
return;
|
|
@@ -257,9 +399,17 @@ export function createFlowWiring() {
|
|
|
257
399
|
if ("stepName" in e && e.stepName) eventData.stepName = e.stepName;
|
|
258
400
|
if ("stepId" in e && e.stepId) eventData.stepId = e.stepId;
|
|
259
401
|
if ("attempt" in e && e.attempt) eventData.attempt = e.attempt;
|
|
260
|
-
const persistedEvent = await store.append(streamName, eventData);
|
|
402
|
+
const persistedEvent = await store.stream.append(streamName, eventData);
|
|
261
403
|
await bus.publish(persistedEvent);
|
|
262
404
|
if (e.type === "flow.completed" || e.type === "flow.failed") {
|
|
405
|
+
const publishKey = `${runId}:terminal`;
|
|
406
|
+
setTimeout(() => {
|
|
407
|
+
try {
|
|
408
|
+
publishingTerminalEvents.delete(publishKey);
|
|
409
|
+
} catch (err) {
|
|
410
|
+
logger.debug("Error cleaning up terminal event tracker", { publishKey, error: err?.message });
|
|
411
|
+
}
|
|
412
|
+
}, 200);
|
|
263
413
|
logger.info("Stored terminal event", {
|
|
264
414
|
type: e.type,
|
|
265
415
|
flowName,
|
|
@@ -284,6 +434,107 @@ export function createFlowWiring() {
|
|
|
284
434
|
});
|
|
285
435
|
}
|
|
286
436
|
};
|
|
437
|
+
const handleFlowStats = async (e) => {
|
|
438
|
+
try {
|
|
439
|
+
if (!e.id || !e.ts) {
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
const flowName = e.flowName;
|
|
443
|
+
if (!flowName) return;
|
|
444
|
+
const flowIndexKey = StoreSubjects.flowIndex();
|
|
445
|
+
if (e.type === "flow.start") {
|
|
446
|
+
if (store.index.increment) {
|
|
447
|
+
await store.index.increment(flowIndexKey, flowName, "stats.total", 1);
|
|
448
|
+
await store.index.increment(flowIndexKey, flowName, "stats.running", 1);
|
|
449
|
+
}
|
|
450
|
+
if (store.index.updateWithRetry) {
|
|
451
|
+
await store.index.updateWithRetry(flowIndexKey, flowName, {
|
|
452
|
+
lastRunAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
logger.debug("Updated flow stats for start", { flowName });
|
|
456
|
+
} else if (e.type === "flow.completed") {
|
|
457
|
+
if (store.index.increment) {
|
|
458
|
+
await store.index.increment(flowIndexKey, flowName, "stats.running", -1);
|
|
459
|
+
await store.index.increment(flowIndexKey, flowName, "stats.success", 1);
|
|
460
|
+
}
|
|
461
|
+
if (store.index.updateWithRetry) {
|
|
462
|
+
await store.index.updateWithRetry(flowIndexKey, flowName, {
|
|
463
|
+
lastCompletedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
logger.debug("Updated flow stats for completion", { flowName });
|
|
467
|
+
} else if (e.type === "flow.failed") {
|
|
468
|
+
if (store.index.increment) {
|
|
469
|
+
await store.index.increment(flowIndexKey, flowName, "stats.running", -1);
|
|
470
|
+
await store.index.increment(flowIndexKey, flowName, "stats.failure", 1);
|
|
471
|
+
}
|
|
472
|
+
if (store.index.updateWithRetry) {
|
|
473
|
+
await store.index.updateWithRetry(flowIndexKey, flowName, {
|
|
474
|
+
lastCompletedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
logger.debug("Updated flow stats for failure", { flowName });
|
|
478
|
+
} else if (e.type === "flow.cancel") {
|
|
479
|
+
if (store.index.increment) {
|
|
480
|
+
await store.index.increment(flowIndexKey, flowName, "stats.running", -1);
|
|
481
|
+
await store.index.increment(flowIndexKey, flowName, "stats.cancel", 1);
|
|
482
|
+
}
|
|
483
|
+
if (store.index.updateWithRetry) {
|
|
484
|
+
await store.index.updateWithRetry(flowIndexKey, flowName, {
|
|
485
|
+
lastCompletedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
logger.debug("Updated flow stats for cancellation", { flowName });
|
|
489
|
+
} else if (e.type === "flow.stalled") {
|
|
490
|
+
if (store.index.increment && e.data?.previousStatus) {
|
|
491
|
+
if (e.data.previousStatus === "awaiting") {
|
|
492
|
+
await store.index.increment(flowIndexKey, flowName, "stats.awaiting", -1);
|
|
493
|
+
logger.debug("Updated flow stats for stalled detection (was awaiting)", { flowName });
|
|
494
|
+
} else if (e.data.previousStatus === "running") {
|
|
495
|
+
await store.index.increment(flowIndexKey, flowName, "stats.running", -1);
|
|
496
|
+
logger.debug("Updated flow stats for stalled detection (was running)", { flowName });
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
} else if (e.type === "await.registered") {
|
|
500
|
+
if (store.index.increment) {
|
|
501
|
+
await store.index.increment(flowIndexKey, flowName, "stats.running", -1);
|
|
502
|
+
await store.index.increment(flowIndexKey, flowName, "stats.awaiting", 1);
|
|
503
|
+
}
|
|
504
|
+
logger.debug("Updated flow stats for await registered", { flowName });
|
|
505
|
+
} else if (e.type === "await.resolved" || e.type === "await.timeout") {
|
|
506
|
+
if (store.index.increment) {
|
|
507
|
+
await store.index.increment(flowIndexKey, flowName, "stats.awaiting", -1);
|
|
508
|
+
await store.index.increment(flowIndexKey, flowName, "stats.running", 1);
|
|
509
|
+
}
|
|
510
|
+
logger.debug("Updated flow stats for await resolved/timeout", { flowName, type: e.type });
|
|
511
|
+
}
|
|
512
|
+
try {
|
|
513
|
+
const indexEntry = await store.index.get(flowIndexKey, flowName);
|
|
514
|
+
if (indexEntry) {
|
|
515
|
+
await bus.publish({
|
|
516
|
+
type: "flow.stats.updated",
|
|
517
|
+
flowName,
|
|
518
|
+
id: indexEntry.id,
|
|
519
|
+
metadata: indexEntry.metadata,
|
|
520
|
+
ts: Date.now()
|
|
521
|
+
});
|
|
522
|
+
logger.debug("Published flow stats update event to bus", { flowName });
|
|
523
|
+
}
|
|
524
|
+
} catch (err) {
|
|
525
|
+
logger.warn("Failed to publish flow stats update event", {
|
|
526
|
+
flowName,
|
|
527
|
+
error: err?.message
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
} catch (err) {
|
|
531
|
+
logger.warn("Failed to update flow stats", {
|
|
532
|
+
type: e.type,
|
|
533
|
+
flowName: e.flowName,
|
|
534
|
+
error: err?.message
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
};
|
|
287
538
|
const handleOrchestration = async (e) => {
|
|
288
539
|
try {
|
|
289
540
|
if (e.id && e.ts) {
|
|
@@ -296,8 +547,8 @@ export function createFlowWiring() {
|
|
|
296
547
|
if (!runId) return;
|
|
297
548
|
const flowName = e.flowName;
|
|
298
549
|
if (!flowName) return;
|
|
299
|
-
const streamName =
|
|
300
|
-
const indexKey =
|
|
550
|
+
const streamName = StoreSubjects.flowRun(runId);
|
|
551
|
+
const indexKey = StoreSubjects.flowRunIndex(flowName);
|
|
301
552
|
if (e.type === "flow.start") {
|
|
302
553
|
const timestamp = Date.now();
|
|
303
554
|
await indexFlowRun(flowName, runId, timestamp, {
|
|
@@ -307,13 +558,14 @@ export function createFlowWiring() {
|
|
|
307
558
|
// Initialize for stall detection
|
|
308
559
|
stepCount: 0,
|
|
309
560
|
completedSteps: 0,
|
|
310
|
-
emittedEvents:
|
|
561
|
+
emittedEvents: {}
|
|
562
|
+
// Object for atomic updates
|
|
311
563
|
});
|
|
312
564
|
}
|
|
313
565
|
if (e.type === "flow.cancel") {
|
|
314
566
|
try {
|
|
315
|
-
if (store.
|
|
316
|
-
await store.
|
|
567
|
+
if (store.index.updateWithRetry) {
|
|
568
|
+
await store.index.updateWithRetry(indexKey, runId, {
|
|
317
569
|
status: "canceled",
|
|
318
570
|
completedAt: Date.now()
|
|
319
571
|
});
|
|
@@ -334,8 +586,8 @@ export function createFlowWiring() {
|
|
|
334
586
|
}
|
|
335
587
|
if (e.type === "step.completed") {
|
|
336
588
|
try {
|
|
337
|
-
if (store.
|
|
338
|
-
const newCount = await store.
|
|
589
|
+
if (store.index.increment) {
|
|
590
|
+
const newCount = await store.index.increment(indexKey, runId, "completedSteps", 1);
|
|
339
591
|
logger.debug("Incremented completedSteps", {
|
|
340
592
|
flowName,
|
|
341
593
|
runId,
|
|
@@ -351,29 +603,161 @@ export function createFlowWiring() {
|
|
|
351
603
|
});
|
|
352
604
|
}
|
|
353
605
|
}
|
|
606
|
+
if (e.type === "await.registered") {
|
|
607
|
+
const awaitEvent = e;
|
|
608
|
+
const { stepName, awaitType, position, config: config2 } = awaitEvent;
|
|
609
|
+
try {
|
|
610
|
+
if (store.index.updateWithRetry) {
|
|
611
|
+
const now = Date.now();
|
|
612
|
+
let timeoutAt;
|
|
613
|
+
if (awaitType === "time" && config2.delay) {
|
|
614
|
+
timeoutAt = now + config2.delay;
|
|
615
|
+
} else if (awaitType === "schedule" && config2.nextOccurrence) {
|
|
616
|
+
timeoutAt = config2.nextOccurrence;
|
|
617
|
+
} else if (config2.timeout) {
|
|
618
|
+
timeoutAt = now + config2.timeout;
|
|
619
|
+
}
|
|
620
|
+
if (!timeoutAt) {
|
|
621
|
+
timeoutAt = now + 24 * 60 * 60 * 1e3;
|
|
622
|
+
}
|
|
623
|
+
const updatePayload = {
|
|
624
|
+
awaitingSteps: {
|
|
625
|
+
[stepName]: {
|
|
626
|
+
status: "awaiting",
|
|
627
|
+
awaitType,
|
|
628
|
+
position,
|
|
629
|
+
config: config2,
|
|
630
|
+
registeredAt: now,
|
|
631
|
+
timeoutAt
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
await store.index.updateWithRetry(indexKey, runId, updatePayload);
|
|
636
|
+
logger.info("Await registered in index", {
|
|
637
|
+
runId,
|
|
638
|
+
stepName,
|
|
639
|
+
awaitType,
|
|
640
|
+
position,
|
|
641
|
+
timeoutAt: new Date(timeoutAt).toISOString()
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
} catch (err) {
|
|
645
|
+
logger.error("Error updating await status", {
|
|
646
|
+
runId,
|
|
647
|
+
stepName,
|
|
648
|
+
error: err?.message
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
if (e.type === "await.resolved") {
|
|
653
|
+
const awaitEvent = e;
|
|
654
|
+
const { stepName, triggerData } = awaitEvent;
|
|
655
|
+
try {
|
|
656
|
+
if (store.index.updateWithRetry) {
|
|
657
|
+
await store.index.updateWithRetry(indexKey, runId, {
|
|
658
|
+
awaitingSteps: {
|
|
659
|
+
[stepName]: {
|
|
660
|
+
status: "resolved",
|
|
661
|
+
triggerData
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
await checkAndTriggerPendingSteps(flowName, runId, store);
|
|
667
|
+
} catch (err) {
|
|
668
|
+
logger.error("Error handling await resolution", {
|
|
669
|
+
runId,
|
|
670
|
+
stepName,
|
|
671
|
+
error: err?.message
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
if (e.type === "await.timeout") {
|
|
676
|
+
const timeoutEvent = e;
|
|
677
|
+
const { stepName, timeoutAction, position, awaitType } = timeoutEvent;
|
|
678
|
+
const action = timeoutAction || "fail";
|
|
679
|
+
logger.warn("Await timeout occurred", {
|
|
680
|
+
runId,
|
|
681
|
+
stepName,
|
|
682
|
+
awaitType,
|
|
683
|
+
position,
|
|
684
|
+
action
|
|
685
|
+
});
|
|
686
|
+
try {
|
|
687
|
+
if (action === "fail") {
|
|
688
|
+
if (store.index.updateWithRetry) {
|
|
689
|
+
await store.index.updateWithRetry(indexKey, runId, {
|
|
690
|
+
awaitingSteps: {
|
|
691
|
+
[stepName]: {
|
|
692
|
+
status: "timeout",
|
|
693
|
+
timedOutAt: Date.now()
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
bus.publish({
|
|
699
|
+
type: "step.failed",
|
|
700
|
+
runId,
|
|
701
|
+
flowName,
|
|
702
|
+
stepName,
|
|
703
|
+
stepId: `${runId}__${stepName}__timeout`,
|
|
704
|
+
attempt: 1,
|
|
705
|
+
data: {
|
|
706
|
+
error: `Await timeout: ${awaitType} await exceeded timeout`
|
|
707
|
+
}
|
|
708
|
+
});
|
|
709
|
+
} else if (action === "continue") {
|
|
710
|
+
if (store.index.updateWithRetry) {
|
|
711
|
+
await store.index.updateWithRetry(indexKey, runId, {
|
|
712
|
+
awaitingSteps: {
|
|
713
|
+
[stepName]: {
|
|
714
|
+
status: "resolved",
|
|
715
|
+
triggerData: null,
|
|
716
|
+
timedOutAt: Date.now()
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
logger.info("Await timeout - continuing with null data", { runId, stepName });
|
|
722
|
+
await checkAndTriggerPendingSteps(flowName, runId, store);
|
|
723
|
+
}
|
|
724
|
+
} catch (err) {
|
|
725
|
+
logger.error("Error handling await timeout", {
|
|
726
|
+
runId,
|
|
727
|
+
stepName,
|
|
728
|
+
error: err?.message
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
}
|
|
354
732
|
if (e.type === "emit") {
|
|
355
733
|
const eventName = e.data?.name || e.data?.topic;
|
|
356
734
|
if (!eventName) {
|
|
357
735
|
logger.warn("Emit event missing name/topic", { flowName, runId, data: e.data });
|
|
358
736
|
} else {
|
|
359
737
|
try {
|
|
360
|
-
if (!store.
|
|
361
|
-
logger.warn("StoreAdapter does not support
|
|
738
|
+
if (!store.index.updateWithRetry) {
|
|
739
|
+
logger.warn("StoreAdapter does not support indexUpdateWithRetry");
|
|
362
740
|
return;
|
|
363
741
|
}
|
|
364
|
-
const
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
flowName,
|
|
372
|
-
runId,
|
|
373
|
-
name: eventName,
|
|
374
|
-
allEmitted: [...emittedEvents, eventName]
|
|
375
|
-
});
|
|
742
|
+
const timestamp = Date.now();
|
|
743
|
+
const eventParts = eventName.split(".");
|
|
744
|
+
const emittedEventsUpdate = {};
|
|
745
|
+
let current = emittedEventsUpdate;
|
|
746
|
+
for (let i = 0; i < eventParts.length - 1; i++) {
|
|
747
|
+
current[eventParts[i]] = {};
|
|
748
|
+
current = current[eventParts[i]];
|
|
376
749
|
}
|
|
750
|
+
current[eventParts[eventParts.length - 1]] = timestamp;
|
|
751
|
+
const updatePayload = {
|
|
752
|
+
emittedEvents: emittedEventsUpdate
|
|
753
|
+
};
|
|
754
|
+
await store.index.updateWithRetry(indexKey, runId, updatePayload);
|
|
755
|
+
logger.debug("Tracked emit event", {
|
|
756
|
+
flowName,
|
|
757
|
+
runId,
|
|
758
|
+
name: eventName,
|
|
759
|
+
timestamp
|
|
760
|
+
});
|
|
377
761
|
} catch (err) {
|
|
378
762
|
logger.warn("Failed to track emitted event", {
|
|
379
763
|
flowName,
|
|
@@ -384,16 +768,27 @@ export function createFlowWiring() {
|
|
|
384
768
|
}
|
|
385
769
|
}
|
|
386
770
|
}
|
|
771
|
+
if (e.type === "step.completed") {
|
|
772
|
+
try {
|
|
773
|
+
await checkAndTriggerPendingSteps(flowName, runId, store);
|
|
774
|
+
} catch (err) {
|
|
775
|
+
logger.error("Error checking pending steps", {
|
|
776
|
+
flowName,
|
|
777
|
+
runId,
|
|
778
|
+
error: err.message
|
|
779
|
+
});
|
|
780
|
+
throw err;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
387
783
|
if (e.type === "step.completed" || e.type === "step.failed") {
|
|
388
|
-
await checkAndTriggerPendingSteps(flowName, runId, store);
|
|
389
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
390
784
|
try {
|
|
391
|
-
const allEvents = await store.read(streamName);
|
|
785
|
+
const allEvents = await store.stream.read(streamName);
|
|
392
786
|
const analyzedFlows = $useAnalyzedFlows();
|
|
393
787
|
const flowDef = analyzedFlows.find((f) => f.id === flowName);
|
|
394
788
|
if (flowDef?.steps) {
|
|
395
789
|
const entryStepName = flowDef.entry?.step;
|
|
396
|
-
const
|
|
790
|
+
const entryStepDef = flowDef.entry;
|
|
791
|
+
const analysis = analyzeFlowCompletion(flowDef.steps, entryStepName, allEvents, entryStepDef);
|
|
397
792
|
const updateMetadata = {
|
|
398
793
|
status: analysis.status,
|
|
399
794
|
stepCount: analysis.totalSteps
|
|
@@ -401,19 +796,57 @@ export function createFlowWiring() {
|
|
|
401
796
|
if (analysis.status !== "running" && analysis.completedAt) {
|
|
402
797
|
updateMetadata.completedAt = analysis.completedAt;
|
|
403
798
|
}
|
|
404
|
-
if (store.
|
|
405
|
-
await store.
|
|
799
|
+
if (store.index.get) {
|
|
800
|
+
const currentEntry = await store.index.get(indexKey, runId);
|
|
801
|
+
const awaitingStepsObj = currentEntry?.metadata?.awaitingSteps || {};
|
|
802
|
+
let hasActiveAwaits = false;
|
|
803
|
+
let hasTimedOutAwaits = false;
|
|
804
|
+
for (const [_stepName, awaitState] of Object.entries(awaitingStepsObj)) {
|
|
805
|
+
if (awaitState?.status === "awaiting") {
|
|
806
|
+
hasActiveAwaits = true;
|
|
807
|
+
} else if (awaitState?.status === "timeout") {
|
|
808
|
+
hasTimedOutAwaits = true;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
if (hasActiveAwaits) {
|
|
812
|
+
updateMetadata.status = "awaiting";
|
|
813
|
+
}
|
|
814
|
+
if (hasTimedOutAwaits) {
|
|
815
|
+
updateMetadata.status = "failed";
|
|
816
|
+
if (!updateMetadata.completedAt) {
|
|
817
|
+
updateMetadata.completedAt = Date.now();
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
if (store.index.updateWithRetry) {
|
|
822
|
+
await store.index.updateWithRetry(indexKey, runId, updateMetadata);
|
|
406
823
|
}
|
|
407
|
-
|
|
408
|
-
|
|
824
|
+
const finalStatus = updateMetadata.status || analysis.status;
|
|
825
|
+
if (finalStatus === "completed" || finalStatus === "failed") {
|
|
826
|
+
const eventType = finalStatus === "completed" ? "flow.completed" : "flow.failed";
|
|
827
|
+
let currentStatus = null;
|
|
828
|
+
if (store.index.get) {
|
|
829
|
+
const currentEntry = await store.index.get(indexKey, runId);
|
|
830
|
+
currentStatus = currentEntry?.metadata?.status;
|
|
831
|
+
}
|
|
409
832
|
const terminalEventExists = allEvents.some((evt) => evt.type === "flow.completed" || evt.type === "flow.failed");
|
|
833
|
+
const publishKey = `${runId}:terminal`;
|
|
834
|
+
const alreadyPublishing = publishingTerminalEvents.has(publishKey);
|
|
410
835
|
if (terminalEventExists) {
|
|
411
|
-
logger.debug("
|
|
836
|
+
logger.debug("Flow terminal event already exists in stream, skipping publish", {
|
|
837
|
+
flowName,
|
|
838
|
+
runId,
|
|
839
|
+
currentStatus,
|
|
840
|
+
eventType
|
|
841
|
+
});
|
|
842
|
+
} else if (alreadyPublishing) {
|
|
843
|
+
logger.debug("Flow terminal event already being published, skipping duplicate", {
|
|
412
844
|
flowName,
|
|
413
845
|
runId,
|
|
414
846
|
eventType
|
|
415
847
|
});
|
|
416
848
|
} else {
|
|
849
|
+
publishingTerminalEvents.add(publishKey);
|
|
417
850
|
logger.info("Publishing terminal event to bus", {
|
|
418
851
|
flowName,
|
|
419
852
|
runId,
|
|
@@ -441,11 +874,48 @@ export function createFlowWiring() {
|
|
|
441
874
|
type: e.type,
|
|
442
875
|
runId: e.runId,
|
|
443
876
|
flowName: e.flowName,
|
|
444
|
-
error: err?.message
|
|
445
|
-
stack: err?.stack
|
|
877
|
+
error: err?.message
|
|
446
878
|
});
|
|
447
879
|
}
|
|
448
880
|
};
|
|
881
|
+
const processEventSequentially = async (event) => {
|
|
882
|
+
const runId = event.runId;
|
|
883
|
+
if (!runId) {
|
|
884
|
+
await handlePersistence(event);
|
|
885
|
+
await handleOrchestration(event);
|
|
886
|
+
await handleFlowStats(event);
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
const previousProcessing = flowProcessingChain.get(runId) || Promise.resolve();
|
|
890
|
+
const currentProcessing = previousProcessing.then(async () => {
|
|
891
|
+
try {
|
|
892
|
+
await handlePersistence(event);
|
|
893
|
+
await handleOrchestration(event);
|
|
894
|
+
await handleFlowStats(event);
|
|
895
|
+
} catch (err) {
|
|
896
|
+
logger.error("Error in sequential event processing", {
|
|
897
|
+
runId,
|
|
898
|
+
type: event.type,
|
|
899
|
+
error: err.message,
|
|
900
|
+
stack: err.stack
|
|
901
|
+
});
|
|
902
|
+
} finally {
|
|
903
|
+
const existingTimer = cleanupTimers.get(runId);
|
|
904
|
+
if (existingTimer) {
|
|
905
|
+
clearTimeout(existingTimer);
|
|
906
|
+
}
|
|
907
|
+
const timer = setTimeout(() => {
|
|
908
|
+
if (flowProcessingChain.get(runId) === currentProcessing) {
|
|
909
|
+
flowProcessingChain.delete(runId);
|
|
910
|
+
cleanupTimers.delete(runId);
|
|
911
|
+
}
|
|
912
|
+
}, 6e4);
|
|
913
|
+
cleanupTimers.set(runId, timer);
|
|
914
|
+
}
|
|
915
|
+
});
|
|
916
|
+
flowProcessingChain.set(runId, currentProcessing);
|
|
917
|
+
return currentProcessing;
|
|
918
|
+
};
|
|
449
919
|
const eventTypes = [
|
|
450
920
|
"flow.start",
|
|
451
921
|
"flow.completed",
|
|
@@ -455,29 +925,82 @@ export function createFlowWiring() {
|
|
|
455
925
|
"step.completed",
|
|
456
926
|
"step.failed",
|
|
457
927
|
"step.retry",
|
|
928
|
+
"await.registered",
|
|
929
|
+
"await.resolved",
|
|
930
|
+
"await.timeout",
|
|
458
931
|
"log",
|
|
459
932
|
"emit",
|
|
460
933
|
"state"
|
|
461
934
|
];
|
|
462
935
|
for (const type of eventTypes) {
|
|
463
|
-
unsubs.push(bus.onType(type,
|
|
464
|
-
}
|
|
465
|
-
for (const type of eventTypes) {
|
|
466
|
-
unsubs.push(bus.onType(type, handleOrchestration));
|
|
936
|
+
unsubs.push(bus.onType(type, processEventSequentially));
|
|
467
937
|
}
|
|
468
938
|
const config = useRuntimeConfig();
|
|
469
|
-
const flowConfig = config.nvent.
|
|
939
|
+
const flowConfig = config.nvent.flow || {};
|
|
470
940
|
stallDetector = createStallDetector(store, flowConfig.stallDetection);
|
|
471
|
-
if (flowConfig.stallDetection
|
|
472
|
-
stallDetector.start();
|
|
473
|
-
|
|
941
|
+
if (flowConfig.stallDetection?.enabled) {
|
|
942
|
+
await stallDetector.start();
|
|
943
|
+
const scheduleConfig = stallDetector.getScheduleConfig();
|
|
944
|
+
if (scheduleConfig.enabled) {
|
|
945
|
+
try {
|
|
946
|
+
const scheduler = useScheduler();
|
|
947
|
+
logger.info("Scheduling periodic stall detector from flowWiring", {
|
|
948
|
+
checkInterval: `${scheduleConfig.interval / 1e3}s`
|
|
949
|
+
});
|
|
950
|
+
const jobId = await scheduler.schedule({
|
|
951
|
+
id: "stall-detection",
|
|
952
|
+
name: "Flow Stall Detection",
|
|
953
|
+
type: "interval",
|
|
954
|
+
interval: scheduleConfig.interval,
|
|
955
|
+
handler: async () => {
|
|
956
|
+
if (!stallDetector || !wired) {
|
|
957
|
+
logger.debug("Stall detector handler called but wiring stopped");
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
try {
|
|
961
|
+
logger.info("Stall detector running periodic check");
|
|
962
|
+
const analyzedFlows = $useAnalyzedFlows();
|
|
963
|
+
const flowNames = analyzedFlows.map((f) => f.id).filter(Boolean);
|
|
964
|
+
if (flowNames.length > 0) {
|
|
965
|
+
await stallDetector.checkFlowsForStalls(flowNames);
|
|
966
|
+
}
|
|
967
|
+
} catch (error) {
|
|
968
|
+
logger.error("Stall detector periodic check failed", {
|
|
969
|
+
error: error.message,
|
|
970
|
+
stack: error.stack
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
},
|
|
974
|
+
metadata: {
|
|
975
|
+
component: "stall-detector",
|
|
976
|
+
stallTimeout: scheduleConfig.stallTimeout,
|
|
977
|
+
checkInterval: scheduleConfig.interval
|
|
978
|
+
}
|
|
979
|
+
});
|
|
980
|
+
stallDetector.setSchedulerJobId(jobId);
|
|
981
|
+
logger.info("Stall detector started and scheduled", { jobId });
|
|
982
|
+
} catch (error) {
|
|
983
|
+
logger.error("Failed to schedule stall detector - periodic checks disabled", {
|
|
984
|
+
error: error.message,
|
|
985
|
+
stack: error.stack
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
} else {
|
|
989
|
+
logger.info("Stall detector started (periodic check disabled)");
|
|
990
|
+
}
|
|
474
991
|
}
|
|
475
992
|
}
|
|
476
|
-
function stop() {
|
|
993
|
+
async function stop() {
|
|
477
994
|
const logger = useNventLogger("flow-wiring");
|
|
478
995
|
if (stallDetector) {
|
|
479
|
-
|
|
480
|
-
|
|
996
|
+
try {
|
|
997
|
+
await stallDetector.stop();
|
|
998
|
+
stallDetector = void 0;
|
|
999
|
+
} catch (error) {
|
|
1000
|
+
logger.error("Error stopping stall detector", {
|
|
1001
|
+
error: error.message
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
481
1004
|
}
|
|
482
1005
|
for (const u of unsubs.splice(0)) {
|
|
483
1006
|
try {
|
|
@@ -485,8 +1008,13 @@ export function createFlowWiring() {
|
|
|
485
1008
|
} catch {
|
|
486
1009
|
}
|
|
487
1010
|
}
|
|
1011
|
+
publishingTerminalEvents.clear();
|
|
1012
|
+
for (const timer of cleanupTimers.values()) {
|
|
1013
|
+
clearTimeout(timer);
|
|
1014
|
+
}
|
|
1015
|
+
cleanupTimers.clear();
|
|
1016
|
+
flowProcessingChain.clear();
|
|
488
1017
|
wired = false;
|
|
489
|
-
logger.debug("Flow wiring stopped");
|
|
490
1018
|
}
|
|
491
1019
|
return { start, stop };
|
|
492
1020
|
}
|