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.

238 lines (236 loc) 7.62 kB
import { InngestCommHandler } from "./components/InngestCommHandler.js"; import { createWebApiCommHandler } from "./components/createWebApiCommHandler.js"; import { handleDurableEndpointProxyRequest } from "./components/InngestDurableEndpointProxy.js"; import { InngestEndpointAdapter } from "./components/InngestEndpointAdapter.js"; import http from "node:http"; import { URL } from "node:url"; //#region src/node.ts /** * The name of the framework, used to identify the framework in Inngest * dashboards and during testing. */ const frameworkName = "nodejs"; /** * Read the incoming message body as text. * * Collects Buffer chunks and decodes once with `Buffer.concat` so multi-byte * UTF-8 characters aren't corrupted when split across chunk boundaries. * Reads via `req.on('data'|'end')` so body-replay wrappers — notably * `@vercel/node`'s `restoreBody()`, which patches only those two events — * deliver the replayed bytes; async-iterator and `readable`-event readers * see an empty body under that wrapper. */ async function readRequestBody(req) { return new Promise((resolve, reject) => { const chunks = []; req.on("data", (chunk) => chunks.push(chunk)); req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8"))); req.on("error", reject); }); } function getURL(req, hostnameOption) { const protocol = req.headers["x-forwarded-proto"] || (req.socket?.encrypted ? "https" : "http"); const origin = hostnameOption || `${protocol}://${req.headers.host}`; return new URL(req.url || "", origin); } const commHandler = (options) => { return new InngestCommHandler({ frameworkName, ...options, handler: (req, res) => { return { body: () => readRequestBody(req), headers: (key) => { return req.headers[key] && Array.isArray(req.headers[key]) ? req.headers[key][0] : req.headers[key]; }, method: () => { if (!req.method) throw new Error("Request method not defined. Potential use outside of context of Server."); return req.method; }, url: () => getURL(req, options.serveOrigin), transformResponse: ({ body, status, headers }) => { res.writeHead(status, headers); res.end(body); }, transformStreamingResponse: async ({ body, headers, status }) => { res.writeHead(status, headers); const reader = body.getReader(); try { let done = false; while (!done) { const result = await reader.read(); done = result.done; if (!done) res.write(result.value); } res.end(); } catch (error) { if (error instanceof Error) res.destroy(error); else res.destroy(new Error(String(error))); } } }; } }); }; /** * Serve and register any declared functions with Inngest, making them available * to be triggered by events. * * @example Serve Inngest functions on all paths * ```ts * import { serve } from "inngest/node"; * import { inngest } from "./src/inngest/client"; * import myFn from "./src/inngest/myFn"; // Your own function * * const server = http.createServer(serve({ * client: inngest, functions: [myFn] * })); * server.listen(3000); * ``` * * @example Serve Inngest on a specific path * ```ts * import { serve } from "inngest/node"; * import { inngest } from "./src/inngest/client"; * import myFn from "./src/inngest/myFn"; // Your own function * * const server = http.createServer((req, res) => { * if (req.url.start === '/api/inngest') { * return serve({ * client: inngest, functions: [myFn] * })(req, res); * } * // ... * }); * server.listen(3000); * ``` * * @public */ const serve = (options) => { return commHandler(options).createHandler(); }; /** * EXPERIMENTAL - Create an http server to serve Inngest functions. * * @example * ```ts * import { createServer } from "inngest/node"; * import { inngest } from "./src/inngest/client"; * import myFn from "./src/inngest/myFn"; // Your own function * * const server = createServer({ * client: inngest, functions: [myFn] * }); * server.listen(3000); * ``` * * @public */ const createServer = (options) => { const server = http.createServer((req, res) => { const url = getURL(req, options.serveOrigin); const pathname = options.servePath || "/api/inngest"; if (url.pathname === pathname) return serve(options)(req, res); res.writeHead(404); res.end(); }); server.on("clientError", (_err, socket) => { socket.end("HTTP/1.1 400 Bad Request\r\n\r\n"); }); return server; }; /** * Comm handler for durable endpoints. Uses Web API Request/Response since * that's the interface users write against, regardless of the underlying * runtime. */ function endpointCommHandler(options, syncOptions) { return createWebApiCommHandler(frameworkName, options, syncOptions); } /** * Creates a durable endpoint proxy handler for Node.js environments. */ function createDurableEndpointProxyHandler(options) { return async (req, res) => { const url = getURL(req); const result = await handleDurableEndpointProxyRequest(options.client, { runId: url.searchParams.get("runId"), token: url.searchParams.get("token"), method: req.method || "GET" }); res.writeHead(result.status, result.headers); res.end(result.body); }; } /** * In a Node.js environment, create a function that can wrap any endpoint to be * able to use steps seamlessly within that API. */ const endpointAdapter = InngestEndpointAdapter.create((options) => { return endpointCommHandler(options, options).createSyncHandler(); }, createDurableEndpointProxyHandler); /** * Bridge a Web API endpoint handler to a Node.js `http.RequestListener`. * * Converts an incoming `http.IncomingMessage` into a Web API `Request`, * invokes the handler, then streams the resulting `Response` back through * the Node.js `http.ServerResponse`. * * Important: uses `value != null` (not `value`) when forwarding headers so * that empty-string headers (like `X-Inngest-Signature: ""` in dev mode) * are preserved. Dropping them breaks `isInngestReq()` detection. */ function serveEndpoint(handler) { return async (req, res) => { const body = await readRequestBody(req); const headers = new Headers(); for (const [key, value] of Object.entries(req.headers)) if (value != null) { if (Array.isArray(value)) for (const v of value) headers.append(key, v); else if (typeof value === "string") headers.set(key, value); } const url = getURL(req); const webRequest = new Request(url.href, { method: req.method, headers, body: body.length > 0 ? body : void 0 }); try { const webResponse = await handler(webRequest); const resHeaders = {}; webResponse.headers.forEach((v, k) => { resHeaders[k] = v; }); res.writeHead(webResponse.status, resHeaders); if (webResponse.body) { const reader = webResponse.body.getReader(); while (true) { const { done, value } = await reader.read(); if (done) break; res.write(value); } } res.end(); } catch (err) { if (!res.headersSent) res.writeHead(500); res.end(String(err)); } }; } /** * Create an HTTP server that serves a durable endpoint handler. * * This bridges the Web API `Request`/`Response` interface that Durable * Endpoints use with Node.js's `http.Server`. */ function createEndpointServer(handler) { const listener = serveEndpoint(handler); const server = http.createServer(listener); server.on("clientError", (_err, socket) => { socket.end("HTTP/1.1 400 Bad Request\r\n\r\n"); }); return server; } //#endregion export { createEndpointServer, createServer, endpointAdapter, frameworkName, readRequestBody, serve, serveEndpoint }; //# sourceMappingURL=node.js.map