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
@@ -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
@@ -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
@@ -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() {
@@ -54,14 +52,15 @@ export function buildContext(partial) {
54
52
  log: (level, msg, meta) => {
55
53
  const runId = partial?.flowId || "unknown";
56
54
  const flowName = meta?.flowName || "unknown";
55
+ const metaObj = meta !== null && meta !== void 0 ? typeof meta === "object" && !Array.isArray(meta) ? meta : { value: meta } : {};
57
56
  void eventManager.publishBus({
58
57
  type: "log",
59
58
  runId,
60
59
  flowName,
61
- stepName: meta?.stepName,
62
- stepId: meta?.stepId || meta?.stepRunId,
63
- attempt: meta?.attempt,
64
- 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 }
65
64
  });
66
65
  }
67
66
  };
@@ -84,19 +83,21 @@ export function buildContext(partial) {
84
83
  }
85
84
  return baseFlowEngine.cancelFlow(partial.flowName, partial.flowId);
86
85
  },
87
- isRunning: async (flowName, runId) => {
86
+ isRunning: async (flowName, runId, options) => {
88
87
  const targetFlowName = flowName || partial?.flowName;
89
88
  if (!targetFlowName) {
90
89
  throw new Error("flowName is required to check if flow is running");
91
90
  }
92
- return baseFlowEngine.isRunning(targetFlowName, runId);
91
+ const effectiveOptions = options || (partial?.flowId ? { excludeRunIds: [partial.flowId] } : void 0);
92
+ return baseFlowEngine.isRunning(targetFlowName, runId, effectiveOptions);
93
93
  },
94
- getRunningFlows: async (flowName) => {
94
+ getRunningFlows: async (flowName, options) => {
95
95
  const targetFlowName = flowName || partial?.flowName;
96
96
  if (!targetFlowName) {
97
97
  throw new Error("flowName is required to get running flows");
98
98
  }
99
- return baseFlowEngine.getRunningFlows(targetFlowName);
99
+ const effectiveOptions = options || (partial?.flowId ? { excludeRunIds: [partial.flowId] } : void 0);
100
+ return baseFlowEngine.getRunningFlows(targetFlowName, effectiveOptions);
100
101
  }
101
102
  };
102
103
  return {
@@ -136,49 +137,7 @@ export function createJobProcessor(handler, queueName) {
136
137
  const awaitAfter = stepMeta?.awaitAfter;
137
138
  const isAwaitResume = job.data?.awaitResolved === true;
138
139
  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
- }
140
+ const awaitPosition = job.data?.awaitPosition;
182
141
  const ctx = buildContext({
183
142
  jobId: job.id,
184
143
  queue: queueName,
@@ -192,7 +151,8 @@ export function createJobProcessor(handler, queueName) {
192
151
  });
193
152
  const attemptLogger = {
194
153
  log: (level, msg, meta) => {
195
- 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 };
196
156
  ctx.logger.log(level, msg, enriched);
197
157
  }
198
158
  };
@@ -204,7 +164,11 @@ export function createJobProcessor(handler, queueName) {
204
164
  stepName: job.name,
205
165
  stepId: stepRunId,
206
166
  attempt,
207
- data: { jobId: job.id, name: job.name, queue: queueName }
167
+ data: {
168
+ jobId: job.id,
169
+ name: job.name,
170
+ queue: queueName
171
+ }
208
172
  });
209
173
  } catch {
210
174
  }
@@ -275,50 +239,31 @@ export function createJobProcessor(handler, queueName) {
275
239
  });
276
240
  } catch {
277
241
  }
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
- });
242
+ const shouldRegisterAwaitAfter = awaitAfter && (!isAwaitResume || awaitPosition === "before");
243
+ if (shouldRegisterAwaitAfter) {
286
244
  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
- }
245
+ const queue = useQueueAdapter();
246
+ const analyzedFlows = $useAnalyzedFlows();
247
+ const flowDef = analyzedFlows.find((f) => f.id === flowName);
248
+ const analyzedAwaitStep = flowDef?.analyzed?.steps?.[job.name];
249
+ const awaitStepTimeout = analyzedAwaitStep?.stepTimeout;
250
+ await queue.enqueue(queueName, {
251
+ name: SYSTEM_HANDLERS.AWAIT_REGISTER,
252
+ data: {
253
+ flowId: flowId || "unknown",
254
+ flowName,
255
+ stepName: job.name,
256
+ position: "after",
257
+ awaitConfig: awaitAfter,
258
+ input: { ...job.data, result }
259
+ },
260
+ opts: { jobId: `${flowId}__${job.name}__await-register-after`, timeout: awaitStepTimeout }
261
+ });
318
262
  } catch (err) {
319
- awaitLogger.error("Failed to register awaitAfter pattern", {
320
- error: err.message,
321
- stack: err.stack
263
+ logger.error("Failed to register awaitAfter pattern", {
264
+ flowName,
265
+ stepName: job.name,
266
+ error: err.message
322
267
  });
323
268
  }
324
269
  }
@@ -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
+ }>;