Skip to content

Commit

Permalink
Add vanilla Node.js handler
Browse files Browse the repository at this point in the history
  • Loading branch information
djfarrelly committed Dec 20, 2024
1 parent 1ec48bc commit e0def78
Show file tree
Hide file tree
Showing 3 changed files with 181 additions and 0 deletions.
38 changes: 38 additions & 0 deletions packages/inngest/src/node.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import http from "node:http";
import { Socket } from "node:net";
import { Buffer } from "node:buffer";
import * as NodeHandler from "@local/node";
import { testFramework } from "./test/helpers";

testFramework("Node", NodeHandler, {
transformReq: (req, res) => {
const socket = new Socket();
const nodeReq = new http.IncomingMessage(socket);

// Set the method and URL
nodeReq.method = req.method;
nodeReq.url = req.url;

if (req.protocol === "https") {
nodeReq.headers["x-forwarded-proto"] = req.protocol;
}

// Set headers
for (const [key, value] of Object.entries(req.headers)) {
nodeReq.headers[key.toLowerCase()] = value;
}

// Mock the body data
const bodyData = Buffer.from(JSON.stringify(req.body));

nodeReq.on("end", () => {
console.log("END!");
});

// Override the read methods to return the body data
nodeReq.push(bodyData);
nodeReq.push(null); // Signals the end of the stream

return [nodeReq, res];
},
});
142 changes: 142 additions & 0 deletions packages/inngest/src/node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import http from "node:http";
import { type TLSSocket } from "node:tls";
import { URL } from "node:url";
import {
InngestCommHandler,
type ServeHandlerOptions,
} from "./components/InngestCommHandler.js";
import { type SupportedFrameworkName } from "./types.js";

/**
* The name of the framework, used to identify the framework in Inngest
* dashboards and during testing.
*/
export const frameworkName: SupportedFrameworkName = "nodejs";

/**
* Parse the incoming message request as a JSON body
*/
async function parseRequestBody(req: http.IncomingMessage): Promise<unknown> {
return new Promise((resolve, reject) => {
let body = "";
req.on("data", (chunk) => {
body += chunk;
});
req.on("end", () => {
try {
const json = JSON.parse(body) as unknown;
resolve(json);
} catch (err) {
reject(err);
}
});
});
}

function getURL(req: http.IncomingMessage, hostnameOption?: string): URL {
const protocol =
(req.headers["x-forwarded-proto"] as string) ||
((req.socket as TLSSocket)?.encrypted ? "https" : "http");
const origin = hostnameOption || `${protocol}://${req.headers.host}`;
return new URL(req.url || "", origin);
}

/**
* 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
*/
// Has explicit return type to avoid JSR-defined "slow types"
export const serve = (options: ServeHandlerOptions): http.RequestListener => {
const handler = new InngestCommHandler({
frameworkName,
...options,
handler: (req: http.IncomingMessage, res: http.ServerResponse) => {
return {
body: async () => parseRequestBody(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.serveHost),
transformResponse: ({ body, status, headers }) => {
res.writeHead(status, headers);
res.end(body);
},
};
},
});
return handler.createHandler() as http.RequestListener;
};

/**
* 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
*/
export const createServer = (options: ServeHandlerOptions) => {
const server = http.createServer((req, res) => {
const url = getURL(req, options.serveHost);
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;
};
1 change: 1 addition & 0 deletions packages/inngest/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1254,6 +1254,7 @@ export type SupportedFrameworkName =
| "express"
| "aws-lambda"
| "nextjs"
| "nodejs"
| "nuxt"
| "h3"
| "redwoodjs"
Expand Down

0 comments on commit e0def78

Please sign in to comment.