@classytic/streamline 1.0.0 → 2.0.0
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/README.md +66 -23
- package/dist/{container-BzpIMrrj.mjs → container-BSxj_Or4.mjs} +294 -165
- package/dist/context-DMkusPr_.mjs +224 -0
- package/dist/{errors-BqunvWPz.mjs → errors-Ba7ZziTN.mjs} +1 -16
- package/dist/{events-C0sZINZq.d.mts → events-DC0ddZZ9.d.mts} +1 -1
- package/dist/index.d.mts +179 -51
- package/dist/index.mjs +140 -43
- package/dist/integrations/fastify.d.mts +1 -1
- package/dist/integrations/fastify.mjs +1 -1
- package/dist/rolldown-runtime-95iHPtFO.mjs +18 -0
- package/dist/telemetry/index.d.mts +1 -1
- package/dist/telemetry/index.mjs +1 -1
- package/dist/{types-DG85_LzF.d.mts → types-DjgzSrNY.d.mts} +111 -1
- package/package.json +11 -10
- /package/dist/{events-B5aTz7kD.mjs → events-CpesEn3I.mjs} +0 -0
|
@@ -1,144 +1,10 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
import {
|
|
1
|
+
import { a as logger, n as StepContextImpl, r as WaitSignal, t as GotoSignal } from "./context-DMkusPr_.mjs";
|
|
2
|
+
import { a as StepNotFoundError, c as WorkflowNotFoundError, r as InvalidStateError } from "./errors-Ba7ZziTN.mjs";
|
|
3
|
+
import { n as globalEventBus, t as WorkflowEventBus } from "./events-CpesEn3I.mjs";
|
|
3
4
|
import { randomUUID } from "node:crypto";
|
|
4
5
|
import { Repository, methodRegistryPlugin, mongoOperationsPlugin } from "@classytic/mongokit";
|
|
5
6
|
import mongoose, { Schema } from "mongoose";
|
|
6
7
|
|
|
7
|
-
//#region src/utils/logger.ts
|
|
8
|
-
var Logger = class {
|
|
9
|
-
minLevel = "info";
|
|
10
|
-
setLevel(level) {
|
|
11
|
-
this.minLevel = level;
|
|
12
|
-
}
|
|
13
|
-
debug(message, context) {
|
|
14
|
-
this.log("debug", message, context);
|
|
15
|
-
}
|
|
16
|
-
info(message, context) {
|
|
17
|
-
this.log("info", message, context);
|
|
18
|
-
}
|
|
19
|
-
warn(message, context) {
|
|
20
|
-
this.log("warn", message, context);
|
|
21
|
-
}
|
|
22
|
-
error(message, error, context) {
|
|
23
|
-
const errorContext = error instanceof Error ? {
|
|
24
|
-
error: {
|
|
25
|
-
message: error.message,
|
|
26
|
-
stack: error.stack
|
|
27
|
-
},
|
|
28
|
-
...context
|
|
29
|
-
} : {
|
|
30
|
-
error,
|
|
31
|
-
...context
|
|
32
|
-
};
|
|
33
|
-
this.log("error", message, errorContext);
|
|
34
|
-
}
|
|
35
|
-
log(level, message, context) {
|
|
36
|
-
if (!this.shouldLog(level)) return;
|
|
37
|
-
const logEntry = {
|
|
38
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
39
|
-
level: level.toUpperCase(),
|
|
40
|
-
message,
|
|
41
|
-
...context
|
|
42
|
-
};
|
|
43
|
-
this.getLogFunction(level)(JSON.stringify(logEntry));
|
|
44
|
-
}
|
|
45
|
-
shouldLog(level) {
|
|
46
|
-
const levels = [
|
|
47
|
-
"debug",
|
|
48
|
-
"info",
|
|
49
|
-
"warn",
|
|
50
|
-
"error"
|
|
51
|
-
];
|
|
52
|
-
return levels.indexOf(level) >= levels.indexOf(this.minLevel);
|
|
53
|
-
}
|
|
54
|
-
getLogFunction(level) {
|
|
55
|
-
switch (level) {
|
|
56
|
-
case "error": return console.error;
|
|
57
|
-
case "warn": return console.warn;
|
|
58
|
-
case "info": return console.info;
|
|
59
|
-
default: return console.debug;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
};
|
|
63
|
-
const logger = new Logger();
|
|
64
|
-
if (process.env.NODE_ENV === "development") logger.setLevel("debug");
|
|
65
|
-
|
|
66
|
-
//#endregion
|
|
67
|
-
//#region src/execution/context.ts
|
|
68
|
-
var WaitSignal = class extends Error {
|
|
69
|
-
constructor(type, reason, data) {
|
|
70
|
-
super(reason);
|
|
71
|
-
this.type = type;
|
|
72
|
-
this.reason = reason;
|
|
73
|
-
this.data = data;
|
|
74
|
-
this.name = "WaitSignal";
|
|
75
|
-
}
|
|
76
|
-
};
|
|
77
|
-
var StepContextImpl = class {
|
|
78
|
-
signal;
|
|
79
|
-
constructor(runId, stepId, context, input, attempt, run, repository, eventBus, signal) {
|
|
80
|
-
this.runId = runId;
|
|
81
|
-
this.stepId = stepId;
|
|
82
|
-
this.context = context;
|
|
83
|
-
this.input = input;
|
|
84
|
-
this.attempt = attempt;
|
|
85
|
-
this.run = run;
|
|
86
|
-
this.repository = repository;
|
|
87
|
-
this.eventBus = eventBus;
|
|
88
|
-
this.signal = signal ?? new AbortController().signal;
|
|
89
|
-
}
|
|
90
|
-
async set(key, value) {
|
|
91
|
-
if (this.signal.aborted) throw new Error(`Cannot update context: workflow ${this.runId} has been cancelled`);
|
|
92
|
-
this.context[key] = value;
|
|
93
|
-
this.run.context[key] = value;
|
|
94
|
-
if ((await this.repository.updateOne({
|
|
95
|
-
_id: this.runId,
|
|
96
|
-
status: { $ne: "cancelled" }
|
|
97
|
-
}, { $set: {
|
|
98
|
-
[`context.${String(key)}`]: value,
|
|
99
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
100
|
-
} }, { bypassTenant: true })).modifiedCount === 0) throw new Error(`Cannot update context: workflow ${this.runId} may have been cancelled`);
|
|
101
|
-
}
|
|
102
|
-
getOutput(stepId) {
|
|
103
|
-
return this.run.steps.find((s) => s.stepId === stepId)?.output;
|
|
104
|
-
}
|
|
105
|
-
async wait(reason, data) {
|
|
106
|
-
throw new WaitSignal("human", reason, data);
|
|
107
|
-
}
|
|
108
|
-
async waitFor(eventName, reason) {
|
|
109
|
-
throw new WaitSignal("event", reason || `Waiting for ${eventName}`, { eventName });
|
|
110
|
-
}
|
|
111
|
-
async sleep(ms) {
|
|
112
|
-
const resumeAt = new Date(Date.now() + ms);
|
|
113
|
-
throw new WaitSignal("timer", `Sleep ${ms}ms`, { resumeAt });
|
|
114
|
-
}
|
|
115
|
-
async heartbeat() {
|
|
116
|
-
if (this.signal.aborted) return;
|
|
117
|
-
try {
|
|
118
|
-
await this.repository.updateOne({
|
|
119
|
-
_id: this.runId,
|
|
120
|
-
status: { $ne: "cancelled" }
|
|
121
|
-
}, { lastHeartbeat: /* @__PURE__ */ new Date() }, { bypassTenant: true });
|
|
122
|
-
} catch {}
|
|
123
|
-
}
|
|
124
|
-
emit(eventName, data) {
|
|
125
|
-
this.eventBus.emit(eventName, {
|
|
126
|
-
runId: this.runId,
|
|
127
|
-
stepId: this.stepId,
|
|
128
|
-
data
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
log(message, data) {
|
|
132
|
-
logger.info(message, {
|
|
133
|
-
runId: this.runId,
|
|
134
|
-
stepId: this.stepId,
|
|
135
|
-
attempt: this.attempt,
|
|
136
|
-
...data !== void 0 && { data }
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
//#endregion
|
|
142
8
|
//#region src/core/status.ts
|
|
143
9
|
const STEP_STATUS_VALUES = [
|
|
144
10
|
"pending",
|
|
@@ -400,11 +266,12 @@ const CANCELLED_GUARD = { status: { $ne: "cancelled" } };
|
|
|
400
266
|
var StepExecutor = class {
|
|
401
267
|
/** Track active AbortControllers by runId for cancellation support */
|
|
402
268
|
activeControllers = /* @__PURE__ */ new Map();
|
|
403
|
-
constructor(registry, repository, eventBus, cache) {
|
|
269
|
+
constructor(registry, repository, eventBus, cache, signalStore) {
|
|
404
270
|
this.registry = registry;
|
|
405
271
|
this.repository = repository;
|
|
406
272
|
this.eventBus = eventBus;
|
|
407
273
|
this.cache = cache;
|
|
274
|
+
this.signalStore = signalStore;
|
|
408
275
|
}
|
|
409
276
|
/**
|
|
410
277
|
* Abort any in-flight step execution for a workflow.
|
|
@@ -566,7 +433,7 @@ var StepExecutor = class {
|
|
|
566
433
|
this.activeControllers.set(run._id, abortController);
|
|
567
434
|
try {
|
|
568
435
|
const stepState = run.steps.find((s) => s.stepId === stepId);
|
|
569
|
-
const ctx = new StepContextImpl(run._id, stepId, run.context, run.input, stepState.attempts, run, this.repository, this.eventBus, abortController.signal);
|
|
436
|
+
const ctx = new StepContextImpl(run._id, stepId, run.context, run.input, stepState.attempts, run, this.repository, this.eventBus, abortController.signal, this.signalStore);
|
|
570
437
|
const output = await this.executeWithTimeout(handler, ctx, step.timeout || this.registry.definition.defaults?.timeout, run._id, abortController);
|
|
571
438
|
run = await this.updateStepState(run, stepId, {
|
|
572
439
|
status: "done",
|
|
@@ -584,6 +451,11 @@ var StepExecutor = class {
|
|
|
584
451
|
return await this.moveToNextStep(run);
|
|
585
452
|
} catch (error) {
|
|
586
453
|
if (error instanceof WaitSignal) return await this.handleWait(run, stepId, error);
|
|
454
|
+
else if (error instanceof GotoSignal) try {
|
|
455
|
+
return await this.handleGoto(run, stepId, error.targetStepId);
|
|
456
|
+
} catch (gotoError) {
|
|
457
|
+
return await this.handleFailure(run, stepId, gotoError);
|
|
458
|
+
}
|
|
587
459
|
else if (error instanceof InvalidStateError) throw error;
|
|
588
460
|
else return await this.handleFailure(run, stepId, error);
|
|
589
461
|
} finally {
|
|
@@ -593,11 +465,22 @@ var StepExecutor = class {
|
|
|
593
465
|
async executeWithTimeout(handler, ctx, timeout, runId, abortController) {
|
|
594
466
|
let heartbeatTimer;
|
|
595
467
|
let timeoutHandle;
|
|
596
|
-
if (runId)
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
468
|
+
if (runId) {
|
|
469
|
+
let consecutiveHeartbeatFailures = 0;
|
|
470
|
+
heartbeatTimer = setInterval(async () => {
|
|
471
|
+
try {
|
|
472
|
+
await this.repository.updateOne({ _id: runId }, { lastHeartbeat: /* @__PURE__ */ new Date() }, { bypassTenant: true });
|
|
473
|
+
consecutiveHeartbeatFailures = 0;
|
|
474
|
+
} catch (error) {
|
|
475
|
+
consecutiveHeartbeatFailures++;
|
|
476
|
+
this.eventBus.emit("engine:error", {
|
|
477
|
+
runId,
|
|
478
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
479
|
+
context: consecutiveHeartbeatFailures >= 3 ? "heartbeat-critical" : "heartbeat-warning"
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
}, TIMING.HEARTBEAT_INTERVAL_MS);
|
|
483
|
+
}
|
|
601
484
|
const cleanup = () => {
|
|
602
485
|
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
603
486
|
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
@@ -793,6 +676,55 @@ var StepExecutor = class {
|
|
|
793
676
|
return run;
|
|
794
677
|
}
|
|
795
678
|
/**
|
|
679
|
+
* Handle a goto signal — jump execution to a target step.
|
|
680
|
+
* Marks the current step as done and sets currentStepId to the target.
|
|
681
|
+
*/
|
|
682
|
+
async handleGoto(run, fromStepId, targetStepId) {
|
|
683
|
+
if (!this.registry.getStep(targetStepId)) {
|
|
684
|
+
const availableSteps = this.registry.definition.steps.map((s) => s.id);
|
|
685
|
+
throw new StepNotFoundError(targetStepId, run.workflowId, availableSteps);
|
|
686
|
+
}
|
|
687
|
+
run = await this.updateStepState(run, fromStepId, {
|
|
688
|
+
status: "skipped",
|
|
689
|
+
endedAt: /* @__PURE__ */ new Date()
|
|
690
|
+
});
|
|
691
|
+
this.eventBus.emit("step:completed", {
|
|
692
|
+
runId: run._id,
|
|
693
|
+
stepId: fromStepId,
|
|
694
|
+
data: { goto: targetStepId }
|
|
695
|
+
});
|
|
696
|
+
const now = /* @__PURE__ */ new Date();
|
|
697
|
+
const targetIndex = this.registry.definition.steps.findIndex((s) => s.id === targetStepId);
|
|
698
|
+
const updates = {
|
|
699
|
+
currentStepId: targetStepId,
|
|
700
|
+
status: "running",
|
|
701
|
+
updatedAt: now
|
|
702
|
+
};
|
|
703
|
+
if (targetIndex !== -1) {
|
|
704
|
+
updates[`steps.${targetIndex}.status`] = "pending";
|
|
705
|
+
updates[`steps.${targetIndex}.attempts`] = 0;
|
|
706
|
+
const step = run.steps[targetIndex];
|
|
707
|
+
if (step) {
|
|
708
|
+
step.status = "pending";
|
|
709
|
+
step.attempts = 0;
|
|
710
|
+
step.output = void 0;
|
|
711
|
+
step.error = void 0;
|
|
712
|
+
step.startedAt = void 0;
|
|
713
|
+
step.endedAt = void 0;
|
|
714
|
+
step.waitingFor = void 0;
|
|
715
|
+
step.retryAfter = void 0;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
await this.repository.updateOne({
|
|
719
|
+
_id: run._id,
|
|
720
|
+
status: { $ne: "cancelled" }
|
|
721
|
+
}, { $set: updates }, { bypassTenant: true });
|
|
722
|
+
run.currentStepId = targetStepId;
|
|
723
|
+
run.status = "running";
|
|
724
|
+
run.updatedAt = now;
|
|
725
|
+
return run;
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
796
728
|
* Resume a waiting step with payload.
|
|
797
729
|
* Marks the step as done and continues to next step.
|
|
798
730
|
*
|
|
@@ -805,7 +737,7 @@ var StepExecutor = class {
|
|
|
805
737
|
const stepId = run.currentStepId;
|
|
806
738
|
const { step: stepState } = this.findStepOrThrow(run, stepId);
|
|
807
739
|
if (stepState.status !== "waiting") {
|
|
808
|
-
const { InvalidStateError } = await import("./errors-
|
|
740
|
+
const { InvalidStateError } = await import("./errors-Ba7ZziTN.mjs").then((n) => n.l);
|
|
809
741
|
throw new InvalidStateError("resume step", stepState.status, ["waiting"], {
|
|
810
742
|
runId: run._id,
|
|
811
743
|
stepId
|
|
@@ -906,7 +838,8 @@ const DEFAULT_SCHEDULER_CONFIG = {
|
|
|
906
838
|
adaptivePolling: true,
|
|
907
839
|
maxConsecutiveFailures: SCHEDULER.MAX_CONSECUTIVE_FAILURES,
|
|
908
840
|
staleCheckInterval: SCHEDULER.STALE_CHECK_INTERVAL_MS,
|
|
909
|
-
staleThreshold: TIMING.STALE_WORKFLOW_THRESHOLD_MS
|
|
841
|
+
staleThreshold: TIMING.STALE_WORKFLOW_THRESHOLD_MS,
|
|
842
|
+
maxConcurrentExecutions: Infinity
|
|
910
843
|
};
|
|
911
844
|
var SmartScheduler = class {
|
|
912
845
|
timers = /* @__PURE__ */ new Map();
|
|
@@ -1108,7 +1041,17 @@ var SmartScheduler = class {
|
|
|
1108
1041
|
const startTime = Date.now();
|
|
1109
1042
|
try {
|
|
1110
1043
|
const now = /* @__PURE__ */ new Date();
|
|
1111
|
-
|
|
1044
|
+
let limit = this.config.maxWorkflowsPerPoll;
|
|
1045
|
+
if (this.config.maxConcurrentExecutions !== Infinity) {
|
|
1046
|
+
const activeCount = (await this.repository.getRunningRuns()).length;
|
|
1047
|
+
if (activeCount >= this.config.maxConcurrentExecutions) {
|
|
1048
|
+
const duration = Date.now() - startTime;
|
|
1049
|
+
this.metrics.recordPoll(duration, true, 0);
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
const availableSlots = this.config.maxConcurrentExecutions - activeCount;
|
|
1053
|
+
limit = Math.min(limit, availableSlots);
|
|
1054
|
+
}
|
|
1112
1055
|
const waiting = await this.repository.getReadyToResume(now, limit);
|
|
1113
1056
|
const retrying = await this.repository.getReadyForRetry(now, limit);
|
|
1114
1057
|
let resumedCount = 0;
|
|
@@ -1126,17 +1069,6 @@ var SmartScheduler = class {
|
|
|
1126
1069
|
else logger.warn(`Retry workflow detected but no retry callback set`, { runId: run._id });
|
|
1127
1070
|
const scheduled = (await this.repository.getScheduledWorkflowsReadyToExecute(now, { limit })).docs || [];
|
|
1128
1071
|
for (const run of scheduled) if (this.retryCallback) try {
|
|
1129
|
-
if ((await this.repository.updateOne({
|
|
1130
|
-
_id: run._id,
|
|
1131
|
-
status: "draft",
|
|
1132
|
-
"scheduling.executionTime": { $lte: now },
|
|
1133
|
-
paused: { $ne: true }
|
|
1134
|
-
}, {
|
|
1135
|
-
status: "running",
|
|
1136
|
-
startedAt: now,
|
|
1137
|
-
updatedAt: now,
|
|
1138
|
-
lastHeartbeat: now
|
|
1139
|
-
}, { bypassTenant: true })).modifiedCount === 0) continue;
|
|
1140
1072
|
await this.retryCallback(run._id);
|
|
1141
1073
|
resumedCount++;
|
|
1142
1074
|
} catch (err) {
|
|
@@ -1389,15 +1321,39 @@ var HookRegistry = class {
|
|
|
1389
1321
|
/** Global hook registry instance */
|
|
1390
1322
|
const hookRegistry = new HookRegistry();
|
|
1391
1323
|
/**
|
|
1324
|
+
* Global registry mapping workflowId → engine.
|
|
1325
|
+
* Populated by createWorkflow(). Enables ctx.startChildWorkflow() to find
|
|
1326
|
+
* and start child workflows by ID without the caller needing a reference.
|
|
1327
|
+
*/
|
|
1328
|
+
var WorkflowRegistryGlobal = class {
|
|
1329
|
+
engines = /* @__PURE__ */ new Map();
|
|
1330
|
+
register(workflowId, engine) {
|
|
1331
|
+
this.engines.set(workflowId, new WeakRef(engine));
|
|
1332
|
+
}
|
|
1333
|
+
getEngine(workflowId) {
|
|
1334
|
+
const ref = this.engines.get(workflowId);
|
|
1335
|
+
if (!ref) return void 0;
|
|
1336
|
+
const engine = ref.deref();
|
|
1337
|
+
if (!engine) {
|
|
1338
|
+
this.engines.delete(workflowId);
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
return engine;
|
|
1342
|
+
}
|
|
1343
|
+
};
|
|
1344
|
+
const workflowRegistry = new WorkflowRegistryGlobal();
|
|
1345
|
+
/**
|
|
1392
1346
|
* Clean up all event listeners for a specific workflow
|
|
1393
1347
|
*/
|
|
1394
1348
|
function cleanupEventListeners(runId, listeners, eventBus) {
|
|
1395
1349
|
const prefix = `${runId}:`;
|
|
1396
|
-
const
|
|
1350
|
+
const signalPrefix = `signal:${runId}:`;
|
|
1351
|
+
const keysToRemove = Array.from(listeners.keys()).filter((key) => key.startsWith(prefix) || key.startsWith(signalPrefix));
|
|
1397
1352
|
for (const key of keysToRemove) {
|
|
1398
1353
|
const entry = listeners.get(key);
|
|
1399
1354
|
if (entry) {
|
|
1400
|
-
|
|
1355
|
+
if (key.startsWith("signal:")) entry.listener();
|
|
1356
|
+
else eventBus.off(entry.eventName, entry.listener);
|
|
1401
1357
|
listeners.delete(key);
|
|
1402
1358
|
}
|
|
1403
1359
|
}
|
|
@@ -1456,7 +1412,7 @@ var WorkflowEngine = class {
|
|
|
1456
1412
|
this.handlers = handlers;
|
|
1457
1413
|
this.container = container;
|
|
1458
1414
|
this.registry = new WorkflowRegistry(definition, handlers);
|
|
1459
|
-
this.executor = new StepExecutor(this.registry, container.repository, container.eventBus, container.cache);
|
|
1415
|
+
this.executor = new StepExecutor(this.registry, container.repository, container.eventBus, container.cache, container.signalStore);
|
|
1460
1416
|
this.eventListeners = /* @__PURE__ */ new Map();
|
|
1461
1417
|
const schedulerConfig = {
|
|
1462
1418
|
...DEFAULT_SCHEDULER_CONFIG,
|
|
@@ -1465,6 +1421,7 @@ var WorkflowEngine = class {
|
|
|
1465
1421
|
this.scheduler = new SmartScheduler(container.repository, async (runId) => {
|
|
1466
1422
|
await this.resume(runId);
|
|
1467
1423
|
}, schedulerConfig, container.eventBus);
|
|
1424
|
+
workflowRegistry.register(definition.id, this);
|
|
1468
1425
|
this.scheduler.setStaleRecoveryCallback(async (runId, thresholdMs) => {
|
|
1469
1426
|
return this.recoverStale(runId, thresholdMs);
|
|
1470
1427
|
});
|
|
@@ -1555,11 +1512,67 @@ var WorkflowEngine = class {
|
|
|
1555
1512
|
if (error instanceof InvalidStateError) {
|
|
1556
1513
|
const cancelled = await this.get(runId);
|
|
1557
1514
|
if (cancelled) run = cancelled;
|
|
1515
|
+
} else if (error instanceof StepNotFoundError) {
|
|
1516
|
+
const stepId = run.currentStepId;
|
|
1517
|
+
this.container.eventBus.emit("engine:error", {
|
|
1518
|
+
runId,
|
|
1519
|
+
error: /* @__PURE__ */ new Error(`Version mismatch: workflow "${run.workflowId}" run has currentStepId="${stepId}" but this engine (v${this.definition.version}) doesn't define it. Register the old version with workflowRegistry or migrate in-flight runs.`),
|
|
1520
|
+
context: "version-mismatch"
|
|
1521
|
+
});
|
|
1522
|
+
const now = /* @__PURE__ */ new Date();
|
|
1523
|
+
await this.container.repository.updateOne({ _id: runId }, { $set: {
|
|
1524
|
+
status: "failed",
|
|
1525
|
+
updatedAt: now,
|
|
1526
|
+
endedAt: now,
|
|
1527
|
+
error: {
|
|
1528
|
+
message: `Step "${stepId}" not found — code version changed while workflow was in-flight`,
|
|
1529
|
+
code: "VERSION_MISMATCH"
|
|
1530
|
+
}
|
|
1531
|
+
} }, { bypassTenant: true });
|
|
1532
|
+
run.status = "failed";
|
|
1533
|
+
run.endedAt = now;
|
|
1534
|
+
run.error = {
|
|
1535
|
+
message: `Step "${stepId}" not found — code version changed while workflow was in-flight`,
|
|
1536
|
+
code: "VERSION_MISMATCH"
|
|
1537
|
+
};
|
|
1558
1538
|
} else throw error;
|
|
1559
1539
|
}
|
|
1560
1540
|
if (isTerminalState(run.status)) {
|
|
1561
1541
|
cleanupEventListeners(runId, this.eventListeners, this.container.eventBus);
|
|
1562
1542
|
hookRegistry.unregister(runId);
|
|
1543
|
+
if (run.status === "failed" && this.options.compensationHandlers) run = await this.runCompensation(run);
|
|
1544
|
+
}
|
|
1545
|
+
return run;
|
|
1546
|
+
}
|
|
1547
|
+
/**
|
|
1548
|
+
* Run saga compensation handlers for completed steps in reverse order.
|
|
1549
|
+
* Called automatically when a workflow fails and compensation handlers are registered.
|
|
1550
|
+
*/
|
|
1551
|
+
async runCompensation(run) {
|
|
1552
|
+
const compensationHandlers = this.options.compensationHandlers;
|
|
1553
|
+
if (!compensationHandlers) return run;
|
|
1554
|
+
const completedSteps = run.steps.filter((s) => s.status === "done" && compensationHandlers[s.stepId]).reverse();
|
|
1555
|
+
if (completedSteps.length === 0) return run;
|
|
1556
|
+
this.container.eventBus.emit("workflow:compensating", {
|
|
1557
|
+
runId: run._id,
|
|
1558
|
+
data: { steps: completedSteps.map((s) => s.stepId) }
|
|
1559
|
+
});
|
|
1560
|
+
for (const stepState of completedSteps) {
|
|
1561
|
+
const handler = compensationHandlers[stepState.stepId];
|
|
1562
|
+
if (!handler) continue;
|
|
1563
|
+
try {
|
|
1564
|
+
await handler(new (await (import("./context-DMkusPr_.mjs").then((n) => n.i))).StepContextImpl(run._id, stepState.stepId, run.context, run.input, stepState.attempts, run, this.container.repository, this.container.eventBus));
|
|
1565
|
+
this.container.eventBus.emit("step:compensated", {
|
|
1566
|
+
runId: run._id,
|
|
1567
|
+
stepId: stepState.stepId
|
|
1568
|
+
});
|
|
1569
|
+
} catch (err) {
|
|
1570
|
+
this.container.eventBus.emit("engine:error", {
|
|
1571
|
+
runId: run._id,
|
|
1572
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
1573
|
+
context: `compensation-${stepState.stepId}`
|
|
1574
|
+
});
|
|
1575
|
+
}
|
|
1563
1576
|
}
|
|
1564
1577
|
return run;
|
|
1565
1578
|
}
|
|
@@ -1584,6 +1597,9 @@ var WorkflowEngine = class {
|
|
|
1584
1597
|
checkNoProgress(run, prevStepId, prevStepStatus) {
|
|
1585
1598
|
const currentStep = run.steps.find((s) => s.stepId === run.currentStepId);
|
|
1586
1599
|
const isRetryPending = currentStep?.status === "pending" && currentStep?.retryAfter;
|
|
1600
|
+
if (prevStepId) {
|
|
1601
|
+
if (run.steps.find((s) => s.stepId === prevStepId)?.status === "skipped") return false;
|
|
1602
|
+
}
|
|
1587
1603
|
return run.currentStepId === prevStepId && currentStep?.status === prevStepStatus && !isRetryPending;
|
|
1588
1604
|
}
|
|
1589
1605
|
/**
|
|
@@ -1612,6 +1628,46 @@ var WorkflowEngine = class {
|
|
|
1612
1628
|
}
|
|
1613
1629
|
return false;
|
|
1614
1630
|
}
|
|
1631
|
+
if (stepState.waitingFor?.type === "childWorkflow") {
|
|
1632
|
+
const data = stepState.waitingFor.data;
|
|
1633
|
+
if (data?.childWorkflowId && !data.childRunId) {
|
|
1634
|
+
const childEngine = workflowRegistry.getEngine(data.childWorkflowId);
|
|
1635
|
+
if (childEngine) {
|
|
1636
|
+
const childRun = await childEngine.start(data.childInput);
|
|
1637
|
+
const stepIndex = run.steps.findIndex((s) => s.stepId === run.currentStepId);
|
|
1638
|
+
if (stepIndex !== -1) await this.container.repository.updateOne({ _id: runId }, { $set: { [`steps.${stepIndex}.waitingFor.data.childRunId`]: childRun._id } }, { bypassTenant: true });
|
|
1639
|
+
const childCompletionHandler = async (payload) => {
|
|
1640
|
+
if (!payload.runId || payload.runId !== childRun._id) return;
|
|
1641
|
+
try {
|
|
1642
|
+
const completedChild = await childEngine.get(childRun._id);
|
|
1643
|
+
const output = completedChild?.output ?? completedChild?.context;
|
|
1644
|
+
await this.resume(runId, output);
|
|
1645
|
+
} catch {}
|
|
1646
|
+
this.container.eventBus.off("workflow:completed", childCompletionHandler);
|
|
1647
|
+
this.container.eventBus.off("workflow:failed", childFailHandler);
|
|
1648
|
+
};
|
|
1649
|
+
const childFailHandler = async (payload) => {
|
|
1650
|
+
if (!payload.runId || payload.runId !== childRun._id) return;
|
|
1651
|
+
try {
|
|
1652
|
+
const failedChild = await childEngine.get(childRun._id);
|
|
1653
|
+
await this.resume(runId, {
|
|
1654
|
+
__childFailed: true,
|
|
1655
|
+
error: failedChild?.error
|
|
1656
|
+
});
|
|
1657
|
+
} catch {}
|
|
1658
|
+
this.container.eventBus.off("workflow:completed", childCompletionHandler);
|
|
1659
|
+
this.container.eventBus.off("workflow:failed", childFailHandler);
|
|
1660
|
+
};
|
|
1661
|
+
this.container.eventBus.on("workflow:completed", childCompletionHandler);
|
|
1662
|
+
this.container.eventBus.on("workflow:failed", childFailHandler);
|
|
1663
|
+
} else this.container.eventBus.emit("engine:error", {
|
|
1664
|
+
runId,
|
|
1665
|
+
error: /* @__PURE__ */ new Error(`Child workflow '${data.childWorkflowId}' not registered. Ensure the child workflow is created with createWorkflow() before the parent starts. Or resume the parent manually when the child completes.`),
|
|
1666
|
+
context: "child-workflow-not-found"
|
|
1667
|
+
});
|
|
1668
|
+
}
|
|
1669
|
+
return false;
|
|
1670
|
+
}
|
|
1615
1671
|
return false;
|
|
1616
1672
|
}
|
|
1617
1673
|
findCurrentStep(run) {
|
|
@@ -1684,6 +1740,28 @@ var WorkflowEngine = class {
|
|
|
1684
1740
|
listener,
|
|
1685
1741
|
eventName
|
|
1686
1742
|
});
|
|
1743
|
+
const signalUnsub = this.container.signalStore.subscribe(`streamline:event:${eventName}`, (data) => {
|
|
1744
|
+
const signalPayload = data;
|
|
1745
|
+
if (!signalPayload || signalPayload.runId === runId || signalPayload.broadcast) listener(signalPayload);
|
|
1746
|
+
});
|
|
1747
|
+
const signalKey = `signal:${listenerKey}`;
|
|
1748
|
+
const currentEntry = this.eventListeners.get(listenerKey);
|
|
1749
|
+
if (currentEntry) {
|
|
1750
|
+
const origListener = currentEntry.listener;
|
|
1751
|
+
this.eventListeners.set(listenerKey, {
|
|
1752
|
+
...currentEntry,
|
|
1753
|
+
listener: ((...args) => {
|
|
1754
|
+
origListener(...args);
|
|
1755
|
+
})
|
|
1756
|
+
});
|
|
1757
|
+
this.eventListeners.set(signalKey, {
|
|
1758
|
+
listener: (() => {
|
|
1759
|
+
if (typeof signalUnsub === "function") signalUnsub();
|
|
1760
|
+
else if (signalUnsub instanceof Promise) signalUnsub.then((fn) => fn());
|
|
1761
|
+
}),
|
|
1762
|
+
eventName: signalKey
|
|
1763
|
+
});
|
|
1764
|
+
}
|
|
1687
1765
|
}
|
|
1688
1766
|
/**
|
|
1689
1767
|
* Handle retry backoff wait with inline execution for short delays
|
|
@@ -1716,9 +1794,15 @@ var WorkflowEngine = class {
|
|
|
1716
1794
|
async resume(runId, payload) {
|
|
1717
1795
|
const run = await this.getOrThrow(runId);
|
|
1718
1796
|
if (run.paused) {
|
|
1797
|
+
if ((await this.container.repository.updateOne({
|
|
1798
|
+
_id: runId,
|
|
1799
|
+
paused: true
|
|
1800
|
+
}, { $set: {
|
|
1801
|
+
paused: false,
|
|
1802
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1803
|
+
} }, { bypassTenant: true })).modifiedCount === 0) return await this.getOrThrow(runId);
|
|
1719
1804
|
run.paused = false;
|
|
1720
1805
|
run.updatedAt = /* @__PURE__ */ new Date();
|
|
1721
|
-
await this.container.repository.update(runId, run, { bypassTenant: true });
|
|
1722
1806
|
this.container.cache.set(run);
|
|
1723
1807
|
}
|
|
1724
1808
|
if (run.status === "waiting") return this.resumeWaitingWorkflow(run, payload);
|
|
@@ -1757,6 +1841,15 @@ var WorkflowEngine = class {
|
|
|
1757
1841
|
updatedAt: /* @__PURE__ */ new Date()
|
|
1758
1842
|
}, { bypassTenant: true })).modifiedCount === 0) return null;
|
|
1759
1843
|
this.container.cache.delete(runId);
|
|
1844
|
+
const run = await this.get(runId);
|
|
1845
|
+
if (run?.currentStepId) {
|
|
1846
|
+
const stepIndex = run.steps.findIndex((s) => s.stepId === run.currentStepId);
|
|
1847
|
+
if (stepIndex !== -1 && run.steps[stepIndex]?.status === "running") await this.container.repository.updateOne({ _id: runId }, { $set: {
|
|
1848
|
+
[`steps.${stepIndex}.status`]: "pending",
|
|
1849
|
+
[`steps.${stepIndex}.startedAt`]: void 0
|
|
1850
|
+
} }, { bypassTenant: true });
|
|
1851
|
+
}
|
|
1852
|
+
this.container.cache.delete(runId);
|
|
1760
1853
|
this.container.eventBus.emit("workflow:recovered", { runId });
|
|
1761
1854
|
return this.execute(runId);
|
|
1762
1855
|
}
|
|
@@ -2135,7 +2228,11 @@ const WorkflowRunSchema = new Schema({
|
|
|
2135
2228
|
meta: Schema.Types.Mixed
|
|
2136
2229
|
}, {
|
|
2137
2230
|
collection: "workflow_runs",
|
|
2138
|
-
timestamps: false
|
|
2231
|
+
timestamps: false,
|
|
2232
|
+
writeConcern: {
|
|
2233
|
+
w: "majority",
|
|
2234
|
+
j: true
|
|
2235
|
+
}
|
|
2139
2236
|
});
|
|
2140
2237
|
WorkflowRunSchema.index({
|
|
2141
2238
|
workflowId: 1,
|
|
@@ -2169,6 +2266,18 @@ WorkflowRunSchema.index({
|
|
|
2169
2266
|
"scheduling.executionTime": 1,
|
|
2170
2267
|
paused: 1
|
|
2171
2268
|
});
|
|
2269
|
+
WorkflowRunSchema.index({
|
|
2270
|
+
status: 1,
|
|
2271
|
+
paused: 1,
|
|
2272
|
+
updatedAt: -1,
|
|
2273
|
+
_id: -1
|
|
2274
|
+
});
|
|
2275
|
+
WorkflowRunSchema.index({
|
|
2276
|
+
status: 1,
|
|
2277
|
+
paused: 1,
|
|
2278
|
+
updatedAt: 1,
|
|
2279
|
+
_id: 1
|
|
2280
|
+
});
|
|
2172
2281
|
/**
|
|
2173
2282
|
* MULTI-TENANCY & SCHEDULED WORKFLOWS - COMPOSITE INDEXES
|
|
2174
2283
|
*
|
|
@@ -2637,6 +2746,24 @@ const workflowRunRepository = createWorkflowRepository();
|
|
|
2637
2746
|
* All shared dependencies are passed through this container.
|
|
2638
2747
|
*/
|
|
2639
2748
|
/**
|
|
2749
|
+
* Default in-memory signal store (process-local).
|
|
2750
|
+
* Sufficient for single-worker deployments and testing.
|
|
2751
|
+
*/
|
|
2752
|
+
var InMemorySignalStore = class {
|
|
2753
|
+
listeners = /* @__PURE__ */ new Map();
|
|
2754
|
+
publish(channel, data) {
|
|
2755
|
+
const handlers = this.listeners.get(channel);
|
|
2756
|
+
if (handlers) for (const handler of handlers) handler(data);
|
|
2757
|
+
}
|
|
2758
|
+
subscribe(channel, handler) {
|
|
2759
|
+
if (!this.listeners.has(channel)) this.listeners.set(channel, /* @__PURE__ */ new Set());
|
|
2760
|
+
this.listeners.get(channel).add(handler);
|
|
2761
|
+
return () => {
|
|
2762
|
+
this.listeners.get(channel)?.delete(handler);
|
|
2763
|
+
};
|
|
2764
|
+
}
|
|
2765
|
+
};
|
|
2766
|
+
/**
|
|
2640
2767
|
* Create a new container with configurable dependencies.
|
|
2641
2768
|
*
|
|
2642
2769
|
* @param options - Optional configuration for container dependencies
|
|
@@ -2678,10 +2805,12 @@ function createContainer(options = {}) {
|
|
|
2678
2805
|
else if (options.eventBus instanceof WorkflowEventBus) eventBus = options.eventBus;
|
|
2679
2806
|
else eventBus = new WorkflowEventBus();
|
|
2680
2807
|
const cache = options.cache ?? new WorkflowCache();
|
|
2808
|
+
const signalStore = options.signalStore ?? new InMemorySignalStore();
|
|
2681
2809
|
return {
|
|
2682
2810
|
repository,
|
|
2683
2811
|
eventBus,
|
|
2684
|
-
cache
|
|
2812
|
+
cache,
|
|
2813
|
+
signalStore
|
|
2685
2814
|
};
|
|
2686
2815
|
}
|
|
2687
2816
|
/**
|
|
@@ -2694,4 +2823,4 @@ function isStreamlineContainer(obj) {
|
|
|
2694
2823
|
}
|
|
2695
2824
|
|
|
2696
2825
|
//#endregion
|
|
2697
|
-
export {
|
|
2826
|
+
export { isValidRunTransition as A, shouldSkipStep as C, isRunStatus as D, deriveRunStatus as E, isStepStatus as O, isConditionalStep as S, STEP_STATUS_VALUES as T, validateRetryConfig as _, singleTenantPlugin as a, conditions as b, RUN_STATUS as c, WorkflowRunModel as d, WorkflowCache as f, validateId as g, workflowRegistry as h, workflowRunRepository as i, isValidStepTransition as j, isTerminalState as k, STEP_STATUS as l, hookRegistry as m, isStreamlineContainer as n, tenantFilterPlugin as o, WorkflowEngine as p, createWorkflowRepository as r, CommonQueries as s, createContainer as t, WorkflowQueryBuilder as u, COMPUTED as v, RUN_STATUS_VALUES as w, createCondition as x, SCHEDULING as y };
|