nvent 0.5.6 → 0.5.8

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nvent",
3
- "version": "0.5.6",
3
+ "version": "0.5.8",
4
4
  "configKey": "nvent",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
package/dist/module.mjs CHANGED
@@ -986,6 +986,7 @@ export type {
986
986
  FlowStats,
987
987
  StartFlowResult,
988
988
  CancelFlowResult,
989
+ RestartFlowResult,
989
990
  RunningFlow,
990
991
  FlowComposable,
991
992
  } from ${JSON.stringify(resolverFn("./runtime/nitro/utils/useFlow"))}
@@ -6,6 +6,7 @@ export async function scheduleTrigger(triggerName, scheduleConfig, triggerStatus
6
6
  const eventBus = getEventBus();
7
7
  try {
8
8
  const jobId = `trigger:${triggerName}`;
9
+ const isEnabled = triggerStatus === "active";
9
10
  const handler = async () => {
10
11
  logger.debug("Schedule trigger fired", { trigger: triggerName });
11
12
  await eventBus.publish({
@@ -28,7 +29,7 @@ export async function scheduleTrigger(triggerName, scheduleConfig, triggerStatus
28
29
  type: "schedule-trigger",
29
30
  scheduleConfig
30
31
  },
31
- enabled: triggerStatus === "active"
32
+ enabled: isEnabled
32
33
  };
33
34
  if (scheduleConfig.cron) {
34
35
  jobConfig.cron = scheduleConfig.cron;
@@ -39,6 +40,7 @@ export async function scheduleTrigger(triggerName, scheduleConfig, triggerStatus
39
40
  await scheduler.schedule(jobConfig);
40
41
  logger.info("Scheduled trigger", {
41
42
  trigger: triggerName,
43
+ enabled: isEnabled,
42
44
  cron: scheduleConfig.cron,
43
45
  interval: scheduleConfig.interval,
44
46
  timezone: scheduleConfig.timezone
@@ -39,8 +39,15 @@ export declare class FlowStallDetector {
39
39
  /**
40
40
  * Mark a flow as stalled
41
41
  * Emits a flow.stalled event and updates the flow status
42
+ * Also emits step.stalled events for any steps that were running when the flow stalled
42
43
  */
43
44
  markAsStalled(flowName: string, runId: string, reason?: string): Promise<void>;
45
+ /**
46
+ * Find steps that were running when the flow stalled
47
+ * A step is considered "running" if it has step.started but no terminal event
48
+ * (step.completed, step.failed, step.stalled)
49
+ */
50
+ private findStalledSteps;
44
51
  /**
45
52
  * Run startup recovery to clean up flows left in running state from previous server instance
46
53
  * This marks all running flows as stalled since their in-memory state is lost
@@ -34,6 +34,7 @@ export class FlowStallDetector {
34
34
  /**
35
35
  * Mark a flow as stalled
36
36
  * Emits a flow.stalled event and updates the flow status
37
+ * Also emits step.stalled events for any steps that were running when the flow stalled
37
38
  */
38
39
  async markAsStalled(flowName, runId, reason = "No activity timeout") {
39
40
  const { StoreSubjects } = useStreamTopics();
@@ -57,14 +58,46 @@ export class FlowStallDetector {
57
58
  });
58
59
  }
59
60
  const streamName = StoreSubjects.flowRun(runId);
61
+ const stalledAt = Date.now();
62
+ try {
63
+ const allEvents = await this.store.stream.read(streamName);
64
+ const stalledSteps = this.findStalledSteps(allEvents);
65
+ for (const stepName of stalledSteps) {
66
+ await this.store.stream.append(streamName, {
67
+ type: "step.stalled",
68
+ runId,
69
+ flowName,
70
+ stepName,
71
+ data: {
72
+ reason: "Flow stalled - step execution interrupted",
73
+ stalledAt
74
+ }
75
+ });
76
+ this.logger.debug(`Emitted step.stalled for step '${stepName}'`, { flowName, runId });
77
+ }
78
+ if (stalledSteps.length > 0) {
79
+ this.logger.info(`Marked ${stalledSteps.length} step(s) as stalled`, {
80
+ flowName,
81
+ runId,
82
+ steps: stalledSteps
83
+ });
84
+ }
85
+ } catch (err) {
86
+ this.logger.warn("Failed to emit step.stalled events", {
87
+ flowName,
88
+ runId,
89
+ error: err.message
90
+ });
91
+ }
60
92
  await this.store.stream.append(streamName, {
61
93
  type: "flow.stalled",
62
94
  runId,
63
95
  flowName,
64
96
  data: {
65
97
  reason,
66
- previousStatus
98
+ previousStatus,
67
99
  // Include previous status so stats handler knows which counter to decrement
100
+ stalledAt
68
101
  }
69
102
  });
70
103
  this.logger.info(`Marked flow as stalled - '${flowName}' runId '${runId}': ${reason}`);
@@ -72,6 +105,39 @@ export class FlowStallDetector {
72
105
  this.logger.error(`Failed to mark flow as stalled for '${flowName}' runId '${runId}': ${error.message}`);
73
106
  }
74
107
  }
108
+ /**
109
+ * Find steps that were running when the flow stalled
110
+ * A step is considered "running" if it has step.started but no terminal event
111
+ * (step.completed, step.failed, step.stalled)
112
+ */
113
+ findStalledSteps(events) {
114
+ const stepStates = /* @__PURE__ */ new Map();
115
+ for (const e of events) {
116
+ const stepName = e.stepName;
117
+ if (!stepName) continue;
118
+ if (e.type === "step.started") {
119
+ if (!stepStates.has(stepName) || stepStates.get(stepName) === "started") {
120
+ stepStates.set(stepName, "started");
121
+ }
122
+ } else if (e.type === "step.completed") {
123
+ stepStates.set(stepName, "completed");
124
+ } else if (e.type === "step.failed") {
125
+ const willRetry = e.data?.willRetry || e.data?.retry;
126
+ if (!willRetry) {
127
+ stepStates.set(stepName, "failed");
128
+ }
129
+ } else if (e.type === "step.stalled") {
130
+ stepStates.set(stepName, "stalled");
131
+ }
132
+ }
133
+ const stalledSteps = [];
134
+ for (const [stepName, state] of stepStates) {
135
+ if (state === "started") {
136
+ stalledSteps.push(stepName);
137
+ }
138
+ }
139
+ return stalledSteps;
140
+ }
75
141
  /**
76
142
  * Run startup recovery to clean up flows left in running state from previous server instance
77
143
  * This marks all running flows as stalled since their in-memory state is lost
@@ -25,6 +25,12 @@ export interface CancelFlowResult {
25
25
  runId: string;
26
26
  flowName: string;
27
27
  }
28
+ export interface RestartFlowResult {
29
+ success: boolean;
30
+ oldRunId: string;
31
+ newRunId: string;
32
+ flowName: string;
33
+ }
28
34
  export interface RunningFlow {
29
35
  id: string;
30
36
  flowName: string;
@@ -37,6 +43,7 @@ export interface FlowComposable {
37
43
  startFlow: (flowName: string, payload?: any) => Promise<StartFlowResult>;
38
44
  emit: (trigger: string, payload?: any) => Promise<any[]>;
39
45
  cancelFlow: (flowName: string, runId: string) => Promise<CancelFlowResult>;
46
+ restartFlow: (flowName: string, runId: string) => Promise<RestartFlowResult>;
40
47
  isRunning: (flowName: string, runId?: string, options?: {
41
48
  excludeRunIds?: string[];
42
49
  }) => Promise<boolean>;
@@ -94,6 +94,51 @@ export function useFlow() {
94
94
  throw err;
95
95
  }
96
96
  },
97
+ /**
98
+ * Restart a flow by canceling the current run and starting a new one with the same input
99
+ * @param flowName - The name of the flow
100
+ * @param runId - The run ID to restart
101
+ */
102
+ async restartFlow(flowName, runId) {
103
+ try {
104
+ const streamName = StoreSubjects.flowRun(runId);
105
+ const events = await store.stream.read(streamName);
106
+ const startEvent = events.find((e) => e.type === "flow.start" || e.type === "flow.started");
107
+ const originalInput = startEvent?.data?.input || {};
108
+ logger.debug("Found original input for restart", { flowName, runId, hasInput: !!startEvent });
109
+ const runIndexKey = StoreSubjects.flowRunIndex(flowName);
110
+ const entry = await store.index.get(runIndexKey, runId);
111
+ const currentStatus = entry?.metadata?.status;
112
+ if (currentStatus === "running" || currentStatus === "awaiting") {
113
+ await eventsManager.publishBus({
114
+ type: "flow.cancel",
115
+ runId,
116
+ flowName,
117
+ data: {
118
+ canceledAt: (/* @__PURE__ */ new Date()).toISOString(),
119
+ previousStatus: currentStatus,
120
+ reason: "Restarted"
121
+ }
122
+ });
123
+ logger.info("Canceled flow for restart", { flowName, runId, previousStatus: currentStatus });
124
+ }
125
+ const newResult = await this.startFlow(flowName, originalInput);
126
+ logger.info("Flow restarted", {
127
+ flowName,
128
+ oldRunId: runId,
129
+ newRunId: newResult.flowId
130
+ });
131
+ return {
132
+ success: true,
133
+ oldRunId: runId,
134
+ newRunId: newResult.flowId,
135
+ flowName
136
+ };
137
+ } catch (err) {
138
+ logger.error("Failed to restart flow", { flowName, runId, error: err });
139
+ throw err;
140
+ }
141
+ },
97
142
  /**
98
143
  * Check if a flow is currently running (includes 'running' and 'awaiting' status)
99
144
  * @param flowName - The name of the flow to check
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nvent",
3
- "version": "0.5.6",
3
+ "version": "0.5.8",
4
4
  "description": "Event-driven workflows for Nuxt",
5
5
  "repository": "DevJoghurt/nvent",
6
6
  "license": "MIT",