durabull
Version:
A durable workflow engine built on top of BullMQ and Redis
165 lines (164 loc) • 7.19 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ReplayEngine = void 0;
const errors_1 = require("../errors");
const context_1 = require("./context");
const decorators_1 = require("../decorators");
const isPromiseLike = (value) => {
return Boolean(value) && typeof value.then === 'function';
};
class ReplayEngine {
static async run(options) {
const { workflowId, workflow, record, isResume, signals, onStep } = options;
let { history } = options;
const context = {
workflowId,
workflow,
record,
history,
isResume,
clockCursor: 0,
timerCursor: 0,
sideEffectCursor: 0,
activityCursor: 0,
childWorkflowCursor: 0,
};
const signalMethods = (0, decorators_1.getSignalMethods)(workflow.constructor);
const sortedSignals = [...signals].sort((a, b) => a.ts - b.ts);
const signalCursor = { ...(record.signalCursor ?? {}) };
let signalCursorDirty = false;
const replaySignals = (index, opts) => {
if (signalMethods.length === 0 || signals.length === 0) {
return;
}
const key = index.toString();
if (!opts.initial && !opts.log) {
return;
}
let lowerBound = signalCursor[key] ?? Number.NEGATIVE_INFINITY;
if (opts.log) {
lowerBound = Math.max(lowerBound, opts.log.ts ?? Number.NEGATIVE_INFINITY);
}
const upperBound = opts.nextLog ? opts.nextLog.ts : Number.POSITIVE_INFINITY;
const toReplay = sortedSignals.filter((signal) => signal.ts > lowerBound && signal.ts <= upperBound);
for (const signal of toReplay) {
if (signalMethods.includes(signal.name)) {
const argsForSignal = Array.isArray(signal.payload)
? signal.payload
: [signal.payload];
workflow[signal.name](...argsForSignal);
}
}
if (toReplay.length > 0) {
signalCursor[key] = toReplay[toReplay.length - 1].ts;
signalCursorDirty = true;
}
};
let currentIndex = history.cursor ?? 0;
const effectiveArgs = record.args ?? [];
const getLogsForIndex = async (index) => {
if (options.getHistory) {
history = await options.getHistory();
// CRITICAL: Update context.history so ActivityStub checks against latest history
context.history = history;
}
return {
log: history.events[index],
nextLog: history.events[index + 1],
};
};
const initialLogs = await getLogsForIndex(currentIndex);
replaySignals(currentIndex, { initial: true, ...initialLogs });
let generator;
let result;
try {
await (0, context_1.runInWorkflowContext)(context, async () => {
generator = workflow.execute(...effectiveArgs);
result = await generator.next();
});
if (typeof result === 'undefined') {
return {
workflow,
error: new Error('Workflow did not yield a result before replay loop.'),
status: 'failed',
signalCursor: signalCursorDirty ? signalCursor : undefined
};
}
while (!result.done) {
let { log, nextLog } = await getLogsForIndex(currentIndex);
while (log && log.type === 'timer-fired') {
currentIndex++;
({ log, nextLog } = await getLogsForIndex(currentIndex));
}
replaySignals(currentIndex, { initial: false, log, nextLog });
if (isPromiseLike(result.value)) {
try {
const resolved = await (0, context_1.runInWorkflowContext)(context, async () => await result.value);
currentIndex++;
if (onStep) {
history.cursor = currentIndex;
await onStep(currentIndex, history);
}
result = await (0, context_1.runInWorkflowContext)(context, async () => await generator.next(resolved));
}
catch (error) {
if (error instanceof errors_1.WorkflowWaitError || error.name === 'WorkflowWaitError') {
return {
workflow,
status: 'waiting',
signalCursor: signalCursorDirty ? signalCursor : undefined
};
}
if (error instanceof errors_1.WorkflowContinueAsNewError || error.name === 'WorkflowContinueAsNewError') {
return {
workflow,
status: 'continued',
newWorkflowId: error.workflowId,
signalCursor: signalCursorDirty ? signalCursor : undefined
};
}
result = await (0, context_1.runInWorkflowContext)(context, async () => await generator.throw(error));
}
}
else {
currentIndex++;
if (onStep) {
history.cursor = currentIndex;
await onStep(currentIndex, history);
}
result = await (0, context_1.runInWorkflowContext)(context, async () => await generator.next(result.value));
}
}
return {
workflow,
result: result.value,
status: 'completed',
signalCursor: signalCursorDirty ? signalCursor : undefined
};
}
catch (error) {
if (error instanceof errors_1.WorkflowWaitError || error.name === 'WorkflowWaitError') {
return {
workflow,
status: 'waiting',
signalCursor: signalCursorDirty ? signalCursor : undefined
};
}
if (error instanceof errors_1.WorkflowContinueAsNewError || error.name === 'WorkflowContinueAsNewError') {
return {
workflow,
status: 'continued',
newWorkflowId: error.workflowId,
signalCursor: signalCursorDirty ? signalCursor : undefined
};
}
return {
workflow,
error,
status: 'failed',
signalCursor: signalCursorDirty ? signalCursor : undefined
};
}
}
}
exports.ReplayEngine = ReplayEngine;