durable-execution
Version:
A durable task engine for running tasks durably and resiliently
401 lines (394 loc) • 12.2 kB
JavaScript
// 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