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.
Files changed (46) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/module.mjs +127 -23
  3. package/dist/runtime/adapters/builtin/memory-store.d.ts +2 -1
  4. package/dist/runtime/adapters/builtin/memory-store.js +28 -4
  5. package/dist/runtime/adapters/factory.js +8 -7
  6. package/dist/runtime/adapters/interfaces/queue.d.ts +5 -0
  7. package/dist/runtime/adapters/interfaces/store.d.ts +3 -1
  8. package/dist/runtime/config/index.js +14 -1
  9. package/dist/runtime/config/types.d.ts +42 -0
  10. package/dist/runtime/events/types.d.ts +0 -1
  11. package/dist/runtime/events/utils/stallDetector.d.ts +13 -77
  12. package/dist/runtime/events/utils/stallDetector.js +8 -192
  13. package/dist/runtime/events/wiring/flowWiring.js +347 -109
  14. package/dist/runtime/events/wiring/registry.js +9 -1
  15. package/dist/runtime/events/wiring/triggerWiring.js +11 -1
  16. package/dist/runtime/nitro/plugins/02.workers.js +31 -2
  17. package/dist/runtime/nitro/routes/webhook.await.js +28 -6
  18. package/dist/runtime/nitro/routes/webhook.trigger.d.ts +17 -0
  19. package/dist/runtime/nitro/routes/webhook.trigger.js +9 -0
  20. package/dist/runtime/nitro/utils/awaitPatterns/event.js +58 -50
  21. package/dist/runtime/nitro/utils/awaitPatterns/schedule.js +6 -1
  22. package/dist/runtime/nitro/utils/awaitPatterns/time.d.ts +1 -1
  23. package/dist/runtime/nitro/utils/awaitPatterns/time.js +6 -2
  24. package/dist/runtime/nitro/utils/awaitPatterns/webhook.js +53 -45
  25. package/dist/runtime/nitro/utils/defineFunction.d.ts +2 -9
  26. package/dist/runtime/nitro/utils/defineFunction.js +1 -14
  27. package/dist/runtime/nitro/utils/defineFunctionConfig.d.ts +84 -16
  28. package/dist/runtime/nitro/utils/defineHooks.d.ts +64 -10
  29. package/dist/runtime/nitro/utils/defineHooks.js +3 -0
  30. package/dist/runtime/nitro/utils/useAwait.d.ts +12 -0
  31. package/dist/runtime/nitro/utils/useAwait.js +34 -4
  32. package/dist/runtime/nitro/utils/useFlow.d.ts +39 -48
  33. package/dist/runtime/nitro/utils/useFlow.js +53 -14
  34. package/dist/runtime/nitro/utils/useHookRegistry.d.ts +10 -4
  35. package/dist/runtime/nitro/utils/useTrigger.js +7 -16
  36. package/dist/runtime/scheduler/index.js +5 -1
  37. package/dist/runtime/scheduler/scheduler.d.ts +19 -0
  38. package/dist/runtime/scheduler/scheduler.js +184 -8
  39. package/dist/runtime/scheduler/types.d.ts +6 -0
  40. package/dist/runtime/worker/node/runner.d.ts +44 -2
  41. package/dist/runtime/worker/node/runner.js +45 -100
  42. package/dist/runtime/worker/system/awaitHandlers.d.ts +27 -0
  43. package/dist/runtime/worker/system/awaitHandlers.js +230 -0
  44. package/dist/runtime/worker/system/index.d.ts +24 -0
  45. package/dist/runtime/worker/system/index.js +39 -0
  46. 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 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
- }
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 awaitState2 = awaitingSteps[emitStepName];
115
- if (awaitState2?.position === "after" && awaitState2?.status === "awaiting") {
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
- if (awaitState?.status === "resolved" && awaitState?.position === "before") {
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 opts = { ...defaultOpts, jobId };
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
- await store.index.increment(flowIndexKey, flowName, "stats.running", -1);
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
- logger.warn("Failed to update flow stats", {
532
- type: e.type,
533
- flowName: e.flowName,
534
- error: err?.message
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
- if (stallDetector) {
584
- await stallDetector.updateActivity(flowName, runId);
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
- [stepName]: {
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
- [stepName]: {
813
+ [awaitKey]: {
660
814
  status: "resolved",
661
- triggerData
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
- 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
- }
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
- return {
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,