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 lines • 18.9 kB
{"version":3,"file":"middleware.cjs","names":[],"sources":["../../../src/components/middleware/middleware.ts"],"sourcesContent":["import type { Jsonify } from \"../../helpers/jsonify.ts\";\nimport type { MaybePromise } from \"../../helpers/types.ts\";\nimport type {\n Context,\n EventPayload,\n JsonError,\n SendEventBaseOutput,\n StepOptions,\n} from \"../../types.ts\";\nimport type { Inngest } from \"../Inngest.ts\";\nimport type { InngestFunction } from \"../InngestFunction.ts\";\nimport type { createStepTools } from \"../InngestStepTools.ts\";\nimport type { OpenStringUnion } from \"./types.ts\";\n\n/**\n * Namespace containing middleware-related types and base class.\n */\nexport namespace Middleware {\n /**\n * Base interface for output transformers. Extend this and override `Out` to\n * create custom transformers. This is necessary because TypeScript doesn't\n * support higher-kinded types.\n *\n * @example\n * ```ts\n * interface BooleanToStringTransform extends Middleware.StaticTransform {\n * Out: this[\"In\"] extends boolean ? string : this[\"In\"];\n * }\n * ```\n */\n export type StaticTransform = {\n In: unknown;\n Out: unknown;\n };\n\n /**\n * Default transform. Applies the same transform as `JSON.stringify`.\n */\n export interface DefaultStaticTransform extends StaticTransform {\n Out: Jsonify<this[\"In\"]>;\n }\n\n /**\n * The step tools available to middleware for extending step functionality.\n * This is the same type as `step` in the function handler context.\n */\n export type StepTools = ReturnType<typeof createStepTools<Inngest.Any>>;\n\n /**\n * The argument passed to `transformSendEvent`.\n */\n export type TransformSendEventArgs = {\n events: EventPayload<Record<string, unknown>>[];\n readonly fn: DeepReadonly<InngestFunction.Any> | null;\n };\n\n /**\n * The argument passed to `transformStepInput`.\n */\n export type TransformStepInputArgs = {\n readonly fn: DeepReadonly<InngestFunction.Any>;\n readonly stepInfo: Readonly<\n Pick<StepInfo, \"hashedId\" | \"memoized\" | \"stepType\">\n >;\n stepOptions: StepOptions;\n input: unknown[];\n };\n\n /** The argument passed to `wrapStepHandler`. */\n export type WrapStepHandlerArgs = DeepReadonly<{\n ctx: Context.Any;\n fn: InngestFunction.Any;\n next: () => Promise<unknown>;\n stepInfo: StepInfo;\n }>;\n\n /**\n * A single memoized step entry received in `transformFunctionInput`.\n */\n type MemoizedStep =\n | { type: \"data\"; data: unknown }\n | { type: \"error\"; error: JsonError }\n | { type: \"input\"; input: unknown };\n\n /**\n * Memoized step state keyed by hashed step ID.\n */\n type MemoizedSteps = Record<string, MemoizedStep>;\n\n /**\n * The argument passed to `transformFunctionInput`.\n */\n export type TransformFunctionInputArgs = {\n ctx: Context.Any;\n readonly fn: DeepReadonly<InngestFunction.Any>;\n steps: MemoizedSteps;\n };\n\n /**\n * The argument passed to the static `onRegister` hook.\n */\n export type OnRegisterArgs = Readonly<{\n client: Inngest.Any;\n fn: InngestFunction.Any | null;\n }>;\n\n /**\n * Information about the incoming HTTP request that triggered this execution.\n */\n export type Request = {\n body: () => Promise<unknown>;\n headers: Readonly<Record<string, string>>;\n method: string;\n url: URL;\n };\n\n /** The argument passed to `wrapFunctionHandler`. */\n export type WrapFunctionHandlerArgs = DeepReadonly<{\n ctx: Context.Any;\n fn: InngestFunction.Any;\n next: () => Promise<unknown>;\n }>;\n\n /** The argument passed to `wrapRequest`. */\n export type WrapRequestArgs = DeepReadonly<{\n fn: InngestFunction.Any | null;\n next: () => Promise<Response>;\n requestArgs: readonly unknown[];\n requestInfo: Request;\n runId: string;\n }>;\n\n /** The argument passed to `wrapSendEvent`. */\n export type WrapSendEventArgs = DeepReadonly<{\n events: EventPayload<Record<string, unknown>>[];\n fn: InngestFunction.Any | null;\n next: () => Promise<SendEventBaseOutput>;\n }>;\n\n /** The argument passed to `wrapStep`. */\n export type WrapStepArgs = DeepReadonly<{\n ctx: Context.Any;\n fn: InngestFunction.Any;\n next: () => Promise<unknown>;\n stepInfo: StepInfo;\n }>;\n\n /**\n * The shape of the HTTP response returned by the middleware chain.\n * This is what `next()` resolves with inside `wrapRequest`.\n */\n export type Response = {\n body: string;\n headers: Record<string, string>;\n status: number;\n };\n\n /**\n * The argument passed to `onMemoizationEnd`.\n */\n export type OnMemoizationEndArgs = DeepReadonly<{\n ctx: Context.Any;\n fn: InngestFunction.Any;\n }>;\n\n /**\n * The argument passed to `onStepStart`.\n */\n export type OnStepStartArgs = DeepReadonly<{\n ctx: Context.Any;\n fn: InngestFunction.Any;\n stepInfo: StepInfo;\n }>;\n\n /**\n * The argument passed to `onStepError`.\n */\n export type OnStepErrorArgs = DeepReadonly<{\n ctx: Context.Any;\n error: Error;\n fn: InngestFunction.Any;\n\n /**\n * Whether this is the final attempt for the step, meaning retries are\n * exhausted or the error is non-retriable. When `false`, the step will be\n * retried.\n */\n isFinalAttempt: boolean;\n\n stepInfo: StepInfo;\n }>;\n\n /**\n * The argument passed to `onStepComplete`.\n */\n export type OnStepCompleteArgs = DeepReadonly<{\n ctx: Context.Any;\n fn: InngestFunction.Any;\n output: unknown;\n stepInfo: StepInfo;\n }>;\n\n /**\n * The argument passed to `onRunStart`.\n */\n export type OnRunStartArgs = DeepReadonly<{\n ctx: Context.Any;\n fn: InngestFunction.Any;\n }>;\n\n /**\n * The argument passed to `onRunComplete`.\n */\n export type OnRunCompleteArgs = DeepReadonly<{\n ctx: Context.Any;\n fn: InngestFunction.Any;\n output: unknown;\n }>;\n\n /**\n * The argument passed to `onRunError`.\n */\n export type OnRunErrorArgs = DeepReadonly<{\n ctx: Context.Any;\n error: Error;\n fn: InngestFunction.Any;\n\n /**\n * Whether this is the final attempt for the function, meaning retries are\n * exhausted or the error is non-retriable. When `false`, the function will\n * be retried.\n */\n isFinalAttempt: boolean;\n }>;\n\n /**\n * The type of step. This union may be extended in the future, and will not be\n * considered a breaking change.\n */\n export type StepType = OpenStringUnion<\n | \"ai.infer\"\n | \"ai.wrap\"\n | \"fetch\"\n | \"group.experiment\"\n | \"invoke\"\n | \"realtime.publish\"\n | \"run\"\n | \"sendEvent\"\n | \"sleep\"\n | \"waitForEvent\"\n >;\n\n export type StepInfo = {\n /**\n * Unique ID for the step. This is a hash of the user-defined step ID,\n * including the implicit index if the user-defined ID is not unique.\n */\n hashedId: string;\n\n /**\n * The arguments passed to the step function, if any. For `step.run()`,\n * these are the arguments after the id and handler function.\n */\n input?: unknown[];\n\n /**\n * Whether the step result is being retrieved from memoized state (true)\n * or being executed fresh (false).\n */\n memoized: boolean;\n\n /**\n * Based on the first argument passed to the `step` method.\n */\n options: StepOptions;\n\n stepType: StepType;\n };\n\n /**\n * Base class for creating middleware. Extend this class to create custom\n * middleware with hooks for step execution.\n */\n // @privateRemark\n // Methods are nullish instead of noops as a performance optimization. This is\n // primarily because of `wrapStep`. Each defined `wrapStep` method adds 1 more\n // promise to the chain for each step. This chain runs every time the step\n // completes/errors (even when memoized).\n export abstract class BaseMiddleware {\n readonly client: Inngest.Any;\n\n /**\n * Used to identify the middleware instance in logs. Uniqueness is not\n * required, though using multiple middleware with the same ID in the same\n * app may cause confusion when debugging.\n */\n abstract readonly id: string;\n\n /**\n * Declare this to statically specify how function return types are\n * transformed. By default, the function return type is Jsonified.\n *\n * Must match the same structure as `StaticTransform` to imitate\n * higher-kinded types.\n *\n * @example\n * ```ts\n * interface PreserveDate extends Middleware.StaticTransform {\n * Out: this[\"In\"] extends Date ? Date : Jsonify<this[\"In\"]>;\n * }\n *\n * class MyMiddleware extends Middleware.BaseMiddleware {\n * declare functionOutputTransform: PreserveDate;\n * }\n * ```\n *\n * @default Middleware.DefaultStaticTransform (e.g. Date -> string)\n */\n declare functionOutputTransform: DefaultStaticTransform;\n\n /**\n * Declare this to statically specify how step output types are transformed.\n * By default, the step output type is Jsonified.\n *\n * Must match the same structure as `StaticTransform` to imitate\n * higher-kinded types.\n *\n * @example\n * ```ts\n * interface PreserveDate extends Middleware.StaticTransform {\n * Out: this[\"In\"] extends Date ? Date : Jsonify<this[\"In\"]>;\n * }\n *\n * class MyMiddleware extends Middleware.BaseMiddleware {\n * declare stepOutputTransform: PreserveDate;\n * }\n * ```\n *\n * @default Middleware.DefaultStaticTransform (e.g. Date -> string)\n */\n declare stepOutputTransform: DefaultStaticTransform;\n\n constructor({ client }: { client: Inngest.Any }) {\n this.client = client;\n }\n\n /**\n * Called when the middleware class is added to an Inngest client or Inngest\n * function. Use this for one-time setup that needs a reference to the\n * client instance (e.g. registering processors, setting feature flags).\n *\n * Do not mutate arguments.\n */\n static onRegister?(args: Middleware.OnRegisterArgs): void;\n\n /**\n * Called 1 time per request, after memoization completes.\n *\n * If all memoized steps have been resolved/rejected, then this hook calls.\n * This is at the top of the function handler when there are 0 memoized\n * steps.\n *\n * If a new step is found before resolving/rejecting all memoized steps,\n * then this calls.\n *\n * Do not mutate arguments.\n */\n onMemoizationEnd?(arg: Middleware.OnMemoizationEndArgs): MaybePromise<void>;\n\n /**\n * Called when the run completes successfully. Does NOT call when the run\n * errors: `onRunError` calls instead.\n *\n * Do not mutate arguments.\n */\n onRunComplete?(arg: Middleware.OnRunCompleteArgs): MaybePromise<void>;\n\n /**\n * Called when the function throws an error.\n *\n * Do not mutate arguments.\n */\n onRunError?(arg: Middleware.OnRunErrorArgs): MaybePromise<void>;\n\n /**\n * Called 1 time per run on the very first request (0 memoized steps,\n * attempt 0). Does NOT call on subsequent requests where steps are being\n * replayed.\n *\n * Do not mutate arguments.\n */\n onRunStart?(arg: Middleware.OnRunStartArgs): MaybePromise<void>;\n\n /**\n * Called when a step successfully completes. Only called for `step.run`\n * and `step.sendEvent`. Never called for memoized step outputs. Does NOT\n * call when the step errors: `onStepError` calls instead.\n *\n * Do not mutate arguments.\n */\n onStepComplete?(arg: Middleware.OnStepCompleteArgs): MaybePromise<void>;\n\n /**\n * Called each time a step errors. Only called for `step.run` and\n * `step.sendEvent`. Never called for memoized errors.\n *\n * Do not mutate arguments.\n */\n onStepError?(arg: Middleware.OnStepErrorArgs): MaybePromise<void>;\n\n /**\n * Called 1 time per step before running its handler. Only called for\n * `step.run` and `step.sendEvent`.\n *\n * Do not mutate arguments.\n */\n onStepStart?(arg: Middleware.OnStepStartArgs): MaybePromise<void>;\n\n /**\n * Called 1 time per request (likely multiple times per run). Return the\n * (potentially modified) arg object.\n *\n * Use cases:\n * - Dependency injection.\n * - Deserialize events before passing it to the function handler.\n *\n * Do not mutate arguments.\n */\n // @privateRemark\n // This hook exists because `wrapFunctionHandler` can't be used for the\n // transformation's static type inference. For example, if the user added\n // `ctx.db` in `wrapFunctionHandler` then the static types wouldn't show\n // `ctx.db` in the function handler.\n transformFunctionInput?(\n arg: Middleware.TransformFunctionInputArgs,\n ): MaybePromise<Middleware.TransformFunctionInputArgs>;\n\n /**\n * Called when sending events. This is either `step.sendEvent` or\n * `client.send`. Return the (potentially modified) arg object.\n *\n * Use cases:\n * - Serialize event data before sending it to the Inngest Server.\n *\n * Do not mutate arguments.\n */\n transformSendEvent?(\n arg: Middleware.TransformSendEventArgs,\n ): MaybePromise<Middleware.TransformSendEventArgs>;\n\n /**\n * Called 1 time per step per request (likely multiple times per step).\n * Return the (potentially modified) arg object.\n *\n * Use cases:\n * - Modify step options (e.g. the step ID).\n * - Modify step input args.\n *\n * Do not mutate arguments.\n */\n // @privateRemark\n // Step input transformation could happen in `wrapStep`, but we chose not to\n // for the following reasons:\n // 1. `wrapStep` may have a negative performance impact under certain\n // workloads.\n // 2. `wrapStep` is a little more complicated to use.\n // 3. Since `transformFunctionInput` must exist, having this hook\n // establishes a consistent pattern for input transformation.\n transformStepInput?(\n arg: TransformStepInputArgs,\n ): MaybePromise<TransformStepInputArgs>;\n\n /**\n * Called 1 time per request.\n *\n * Use cases:\n * - AsyncLocalStorage context.\n * - Function-level output/error transformation.\n * - Prepend/append steps around the function handler.\n *\n * Must call `next()` to continue processing. Do not mutate arguments.\n *\n * **Important:** `next()` only resolves when the function completes. On\n * requests where a fresh step is discovered, control flow is interrupted\n * and `next()` never resolves.\n */\n wrapFunctionHandler?(args: WrapFunctionHandlerArgs): Promise<unknown>;\n\n /**\n * Called 1 time per request.\n *\n * Use cases:\n * - Custom auth.\n * - Expose request data to the Inngest function handler.\n * - Metrics.\n *\n * Must call `next()` to continue processing. Do not mutate arguments.\n */\n wrapRequest?(args: WrapRequestArgs): Promise<Response>;\n\n /**\n * Called each time events are sent (either `client.send` or\n * `step.sendEvent`).\n *\n * Use cases:\n * - Backup events (e.g. blob store) when they fail to send.\n * - Metrics.\n *\n * Must call `next()` to continue processing. Do not mutate arguments.\n */\n wrapSendEvent?(args: WrapSendEventArgs): Promise<SendEventBaseOutput>;\n\n /**\n * Called 1 time per step per request. Called for all step kinds. Depending\n * on your use case, you may want `wrapStepHandler` instead.\n *\n * Use cases:\n * - Deserialize step output before returning it to the function handler.\n * - Handle step failure errors (after exhausting retries).\n * - Prepend/append steps around a step.\n *\n * Must call `next()` to continue processing. Do not mutate arguments.\n *\n * NOTE: `next()` only resolves when the step completes/fails. On requests\n * where a fresh step is discovered, control flow is interrupted and\n * `next()` never resolves.\n */\n wrapStep?(args: WrapStepArgs): Promise<unknown>;\n\n /**\n * Called 1 time per step attempt. Wraps the step's handler. Only called for\n * `step.run` and `step.sendEvent`. Use this to modify the handler's\n * returned output or thrown error before it's sent to the Inngest Server.\n *\n * Use cases:\n * - Serialize step output before sending it to the Inngest Server.\n * - Handle step attempt errors (before exhausting retries).\n *\n * Must call `next()` to continue processing. Do not mutate arguments.\n */\n // @privateRemark\n // This hook exists because of checkpointing. For serialization middleware\n // to work with checkpointing, we need to both:\n // 1. Serialize the step output before sending it to the Inngest Server.\n // 2. Deserialize the step output before returning it to the function handler.\n //\n // We initially solved this by calling `wrapStep` twice per step per\n // request. But this breaks the \"prepend with step\" logic since we'd\n // encounter the prepended step twice; this caused us to run the prepended\n // step twice.\n //\n // Now that we have `wrapStepHandler`, it can be responsible for\n // serialization and `wrapStep` can be responsible for deserialization.\n wrapStepHandler?(args: WrapStepHandlerArgs): Promise<unknown>;\n }\n\n /**\n * A no-arg constructor for a BaseMiddleware subclass. Used in client and\n * function options so that fresh instances are created per-request.\n */\n export type Class = (new (args: {\n client: Inngest.Any;\n }) => BaseMiddleware) & {\n // Static methods aren't captured by `new () => ...`, so we repeat it here.\n onRegister?(arg: OnRegisterArgs): void;\n };\n}\n\ntype DeepReadonly<T> = T extends (infer U)[]\n ? readonly DeepReadonly<U>[]\n : // Do not make functions readonly, since that leads to weird stuff like not\n // being able to call `.catch()` on the returned promise.\n T extends Function\n ? T\n : // Do not recurse into these types. If that happens, then the resulting\n // type will not be compatible with the original type\n T extends Date | RegExp | Error | Map<unknown, unknown> | Set<unknown>\n ? Readonly<T>\n : T extends object\n ? { readonly [K in keyof T]: DeepReadonly<T[K]> }\n : T;\n"],"mappings":";;;;CAgSS,MAAe,eAAe;EACnC,AAAS;EAqDT,YAAY,EAAE,UAAmC;AAC/C,QAAK,SAAS"}