UNPKG

@rivetkit/next-js

Version:

Next.js integration for RivetKit actors and client

1 lines 10.3 kB
{"version":3,"sources":["../src/mod.ts","../src/log.ts"],"sourcesContent":["import { existsSync, statSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { Registry, RunConfigInput } from \"rivetkit\";\nimport { stringifyError } from \"rivetkit/utils\";\nimport { logger } from \"./log\";\n\nexport const toNextHandler = (\n\tregistry: Registry<any>,\n\tinputConfig: RunConfigInput = {},\n) => {\n\t// Don't run server locally since we're using the fetch handler directly\n\tinputConfig.disableDefaultServer = true;\n\n\t// Configure serverless\n\tinputConfig.runnerKind = \"serverless\";\n\n\tif (process.env.NODE_ENV !== \"production\") {\n\t\t// Auto-configure serverless runner if not in prod\n\t\tlogger().debug(\n\t\t\t\"detected development environment, auto-starting engine and auto-configuring serverless\",\n\t\t);\n\n\t\tconst publicUrl =\n\t\t\tprocess.env.NEXT_PUBLIC_SITE_URL ??\n\t\t\tprocess.env.NEXT_PUBLIC_VERCEL_URL ??\n\t\t\t`http://127.0.0.1:${process.env.PORT ?? 3000}`;\n\n\t\tinputConfig.runEngine = true;\n\t\tinputConfig.autoConfigureServerless = {\n\t\t\turl: `${publicUrl}/api/rivet`,\n\t\t\tminRunners: 0,\n\t\t\tmaxRunners: 100_000,\n\t\t\trequestLifespan: 300,\n\t\t\tslotsPerRunner: 1,\n\t\t\tmetadata: { provider: \"next-js\" },\n\t\t};\n\t} else {\n\t\tlogger().debug(\n\t\t\t\"detected production environment, will not auto-start engine and auto-configure serverless\",\n\t\t);\n\t}\n\n\t// Next logs this on every request\n\tinputConfig.noWelcome = true;\n\n\tconst { fetch } = registry.start(inputConfig);\n\n\t// Function that Next will call when handling requests\n\tconst fetchWrapper = async (\n\t\trequest: Request,\n\t\t{ params }: { params: Promise<{ all: string[] }> },\n\t): Promise<Response> => {\n\t\tconst { all } = await params;\n\n\t\tconst newUrl = new URL(request.url);\n\t\tnewUrl.pathname = all.join(\"/\");\n\n\t\tif (process.env.NODE_ENV !== \"development\") {\n\t\t\t// Handle request\n\t\t\tconst newReq = new Request(newUrl, request);\n\t\t\treturn await fetch(newReq);\n\t\t} else {\n\t\t\t// Special request handling for file watching\n\t\t\treturn await handleRequestWithFileWatcher(request, newUrl, fetch);\n\t\t}\n\t};\n\n\treturn {\n\t\tGET: fetchWrapper,\n\t\tPOST: fetchWrapper,\n\t\tPUT: fetchWrapper,\n\t\tPATCH: fetchWrapper,\n\t\tHEAD: fetchWrapper,\n\t\tOPTIONS: fetchWrapper,\n\t};\n};\n\n/**\n * Special request handler that will watch the source file to terminate this\n * request once complete.\n *\n * See docs on watchRouteFile for more information.\n */\nasync function handleRequestWithFileWatcher(\n\trequest: Request,\n\tnewUrl: URL,\n\tfetch: (request: Request, ...args: any) => Response | Promise<Response>,\n): Promise<Response> {\n\t// Create a new abort controller that we can abort, since the signal on\n\t// the request we cannot control\n\tconst mergedController = new AbortController();\n\tconst abortMerged = () => mergedController.abort();\n\trequest.signal?.addEventListener(\"abort\", abortMerged);\n\n\t// Watch for file changes in dev\n\t//\n\t// We spawn one watcher per-request since there is not a clean way of\n\t// cleaning up global watchers when hot reloading in Next\n\tconst watchIntervalId = watchRouteFile(mergedController);\n\n\t// Clear interval if request is aborted\n\trequest.signal.addEventListener(\"abort\", () => {\n\t\tlogger().debug(\"clearing file watcher interval: request aborted\");\n\t\tclearInterval(watchIntervalId);\n\t});\n\n\t// Replace URL and abort signal\n\tconst newReq = new Request(newUrl, {\n\t\t// Copy old request properties\n\t\tmethod: request.method,\n\t\theaders: request.headers,\n\t\tbody: request.body,\n\t\tcredentials: request.credentials,\n\t\tcache: request.cache,\n\t\tredirect: request.redirect,\n\t\treferrer: request.referrer,\n\t\tintegrity: request.integrity,\n\t\t// Override with new signal\n\t\tsignal: mergedController.signal,\n\t\t// Required for streaming body\n\t\tduplex: \"half\",\n\t} as RequestInit);\n\n\t// Handle request\n\tconst response = await fetch(newReq);\n\n\t// HACK: Next.js does not provide a way to detect when a request\n\t// finishes, so we need to tap the response stream\n\t//\n\t// We can't just wait for `await fetch` to finish since SSE streams run\n\t// for longer\n\tif (response.body) {\n\t\tconst wrappedStream = waitForStreamFinish(response.body, () => {\n\t\t\tlogger().debug(\"clearing file watcher interval: stream finished\");\n\t\t\tclearInterval(watchIntervalId);\n\t\t});\n\t\treturn new Response(wrappedStream, {\n\t\t\tstatus: response.status,\n\t\t\tstatusText: response.statusText,\n\t\t\theaders: response.headers,\n\t\t});\n\t} else {\n\t\t// No response body, clear interval immediately\n\t\tlogger().debug(\"clearing file watcher interval: no response body\");\n\t\tclearInterval(watchIntervalId);\n\t\treturn response;\n\t}\n}\n\n/**\n * HACK: Watch for file changes on this route in order to shut down the runner.\n * We do this because Next.js does not terminate long-running requests on file\n * change, so we need to manually shut down the runner in order to trigger a\n * new `/start` request with the new code.\n *\n * We don't use file watchers since those are frequently buggy x-platform and\n * subject to misconfigured inotify limits.\n */\nfunction watchRouteFile(abortController: AbortController): NodeJS.Timeout {\n\tlogger().debug(\"starting file watcher\");\n\n\tconst routePath = join(\n\t\tprocess.cwd(),\n\t\t\".next/server/app/api/rivet/[...all]/route.js\",\n\t);\n\n\tlet lastMtime: number | null = null;\n\tconst checkFile = () => {\n\t\tlogger().debug({ msg: \"checking for file changes\", routePath });\n\t\ttry {\n\t\t\tif (!existsSync(routePath)) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst stats = statSync(routePath);\n\t\t\tconst mtime = stats.mtimeMs;\n\n\t\t\tif (lastMtime !== null && mtime !== lastMtime) {\n\t\t\t\tlogger().info({ msg: \"route file changed\", routePath });\n\t\t\t\tabortController.abort();\n\t\t\t}\n\n\t\t\tlastMtime = mtime;\n\t\t} catch (err) {\n\t\t\tlogger().info({\n\t\t\t\tmsg: \"failed to check for route file change\",\n\t\t\t\terr: stringifyError(err),\n\t\t\t});\n\t\t}\n\t};\n\n\tcheckFile();\n\n\treturn setInterval(checkFile, 1000);\n}\n\n/**\n * Waits for a stream to finish and calls onFinish on complete.\n *\n * Used for cancelling the file watcher.\n */\nfunction waitForStreamFinish(\n\tbody: ReadableStream<Uint8Array>,\n\tonFinish: () => void,\n): ReadableStream {\n\tconst reader = body.getReader();\n\treturn new ReadableStream({\n\t\tasync start(controller) {\n\t\t\ttry {\n\t\t\t\twhile (true) {\n\t\t\t\t\tconst { done, value } = await reader.read();\n\t\t\t\t\tif (done) {\n\t\t\t\t\t\tlogger().debug(\"stream completed\");\n\t\t\t\t\t\tonFinish();\n\t\t\t\t\t\tcontroller.close();\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tcontroller.enqueue(value);\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\tlogger().debug(\"stream errored\");\n\t\t\t\tonFinish();\n\t\t\t\tcontroller.error(err);\n\t\t\t}\n\t\t},\n\t\tcancel() {\n\t\t\tlogger().debug(\"stream cancelled\");\n\t\t\tonFinish();\n\t\t\treader.cancel();\n\t\t},\n\t});\n}\n","import { getLogger } from \"rivetkit/log\";\n\nexport function logger() {\n\treturn getLogger(\"driver-next-js\");\n}\n"],"mappings":"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","names":[]}