UNPKG

durabull

Version:

A durable workflow engine built on top of BullMQ and Redis

165 lines (164 loc) 7.19 kB
"use strict"; 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;