nvent 0.5.3 → 0.5.5

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 (36) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/module.mjs +110 -24
  3. package/dist/runtime/adapters/factory.js +8 -7
  4. package/dist/runtime/adapters/interfaces/queue.d.ts +5 -0
  5. package/dist/runtime/config/index.js +14 -1
  6. package/dist/runtime/config/types.d.ts +42 -0
  7. package/dist/runtime/events/types.d.ts +0 -1
  8. package/dist/runtime/events/utils/stallDetector.d.ts +13 -77
  9. package/dist/runtime/events/utils/stallDetector.js +8 -192
  10. package/dist/runtime/events/wiring/flowWiring.js +311 -107
  11. package/dist/runtime/events/wiring/triggerWiring.js +3 -1
  12. package/dist/runtime/nitro/plugins/02.workers.js +31 -2
  13. package/dist/runtime/nitro/routes/webhook.await.js +28 -6
  14. package/dist/runtime/nitro/utils/awaitPatterns/event.js +58 -50
  15. package/dist/runtime/nitro/utils/awaitPatterns/schedule.js +6 -1
  16. package/dist/runtime/nitro/utils/awaitPatterns/time.d.ts +1 -1
  17. package/dist/runtime/nitro/utils/awaitPatterns/time.js +6 -2
  18. package/dist/runtime/nitro/utils/awaitPatterns/webhook.js +53 -45
  19. package/dist/runtime/nitro/utils/defineFunction.d.ts +2 -9
  20. package/dist/runtime/nitro/utils/defineFunction.js +1 -14
  21. package/dist/runtime/nitro/utils/defineFunctionConfig.d.ts +84 -16
  22. package/dist/runtime/nitro/utils/defineHooks.d.ts +64 -10
  23. package/dist/runtime/nitro/utils/defineHooks.js +3 -0
  24. package/dist/runtime/nitro/utils/useAwait.d.ts +12 -0
  25. package/dist/runtime/nitro/utils/useAwait.js +34 -4
  26. package/dist/runtime/nitro/utils/useFlow.js +13 -2
  27. package/dist/runtime/nitro/utils/useHookRegistry.d.ts +10 -4
  28. package/dist/runtime/scheduler/scheduler.d.ts +19 -0
  29. package/dist/runtime/scheduler/scheduler.js +184 -8
  30. package/dist/runtime/scheduler/types.d.ts +6 -0
  31. package/dist/runtime/worker/node/runner.js +32 -91
  32. package/dist/runtime/worker/system/awaitHandlers.d.ts +27 -0
  33. package/dist/runtime/worker/system/awaitHandlers.js +230 -0
  34. package/dist/runtime/worker/system/index.d.ts +24 -0
  35. package/dist/runtime/worker/system/index.js +39 -0
  36. package/package.json +1 -1
@@ -10,7 +10,34 @@ import {
10
10
  registerTimeAwait,
11
11
  resolveTimeAwait
12
12
  } from "./awaitPatterns/index.js";
13
- import { useStoreAdapter, useStreamTopics, useNventLogger } from "#imports";
13
+ import { useStoreAdapter, useStreamTopics, useNventLogger, useRuntimeConfig } from "#imports";
14
+ export function useAwaitDefaults() {
15
+ try {
16
+ const config = useRuntimeConfig();
17
+ const awaitDefaults = config?.nvent?.flow?.awaitDefaults;
18
+ return {
19
+ webhookTimeout: awaitDefaults?.webhookTimeout ?? 24 * 60 * 60 * 1e3,
20
+ // 24 hours
21
+ eventTimeout: awaitDefaults?.eventTimeout ?? 24 * 60 * 60 * 1e3,
22
+ // 24 hours
23
+ timeTimeout: awaitDefaults?.timeTimeout,
24
+ // undefined by default
25
+ scheduleTimeout: awaitDefaults?.scheduleTimeout,
26
+ // undefined by default
27
+ timeoutAction: awaitDefaults?.timeoutAction ?? "fail"
28
+ };
29
+ } catch {
30
+ return {
31
+ webhookTimeout: 24 * 60 * 60 * 1e3,
32
+ // 24 hours
33
+ eventTimeout: 24 * 60 * 60 * 1e3,
34
+ // 24 hours
35
+ timeTimeout: void 0,
36
+ scheduleTimeout: void 0,
37
+ timeoutAction: "fail"
38
+ };
39
+ }
40
+ }
14
41
  export function useAwait() {
15
42
  const logger = useNventLogger("await");
16
43
  const store = useStoreAdapter();
@@ -62,7 +89,10 @@ export function useAwait() {
62
89
  }
63
90
  const awaitingSteps = entry.metadata.awaitingSteps || {};
64
91
  if (stepName) {
65
- return awaitingSteps[stepName] || null;
92
+ const awaitKeyBefore = `${stepName}:before`;
93
+ const awaitKeyAfter = `${stepName}:after`;
94
+ const awaitState = awaitingSteps[awaitKeyBefore] || awaitingSteps[awaitKeyAfter] || awaitingSteps[stepName];
95
+ return awaitState || null;
66
96
  }
67
97
  return awaitingSteps;
68
98
  },
@@ -78,8 +108,8 @@ export function useAwait() {
78
108
  */
79
109
  async getAllActiveAwaits(flowName) {
80
110
  const activeAwaits = [];
81
- if (!store.indexScan) {
82
- logger.warn("Store does not support indexScan");
111
+ if (!store.index.read) {
112
+ logger.warn("Store does not support index read");
83
113
  return activeAwaits;
84
114
  }
85
115
  if (flowName) {
@@ -20,9 +20,20 @@ export function useFlow() {
20
20
  );
21
21
  const opts = entryWorker?.queue?.defaultJobOptions || {};
22
22
  const flowId = randomUUID();
23
- const id = await queueAdapter.enqueue(queueName, { name: flow.entry.step, data: { ...payload, flowId, flowName }, opts });
23
+ const id = await queueAdapter.enqueue(queueName, {
24
+ name: flow.entry.step,
25
+ data: { ...payload, flowId, flowName },
26
+ opts
27
+ });
24
28
  try {
25
- await eventsManager.publishBus({ type: "flow.start", runId: flowId, flowName, data: { input: payload } });
29
+ await eventsManager.publishBus({
30
+ type: "flow.start",
31
+ runId: flowId,
32
+ flowName,
33
+ data: {
34
+ input: payload
35
+ }
36
+ });
26
37
  } catch {
27
38
  }
28
39
  return { id, queue: queueName, step: flow.entry.step, flowId };
@@ -5,18 +5,24 @@
5
5
  export interface LifecycleHooks {
6
6
  /**
7
7
  * Called when await pattern is registered
8
- * @param webhookUrl - Generated webhook URL (for webhook awaits) or event/schedule info
8
+ * @param hookData - Type-safe data based on await type (webhookUrl, eventName, etc.)
9
9
  * @param stepData - Current step data
10
- * @param ctx - Worker context
10
+ * @param ctx - Worker context with awaitType and awaitConfig
11
11
  */
12
- onAwaitRegister?: (webhookUrl: string, stepData: any, ctx: any) => Promise<void>;
12
+ onAwaitRegister?: (hookData: any, stepData: any, ctx: any) => Promise<void>;
13
13
  /**
14
14
  * Called when await pattern is resolved
15
15
  * @param resolvedData - Data from the trigger that resolved the await
16
16
  * @param stepData - Current step data
17
- * @param ctx - Worker context
17
+ * @param ctx - Worker context with awaitType
18
18
  */
19
19
  onAwaitResolve?: (resolvedData: any, stepData: any, ctx: any) => Promise<void>;
20
+ /**
21
+ * Called when await pattern times out
22
+ * @param stepData - Current step data
23
+ * @param ctx - Worker context with awaitType and timeoutAction
24
+ */
25
+ onAwaitTimeout?: (stepData: any, ctx: any) => Promise<void>;
20
26
  }
21
27
  export declare function useHookRegistry(): {
22
28
  /**
@@ -44,6 +44,7 @@ export declare class Scheduler implements SchedulerAdapter {
44
44
  private lockRenewalTimers;
45
45
  private started;
46
46
  private logger;
47
+ private syncSubscription?;
47
48
  constructor(options: SchedulerOptions);
48
49
  schedule(job: ScheduledJob): Promise<string>;
49
50
  /**
@@ -60,6 +61,18 @@ export declare class Scheduler implements SchedulerAdapter {
60
61
  * Release distributed lock
61
62
  */
62
63
  private releaseLock;
64
+ /**
65
+ * Poll for new jobs created by other instances
66
+ * This ensures all instances have the same jobs in-memory
67
+ *
68
+ * NOTE: This is a simple but not optimal solution for distributed systems.
69
+ * TODO: For production multi-instance deployments, consider:
70
+ * - Pub/Sub notifications (Redis PUBSUB, Postgres NOTIFY/LISTEN)
71
+ * - Dedicated scheduler instance with queue-based execution
72
+ * - Service mesh with leader election
73
+ * See specs/distributed-scheduler.md for detailed architecture
74
+ */
75
+ private startJobSyncPolling;
63
76
  /**
64
77
  * Start periodic lock renewal (for long-running jobs)
65
78
  */
@@ -104,6 +117,12 @@ export declare class Scheduler implements SchedulerAdapter {
104
117
  start(): Promise<void>;
105
118
  stop(): Promise<void>;
106
119
  getScheduledJobs(): Promise<ScheduledJob[]>;
120
+ /**
121
+ * Get jobs by pattern (e.g., by runId)
122
+ * This queries the persisted store, not just in-memory jobs
123
+ * Works across all instances in a distributed setup
124
+ */
125
+ getJobsByPattern(pattern: string): Promise<ScheduledJob[]>;
107
126
  /**
108
127
  * Get all persisted jobs from store (for debugging/monitoring)
109
128
  * This shows ALL jobs across ALL instances with their runtime stats
@@ -2,7 +2,8 @@ import { CronJob } from "cron";
2
2
  import { getEventBus } from "../events/eventBus.js";
3
3
  import { resolveTimeAwait } from "../nitro/utils/awaitPatterns/time.js";
4
4
  import { resolveScheduleAwait } from "../nitro/utils/awaitPatterns/schedule.js";
5
- import { useNventLogger } from "#imports";
5
+ import { useNventLogger, useStoreAdapter, useRuntimeConfig } from "#imports";
6
+ import { createStallDetector } from "../events/utils/stallDetector.js";
6
7
  export class Scheduler {
7
8
  store;
8
9
  keyPrefix;
@@ -14,6 +15,7 @@ export class Scheduler {
14
15
  lockRenewalTimers = /* @__PURE__ */ new Map();
15
16
  started = false;
16
17
  logger = useNventLogger("scheduler");
18
+ syncSubscription;
17
19
  constructor(options) {
18
20
  this.store = options.store;
19
21
  this.keyPrefix = options.keyPrefix || "nvent:scheduler";
@@ -194,6 +196,54 @@ export class Scheduler {
194
196
  await this.store.kv.delete(lockKey);
195
197
  }
196
198
  }
199
+ /**
200
+ * Poll for new jobs created by other instances
201
+ * This ensures all instances have the same jobs in-memory
202
+ *
203
+ * NOTE: This is a simple but not optimal solution for distributed systems.
204
+ * TODO: For production multi-instance deployments, consider:
205
+ * - Pub/Sub notifications (Redis PUBSUB, Postgres NOTIFY/LISTEN)
206
+ * - Dedicated scheduler instance with queue-based execution
207
+ * - Service mesh with leader election
208
+ * See specs/distributed-scheduler.md for detailed architecture
209
+ */
210
+ startJobSyncPolling() {
211
+ const pollInterval = setInterval(async () => {
212
+ if (!this.started) {
213
+ clearInterval(pollInterval);
214
+ return;
215
+ }
216
+ try {
217
+ if (!this.store.index.read) {
218
+ return;
219
+ }
220
+ const jobIndex = `${this.keyPrefix}:jobs`;
221
+ const entries = await this.store.index.read(jobIndex, { limit: 1e4 });
222
+ let syncedCount = 0;
223
+ for (const entry of entries) {
224
+ const jobData = entry.metadata;
225
+ if (jobData && !this.jobs.has(jobData.id) && jobData.enabled !== false) {
226
+ this.logger.debug("Syncing job from another instance", {
227
+ jobId: jobData.id
228
+ });
229
+ await this.recoverJob(jobData);
230
+ syncedCount++;
231
+ }
232
+ }
233
+ if (syncedCount > 0) {
234
+ this.logger.info("Synced jobs from other instances", {
235
+ count: syncedCount,
236
+ totalJobs: this.jobs.size
237
+ });
238
+ }
239
+ } catch (error) {
240
+ this.logger.error("Error during job sync polling", {
241
+ error: error.message
242
+ });
243
+ }
244
+ }, 3e4);
245
+ this.syncSubscription = () => clearInterval(pollInterval);
246
+ }
197
247
  /**
198
248
  * Start periodic lock renewal (for long-running jobs)
199
249
  */
@@ -350,6 +400,12 @@ export class Scheduler {
350
400
  this.logger.debug("Skipping disabled job", { jobId: jobData.id });
351
401
  return;
352
402
  }
403
+ if (jobData.executeAt && typeof jobData.executeAt === "string") {
404
+ jobData.executeAt = new Date(jobData.executeAt).getTime();
405
+ }
406
+ if (jobData.interval && typeof jobData.interval === "string") {
407
+ jobData.interval = Number(jobData.interval);
408
+ }
353
409
  if (jobData.metadata?.type === "schedule-trigger" && jobData.metadata?.triggerName) {
354
410
  jobData.handler = async () => {
355
411
  const logger = useNventLogger("scheduler");
@@ -442,6 +498,19 @@ export class Scheduler {
442
498
  flowName,
443
499
  runId
444
500
  });
501
+ } else if (jobData.metadata?.component === "stall-detector") {
502
+ const { flowName, runId } = jobData.metadata;
503
+ jobData.handler = async () => {
504
+ const store = useStoreAdapter();
505
+ const config = useRuntimeConfig();
506
+ const stallDetector = createStallDetector(store, config.nvent?.flow?.stallDetection);
507
+ this.logger.info(`Per-flow stall timeout fired for '${flowName}' runId '${runId}'`);
508
+ await stallDetector.markAsStalled(flowName, runId, "Stall timeout reached");
509
+ };
510
+ this.logger.info("Reconstructed stall detector handler", {
511
+ flowName,
512
+ runId
513
+ });
445
514
  } else if (!jobData.handler) {
446
515
  this.logger.debug("Skipping job - no handler available, waiting for re-registration", {
447
516
  jobId: jobData.id
@@ -472,9 +541,85 @@ export class Scheduler {
472
541
  this.jobs.set(jobData.id, intervalId);
473
542
  this.logger.info("Recovered interval job", { jobId: jobData.id });
474
543
  } else if (jobData.type === "one-time" && jobData.executeAt) {
475
- const delay = jobData.executeAt - Date.now();
544
+ const now = Date.now();
545
+ const delay = jobData.executeAt - now;
476
546
  const isAwaitPattern = jobData.metadata?.component === "await-pattern";
477
- if (delay > 0) {
547
+ const isStallDetector = jobData.metadata?.component === "stall-detector";
548
+ const awaitType = jobData.metadata?.awaitType;
549
+ this.logger.debug("Recovering one-time job", {
550
+ jobId: jobData.id,
551
+ component: jobData.metadata?.component,
552
+ awaitType,
553
+ executeAt: jobData.executeAt,
554
+ executeAtISO: new Date(jobData.executeAt).toISOString(),
555
+ now,
556
+ nowISO: new Date(now).toISOString(),
557
+ delay,
558
+ delayHours: (delay / 1e3 / 60 / 60).toFixed(2)
559
+ });
560
+ if (isAwaitPattern && (awaitType === "webhook" || awaitType === "event")) {
561
+ if (delay > 0) {
562
+ const timeoutId = setTimeout(
563
+ async () => {
564
+ await this.executeWithLock(jobData);
565
+ await this.unschedule(jobData.id);
566
+ },
567
+ delay
568
+ );
569
+ this.jobs.set(jobData.id, timeoutId);
570
+ this.logger.info("Recovered webhook/event await timeout", {
571
+ jobId: jobData.id,
572
+ awaitType,
573
+ flowName: jobData.metadata?.flowName,
574
+ remainingMs: delay
575
+ });
576
+ } else {
577
+ this.logger.warn("Webhook/event await timeout is overdue - flow may be stalled or orphaned", {
578
+ jobId: jobData.id,
579
+ awaitType,
580
+ flowName: jobData.metadata?.flowName,
581
+ stepName: jobData.metadata?.stepName,
582
+ runId: jobData.metadata?.runId,
583
+ scheduledFor: new Date(jobData.executeAt).toISOString(),
584
+ overdueBy: Math.abs(delay)
585
+ });
586
+ await this.unschedule(jobData.id);
587
+ }
588
+ } else if (isStallDetector) {
589
+ if (delay > 0) {
590
+ const timeoutId = setTimeout(
591
+ async () => {
592
+ await this.executeWithLock(jobData);
593
+ await this.unschedule(jobData.id);
594
+ },
595
+ delay
596
+ );
597
+ this.jobs.set(jobData.id, timeoutId);
598
+ this.logger.info("Recovered stall detection timeout", {
599
+ jobId: jobData.id,
600
+ flowName: jobData.metadata?.flowName,
601
+ remainingMs: delay
602
+ });
603
+ } else {
604
+ this.logger.info("Executing overdue stall detection immediately", {
605
+ jobId: jobData.id,
606
+ flowName: jobData.metadata?.flowName,
607
+ runId: jobData.metadata?.runId,
608
+ overdueBy: Math.abs(delay)
609
+ });
610
+ setImmediate(async () => {
611
+ try {
612
+ await jobData.handler();
613
+ await this.unschedule(jobData.id);
614
+ } catch (error) {
615
+ this.logger.error("Failed to execute overdue stall detection", {
616
+ jobId: jobData.id,
617
+ error: error.message
618
+ });
619
+ }
620
+ });
621
+ }
622
+ } else if (delay > 0) {
478
623
  const timeoutId = setTimeout(
479
624
  async () => {
480
625
  await this.executeWithLock(jobData);
@@ -483,12 +628,13 @@ export class Scheduler {
483
628
  delay
484
629
  );
485
630
  this.jobs.set(jobData.id, timeoutId);
486
- this.logger.info("Recovered one-time job", { jobId: jobData.id });
487
- } else if (isAwaitPattern) {
488
- this.logger.info("Executing overdue await pattern immediately", {
631
+ this.logger.info("Recovered one-time job", { jobId: jobData.id, remainingMs: delay });
632
+ } else if (isAwaitPattern && (awaitType === "time" || awaitType === "schedule")) {
633
+ this.logger.info("Executing overdue time/schedule await immediately", {
489
634
  jobId: jobData.id,
490
- awaitType: jobData.metadata?.awaitType,
491
- flowName: jobData.metadata?.flowName
635
+ awaitType,
636
+ flowName: jobData.metadata?.flowName,
637
+ overdueBy: Math.abs(delay)
492
638
  });
493
639
  setImmediate(async () => {
494
640
  try {
@@ -541,10 +687,15 @@ export class Scheduler {
541
687
  if (this.started) return;
542
688
  this.started = true;
543
689
  await this.recoverJobs();
690
+ this.startJobSyncPolling();
544
691
  this.logger.info("Started with active jobs", { count: this.jobs.size });
545
692
  }
546
693
  async stop() {
547
694
  this.started = false;
695
+ if (this.syncSubscription) {
696
+ this.syncSubscription();
697
+ this.syncSubscription = void 0;
698
+ }
548
699
  for (const job of this.jobs.values()) {
549
700
  if (job instanceof CronJob) {
550
701
  job.stop();
@@ -572,6 +723,31 @@ export class Scheduler {
572
723
  async getScheduledJobs() {
573
724
  return Array.from(this.jobConfigs.values());
574
725
  }
726
+ /**
727
+ * Get jobs by pattern (e.g., by runId)
728
+ * This queries the persisted store, not just in-memory jobs
729
+ * Works across all instances in a distributed setup
730
+ */
731
+ async getJobsByPattern(pattern) {
732
+ const jobs = [];
733
+ try {
734
+ if (this.store.index.read) {
735
+ const jobIndex = `${this.keyPrefix}:jobs`;
736
+ const entries = await this.store.index.read(jobIndex, { limit: 1e4 });
737
+ for (const entry of entries) {
738
+ if (entry.metadata && entry.id.includes(pattern)) {
739
+ jobs.push(entry.metadata);
740
+ }
741
+ }
742
+ } else {
743
+ this.logger.warn("Store does not support index read, falling back to in-memory jobs");
744
+ return Array.from(this.jobConfigs.values()).filter((job) => job.id.includes(pattern));
745
+ }
746
+ } catch (error) {
747
+ this.logger.error("Error getting jobs by pattern", { pattern, error: error.message });
748
+ }
749
+ return jobs;
750
+ }
575
751
  /**
576
752
  * Get all persisted jobs from store (for debugging/monitoring)
577
753
  * This shows ALL jobs across ALL instances with their runtime stats
@@ -90,6 +90,12 @@ export interface SchedulerAdapter {
90
90
  * Get all scheduled jobs (in-memory, for this instance)
91
91
  */
92
92
  getScheduledJobs(): Promise<ScheduledJob[]>;
93
+ /**
94
+ * Get jobs matching a pattern (e.g., by runId)
95
+ * Queries persisted store, works across all instances
96
+ * More efficient than getAllPersistedJobs() when filtering
97
+ */
98
+ getJobsByPattern(pattern: string): Promise<ScheduledJob[]>;
93
99
  /**
94
100
  * Get all persisted jobs from store (across all instances)
95
101
  * Useful for debugging and monitoring in horizontal setups
@@ -5,13 +5,11 @@ import {
5
5
  useEventManager,
6
6
  useNventLogger,
7
7
  $useFunctionRegistry,
8
- useAwait,
9
- useHookRegistry,
10
- useStreamTopics,
8
+ $useAnalyzedFlows,
11
9
  useStateAdapter,
12
- useStoreAdapter,
13
- useRunContext
10
+ useQueueAdapter
14
11
  } from "#imports";
12
+ import { SYSTEM_HANDLERS } from "../system/index.js";
15
13
  const logger = useNventLogger("node-runner");
16
14
  const defaultState = {
17
15
  async get() {
@@ -136,49 +134,7 @@ export function createJobProcessor(handler, queueName) {
136
134
  const awaitAfter = stepMeta?.awaitAfter;
137
135
  const isAwaitResume = job.data?.awaitResolved === true;
138
136
  const awaitData = job.data?.awaitData;
139
- if (awaitBefore && !isAwaitResume) {
140
- const awaitLogger = useNventLogger("await-before");
141
- awaitLogger.info("Step has awaitBefore, registering await pattern", {
142
- flowName,
143
- runId: flowId,
144
- stepName: job.name,
145
- awaitType: awaitBefore.type
146
- });
147
- try {
148
- const { register } = useAwait();
149
- const awaitResult = await register(
150
- flowId || "unknown",
151
- job.name,
152
- flowName,
153
- awaitBefore,
154
- "before"
155
- // Position: awaitBefore means wait before execution
156
- );
157
- const hookRegistry = useHookRegistry();
158
- const hooks = hookRegistry.load(flowName, job.name);
159
- if (hooks?.onAwaitRegister) {
160
- try {
161
- await hooks.onAwaitRegister(
162
- awaitResult.webhookUrl || awaitResult.eventName || "",
163
- job.data,
164
- useRunContext({ flowId, flowName, stepName: job.name })
165
- );
166
- } catch (err) {
167
- awaitLogger.error("onAwaitRegister hook failed", { error: err.message });
168
- }
169
- }
170
- return {
171
- awaiting: true,
172
- awaitType: awaitBefore.type,
173
- awaitConfig: awaitBefore
174
- };
175
- } catch (err) {
176
- awaitLogger.error("Failed to register await pattern", {
177
- error: err.message,
178
- stack: err.stack
179
- });
180
- }
181
- }
137
+ const awaitPosition = job.data?.awaitPosition;
182
138
  const ctx = buildContext({
183
139
  jobId: job.id,
184
140
  queue: queueName,
@@ -204,7 +160,11 @@ export function createJobProcessor(handler, queueName) {
204
160
  stepName: job.name,
205
161
  stepId: stepRunId,
206
162
  attempt,
207
- data: { jobId: job.id, name: job.name, queue: queueName }
163
+ data: {
164
+ jobId: job.id,
165
+ name: job.name,
166
+ queue: queueName
167
+ }
208
168
  });
209
169
  } catch {
210
170
  }
@@ -275,50 +235,31 @@ export function createJobProcessor(handler, queueName) {
275
235
  });
276
236
  } catch {
277
237
  }
278
- if (awaitAfter && !isAwaitResume) {
279
- const awaitLogger = useNventLogger("await-after");
280
- awaitLogger.info("Step has awaitAfter, registering await pattern", {
281
- flowName,
282
- runId: flowId,
283
- stepName: job.name,
284
- awaitType: awaitAfter.type
285
- });
238
+ const shouldRegisterAwaitAfter = awaitAfter && (!isAwaitResume || awaitPosition === "before");
239
+ if (shouldRegisterAwaitAfter) {
286
240
  try {
287
- const store = useStoreAdapter();
288
- const { StoreSubjects } = useStreamTopics();
289
- const streamName = StoreSubjects.flowRun(flowId || "unknown");
290
- let _emitEvents = [];
291
- if (store.stream.read) {
292
- const recentEvents = await store.stream.read(streamName, { limit: 100 });
293
- _emitEvents = recentEvents.filter(
294
- (evt) => evt.type === "emit" && evt.stepName === job.name && evt.stepId === stepRunId
295
- );
296
- }
297
- const { register } = useAwait();
298
- const awaitResult = await register(
299
- flowId || "unknown",
300
- job.name,
301
- flowName,
302
- awaitAfter,
303
- "after"
304
- );
305
- const hookRegistry = useHookRegistry();
306
- const hooks = hookRegistry.load(flowName, job.name);
307
- if (hooks?.onAwaitRegister) {
308
- try {
309
- await hooks.onAwaitRegister(
310
- awaitResult.webhookUrl || awaitResult.eventName || "",
311
- { ...job.data, result },
312
- ctx
313
- );
314
- } catch (err) {
315
- awaitLogger.error("onAwaitRegister hook failed", { error: err.message });
316
- }
317
- }
241
+ const queue = useQueueAdapter();
242
+ const analyzedFlows = $useAnalyzedFlows();
243
+ const flowDef = analyzedFlows.find((f) => f.id === flowName);
244
+ const analyzedAwaitStep = flowDef?.analyzed?.steps?.[job.name];
245
+ const awaitStepTimeout = analyzedAwaitStep?.stepTimeout;
246
+ await queue.enqueue(queueName, {
247
+ name: SYSTEM_HANDLERS.AWAIT_REGISTER,
248
+ data: {
249
+ flowId: flowId || "unknown",
250
+ flowName,
251
+ stepName: job.name,
252
+ position: "after",
253
+ awaitConfig: awaitAfter,
254
+ input: { ...job.data, result }
255
+ },
256
+ opts: { jobId: `${flowId}__${job.name}__await-register-after`, timeout: awaitStepTimeout }
257
+ });
318
258
  } catch (err) {
319
- awaitLogger.error("Failed to register awaitAfter pattern", {
320
- error: err.message,
321
- stack: err.stack
259
+ logger.error("Failed to register awaitAfter pattern", {
260
+ flowName,
261
+ stepName: job.name,
262
+ error: err.message
322
263
  });
323
264
  }
324
265
  }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Internal system handlers for await lifecycle events
3
+ * These handlers run in job context to handle await registration, resolution, and timeout
4
+ */
5
+ import type { QueueJob } from '../node/runner.js';
6
+ /**
7
+ * System handler for await registration
8
+ * Registers await pattern and calls onAwaitRegister hook
9
+ */
10
+ export declare function awaitRegisterHandler(job: QueueJob): Promise<{
11
+ success: boolean;
12
+ awaitResult: any;
13
+ }>;
14
+ /**
15
+ * System handler for await resolution
16
+ * Calls onAwaitResolve hook and enqueues the actual step (for awaitBefore)
17
+ */
18
+ export declare function awaitResolveHandler(job: QueueJob): Promise<{
19
+ success: boolean;
20
+ }>;
21
+ /**
22
+ * System handler for await timeout
23
+ * Calls onAwaitTimeout hook
24
+ */
25
+ export declare function awaitTimeoutHandler(job: QueueJob): Promise<{
26
+ success: boolean;
27
+ }>;