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.
621 lines (619 loc) • 20.3 kB
JavaScript
const require_consts = require('../helpers/consts.cjs');
const require_env = require('../helpers/env.cjs');
const require_errors = require('../helpers/errors.cjs');
const require_types = require('../types.cjs');
const require_log = require('../helpers/log.cjs');
const require_promises = require('../helpers/promises.cjs');
const require_strings = require('../helpers/strings.cjs');
const require_als = require('./execution/als.cjs');
const require_api = require('../api/api.cjs');
const require_crypto = require('../helpers/crypto.cjs');
const require_logger = require('../middleware/logger.cjs');
const require_InngestFunction = require('./InngestFunction.cjs');
const require_middleware = require('./middleware/middleware.cjs');
const require_InngestMetadata = require('./InngestMetadata.cjs');
const require_InngestStepTools = require('./InngestStepTools.cjs');
const require_utils = require('./middleware/utils.cjs');
const require_index = require('./realtime/subscribe/index.cjs');
const require_typeHelpers = require('./triggers/typeHelpers.cjs');
//#region src/components/Inngest.ts
/**
* A client used to interact with the Inngest API by sending or reacting to
* events.
*
* ```ts
* const inngest = new Inngest({ id: "my-app" });
* ```
*
* @public
*/
/**
* Symbol for accessing the SDK's internal logger. Not part of the public API.
* @internal
*/
const internalLoggerSymbol = Symbol.for("inngest.internalLogger");
var Inngest = class Inngest {
get [Symbol.toStringTag]() {
return Inngest.Tag;
}
/**
* The ID of this instance, most commonly a reference to the application it
* resides in.
*
* The ID of your client should remain the same for its lifetime; if you'd
* like to change the name of your client as it appears in the Inngest UI,
* change the `name` property instead.
*/
id;
/**
* Stores the options so we can remember explicit settings the user has
* provided.
*/
options;
inngestApi;
_userProvidedFetch;
_cachedFetch;
_logger;
/**
* Logger for SDK internal messages. Falls back to the user's `logger` if
* `internalLogger` is not provided in client options.
*
* @internal
*/
[internalLoggerSymbol];
localFns = [];
/**
* Middleware instances that provide simpler hooks.
*/
middleware;
_env = {};
_appVersion;
/**
* @internal
* Flag set by metadataMiddleware to enable step.metadata()
*/
experimentalMetadataEnabled = false;
/**
* A dummy Inngest function used in Durable Endpoints. This is necessary
* because the vast majority of middleware hooks require the Inngest function.
* But for Durable Endpoints, there is no Inngest function. So we need some
* placeholder.
*/
dummyDurableEndpointFunction = null;
getDummyDurableEndpointFunction() {
if (this.dummyDurableEndpointFunction) return this.dummyDurableEndpointFunction;
this.dummyDurableEndpointFunction = new require_InngestFunction.InngestFunction(this, {
id: "__proxy__",
triggers: []
}, async () => {});
return this.dummyDurableEndpointFunction;
}
/**
* Try to parse the `INNGEST_DEV` environment variable as a URL.
* Returns the URL if valid, otherwise `undefined`.
*/
get explicitDevUrl() {
const devEnvValue = this._env[require_consts.envKeys.InngestDevMode];
if (typeof devEnvValue !== "string" || !devEnvValue) return;
if (require_env.parseAsBoolean(devEnvValue) !== void 0) return;
try {
return new URL(require_env.normalizeUrl(devEnvValue));
} catch {
return;
}
}
/**
* Given a default cloud URL, return the appropriate URL based on the
* current mode and environment variables.
*
* If `INNGEST_DEV` is set to a URL, that URL is used. Otherwise, we use
* the default cloud URL in cloud mode or the default dev server host in
* dev mode.
*/
resolveDefaultUrl(cloudUrl) {
const explicitDevUrl = this.explicitDevUrl;
if (explicitDevUrl) return explicitDevUrl.href;
return this.mode === "cloud" ? cloudUrl : require_consts.defaultDevServerHost;
}
get apiBaseUrl() {
return this.options.baseUrl || this._env[require_consts.envKeys.InngestApiBaseUrl] || this._env[require_consts.envKeys.InngestBaseUrl] || this.resolveDefaultUrl(require_consts.defaultInngestApiBaseUrl);
}
get eventBaseUrl() {
return this.options.baseUrl || this._env[require_consts.envKeys.InngestEventApiBaseUrl] || this._env[require_consts.envKeys.InngestBaseUrl] || this.resolveDefaultUrl(require_consts.defaultInngestEventBaseUrl);
}
get eventKey() {
return this.options.eventKey || this._env[require_consts.envKeys.InngestEventKey] || void 0;
}
get fetch() {
if (!this._cachedFetch) this._cachedFetch = this._userProvidedFetch ? require_env.getFetch(this[internalLoggerSymbol], this._userProvidedFetch) : require_env.getFetch(this[internalLoggerSymbol], globalThis.fetch);
return this._cachedFetch;
}
get signingKey() {
return this.options.signingKey || this._env[require_consts.envKeys.InngestSigningKey];
}
get signingKeyFallback() {
return this.options.signingKeyFallback || this._env[require_consts.envKeys.InngestSigningKeyFallback];
}
get headers() {
return require_env.inngestHeaders({
inngestEnv: this.options.env,
env: this._env
});
}
/**
* The base logger for this client. Passed to user functions as `ctx.logger`.
*/
get logger() {
return this._logger;
}
get env() {
return this.headers[require_consts.headerKeys.Environment] ?? null;
}
get appVersion() {
return this._appVersion;
}
/**
* Access the metadata builder for updating run and step metadata.
*
* @example
* ```ts
* // Update metadata for the current run
* await inngest.metadata.update({ status: "processing" });
*
* // Update metadata for a different run
* await inngest.metadata.run(otherRunId).update({ key: "val" });
*
* ```
*/
get metadata() {
if (!this.experimentalMetadataEnabled) throw new Error("inngest.metadata is experimental. Enable it by adding metadataMiddleware() from \"inngest/experimental\" to your client middleware.");
return new require_InngestMetadata.UnscopedMetadataBuilder(this);
}
/**
* A client used to interact with the Inngest API by sending or reacting to
* events.
*
* ```ts
* const inngest = new Inngest({ id: "my-app" });
* ```
*/
constructor(options) {
this.options = options;
const { id, logger, middleware, appVersion } = this.options;
if (!id) throw new Error("An `id` must be passed to create an Inngest instance.");
this.id = id;
this._env = { ...require_env.allProcessEnv() };
this._userProvidedFetch = options.fetch;
this.inngestApi = new require_api.InngestApi({
baseUrl: () => this.apiBaseUrl,
signingKey: () => this.signingKey,
signingKeyFallback: () => this.signingKeyFallback,
fetch: () => this.fetch
});
this._logger = logger ?? new require_logger.ConsoleLogger();
this[internalLoggerSymbol] = this.options.internalLogger ?? this._logger;
this.middleware = [...builtInMiddleware(this._logger), ...middleware ?? []];
for (const mw of this.middleware) mw.onRegister?.({
client: this,
fn: null
});
this._appVersion = appVersion;
}
/**
* Returns a `Promise` that resolves when the app is ready and all middleware
* has been initialized.
*/
get ready() {
return Promise.resolve();
}
/**
* Set the environment variables for this client. This is useful if you are
* passed environment variables at runtime instead of as globals and need to
* update the client with those values as requests come in.
*/
setEnvVars(env = require_env.allProcessEnv()) {
this._env = {
...this._env,
...env
};
return this;
}
get mode() {
if (typeof this.options.isDev === "boolean") return this.options.isDev ? "dev" : "cloud";
const envIsDev = require_env.parseAsBoolean(this._env[require_consts.envKeys.InngestDevMode]);
if (typeof envIsDev === "boolean") return envIsDev ? "dev" : "cloud";
if (this.explicitDevUrl) return "dev";
return "cloud";
}
/**
* Given a response from Inngest, relay the error to the caller.
*/
async getResponseError(response, rawBody, foundErr = "Unknown error") {
let errorMessage = foundErr;
if (errorMessage === "Unknown error") switch (response.status) {
case 401:
errorMessage = "Event key Not Found";
break;
case 400:
errorMessage = "Cannot process event payload";
break;
case 403:
errorMessage = "Forbidden";
break;
case 404:
errorMessage = "Event key not found";
break;
case 406:
errorMessage = `${JSON.stringify(await rawBody)}`;
break;
case 409:
case 412:
errorMessage = "Event transformation failed";
break;
case 413:
errorMessage = "Event payload too large";
break;
case 500:
errorMessage = "Internal server error";
break;
default:
try {
errorMessage = await response.text();
} catch (_err) {
errorMessage = `${JSON.stringify(await rawBody)}`;
}
break;
}
return /* @__PURE__ */ new Error(`Inngest API Error: ${response.status} ${errorMessage}`);
}
eventKeySet() {
return this.eventKey !== void 0;
}
/**
* EXPERIMENTAL: This API is not yet stable and may change in the future
* without a major version bump.
*
* Send a Signal to Inngest.
*/
async sendSignal({ signal, data, env }) {
const headers = { ...env ? { [require_consts.headerKeys.Environment]: env } : {} };
return this._sendSignal({
signal,
data,
headers
});
}
async _sendSignal({ signal, data, headers }) {
const res = await this.inngestApi.sendSignal({
signal,
data
}, {
...this.headers,
...headers
});
if (res.ok) return res.value;
throw new Error(`Failed to send signal: ${res.error?.error || "Unknown error"}`);
}
async updateMetadata({ target, metadata, headers }) {
const res = await this.inngestApi.updateMetadata({
target,
metadata
}, { headers });
if (res.ok) return res.value;
throw new Error(`Failed to update metadata: ${res.error?.error || "Unknown error"}`);
}
async warnMetadata(target, kind, log) {
const fields = {};
if (log.code) fields.code = log.code;
if (log.explanation) fields.explanation = log.explanation;
if (log.action) fields.action = log.action;
if (log.docs) fields.docs = log.docs;
if (Object.keys(fields).length > 0) this[internalLoggerSymbol].warn(fields, log.message);
else this[internalLoggerSymbol].warn(log.message);
if (!this.experimentalMetadataEnabled) return;
await this.updateMetadata({
target,
metadata: [{
kind: "inngest.warnings",
op: "merge",
values: { [`sdk.${kind}`]: require_log.formatLogMessage(log) }
}]
});
}
/**
* Realtime-related functionality for this Inngest client.
*/
realtime = {
publish: async (topicRef, data) => {
const topicConfig = topicRef.config;
if (topicConfig && "schema" in topicConfig && topicConfig.schema) {
if ((await topicConfig.schema["~standard"].validate(data)).issues) throw new Error(`Schema validation failed for topic "${topicRef.topic}"`);
}
const runId = (await require_als.getAsyncCtx())?.execution?.ctx.runId;
const res = await this.inngestApi.publish({
channel: topicRef.channel,
topics: [topicRef.topic],
runId
}, data);
if (!res.ok) throw new Error(`Failed to publish to realtime: ${res.error?.error || "Unknown error"}`);
},
subscribe: async (opts) => {
return require_index.subscribe({
...opts,
app: this
});
},
token: async (opts) => {
return require_index.getSubscriptionToken(this, opts);
}
};
endpoint(handler) {
if (!this.options.endpointAdapter) throw new Error("No endpoint adapter configured for this Inngest client.");
return this.options.endpointAdapter({ client: this })(handler);
}
/**
* Creates a proxy handler that polls Inngest for durable endpoint results.
*
* The proxy:
* - Extracts `runId` and `token` from query params
* - Fetches the result from Inngest API
* - Runs the response through middleware (e.g., decryption)
* - Adds CORS headers
*
* Use this in combination with the `asyncRedirectUrl` option on your
* endpoint adapter to redirect users to your own proxy endpoint instead
* of directly to Inngest.
*
* @example
* ```ts
* import { Inngest } from "inngest";
* import { endpointAdapter } from "inngest/edge";
*
* const inngest = new Inngest({
* id: "my-app",
* endpointAdapter: endpointAdapter.withOptions({
* asyncRedirectUrl: "/api/inngest/poll",
* }),
* });
*
* // Durable endpoint
* export const GET = inngest.endpoint(async (req) => {
* const result = await step.run("work", () => "done");
* return new Response(result);
* });
*
* // Proxy endpoint at /api/inngest/poll
* export const GET = inngest.endpointProxy();
* ```
*/
endpointProxy() {
if (!this.options.endpointAdapter) throw new Error("No endpoint adapter configured for this Inngest client.");
if (!this.options.endpointAdapter.createProxyHandler) throw new Error("The configured endpoint adapter does not support proxy handlers.");
return this.options.endpointAdapter.createProxyHandler({ client: this });
}
/**
* Decrypt a proxy response using the client's middleware stack.
*
* Runs `transformFunctionInput` on each middleware instance to decrypt
* step data (used by encryption middleware).
*
* Uses type assertions because we're creating a minimal "fake" execution
* context just to run the decryption middleware hooks - not a full execution.
*
* @internal
*/
async decryptProxyResult(result) {
if (!result.data) return result;
const mwInstances = this.middleware.map((Cls) => {
return new Cls({ client: this });
});
const dummyEvent = {
name: "__proxy__",
data: {}
};
let transformArgs = {
ctx: {
event: dummyEvent,
events: [dummyEvent],
runId: "__proxy__",
attempt: 0,
step: require_InngestStepTools.step
},
fn: this.getDummyDurableEndpointFunction(),
steps: { __result__: {
type: "data",
data: result.data
} }
};
for (const mw of mwInstances) if (mw.transformFunctionInput) transformArgs = await mw.transformFunctionInput(transformArgs);
const decryptedStep = transformArgs.steps?.__result__;
let decryptedData = result.data;
if (decryptedStep && "data" in decryptedStep) decryptedData = decryptedStep.data;
return {
...result,
data: decryptedData
};
}
/**
* Send one or many events to Inngest. Takes an entire payload (including
* name) as each input.
*
* ```ts
* await inngest.send({ name: "app/user.created", data: { id: 123 } });
* ```
*
* Returns a promise that will resolve if the event(s) were sent successfully,
* else throws with an error explaining what went wrong.
*/
async send(payload, options) {
const headers = { ...options?.env ? { [require_consts.headerKeys.Environment]: options.env } : {} };
return this._send({
payload,
headers,
fnMiddleware: [],
fn: null
});
}
/**
* Internal method for sending an event, used to allow Inngest internals to
* further customize the request sent to an Inngest Server.
*/
async _send({ payload, headers, fn, fnMiddleware }) {
const nowMillis = (/* @__PURE__ */ new Date()).getTime();
let maxAttempts = 5;
try {
const entropy = require_crypto.createEntropy(10);
const entropyBase64 = Buffer.from(entropy).toString("base64");
headers = {
...headers,
[require_consts.headerKeys.EventIdSeed]: `${nowMillis},${entropyBase64}`
};
} catch (err) {
this[internalLoggerSymbol].debug({ err }, "Event-sending retries disabled");
maxAttempts = 1;
}
let payloads = Array.isArray(payload) ? payload : payload ? [payload] : [];
const mwInstances = [...this.middleware, ...fnMiddleware].map((Cls) => new Cls({ client: this }));
for (const mw of mwInstances) if (mw?.transformSendEvent) {
const transformed = await mw.transformSendEvent({
events: payloads,
fn: fn ?? null
});
if (transformed !== void 0) payloads = transformed.events;
}
for (const payload$1 of payloads) if (require_typeHelpers.isValidatable(payload$1)) await payload$1.validate();
payloads = payloads.map((p) => {
return {
...p,
id: p.id,
ts: p.ts || nowMillis,
data: p.data || {}
};
});
/**
* It can be valid for a user to send an empty list of events; if this
* happens, show a warning that this may not be intended, but don't throw.
*/
if (!payloads.length) {
this[internalLoggerSymbol].warn("inngest.send() called with no events; the returned promise will resolve, but no events have been sent");
return { ids: [] };
}
/**
* If in prod mode and key is not present, fail now.
*/
if (this.mode === "cloud" && !this.eventKeySet()) throw new Error(require_log.formatLogMessage({
message: "Failed to send event",
explanation: "Your event or events were not sent to Inngest. We couldn't find an event key to use to send events to Inngest.",
action: require_errors.fixEventKeyMissingSteps.join("; ")
}));
const innerHandler = async () => {
return { ids: (await require_promises.retryWithBackoff(async () => {
let rawBody;
let body;
const url = new URL(`e/${this.eventKey ?? require_consts.dummyEventKey}`, this.eventBaseUrl);
const response = await this.fetch(url.href, {
method: "POST",
body: require_strings.stringify(payloads),
headers: {
...this.headers,
...headers
}
});
try {
rawBody = await response.json();
body = await require_types.sendEventResponseSchema.parseAsync(rawBody);
} catch (_err) {
throw await this.getResponseError(response, rawBody);
}
if (body.status !== 200 || body.error) throw await this.getResponseError(response, rawBody, body.error);
return body;
}, {
maxAttempts,
baseDelay: 100
})).ids };
};
return await require_utils.buildWrapSendEventChain(mwInstances, innerHandler, payloads, fn)();
}
createFunction = (rawOptions, handler) => {
const fn = this._createFunction(rawOptions, handler);
for (const mw of fn.opts.middleware ?? []) mw.onRegister?.({
client: this,
fn
});
this.localFns.push(fn);
return fn;
};
get funcs() {
return this.localFns;
}
_createFunction = (rawOptions, handler) => {
if (typeof handler !== "function") throw new Error(`"createFunction" expected a handler function as the second argument. Triggers belong in the first argument: createFunction({ id, triggers: { event: "..." } }, handler)`);
const options = {
...rawOptions,
triggers: this.sanitizeTriggers(rawOptions.triggers)
};
return new require_InngestFunction.InngestFunction(this, options, handler);
};
/**
* Runtime-only validation.
*/
sanitizeTriggers(triggers) {
if (triggers === void 0) return [];
if (!Array.isArray(triggers)) return [triggers];
return triggers;
}
};
/**
* Default middleware that is included in every client, placed before the user's
* middleware. Returns new-style `Middleware.Class` constructors. Uses a closure
* so the no-arg constructors can capture the base logger.
*/
function builtInMiddleware(baseLogger) {
return [class LoggerMiddleware extends require_middleware.Middleware.BaseMiddleware {
id = "inngest:logger";
proxyLogger = new require_logger.ProxyLogger(baseLogger);
transformFunctionInput(arg) {
let logger = baseLogger;
if ("child" in logger) try {
logger = logger.child({
runID: arg.ctx.runId,
eventName: arg.ctx.event.name
});
} catch (err) {
logger.error({ err }, "failed to create \"childLogger\" with error");
}
this.proxyLogger = new require_logger.ProxyLogger(logger);
return {
...arg,
ctx: Object.assign({}, arg.ctx, { logger: this.proxyLogger })
};
}
onMemoizationEnd() {
this.proxyLogger.enable();
}
onStepError(arg) {
this.proxyLogger.error({ err: arg.error }, "Inngest step error");
}
wrapFunctionHandler({ next }) {
return next().catch((err) => {
this.proxyLogger.error({ err }, "Inngest function error");
throw err;
});
}
wrapRequest({ next }) {
return next().finally(() => this.proxyLogger.flush());
}
}];
}
(function(_Inngest) {
_Inngest.Tag = "Inngest.App";
})(Inngest || (Inngest = {}));
//#endregion
Object.defineProperty(exports, 'Inngest', {
enumerable: true,
get: function () {
return Inngest;
}
});
exports.internalLoggerSymbol = internalLoggerSymbol;
//# sourceMappingURL=Inngest.cjs.map