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.
- package/dist/module.json +1 -1
- package/dist/module.mjs +127 -23
- package/dist/runtime/adapters/builtin/memory-store.d.ts +2 -1
- package/dist/runtime/adapters/builtin/memory-store.js +28 -4
- package/dist/runtime/adapters/factory.js +8 -7
- package/dist/runtime/adapters/interfaces/queue.d.ts +5 -0
- package/dist/runtime/adapters/interfaces/store.d.ts +3 -1
- package/dist/runtime/config/index.js +14 -1
- package/dist/runtime/config/types.d.ts +42 -0
- package/dist/runtime/events/types.d.ts +0 -1
- package/dist/runtime/events/utils/stallDetector.d.ts +13 -77
- package/dist/runtime/events/utils/stallDetector.js +8 -192
- package/dist/runtime/events/wiring/flowWiring.js +347 -109
- package/dist/runtime/events/wiring/registry.js +9 -1
- package/dist/runtime/events/wiring/triggerWiring.js +11 -1
- package/dist/runtime/nitro/plugins/02.workers.js +31 -2
- package/dist/runtime/nitro/routes/webhook.await.js +28 -6
- package/dist/runtime/nitro/routes/webhook.trigger.d.ts +17 -0
- package/dist/runtime/nitro/routes/webhook.trigger.js +9 -0
- package/dist/runtime/nitro/utils/awaitPatterns/event.js +58 -50
- package/dist/runtime/nitro/utils/awaitPatterns/schedule.js +6 -1
- package/dist/runtime/nitro/utils/awaitPatterns/time.d.ts +1 -1
- package/dist/runtime/nitro/utils/awaitPatterns/time.js +6 -2
- package/dist/runtime/nitro/utils/awaitPatterns/webhook.js +53 -45
- package/dist/runtime/nitro/utils/defineFunction.d.ts +2 -9
- package/dist/runtime/nitro/utils/defineFunction.js +1 -14
- package/dist/runtime/nitro/utils/defineFunctionConfig.d.ts +84 -16
- package/dist/runtime/nitro/utils/defineHooks.d.ts +64 -10
- package/dist/runtime/nitro/utils/defineHooks.js +3 -0
- package/dist/runtime/nitro/utils/useAwait.d.ts +12 -0
- package/dist/runtime/nitro/utils/useAwait.js +34 -4
- package/dist/runtime/nitro/utils/useFlow.d.ts +39 -48
- package/dist/runtime/nitro/utils/useFlow.js +53 -14
- package/dist/runtime/nitro/utils/useHookRegistry.d.ts +10 -4
- package/dist/runtime/nitro/utils/useTrigger.js +7 -16
- package/dist/runtime/scheduler/index.js +5 -1
- package/dist/runtime/scheduler/scheduler.d.ts +19 -0
- package/dist/runtime/scheduler/scheduler.js +184 -8
- package/dist/runtime/scheduler/types.d.ts +6 -0
- package/dist/runtime/worker/node/runner.d.ts +44 -2
- package/dist/runtime/worker/node/runner.js +45 -100
- package/dist/runtime/worker/system/awaitHandlers.d.ts +27 -0
- package/dist/runtime/worker/system/awaitHandlers.js +230 -0
- package/dist/runtime/worker/system/index.d.ts +24 -0
- package/dist/runtime/worker/system/index.js +39 -0
- 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
|
|
544
|
+
const now = Date.now();
|
|
545
|
+
const delay = jobData.executeAt - now;
|
|
476
546
|
const isAwaitPattern = jobData.metadata?.component === "await-pattern";
|
|
477
|
-
|
|
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
|
|
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
|
|
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 {
|
|
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:
|
|
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
|
-
|
|
9
|
-
useHookRegistry,
|
|
10
|
-
useStreamTopics,
|
|
8
|
+
$useAnalyzedFlows,
|
|
11
9
|
useStateAdapter,
|
|
12
|
-
|
|
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:
|
|
62
|
-
stepId:
|
|
63
|
-
attempt:
|
|
64
|
-
data: { level, message: msg, ...
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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: {
|
|
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
|
-
|
|
279
|
-
|
|
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
|
|
288
|
-
const
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
+
}>;
|