UNPKG

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.

1,203 lines (1,201 loc) • 45.7 kB
const require_rolldown_runtime = require('../../_virtual/rolldown_runtime.cjs'); const require_consts = require('../../helpers/consts.cjs'); const require_version = require('../../version.cjs'); const require_NonRetriableError = require('../NonRetriableError.cjs'); const require_errors = require('../../helpers/errors.cjs'); const require_types = require('../../types.cjs'); const require_InngestExecution = require('./InngestExecution.cjs'); const require_functions = require('../../helpers/functions.cjs'); const require_promises = require('../../helpers/promises.cjs'); const require_als = require('./als.cjs'); const require_temporal = require('../../helpers/temporal.cjs'); const require_InngestStepTools = require('../InngestStepTools.cjs'); const require_utils = require('../middleware/utils.cjs'); const require_manager = require('../middleware/manager.cjs'); const require_Inngest = require('../Inngest.cjs'); const require_InngestGroupTools = require('../InngestGroupTools.cjs'); const require_RetryAfterError = require('../RetryAfterError.cjs'); const require_StepError = require('../StepError.cjs'); const require_utils$1 = require('../triggers/utils.cjs'); const require_access = require('./otel/access.cjs'); let zod_v3 = require("zod/v3"); let hash_js = require("hash.js"); hash_js = require_rolldown_runtime.__toESM(hash_js); let ms = require("ms"); ms = require_rolldown_runtime.__toESM(ms); let __opentelemetry_api = require("@opentelemetry/api"); //#region src/components/execution/engine.ts var engine_exports = /* @__PURE__ */ require_rolldown_runtime.__export({ _internals: () => _internals, createExecutionEngine: () => createExecutionEngine }); const { sha1 } = hash_js.default; /** * Retry configuration for checkpoint operations. * * Checkpoint calls use exponential backoff with jitter to handle transient * network failures (e.g., dev server temporarily down, cloud hiccup). If * retries exhaust, the error propagates up - for Sync mode this results in a * 500 error, for AsyncCheckpointing the caller handles fallback. */ const CHECKPOINT_RETRY_OPTIONS = { maxAttempts: 5, baseDelay: 100 }; const STEP_NOT_FOUND_MAX_FOUND_STEPS = 25; const createExecutionEngine = (options) => { return new InngestExecutionEngine(options); }; var InngestExecutionEngine = class extends require_InngestExecution.InngestExecution { version = require_consts.ExecutionVersion.V2; state; fnArg; checkpointHandlers; timeoutDuration = 1e3 * 10; execution; userFnToRun; middlewareManager; /** * If we're supposed to run a particular step via `requestedRunStep`, this * will be a `Promise` that resolves after no steps have been found for * `timeoutDuration` milliseconds. * * If we're not supposed to run a particular step, this will be `undefined`. */ timeout; rootSpanId; /** * If we're checkpointing and have been given a maximum runtime, this will be * a `Promise` that resolves after that duration has elapsed, allowing us to * ensure that we end the execution in good time, especially in serverless * environments. */ checkpointingMaxRuntimeTimer; /** * If we're checkpointing and have been given a maximum buffer interval, this * will be a `Promise` that resolves after that duration has elapsed, allowing * us to periodically checkpoint even if the step buffer hasn't filled. */ checkpointingMaxBufferIntervalTimer; constructor(rawOptions) { const options = { ...rawOptions, stepMode: rawOptions.stepMode ?? require_types.StepMode.Async }; super(options); /** * Check we have everything we need for checkpointing */ if (this.options.stepMode === require_types.StepMode.Sync) { if (!this.options.createResponse) throw new Error("createResponse is required for sync step mode"); } this.userFnToRun = this.getUserFnToRun(); this.state = this.createExecutionState(); this.fnArg = this.createFnArg(); const mwInstances = this.options.middlewareInstances ?? (this.options.client.middleware || []).map((Cls) => { return new Cls({ client: this.options.client }); }); this.middlewareManager = new require_manager.MiddlewareManager(this.fnArg, () => this.state.stepState, mwInstances, this.options.fn, this.options.client[require_Inngest.internalLoggerSymbol]); this.checkpointHandlers = this.createCheckpointHandlers(); this.initializeTimer(this.state); this.initializeCheckpointRuntimeTimer(this.state); this.devDebug("created new V1 execution for run;", this.options.requestedRunStep ? `wanting to run step "${this.options.requestedRunStep}"` : "discovering steps"); this.devDebug("existing state keys:", Object.keys(this.state.stepState)); } /** * Idempotently start the execution of the user's function. */ start() { if (!this.execution) { this.devDebug("starting V1 execution"); const tracer = __opentelemetry_api.trace.getTracer("inngest", require_version.version); this.execution = require_als.getAsyncLocalStorage().then((als) => { return als.run({ app: this.options.client, execution: { ctx: this.fnArg, instance: this } }, async () => { return tracer.startActiveSpan("inngest.execution", (span) => { this.rootSpanId = span.spanContext().spanId; require_access.clientProcessorMap.get(this.options.client)?.declareStartingSpan({ span, runId: this.options.runId, traceparent: this.options.headers[require_consts.headerKeys.TraceParent], tracestate: this.options.headers[require_consts.headerKeys.TraceState] }); return this._start().then((result) => { this.devDebug("result:", result); return result; }).finally(() => { span.end(); }); }); }); }); } return this.execution; } addMetadata(stepId, kind, scope, op, values) { if (!this.state.metadata) this.state.metadata = /* @__PURE__ */ new Map(); const updates = this.state.metadata.get(stepId) ?? []; updates.push({ kind, scope, op, values }); this.state.metadata.set(stepId, updates); return true; } /** * Starts execution of the user's function and the core loop. */ async _start() { try { const allCheckpointHandler = this.getCheckpointHandler(""); await this.startExecution(); let i = 0; for await (const checkpoint of this.state.loop) { await allCheckpointHandler(checkpoint, i); const result = await this.getCheckpointHandler(checkpoint.type)(checkpoint, i++); if (result) return result; } } catch (error) { return this.transformOutput({ error }); } finally { this.state.loop.return(); } /** * If we're here, the generator somehow finished without returning a value. * This should never happen. */ throw new Error("Core loop finished without returning a value"); } async checkpoint(steps) { if (this.options.stepMode === require_types.StepMode.Sync) if (!this.state.checkpointedRun) { const res = await require_promises.retryWithBackoff(() => this.options.client["inngestApi"].checkpointNewRun({ runId: this.fnArg.runId, event: this.fnArg.event, steps, executionVersion: this.version, retries: this.fnArg.maxAttempts ?? require_consts.defaultMaxRetries }), CHECKPOINT_RETRY_OPTIONS); this.state.checkpointedRun = { appId: res.data.app_id, fnId: res.data.fn_id, token: res.data.token }; } else await require_promises.retryWithBackoff(() => this.options.client["inngestApi"].checkpointSteps({ appId: this.state.checkpointedRun.appId, fnId: this.state.checkpointedRun.fnId, runId: this.fnArg.runId, steps }), CHECKPOINT_RETRY_OPTIONS); else if (this.options.stepMode === require_types.StepMode.AsyncCheckpointing) { if (!this.options.queueItemId) throw new Error("Missing queueItemId for async checkpointing. This is a bug in the Inngest SDK."); if (!this.options.internalFnId) throw new Error("Missing internalFnId for async checkpointing. This is a bug in the Inngest SDK."); await require_promises.retryWithBackoff(() => this.options.client["inngestApi"].checkpointStepsAsync({ runId: this.fnArg.runId, fnId: this.options.internalFnId, queueItemId: this.options.queueItemId, steps }), CHECKPOINT_RETRY_OPTIONS); } else throw new Error("Checkpointing is only supported in Sync and AsyncCheckpointing step modes. This is a bug in the Inngest SDK."); } async checkpointAndSwitchToAsync(steps) { await this.checkpoint(steps); if (!this.state.checkpointedRun?.token) throw new Error("Failed to checkpoint and switch to async mode"); return { type: "change-mode", ctx: this.fnArg, ops: this.ops, to: require_types.StepMode.Async, token: this.state.checkpointedRun?.token }; } /** * Returns whether we're in the final attempt of execution, or `null` if we * can't determine this in the SDK. */ inFinalAttempt() { if (typeof this.fnArg.maxAttempts !== "number") return null; return this.fnArg.attempt + 1 >= this.fnArg.maxAttempts; } /** * Creates a handler for every checkpoint type, defining what to do when we * reach that checkpoint in the core loop. */ createCheckpointHandlers() { const commonCheckpointHandler = (checkpoint) => { this.devDebug(`${this.options.stepMode} checkpoint:`, checkpoint); }; const stepRanHandler = async (stepResult) => { const transformResult = await this.transformOutput(stepResult); /** * Transforming output will always return either function rejection or * resolution. In most cases, this can be immediately returned, but in * this particular case we want to handle it differently. */ if (transformResult.type === "function-resolved") return { type: "step-ran", ctx: transformResult.ctx, ops: transformResult.ops, step: { ...stepResult, data: transformResult.data } }; else if (transformResult.type === "function-rejected") { const stepForResponse = { ...stepResult, error: transformResult.error }; if (stepResult.op === require_types.StepOpCode.StepFailed) { const ser = require_errors.serializeError(transformResult.error); stepForResponse.data = { __serialized: true, name: ser.name, message: ser.message, stack: "" }; } return { type: "step-ran", ctx: transformResult.ctx, ops: transformResult.ops, retriable: transformResult.retriable, step: stepForResponse }; } return transformResult; }; const maybeReturnNewSteps = async () => { const newSteps = await this.filterNewSteps(Array.from(this.state.steps.values())); if (newSteps) return { type: "steps-found", ctx: this.fnArg, ops: this.ops, steps: newSteps }; }; const attemptCheckpointAndResume = async (stepResult, resume = true, force = false) => { if (stepResult) { const stepToResume = this.resumeStepWithResult(stepResult, resume); delete this.state.executingStep; this.state.checkpointingStepBuffer.push({ ...stepToResume, data: stepResult.data }); } if (force || !this.options.checkpointingConfig?.bufferedSteps || this.state.checkpointingStepBuffer.length >= this.options.checkpointingConfig.bufferedSteps) { this.devDebug("checkpointing and resuming execution after step run"); try { this.devDebug(`checkpointing all buffered steps:`, this.state.checkpointingStepBuffer.map((op) => op.displayName || op.id).join(", ")); await this.checkpoint(this.state.checkpointingStepBuffer); return; } catch (err) { this.devDebug("error checkpointing after step run, so falling back to async", err); const buffered = this.state.checkpointingStepBuffer; if (buffered.length) return { type: "steps-found", ctx: this.fnArg, ops: this.ops, steps: buffered }; return; } finally { this.state.checkpointingStepBuffer = []; } } else this.devDebug(`not checkpointing yet, continuing execution as we haven't reached buffered step limit of ${this.options.checkpointingConfig?.bufferedSteps}`); }; const syncHandlers = { "": commonCheckpointHandler, "function-resolved": async (checkpoint, i) => { const transformedData = checkpoint.data; await this.checkpoint([{ op: require_types.StepOpCode.RunComplete, id: _internals.hashId("complete"), data: await this.options.createResponse(transformedData) }]); return await this.transformOutput({ data: checkpoint.data }); }, "function-rejected": async (checkpoint) => { if (this.inFinalAttempt()) return await this.transformOutput({ error: checkpoint.error }); return this.checkpointAndSwitchToAsync([{ id: _internals.hashId("complete"), op: require_types.StepOpCode.StepError, error: checkpoint.error }]); }, "step-not-found": () => { return { type: "function-rejected", ctx: this.fnArg, error: /* @__PURE__ */ new Error("Step not found when checkpointing; this should never happen"), ops: this.ops, retriable: false }; }, "steps-found": async ({ steps }) => { if (steps.length !== 1 || steps[0].mode !== require_types.StepMode.Sync) return this.checkpointAndSwitchToAsync(steps.map((step) => ({ ...step, id: step.hashedId }))); const result = await this.executeStep(steps[0]); const transformed = await stepRanHandler(result); if (transformed.type !== "step-ran") throw new Error("Unexpected checkpoint handler result type after running step in sync mode"); if (result.error) return this.checkpointAndSwitchToAsync([transformed.step]); const stepToResume = this.resumeStepWithResult(result); delete this.state.executingStep; const stepForCheckpoint = { ...stepToResume, data: transformed.step.data }; await this.checkpoint([stepForCheckpoint]); }, "checkpointing-runtime-reached": () => { return this.checkpointAndSwitchToAsync([{ op: require_types.StepOpCode.DiscoveryRequest, id: _internals.hashId(`discovery-request-${Date.now()}`) }]); }, "checkpointing-buffer-interval-reached": () => { return attemptCheckpointAndResume(void 0, false, true); } }; const asyncHandlers = { "": commonCheckpointHandler, "function-resolved": async ({ data }) => { const newStepsResult = await maybeReturnNewSteps(); if (newStepsResult) return newStepsResult; if (this.options.createResponse) data = await this.options.createResponse(data); return await this.transformOutput({ data }); }, "function-rejected": async (checkpoint) => { return await this.transformOutput({ error: checkpoint.error }); }, "steps-found": async ({ steps }) => { const stepResult = await this.tryExecuteStep(steps); if (stepResult) return stepRanHandler(stepResult); return maybeReturnNewSteps(); }, "step-not-found": ({ step }) => { const { foundSteps, totalFoundSteps } = this.getStepNotFoundDetails(); return { type: "step-not-found", ctx: this.fnArg, ops: this.ops, step, foundSteps, totalFoundSteps }; }, "checkpointing-runtime-reached": () => { throw new Error("Checkpointing maximum runtime reached, but this is not in a checkpointing step mode. This is a bug in the Inngest SDK."); }, "checkpointing-buffer-interval-reached": () => { throw new Error("Checkpointing maximum buffer interval reached, but this is not in a checkpointing step mode. This is a bug in the Inngest SDK."); } }; const asyncCheckpointingHandlers = { "": commonCheckpointHandler, "function-resolved": async (checkpoint, i) => { const output = await asyncHandlers["function-resolved"](checkpoint, i); if (output?.type === "function-resolved") { const steps = this.state.checkpointingStepBuffer.concat({ op: require_types.StepOpCode.RunComplete, id: _internals.hashId("complete"), data: output.data }); if (isNonEmpty(steps)) return { type: "steps-found", ctx: output.ctx, ops: output.ops, steps }; } }, "function-rejected": async (checkpoint) => { if (this.state.checkpointingStepBuffer.length) { const fallback = await attemptCheckpointAndResume(void 0, false, true); if (fallback) return fallback; } return await this.transformOutput({ error: checkpoint.error }); }, "step-not-found": asyncHandlers["step-not-found"], "steps-found": async ({ steps }) => { const { stepsToResume, newSteps } = steps.reduce((acc, step) => { if (!step.hasStepState) acc.newSteps.push(step); else if (!step.fulfilled) acc.stepsToResume.push(step); return acc; }, { stepsToResume: [], newSteps: [] }); this.devDebug("split found steps in to:", { stepsToResume: stepsToResume.length, newSteps: newSteps.length }); if (!this.options.requestedRunStep && newSteps.length) { const stepResult = await this.tryExecuteStep(newSteps); if (stepResult) { this.devDebug(`executed step "${stepResult.id}" successfully`); if (stepResult.error) { if (this.state.checkpointingStepBuffer.length) { const fallback = await attemptCheckpointAndResume(void 0, false, true); if (fallback) return fallback; } return stepRanHandler(stepResult); } return await attemptCheckpointAndResume(stepResult); } if (this.state.checkpointingStepBuffer.length) { const fallback = await attemptCheckpointAndResume(void 0, false, true); if (fallback) return fallback; } return maybeReturnNewSteps(); } if (stepsToResume.length) { this.devDebug(`resuming ${stepsToResume.length} steps`); for (const st of stepsToResume) this.resumeStepWithResult({ ...st, id: st.hashedId }); } }, "checkpointing-runtime-reached": async () => { return { type: "steps-found", ctx: this.fnArg, ops: this.ops, steps: [{ op: require_types.StepOpCode.DiscoveryRequest, id: _internals.hashId(`discovery-request-${Date.now()}`) }] }; }, "checkpointing-buffer-interval-reached": () => { return attemptCheckpointAndResume(void 0, false, true); } }; return { [require_types.StepMode.Async]: asyncHandlers, [require_types.StepMode.Sync]: syncHandlers, [require_types.StepMode.AsyncCheckpointing]: asyncCheckpointingHandlers }; } getCheckpointHandler(type) { return this.checkpointHandlers[this.options.stepMode][type]; } async tryExecuteStep(steps) { const hashedStepIdToRun = this.options.requestedRunStep || this.getEarlyExecRunStep(steps); if (!hashedStepIdToRun) return; const step = steps.find((step$1) => step$1.hashedId === hashedStepIdToRun && step$1.fn); if (step) return await this.executeStep(step); this.timeout?.reset(); } /** * Given a list of outgoing ops, decide if we can execute an op early and * return the ID of the step to execute if we can. */ getEarlyExecRunStep(steps) { /** * We may have been disabled due to parallelism, in which case we can't * immediately execute unless explicitly requested. */ if (this.options.disableImmediateExecution) return; const unfulfilledSteps = steps.filter((step) => !step.fulfilled); if (unfulfilledSteps.length !== 1) return; const op = unfulfilledSteps[0]; if (op && op.op === require_types.StepOpCode.StepPlanned) return op.hashedId; } async filterNewSteps(foundSteps) { if (this.options.requestedRunStep) return; const newSteps = foundSteps.reduce((acc, step) => { if (!step.hasStepState) acc.push(step); return acc; }, []); if (!newSteps.length) return; await this.middlewareManager.onMemoizationEnd(); const stepList = newSteps.map((step) => { return { displayName: step.displayName, op: step.op, id: step.hashedId, name: step.name, opts: step.opts, userland: step.userland }; }); if (!isNonEmpty(stepList)) throw new require_utils.UnreachableError("stepList is empty"); return stepList; } async executeStep(foundStep) { const { id, name, opts, fn, displayName, userland, hashedId } = foundStep; const { stepInfo, wrappedHandler, setActualHandler } = foundStep.middleware; this.devDebug(`preparing to execute step "${id}"`); this.timeout?.clear(); const outgoingOp = { id: hashedId, op: require_types.StepOpCode.StepRun, name, opts, displayName, userland }; this.state.executingStep = outgoingOp; const store = await require_als.getAsyncCtx(); if (store?.execution) store.execution.executingStep = { id, name: displayName }; this.devDebug(`executing step "${id}"`); if (this.rootSpanId && this.options.checkpointingConfig) require_access.clientProcessorMap.get(this.options.client)?.declareStepExecution(this.rootSpanId, hashedId, this.options.data?.attempt ?? 0); let interval; const actualHandler = () => require_promises.runAsPromise(fn); await this.middlewareManager.onMemoizationEnd(); await this.middlewareManager.onStepStart(stepInfo); if (!foundStep.memoizationDeferred) { const deferred = require_promises.createDeferredPromise(); foundStep.memoizationDeferred = deferred; setActualHandler(() => deferred.promise); foundStep.transformedResultPromise = wrappedHandler(); foundStep.transformedResultPromise.catch(() => {}); } const wrappedActualHandler = this.middlewareManager.buildWrapStepHandlerChain(actualHandler, stepInfo); return require_promises.goIntervalTiming(() => wrappedActualHandler()).finally(() => { this.devDebug(`finished executing step "${id}"`); this.state.executingStep = void 0; if (this.rootSpanId && this.options.checkpointingConfig) require_access.clientProcessorMap.get(this.options.client)?.clearStepExecution(this.rootSpanId); if (store?.execution) delete store.execution.executingStep; }).then(async ({ resultPromise, interval: _interval }) => { interval = _interval; const metadata = this.state.metadata?.get(id); const serverData = await resultPromise; await this.middlewareManager.onStepComplete(stepInfo, serverData); return { ...outgoingOp, data: serverData, ...metadata && metadata.length > 0 ? { metadata } : {} }; }).catch((error) => { return this.buildStepErrorOp({ error, id, outgoingOp, stepInfo }); }).then((op) => ({ ...op, timing: interval })); } /** * Starts execution of the user's function, including triggering checkpoints * and middleware hooks where appropriate. */ async startExecution() { /** * Start the timer to time out the run if needed. */ this.timeout?.start(); this.checkpointingMaxRuntimeTimer?.start(); this.checkpointingMaxBufferIntervalTimer?.start(); const fnInputResult = await this.middlewareManager.transformFunctionInput(); this.applyFunctionInputMutations(fnInputResult); if (this.state.allStateUsed()) await this.middlewareManager.onMemoizationEnd(); if (this.state.stepsToFulfill === 0 && this.fnArg.attempt === 0) await this.middlewareManager.onRunStart(); const innerHandler = async () => { await this.validateEventSchemas(); return this.userFnToRun(this.fnArg); }; require_promises.runAsPromise(this.middlewareManager.wrapRunHandler(innerHandler)).then(async (data) => { await this.middlewareManager.onRunComplete(data); this.state.setCheckpoint({ type: "function-resolved", data }); }).catch(async (error) => { let err; if (error instanceof Error) err = error; else if (typeof error === "object") err = new Error(JSON.stringify(error)); else err = new Error(String(error)); await this.middlewareManager.onRunError(err, !this.retriability(err)); this.state.setCheckpoint({ type: "function-rejected", error: err }); }); } /** * Determine whether the given error is retriable. Returns `false` when the * run should not be retried, a duration string for `RetryAfterError`, or * `true` for normal retry behavior. */ retriability(error) { if (this.fnArg.maxAttempts && this.fnArg.maxAttempts - 1 === this.fnArg.attempt) return false; if (error instanceof require_NonRetriableError.NonRetriableError || error?.name === "NonRetriableError") return false; if (error instanceof require_StepError.StepError && error === this.state.recentlyRejectedStepError) return false; if (error instanceof require_RetryAfterError.RetryAfterError || error?.name === "RetryAfterError") return error.retryAfter; return true; } /** * Build the OutgoingOp for a failed step, notifying middleware and choosing * retriable vs non-retriable opcode. */ async buildStepErrorOp({ error, id, outgoingOp, stepInfo }) { const isFinal = !this.retriability(error); const metadata = this.state.metadata?.get(id); await this.middlewareManager.onStepError(stepInfo, error instanceof Error ? error : new Error(String(error)), isFinal); const serialized = require_errors.serializeError(error); return { ...outgoingOp, error: serialized, op: isFinal ? require_types.StepOpCode.StepFailed : require_types.StepOpCode.StepError, ...metadata && metadata.length > 0 ? { metadata } : {} }; } /** * Validate event data against schemas defined in function triggers. */ async validateEventSchemas() { if (this.options.isFailureHandler) return; const triggers = this.options.fn.opts.triggers; if (!triggers || triggers.length === 0) return; const fnArgEvents = this.fnArg.events; if (!fnArgEvents || fnArgEvents.length === 0) return; await require_utils$1.validateEvents(fnArgEvents.map((event) => ({ name: event.name, data: event.data })), triggers); } /** * Using middleware, transform output before returning. */ transformOutput(dataOrError) { const { data, error } = dataOrError; if (typeof error !== "undefined") { const retriable = this.retriability(error); const serializedError = require_errors.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: require_functions.undefinedToNull(data) }; } createExecutionState() { const d = require_promises.createDeferredPromiseWithStack(); let checkpointResolve = d.deferred.resolve; const checkpointResults = d.results; const loop = (async function* (cleanUp) { try { while (true) { const res = (await checkpointResults.next()).value; if (res) yield res; } } finally { cleanUp?.(); } })(() => { this.timeout?.clear(); this.checkpointingMaxRuntimeTimer?.clear(); this.checkpointingMaxBufferIntervalTimer?.clear(); checkpointResults.return(); }); const stepsToFulfill = Object.keys(this.options.stepState).length; return { stepState: this.options.stepState, stepsToFulfill, steps: /* @__PURE__ */ new Map(), loop, hasSteps: Boolean(stepsToFulfill), stepCompletionOrder: [...this.options.stepCompletionOrder], remainingStepsToBeSeen: new Set(this.options.stepCompletionOrder), setCheckpoint: (checkpoint) => { this.devDebug("setting checkpoint:", checkpoint.type); ({resolve: checkpointResolve} = checkpointResolve(checkpoint)); }, allStateUsed: () => { return this.state.remainingStepsToBeSeen.size === 0; }, checkpointingStepBuffer: [], metadata: /* @__PURE__ */ new Map() }; } get ops() { return Object.fromEntries(this.state.steps); } createFnArg() { const step = this.createStepTools(); const experimentStepRun = step[require_InngestStepTools.experimentStepRunSymbol]; let fnArg = { ...this.options.data, step, group: require_InngestGroupTools.createGroupTools({ experimentStepRun }) }; /** * Handle use of the `onFailure` option by deserializing the error. */ if (this.options.isFailureHandler) { const eventData = zod_v3.z.object({ error: require_types.jsonErrorSchema }).parse(fnArg.event?.data); fnArg = { ...fnArg, error: require_errors.deserializeError(eventData.error) }; } return this.options.transformCtx?.(fnArg) ?? fnArg; } /** * Apply mutations from `transformFunctionInput` back to execution state. * Allows middleware to modify event data, step tools, memoized step data, * and inject custom fields into the handler context. */ applyFunctionInputMutations(result) { const { event, events, step, ...extensions } = result.ctx; if (event !== this.fnArg.event) this.fnArg.event = event; if (events !== this.fnArg.events) this.fnArg.events = events; if (step !== this.fnArg.step) this.fnArg.step = step; if (Object.keys(extensions).length > 0) Object.assign(this.fnArg, extensions); for (const [hashedId, stepData] of Object.entries(result.steps)) { const existing = this.state.stepState[hashedId]; if (existing && stepData && stepData.type === "data" && stepData.data !== existing.data) this.state.stepState[hashedId] = { ...existing, data: stepData.data }; } } createStepTools() { /** * A list of steps that have been found and are being rolled up before being * reported to the core loop. */ const foundStepsToReport = /* @__PURE__ */ new Map(); /** * A map of the subset of found steps to report that have not yet been * handled. Used for fast access to steps that need to be handled in order. */ const unhandledFoundStepsToReport = /* @__PURE__ */ new Map(); /** * A map of the latest sequential step indexes found for each step ID. Used * to ensure that we don't index steps in parallel. * * Note that these must be sequential; if we've seen or assigned `a:1`, * `a:2` and `a:4`, the latest sequential step index is `2`. * */ const expectedNextStepIndexes = /* @__PURE__ */ new Map(); /** * An ordered list of step IDs that have yet to be handled in this * execution. Used to ensure that we handle steps in the order they were * found and based on the `stepCompletionOrder` in this execution's state. */ const remainingStepCompletionOrder = this.state.stepCompletionOrder.slice(); /** * A promise that's used to ensure that step reporting cannot be run more than * once in a given asynchronous time span. */ let foundStepsReportPromise; /** * A flag used to ensure that we only warn about parallel indexing once per * execution to avoid spamming the console. */ let warnOfParallelIndexing = false; /** * Counts the number of times we've extended this tick. */ let tickExtensionCount = 0; /** * Given a colliding step ID, maybe warn the user about parallel indexing. */ const maybeWarnOfParallelIndexing = (userlandCollisionId) => { if (warnOfParallelIndexing) return; const hashedCollisionId = _internals.hashId(userlandCollisionId); if (this.state.steps.has(hashedCollisionId)) { if (!foundStepsToReport.has(hashedCollisionId)) { warnOfParallelIndexing = true; this.options.client["warnMetadata"]({ run_id: this.fnArg.runId }, require_errors.ErrCode.AUTOMATIC_PARALLEL_INDEXING, { message: `Duplicate step ID "${userlandCollisionId}" detected across parallel chains`, explanation: "Using the same ID for steps in different parallel chains can cause unexpected behaviour. Your function is still running.", action: "Use a unique ID for each step, especially those in parallel.", code: require_errors.ErrCode.AUTOMATIC_PARALLEL_INDEXING }); } } }; /** * A helper used to report steps to the core loop. Used after adding an item * to `foundStepsToReport`. */ const reportNextTick = () => { if (foundStepsReportPromise) return; let extensionPromise; if (++tickExtensionCount >= 10) { tickExtensionCount = 0; extensionPromise = require_promises.resolveNextTick(); } else extensionPromise = require_promises.resolveAfterPending(); foundStepsReportPromise = extensionPromise.then(() => { foundStepsReportPromise = void 0; for (let i = 0; i < remainingStepCompletionOrder.length; i++) { const nextStepId = remainingStepCompletionOrder[i]; if (!nextStepId) continue; if (unhandledFoundStepsToReport.get(nextStepId)?.handle()) { remainingStepCompletionOrder.splice(i, 1); unhandledFoundStepsToReport.delete(nextStepId); reportNextTick(); return; } } const steps = [...foundStepsToReport.values()]; foundStepsToReport.clear(); unhandledFoundStepsToReport.clear(); if (!isNonEmpty(steps)) return; this.state.setCheckpoint({ type: "steps-found", steps }); }); }; /** * A helper used to push a step to the list of steps to report. */ const pushStepToReport = (step) => { foundStepsToReport.set(step.hashedId, step); unhandledFoundStepsToReport.set(step.hashedId, step); reportNextTick(); }; const stepHandler = async ({ args, matchOp, opts }) => { const opId = matchOp(require_InngestStepTools.getStepOptions(args[0]), ...args.slice(1)); if (this.state.executingStep) /** * If a step is found after asynchronous actions during another step's * execution, everything is fine. The problem here is if we've found * that a step nested inside another a step, which is something we don't * support at the time of writing. * * In this case, we could use something like Async Hooks to understand * how the step is being triggered, though this isn't available in all * environments. * * Therefore, we'll only show a warning here to indicate that this is * potentially an issue. */ this.options.client["warnMetadata"]({ run_id: this.fnArg.runId }, require_errors.ErrCode.NESTING_STEPS, { message: `Nested step tooling detected in "${opId.displayName ?? opId.id}"`, explanation: "Nesting step.* calls is not supported. This warning may also appear if steps are separated by regular async calls, which is fine.", action: "Avoid using step.* inside other step.* calls. Use a separate async function or promise chaining to compose steps.", code: require_errors.ErrCode.NESTING_STEPS }); const { hashedId, isFulfilled, setActualHandler, stepInfo, stepState, wrappedHandler } = await this.applyMiddlewareToStep(opId, expectedNextStepIndexes, maybeWarnOfParallelIndexing); const { promise, resolve, reject } = require_promises.createDeferredPromise(); let extraOpts; let fnArgs = [...args]; if (typeof stepState?.input !== "undefined" && Array.isArray(stepState.input)) switch (opId.op) { case require_types.StepOpCode.StepPlanned: fnArgs = [...args.slice(0, 2), ...stepState.input]; extraOpts = { input: [...stepState.input] }; break; case require_types.StepOpCode.AiGateway: extraOpts = { body: { ...typeof opId.opts?.body === "object" ? { ...opId.opts.body } : {}, ...stepState.input[0] } }; break; } if (!extraOpts && Array.isArray(stepInfo.input)) fnArgs = [...args.slice(0, 2), ...stepInfo.input]; const step = { ...opId, opts: { ...opId.opts, ...extraOpts }, rawArgs: fnArgs, hashedId, input: stepState?.input, fn: opts?.fn ? () => opts.fn?.(this.fnArg, ...fnArgs) : void 0, promise, fulfilled: isFulfilled, hasStepState: Boolean(stepState), displayName: opId.displayName ?? opId.id, handled: false, middleware: { wrappedHandler, stepInfo, setActualHandler }, handle: () => { if (step.handled) return false; this.devDebug(`handling step "${hashedId}"`); step.handled = true; const result = this.state.stepState[hashedId]; if (step.fulfilled && result) { result.fulfilled = true; Promise.all([ result.data, result.error, result.input ]).then(async () => { if (step.transformedResultPromise) { if (step.memoizationDeferred) if (typeof result.data !== "undefined") step.memoizationDeferred.resolve(await result.data); else { const stepError = new require_StepError.StepError(opId.id, result.error); this.state.recentlyRejectedStepError = stepError; step.memoizationDeferred.reject(stepError); } step.transformedResultPromise.then(resolve, reject); return; } step.middleware.stepInfo.memoized = true; if (typeof result.data !== "undefined") { if (opId.op === require_types.StepOpCode.WaitForEvent && result.data !== null) { const { event } = step.rawArgs?.[1] ?? {}; if (!event) throw new Error("Missing event option in waitForEvent"); try { await require_utils$1.validateEvents([result.data], [{ event }]); } catch (err) { this.state.recentlyRejectedStepError = new require_StepError.StepError(opId.id, err); reject(this.state.recentlyRejectedStepError); return; } } step.middleware.setActualHandler(() => Promise.resolve(result.data)); step.middleware.wrappedHandler().then(resolve); } else { const stepError = new require_StepError.StepError(opId.id, result.error); this.state.recentlyRejectedStepError = stepError; step.middleware.setActualHandler(() => Promise.reject(stepError)); step.middleware.wrappedHandler().catch(reject); } }); } return true; } }; this.state.steps.set(hashedId, step); this.state.hasSteps = true; if (!isFulfilled && !stepState && step.fn) { const deferred = require_promises.createDeferredPromise(); step.memoizationDeferred = deferred; setActualHandler(() => { pushStepToReport(step); return deferred.promise; }); step.transformedResultPromise = wrappedHandler(); step.transformedResultPromise.catch((error) => { reject(error); }); } else pushStepToReport(step); return promise; }; return require_InngestStepTools.createStepTools(this.options.client, this, stepHandler); } /** * Applies middleware transformations to a step, resolves ID collisions, * and performs memoization lookup. */ async applyMiddlewareToStep(opId, expectedNextStepIndexes, maybeWarnOfParallelIndexing) { const initialCollision = resolveStepIdCollision({ baseId: opId.id, expectedIndexes: expectedNextStepIndexes, stepsMap: this.state.steps }); if (initialCollision.finalId !== opId.id) { maybeWarnOfParallelIndexing(opId.id); opId.id = initialCollision.finalId; if (initialCollision.index !== void 0) opId.userland.index = initialCollision.index; } const originalId = opId.userland.id; let hashedId = _internals.hashId(opId.id); const { entryPoint, opName, opOpts, setActualHandler, stepInfo } = await this.middlewareManager.applyToStep({ displayName: opId.displayName ?? opId.userland.id, hashedId, memoized: Boolean(this.state.stepState[hashedId]) && typeof this.state.stepState[hashedId]?.input === "undefined", op: opId.op, opts: opId.opts, userlandId: opId.userland.id }); if (opName !== void 0) opId.name = opName; if (opOpts !== void 0) opId.opts = opOpts; if (stepInfo.options.id !== originalId) { opId.id = stepInfo.options.id; opId.userland.id = stepInfo.options.id; const secondCollision = resolveStepIdCollision({ baseId: stepInfo.options.id, expectedIndexes: expectedNextStepIndexes, stepsMap: this.state.steps }); if (secondCollision.finalId !== stepInfo.options.id) { opId.id = secondCollision.finalId; opId.userland.id = secondCollision.finalId; stepInfo.options.id = secondCollision.finalId; if (secondCollision.index !== void 0) opId.userland.index = secondCollision.index; } hashedId = _internals.hashId(opId.id); stepInfo.hashedId = hashedId; } const stepState = this.state.stepState[hashedId]; let isFulfilled = false; if (stepState) { stepState.seen = true; this.state.remainingStepsToBeSeen.delete(hashedId); if (this.state.allStateUsed()) await this.middlewareManager.onMemoizationEnd(); if (typeof stepState.input === "undefined") isFulfilled = true; stepInfo.memoized = isFulfilled; } else stepInfo.memoized = false; const wrappedHandler = this.middlewareManager.buildWrapStepChain(entryPoint, stepInfo); return { hashedId, stepInfo, wrappedHandler, setActualHandler, stepState, isFulfilled }; } resumeStepWithResult(resultOp, resume = true) { const userlandStep = this.state.steps.get(resultOp.id); if (!userlandStep) throw new Error("Step not found in memoization state during async checkpointing; this should never happen and is a bug in the Inngest SDK"); userlandStep.data = require_functions.undefinedToNull(resultOp.data); userlandStep.timing = resultOp.timing; userlandStep.op = resultOp.op; userlandStep.id = resultOp.id; if (resume) { userlandStep.fulfilled = true; userlandStep.hasStepState = true; this.state.stepState[resultOp.id] = userlandStep; userlandStep.handle(); } return userlandStep; } 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"); return this.options.fn["onFailureFn"]; } initializeTimer(state) { if (!this.options.requestedRunStep) return; this.timeout = require_promises.createTimeoutPromise(this.timeoutDuration); this.timeout.then(async () => { await this.middlewareManager.onMemoizationEnd(); const { foundSteps, totalFoundSteps } = this.getStepNotFoundDetails(); state.setCheckpoint({ type: "step-not-found", step: { id: this.options.requestedRunStep, op: require_types.StepOpCode.StepNotFound }, foundSteps, totalFoundSteps }); }); } getStepNotFoundDetails() { const foundSteps = [...this.state.steps.values()].filter((step) => !step.hasStepState).map((step) => ({ id: step.hashedId, name: step.name, displayName: step.displayName })).sort((a, b) => a.id.localeCompare(b.id)); return { foundSteps: foundSteps.slice(0, STEP_NOT_FOUND_MAX_FOUND_STEPS), totalFoundSteps: foundSteps.length }; } initializeCheckpointRuntimeTimer(state) { this.devDebug("initializing checkpointing runtime timers", this.options.checkpointingConfig); if (this.options.checkpointingConfig?.maxRuntime) { const maxRuntimeMs = require_temporal.isTemporalDuration(this.options.checkpointingConfig.maxRuntime) ? this.options.checkpointingConfig.maxRuntime.total({ unit: "milliseconds" }) : typeof this.options.checkpointingConfig.maxRuntime === "string" ? (0, ms.default)(this.options.checkpointingConfig.maxRuntime) : this.options.checkpointingConfig.maxRuntime; if (Number.isFinite(maxRuntimeMs) && maxRuntimeMs > 0) { this.checkpointingMaxRuntimeTimer = require_promises.createTimeoutPromise(maxRuntimeMs); this.checkpointingMaxRuntimeTimer.then(async () => { await this.middlewareManager.onMemoizationEnd(); state.setCheckpoint({ type: "checkpointing-runtime-reached" }); }); } } if (this.options.checkpointingConfig?.maxInterval) { const maxIntervalMs = require_temporal.isTemporalDuration(this.options.checkpointingConfig.maxInterval) ? this.options.checkpointingConfig.maxInterval.total({ unit: "milliseconds" }) : typeof this.options.checkpointingConfig.maxInterval === "string" ? (0, ms.default)(this.options.checkpointingConfig.maxInterval) : this.options.checkpointingConfig.maxInterval; if (Number.isFinite(maxIntervalMs) && maxIntervalMs > 0) { this.checkpointingMaxBufferIntervalTimer = require_promises.createTimeoutPromise(maxIntervalMs); this.checkpointingMaxBufferIntervalTimer.then(async () => { state.setCheckpoint({ type: "checkpointing-buffer-interval-reached" }); this.checkpointingMaxBufferIntervalTimer?.reset(); }); } } } }; const hashId = (id) => { return sha1().update(id).digest("hex"); }; const hashOp = (op) => { return { ...op, id: hashId(op.id) }; }; /** * Resolves step ID collisions by appending an index suffix if needed. * Consolidates the duplicated collision detection logic. * * @param baseId - The original step ID * @param stepsMap - Map of existing steps (keyed by hashed ID) * @param expectedIndexes - Map tracking expected next index for each base ID * @returns The final ID to use and optional index */ function resolveStepIdCollision({ baseId, expectedIndexes, stepsMap }) { const hashedBaseId = hashId(baseId); if (!stepsMap.has(hashedBaseId) && !expectedIndexes.has(baseId)) { expectedIndexes.set(baseId, 1); return { finalId: baseId }; } const expectedNextIndex = expectedIndexes.get(baseId) ?? 1; const maxIndex = expectedNextIndex + stepsMap.size + 1; for (let i = expectedNextIndex; i < maxIndex; i++) { const indexedId = baseId + require_InngestStepTools.STEP_INDEXING_SUFFIX + i; const hashedIndexedId = hashId(indexedId); if (!stepsMap.has(hashedIndexedId)) { expectedIndexes.set(baseId, i + 1); return { finalId: indexedId, index: i }; } } throw new require_utils.UnreachableError(`Could not resolve step ID collision for "${baseId}" after ${stepsMap.size + 1} attempts`); } function isNonEmpty(arr) { return arr.length > 0; } /** * Exported for testing. */ const _internals = { hashOp, hashId, resolveStepIdCollision }; //#endregion exports._internals = _internals; exports.createExecutionEngine = createExecutionEngine; Object.defineProperty(exports, 'engine_exports', { enumerable: true, get: function () { return engine_exports; } }); //# sourceMappingURL=engine.cjs.map