Internals: The server

Internals

The server

Grimoire runs a coordinator and a set of workers. The coordinator owns the listening socket and decides which worker gets a request; workers run the app. That split is what lets sigil start --scale run several instances of the same app.

The path of a request

  1. Coordinator accepts the connection.
  2. Routing matches the URL against the route table the scanner built at startup — static, then required parameters, then optional, then rest.
  3. hooks.server.ts handle runs. Code before resolve(event) is "before"; after it is "after".
  4. +page.server.ts load runs, and a +server.ts handler is chosen if the route has one.
  5. Render — either streamed in two passes or rendered whole.
  6. handleError sees anything that would be a 500; the nearest +error.tsx decides what the user sees.

Coordinator and workers

The coordinator is a thin front: it scans, matches, and forwards. It holds no application state. sigil routes is the coordinator's own view of the table, which is why a route missing there is missing from the build entirely.

Workers are spawned per the --scale spec and supervised — one that dies is respawned. onWorkerSpawn lets plugins inject environment into each one before it starts, and all plugins' results are merged.

A useful consequence of the split: a shared module-level $store in a worker is per-worker, and on the server that means per request context, not per process. The same store read by two concurrent requests is not the same object. See Sharing state.

Streaming

When a page exports loading, the response is written in two passes:

  1. The loading shell is wrapped in the layout tree, its stylesheets hoisted and its scoped styles collected, and flushed immediately. The browser paints the skeleton.
  2. load runs while the browser is already rendering that shell.
  3. The real page is rendered, wrapped in the same layout tree, and flushed to swap the shell out.
  4. A script closes the response and patches in any deferred load values.

Because the shell is wrapped in the same layouts, the skeleton arrives inside the correct chrome — nav, sidebar and all — so the swap does not shift the page.

The cost is that the whole document cannot be buffered, which is why a plugin using onRouteRender disables the whole mechanism. See Known limitations.

Non-streamed requests

Without a loading export, the response is rendered whole and sent as a single body. Client-side navigation always takes this path: there is already a page on screen, so there is nothing to stream a shell into.

Error pages

renderErrorPage finds the closest +error.tsx for the path and renders it through the same layout tree. A +error.tsx is therefore a fragment — it must not render <html>, <head> or its own header, because the layout has already done that. Getting that wrong duplicates the navbar.

Error pages get no hydration script and no state payload, since they are not in the client route table.

Cookies

cookies on the request event is a wrapper over a Set-Cookie map, not the browser's jar. set and delete record response cookies; get reads the request's. path defaults to / on both, and values are URL-encoded out and decoded back in, so a value with a comma or a space round-trips.

Plugins

Plugins wrap the pipeline at three useful points: onRequest for every request, onRouteLoad for data loading, and onRouteRender for the finished HTML — the last of which costs you streaming. See plugins.

Next

Edit this page