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
package/dist/module.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nvent",
3
- "version": "0.4.1",
3
+ "version": "0.5.6",
4
4
  "configKey": "nvent",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
package/dist/module.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { dirname, join, extname, relative } from 'node:path';
2
2
  import { useLogger, defineNuxtModule, createResolver, addTemplate, addTypeTemplate, addServerPlugin, addServerHandler, addServerImports, updateTemplates } from '@nuxt/kit';
3
3
  import defu from 'defu';
4
- import { existsSync, realpathSync } from 'node:fs';
4
+ import { existsSync, realpathSync, readFileSync } from 'node:fs';
5
5
  import { globby } from 'globby';
6
6
  import { pathToFileURL, fileURLToPath } from 'node:url';
7
7
  import { readFile } from 'node:fs/promises';
@@ -32,6 +32,7 @@ async function loadJsConfig(absPath) {
32
32
  emits: flowCfg.emits,
33
33
  subscribes,
34
34
  triggers: flowCfg.triggers,
35
+ stepTimeout: flowCfg.stepTimeout,
35
36
  awaitBefore: flowCfg.awaitBefore,
36
37
  awaitAfter: flowCfg.awaitAfter
37
38
  };
@@ -45,7 +46,7 @@ async function loadJsConfig(absPath) {
45
46
  } : void 0;
46
47
  const workerCfg = cfg?.worker && typeof cfg.worker === "object" ? { ...cfg.worker } : void 0;
47
48
  const hasDefaultExport = !!(mod && mod.default);
48
- const hasHooks = !!(mod && typeof mod.onAwaitRegister === "function" || mod && typeof mod.onAwaitResolve === "function");
49
+ const hasHooks = !!(mod && typeof mod.onAwaitRegister === "function" || mod && typeof mod.onAwaitResolve === "function" || mod && typeof mod.onAwaitTimeout === "function");
49
50
  return { queueName, flow, runtype, queue: queueCfg, worker: workerCfg, hasDefaultExport, hasHooks };
50
51
  }
51
52
 
@@ -75,6 +76,7 @@ async function loadTsConfig(absPath) {
75
76
  emits: flowCfg.emits,
76
77
  subscribes,
77
78
  triggers: flowCfg.triggers,
79
+ stepTimeout: flowCfg.stepTimeout,
78
80
  awaitBefore: flowCfg.awaitBefore,
79
81
  awaitAfter: flowCfg.awaitAfter
80
82
  };
@@ -87,7 +89,7 @@ async function loadTsConfig(absPath) {
87
89
  limiter: cfg.queue.limiter
88
90
  } : void 0;
89
91
  const workerCfg = cfg?.worker && typeof cfg.worker === "object" ? { ...cfg.worker } : void 0;
90
- const hasHooks = !!(mod.exports.onAwaitRegister || mod.exports.onAwaitResolve);
92
+ const hasHooks = !!(mod.exports.onAwaitRegister || mod.exports.onAwaitResolve || mod.exports.onAwaitTimeout);
91
93
  return { queueName, flow, runtype, queue: queueCfg, worker: workerCfg, hasDefaultExport, hasHooks };
92
94
  } catch (error) {
93
95
  throw new Error(`Failed to parse config from ${absPath}: ${error}`);
@@ -185,6 +187,7 @@ async function loadPyConfig(absPath, logger) {
185
187
  emits: flowCfg.emits,
186
188
  subscribes,
187
189
  triggers: flowCfg.triggers,
190
+ stepTimeout: flowCfg.stepTimeout,
188
191
  awaitBefore: flowCfg.awaitBefore,
189
192
  awaitAfter: flowCfg.awaitAfter
190
193
  };
@@ -334,6 +337,7 @@ function buildFlows(flowSources) {
334
337
  queue,
335
338
  workerId: id,
336
339
  emits: f.emits,
340
+ stepTimeout: f.stepTimeout,
337
341
  awaitBefore: f.awaitBefore,
338
342
  awaitAfter: f.awaitAfter
339
343
  };
@@ -347,6 +351,7 @@ function buildFlows(flowSources) {
347
351
  workerId: id,
348
352
  subscribes: f.subscribes,
349
353
  emits: f.emits,
354
+ stepTimeout: f.stepTimeout,
350
355
  awaitBefore: f.awaitBefore,
351
356
  awaitAfter: f.awaitAfter
352
357
  };
@@ -361,6 +366,7 @@ function buildFlows(flowSources) {
361
366
  workerId: id,
362
367
  subscribes: f.subscribes,
363
368
  emits: f.emits,
369
+ stepTimeout: f.stepTimeout,
364
370
  awaitBefore: f.awaitBefore,
365
371
  awaitAfter: f.awaitAfter
366
372
  };
@@ -407,10 +413,10 @@ function parseSubscription(token) {
407
413
  }
408
414
  return { type: "implicit", value: token };
409
415
  }
410
- function findEmitter(token, entryStep, steps) {
416
+ function findEmitter(token, entryStep, steps, entry) {
411
417
  const { type, value } = parseSubscription(token);
412
- if (entryStep) {
413
- const entryEmits = steps[entryStep]?.emits || [];
418
+ if (entryStep && entry) {
419
+ const entryEmits = entry.emits || [];
414
420
  if (entryEmits.includes(token) || entryEmits.includes(value)) {
415
421
  return entryStep;
416
422
  }
@@ -439,13 +445,13 @@ function findEmitter(token, entryStep, steps) {
439
445
  }
440
446
  return null;
441
447
  }
442
- function buildDependencyGraph(entryStep, steps) {
448
+ function buildDependencyGraph(entryStep, steps, entry) {
443
449
  const dependencies = {};
444
450
  for (const [stepName, step] of Object.entries(steps)) {
445
451
  const deps = /* @__PURE__ */ new Set();
446
452
  const subscribes = step.subscribes || [];
447
453
  for (const token of subscribes) {
448
- const emitter = findEmitter(token, entryStep, steps);
454
+ const emitter = findEmitter(token, entryStep, steps, entry);
449
455
  if (emitter && emitter !== stepName) {
450
456
  deps.add(emitter);
451
457
  }
@@ -520,27 +526,67 @@ function findTriggeredSteps(stepName, step, allSteps) {
520
526
  }
521
527
  return Array.from(triggered);
522
528
  }
529
+ function getAwaitDefaultTimeout(awaitConfig) {
530
+ if (!awaitConfig) return 0;
531
+ if (awaitConfig.timeout && awaitConfig.timeout > 0) {
532
+ return awaitConfig.timeout;
533
+ }
534
+ const type = awaitConfig.type;
535
+ switch (type) {
536
+ case "webhook":
537
+ return 24 * 60 * 60 * 1e3;
538
+ // 24 hours (matches flow.awaitDefaults.webhookTimeout)
539
+ case "event":
540
+ return 24 * 60 * 60 * 1e3;
541
+ // 24 hours (matches flow.awaitDefaults.eventTimeout)
542
+ case "time":
543
+ return 0;
544
+ // No timeout for time awaits by default
545
+ case "schedule":
546
+ return 0;
547
+ // No timeout for schedule awaits by default
548
+ default:
549
+ return 0;
550
+ }
551
+ }
523
552
  function getStepAwaitTimeout(step) {
524
553
  let timeout = 0;
525
- if (step.awaitBefore?.timeout) timeout += step.awaitBefore.timeout;
526
- if (step.awaitAfter?.timeout) timeout += step.awaitAfter.timeout;
554
+ if (step.awaitBefore) {
555
+ timeout += getAwaitDefaultTimeout(step.awaitBefore);
556
+ }
557
+ if (step.awaitAfter) {
558
+ timeout += getAwaitDefaultTimeout(step.awaitAfter);
559
+ }
527
560
  return timeout;
528
561
  }
562
+ function getStepExecutionTimeout(step, config) {
563
+ if (step.stepTimeout !== void 0) {
564
+ return step.stepTimeout;
565
+ }
566
+ if (config?.flow?.stepTimeout !== void 0) {
567
+ return config.flow.stepTimeout;
568
+ }
569
+ if (config?.queue?.defaultJobOptions?.timeout !== void 0) {
570
+ return config.queue.defaultJobOptions.timeout;
571
+ }
572
+ return void 0;
573
+ }
529
574
  function calculateFlowStallTimeout(steps, levels) {
530
575
  const DEFAULT_STALL_TIMEOUT = 30 * 60 * 1e3;
531
576
  const MIN_BUFFER = 5 * 60 * 1e3;
532
577
  const BUFFER_PERCENTAGE = 0.1;
578
+ const DEFAULT_STEP_TIMEOUT = 5 * 60 * 1e3;
533
579
  const levelTimeouts = [];
534
- let awaitCount = 0;
535
580
  for (const levelSteps of levels) {
536
581
  let maxLevelTimeout = 0;
537
582
  for (const stepName of levelSteps) {
538
583
  const step = steps[stepName];
539
584
  if (!step) continue;
540
- const stepTimeout = getStepAwaitTimeout(step);
541
- if (stepTimeout > 0) {
542
- awaitCount++;
543
- maxLevelTimeout = Math.max(maxLevelTimeout, stepTimeout);
585
+ const stepExecTimeout = step.stepTimeout ?? DEFAULT_STEP_TIMEOUT;
586
+ const awaitTimeout = getStepAwaitTimeout(step);
587
+ const totalStepTimeout = stepExecTimeout + awaitTimeout;
588
+ if (totalStepTimeout > 0) {
589
+ maxLevelTimeout = Math.max(maxLevelTimeout, totalStepTimeout);
544
590
  }
545
591
  }
546
592
  if (maxLevelTimeout > 0) {
@@ -555,31 +601,57 @@ function calculateFlowStallTimeout(steps, levels) {
555
601
  const calculatedTimeout = totalAwaitTimeout + buffer;
556
602
  if (calculatedTimeout > DEFAULT_STALL_TIMEOUT * 2) {
557
603
  console.log(
558
- `[flow-analyzer] Flow has ${awaitCount} await patterns across ${levelTimeouts.length} levels, calculated stall timeout: ${calculatedTimeout / 1e3}s (total await time: ${totalAwaitTimeout / 1e3}s, level timeouts: [${levelTimeouts.map((t) => `${t / 1e3}s`).join(", ")}])`
604
+ `[flow-analyzer] Flow stall timeout calculated across ${levelTimeouts.length} levels: ${calculatedTimeout / 1e3}s (total time: ${totalAwaitTimeout / 1e3}s, level timeouts: [${levelTimeouts.map((t) => `${t / 1e3}s`).join(", ")}])`
559
605
  );
560
606
  }
561
607
  return calculatedTimeout;
562
608
  }
563
- function analyzeFlow(flow) {
609
+ function analyzeFlow(flow, config) {
564
610
  const entryStepName = flow.entry?.step;
565
611
  const steps = flow.steps || {};
566
- const dependencies = buildDependencyGraph(entryStepName, steps);
612
+ const dependencies = buildDependencyGraph(entryStepName, steps, flow.entry);
567
613
  const levels = calculateLevels(entryStepName, dependencies);
568
614
  const analyzedSteps = {};
615
+ if (flow.entry && entryStepName) {
616
+ const hasAwaitPattern = !!(flow.entry.awaitBefore || flow.entry.awaitAfter);
617
+ const entryStep = {
618
+ queue: flow.entry.queue,
619
+ workerId: flow.entry.workerId,
620
+ emits: flow.entry.emits,
621
+ stepTimeout: flow.entry.stepTimeout,
622
+ // Include stepTimeout from flow metadata
623
+ awaitBefore: flow.entry.awaitBefore,
624
+ awaitAfter: flow.entry.awaitAfter,
625
+ name: entryStepName,
626
+ dependsOn: [],
627
+ triggers: findTriggeredSteps(entryStepName, flow.entry, steps),
628
+ level: 0,
629
+ hasAwaitPattern
630
+ };
631
+ entryStep.stepTimeout = getStepExecutionTimeout(entryStep, config);
632
+ analyzedSteps[entryStepName] = entryStep;
633
+ }
569
634
  for (const [stepName, step] of Object.entries(steps)) {
570
635
  const hasAwaitPattern = !!(step.awaitBefore || step.awaitAfter);
571
- analyzedSteps[stepName] = {
636
+ const analyzedStep = {
572
637
  ...step,
573
638
  name: stepName,
574
639
  dependsOn: dependencies[stepName] || [],
575
640
  triggers: findTriggeredSteps(stepName, step, steps),
576
641
  level: levels[stepName] ?? 1,
577
642
  hasAwaitPattern
643
+ // stepTimeout from ...step spread above (per-function config)
578
644
  };
645
+ analyzedStep.stepTimeout = getStepExecutionTimeout(analyzedStep, config);
646
+ analyzedSteps[stepName] = analyzedStep;
579
647
  }
580
648
  const maxLevel = Math.max(0, ...Object.values(levels));
581
649
  const levelGroups = Array.from({ length: maxLevel + 1 }, () => []);
650
+ if (entryStepName && flow.entry) {
651
+ levelGroups[0]?.push(entryStepName);
652
+ }
582
653
  for (const [stepName, level] of Object.entries(levels)) {
654
+ if (stepName === entryStepName) continue;
583
655
  const levelArray = levelGroups[level];
584
656
  if (levelArray) {
585
657
  levelArray.push(stepName);
@@ -822,7 +894,7 @@ function generateAnalyzedFlowsTemplate(registry) {
822
894
  entry,
823
895
  steps
824
896
  };
825
- const analyzed = analyzeFlow(flowMeta);
897
+ const analyzed = analyzeFlow(flowMeta, registry.config);
826
898
  return {
827
899
  ...flowMeta,
828
900
  analyzed: {
@@ -909,6 +981,25 @@ export type {
909
981
  ListOptions,
910
982
  } from ${JSON.stringify(resolverFn("./runtime/adapters/interfaces/store"))}
911
983
 
984
+ // Flow Types
985
+ export type {
986
+ FlowStats,
987
+ StartFlowResult,
988
+ CancelFlowResult,
989
+ RunningFlow,
990
+ FlowComposable,
991
+ } from ${JSON.stringify(resolverFn("./runtime/nitro/utils/useFlow"))}
992
+
993
+ // Runner Context Types
994
+ export type {
995
+ QueueJob,
996
+ RunLogger,
997
+ RunState,
998
+ RunContextFlow,
999
+ RunContext,
1000
+ NodeHandler,
1001
+ } from ${JSON.stringify(resolverFn("./runtime/worker/node/runner"))}
1002
+
912
1003
  // Event Types
913
1004
  export type {
914
1005
  EventType,
@@ -998,6 +1089,10 @@ function getServerImports(resolverFn, buildDir) {
998
1089
  name: "defineAwaitResolveHook",
999
1090
  from: resolverFn("./runtime/nitro/utils/defineHooks")
1000
1091
  },
1092
+ {
1093
+ name: "defineAwaitTimeoutHook",
1094
+ from: resolverFn("./runtime/nitro/utils/defineHooks")
1095
+ },
1001
1096
  // Adapter composables
1002
1097
  {
1003
1098
  name: "useQueueAdapter",
@@ -1070,9 +1165,10 @@ function getServerImports(resolverFn, buildDir) {
1070
1165
  ];
1071
1166
  }
1072
1167
 
1168
+ const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
1073
1169
  const meta = {
1074
1170
  name: "nvent",
1075
- version: "0.4.1",
1171
+ version: packageJson.version,
1076
1172
  configKey: "nvent"
1077
1173
  };
1078
1174
  const module$1 = defineNuxtModule().with({
@@ -1117,7 +1213,11 @@ const module$1 = defineNuxtModule().with({
1117
1213
  logger: { name: "console", level: "info" },
1118
1214
  runner: { ts: { isolate: "inprocess" }, py: { enabled: false, cmd: "python3", importMode: "file" } },
1119
1215
  flows: {},
1120
- eventIndex: {}
1216
+ eventIndex: {},
1217
+ config: {
1218
+ flow: config.flow,
1219
+ queue: config.queue
1220
+ }
1121
1221
  });
1122
1222
  const compiledSnapshot = JSON.parse(JSON.stringify(compiledWithMeta));
1123
1223
  let lastCompiledRegistry = compiledSnapshot;
@@ -1177,7 +1277,11 @@ const module$1 = defineNuxtModule().with({
1177
1277
  logger: { name: "console", level: "info" },
1178
1278
  runner: { ts: { isolate: "inprocess" }, py: { enabled: false, cmd: "python3", importMode: "file" } },
1179
1279
  flows: {},
1180
- eventIndex: {}
1280
+ eventIndex: {},
1281
+ config: {
1282
+ flow: config.flow,
1283
+ queue: config.queue
1284
+ }
1181
1285
  })));
1182
1286
  console.log(`[nvent] registry refreshed (${reason})`, changedPath || "");
1183
1287
  console.log(`[nvent] new registry has ${lastCompiledRegistry.workers?.length || 0} workers`);
@@ -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
  }
@@ -69,13 +69,14 @@ async function createStoreAdapter(config) {
69
69
  try {
70
70
  const { StoreSubjects } = useStreamTopics();
71
71
  const flowIndexKey = StoreSubjects.flowIndex();
72
- const existingFlows = await adapter.index.read(flowIndexKey, { limit: 1 });
73
- if (existingFlows.length === 0) {
74
- const analyzedFlows = $useAnalyzedFlows();
75
- if (analyzedFlows && analyzedFlows.length > 0) {
76
- const now = (/* @__PURE__ */ new Date()).toISOString();
77
- for (const flow of analyzedFlows) {
78
- await adapter.index.add(flowIndexKey, flow.id, Date.now(), {
72
+ const analyzedFlows = $useAnalyzedFlows();
73
+ if (analyzedFlows && analyzedFlows.length > 0) {
74
+ const existingFlows = await adapter.index.read(flowIndexKey, { limit: 100 });
75
+ const existingFlowIds = new Set(existingFlows.map((f) => f.id));
76
+ const now = Date.now();
77
+ for (const flow of analyzedFlows) {
78
+ if (!existingFlowIds.has(flow.id)) {
79
+ await adapter.index.add(flowIndexKey, flow.id, now, {
79
80
  name: flow.id,
80
81
  displayName: flow.id,
81
82
  registeredAt: now,
@@ -49,6 +49,11 @@ export interface QueueAdapter {
49
49
  * @returns true if removed, false if not found
50
50
  */
51
51
  removeScheduledJob?(scheduleId: string): Promise<boolean>;
52
+ /**
53
+ * Remove/cancel a job by ID
54
+ * @returns true if removed, false if not found
55
+ */
56
+ removeJob?(queueName: string, jobId: string): Promise<boolean>;
52
57
  /**
53
58
  * Pause the queue
54
59
  */
@@ -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;
@@ -45,7 +45,20 @@ export function normalizeModuleOptions(options) {
45
45
  checkInterval: 15 * 60 * 1e3,
46
46
  // 15 minutes
47
47
  enablePeriodicCheck: true
48
- }
48
+ },
49
+ awaitDefaults: {
50
+ webhookTimeout: 24 * 60 * 60 * 1e3,
51
+ // 24 hours
52
+ eventTimeout: 24 * 60 * 60 * 1e3,
53
+ // 24 hours
54
+ timeTimeout: void 0,
55
+ // No default timeout for time awaits
56
+ scheduleTimeout: void 0,
57
+ // No default timeout for schedule awaits
58
+ timeoutAction: "fail"
59
+ },
60
+ stepTimeout: 5 * 60 * 1e3
61
+ // 5 minutes default step execution timeout
49
62
  },
50
63
  webhooks: {
51
64
  // baseUrl will be determined at runtime from Nitro context
@@ -238,6 +238,48 @@ export interface FlowConfig {
238
238
  */
239
239
  enablePeriodicCheck?: boolean;
240
240
  };
241
+ /**
242
+ * Default timeout values for await patterns
243
+ * These are used when developers don't specify explicit timeouts
244
+ * @since v0.5.0
245
+ */
246
+ awaitDefaults?: {
247
+ /**
248
+ * Default timeout for webhook await patterns in milliseconds
249
+ * @default 86400000 (24 hours)
250
+ */
251
+ webhookTimeout?: number;
252
+ /**
253
+ * Default timeout for event await patterns in milliseconds
254
+ * @default 86400000 (24 hours)
255
+ */
256
+ eventTimeout?: number;
257
+ /**
258
+ * Default timeout for time await patterns in milliseconds
259
+ * Time awaits typically don't need a timeout since they resolve based on delay
260
+ * @default undefined (no timeout)
261
+ */
262
+ timeTimeout?: number;
263
+ /**
264
+ * Default timeout for schedule await patterns in milliseconds
265
+ * Schedule awaits typically don't need a timeout since they resolve based on cron
266
+ * @default undefined (no timeout)
267
+ */
268
+ scheduleTimeout?: number;
269
+ /**
270
+ * Default timeout action when await times out
271
+ * @default 'fail'
272
+ */
273
+ timeoutAction?: 'fail' | 'continue' | 'retry';
274
+ };
275
+ /**
276
+ * Default step execution timeout in milliseconds
277
+ * This is the maximum time a step function can run before timing out
278
+ * Priority: defineFunctionConfig > flow.stepTimeout > queue.defaultJobOptions.timeout
279
+ * @default 300000 (5 minutes)
280
+ * @since v0.5.0
281
+ */
282
+ stepTimeout?: number;
241
283
  }
242
284
  /**
243
285
  * State management configuration
@@ -57,7 +57,6 @@ export interface FlowStalledEvent extends BaseEvent {
57
57
  type: 'flow.stalled';
58
58
  data?: {
59
59
  lastActivityAt?: number;
60
- stallTimeout?: number;
61
60
  };
62
61
  }
63
62
  export interface StepStartedEvent extends StepEvent {
@@ -2,102 +2,45 @@
2
2
  * Flow Stall Detection System
3
3
  *
4
4
  * Detects and marks flows that have been in "running" state for too long without activity.
5
- * Uses a hybrid approach:
6
- * 1. Lazy detection: Check stall status when flows are queried (zero overhead)
7
- * 2. Periodic cleanup: Background job that checks all running flows periodically (safety net)
5
+ * Uses per-flow scheduler jobs for precise timeout tracking:
8
6
  *
9
- * A flow is considered "stalled" when:
10
- * - Status is "running"
11
- * - No activity (step events) for longer than STALL_TIMEOUT
12
- * - lastActivityAt timestamp is older than threshold
7
+ * - On flow.start: Schedule timeout job with flow-specific deadline
8
+ * - On step events: Reschedule to extend timeout from current time
9
+ * - On flow end: Unschedule the timeout job
10
+ * - When timeout fires: Mark flow as stalled
11
+ *
12
+ * Startup recovery handles flows left running from previous server instance.
13
13
  */
14
14
  import type { StoreAdapter } from '../../adapters/interfaces/store.js';
15
15
  export interface StallDetectorConfig {
16
16
  /**
17
- * Time in milliseconds after which a running flow without activity is considered stalled
18
- * @default 1800000 (30 minutes)
19
- */
20
- stallTimeout?: number;
21
- /**
22
- * Interval in milliseconds for periodic stall checks
23
- * @default 900000 (15 minutes)
24
- */
25
- checkInterval?: number;
26
- /**
27
- * Enable periodic background checks
28
- * Set to false to use only lazy detection
17
+ * Enable stall detection system
29
18
  * @default true
30
19
  */
31
- enablePeriodicCheck?: boolean;
20
+ enabled?: boolean;
32
21
  }
33
22
  export type FlowStatus = 'running' | 'completed' | 'failed' | 'canceled' | 'stalled';
34
- export interface FlowActivity {
35
- runId: string;
36
- flowName: string;
37
- status: FlowStatus;
38
- startedAt: number;
39
- lastActivityAt: number;
40
- metadata?: any;
41
- }
42
23
  export declare class FlowStallDetector {
43
24
  private store;
44
25
  private config;
45
26
  private logger;
46
- private schedulerJobId?;
47
27
  private started;
48
28
  constructor(store: StoreAdapter, config?: StallDetectorConfig);
49
29
  /**
50
- * Start the periodic stall detector
51
- * Should be called once per instance after adapters are initialized
30
+ * Start the stall detector
52
31
  * Runs startup recovery to clean up flows from previous server instances
32
+ * Note: Periodic checking removed - now uses per-flow scheduler jobs
53
33
  */
54
34
  start(): Promise<void>;
55
35
  /**
56
- * Get the configuration for scheduling
57
- * Returns config needed by flowWiring to register the scheduler job
58
- */
59
- getScheduleConfig(): {
60
- enabled: boolean;
61
- interval: number;
62
- stallTimeout: number;
63
- };
64
- /**
65
- * Set the scheduler job ID (called from flowWiring after scheduling)
66
- */
67
- setSchedulerJobId(jobId: string): void;
68
- /**
69
- * Stop the periodic stall detector
36
+ * Stop the stall detector
70
37
  */
71
38
  stop(): Promise<void>;
72
- /**
73
- * Get stall timeout for a specific flow
74
- * Uses flow-specific timeout from analyzed metadata, falls back to global config
75
- */
76
- private getFlowStallTimeout;
77
- /**
78
- * Update activity timestamp for a flow
79
- * Should be called on every step event (started, completed, failed, retry)
80
- */
81
- updateActivity(flowName: string, runId: string): Promise<void>;
82
- /**
83
- * Check if a specific flow is stalled (lazy detection)
84
- * Returns true if the flow should be marked as stalled
85
- * v0.5: Await-aware - uses flow-specific timeout and skips awaiting flows
86
- */
87
- isStalled(flowName: string, runId: string): Promise<boolean>;
88
39
  /**
89
40
  * Mark a flow as stalled
90
41
  * Emits a flow.stalled event and updates the flow status
91
42
  */
92
43
  markAsStalled(flowName: string, runId: string, reason?: string): Promise<void>;
93
- /**
94
- * Check all running flows and mark stalled ones
95
- * This is called by the periodic background job
96
- *
97
- * Note: This method requires knowledge of which flows exist.
98
- * For now, we'll need to pass flow names to check, or iterate known flows from registry.
99
- */
100
- checkFlowsForStalls(flowNames: string[]): Promise<void>;
101
44
  /**
102
45
  * Run startup recovery to clean up flows left in running state from previous server instance
103
46
  * This marks all running flows as stalled since their in-memory state is lost
@@ -119,19 +62,12 @@ export declare class FlowStallDetector {
119
62
  * - Minor discrepancies don't affect runtime behavior
120
63
  */
121
64
  private validateFlowStats;
122
- /**
123
- * Internal method for periodic checks
124
- * Gets flow names from registry and checks them
125
- */
126
- private checkAllRunningFlows;
127
65
  /**
128
66
  * Get stall detector statistics
129
67
  */
130
68
  getStats(): {
131
69
  enabled: boolean;
132
- periodicCheckEnabled: boolean;
133
- stallTimeout: number;
134
- checkInterval: number;
70
+ mode: string;
135
71
  };
136
72
  }
137
73
  /**