UNPKG

@rsc-kit/mcp

Version:

An MCP server over what an rsc-kit build decided: the routes, why each one is static or not, and what it costs the browser.

122 lines (95 loc) • 5.19 kB
# Startup > instrumentation.ts runs once, before anything else. Some setup belongs to the process, not to a page: validating the environment, configuring a shared package, warming a connection. Put it in `src/instrumentation.ts` and it runs once — before any page module evaluates, and before the first request. ```ts title="src/instrumentation.ts" // Runs at import, before any page: a bad variable stops the server here. import './env' // Anything asynchronous the app needs before it serves. Called once per // process, at startup; the first render waits for it, later ones do not. export async function register() { await db.connect() } ``` The name is Next's, so a reader arriving from there knows what it is. Both halves are optional: a file with only imports is a bootstrap, a file with only `register()` is a hook. ## Why it is a framework file Two things make this a file the framework has to know about rather than a module the app imports. **Import order.** A page that configures a shared package at import time — a logger, an ORM, a metrics client — runs before any module the app could put in front of it, because the generated entry imports the pages. The entry imports `instrumentation.ts` *first*, so whatever it sets up is set up when the first page module evaluates. Importing a bootstrap module from every file that needs it works until someone forgets, and the failure is a page that throws "not configured" for the one visitor who reached it first. **Before the first request.** `register()` is **called once**, and every entry point a render can come through — the built server, the dev server, the prerender, a route's middleware check, a server action, an api route — waits on that one call rather than making its own. After startup it has long since finished, so a request waits on nothing: it checks a promise that has already settled. That is how "before the first render" holds without the app knowing the list of entry points, and without a request ever paying for it. ## When it runs | where | when | | --- | --- | | a server (`bun`, `node`, any long-lived preset) | at startup, before the server reports itself up | | a Worker | at the isolate's first request — there is no startup, and a binding is only readable once a request has arrived | | `vite dev` | when the dev server first evaluates the app, and again when the file or anything it imports changes | | `vite build` | before the first page is prerendered | **Once per process, not once per request.** Measured on a built server and on a compiled binary, with a `register()` that logs: one call before the first request, and still one after a stream of them. A `register()` that resolved is not called again. One that *rejected* is the exception: the next request calls it again, so a database that was not up yet is asked again rather than leaving the process permanently refusing. On a server that case is rare in practice — a startup failure exits the process, below. On a server, a failure at startup — the file throwing at import, or `register()` rejecting — is reported and the process exits. A server that could not bootstrap has nothing correct to serve, and a health check that passed on a server about to fail its first visitor is worse than one that never passed. The dev server stays up and reports the error on the page. ## Shutting down Export `shutdown()` beside `register()` to close what `register()` opened. A built server calls it when it is stopped, after in-flight requests have drained, and then exits: ```ts title="src/instrumentation.ts" export async function register() { await db.connect() } export async function shutdown() { await db.end() } ``` It is bounded by `RSC_SHUTDOWN_TIMEOUT` (10 seconds), and one that throws is reported rather than holding the process. The server exits whether or not you export it; see [Stopping gracefully](/hosts/deployment/#stopping-gracefully). Not called on a Worker, which has no process to stop, or by the dev server. ## Environment validation The scaffold writes this file when the project validates its environment, and the only line it needs is the import: `src/env.ts` refuses at import, and importing it here is what makes a missing variable the server's failure at startup rather than a visitor's three calls later. The build prerenders pages, so it runs the bootstrap too. A build machine without the production variables sets `SKIP_ENV_VALIDATION=1`, which the generated schema honours; the server that runs the build validates at startup regardless. ## On a Worker Read bindings inside `register()`, not at the top of the module. A Worker evaluates the module before any request exists, and `process.env` is empty until Nitro maps the first request's bindings onto it — so a top-level `process.env.DATABASE_URL` is `undefined` at exactly the moment it looks like it should not be. Inside `register()`, which runs at the first request, it is there. ```ts export async function register() { const url = process.env.DATABASE_URL // readable here on every host await db.connect(url) } ``` The same file works unchanged on a server, where both moments have the environment.