()
| 108 | ); |
| 109 | |
| 110 | export const startServer = async (): Promise<void> => { |
| 111 | const config = loadConfig(); |
| 112 | const { AppLayer, betterAuth, closeDb } = await makeSelfHostApp(); |
| 113 | |
| 114 | // Serve the built SPA, split by cacheability so a redeploy is picked up at |
| 115 | // once instead of stranding browsers on a stale shell: |
| 116 | // - `/assets/*` are Vite content-hashed (a new build emits new filenames), |
| 117 | // so they're safe to cache forever. |
| 118 | // - index.html (and the SPA fallback for client routes) is the mutable |
| 119 | // entry point that references those hashes; it must always revalidate, or |
| 120 | // a browser keeps an old index.html plus its old hashed bundles (still in |
| 121 | // cache) and renders a stale UI until a hard refresh. |
| 122 | // Without explicit headers `HttpStaticServer` sends no Cache-Control at all, |
| 123 | // so browsers heuristically cache index.html across deploys. The hashed |
| 124 | // `/assets` route is the more specific match, so it wins over the SPA |
| 125 | // catch-all. Other built-in API/docs/auth/mcp routes still take precedence; |
| 126 | // `spa: true` falls back to index.html for any remaining path (client routing). |
| 127 | const AssetsLive = HttpStaticServer.layer({ |
| 128 | root: assetsDir, |
| 129 | prefix: "/assets", |
| 130 | cacheControl: "public, max-age=31536000, immutable", |
| 131 | }).pipe(Layer.provide(BunFileSystem.layer), Layer.provide(BunPath.layer)); |
| 132 | |
| 133 | const SpaLive = HttpStaticServer.layer({ |
| 134 | root: distDir, |
| 135 | spa: true, |
| 136 | cacheControl: "no-cache", |
| 137 | }).pipe(Layer.provide(BunFileSystem.layer), Layer.provide(BunPath.layer)); |
| 138 | |
| 139 | // Server-scope finalizer: flush buffered analytics on graceful shutdown. |
| 140 | const AnalyticsFlushLive = Layer.effectDiscard( |
| 141 | Effect.addFinalizer(() => Effect.promise(() => disposeAnalytics())), |
| 142 | ); |
| 143 | |
| 144 | // Server-scope finalizer: release what the app opened at boot — every live |
| 145 | // MCP session (transport, server, and its execution engine, whose detached |
| 146 | // sandbox fibers keep querying the DB until the engine is shut down) and then |
| 147 | // the shared libSQL handle itself. `makeSelfHostApiHandler` runs this in its |
| 148 | // `dispose`, so tests have always released it; the long-lived server dropped |
| 149 | // the hook on the floor and leaked both across a graceful shutdown. |
| 150 | const AppResourcesLive = Layer.effectDiscard( |
| 151 | Effect.addFinalizer(() => Effect.promise(() => closeDb())), |
| 152 | ); |
| 153 | |
| 154 | // OTLP export, or `Layer.empty` when no collector is configured (see |
| 155 | // ./telemetry). The `http.server` envelope span each request's `withSpan` |
| 156 | // children parent under is NOT wired here: `HttpEffect.toHandled` already |
| 157 | // wraps every served app in `HttpMiddleware.tracer`, which also continues an |
| 158 | // inbound `traceparent` so a request arriving through a tunnel keeps one |
| 159 | // trace id. Adding it here as well produced two identical `http.server` |
| 160 | // spans per request, one the parent of the other. |
| 161 | const TelemetryLive = makeTelemetryLive(); |
| 162 | |
| 163 | const ServerLive = HttpRouter.serve(Layer.mergeAll(AppLayer, AssetsLive, SpaLive), { |
| 164 | middleware: selfHostHttpMiddleware(betterAuth), |
| 165 | }).pipe( |
| 166 | Layer.provide( |
| 167 | BunHttpServer.layer({ hostname: config.host, port: config.port, idleTimeout: 0 }), |
no test coverage detected