nvent 0.5.4 → 0.5.6
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.json +1 -1
- package/dist/module.mjs +127 -23
- package/dist/runtime/adapters/builtin/memory-store.d.ts +2 -1
- package/dist/runtime/adapters/builtin/memory-store.js +28 -4
- package/dist/runtime/adapters/factory.js +8 -7
- package/dist/runtime/adapters/interfaces/queue.d.ts +5 -0
- package/dist/runtime/adapters/interfaces/store.d.ts +3 -1
- package/dist/runtime/config/index.js +14 -1
- package/dist/runtime/config/types.d.ts +42 -0
- package/dist/runtime/events/types.d.ts +0 -1
- package/dist/runtime/events/utils/stallDetector.d.ts +13 -77
- package/dist/runtime/events/utils/stallDetector.js +8 -192
- package/dist/runtime/events/wiring/flowWiring.js +347 -109
- package/dist/runtime/events/wiring/registry.js +9 -1
- package/dist/runtime/events/wiring/triggerWiring.js +11 -1
- package/dist/runtime/nitro/plugins/02.workers.js +31 -2
- package/dist/runtime/nitro/routes/webhook.await.js +28 -6
- package/dist/runtime/nitro/routes/webhook.trigger.d.ts +17 -0
- package/dist/runtime/nitro/routes/webhook.trigger.js +9 -0
- package/dist/runtime/nitro/utils/awaitPatterns/event.js +58 -50
- package/dist/runtime/nitro/utils/awaitPatterns/schedule.js +6 -1
- package/dist/runtime/nitro/utils/awaitPatterns/time.d.ts +1 -1
- package/dist/runtime/nitro/utils/awaitPatterns/time.js +6 -2
- package/dist/runtime/nitro/utils/awaitPatterns/webhook.js +53 -45
- package/dist/runtime/nitro/utils/defineFunction.d.ts +2 -9
- package/dist/runtime/nitro/utils/defineFunction.js +1 -14
- package/dist/runtime/nitro/utils/defineFunctionConfig.d.ts +84 -16
- package/dist/runtime/nitro/utils/defineHooks.d.ts +64 -10
- package/dist/runtime/nitro/utils/defineHooks.js +3 -0
- package/dist/runtime/nitro/utils/useAwait.d.ts +12 -0
- package/dist/runtime/nitro/utils/useAwait.js +34 -4
- package/dist/runtime/nitro/utils/useFlow.d.ts +39 -48
- package/dist/runtime/nitro/utils/useFlow.js +53 -14
- package/dist/runtime/nitro/utils/useHookRegistry.d.ts +10 -4
- package/dist/runtime/nitro/utils/useTrigger.js +7 -16
- package/dist/runtime/scheduler/index.js +5 -1
- package/dist/runtime/scheduler/scheduler.d.ts +19 -0
- package/dist/runtime/scheduler/scheduler.js +184 -8
- package/dist/runtime/scheduler/types.d.ts +6 -0
- package/dist/runtime/worker/node/runner.d.ts +44 -2
- package/dist/runtime/worker/node/runner.js +45 -100
- package/dist/runtime/worker/system/awaitHandlers.d.ts +27 -0
- package/dist/runtime/worker/system/awaitHandlers.js +230 -0
- package/dist/runtime/worker/system/index.d.ts +24 -0
- package/dist/runtime/worker/system/index.js +39 -0
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getEventBus } from "../eventBus.js";
|
|
2
2
|
import { useNventLogger, useStoreAdapter, useQueueAdapter, $useAnalyzedFlows, $useFunctionRegistry, useStreamTopics, useRuntimeConfig, useScheduler } from "#imports";
|
|
3
3
|
import { createStallDetector } from "../utils/stallDetector.js";
|
|
4
|
+
import { SYSTEM_HANDLERS } from "../../worker/system/index.js";
|
|
4
5
|
export function checkPendingStepTriggers(step, emittedEvents, completedSteps) {
|
|
5
6
|
if (!step.subscribes || step.subscribes.length === 0) {
|
|
6
7
|
return true;
|
|
@@ -70,49 +71,22 @@ export async function checkAndTriggerPendingSteps(flowName, runId, store) {
|
|
|
70
71
|
for (const [stepName, stepDef] of Object.entries(flowDef.steps)) {
|
|
71
72
|
const step = stepDef;
|
|
72
73
|
if (!step.subscribes || completedSteps.has(stepName)) continue;
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
}
|
|
74
|
+
const awaitBeforeKey = `${stepName}:before`;
|
|
75
|
+
const awaitState = flowEntry?.metadata?.awaitingSteps?.[awaitBeforeKey];
|
|
76
|
+
if (awaitState && awaitState.status === "awaiting") continue;
|
|
77
|
+
if (awaitState && awaitState.status === "timeout") continue;
|
|
103
78
|
const isDependencyAwaiting = step.subscribes.some((sub) => {
|
|
104
79
|
const emitEvent = allEvents.find(
|
|
105
80
|
(evt) => evt.type === "emit" && evt.data?.name === sub
|
|
106
81
|
);
|
|
107
|
-
if (!emitEvent)
|
|
108
|
-
return false;
|
|
109
|
-
}
|
|
82
|
+
if (!emitEvent) return false;
|
|
110
83
|
const emitStepName = emitEvent.stepName;
|
|
111
84
|
if (!emitStepName) {
|
|
112
85
|
return false;
|
|
113
86
|
}
|
|
114
|
-
const
|
|
115
|
-
|
|
87
|
+
const awaitAfterKey = `${emitStepName}:after`;
|
|
88
|
+
const awaitState2 = awaitingSteps[awaitAfterKey];
|
|
89
|
+
if (awaitState2?.status === "awaiting") {
|
|
116
90
|
return true;
|
|
117
91
|
}
|
|
118
92
|
if (awaitState2?.status === "resolved") {
|
|
@@ -128,20 +102,80 @@ export async function checkAndTriggerPendingSteps(flowName, runId, store) {
|
|
|
128
102
|
);
|
|
129
103
|
if (stepCompleted) {
|
|
130
104
|
const awaitResolved = allEvents.some(
|
|
131
|
-
(evt) => evt.type === "await.resolved" && evt.stepName === emitStepName
|
|
105
|
+
(evt) => evt.type === "await.resolved" && evt.stepName === emitStepName && evt.position === "after"
|
|
132
106
|
);
|
|
133
|
-
if (!awaitResolved)
|
|
134
|
-
return true;
|
|
135
|
-
}
|
|
107
|
+
if (!awaitResolved) return true;
|
|
136
108
|
}
|
|
137
109
|
}
|
|
138
110
|
return false;
|
|
139
111
|
});
|
|
140
|
-
if (isDependencyAwaiting)
|
|
141
|
-
continue;
|
|
142
|
-
}
|
|
112
|
+
if (isDependencyAwaiting) continue;
|
|
143
113
|
const canTrigger = checkPendingStepTriggers(step, emittedEvents, completedSteps);
|
|
144
|
-
if (canTrigger) {
|
|
114
|
+
if (canTrigger && step.awaitBefore) {
|
|
115
|
+
if (awaitState?.status === "awaiting") continue;
|
|
116
|
+
if (awaitState?.status !== "resolved") {
|
|
117
|
+
try {
|
|
118
|
+
const emitData = {};
|
|
119
|
+
const subscribes = step.subscribes || [];
|
|
120
|
+
for (const sub of subscribes) {
|
|
121
|
+
const emitEvent = allEvents.find(
|
|
122
|
+
(evt) => evt.type === "emit" && evt.data?.name === sub
|
|
123
|
+
);
|
|
124
|
+
if (emitEvent && emitEvent.data?.payload !== void 0) {
|
|
125
|
+
emitData[sub] = emitEvent.data.payload;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const payload = {
|
|
129
|
+
flowId: runId,
|
|
130
|
+
flowName,
|
|
131
|
+
stepName,
|
|
132
|
+
position: "before",
|
|
133
|
+
awaitConfig: step.awaitBefore,
|
|
134
|
+
input: emitData
|
|
135
|
+
};
|
|
136
|
+
const jobId = `${runId}__${stepName}__await-register-before`;
|
|
137
|
+
const flowRegistry = (registry?.flows || {})[flowName];
|
|
138
|
+
const stepMeta = flowRegistry?.steps?.[stepName];
|
|
139
|
+
let stepQueue = stepMeta?.queue;
|
|
140
|
+
if (!stepQueue && flowRegistry?.entry?.step === stepName) {
|
|
141
|
+
stepQueue = flowRegistry.entry.queue;
|
|
142
|
+
}
|
|
143
|
+
if (!stepQueue && registry?.workers) {
|
|
144
|
+
const worker = registry.workers.find((w) => {
|
|
145
|
+
const flowNames = w?.flow?.names || (w?.flow?.name ? [w?.flow?.name] : []);
|
|
146
|
+
const stepMatch = w?.flow?.step === stepName || Array.isArray(w?.flow?.step) && w?.flow?.step.includes(stepName);
|
|
147
|
+
return flowNames.includes(flowName) && stepMatch;
|
|
148
|
+
});
|
|
149
|
+
stepQueue = worker?.queue?.name;
|
|
150
|
+
}
|
|
151
|
+
if (!stepQueue) {
|
|
152
|
+
logger.error("Cannot find queue for step", {
|
|
153
|
+
stepName,
|
|
154
|
+
flowName,
|
|
155
|
+
availableSteps: Object.keys(flowRegistry?.steps || {}),
|
|
156
|
+
entryStep: flowRegistry?.entry?.step
|
|
157
|
+
});
|
|
158
|
+
throw new Error(`Cannot register await: queue not found for step ${stepName} in flow ${flowName}`);
|
|
159
|
+
}
|
|
160
|
+
const analyzedAwaitStep = (flowDef.analyzed?.steps || {})[stepName];
|
|
161
|
+
const awaitStepTimeout = analyzedAwaitStep?.stepTimeout;
|
|
162
|
+
await queue.enqueue(stepQueue, {
|
|
163
|
+
name: SYSTEM_HANDLERS.AWAIT_REGISTER,
|
|
164
|
+
data: payload,
|
|
165
|
+
opts: { jobId, timeout: awaitStepTimeout }
|
|
166
|
+
});
|
|
167
|
+
} catch (err) {
|
|
168
|
+
logger.error("Failed to register awaitBefore pattern", {
|
|
169
|
+
flowName,
|
|
170
|
+
stepName,
|
|
171
|
+
error: err.message
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const shouldEnqueueStep = canTrigger && (!step.awaitBefore || awaitState?.status === "resolved");
|
|
178
|
+
if (shouldEnqueueStep) {
|
|
145
179
|
const flowRegistry = (registry?.flows || {})[flowName];
|
|
146
180
|
const stepMeta = flowRegistry?.steps?.[stepName];
|
|
147
181
|
if (stepMeta?.queue) {
|
|
@@ -161,16 +195,20 @@ export async function checkAndTriggerPendingSteps(flowName, runId, store) {
|
|
|
161
195
|
input: emitData
|
|
162
196
|
// Keyed by event name
|
|
163
197
|
};
|
|
164
|
-
|
|
198
|
+
const isAwaitResuming = awaitState?.status === "resolved" && awaitState?.position === "before";
|
|
199
|
+
if (isAwaitResuming) {
|
|
165
200
|
payload.awaitResolved = true;
|
|
166
201
|
payload.awaitData = awaitState.triggerData;
|
|
202
|
+
payload.awaitPosition = "before";
|
|
167
203
|
}
|
|
168
|
-
const jobId = `${runId}__${stepName}`;
|
|
204
|
+
const jobId = isAwaitResuming ? `${runId}__${stepName}__resumed` : `${runId}__${stepName}`;
|
|
169
205
|
const worker = registry?.workers?.find(
|
|
170
206
|
(w) => w?.flow?.step === stepName && w?.queue?.name === stepMeta.queue
|
|
171
207
|
);
|
|
172
208
|
const defaultOpts = worker?.queue?.defaultJobOptions || {};
|
|
173
|
-
const
|
|
209
|
+
const analyzedStep = (flowDef.analyzed?.steps || {})[stepName];
|
|
210
|
+
const stepTimeout = analyzedStep?.stepTimeout;
|
|
211
|
+
const opts = { ...defaultOpts, jobId, timeout: stepTimeout };
|
|
174
212
|
try {
|
|
175
213
|
await queue.enqueue(stepMeta.queue, { name: stepName, data: payload, opts });
|
|
176
214
|
} catch {
|
|
@@ -402,6 +440,21 @@ export function createFlowWiring() {
|
|
|
402
440
|
const persistedEvent = await store.stream.append(streamName, eventData);
|
|
403
441
|
await bus.publish(persistedEvent);
|
|
404
442
|
if (e.type === "flow.completed" || e.type === "flow.failed") {
|
|
443
|
+
try {
|
|
444
|
+
const scheduler = useScheduler();
|
|
445
|
+
const flowJobs = await scheduler.getJobsByPattern(runId);
|
|
446
|
+
for (const job of flowJobs) {
|
|
447
|
+
await scheduler.unschedule(job.id);
|
|
448
|
+
logger.debug(`Unscheduled job for ${e.type} flow: ${job.id}`);
|
|
449
|
+
}
|
|
450
|
+
logger.debug(`Unscheduled ${flowJobs.length} scheduled jobs for ${e.type} flow runId '${runId}'`, {
|
|
451
|
+
jobs: flowJobs.map((j) => j.id)
|
|
452
|
+
});
|
|
453
|
+
} catch (error) {
|
|
454
|
+
logger.debug(`Could not unschedule jobs for runId '${runId}'`, {
|
|
455
|
+
error: error.message
|
|
456
|
+
});
|
|
457
|
+
}
|
|
405
458
|
const publishKey = `${runId}:terminal`;
|
|
406
459
|
setTimeout(() => {
|
|
407
460
|
try {
|
|
@@ -477,7 +530,12 @@ export function createFlowWiring() {
|
|
|
477
530
|
logger.debug("Updated flow stats for failure", { flowName });
|
|
478
531
|
} else if (e.type === "flow.cancel") {
|
|
479
532
|
if (store.index.increment) {
|
|
480
|
-
|
|
533
|
+
const previousStatus = e.data?.previousStatus;
|
|
534
|
+
if (previousStatus === "awaiting") {
|
|
535
|
+
await store.index.increment(flowIndexKey, flowName, "stats.awaiting", -1);
|
|
536
|
+
} else {
|
|
537
|
+
await store.index.increment(flowIndexKey, flowName, "stats.running", -1);
|
|
538
|
+
}
|
|
481
539
|
await store.index.increment(flowIndexKey, flowName, "stats.cancel", 1);
|
|
482
540
|
}
|
|
483
541
|
if (store.index.updateWithRetry) {
|
|
@@ -485,7 +543,7 @@ export function createFlowWiring() {
|
|
|
485
543
|
lastCompletedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
486
544
|
});
|
|
487
545
|
}
|
|
488
|
-
logger.debug("Updated flow stats for cancellation", { flowName });
|
|
546
|
+
logger.debug("Updated flow stats for cancellation", { flowName, previousStatus: e.data?.previousStatus });
|
|
489
547
|
} else if (e.type === "flow.stalled") {
|
|
490
548
|
if (store.index.increment && e.data?.previousStatus) {
|
|
491
549
|
if (e.data.previousStatus === "awaiting") {
|
|
@@ -528,11 +586,19 @@ export function createFlowWiring() {
|
|
|
528
586
|
});
|
|
529
587
|
}
|
|
530
588
|
} catch (err) {
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
589
|
+
const errorMsg = err?.message || "";
|
|
590
|
+
if (!errorMsg.includes("Entry not found")) {
|
|
591
|
+
logger.warn("Failed to update flow stats", {
|
|
592
|
+
type: e.type,
|
|
593
|
+
flowName: e.flowName,
|
|
594
|
+
error: errorMsg
|
|
595
|
+
});
|
|
596
|
+
} else {
|
|
597
|
+
logger.debug("Flow entry not found (will be created)", {
|
|
598
|
+
type: e.type,
|
|
599
|
+
flowName: e.flowName
|
|
600
|
+
});
|
|
601
|
+
}
|
|
536
602
|
}
|
|
537
603
|
};
|
|
538
604
|
const handleOrchestration = async (e) => {
|
|
@@ -561,6 +627,35 @@ export function createFlowWiring() {
|
|
|
561
627
|
emittedEvents: {}
|
|
562
628
|
// Object for atomic updates
|
|
563
629
|
});
|
|
630
|
+
try {
|
|
631
|
+
const analyzedFlows = $useAnalyzedFlows();
|
|
632
|
+
const flowMeta = analyzedFlows.find((f) => f.id === flowName);
|
|
633
|
+
const stallTimeout = flowMeta?.analyzed?.stallTimeout || 30 * 60 * 1e3;
|
|
634
|
+
const scheduler = useScheduler();
|
|
635
|
+
const stallJobId = `stall-timeout:${runId}`;
|
|
636
|
+
await scheduler.schedule({
|
|
637
|
+
id: stallJobId,
|
|
638
|
+
name: `Stall Timeout - ${flowName}`,
|
|
639
|
+
type: "one-time",
|
|
640
|
+
executeAt: timestamp + stallTimeout,
|
|
641
|
+
handler: async () => {
|
|
642
|
+
if (stallDetector) {
|
|
643
|
+
logger.info(`Per-flow stall timeout fired for '${flowName}' runId '${runId}'`);
|
|
644
|
+
await stallDetector.markAsStalled(flowName, runId, "Stall timeout reached");
|
|
645
|
+
}
|
|
646
|
+
},
|
|
647
|
+
metadata: {
|
|
648
|
+
component: "stall-detector",
|
|
649
|
+
flowName,
|
|
650
|
+
runId
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
logger.debug(`Scheduled stall timeout for flow '${flowName}' runId '${runId}' in ${stallTimeout / 1e3}s`, { jobId: stallJobId });
|
|
654
|
+
} catch (error) {
|
|
655
|
+
logger.warn(`Failed to schedule stall timeout for flow '${flowName}' runId '${runId}'`, {
|
|
656
|
+
error: error.message
|
|
657
|
+
});
|
|
658
|
+
}
|
|
564
659
|
}
|
|
565
660
|
if (e.type === "flow.cancel") {
|
|
566
661
|
try {
|
|
@@ -571,8 +666,17 @@ export function createFlowWiring() {
|
|
|
571
666
|
});
|
|
572
667
|
logger.info("Marked flow as canceled", { flowName, runId });
|
|
573
668
|
}
|
|
669
|
+
const scheduler = useScheduler();
|
|
670
|
+
const flowJobs = await scheduler.getJobsByPattern(runId);
|
|
671
|
+
for (const job of flowJobs) {
|
|
672
|
+
await scheduler.unschedule(job.id);
|
|
673
|
+
logger.debug(`Unscheduled job for canceled flow: ${job.id}`);
|
|
674
|
+
}
|
|
675
|
+
logger.debug(`Unscheduled ${flowJobs.length} scheduled jobs for canceled flow runId '${runId}'`, {
|
|
676
|
+
jobs: flowJobs.map((j) => j.id)
|
|
677
|
+
});
|
|
574
678
|
} catch (err) {
|
|
575
|
-
logger.warn("Failed to update canceled status", {
|
|
679
|
+
logger.warn("Failed to update canceled status or unschedule flow jobs", {
|
|
576
680
|
flowName,
|
|
577
681
|
runId,
|
|
578
682
|
error: err?.message
|
|
@@ -580,8 +684,36 @@ export function createFlowWiring() {
|
|
|
580
684
|
}
|
|
581
685
|
}
|
|
582
686
|
if (e.type === "step.started" || e.type === "step.completed" || e.type === "step.failed" || e.type === "step.retry") {
|
|
583
|
-
|
|
584
|
-
|
|
687
|
+
try {
|
|
688
|
+
const scheduler = useScheduler();
|
|
689
|
+
const stallJobId = `stall-timeout:${runId}`;
|
|
690
|
+
const analyzedFlows = $useAnalyzedFlows();
|
|
691
|
+
const flowMeta = analyzedFlows.find((f) => f.id === flowName);
|
|
692
|
+
const stallTimeout = flowMeta?.analyzed?.stallTimeout || 30 * 60 * 1e3;
|
|
693
|
+
await scheduler.unschedule(stallJobId);
|
|
694
|
+
await scheduler.schedule({
|
|
695
|
+
id: stallJobId,
|
|
696
|
+
name: `Stall Timeout - ${flowName}`,
|
|
697
|
+
type: "one-time",
|
|
698
|
+
executeAt: Date.now() + stallTimeout,
|
|
699
|
+
handler: async () => {
|
|
700
|
+
if (stallDetector) {
|
|
701
|
+
logger.info(`Per-flow stall timeout fired for '${flowName}' runId '${runId}'`);
|
|
702
|
+
await stallDetector.markAsStalled(flowName, runId, "Stall timeout reached");
|
|
703
|
+
}
|
|
704
|
+
},
|
|
705
|
+
metadata: {
|
|
706
|
+
component: "stall-detector",
|
|
707
|
+
flowName,
|
|
708
|
+
runId,
|
|
709
|
+
stallTimeout
|
|
710
|
+
}
|
|
711
|
+
});
|
|
712
|
+
logger.debug(`Rescheduled stall timeout for flow '${flowName}' runId '${runId}' (activity: ${e.type})`);
|
|
713
|
+
} catch (error) {
|
|
714
|
+
logger.debug(`Could not reschedule stall timeout for flow '${flowName}' runId '${runId}'`, {
|
|
715
|
+
error: error.message
|
|
716
|
+
});
|
|
585
717
|
}
|
|
586
718
|
}
|
|
587
719
|
if (e.type === "step.completed") {
|
|
@@ -608,6 +740,14 @@ export function createFlowWiring() {
|
|
|
608
740
|
const { stepName, awaitType, position, config: config2 } = awaitEvent;
|
|
609
741
|
try {
|
|
610
742
|
if (store.index.updateWithRetry) {
|
|
743
|
+
if (store.index.get) {
|
|
744
|
+
const currentEntry = await store.index.get(indexKey, runId);
|
|
745
|
+
const currentStatus = currentEntry?.metadata?.status;
|
|
746
|
+
if (currentStatus === "canceled") {
|
|
747
|
+
logger.debug("Flow already canceled, skipping await registration", { flowName, runId, stepName });
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
611
751
|
const now = Date.now();
|
|
612
752
|
let timeoutAt;
|
|
613
753
|
if (awaitType === "time" && config2.delay) {
|
|
@@ -620,10 +760,15 @@ export function createFlowWiring() {
|
|
|
620
760
|
if (!timeoutAt) {
|
|
621
761
|
timeoutAt = now + 24 * 60 * 60 * 1e3;
|
|
622
762
|
}
|
|
763
|
+
const awaitKey = `${stepName}:${position}`;
|
|
623
764
|
const updatePayload = {
|
|
765
|
+
status: "awaiting",
|
|
766
|
+
// Set flow status to awaiting
|
|
624
767
|
awaitingSteps: {
|
|
625
|
-
[
|
|
768
|
+
[awaitKey]: {
|
|
626
769
|
status: "awaiting",
|
|
770
|
+
stepName,
|
|
771
|
+
// Keep stepName for queries
|
|
627
772
|
awaitType,
|
|
628
773
|
position,
|
|
629
774
|
config: config2,
|
|
@@ -651,18 +796,88 @@ export function createFlowWiring() {
|
|
|
651
796
|
}
|
|
652
797
|
if (e.type === "await.resolved") {
|
|
653
798
|
const awaitEvent = e;
|
|
654
|
-
const { stepName, triggerData } = awaitEvent;
|
|
799
|
+
const { stepName, triggerData, position } = awaitEvent;
|
|
655
800
|
try {
|
|
801
|
+
if (store.index.get) {
|
|
802
|
+
const currentEntry = await store.index.get(indexKey, runId);
|
|
803
|
+
const currentStatus = currentEntry?.metadata?.status;
|
|
804
|
+
if (currentStatus === "canceled") {
|
|
805
|
+
logger.debug("Flow already canceled, skipping await resolution", { flowName, runId, stepName });
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
656
809
|
if (store.index.updateWithRetry) {
|
|
810
|
+
const awaitKey = `${stepName}:${position}`;
|
|
657
811
|
await store.index.updateWithRetry(indexKey, runId, {
|
|
658
812
|
awaitingSteps: {
|
|
659
|
-
[
|
|
813
|
+
[awaitKey]: {
|
|
660
814
|
status: "resolved",
|
|
661
|
-
|
|
815
|
+
stepName,
|
|
816
|
+
// Keep stepName for queries
|
|
817
|
+
triggerData,
|
|
818
|
+
position
|
|
662
819
|
}
|
|
663
820
|
}
|
|
664
821
|
});
|
|
665
822
|
}
|
|
823
|
+
const queue = useQueueAdapter();
|
|
824
|
+
const { StoreSubjects: StoreSubjects2 } = useStreamTopics();
|
|
825
|
+
const streamName2 = StoreSubjects2.flowRun(runId);
|
|
826
|
+
const inputData = {};
|
|
827
|
+
if (store.stream.read) {
|
|
828
|
+
const events = await store.stream.read(streamName2, { limit: 100 });
|
|
829
|
+
const registry2 = $useFunctionRegistry();
|
|
830
|
+
const flowRegistry2 = (registry2?.flows || {})[flowName];
|
|
831
|
+
const stepMeta2 = flowRegistry2?.steps?.[stepName];
|
|
832
|
+
const subscribes = stepMeta2?.subscribes || [];
|
|
833
|
+
for (const sub of subscribes) {
|
|
834
|
+
const emitEvent = events.find(
|
|
835
|
+
(evt) => evt.type === "emit" && evt.data?.name === sub
|
|
836
|
+
);
|
|
837
|
+
if (emitEvent && emitEvent.data?.payload !== void 0) {
|
|
838
|
+
inputData[sub] = emitEvent.data.payload;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
const { SYSTEM_HANDLERS: SYSTEM_HANDLERS2 } = await import("../../worker/system/index.js");
|
|
843
|
+
const payload = {
|
|
844
|
+
flowId: runId,
|
|
845
|
+
flowName,
|
|
846
|
+
stepName,
|
|
847
|
+
position,
|
|
848
|
+
triggerData,
|
|
849
|
+
input: inputData
|
|
850
|
+
};
|
|
851
|
+
const jobId = `${runId}__${stepName}__await-resolve`;
|
|
852
|
+
const registry = $useFunctionRegistry();
|
|
853
|
+
const flowRegistry = (registry?.flows || {})[flowName];
|
|
854
|
+
const stepMeta = flowRegistry?.steps?.[stepName];
|
|
855
|
+
let stepQueue = stepMeta?.queue;
|
|
856
|
+
if (!stepQueue && flowRegistry?.entry?.step === stepName) {
|
|
857
|
+
stepQueue = flowRegistry.entry.queue;
|
|
858
|
+
}
|
|
859
|
+
if (!stepQueue && registry?.workers) {
|
|
860
|
+
const worker = registry.workers.find((w) => {
|
|
861
|
+
const flowNames = w?.flow?.names || (w?.flow?.name ? [w?.flow?.name] : []);
|
|
862
|
+
const stepMatch = w?.flow?.step === stepName || Array.isArray(w?.flow?.step) && w?.flow?.step.includes(stepName);
|
|
863
|
+
return flowNames.includes(flowName) && stepMatch;
|
|
864
|
+
});
|
|
865
|
+
stepQueue = worker?.queue?.name;
|
|
866
|
+
}
|
|
867
|
+
if (!stepQueue) {
|
|
868
|
+
logger.error("Cannot find queue for step", {
|
|
869
|
+
stepName,
|
|
870
|
+
flowName,
|
|
871
|
+
availableSteps: Object.keys(flowRegistry?.steps || {}),
|
|
872
|
+
entryStep: flowRegistry?.entry?.step
|
|
873
|
+
});
|
|
874
|
+
throw new Error(`Cannot resolve await: queue not found for step ${stepName} in flow ${flowName}`);
|
|
875
|
+
}
|
|
876
|
+
await queue.enqueue(stepQueue, {
|
|
877
|
+
name: SYSTEM_HANDLERS2.AWAIT_RESOLVE,
|
|
878
|
+
data: payload,
|
|
879
|
+
opts: { jobId }
|
|
880
|
+
});
|
|
666
881
|
await checkAndTriggerPendingSteps(flowName, runId, store);
|
|
667
882
|
} catch (err) {
|
|
668
883
|
logger.error("Error handling await resolution", {
|
|
@@ -676,6 +891,14 @@ export function createFlowWiring() {
|
|
|
676
891
|
const timeoutEvent = e;
|
|
677
892
|
const { stepName, timeoutAction, position, awaitType } = timeoutEvent;
|
|
678
893
|
const action = timeoutAction || "fail";
|
|
894
|
+
if (store.index.get) {
|
|
895
|
+
const currentEntry = await store.index.get(indexKey, runId);
|
|
896
|
+
const currentStatus = currentEntry?.metadata?.status;
|
|
897
|
+
if (currentStatus === "canceled") {
|
|
898
|
+
logger.debug("Flow already canceled, skipping await timeout handling", { flowName, runId, stepName });
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
679
902
|
logger.warn("Await timeout occurred", {
|
|
680
903
|
runId,
|
|
681
904
|
stepName,
|
|
@@ -684,6 +907,63 @@ export function createFlowWiring() {
|
|
|
684
907
|
action
|
|
685
908
|
});
|
|
686
909
|
try {
|
|
910
|
+
const queue = useQueueAdapter();
|
|
911
|
+
const { StoreSubjects: StoreSubjects2 } = useStreamTopics();
|
|
912
|
+
const streamName2 = StoreSubjects2.flowRun(runId);
|
|
913
|
+
const inputData = {};
|
|
914
|
+
if (store.stream.read) {
|
|
915
|
+
const events = await store.stream.read(streamName2, { limit: 100 });
|
|
916
|
+
const registry2 = $useFunctionRegistry();
|
|
917
|
+
const flowRegistry2 = (registry2?.flows || {})[flowName];
|
|
918
|
+
const stepMeta2 = flowRegistry2?.steps?.[stepName];
|
|
919
|
+
const subscribes = stepMeta2?.subscribes || [];
|
|
920
|
+
for (const sub of subscribes) {
|
|
921
|
+
const emitEvent = events.find(
|
|
922
|
+
(evt) => evt.type === "emit" && evt.data?.name === sub
|
|
923
|
+
);
|
|
924
|
+
if (emitEvent && emitEvent.data?.payload !== void 0) {
|
|
925
|
+
inputData[sub] = emitEvent.data.payload;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
const payload = {
|
|
930
|
+
flowId: runId,
|
|
931
|
+
flowName,
|
|
932
|
+
stepName,
|
|
933
|
+
position,
|
|
934
|
+
timeoutAction: action,
|
|
935
|
+
input: inputData
|
|
936
|
+
};
|
|
937
|
+
const jobId = `${runId}__${stepName}__await-timeout`;
|
|
938
|
+
const registry = $useFunctionRegistry();
|
|
939
|
+
const flowRegistry = (registry?.flows || {})[flowName];
|
|
940
|
+
const stepMeta = flowRegistry?.steps?.[stepName];
|
|
941
|
+
let stepQueue = stepMeta?.queue;
|
|
942
|
+
if (!stepQueue && flowRegistry?.entry?.step === stepName) {
|
|
943
|
+
stepQueue = flowRegistry.entry.queue;
|
|
944
|
+
}
|
|
945
|
+
if (!stepQueue && registry?.workers) {
|
|
946
|
+
const worker = registry.workers.find((w) => {
|
|
947
|
+
const flowNames = w?.flow?.names || (w?.flow?.name ? [w?.flow?.name] : []);
|
|
948
|
+
const stepMatch = w?.flow?.step === stepName || Array.isArray(w?.flow?.step) && w?.flow?.step.includes(stepName);
|
|
949
|
+
return flowNames.includes(flowName) && stepMatch;
|
|
950
|
+
});
|
|
951
|
+
stepQueue = worker?.queue?.name;
|
|
952
|
+
}
|
|
953
|
+
if (!stepQueue) {
|
|
954
|
+
logger.error("Cannot find queue for step", {
|
|
955
|
+
stepName,
|
|
956
|
+
flowName,
|
|
957
|
+
availableSteps: Object.keys(flowRegistry?.steps || {}),
|
|
958
|
+
entryStep: flowRegistry?.entry?.step
|
|
959
|
+
});
|
|
960
|
+
throw new Error(`Cannot handle await timeout: queue not found for step ${stepName} in flow ${flowName}`);
|
|
961
|
+
}
|
|
962
|
+
await queue.enqueue(stepQueue, {
|
|
963
|
+
name: SYSTEM_HANDLERS.AWAIT_TIMEOUT,
|
|
964
|
+
data: payload,
|
|
965
|
+
opts: { jobId }
|
|
966
|
+
});
|
|
687
967
|
if (action === "fail") {
|
|
688
968
|
if (store.index.updateWithRetry) {
|
|
689
969
|
await store.index.updateWithRetry(indexKey, runId, {
|
|
@@ -798,6 +1078,11 @@ export function createFlowWiring() {
|
|
|
798
1078
|
}
|
|
799
1079
|
if (store.index.get) {
|
|
800
1080
|
const currentEntry = await store.index.get(indexKey, runId);
|
|
1081
|
+
const currentStatus = currentEntry?.metadata?.status;
|
|
1082
|
+
if (currentStatus === "canceled") {
|
|
1083
|
+
logger.debug("Flow already canceled, skipping status update", { flowName, runId });
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
801
1086
|
const awaitingStepsObj = currentEntry?.metadata?.awaitingSteps || {};
|
|
802
1087
|
let hasActiveAwaits = false;
|
|
803
1088
|
let hasTimedOutAwaits = false;
|
|
@@ -938,56 +1223,9 @@ export function createFlowWiring() {
|
|
|
938
1223
|
const config = useRuntimeConfig();
|
|
939
1224
|
const flowConfig = config.nvent.flow || {};
|
|
940
1225
|
stallDetector = createStallDetector(store, flowConfig.stallDetection);
|
|
941
|
-
if (flowConfig.stallDetection?.enabled) {
|
|
1226
|
+
if (flowConfig.stallDetection?.enabled !== false) {
|
|
942
1227
|
await stallDetector.start();
|
|
943
|
-
|
|
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
|
-
}
|
|
1228
|
+
logger.info("Stall detector initialized - using per-flow scheduler jobs");
|
|
991
1229
|
}
|
|
992
1230
|
}
|
|
993
1231
|
async function stop() {
|
|
@@ -2,7 +2,12 @@ import { createFlowWiring } from "./flowWiring.js";
|
|
|
2
2
|
import { createStreamWiring } from "./streamWiring.js";
|
|
3
3
|
import { createStateWiring } from "./stateWiring.js";
|
|
4
4
|
import { createTriggerWiring } from "./triggerWiring.js";
|
|
5
|
+
const WIRING_KEY = "__nvent_wiring__";
|
|
5
6
|
export function createWiringRegistry(opts) {
|
|
7
|
+
const existingWiring = globalThis[WIRING_KEY];
|
|
8
|
+
if (existingWiring) {
|
|
9
|
+
return existingWiring;
|
|
10
|
+
}
|
|
6
11
|
const wirings = [
|
|
7
12
|
// 1. Flow orchestration (persistence, completion tracking, step triggering)
|
|
8
13
|
createFlowWiring(),
|
|
@@ -16,7 +21,7 @@ export function createWiringRegistry(opts) {
|
|
|
16
21
|
createTriggerWiring()
|
|
17
22
|
];
|
|
18
23
|
let started = false;
|
|
19
|
-
|
|
24
|
+
const wiring = {
|
|
20
25
|
async start() {
|
|
21
26
|
if (started) return;
|
|
22
27
|
started = true;
|
|
@@ -30,6 +35,9 @@ export function createWiringRegistry(opts) {
|
|
|
30
35
|
}
|
|
31
36
|
}
|
|
32
37
|
started = false;
|
|
38
|
+
globalThis[WIRING_KEY] = null;
|
|
33
39
|
}
|
|
34
40
|
};
|
|
41
|
+
globalThis[WIRING_KEY] = wiring;
|
|
42
|
+
return wiring;
|
|
35
43
|
}
|
|
@@ -312,6 +312,14 @@ export async function handleTriggerFired(event) {
|
|
|
312
312
|
const trigger = useTrigger();
|
|
313
313
|
const { triggerName, data } = event;
|
|
314
314
|
logger.debug("Trigger fired", { trigger: triggerName });
|
|
315
|
+
const triggerEntry = trigger.getTrigger(triggerName);
|
|
316
|
+
if (triggerEntry && triggerEntry.status !== "active") {
|
|
317
|
+
logger.info(`Trigger '${triggerName}' is ${triggerEntry.status}, skipping flow starts`, {
|
|
318
|
+
trigger: triggerName,
|
|
319
|
+
status: triggerEntry.status
|
|
320
|
+
});
|
|
321
|
+
return [];
|
|
322
|
+
}
|
|
315
323
|
const subscriptions = trigger.getAllSubscriptions().filter((sub) => sub.triggerName === triggerName);
|
|
316
324
|
if (subscriptions.length === 0) {
|
|
317
325
|
logger.warn(`No flows subscribed to trigger: ${triggerName}`);
|
|
@@ -380,8 +388,10 @@ export async function startFlowFromTrigger(flowName, triggerName, triggerData) {
|
|
|
380
388
|
(w) => w?.flow?.step === stepName && w?.queue?.name === queueName
|
|
381
389
|
);
|
|
382
390
|
const defaultOpts = entryWorker?.queue?.defaultJobOptions || {};
|
|
391
|
+
const analyzedEntry = flowDef.analyzed?.steps?.[stepName];
|
|
392
|
+
const stepTimeout = analyzedEntry?.stepTimeout;
|
|
383
393
|
const jobId = `${runId}__${stepName}`;
|
|
384
|
-
const opts = { ...defaultOpts, jobId };
|
|
394
|
+
const opts = { ...defaultOpts, jobId, timeout: stepTimeout };
|
|
385
395
|
try {
|
|
386
396
|
await queue.enqueue(queueName, {
|
|
387
397
|
name: stepName,
|