inngest
Version:
Official SDK for Inngest.com. Inngest is the reliability layer for modern applications. Inngest combines durable execution, events, and queues into a zero-infra platform with built-in observability.
428 lines • 21.7 kB
JavaScript
"use strict";
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports._internals = exports.V0InngestExecution = exports.createV0InngestExecution = void 0;
const canonicalize_1 = __importDefault(require("canonicalize"));
const hash_js_1 = require("hash.js");
const zod_1 = require("zod");
const errors_js_1 = require("../../helpers/errors.js");
const functions_js_1 = require("../../helpers/functions.js");
const promises_js_1 = require("../../helpers/promises.js");
const types_js_1 = require("../../types.js");
const InngestMiddleware_js_1 = require("../InngestMiddleware.js");
const InngestStepTools_js_1 = require("../InngestStepTools.js");
const NonRetriableError_js_1 = require("../NonRetriableError.js");
const RetryAfterError_js_1 = require("../RetryAfterError.js");
const InngestExecution_js_1 = require("./InngestExecution.js");
const createV0InngestExecution = (options) => {
return new V0InngestExecution(options);
};
exports.createV0InngestExecution = createV0InngestExecution;
class V0InngestExecution extends InngestExecution_js_1.InngestExecution {
constructor(options) {
super(options);
this.userFnToRun = this.getUserFnToRun();
this.state = this.createExecutionState();
this.fnArg = this.createFnArg();
}
start() {
var _a;
this.debug("starting V0 execution");
return ((_a = this.execution) !== null && _a !== void 0 ? _a : (this.execution = this._start().then((result) => {
this.debug("result:", result);
return result;
})));
}
async _start() {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x;
this.state.hooks = await this.initializeMiddleware();
try {
await this.transformInput();
await ((_b = (_a = this.state.hooks).beforeMemoization) === null || _b === void 0 ? void 0 : _b.call(_a));
if (this.state.opStack.length === 0 && !this.options.requestedRunStep) {
await ((_d = (_c = this.state.hooks).afterMemoization) === null || _d === void 0 ? void 0 : _d.call(_c));
await ((_f = (_e = this.state.hooks).beforeExecution) === null || _f === void 0 ? void 0 : _f.call(_e));
}
const userFnPromise = (0, promises_js_1.runAsPromise)(() => this.userFnToRun(this.fnArg));
let pos = -1;
do {
if (pos >= 0) {
if (!this.options.requestedRunStep &&
pos === this.state.opStack.length - 1) {
await ((_h = (_g = this.state.hooks).afterMemoization) === null || _h === void 0 ? void 0 : _h.call(_g));
await ((_k = (_j = this.state.hooks).beforeExecution) === null || _k === void 0 ? void 0 : _k.call(_j));
}
this.state.tickOps = {};
const incomingOp = this.state.opStack[pos];
this.state.currentOp = this.state.allFoundOps[incomingOp.id];
if (!this.state.currentOp) {
/**
* We're trying to resume the function, but we can't find where to go.
*
* This means that either the function has changed or there are async
* actions in-between steps that we haven't noticed in previous
* executions.
*
* Whichever the case, this is bad and we can't continue in this
* undefined state.
*/
throw new NonRetriableError_js_1.NonRetriableError((0, errors_js_1.prettyError)({
whatHappened: " Your function was stopped from running",
why: "We couldn't resume your function's state because it may have changed since the run started or there are async actions in-between steps that we haven't noticed in previous executions.",
consequences: "Continuing to run the function may result in unexpected behaviour, so we've stopped your function to ensure nothing unexpected happened!",
toFixNow: "Ensure that your function is either entirely step-based or entirely non-step-based, by either wrapping all asynchronous logic in `step.run()` calls or by removing all `step.*()` calls.",
otherwise: "For more information on why step functions work in this manner, see https://www.inngest.com/docs/functions/multi-step#gotchas",
stack: true,
code: errors_js_1.ErrCode.NON_DETERMINISTIC_FUNCTION,
}));
}
this.state.currentOp.fulfilled = true;
if (typeof incomingOp.data !== "undefined") {
this.state.currentOp.resolve(incomingOp.data);
}
else {
this.state.currentOp.reject(incomingOp.error);
}
}
await (0, promises_js_1.resolveAfterPending)();
this.state.reset();
pos++;
} while (pos < this.state.opStack.length);
await ((_m = (_l = this.state.hooks).afterMemoization) === null || _m === void 0 ? void 0 : _m.call(_l));
const discoveredOps = Object.values(this.state.tickOps).map(tickOpToOutgoing);
const runStep = this.options.requestedRunStep ||
this.getEarlyExecRunStep(discoveredOps);
if (runStep) {
const userFnOp = this.state.allFoundOps[runStep];
const stepToRun = userFnOp === null || userFnOp === void 0 ? void 0 : userFnOp.fn;
if (!stepToRun) {
throw new Error(`Bad stack; executor requesting to run unknown step "${runStep}"`);
}
const outgoingUserFnOp = Object.assign(Object.assign({}, tickOpToOutgoing(userFnOp)), { op: types_js_1.StepOpCode.Step });
await ((_p = (_o = this.state.hooks).beforeExecution) === null || _p === void 0 ? void 0 : _p.call(_o));
this.state.executingStep = true;
const result = await (0, promises_js_1.runAsPromise)(stepToRun)
.finally(() => {
this.state.executingStep = false;
})
.catch(async (error) => {
return await this.transformOutput({ error }, outgoingUserFnOp);
})
.then(async (data) => {
var _a, _b;
await ((_b = (_a = this.state.hooks) === null || _a === void 0 ? void 0 : _a.afterExecution) === null || _b === void 0 ? void 0 : _b.call(_a));
return await this.transformOutput({ data }, outgoingUserFnOp);
});
const { type: _type } = result, rest = __rest(result, ["type"]);
return {
type: "step-ran",
ctx: this.fnArg,
ops: this.ops,
step: Object.assign(Object.assign({}, outgoingUserFnOp), rest),
};
}
if (!discoveredOps.length) {
const fnRet = await Promise.race([
userFnPromise.then((data) => ({ type: "complete", data })),
(0, promises_js_1.resolveNextTick)().then(() => ({ type: "incomplete" })),
]);
if (fnRet.type === "complete") {
await ((_r = (_q = this.state.hooks).afterExecution) === null || _r === void 0 ? void 0 : _r.call(_q));
const allOpsFulfilled = Object.values(this.state.allFoundOps).every((op) => {
return op.fulfilled;
});
if (allOpsFulfilled) {
return await this.transformOutput({ data: fnRet.data });
}
}
else if (!this.state.hasUsedTools) {
this.state.nonStepFnDetected = true;
const data = await userFnPromise;
await ((_t = (_s = this.state.hooks).afterExecution) === null || _t === void 0 ? void 0 : _t.call(_s));
return await this.transformOutput({ data });
}
else {
const hasOpsPending = Object.values(this.state.allFoundOps).some((op) => {
return op.fulfilled === false;
});
if (!hasOpsPending) {
throw new NonRetriableError_js_1.NonRetriableError((0, errors_js_1.functionStoppedRunningErr)(errors_js_1.ErrCode.ASYNC_DETECTED_AFTER_MEMOIZATION));
}
}
}
await ((_v = (_u = this.state.hooks).afterExecution) === null || _v === void 0 ? void 0 : _v.call(_u));
return {
type: "steps-found",
ctx: this.fnArg,
ops: this.ops,
steps: discoveredOps,
};
}
catch (error) {
return await this.transformOutput({ error });
}
finally {
await ((_x = (_w = this.state.hooks).beforeResponse) === null || _x === void 0 ? void 0 : _x.call(_w));
}
}
async initializeMiddleware() {
const ctx = this.options.data;
const hooks = await (0, InngestMiddleware_js_1.getHookStack)(this.options.fn["middleware"], "onFunctionRun", {
ctx,
fn: this.options.fn,
steps: Object.values(this.options.stepState),
reqArgs: this.options.reqArgs,
}, {
transformInput: (prev, output) => {
return {
ctx: Object.assign(Object.assign({}, prev.ctx), output === null || output === void 0 ? void 0 : output.ctx),
fn: this.options.fn,
steps: prev.steps.map((step, i) => {
var _a;
return (Object.assign(Object.assign({}, step), (_a = output === null || output === void 0 ? void 0 : output.steps) === null || _a === void 0 ? void 0 : _a[i]));
}),
reqArgs: prev.reqArgs,
};
},
transformOutput: (prev, output) => {
return {
result: Object.assign(Object.assign({}, prev.result), output === null || output === void 0 ? void 0 : output.result),
step: prev.step,
};
},
});
return hooks;
}
createExecutionState() {
const state = {
allFoundOps: {},
tickOps: {},
tickOpHashes: {},
currentOp: undefined,
hasUsedTools: false,
reset: () => {
state.tickOpHashes = {};
state.allFoundOps = Object.assign(Object.assign({}, state.allFoundOps), state.tickOps);
},
nonStepFnDetected: false,
executingStep: false,
opStack: this.options.stepCompletionOrder.reduce((acc, stepId) => {
const stepState = this.options.stepState[stepId];
if (!stepState) {
return acc;
}
return [...acc, stepState];
}, []),
};
return state;
}
get ops() {
return Object.fromEntries(Object.entries(this.state.allFoundOps).map(([id, op]) => [
id,
{
id: op.id,
rawArgs: op.rawArgs,
data: op.data,
error: op.error,
fulfilled: op.fulfilled,
seen: true,
},
]));
}
getUserFnToRun() {
if (!this.options.isFailureHandler) {
return this.options.fn["fn"];
}
if (!this.options.fn["onFailureFn"]) {
/**
* Somehow, we've ended up detecting that this is a failure handler but
* doesn't have an `onFailure` function. This should never happen.
*/
throw new Error("Cannot find function `onFailure` handler");
}
// TODO: Review; inferred types results in an `any` here!
return this.options.fn["onFailureFn"];
}
createFnArg() {
var _a, _b, _c, _d;
// Start referencing everything
this.state.tickOps = this.state.allFoundOps;
/**
* Create a unique hash of an operation using only a subset of the operation's
* properties; will never use `data` and will guarantee the order of the
* object so we don't rely on individual tools for that.
*
* If the operation already contains an ID, the current ID will be used
* instead, so that users can provide their own IDs.
*/
const hashOp = (
/**
* The op to generate a hash from. We only use a subset of the op's
* properties when creating the hash.
*/
op) => {
var _a, _b, _c, _d;
/**
* It's difficult for v0 to understand whether or not an op has
* historically contained a custom ID, as all step usage now require them.
*
* For this reason, we make the assumption that steps in v0 do not have a
* custom ID and generate one for them as we would in all recommendations
* and examples.
*/
const obj = {
parent: (_b = (_a = this.state.currentOp) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : null,
op: op.op,
name: op.name,
// Historically, no v0 runs could have options for `step.run()` call,
// but this object can be specified in future versions.
//
// For this purpose, we change this to always use `null` if the op is
// that of a `step.run()`.
opts: op.op === types_js_1.StepOpCode.StepPlanned ? null : (_c = op.opts) !== null && _c !== void 0 ? _c : null,
};
const collisionHash = exports._internals.hashData(obj);
const pos = (this.state.tickOpHashes[collisionHash] =
((_d = this.state.tickOpHashes[collisionHash]) !== null && _d !== void 0 ? _d : -1) + 1);
return Object.assign(Object.assign({}, op), { id: exports._internals.hashData(Object.assign({ pos }, obj)) });
};
const stepHandler = ({ args, matchOp, opts }) => {
if (this.state.nonStepFnDetected) {
throw new NonRetriableError_js_1.NonRetriableError((0, errors_js_1.functionStoppedRunningErr)(errors_js_1.ErrCode.STEP_USED_AFTER_ASYNC));
}
if (this.state.executingStep) {
throw new NonRetriableError_js_1.NonRetriableError((0, errors_js_1.prettyError)({
whatHappened: "Your function was stopped from running",
why: "We detected that you have nested `step.*` tooling.",
consequences: "Nesting `step.*` tooling is not supported.",
stack: true,
toFixNow: "Make sure you're not using `step.*` tooling inside of other `step.*` tooling. If you need to compose steps together, you can create a new async function and call it from within your step function, or use promise chaining.",
otherwise: "For more information on step functions with Inngest, see https://www.inngest.com/docs/functions/multi-step",
code: errors_js_1.ErrCode.NESTING_STEPS,
}));
}
this.state.hasUsedTools = true;
const stepOptions = (0, InngestStepTools_js_1.getStepOptions)(args[0]);
const opId = hashOp(matchOp(stepOptions, ...args.slice(1)));
return new Promise((resolve, reject) => {
this.state.tickOps[opId.id] = Object.assign(Object.assign(Object.assign({}, opId), ((opts === null || opts === void 0 ? void 0 : opts.fn) ? { fn: () => { var _a; return (_a = opts.fn) === null || _a === void 0 ? void 0 : _a.call(opts, ...args); } } : {})), { rawArgs: args, resolve,
reject, fulfilled: false });
});
};
const step = (0, InngestStepTools_js_1.createStepTools)(this.options.client, this, stepHandler);
let fnArg = Object.assign(Object.assign({}, this.options.data), { step });
if (this.options.isFailureHandler) {
const eventData = zod_1.z
.object({ error: types_js_1.jsonErrorSchema })
.parse((_a = fnArg.event) === null || _a === void 0 ? void 0 : _a.data);
fnArg = Object.assign(Object.assign({}, fnArg), { error: (0, errors_js_1.deserializeError)(eventData.error) });
}
return (_d = (_c = (_b = this.options).transformCtx) === null || _c === void 0 ? void 0 : _c.call(_b, fnArg)) !== null && _d !== void 0 ? _d : fnArg;
}
/**
* Using middleware, transform input before running.
*/
async transformInput() {
var _a, _b;
const inputMutations = await ((_b = (_a = this.state.hooks) === null || _a === void 0 ? void 0 : _a.transformInput) === null || _b === void 0 ? void 0 : _b.call(_a, {
ctx: Object.assign({}, this.fnArg),
steps: Object.values(this.options.stepState),
fn: this.options.fn,
reqArgs: this.options.reqArgs,
}));
if (inputMutations === null || inputMutations === void 0 ? void 0 : inputMutations.ctx) {
this.fnArg = inputMutations.ctx;
}
if (inputMutations === null || inputMutations === void 0 ? void 0 : inputMutations.steps) {
this.state.opStack = [...inputMutations.steps];
}
}
getEarlyExecRunStep(ops) {
if (ops.length !== 1)
return;
const op = ops[0];
if (op &&
op.op === types_js_1.StepOpCode.StepPlanned
// TODO We must individually check properties here that we do not want to
// execute on, such as retry counts. Nothing exists here that falls in to
// this case, but should be accounted for when we add them.
// && typeof op.opts === "undefined"
) {
return op.id;
}
}
/**
* Using middleware, transform output before returning.
*/
async transformOutput(dataOrError, step) {
var _a, _b, _c, _d;
const output = Object.assign({}, dataOrError);
if (typeof output.error !== "undefined") {
output.data = (0, errors_js_1.serializeError)(output.error);
}
const transformedOutput = await ((_b = (_a = this.state.hooks) === null || _a === void 0 ? void 0 : _a.transformOutput) === null || _b === void 0 ? void 0 : _b.call(_a, {
result: Object.assign({}, output),
step,
}));
const { data, error } = Object.assign(Object.assign({}, output), transformedOutput === null || transformedOutput === void 0 ? void 0 : transformedOutput.result);
if (!step) {
await ((_d = (_c = this.state.hooks) === null || _c === void 0 ? void 0 : _c.finished) === null || _d === void 0 ? void 0 : _d.call(_c, {
result: Object.assign({}, (typeof error !== "undefined" ? { error } : { data })),
}));
}
if (typeof error !== "undefined") {
/**
* Ensure we give middleware the chance to decide on retriable behaviour
* by looking at the error returned from output transformation.
*/
let retriable = !(error instanceof NonRetriableError_js_1.NonRetriableError);
if (retriable && error instanceof RetryAfterError_js_1.RetryAfterError) {
retriable = error.retryAfter;
}
const serializedError = (0, errors_js_1.serializeError)(error);
return {
type: "function-rejected",
ctx: this.fnArg,
ops: this.ops,
error: serializedError,
retriable,
};
}
return {
type: "function-resolved",
ctx: this.fnArg,
ops: this.ops,
data: (0, functions_js_1.undefinedToNull)(data),
};
}
}
exports.V0InngestExecution = V0InngestExecution;
const tickOpToOutgoing = (op) => {
return {
op: op.op,
id: op.id,
name: op.name,
opts: op.opts,
};
};
const hashData = (op) => {
return (0, hash_js_1.sha1)().update((0, canonicalize_1.default)(op)).digest("hex");
};
/**
* Exported for testing.
*/
exports._internals = { hashData };
//# sourceMappingURL=v0.js.map