nvent 0.5.5 → 0.5.7

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.5",
3
+ "version": "0.5.7",
4
4
  "configKey": "nvent",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
package/dist/module.mjs CHANGED
@@ -639,9 +639,8 @@ function analyzeFlow(flow, config) {
639
639
  dependsOn: dependencies[stepName] || [],
640
640
  triggers: findTriggeredSteps(stepName, step, steps),
641
641
  level: levels[stepName] ?? 1,
642
- hasAwaitPattern,
643
- stepTimeout: void 0
644
- // Step execution timeout from config
642
+ hasAwaitPattern
643
+ // stepTimeout from ...step spread above (per-function config)
645
644
  };
646
645
  analyzedStep.stepTimeout = getStepExecutionTimeout(analyzedStep, config);
647
646
  analyzedSteps[stepName] = analyzedStep;
@@ -982,6 +981,26 @@ export type {
982
981
  ListOptions,
983
982
  } from ${JSON.stringify(resolverFn("./runtime/adapters/interfaces/store"))}
984
983
 
984
+ // Flow Types
985
+ export type {
986
+ FlowStats,
987
+ StartFlowResult,
988
+ CancelFlowResult,
989
+ RestartFlowResult,
990
+ RunningFlow,
991
+ FlowComposable,
992
+ } from ${JSON.stringify(resolverFn("./runtime/nitro/utils/useFlow"))}
993
+
994
+ // Runner Context Types
995
+ export type {
996
+ QueueJob,
997
+ RunLogger,
998
+ RunState,
999
+ RunContextFlow,
1000
+ RunContext,
1001
+ NodeHandler,
1002
+ } from ${JSON.stringify(resolverFn("./runtime/worker/node/runner"))}
1003
+
985
1004
  // Event Types
986
1005
  export type {
987
1006
  EventType,
@@ -43,6 +43,7 @@ export declare class MemoryStoreAdapter implements StoreAdapter {
43
43
  read: (key: string, opts?: {
44
44
  offset?: number;
45
45
  limit?: number;
46
+ filter?: Record<string, any>;
46
47
  }) => Promise<Array<{
47
48
  id: string;
48
49
  score: number;
@@ -61,7 +62,7 @@ export declare class MemoryStoreAdapter implements StoreAdapter {
61
62
  /**
62
63
  * Convert dot notation keys to nested objects
63
64
  * e.g., { 'stats.totalFires': 5 } -> { stats: { totalFires: 5 } }
64
- * null values are preserved for deletion
65
+ * null values are tracked for deletion after merge
65
66
  */
66
67
  private expandDotNotation;
67
68
  private generateId;
@@ -173,7 +173,19 @@ export class MemoryStoreAdapter {
173
173
  return entry ? { ...entry } : null;
174
174
  },
175
175
  read: async (key, opts) => {
176
- const index = this.sortedIndices.get(key) || [];
176
+ let index = this.sortedIndices.get(key) || [];
177
+ if (opts?.filter) {
178
+ index = index.filter((entry) => {
179
+ for (const [field, value] of Object.entries(opts.filter)) {
180
+ if (Array.isArray(value)) {
181
+ if (!value.includes(entry.metadata?.[field])) return false;
182
+ } else if (entry.metadata?.[field] !== value) {
183
+ return false;
184
+ }
185
+ }
186
+ return true;
187
+ });
188
+ }
177
189
  const offset = opts?.offset || 0;
178
190
  const limit = opts?.limit || 50;
179
191
  return index.slice(offset, offset + limit).map((e) => ({ ...e }));
@@ -291,9 +303,9 @@ export class MemoryStoreAdapter {
291
303
  /**
292
304
  * Convert dot notation keys to nested objects
293
305
  * e.g., { 'stats.totalFires': 5 } -> { stats: { totalFires: 5 } }
294
- * null values are preserved for deletion
306
+ * null values are tracked for deletion after merge
295
307
  */
296
- expandDotNotation(obj) {
308
+ expandDotNotation(obj, parentPath = []) {
297
309
  const result = {};
298
310
  const deleteMarkers = [];
299
311
  for (const [key, value] of Object.entries(obj)) {
@@ -303,7 +315,7 @@ export class MemoryStoreAdapter {
303
315
  if (key.includes(".")) {
304
316
  const keys = key.split(".");
305
317
  if (value === null || value === void 0) {
306
- deleteMarkers.push({ path: keys, delete: true });
318
+ deleteMarkers.push({ path: [...parentPath, ...keys], delete: true });
307
319
  continue;
308
320
  }
309
321
  let current = result;
@@ -315,6 +327,18 @@ export class MemoryStoreAdapter {
315
327
  current = current[k];
316
328
  }
317
329
  current[keys[keys.length - 1]] = value;
330
+ } else if (value === null || value === void 0) {
331
+ deleteMarkers.push({ path: [...parentPath, key], delete: true });
332
+ } else if (typeof value === "object" && value !== null && !Array.isArray(value)) {
333
+ const nested = this.expandDotNotation(value, [...parentPath, key]);
334
+ const nestedDeleteMarkers = nested.__deleteMarkers;
335
+ delete nested.__deleteMarkers;
336
+ if (nestedDeleteMarkers) {
337
+ deleteMarkers.push(...nestedDeleteMarkers);
338
+ }
339
+ if (Object.keys(nested).length > 0) {
340
+ result[key] = nested;
341
+ }
318
342
  } else {
319
343
  result[key] = value;
320
344
  }
@@ -167,14 +167,16 @@ export interface StoreAdapter {
167
167
  /**
168
168
  * Read entries from a sorted index (ordered by score descending)
169
169
  * @param key - Index key
170
- * @param opts - Pagination options
170
+ * @param opts - Pagination and filter options
171
171
  * @param opts.offset - Number of entries to skip
172
172
  * @param opts.limit - Maximum number of entries to return
173
+ * @param opts.filter - Optional filter criteria for metadata fields (adapter-dependent efficiency)
173
174
  * @returns Array of entries with scores and metadata
174
175
  */
175
176
  read(key: string, opts?: {
176
177
  offset?: number;
177
178
  limit?: number;
179
+ filter?: Record<string, any>;
178
180
  }): Promise<Array<{
179
181
  id: string;
180
182
  score: number;
@@ -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
@@ -530,7 +530,12 @@ export function createFlowWiring() {
530
530
  logger.debug("Updated flow stats for failure", { flowName });
531
531
  } else if (e.type === "flow.cancel") {
532
532
  if (store.index.increment) {
533
- 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
+ }
534
539
  await store.index.increment(flowIndexKey, flowName, "stats.cancel", 1);
535
540
  }
536
541
  if (store.index.updateWithRetry) {
@@ -538,7 +543,7 @@ export function createFlowWiring() {
538
543
  lastCompletedAt: (/* @__PURE__ */ new Date()).toISOString()
539
544
  });
540
545
  }
541
- logger.debug("Updated flow stats for cancellation", { flowName });
546
+ logger.debug("Updated flow stats for cancellation", { flowName, previousStatus: e.data?.previousStatus });
542
547
  } else if (e.type === "flow.stalled") {
543
548
  if (store.index.increment && e.data?.previousStatus) {
544
549
  if (e.data.previousStatus === "awaiting") {
@@ -735,6 +740,14 @@ export function createFlowWiring() {
735
740
  const { stepName, awaitType, position, config: config2 } = awaitEvent;
736
741
  try {
737
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
+ }
738
751
  const now = Date.now();
739
752
  let timeoutAt;
740
753
  if (awaitType === "time" && config2.delay) {
@@ -785,6 +798,14 @@ export function createFlowWiring() {
785
798
  const awaitEvent = e;
786
799
  const { stepName, triggerData, position } = awaitEvent;
787
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
+ }
788
809
  if (store.index.updateWithRetry) {
789
810
  const awaitKey = `${stepName}:${position}`;
790
811
  await store.index.updateWithRetry(indexKey, runId, {
@@ -870,6 +891,14 @@ export function createFlowWiring() {
870
891
  const timeoutEvent = e;
871
892
  const { stepName, timeoutAction, position, awaitType } = timeoutEvent;
872
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
+ }
873
902
  logger.warn("Await timeout occurred", {
874
903
  runId,
875
904
  stepName,
@@ -1049,6 +1078,11 @@ export function createFlowWiring() {
1049
1078
  }
1050
1079
  if (store.index.get) {
1051
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
+ }
1052
1086
  const awaitingStepsObj = currentEntry?.metadata?.awaitingSteps || {};
1053
1087
  let hasActiveAwaits = false;
1054
1088
  let hasTimedOutAwaits = false;
@@ -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}`);
@@ -16,6 +16,7 @@ declare const _default: import("h3").EventHandler<import("h3").EventHandlerReque
16
16
  triggerName?: undefined;
17
17
  expectedType?: undefined;
18
18
  actualType?: undefined;
19
+ status?: undefined;
19
20
  expectedMethod?: undefined;
20
21
  actualMethod?: undefined;
21
22
  success?: undefined;
@@ -27,6 +28,7 @@ declare const _default: import("h3").EventHandler<import("h3").EventHandlerReque
27
28
  message: string;
28
29
  expectedType?: undefined;
29
30
  actualType?: undefined;
31
+ status?: undefined;
30
32
  expectedMethod?: undefined;
31
33
  actualMethod?: undefined;
32
34
  success?: undefined;
@@ -38,6 +40,19 @@ declare const _default: import("h3").EventHandler<import("h3").EventHandlerReque
38
40
  expectedType: string;
39
41
  actualType: any;
40
42
  message?: undefined;
43
+ status?: undefined;
44
+ expectedMethod?: undefined;
45
+ actualMethod?: undefined;
46
+ success?: undefined;
47
+ subscribedFlows?: undefined;
48
+ timestamp?: undefined;
49
+ } | {
50
+ error: string;
51
+ triggerName: string;
52
+ status: any;
53
+ message: string;
54
+ expectedType?: undefined;
55
+ actualType?: undefined;
41
56
  expectedMethod?: undefined;
42
57
  actualMethod?: undefined;
43
58
  success?: undefined;
@@ -51,6 +66,7 @@ declare const _default: import("h3").EventHandler<import("h3").EventHandlerReque
51
66
  triggerName?: undefined;
52
67
  expectedType?: undefined;
53
68
  actualType?: undefined;
69
+ status?: undefined;
54
70
  success?: undefined;
55
71
  subscribedFlows?: undefined;
56
72
  timestamp?: undefined;
@@ -63,6 +79,7 @@ declare const _default: import("h3").EventHandler<import("h3").EventHandlerReque
63
79
  error?: undefined;
64
80
  expectedType?: undefined;
65
81
  actualType?: undefined;
82
+ status?: undefined;
66
83
  expectedMethod?: undefined;
67
84
  actualMethod?: undefined;
68
85
  }>>;
@@ -30,6 +30,15 @@ export default defineEventHandler(async (event) => {
30
30
  actualType: triggerEntry.type
31
31
  };
32
32
  }
33
+ if (triggerEntry.status !== "active") {
34
+ logger.info(`Trigger '${triggerName}' is ${triggerEntry.status}, rejecting webhook`);
35
+ return {
36
+ error: "Trigger not active",
37
+ triggerName,
38
+ status: triggerEntry.status,
39
+ message: `Trigger '${triggerName}' is ${triggerEntry.status}. Update status to 'active' to enable.`
40
+ };
41
+ }
33
42
  const expectedMethod = triggerEntry.webhook?.method || "POST";
34
43
  if (event.method !== expectedMethod) {
35
44
  logger.warn(`Method mismatch: expected ${expectedMethod}, got ${event.method}`);
@@ -14,55 +14,53 @@ export interface FlowStats {
14
14
  };
15
15
  version?: number;
16
16
  }
17
+ export interface StartFlowResult {
18
+ id: string;
19
+ queue: string;
20
+ step: string;
21
+ flowId: string;
22
+ }
23
+ export interface CancelFlowResult {
24
+ success: boolean;
25
+ runId: string;
26
+ flowName: string;
27
+ }
28
+ export interface RestartFlowResult {
29
+ success: boolean;
30
+ oldRunId: string;
31
+ newRunId: string;
32
+ flowName: string;
33
+ }
34
+ export interface RunningFlow {
35
+ id: string;
36
+ flowName: string;
37
+ status: string;
38
+ startedAt: string | undefined;
39
+ stepCount: number;
40
+ completedSteps: number;
41
+ }
42
+ export interface FlowComposable {
43
+ startFlow: (flowName: string, payload?: any) => Promise<StartFlowResult>;
44
+ emit: (trigger: string, payload?: any) => Promise<any[]>;
45
+ cancelFlow: (flowName: string, runId: string) => Promise<CancelFlowResult>;
46
+ restartFlow: (flowName: string, runId: string) => Promise<RestartFlowResult>;
47
+ isRunning: (flowName: string, runId?: string, options?: {
48
+ excludeRunIds?: string[];
49
+ }) => Promise<boolean>;
50
+ getRunningFlows: (flowName: string, options?: {
51
+ excludeRunIds?: string[];
52
+ }) => Promise<RunningFlow[]>;
53
+ getFlowStats: (flowName: string) => Promise<FlowStats | null>;
54
+ getAllFlowStats: (options?: {
55
+ sortBy?: 'registeredAt' | 'lastRunAt' | 'name';
56
+ order?: 'asc' | 'desc';
57
+ limit?: number;
58
+ offset?: number;
59
+ }) => Promise<FlowStats[]>;
60
+ hasFlowStats: (flowName: string) => Promise<boolean>;
61
+ }
17
62
  /**
18
63
  * Flow composable for managing flows and accessing flow statistics
19
64
  * Provides methods for starting, canceling, and querying flows
20
65
  */
21
- export declare function useFlow(): {
22
- /**
23
- * Start a flow with the given payload
24
- */
25
- startFlow(flowName: string, payload?: any): Promise<{
26
- id: any;
27
- queue: any;
28
- step: any;
29
- flowId: `${string}-${string}-${string}-${string}-${string}`;
30
- }>;
31
- /**
32
- * Emit a trigger event for flow coordination
33
- */
34
- emit(trigger: string, payload?: any): Promise<never[]>;
35
- /**
36
- * Cancel a running flow
37
- */
38
- cancelFlow(flowName: string, runId: string): Promise<{
39
- success: boolean;
40
- runId: string;
41
- flowName: string;
42
- }>;
43
- /**
44
- * Check if a flow is currently running
45
- */
46
- isRunning(flowName: string, runId?: string): Promise<any>;
47
- /**
48
- * Get all currently running flows
49
- */
50
- getRunningFlows(flowName: string): Promise<any>;
51
- /**
52
- * Get flow statistics by name
53
- */
54
- getFlowStats(flowName: string): Promise<FlowStats | null>;
55
- /**
56
- * Get all flows with their statistics
57
- */
58
- getAllFlowStats(options?: {
59
- sortBy?: "registeredAt" | "lastRunAt" | "name";
60
- order?: "asc" | "desc";
61
- limit?: number;
62
- offset?: number;
63
- }): Promise<FlowStats[]>;
64
- /**
65
- * Check if a flow has statistics in the index
66
- */
67
- hasFlowStats(flowName: string): Promise<boolean>;
68
- };
66
+ export declare function useFlow(): FlowComposable;
@@ -70,15 +70,24 @@ export function useFlow() {
70
70
  */
71
71
  async cancelFlow(flowName, runId) {
72
72
  try {
73
+ let previousStatus;
74
+ try {
75
+ const runIndexKey = StoreSubjects.flowRunIndex(flowName);
76
+ const entry = await store.index.get(runIndexKey, runId);
77
+ previousStatus = entry?.metadata?.status;
78
+ } catch {
79
+ }
73
80
  await eventsManager.publishBus({
74
81
  type: "flow.cancel",
75
82
  runId,
76
83
  flowName,
77
84
  data: {
78
- canceledAt: (/* @__PURE__ */ new Date()).toISOString()
85
+ canceledAt: (/* @__PURE__ */ new Date()).toISOString(),
86
+ previousStatus
87
+ // Include for correct stats counter decrement
79
88
  }
80
89
  });
81
- logger.info("Flow canceled", { flowName, runId });
90
+ logger.info("Flow canceled", { flowName, runId, previousStatus });
82
91
  return { success: true, runId, flowName };
83
92
  } catch (err) {
84
93
  logger.error("Failed to cancel flow", { flowName, runId, error: err });
@@ -86,36 +95,100 @@ export function useFlow() {
86
95
  }
87
96
  },
88
97
  /**
89
- * Check if a flow is currently running
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
+ },
142
+ /**
143
+ * Check if a flow is currently running (includes 'running' and 'awaiting' status)
144
+ * @param flowName - The name of the flow to check
145
+ * @param runId - Optional specific run ID to check (if provided, only checks that run)
146
+ * @param options - Optional configuration
147
+ * @param options.excludeRunIds - Exclude these run IDs from the check (useful when called from within a flow)
90
148
  */
91
- async isRunning(flowName, runId) {
149
+ async isRunning(flowName, runId, options) {
92
150
  try {
93
151
  if (!store.index.read) {
94
152
  return false;
95
153
  }
96
154
  const runIndexKey = StoreSubjects.flowRunIndex(flowName);
97
- const entries = await store.index.read(runIndexKey, { limit: 1e3 });
155
+ const activeStatuses = ["running", "awaiting"];
98
156
  if (runId) {
99
- const run = entries.find((e) => e.id === runId);
100
- return run?.metadata?.status === "running";
157
+ const run = await store.index.get(runIndexKey, runId);
158
+ return activeStatuses.includes(run?.metadata?.status);
101
159
  }
102
- return entries.some((e) => e.metadata?.status === "running");
160
+ const entries = await store.index.read(runIndexKey, {
161
+ limit: 1e3,
162
+ filter: { status: activeStatuses }
163
+ });
164
+ const excludeSet = new Set(options?.excludeRunIds || []);
165
+ const matchingRuns = entries.filter((e) => !excludeSet.has(e.id));
166
+ return matchingRuns.length > 0;
103
167
  } catch (err) {
104
168
  logger.error("Error checking flow status:", err);
105
169
  return false;
106
170
  }
107
171
  },
108
172
  /**
109
- * Get all currently running flows
173
+ * Get all currently running flows (includes 'running' and 'awaiting' status)
174
+ * @param flowName - The name of the flow to check
175
+ * @param options - Optional configuration
176
+ * @param options.excludeRunIds - Exclude these run IDs from the results (useful when called from within a flow)
110
177
  */
111
- async getRunningFlows(flowName) {
178
+ async getRunningFlows(flowName, options) {
112
179
  try {
113
180
  if (!store.index.read) {
114
181
  return [];
115
182
  }
116
183
  const runIndexKey = StoreSubjects.flowRunIndex(flowName);
117
- const entries = await store.index.read(runIndexKey, { limit: 1e3 });
118
- return entries.filter((e) => e.metadata?.status === "running").map((e) => ({
184
+ const activeStatuses = ["running", "awaiting"];
185
+ const entries = await store.index.read(runIndexKey, {
186
+ limit: 1e3,
187
+ filter: { status: activeStatuses }
188
+ });
189
+ const excludeSet = new Set(options?.excludeRunIds || []);
190
+ const filteredEntries = entries.filter((e) => !excludeSet.has(e.id));
191
+ return filteredEntries.map((e) => ({
119
192
  id: e.id,
120
193
  flowName,
121
194
  status: e.metadata?.status,
@@ -110,6 +110,12 @@ export function useTrigger() {
110
110
  `Emitting unregistered trigger '${name}'. Consider registering it first with registerTrigger().`
111
111
  );
112
112
  }
113
+ if (trigger && trigger.status !== "active") {
114
+ logger.info(
115
+ `Trigger '${name}' is ${trigger.status}, not emitting. Update status to 'active' to enable firing.`
116
+ );
117
+ return;
118
+ }
113
119
  const threshold = opts?.payloadThreshold || trigger?.config?.payloadThreshold || 10 * 1024;
114
120
  const eventData = await runtime.handleLargePayload(name, data, threshold);
115
121
  logger.debug(`Emitting trigger: ${name}`, {
@@ -208,6 +214,7 @@ export function useTrigger() {
208
214
  }
209
215
  if (metadata.subscriptions) {
210
216
  for (const [flowName, subData] of Object.entries(metadata.subscriptions)) {
217
+ if (!subData) continue;
211
218
  const subscription = {
212
219
  triggerName: entry.id,
213
220
  flowName,
@@ -223,22 +230,6 @@ export function useTrigger() {
223
230
  logger.info(
224
231
  `Loaded ${activeCount} active triggers with ${totalSubscriptions} subscriptions from index`
225
232
  );
226
- } else {
227
- logger.warn("Store does not support indexRead, falling back to doc-based loading");
228
- if (store.list) {
229
- const triggers = await store.list("triggers");
230
- for (const { id, doc } of triggers) {
231
- runtime.addTrigger(id, doc);
232
- }
233
- const subscriptions = await store.list("trigger-subscriptions");
234
- for (const { doc } of subscriptions) {
235
- const sub = doc;
236
- runtime.addSubscription(sub.triggerName, sub.flowName, sub);
237
- }
238
- logger.info(
239
- `Loaded ${triggers.length} triggers and ${subscriptions.length} subscriptions from doc store (legacy)`
240
- );
241
- }
242
233
  }
243
234
  runtime.setInitialized(true);
244
235
  },
@@ -1,6 +1,7 @@
1
1
  import { Scheduler } from "./scheduler.js";
2
2
  import { useRuntimeConfig } from "#imports";
3
- let schedulerInstance = null;
3
+ const SCHEDULER_KEY = "__nvent_scheduler__";
4
+ let schedulerInstance = globalThis[SCHEDULER_KEY] || null;
4
5
  export function createScheduler(store) {
5
6
  const config = useRuntimeConfig();
6
7
  const prefix = config.nvent.store?.prefix || "nvent";
@@ -24,6 +25,7 @@ export async function initializeScheduler(store) {
24
25
  return schedulerInstance;
25
26
  }
26
27
  schedulerInstance = createScheduler(store);
28
+ globalThis[SCHEDULER_KEY] = schedulerInstance;
27
29
  await schedulerInstance.start();
28
30
  return schedulerInstance;
29
31
  }
@@ -31,8 +33,10 @@ export async function shutdownScheduler() {
31
33
  if (schedulerInstance) {
32
34
  await schedulerInstance.stop();
33
35
  schedulerInstance = null;
36
+ globalThis[SCHEDULER_KEY] = null;
34
37
  }
35
38
  }
36
39
  export function resetScheduler() {
37
40
  schedulerInstance = null;
41
+ globalThis[SCHEDULER_KEY] = null;
38
42
  }
@@ -1,4 +1,4 @@
1
- import { useFlow } from '#imports';
1
+ import type { FlowStats, StartFlowResult, CancelFlowResult, RunningFlow } from '../../nitro/utils/useFlow.js';
2
2
  /**
3
3
  * Generic job interface that works with any queue adapter
4
4
  * Adapters should provide jobs in this format
@@ -22,6 +22,48 @@ export interface RunState {
22
22
  }): Promise<void>;
23
23
  delete(key: string): Promise<void>;
24
24
  }
25
+ /**
26
+ * Flow context available within step handlers
27
+ * Provides context-aware versions of flow operations with auto-injected flowId/flowName
28
+ */
29
+ export interface RunContextFlow {
30
+ /** Start a new flow with the given payload */
31
+ startFlow: (flowName: string, payload?: any) => Promise<StartFlowResult>;
32
+ /** Emit a trigger event (auto-injects flowId, flowName, stepName from context) */
33
+ emit: (trigger: string, payload?: any) => Promise<any[]>;
34
+ /** Cancel a specific flow by name and runId */
35
+ cancelFlow: (flowName: string, runId: string) => Promise<CancelFlowResult>;
36
+ /** Cancel the current flow (uses flowId from context) */
37
+ cancel: () => Promise<CancelFlowResult>;
38
+ /**
39
+ * Check if a flow is currently running
40
+ * @param flowName - Optional flow name (defaults to current flow)
41
+ * @param runId - Optional specific run ID to check
42
+ * @param options - Optional configuration (auto-excludes current flow if not specified)
43
+ */
44
+ isRunning: (flowName?: string, runId?: string, options?: {
45
+ excludeRunIds?: string[];
46
+ }) => Promise<boolean>;
47
+ /**
48
+ * Get all currently running flows
49
+ * @param flowName - Optional flow name (defaults to current flow)
50
+ * @param options - Optional configuration (auto-excludes current flow if not specified)
51
+ */
52
+ getRunningFlows: (flowName?: string, options?: {
53
+ excludeRunIds?: string[];
54
+ }) => Promise<RunningFlow[]>;
55
+ /** Get flow statistics by name */
56
+ getFlowStats: (flowName: string) => Promise<FlowStats | null>;
57
+ /** Get all flows with their statistics */
58
+ getAllFlowStats: (options?: {
59
+ sortBy?: 'registeredAt' | 'lastRunAt' | 'name';
60
+ order?: 'asc' | 'desc';
61
+ limit?: number;
62
+ offset?: number;
63
+ }) => Promise<FlowStats[]>;
64
+ /** Check if a flow has statistics in the index */
65
+ hasFlowStats: (flowName: string) => Promise<boolean>;
66
+ }
25
67
  export interface RunContext {
26
68
  jobId?: string;
27
69
  queue?: string;
@@ -32,7 +74,7 @@ export interface RunContext {
32
74
  attempt?: number;
33
75
  logger: RunLogger;
34
76
  state: RunState;
35
- flow: ReturnType<typeof useFlow>;
77
+ flow: RunContextFlow;
36
78
  /**
37
79
  * Resolved data from await pattern (awaitBefore only)
38
80
  * Available when step resumes after await resolution
@@ -52,14 +52,15 @@ export function buildContext(partial) {
52
52
  log: (level, msg, meta) => {
53
53
  const runId = partial?.flowId || "unknown";
54
54
  const flowName = meta?.flowName || "unknown";
55
+ const metaObj = meta !== null && meta !== void 0 ? typeof meta === "object" && !Array.isArray(meta) ? meta : { value: meta } : {};
55
56
  void eventManager.publishBus({
56
57
  type: "log",
57
58
  runId,
58
59
  flowName,
59
- stepName: meta?.stepName,
60
- stepId: meta?.stepId || meta?.stepRunId,
61
- attempt: meta?.attempt,
62
- data: { level, message: msg, ...meta }
60
+ stepName: metaObj?.stepName,
61
+ stepId: metaObj?.stepId || metaObj?.stepRunId,
62
+ attempt: metaObj?.attempt,
63
+ data: { level, message: msg, ...metaObj }
63
64
  });
64
65
  }
65
66
  };
@@ -82,19 +83,21 @@ export function buildContext(partial) {
82
83
  }
83
84
  return baseFlowEngine.cancelFlow(partial.flowName, partial.flowId);
84
85
  },
85
- isRunning: async (flowName, runId) => {
86
+ isRunning: async (flowName, runId, options) => {
86
87
  const targetFlowName = flowName || partial?.flowName;
87
88
  if (!targetFlowName) {
88
89
  throw new Error("flowName is required to check if flow is running");
89
90
  }
90
- return baseFlowEngine.isRunning(targetFlowName, runId);
91
+ const effectiveOptions = options || (partial?.flowId ? { excludeRunIds: [partial.flowId] } : void 0);
92
+ return baseFlowEngine.isRunning(targetFlowName, runId, effectiveOptions);
91
93
  },
92
- getRunningFlows: async (flowName) => {
94
+ getRunningFlows: async (flowName, options) => {
93
95
  const targetFlowName = flowName || partial?.flowName;
94
96
  if (!targetFlowName) {
95
97
  throw new Error("flowName is required to get running flows");
96
98
  }
97
- return baseFlowEngine.getRunningFlows(targetFlowName);
99
+ const effectiveOptions = options || (partial?.flowId ? { excludeRunIds: [partial.flowId] } : void 0);
100
+ return baseFlowEngine.getRunningFlows(targetFlowName, effectiveOptions);
98
101
  }
99
102
  };
100
103
  return {
@@ -148,7 +151,8 @@ export function createJobProcessor(handler, queueName) {
148
151
  });
149
152
  const attemptLogger = {
150
153
  log: (level, msg, meta) => {
151
- const enriched = { ...meta || {}, stepName: job.name, attempt, stepRunId, flowName };
154
+ const metaObj = meta !== null && meta !== void 0 ? typeof meta === "object" && !Array.isArray(meta) ? meta : { value: meta } : {};
155
+ const enriched = { ...metaObj, stepName: job.name, attempt, stepRunId, flowName };
152
156
  ctx.logger.log(level, msg, enriched);
153
157
  }
154
158
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nvent",
3
- "version": "0.5.5",
3
+ "version": "0.5.7",
4
4
  "description": "Event-driven workflows for Nuxt",
5
5
  "repository": "DevJoghurt/nvent",
6
6
  "license": "MIT",