UNPKG

durable-execution

Version:

A durable task engine for running tasks durably and resiliently

401 lines (394 loc) 12.2 kB
// src/task-internal.ts import { z } from "zod"; // src/cancel.ts import { getErrorMessage } from "@gpahal/std/errors"; // src/errors.ts import { CustomError } from "ts-custom-error"; var DurableExecutionError = class extends CustomError { /** * Whether the error is retryable. */ isRetryable; /** * @param message - The error message. * @param isRetryable - Whether the error is retryable. */ constructor(message, isRetryable = true) { super(message); this.isRetryable = isRetryable; } getErrorType() { return "generic"; } }; var DurableExecutionTimedOutError = class extends DurableExecutionError { /** * @param message - The error message. */ constructor(message, isRetryable = true) { super(message ?? "Task timed out", isRetryable); } getErrorType() { return "timed_out"; } }; var DurableExecutionCancelledError = class extends DurableExecutionError { /** * @param message - The error message. */ constructor(message) { super(message ?? "Task cancelled", false); } getErrorType() { return "cancelled"; } }; // src/logger.ts function createConsoleLogger(name) { return { debug: (message) => console.debug(`DEBUG [${name}] ${message}`), info: (message) => console.info(`INFO [${name}] ${message}`), error: (message, error) => console.error(`ERROR [${name}] ${message}`, error) }; } // src/cancel.ts function createCancelSignal({ abortSignal, logger } = {}) { logger = logger ?? createConsoleLogger("CancelSignal"); let isCancelled = abortSignal?.aborted ?? false; const subscribers = /* @__PURE__ */ new Set(); const cancel = () => { if (abortSignal) { abortSignal.removeEventListener("abort", cancel); } if (isCancelled) { return; } isCancelled = true; for (const fn of subscribers) { try { fn(); } catch (error) { logger.error(`Error in cancel signal subscriber: ${getErrorMessage(error)}`); } } subscribers.clear(); }; if (abortSignal) { abortSignal.addEventListener("abort", cancel); } const cancelSignal = { onCancelled: (fn) => { if (isCancelled) { fn(); } else { subscribers.add(fn); } }, clearOnCancelled: (fn) => { subscribers.delete(fn); }, isCancelled: () => isCancelled }; return [cancelSignal, cancel]; } function createTimeoutCancelSignal(timeoutMs, { logger } = {}) { const [cancelSignal, cancel] = createCancelSignal({ logger }); setTimeout(() => cancel(), timeoutMs); return cancelSignal; } function createCancellablePromise(promise, signal, cancelledError) { if (!signal) { return promise; } const getCancelledError = () => { if (!cancelledError) { return new DurableExecutionCancelledError(); } if (cancelledError instanceof DurableExecutionError) { return cancelledError; } return new DurableExecutionCancelledError(getErrorMessage(cancelledError)); }; return new Promise((resolve, reject) => { if (signal.isCancelled()) { reject(getCancelledError()); return; } const onCancelled = () => { reject(getCancelledError()); }; signal.onCancelled(onCancelled); promise.then(resolve, reject).finally(() => { signal.clearOnCancelled(onCancelled); }); }); } // src/task.ts function isDurableFinalizeTaskOptionsTaskOptions(options) { return "run" in options && !("runParent" in options); } function isDurableFinalizeTaskOptionsParentTaskOptions(options) { return "runParent" in options && !("run" in options); } // src/utils.ts import { getDotPath } from "@standard-schema/utils"; import { customAlphabet } from "nanoid"; import { sleep } from "@gpahal/std/promises"; var _ALPHABET = "0123456789ABCDEFGHJKMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz"; var generateId = customAlphabet(_ALPHABET, 24); // src/task-internal.ts function convertDurableTaskOptionsOptionsInternal(taskOptions, validateInputFn) { return { id: taskOptions.id, retryOptions: taskOptions.retryOptions, sleepMsBeforeRun: taskOptions.sleepMsBeforeRun, timeoutMs: taskOptions.timeoutMs, validateInputFn, disableChildrenTasksOutputsInOutput: true, runParent: async (ctx, input) => { const output = await taskOptions.run(ctx, input); return { output, childrenTasks: [] }; }, finalizeTask: void 0 }; } function convertDurableParentTaskOptionsOptionsInternal(taskOptions, validateInputFn) { return { id: taskOptions.id, retryOptions: taskOptions.retryOptions, sleepMsBeforeRun: taskOptions.sleepMsBeforeRun, timeoutMs: taskOptions.timeoutMs, validateInputFn, disableChildrenTasksOutputsInOutput: false, runParent: async (ctx, input) => { const runParentOutput = await taskOptions.runParent(ctx, input); return { output: runParentOutput.output, childrenTasks: runParentOutput.childrenTasks ?? [] }; }, finalizeTask: taskOptions.finalizeTask ? isDurableFinalizeTaskOptionsParentTaskOptions(taskOptions.finalizeTask) ? convertDurableParentTaskOptionsOptionsInternal( taskOptions.finalizeTask, void 0 ) : isDurableFinalizeTaskOptionsTaskOptions(taskOptions.finalizeTask) ? convertDurableTaskOptionsOptionsInternal( taskOptions.finalizeTask, void 0 ) : void 0 : void 0 }; } var zRetryOptions = z.object({ maxAttempts: z.number().int().min(0), baseDelayMs: z.number().int().min(0).nullish().transform((val) => { if (val == null) { return void 0; } return val; }), delayMultiplier: z.number().min(0.1).nullish().transform((val) => { if (val == null) { return void 0; } return val; }), maxDelayMs: z.number().int().min(0).nullish().transform((val) => { if (val == null) { return void 0; } return val; }) }).nullish().transform((val) => { if (val == null) { return { maxAttempts: 0 }; } return val; }); var zSleepMsBeforeRun = z.number().int().nullish().transform((val) => { if (val == null || val <= 0) { return 0; } return val; }); var zTimeoutMs = z.number().int().min(1); var DurableTaskInternal = class _DurableTaskInternal { taskInternalsMap; id; retryOptions; sleepMsBeforeRun; timeoutMs; validateInputFn; disableChildrenTasksOutputsInOutput; runParent; finalizeTask; constructor(taskInternalsMap, id, retryOptions, sleepMsBeforeRun, timeoutMs, validateInputFn, disableChildrenTasksOutputsInOutput, runParent, finalizeTask) { this.taskInternalsMap = taskInternalsMap; this.id = id; this.retryOptions = retryOptions; this.sleepMsBeforeRun = sleepMsBeforeRun; this.timeoutMs = timeoutMs; this.validateInputFn = validateInputFn; this.disableChildrenTasksOutputsInOutput = disableChildrenTasksOutputsInOutput; this.runParent = runParent; this.finalizeTask = finalizeTask; } static fromDurableTaskOptionsInternal(taskInternalsMap, taskOptions) { validateTaskId(taskOptions.id); if (taskInternalsMap.has(taskOptions.id)) { throw new DurableExecutionError( `Task ${taskOptions.id} already exists. Use unique ids for tasks`, false ); } const parsedRetryOptions = zRetryOptions.safeParse(taskOptions.retryOptions); if (!parsedRetryOptions.success) { throw new DurableExecutionError( `Invalid retry options for task ${taskOptions.id}: ${z.prettifyError(parsedRetryOptions.error)}`, false ); } const parsedSleepMsBeforeRun = zSleepMsBeforeRun.safeParse(taskOptions.sleepMsBeforeRun); if (!parsedSleepMsBeforeRun.success) { throw new DurableExecutionError( `Invalid sleep ms before run for task ${taskOptions.id}: ${z.prettifyError(parsedSleepMsBeforeRun.error)}`, false ); } const parsedTimeoutMs = zTimeoutMs.safeParse(taskOptions.timeoutMs); if (!parsedTimeoutMs.success) { throw new DurableExecutionError( `Invalid timeout value for task ${taskOptions.id}: ${z.prettifyError(parsedTimeoutMs.error)}`, false ); } const finalizeTask = taskOptions.finalizeTask ? _DurableTaskInternal.fromDurableTaskOptionsInternal( taskInternalsMap, taskOptions.finalizeTask ) : void 0; const taskInternal = new _DurableTaskInternal( taskInternalsMap, taskOptions.id, parsedRetryOptions.data, parsedSleepMsBeforeRun.data, parsedTimeoutMs.data, taskOptions.validateInputFn, taskOptions.disableChildrenTasksOutputsInOutput, taskOptions.runParent, finalizeTask ); taskInternalsMap.set(taskInternal.id, taskInternal); return taskInternal; } static fromDurableTaskOptions(taskInternalsMap, taskOptions, validateInputFn) { return _DurableTaskInternal.fromDurableTaskOptionsInternal( taskInternalsMap, convertDurableTaskOptionsOptionsInternal( taskOptions, validateInputFn ) ); } static fromDurableParentTaskOptions(taskInternalsMap, taskOptions, validateInputFn) { return _DurableTaskInternal.fromDurableTaskOptionsInternal( taskInternalsMap, convertDurableParentTaskOptionsOptionsInternal( taskOptions, validateInputFn ) ); } validateEnqueueOptions(options) { const validatedOptions = {}; if (options?.retryOptions) { const parsedRetryOptions = zRetryOptions.safeParse(options.retryOptions); if (!parsedRetryOptions.success) { throw new DurableExecutionError( `Invalid retry options for task ${this.id}: ${z.prettifyError(parsedRetryOptions.error)}`, false ); } validatedOptions.retryOptions = parsedRetryOptions.data; } if (options?.sleepMsBeforeRun) { const parsedSleepMsBeforeRun = zSleepMsBeforeRun.safeParse(options.sleepMsBeforeRun); if (!parsedSleepMsBeforeRun.success) { throw new DurableExecutionError( `Invalid sleep ms before run for task ${this.id}: ${z.prettifyError(parsedSleepMsBeforeRun.error)}`, false ); } validatedOptions.sleepMsBeforeRun = parsedSleepMsBeforeRun.data; } if (options?.timeoutMs) { const parsedTimeoutMs = zTimeoutMs.safeParse(options.timeoutMs); if (!parsedTimeoutMs.success) { throw new DurableExecutionError( `Invalid timeout value for task ${this.id}: ${z.prettifyError(parsedTimeoutMs.error)}`, false ); } validatedOptions.timeoutMs = parsedTimeoutMs.data; } return validatedOptions; } getRetryOptions(options) { return options?.retryOptions != null ? options.retryOptions : this.retryOptions; } getSleepMsBeforeRun(options) { return options?.sleepMsBeforeRun != null ? options.sleepMsBeforeRun : this.sleepMsBeforeRun; } getTimeoutMs(options) { return options?.timeoutMs != null ? options.timeoutMs : this.timeoutMs; } async validateInput(input) { if (!this.validateInputFn) { return input; } return this.validateInputFn(this.id, input); } async runParentWithTimeoutAndCancellation(ctx, input, timeoutMs, cancelSignal) { const timeoutCancelSignal = createTimeoutCancelSignal(timeoutMs); return await createCancellablePromise( createCancellablePromise( this.runParent(ctx, input), timeoutCancelSignal, new DurableExecutionTimedOutError() ), cancelSignal ); } }; function generateTaskId() { return `t_${generateId(24)}`; } function generateTaskExecutionId() { return `te_${generateId(24)}`; } var _TASK_ID_REGEX = /^\w+$/; function validateTaskId(id) { if (id.length === 0) { throw new DurableExecutionError("Id cannot be empty", false); } if (id.length > 255) { throw new DurableExecutionError("Id cannot be longer than 255 characters", false); } if (!_TASK_ID_REGEX.test(id)) { throw new DurableExecutionError( "Id can only contain alphanumeric characters and underscores", false ); } } export { DurableTaskInternal, generateTaskExecutionId, generateTaskId, validateTaskId }; //# sourceMappingURL=task-internal.js.map