nvent 0.5.5 → 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 +21 -3
- 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/interfaces/store.d.ts +3 -1
- package/dist/runtime/events/wiring/flowWiring.js +36 -2
- package/dist/runtime/events/wiring/registry.js +9 -1
- package/dist/runtime/events/wiring/triggerWiring.js +8 -0
- 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/useFlow.d.ts +39 -48
- package/dist/runtime/nitro/utils/useFlow.js +40 -12
- package/dist/runtime/nitro/utils/useTrigger.js +7 -16
- package/dist/runtime/scheduler/index.js +5 -1
- package/dist/runtime/worker/node/runner.d.ts +44 -2
- package/dist/runtime/worker/node/runner.js +13 -9
- package/package.json +1 -1
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -639,9 +639,8 @@ function analyzeFlow(flow, config) {
|
|
|
639
639
|
dependsOn: dependencies[stepName] || [],
|
|
640
640
|
triggers: findTriggeredSteps(stepName, step, steps),
|
|
641
641
|
level: levels[stepName] ?? 1,
|
|
642
|
-
hasAwaitPattern
|
|
643
|
-
stepTimeout
|
|
644
|
-
// Step execution timeout from config
|
|
642
|
+
hasAwaitPattern
|
|
643
|
+
// stepTimeout from ...step spread above (per-function config)
|
|
645
644
|
};
|
|
646
645
|
analyzedStep.stepTimeout = getStepExecutionTimeout(analyzedStep, config);
|
|
647
646
|
analyzedSteps[stepName] = analyzedStep;
|
|
@@ -982,6 +981,25 @@ export type {
|
|
|
982
981
|
ListOptions,
|
|
983
982
|
} from ${JSON.stringify(resolverFn("./runtime/adapters/interfaces/store"))}
|
|
984
983
|
|
|
984
|
+
// Flow Types
|
|
985
|
+
export type {
|
|
986
|
+
FlowStats,
|
|
987
|
+
StartFlowResult,
|
|
988
|
+
CancelFlowResult,
|
|
989
|
+
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
|
+
|
|
985
1003
|
// Event Types
|
|
986
1004
|
export type {
|
|
987
1005
|
EventType,
|
|
@@ -43,6 +43,7 @@ export declare class MemoryStoreAdapter implements StoreAdapter {
|
|
|
43
43
|
read: (key: string, opts?: {
|
|
44
44
|
offset?: number;
|
|
45
45
|
limit?: number;
|
|
46
|
+
filter?: Record<string, any>;
|
|
46
47
|
}) => Promise<Array<{
|
|
47
48
|
id: string;
|
|
48
49
|
score: number;
|
|
@@ -61,7 +62,7 @@ export declare class MemoryStoreAdapter implements StoreAdapter {
|
|
|
61
62
|
/**
|
|
62
63
|
* Convert dot notation keys to nested objects
|
|
63
64
|
* e.g., { 'stats.totalFires': 5 } -> { stats: { totalFires: 5 } }
|
|
64
|
-
* null values are
|
|
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
|
-
|
|
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
|
|
306
|
+
* null values are tracked for deletion after merge
|
|
295
307
|
*/
|
|
296
|
-
expandDotNotation(obj) {
|
|
308
|
+
expandDotNotation(obj, parentPath = []) {
|
|
297
309
|
const result = {};
|
|
298
310
|
const deleteMarkers = [];
|
|
299
311
|
for (const [key, value] of Object.entries(obj)) {
|
|
@@ -303,7 +315,7 @@ export class MemoryStoreAdapter {
|
|
|
303
315
|
if (key.includes(".")) {
|
|
304
316
|
const keys = key.split(".");
|
|
305
317
|
if (value === null || value === void 0) {
|
|
306
|
-
deleteMarkers.push({ path: keys, delete: true });
|
|
318
|
+
deleteMarkers.push({ path: [...parentPath, ...keys], delete: true });
|
|
307
319
|
continue;
|
|
308
320
|
}
|
|
309
321
|
let current = result;
|
|
@@ -315,6 +327,18 @@ export class MemoryStoreAdapter {
|
|
|
315
327
|
current = current[k];
|
|
316
328
|
}
|
|
317
329
|
current[keys[keys.length - 1]] = value;
|
|
330
|
+
} else if (value === null || value === void 0) {
|
|
331
|
+
deleteMarkers.push({ path: [...parentPath, key], delete: true });
|
|
332
|
+
} else if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
333
|
+
const nested = this.expandDotNotation(value, [...parentPath, key]);
|
|
334
|
+
const nestedDeleteMarkers = nested.__deleteMarkers;
|
|
335
|
+
delete nested.__deleteMarkers;
|
|
336
|
+
if (nestedDeleteMarkers) {
|
|
337
|
+
deleteMarkers.push(...nestedDeleteMarkers);
|
|
338
|
+
}
|
|
339
|
+
if (Object.keys(nested).length > 0) {
|
|
340
|
+
result[key] = nested;
|
|
341
|
+
}
|
|
318
342
|
} else {
|
|
319
343
|
result[key] = value;
|
|
320
344
|
}
|
|
@@ -167,14 +167,16 @@ export interface StoreAdapter {
|
|
|
167
167
|
/**
|
|
168
168
|
* Read entries from a sorted index (ordered by score descending)
|
|
169
169
|
* @param key - Index key
|
|
170
|
-
* @param opts - Pagination options
|
|
170
|
+
* @param opts - Pagination and filter options
|
|
171
171
|
* @param opts.offset - Number of entries to skip
|
|
172
172
|
* @param opts.limit - Maximum number of entries to return
|
|
173
|
+
* @param opts.filter - Optional filter criteria for metadata fields (adapter-dependent efficiency)
|
|
173
174
|
* @returns Array of entries with scores and metadata
|
|
174
175
|
*/
|
|
175
176
|
read(key: string, opts?: {
|
|
176
177
|
offset?: number;
|
|
177
178
|
limit?: number;
|
|
179
|
+
filter?: Record<string, any>;
|
|
178
180
|
}): Promise<Array<{
|
|
179
181
|
id: string;
|
|
180
182
|
score: number;
|
|
@@ -530,7 +530,12 @@ export function createFlowWiring() {
|
|
|
530
530
|
logger.debug("Updated flow stats for failure", { flowName });
|
|
531
531
|
} else if (e.type === "flow.cancel") {
|
|
532
532
|
if (store.index.increment) {
|
|
533
|
-
|
|
533
|
+
const previousStatus = e.data?.previousStatus;
|
|
534
|
+
if (previousStatus === "awaiting") {
|
|
535
|
+
await store.index.increment(flowIndexKey, flowName, "stats.awaiting", -1);
|
|
536
|
+
} else {
|
|
537
|
+
await store.index.increment(flowIndexKey, flowName, "stats.running", -1);
|
|
538
|
+
}
|
|
534
539
|
await store.index.increment(flowIndexKey, flowName, "stats.cancel", 1);
|
|
535
540
|
}
|
|
536
541
|
if (store.index.updateWithRetry) {
|
|
@@ -538,7 +543,7 @@ export function createFlowWiring() {
|
|
|
538
543
|
lastCompletedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
539
544
|
});
|
|
540
545
|
}
|
|
541
|
-
logger.debug("Updated flow stats for cancellation", { flowName });
|
|
546
|
+
logger.debug("Updated flow stats for cancellation", { flowName, previousStatus: e.data?.previousStatus });
|
|
542
547
|
} else if (e.type === "flow.stalled") {
|
|
543
548
|
if (store.index.increment && e.data?.previousStatus) {
|
|
544
549
|
if (e.data.previousStatus === "awaiting") {
|
|
@@ -735,6 +740,14 @@ export function createFlowWiring() {
|
|
|
735
740
|
const { stepName, awaitType, position, config: config2 } = awaitEvent;
|
|
736
741
|
try {
|
|
737
742
|
if (store.index.updateWithRetry) {
|
|
743
|
+
if (store.index.get) {
|
|
744
|
+
const currentEntry = await store.index.get(indexKey, runId);
|
|
745
|
+
const currentStatus = currentEntry?.metadata?.status;
|
|
746
|
+
if (currentStatus === "canceled") {
|
|
747
|
+
logger.debug("Flow already canceled, skipping await registration", { flowName, runId, stepName });
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
738
751
|
const now = Date.now();
|
|
739
752
|
let timeoutAt;
|
|
740
753
|
if (awaitType === "time" && config2.delay) {
|
|
@@ -785,6 +798,14 @@ export function createFlowWiring() {
|
|
|
785
798
|
const awaitEvent = e;
|
|
786
799
|
const { stepName, triggerData, position } = awaitEvent;
|
|
787
800
|
try {
|
|
801
|
+
if (store.index.get) {
|
|
802
|
+
const currentEntry = await store.index.get(indexKey, runId);
|
|
803
|
+
const currentStatus = currentEntry?.metadata?.status;
|
|
804
|
+
if (currentStatus === "canceled") {
|
|
805
|
+
logger.debug("Flow already canceled, skipping await resolution", { flowName, runId, stepName });
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
788
809
|
if (store.index.updateWithRetry) {
|
|
789
810
|
const awaitKey = `${stepName}:${position}`;
|
|
790
811
|
await store.index.updateWithRetry(indexKey, runId, {
|
|
@@ -870,6 +891,14 @@ export function createFlowWiring() {
|
|
|
870
891
|
const timeoutEvent = e;
|
|
871
892
|
const { stepName, timeoutAction, position, awaitType } = timeoutEvent;
|
|
872
893
|
const action = timeoutAction || "fail";
|
|
894
|
+
if (store.index.get) {
|
|
895
|
+
const currentEntry = await store.index.get(indexKey, runId);
|
|
896
|
+
const currentStatus = currentEntry?.metadata?.status;
|
|
897
|
+
if (currentStatus === "canceled") {
|
|
898
|
+
logger.debug("Flow already canceled, skipping await timeout handling", { flowName, runId, stepName });
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
873
902
|
logger.warn("Await timeout occurred", {
|
|
874
903
|
runId,
|
|
875
904
|
stepName,
|
|
@@ -1049,6 +1078,11 @@ export function createFlowWiring() {
|
|
|
1049
1078
|
}
|
|
1050
1079
|
if (store.index.get) {
|
|
1051
1080
|
const currentEntry = await store.index.get(indexKey, runId);
|
|
1081
|
+
const currentStatus = currentEntry?.metadata?.status;
|
|
1082
|
+
if (currentStatus === "canceled") {
|
|
1083
|
+
logger.debug("Flow already canceled, skipping status update", { flowName, runId });
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1052
1086
|
const awaitingStepsObj = currentEntry?.metadata?.awaitingSteps || {};
|
|
1053
1087
|
let hasActiveAwaits = false;
|
|
1054
1088
|
let hasTimedOutAwaits = false;
|
|
@@ -2,7 +2,12 @@ import { createFlowWiring } from "./flowWiring.js";
|
|
|
2
2
|
import { createStreamWiring } from "./streamWiring.js";
|
|
3
3
|
import { createStateWiring } from "./stateWiring.js";
|
|
4
4
|
import { createTriggerWiring } from "./triggerWiring.js";
|
|
5
|
+
const WIRING_KEY = "__nvent_wiring__";
|
|
5
6
|
export function createWiringRegistry(opts) {
|
|
7
|
+
const existingWiring = globalThis[WIRING_KEY];
|
|
8
|
+
if (existingWiring) {
|
|
9
|
+
return existingWiring;
|
|
10
|
+
}
|
|
6
11
|
const wirings = [
|
|
7
12
|
// 1. Flow orchestration (persistence, completion tracking, step triggering)
|
|
8
13
|
createFlowWiring(),
|
|
@@ -16,7 +21,7 @@ export function createWiringRegistry(opts) {
|
|
|
16
21
|
createTriggerWiring()
|
|
17
22
|
];
|
|
18
23
|
let started = false;
|
|
19
|
-
|
|
24
|
+
const wiring = {
|
|
20
25
|
async start() {
|
|
21
26
|
if (started) return;
|
|
22
27
|
started = true;
|
|
@@ -30,6 +35,9 @@ export function createWiringRegistry(opts) {
|
|
|
30
35
|
}
|
|
31
36
|
}
|
|
32
37
|
started = false;
|
|
38
|
+
globalThis[WIRING_KEY] = null;
|
|
33
39
|
}
|
|
34
40
|
};
|
|
41
|
+
globalThis[WIRING_KEY] = wiring;
|
|
42
|
+
return wiring;
|
|
35
43
|
}
|
|
@@ -312,6 +312,14 @@ export async function handleTriggerFired(event) {
|
|
|
312
312
|
const trigger = useTrigger();
|
|
313
313
|
const { triggerName, data } = event;
|
|
314
314
|
logger.debug("Trigger fired", { trigger: triggerName });
|
|
315
|
+
const triggerEntry = trigger.getTrigger(triggerName);
|
|
316
|
+
if (triggerEntry && triggerEntry.status !== "active") {
|
|
317
|
+
logger.info(`Trigger '${triggerName}' is ${triggerEntry.status}, skipping flow starts`, {
|
|
318
|
+
trigger: triggerName,
|
|
319
|
+
status: triggerEntry.status
|
|
320
|
+
});
|
|
321
|
+
return [];
|
|
322
|
+
}
|
|
315
323
|
const subscriptions = trigger.getAllSubscriptions().filter((sub) => sub.triggerName === triggerName);
|
|
316
324
|
if (subscriptions.length === 0) {
|
|
317
325
|
logger.warn(`No flows subscribed to trigger: ${triggerName}`);
|
|
@@ -16,6 +16,7 @@ declare const _default: import("h3").EventHandler<import("h3").EventHandlerReque
|
|
|
16
16
|
triggerName?: undefined;
|
|
17
17
|
expectedType?: undefined;
|
|
18
18
|
actualType?: undefined;
|
|
19
|
+
status?: undefined;
|
|
19
20
|
expectedMethod?: undefined;
|
|
20
21
|
actualMethod?: undefined;
|
|
21
22
|
success?: undefined;
|
|
@@ -27,6 +28,7 @@ declare const _default: import("h3").EventHandler<import("h3").EventHandlerReque
|
|
|
27
28
|
message: string;
|
|
28
29
|
expectedType?: undefined;
|
|
29
30
|
actualType?: undefined;
|
|
31
|
+
status?: undefined;
|
|
30
32
|
expectedMethod?: undefined;
|
|
31
33
|
actualMethod?: undefined;
|
|
32
34
|
success?: undefined;
|
|
@@ -38,6 +40,19 @@ declare const _default: import("h3").EventHandler<import("h3").EventHandlerReque
|
|
|
38
40
|
expectedType: string;
|
|
39
41
|
actualType: any;
|
|
40
42
|
message?: undefined;
|
|
43
|
+
status?: undefined;
|
|
44
|
+
expectedMethod?: undefined;
|
|
45
|
+
actualMethod?: undefined;
|
|
46
|
+
success?: undefined;
|
|
47
|
+
subscribedFlows?: undefined;
|
|
48
|
+
timestamp?: undefined;
|
|
49
|
+
} | {
|
|
50
|
+
error: string;
|
|
51
|
+
triggerName: string;
|
|
52
|
+
status: any;
|
|
53
|
+
message: string;
|
|
54
|
+
expectedType?: undefined;
|
|
55
|
+
actualType?: undefined;
|
|
41
56
|
expectedMethod?: undefined;
|
|
42
57
|
actualMethod?: undefined;
|
|
43
58
|
success?: undefined;
|
|
@@ -51,6 +66,7 @@ declare const _default: import("h3").EventHandler<import("h3").EventHandlerReque
|
|
|
51
66
|
triggerName?: undefined;
|
|
52
67
|
expectedType?: undefined;
|
|
53
68
|
actualType?: undefined;
|
|
69
|
+
status?: undefined;
|
|
54
70
|
success?: undefined;
|
|
55
71
|
subscribedFlows?: undefined;
|
|
56
72
|
timestamp?: undefined;
|
|
@@ -63,6 +79,7 @@ declare const _default: import("h3").EventHandler<import("h3").EventHandlerReque
|
|
|
63
79
|
error?: undefined;
|
|
64
80
|
expectedType?: undefined;
|
|
65
81
|
actualType?: undefined;
|
|
82
|
+
status?: undefined;
|
|
66
83
|
expectedMethod?: undefined;
|
|
67
84
|
actualMethod?: undefined;
|
|
68
85
|
}>>;
|
|
@@ -30,6 +30,15 @@ export default defineEventHandler(async (event) => {
|
|
|
30
30
|
actualType: triggerEntry.type
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
|
+
if (triggerEntry.status !== "active") {
|
|
34
|
+
logger.info(`Trigger '${triggerName}' is ${triggerEntry.status}, rejecting webhook`);
|
|
35
|
+
return {
|
|
36
|
+
error: "Trigger not active",
|
|
37
|
+
triggerName,
|
|
38
|
+
status: triggerEntry.status,
|
|
39
|
+
message: `Trigger '${triggerName}' is ${triggerEntry.status}. Update status to 'active' to enable.`
|
|
40
|
+
};
|
|
41
|
+
}
|
|
33
42
|
const expectedMethod = triggerEntry.webhook?.method || "POST";
|
|
34
43
|
if (event.method !== expectedMethod) {
|
|
35
44
|
logger.warn(`Method mismatch: expected ${expectedMethod}, got ${event.method}`);
|
|
@@ -14,55 +14,46 @@ export interface FlowStats {
|
|
|
14
14
|
};
|
|
15
15
|
version?: number;
|
|
16
16
|
}
|
|
17
|
+
export interface StartFlowResult {
|
|
18
|
+
id: string;
|
|
19
|
+
queue: string;
|
|
20
|
+
step: string;
|
|
21
|
+
flowId: string;
|
|
22
|
+
}
|
|
23
|
+
export interface CancelFlowResult {
|
|
24
|
+
success: boolean;
|
|
25
|
+
runId: string;
|
|
26
|
+
flowName: string;
|
|
27
|
+
}
|
|
28
|
+
export interface RunningFlow {
|
|
29
|
+
id: string;
|
|
30
|
+
flowName: string;
|
|
31
|
+
status: string;
|
|
32
|
+
startedAt: string | undefined;
|
|
33
|
+
stepCount: number;
|
|
34
|
+
completedSteps: number;
|
|
35
|
+
}
|
|
36
|
+
export interface FlowComposable {
|
|
37
|
+
startFlow: (flowName: string, payload?: any) => Promise<StartFlowResult>;
|
|
38
|
+
emit: (trigger: string, payload?: any) => Promise<any[]>;
|
|
39
|
+
cancelFlow: (flowName: string, runId: string) => Promise<CancelFlowResult>;
|
|
40
|
+
isRunning: (flowName: string, runId?: string, options?: {
|
|
41
|
+
excludeRunIds?: string[];
|
|
42
|
+
}) => Promise<boolean>;
|
|
43
|
+
getRunningFlows: (flowName: string, options?: {
|
|
44
|
+
excludeRunIds?: string[];
|
|
45
|
+
}) => Promise<RunningFlow[]>;
|
|
46
|
+
getFlowStats: (flowName: string) => Promise<FlowStats | null>;
|
|
47
|
+
getAllFlowStats: (options?: {
|
|
48
|
+
sortBy?: 'registeredAt' | 'lastRunAt' | 'name';
|
|
49
|
+
order?: 'asc' | 'desc';
|
|
50
|
+
limit?: number;
|
|
51
|
+
offset?: number;
|
|
52
|
+
}) => Promise<FlowStats[]>;
|
|
53
|
+
hasFlowStats: (flowName: string) => Promise<boolean>;
|
|
54
|
+
}
|
|
17
55
|
/**
|
|
18
56
|
* Flow composable for managing flows and accessing flow statistics
|
|
19
57
|
* Provides methods for starting, canceling, and querying flows
|
|
20
58
|
*/
|
|
21
|
-
export declare function useFlow():
|
|
22
|
-
/**
|
|
23
|
-
* Start a flow with the given payload
|
|
24
|
-
*/
|
|
25
|
-
startFlow(flowName: string, payload?: any): Promise<{
|
|
26
|
-
id: any;
|
|
27
|
-
queue: any;
|
|
28
|
-
step: any;
|
|
29
|
-
flowId: `${string}-${string}-${string}-${string}-${string}`;
|
|
30
|
-
}>;
|
|
31
|
-
/**
|
|
32
|
-
* Emit a trigger event for flow coordination
|
|
33
|
-
*/
|
|
34
|
-
emit(trigger: string, payload?: any): Promise<never[]>;
|
|
35
|
-
/**
|
|
36
|
-
* Cancel a running flow
|
|
37
|
-
*/
|
|
38
|
-
cancelFlow(flowName: string, runId: string): Promise<{
|
|
39
|
-
success: boolean;
|
|
40
|
-
runId: string;
|
|
41
|
-
flowName: string;
|
|
42
|
-
}>;
|
|
43
|
-
/**
|
|
44
|
-
* Check if a flow is currently running
|
|
45
|
-
*/
|
|
46
|
-
isRunning(flowName: string, runId?: string): Promise<any>;
|
|
47
|
-
/**
|
|
48
|
-
* Get all currently running flows
|
|
49
|
-
*/
|
|
50
|
-
getRunningFlows(flowName: string): Promise<any>;
|
|
51
|
-
/**
|
|
52
|
-
* Get flow statistics by name
|
|
53
|
-
*/
|
|
54
|
-
getFlowStats(flowName: string): Promise<FlowStats | null>;
|
|
55
|
-
/**
|
|
56
|
-
* Get all flows with their statistics
|
|
57
|
-
*/
|
|
58
|
-
getAllFlowStats(options?: {
|
|
59
|
-
sortBy?: "registeredAt" | "lastRunAt" | "name";
|
|
60
|
-
order?: "asc" | "desc";
|
|
61
|
-
limit?: number;
|
|
62
|
-
offset?: number;
|
|
63
|
-
}): Promise<FlowStats[]>;
|
|
64
|
-
/**
|
|
65
|
-
* Check if a flow has statistics in the index
|
|
66
|
-
*/
|
|
67
|
-
hasFlowStats(flowName: string): Promise<boolean>;
|
|
68
|
-
};
|
|
59
|
+
export declare function useFlow(): FlowComposable;
|
|
@@ -70,15 +70,24 @@ export function useFlow() {
|
|
|
70
70
|
*/
|
|
71
71
|
async cancelFlow(flowName, runId) {
|
|
72
72
|
try {
|
|
73
|
+
let previousStatus;
|
|
74
|
+
try {
|
|
75
|
+
const runIndexKey = StoreSubjects.flowRunIndex(flowName);
|
|
76
|
+
const entry = await store.index.get(runIndexKey, runId);
|
|
77
|
+
previousStatus = entry?.metadata?.status;
|
|
78
|
+
} catch {
|
|
79
|
+
}
|
|
73
80
|
await eventsManager.publishBus({
|
|
74
81
|
type: "flow.cancel",
|
|
75
82
|
runId,
|
|
76
83
|
flowName,
|
|
77
84
|
data: {
|
|
78
|
-
canceledAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
85
|
+
canceledAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
86
|
+
previousStatus
|
|
87
|
+
// Include for correct stats counter decrement
|
|
79
88
|
}
|
|
80
89
|
});
|
|
81
|
-
logger.info("Flow canceled", { flowName, runId });
|
|
90
|
+
logger.info("Flow canceled", { flowName, runId, previousStatus });
|
|
82
91
|
return { success: true, runId, flowName };
|
|
83
92
|
} catch (err) {
|
|
84
93
|
logger.error("Failed to cancel flow", { flowName, runId, error: err });
|
|
@@ -86,36 +95,55 @@ export function useFlow() {
|
|
|
86
95
|
}
|
|
87
96
|
},
|
|
88
97
|
/**
|
|
89
|
-
* Check if a flow is currently running
|
|
98
|
+
* Check if a flow is currently running (includes 'running' and 'awaiting' status)
|
|
99
|
+
* @param flowName - The name of the flow to check
|
|
100
|
+
* @param runId - Optional specific run ID to check (if provided, only checks that run)
|
|
101
|
+
* @param options - Optional configuration
|
|
102
|
+
* @param options.excludeRunIds - Exclude these run IDs from the check (useful when called from within a flow)
|
|
90
103
|
*/
|
|
91
|
-
async isRunning(flowName, runId) {
|
|
104
|
+
async isRunning(flowName, runId, options) {
|
|
92
105
|
try {
|
|
93
106
|
if (!store.index.read) {
|
|
94
107
|
return false;
|
|
95
108
|
}
|
|
96
109
|
const runIndexKey = StoreSubjects.flowRunIndex(flowName);
|
|
97
|
-
const
|
|
110
|
+
const activeStatuses = ["running", "awaiting"];
|
|
98
111
|
if (runId) {
|
|
99
|
-
const run =
|
|
100
|
-
return run?.metadata?.status
|
|
112
|
+
const run = await store.index.get(runIndexKey, runId);
|
|
113
|
+
return activeStatuses.includes(run?.metadata?.status);
|
|
101
114
|
}
|
|
102
|
-
|
|
115
|
+
const entries = await store.index.read(runIndexKey, {
|
|
116
|
+
limit: 1e3,
|
|
117
|
+
filter: { status: activeStatuses }
|
|
118
|
+
});
|
|
119
|
+
const excludeSet = new Set(options?.excludeRunIds || []);
|
|
120
|
+
const matchingRuns = entries.filter((e) => !excludeSet.has(e.id));
|
|
121
|
+
return matchingRuns.length > 0;
|
|
103
122
|
} catch (err) {
|
|
104
123
|
logger.error("Error checking flow status:", err);
|
|
105
124
|
return false;
|
|
106
125
|
}
|
|
107
126
|
},
|
|
108
127
|
/**
|
|
109
|
-
* Get all currently running flows
|
|
128
|
+
* Get all currently running flows (includes 'running' and 'awaiting' status)
|
|
129
|
+
* @param flowName - The name of the flow to check
|
|
130
|
+
* @param options - Optional configuration
|
|
131
|
+
* @param options.excludeRunIds - Exclude these run IDs from the results (useful when called from within a flow)
|
|
110
132
|
*/
|
|
111
|
-
async getRunningFlows(flowName) {
|
|
133
|
+
async getRunningFlows(flowName, options) {
|
|
112
134
|
try {
|
|
113
135
|
if (!store.index.read) {
|
|
114
136
|
return [];
|
|
115
137
|
}
|
|
116
138
|
const runIndexKey = StoreSubjects.flowRunIndex(flowName);
|
|
117
|
-
const
|
|
118
|
-
|
|
139
|
+
const activeStatuses = ["running", "awaiting"];
|
|
140
|
+
const entries = await store.index.read(runIndexKey, {
|
|
141
|
+
limit: 1e3,
|
|
142
|
+
filter: { status: activeStatuses }
|
|
143
|
+
});
|
|
144
|
+
const excludeSet = new Set(options?.excludeRunIds || []);
|
|
145
|
+
const filteredEntries = entries.filter((e) => !excludeSet.has(e.id));
|
|
146
|
+
return filteredEntries.map((e) => ({
|
|
119
147
|
id: e.id,
|
|
120
148
|
flowName,
|
|
121
149
|
status: e.metadata?.status,
|
|
@@ -110,6 +110,12 @@ export function useTrigger() {
|
|
|
110
110
|
`Emitting unregistered trigger '${name}'. Consider registering it first with registerTrigger().`
|
|
111
111
|
);
|
|
112
112
|
}
|
|
113
|
+
if (trigger && trigger.status !== "active") {
|
|
114
|
+
logger.info(
|
|
115
|
+
`Trigger '${name}' is ${trigger.status}, not emitting. Update status to 'active' to enable firing.`
|
|
116
|
+
);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
113
119
|
const threshold = opts?.payloadThreshold || trigger?.config?.payloadThreshold || 10 * 1024;
|
|
114
120
|
const eventData = await runtime.handleLargePayload(name, data, threshold);
|
|
115
121
|
logger.debug(`Emitting trigger: ${name}`, {
|
|
@@ -208,6 +214,7 @@ export function useTrigger() {
|
|
|
208
214
|
}
|
|
209
215
|
if (metadata.subscriptions) {
|
|
210
216
|
for (const [flowName, subData] of Object.entries(metadata.subscriptions)) {
|
|
217
|
+
if (!subData) continue;
|
|
211
218
|
const subscription = {
|
|
212
219
|
triggerName: entry.id,
|
|
213
220
|
flowName,
|
|
@@ -223,22 +230,6 @@ export function useTrigger() {
|
|
|
223
230
|
logger.info(
|
|
224
231
|
`Loaded ${activeCount} active triggers with ${totalSubscriptions} subscriptions from index`
|
|
225
232
|
);
|
|
226
|
-
} else {
|
|
227
|
-
logger.warn("Store does not support indexRead, falling back to doc-based loading");
|
|
228
|
-
if (store.list) {
|
|
229
|
-
const triggers = await store.list("triggers");
|
|
230
|
-
for (const { id, doc } of triggers) {
|
|
231
|
-
runtime.addTrigger(id, doc);
|
|
232
|
-
}
|
|
233
|
-
const subscriptions = await store.list("trigger-subscriptions");
|
|
234
|
-
for (const { doc } of subscriptions) {
|
|
235
|
-
const sub = doc;
|
|
236
|
-
runtime.addSubscription(sub.triggerName, sub.flowName, sub);
|
|
237
|
-
}
|
|
238
|
-
logger.info(
|
|
239
|
-
`Loaded ${triggers.length} triggers and ${subscriptions.length} subscriptions from doc store (legacy)`
|
|
240
|
-
);
|
|
241
|
-
}
|
|
242
233
|
}
|
|
243
234
|
runtime.setInitialized(true);
|
|
244
235
|
},
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Scheduler } from "./scheduler.js";
|
|
2
2
|
import { useRuntimeConfig } from "#imports";
|
|
3
|
-
|
|
3
|
+
const SCHEDULER_KEY = "__nvent_scheduler__";
|
|
4
|
+
let schedulerInstance = globalThis[SCHEDULER_KEY] || null;
|
|
4
5
|
export function createScheduler(store) {
|
|
5
6
|
const config = useRuntimeConfig();
|
|
6
7
|
const prefix = config.nvent.store?.prefix || "nvent";
|
|
@@ -24,6 +25,7 @@ export async function initializeScheduler(store) {
|
|
|
24
25
|
return schedulerInstance;
|
|
25
26
|
}
|
|
26
27
|
schedulerInstance = createScheduler(store);
|
|
28
|
+
globalThis[SCHEDULER_KEY] = schedulerInstance;
|
|
27
29
|
await schedulerInstance.start();
|
|
28
30
|
return schedulerInstance;
|
|
29
31
|
}
|
|
@@ -31,8 +33,10 @@ export async function shutdownScheduler() {
|
|
|
31
33
|
if (schedulerInstance) {
|
|
32
34
|
await schedulerInstance.stop();
|
|
33
35
|
schedulerInstance = null;
|
|
36
|
+
globalThis[SCHEDULER_KEY] = null;
|
|
34
37
|
}
|
|
35
38
|
}
|
|
36
39
|
export function resetScheduler() {
|
|
37
40
|
schedulerInstance = null;
|
|
41
|
+
globalThis[SCHEDULER_KEY] = null;
|
|
38
42
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
|
@@ -52,14 +52,15 @@ export function buildContext(partial) {
|
|
|
52
52
|
log: (level, msg, meta) => {
|
|
53
53
|
const runId = partial?.flowId || "unknown";
|
|
54
54
|
const flowName = meta?.flowName || "unknown";
|
|
55
|
+
const metaObj = meta !== null && meta !== void 0 ? typeof meta === "object" && !Array.isArray(meta) ? meta : { value: meta } : {};
|
|
55
56
|
void eventManager.publishBus({
|
|
56
57
|
type: "log",
|
|
57
58
|
runId,
|
|
58
59
|
flowName,
|
|
59
|
-
stepName:
|
|
60
|
-
stepId:
|
|
61
|
-
attempt:
|
|
62
|
-
data: { level, message: msg, ...
|
|
60
|
+
stepName: metaObj?.stepName,
|
|
61
|
+
stepId: metaObj?.stepId || metaObj?.stepRunId,
|
|
62
|
+
attempt: metaObj?.attempt,
|
|
63
|
+
data: { level, message: msg, ...metaObj }
|
|
63
64
|
});
|
|
64
65
|
}
|
|
65
66
|
};
|
|
@@ -82,19 +83,21 @@ export function buildContext(partial) {
|
|
|
82
83
|
}
|
|
83
84
|
return baseFlowEngine.cancelFlow(partial.flowName, partial.flowId);
|
|
84
85
|
},
|
|
85
|
-
isRunning: async (flowName, runId) => {
|
|
86
|
+
isRunning: async (flowName, runId, options) => {
|
|
86
87
|
const targetFlowName = flowName || partial?.flowName;
|
|
87
88
|
if (!targetFlowName) {
|
|
88
89
|
throw new Error("flowName is required to check if flow is running");
|
|
89
90
|
}
|
|
90
|
-
|
|
91
|
+
const effectiveOptions = options || (partial?.flowId ? { excludeRunIds: [partial.flowId] } : void 0);
|
|
92
|
+
return baseFlowEngine.isRunning(targetFlowName, runId, effectiveOptions);
|
|
91
93
|
},
|
|
92
|
-
getRunningFlows: async (flowName) => {
|
|
94
|
+
getRunningFlows: async (flowName, options) => {
|
|
93
95
|
const targetFlowName = flowName || partial?.flowName;
|
|
94
96
|
if (!targetFlowName) {
|
|
95
97
|
throw new Error("flowName is required to get running flows");
|
|
96
98
|
}
|
|
97
|
-
|
|
99
|
+
const effectiveOptions = options || (partial?.flowId ? { excludeRunIds: [partial.flowId] } : void 0);
|
|
100
|
+
return baseFlowEngine.getRunningFlows(targetFlowName, effectiveOptions);
|
|
98
101
|
}
|
|
99
102
|
};
|
|
100
103
|
return {
|
|
@@ -148,7 +151,8 @@ export function createJobProcessor(handler, queueName) {
|
|
|
148
151
|
});
|
|
149
152
|
const attemptLogger = {
|
|
150
153
|
log: (level, msg, meta) => {
|
|
151
|
-
const
|
|
154
|
+
const metaObj = meta !== null && meta !== void 0 ? typeof meta === "object" && !Array.isArray(meta) ? meta : { value: meta } : {};
|
|
155
|
+
const enriched = { ...metaObj, stepName: job.name, attempt, stepRunId, flowName };
|
|
152
156
|
ctx.logger.log(level, msg, enriched);
|
|
153
157
|
}
|
|
154
158
|
};
|