()
| 108 | ); |
| 109 | |
| 110 | export const startServer = async (): Promise<void> => { |
| 111 | const config = loadConfig(); |
| 112 | const { AppLayer, betterAuth } = 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 | // OTLP export, or `Layer.empty` when no collector is configured (see |
| 145 | // ./telemetry). The `http.server` envelope span each request's `withSpan` |
| 146 | // children parent under is NOT wired here: `HttpEffect.toHandled` already |
| 147 | // wraps every served app in `HttpMiddleware.tracer`, which also continues an |
| 148 | // inbound `traceparent` so a request arriving through a tunnel keeps one |
| 149 | // trace id. Adding it here as well produced two identical `http.server` |
| 150 | // spans per request, one the parent of the other. |
| 151 | const TelemetryLive = makeTelemetryLive(); |
| 152 | |
| 153 | const ServerLive = HttpRouter.serve(Layer.mergeAll(AppLayer, AssetsLive, SpaLive), { |
| 154 | middleware: selfHostHttpMiddleware(betterAuth), |
| 155 | }).pipe( |
| 156 | Layer.provide( |
| 157 | BunHttpServer.layer({ hostname: config.host, port: config.port, idleTimeout: 0 }), |
| 158 | ), |
| 159 | ); |
| 160 | |
| 161 | // Provided UNDER the server, not merged beside it: the tracer must already be |
| 162 | // in scope while the app's layers build, or spans created during construction |
| 163 | // resolve the default no-op tracer and are silently dropped. |
| 164 | await BunRuntime.runMain( |
| 165 | Layer.launch(Layer.merge(ServerLive, AnalyticsFlushLive).pipe(Layer.provide(TelemetryLive))), |
| 166 | ); |
| 167 | }; |
no test coverage detected