| author | |
| committer | |
| log | 0c4c027e92bae27716d94d14e3531c390d9ecef1 |
| tree | 0882fdf8fb3966279ec85163e5f3a2a85b273998 |
| parent | fefea0bdf6b09a596b768047bc8432af48ba1123 |
5 files changed, 99 insertions(+), 60 deletions(-)
apps/server/src/config.ts+2| ... | ... | @@ -16,6 +16,8 @@ export interface ServerConfigShape { |
| 16 | 16 | readonly devUrl: URL | undefined; |
| 17 | 17 | readonly noBrowser: boolean; |
| 18 | 18 | readonly authToken: string | undefined; |
| 19 | readonly autoBootstrapProjectFromCwd: boolean; | |
| 20 | readonly logWebSocketEvents: boolean; | |
| 19 | 21 | } |
| 20 | 22 | |
| 21 | 23 | export class ServerConfig extends ServiceMap.Service<ServerConfig, ServerConfigShape>()( |
apps/server/src/main.test.ts+36-33| ... | ... | @@ -10,20 +10,18 @@ import { beforeEach } from "vitest"; |
| 10 | 10 | import { CliConfig, t3Cli, type CliConfigShape } from "./main"; |
| 11 | 11 | import { NetService, ServerConfig, type ServerConfigShape } from "./config"; |
| 12 | 12 | import { Open, type OpenShape } from "./open"; |
| 13 | import { Server, type ServerShape, type ServerOptions } from "./wsServer"; | |
| 13 | import { Server, type ServerShape } from "./wsServer"; | |
| 14 | 14 | |
| 15 | 15 | const start = vi.fn(() => undefined); |
| 16 | 16 | const stop = vi.fn(() => undefined); |
| 17 | 17 | let resolvedConfig: ServerConfigShape | null = null; |
| 18 | const createServer = vi.fn((_: ServerOptions | undefined) => | |
| 19 | Effect.acquireRelease( | |
| 20 | Effect.gen(function* () { | |
| 21 | resolvedConfig = yield* ServerConfig; | |
| 22 | start(); | |
| 23 | return {} as unknown as Http.Server; | |
| 24 | }), | |
| 25 | () => Effect.sync(() => stop()), | |
| 26 | ), | |
| 18 | const serverStart = Effect.acquireRelease( | |
| 19 | Effect.gen(function* () { | |
| 20 | resolvedConfig = yield* ServerConfig; | |
| 21 | start(); | |
| 22 | return {} as unknown as Http.Server; | |
| 23 | }), | |
| 24 | () => Effect.sync(() => stop()), | |
| 27 | 25 | ); |
| 28 | 26 | const findAvailablePort = vi.fn((preferred: number) => Effect.succeed(preferred)); |
| 29 | 27 | |
| ... | ... | @@ -37,7 +35,7 @@ const testLayer = Layer.mergeAll( |
| 37 | 35 | findAvailablePort, |
| 38 | 36 | }), |
| 39 | 37 | Layer.succeed(Server, { |
| 40 | createServer, | |
| 38 | start: serverStart, | |
| 41 | 39 | stopSignal: Effect.void, |
| 42 | 40 | } satisfies ServerShape), |
| 43 | 41 | Layer.succeed(Open, { |
| ... | ... | @@ -60,16 +58,6 @@ beforeEach(() => { |
| 60 | 58 | resolvedConfig = null; |
| 61 | 59 | start.mockImplementation(() => undefined); |
| 62 | 60 | stop.mockImplementation(() => undefined); |
| 63 | createServer.mockImplementation((_: ServerOptions | undefined) => | |
| 64 | Effect.acquireRelease( | |
| 65 | Effect.gen(function* () { | |
| 66 | resolvedConfig = yield* ServerConfig; | |
| 67 | start(); | |
| 68 | return {} as unknown as Http.Server; | |
| 69 | }), | |
| 70 | () => Effect.sync(() => stop()), | |
| 71 | ), | |
| 72 | ); | |
| 73 | 61 | findAvailablePort.mockImplementation((preferred: number) => Effect.succeed(preferred)); |
| 74 | 62 | }); |
| 75 | 63 | |
| ... | ... | @@ -92,7 +80,7 @@ it.layer(testLayer)("server cli", (it) => { |
| 92 | 80 | "auth-secret", |
| 93 | 81 | ]); |
| 94 | 82 | |
| 95 | assert.equal(createServer.mock.calls.length, 1); | |
| 83 | assert.equal(start.mock.calls.length, 1); | |
| 96 | 84 | assert.equal(resolvedConfig?.mode, "desktop"); |
| 97 | 85 | assert.equal(resolvedConfig?.port, 4010); |
| 98 | 86 | assert.equal(resolvedConfig?.host, "0.0.0.0"); |
| ... | ... | @@ -100,7 +88,8 @@ it.layer(testLayer)("server cli", (it) => { |
| 100 | 88 | assert.equal(resolvedConfig?.devUrl?.toString(), "http://127.0.0.1:5173/"); |
| 101 | 89 | assert.equal(resolvedConfig?.noBrowser, true); |
| 102 | 90 | assert.equal(resolvedConfig?.authToken, "auth-secret"); |
| 103 | assert.equal(start.mock.calls.length, 1); | |
| 91 | assert.equal(resolvedConfig?.autoBootstrapProjectFromCwd, false); | |
| 92 | assert.equal(resolvedConfig?.logWebSocketEvents, true); | |
| 104 | 93 | assert.equal(stop.mock.calls.length, 1); |
| 105 | 94 | }), |
| 106 | 95 | ); |
| ... | ... | @@ -109,7 +98,7 @@ it.layer(testLayer)("server cli", (it) => { |
| 109 | 98 | Effect.gen(function* () { |
| 110 | 99 | yield* runCli(["--token", "token-secret"]); |
| 111 | 100 | |
| 112 | assert.equal(createServer.mock.calls.length, 1); | |
| 101 | assert.equal(start.mock.calls.length, 1); | |
| 113 | 102 | assert.equal(resolvedConfig?.authToken, "token-secret"); |
| 114 | 103 | }), |
| 115 | 104 | ); |
| ... | ... | @@ -126,7 +115,7 @@ it.layer(testLayer)("server cli", (it) => { |
| 126 | 115 | T3CODE_AUTH_TOKEN: "env-token", |
| 127 | 116 | }); |
| 128 | 117 | |
| 129 | assert.equal(createServer.mock.calls.length, 1); | |
| 118 | assert.equal(start.mock.calls.length, 1); | |
| 130 | 119 | assert.equal(resolvedConfig?.mode, "desktop"); |
| 131 | 120 | assert.equal(resolvedConfig?.port, 4999); |
| 132 | 121 | assert.equal(resolvedConfig?.host, "100.88.10.4"); |
| ... | ... | @@ -134,6 +123,8 @@ it.layer(testLayer)("server cli", (it) => { |
| 134 | 123 | assert.equal(resolvedConfig?.devUrl?.toString(), "http://localhost:5173/"); |
| 135 | 124 | assert.equal(resolvedConfig?.noBrowser, true); |
| 136 | 125 | assert.equal(resolvedConfig?.authToken, "env-token"); |
| 126 | assert.equal(resolvedConfig?.autoBootstrapProjectFromCwd, false); | |
| 127 | assert.equal(resolvedConfig?.logWebSocketEvents, true); | |
| 137 | 128 | assert.equal(findAvailablePort.mock.calls.length, 0); |
| 138 | 129 | }), |
| 139 | 130 | ); |
| ... | ... | @@ -147,7 +138,7 @@ it.layer(testLayer)("server cli", (it) => { |
| 147 | 138 | }); |
| 148 | 139 | |
| 149 | 140 | assert.deepStrictEqual(findAvailablePort.mock.calls, [[3773]]); |
| 150 | assert.equal(createServer.mock.calls.length, 1); | |
| 141 | assert.equal(start.mock.calls.length, 1); | |
| 151 | 142 | assert.equal(resolvedConfig?.mode, "web"); |
| 152 | 143 | assert.equal(resolvedConfig?.port, 4666); |
| 153 | 144 | assert.equal(resolvedConfig?.host, undefined); |
| ... | ... | @@ -160,7 +151,7 @@ it.layer(testLayer)("server cli", (it) => { |
| 160 | 151 | T3CODE_NO_BROWSER: "false", |
| 161 | 152 | }); |
| 162 | 153 | |
| 163 | assert.equal(createServer.mock.calls.length, 1); | |
| 154 | assert.equal(start.mock.calls.length, 1); | |
| 164 | 155 | assert.equal(resolvedConfig?.noBrowser, true); |
| 165 | 156 | }), |
| 166 | 157 | ); |
| ... | ... | @@ -171,7 +162,7 @@ it.layer(testLayer)("server cli", (it) => { |
| 171 | 162 | yield* runCli([]); |
| 172 | 163 | |
| 173 | 164 | assert.deepStrictEqual(findAvailablePort.mock.calls, [[3773]]); |
| 174 | assert.equal(createServer.mock.calls.length, 1); | |
| 165 | assert.equal(start.mock.calls.length, 1); | |
| 175 | 166 | assert.equal(resolvedConfig?.port, 5444); |
| 176 | 167 | assert.equal(resolvedConfig?.mode, "web"); |
| 177 | 168 | }), |
| ... | ... | @@ -185,7 +176,7 @@ it.layer(testLayer)("server cli", (it) => { |
| 185 | 176 | }); |
| 186 | 177 | |
| 187 | 178 | assert.equal(findAvailablePort.mock.calls.length, 0); |
| 188 | assert.equal(createServer.mock.calls.length, 1); | |
| 179 | assert.equal(start.mock.calls.length, 1); | |
| 189 | 180 | assert.equal(resolvedConfig?.port, 3773); |
| 190 | 181 | assert.equal(resolvedConfig?.host, "127.0.0.1"); |
| 191 | 182 | assert.equal(resolvedConfig?.mode, "desktop"); |
| ... | ... | @@ -199,17 +190,31 @@ it.layer(testLayer)("server cli", (it) => { |
| 199 | 190 | T3CODE_NO_BROWSER: "true", |
| 200 | 191 | }); |
| 201 | 192 | |
| 202 | assert.equal(createServer.mock.calls.length, 1); | |
| 193 | assert.equal(start.mock.calls.length, 1); | |
| 203 | 194 | assert.equal(resolvedConfig?.mode, "desktop"); |
| 204 | 195 | assert.equal(resolvedConfig?.host, "0.0.0.0"); |
| 205 | 196 | }), |
| 206 | 197 | ); |
| 207 | 198 | |
| 199 | it.effect("supports CLI and env for bootstrap/log websocket toggles", () => | |
| 200 | Effect.gen(function* () { | |
| 201 | yield* runCli(["--auto-bootstrap-project-from-cwd"], { | |
| 202 | T3CODE_MODE: "desktop", | |
| 203 | T3CODE_LOG_WS_EVENTS: "false", | |
| 204 | T3CODE_AUTO_BOOTSTRAP_PROJECT_FROM_CWD: "false", | |
| 205 | T3CODE_NO_BROWSER: "true", | |
| 206 | }); | |
| 207 | ||
| 208 | assert.equal(start.mock.calls.length, 1); | |
| 209 | assert.equal(resolvedConfig?.autoBootstrapProjectFromCwd, true); | |
| 210 | assert.equal(resolvedConfig?.logWebSocketEvents, false); | |
| 211 | }), | |
| 212 | ); | |
| 213 | ||
| 208 | 214 | it.effect("does not start server for invalid --mode values", () => |
| 209 | 215 | Effect.gen(function* () { |
| 210 | 216 | yield* runCli(["--mode", "invalid"]); |
| 211 | 217 | |
| 212 | assert.equal(createServer.mock.calls.length, 0); | |
| 213 | 218 | assert.equal(start.mock.calls.length, 0); |
| 214 | 219 | assert.equal(stop.mock.calls.length, 0); |
| 215 | 220 | }), |
| ... | ... | @@ -219,7 +224,6 @@ it.layer(testLayer)("server cli", (it) => { |
| 219 | 224 | Effect.gen(function* () { |
| 220 | 225 | yield* runCli(["--dev-url", "not-a-url"]).pipe(Effect.catch(() => Effect.void)); |
| 221 | 226 | |
| 222 | assert.equal(createServer.mock.calls.length, 0); | |
| 223 | 227 | assert.equal(start.mock.calls.length, 0); |
| 224 | 228 | assert.equal(stop.mock.calls.length, 0); |
| 225 | 229 | }), |
| ... | ... | @@ -230,7 +234,6 @@ it.layer(testLayer)("server cli", (it) => { |
| 230 | 234 | yield* runCli(["--port", "70000"]); |
| 231 | 235 | |
| 232 | 236 | // effect/unstable/cli renders help/errors for parse failures and returns success. |
| 233 | assert.equal(createServer.mock.calls.length, 0); | |
| 234 | 237 | assert.equal(start.mock.calls.length, 0); |
| 235 | 238 | assert.equal(stop.mock.calls.length, 0); |
| 236 | 239 | }), |
apps/server/src/main.ts+41-2| ... | ... | @@ -28,6 +28,8 @@ interface CliInput { |
| 28 | 28 | readonly devUrl: Option.Option<URL>; |
| 29 | 29 | readonly noBrowser: Option.Option<boolean>; |
| 30 | 30 | readonly authToken: Option.Option<string>; |
| 31 | readonly autoBootstrapProjectFromCwd: Option.Option<boolean>; | |
| 32 | readonly logWebSocketEvents: Option.Option<boolean>; | |
| 31 | 33 | } |
| 32 | 34 | |
| 33 | 35 | export interface CliConfigShape { |
| ... | ... | @@ -79,6 +81,14 @@ const CliEnvConfig = Config.all({ |
| 79 | 81 | Config.option, |
| 80 | 82 | Config.map(Option.getOrUndefined), |
| 81 | 83 | ), |
| 84 | autoBootstrapProjectFromCwd: Config.boolean("T3CODE_AUTO_BOOTSTRAP_PROJECT_FROM_CWD").pipe( | |
| 85 | Config.option, | |
| 86 | Config.map(Option.getOrUndefined), | |
| 87 | ), | |
| 88 | logWebSocketEvents: Config.boolean("T3CODE_LOG_WS_EVENTS").pipe( | |
| 89 | Config.option, | |
| 90 | Config.map(Option.getOrUndefined), | |
| 91 | ), | |
| 82 | 92 | }); |
| 83 | 93 | |
| 84 | 94 | const ServerConfigLive = (input: CliInput) => |
| ... | ... | @@ -119,6 +129,18 @@ const ServerConfigLive = (input: CliInput) => |
| 119 | 129 | onNone: () => env.noBrowser ?? mode === "desktop", |
| 120 | 130 | }); |
| 121 | 131 | const authToken = Option.getOrUndefined(input.authToken) ?? env.authToken; |
| 132 | const autoBootstrapProjectFromCwd = Option.match(input.autoBootstrapProjectFromCwd, { | |
| 133 | // effect/cli boolean flags parse to `false` when absent; in that case | |
| 134 | // we still want env/mode fallbacks to apply. | |
| 135 | onSome: (value) => (value ? true : (env.autoBootstrapProjectFromCwd ?? mode === "web")), | |
| 136 | onNone: () => env.autoBootstrapProjectFromCwd ?? mode === "web", | |
| 137 | }); | |
| 138 | const logWebSocketEvents = Option.match(input.logWebSocketEvents, { | |
| 139 | // effect/cli boolean flags parse to `false` when absent; in that case | |
| 140 | // we still want env/dev fallbacks to apply. | |
| 141 | onSome: (value) => (value ? true : (env.logWebSocketEvents ?? Boolean(devUrl))), | |
| 142 | onNone: () => env.logWebSocketEvents ?? Boolean(devUrl), | |
| 143 | }); | |
| 122 | 144 | const staticDir = devUrl ? undefined : yield* cliConfig.resolveStaticDir; |
| 123 | 145 | const { join } = yield* Path.Path; |
| 124 | 146 | const keybindingsConfigPath = join(stateDir, "keybindings.json"); |
| ... | ... | @@ -138,6 +160,8 @@ const ServerConfigLive = (input: CliInput) => |
| 138 | 160 | devUrl, |
| 139 | 161 | noBrowser, |
| 140 | 162 | authToken, |
| 163 | autoBootstrapProjectFromCwd, | |
| 164 | logWebSocketEvents, | |
| 141 | 165 | } satisfies ServerConfigShape; |
| 142 | 166 | }), |
| 143 | 167 | ); |
| ... | ... | @@ -159,7 +183,7 @@ const formatHostForUrl = (host: string): string => |
| 159 | 183 | const makeServerProgram = (input: CliInput) => |
| 160 | 184 | Effect.gen(function* () { |
| 161 | 185 | const cliConfig = yield* CliConfig; |
| 162 | const { createServer, stopSignal } = yield* Server; | |
| 186 | const { start, stopSignal } = yield* Server; | |
| 163 | 187 | const openDeps = yield* Open; |
| 164 | 188 | yield* cliConfig.fixPath; |
| 165 | 189 | |
| ... | ... | @@ -174,7 +198,7 @@ const makeServerProgram = (input: CliInput) => |
| 174 | 198 | ); |
| 175 | 199 | } |
| 176 | 200 | |
| 177 | yield* createServer(); | |
| 201 | yield* start; | |
| 178 | 202 | |
| 179 | 203 | const localUrl = `http://localhost:${config.port}`; |
| 180 | 204 | const bindUrl = |
| ... | ... | @@ -240,6 +264,19 @@ const authTokenFlag = Flag.string("auth-token").pipe( |
| 240 | 264 | Flag.withAlias("token"), |
| 241 | 265 | Flag.optional, |
| 242 | 266 | ); |
| 267 | const autoBootstrapProjectFromCwdFlag = Flag.boolean("auto-bootstrap-project-from-cwd").pipe( | |
| 268 | Flag.withDescription( | |
| 269 | "Create a project for the current working directory on startup when missing.", | |
| 270 | ), | |
| 271 | Flag.optional, | |
| 272 | ); | |
| 273 | const logWebSocketEventsFlag = Flag.boolean("log-websocket-events").pipe( | |
| 274 | Flag.withDescription( | |
| 275 | "Emit server-side logs for outbound WebSocket push traffic (equivalent to T3CODE_LOG_WS_EVENTS).", | |
| 276 | ), | |
| 277 | Flag.withAlias("log-ws-events"), | |
| 278 | Flag.optional, | |
| 279 | ); | |
| 243 | 280 | |
| 244 | 281 | export const t3Cli = Command.make("t3", { |
| 245 | 282 | mode: modeFlag, |
| ... | ... | @@ -249,6 +286,8 @@ export const t3Cli = Command.make("t3", { |
| 249 | 286 | devUrl: devUrlFlag, |
| 250 | 287 | noBrowser: noBrowserFlag, |
| 251 | 288 | authToken: authTokenFlag, |
| 289 | autoBootstrapProjectFromCwd: autoBootstrapProjectFromCwdFlag, | |
| 290 | logWebSocketEvents: logWebSocketEventsFlag, | |
| 252 | 291 | }).pipe( |
| 253 | 292 | Command.withDescription("Run the T3 Code server."), |
| 254 | 293 | Command.withHandler((input) => Effect.scoped(makeServerProgram(input))), |
apps/server/src/wsServer.test.ts+4-3| ... | ... | @@ -338,6 +338,7 @@ describe("WebSocket Server", () => { |
| 338 | 338 | persistenceLayer?: Layer.Layer<SqlClient.SqlClient, any>; |
| 339 | 339 | cwd?: string; |
| 340 | 340 | autoBootstrapProjectFromCwd?: boolean; |
| 341 | logWebSocketEvents?: boolean; | |
| 341 | 342 | devUrl?: string; |
| 342 | 343 | authToken?: string; |
| 343 | 344 | stateDir?: string; |
| ... | ... | @@ -368,6 +369,8 @@ describe("WebSocket Server", () => { |
| 368 | 369 | devUrl: options.devUrl ? new URL(options.devUrl) : undefined, |
| 369 | 370 | noBrowser: true, |
| 370 | 371 | authToken: options.authToken, |
| 372 | autoBootstrapProjectFromCwd: options.autoBootstrapProjectFromCwd ?? false, | |
| 373 | logWebSocketEvents: options.logWebSocketEvents ?? Boolean(options.devUrl), | |
| 371 | 374 | } satisfies ServerConfigShape); |
| 372 | 375 | const infrastructureLayer = providerLayer.pipe(Layer.provideMerge(persistenceLayer)); |
| 373 | 376 | const runtimeOverrides = Layer.mergeAll( |
| ... | ... | @@ -397,9 +400,7 @@ describe("WebSocket Server", () => { |
| 397 | 400 | |
| 398 | 401 | try { |
| 399 | 402 | const runtime = await Effect.runPromise( |
| 400 | createServer({ | |
| 401 | autoBootstrapProjectFromCwd: options.autoBootstrapProjectFromCwd ?? false, | |
| 402 | }).pipe(Effect.provide(runtimeServices), Scope.provide(scope)), | |
| 403 | createServer().pipe(Effect.provide(runtimeServices), Scope.provide(scope)), | |
| 403 | 404 | ); |
| 404 | 405 | serverScope = scope; |
| 405 | 406 | return runtime; |
apps/server/src/wsServer.ts+16-22| ... | ... | @@ -49,15 +49,12 @@ const MIME_TYPES: Record<string, string> = { |
| 49 | 49 | ".map": "application/json", |
| 50 | 50 | }; |
| 51 | 51 | |
| 52 | export interface ServerOptions { | |
| 53 | autoBootstrapProjectFromCwd?: boolean | undefined; | |
| 54 | logWebSocketEvents?: boolean | undefined; | |
| 55 | } | |
| 56 | ||
| 57 | 52 | export interface ServerShape { |
| 58 | readonly createServer: ( | |
| 59 | options?: ServerOptions, | |
| 60 | ) => Effect.Effect<http.Server, unknown, Scope.Scope | ServerRuntimeServices | ServerConfig>; | |
| 53 | readonly start: Effect.Effect< | |
| 54 | http.Server, | |
| 55 | unknown, | |
| 56 | Scope.Scope | ServerRuntimeServices | ServerConfig | |
| 57 | >; | |
| 61 | 58 | readonly stopSignal: Effect.Effect<unknown, never>; |
| 62 | 59 | } |
| 63 | 60 | |
| ... | ... | @@ -82,14 +79,6 @@ function rejectUpgrade(socket: Duplex, statusCode: number, message: string): voi |
| 82 | 79 | ); |
| 83 | 80 | } |
| 84 | 81 | |
| 85 | function parseBooleanEnv(value: string | undefined): boolean | undefined { | |
| 86 | if (value === undefined) return undefined; | |
| 87 | const normalized = value.trim().toLowerCase(); | |
| 88 | if (["1", "true", "yes", "on"].includes(normalized)) return true; | |
| 89 | if (["0", "false", "no", "off"].includes(normalized)) return false; | |
| 90 | return undefined; | |
| 91 | } | |
| 92 | ||
| 93 | 82 | function websocketRawToString(raw: unknown): string | null { |
| 94 | 83 | if (typeof raw === "string") { |
| 95 | 84 | return raw; |
| ... | ... | @@ -153,17 +142,22 @@ class RouteRequestError extends Schema.TaggedErrorClass<RouteRequestError>()("Ro |
| 153 | 142 | message: Schema.String, |
| 154 | 143 | }) {} |
| 155 | 144 | |
| 156 | export const createServer = Effect.fn(function* (options: ServerOptions = {}): Effect.fn.Return< | |
| 145 | export const createServer = Effect.fn(function* (): Effect.fn.Return< | |
| 157 | 146 | http.Server, |
| 158 | 147 | unknown, // TODO: This should not be unknown |
| 159 | 148 | Scope.Scope | ServerRuntimeServices | ServerConfig |
| 160 | 149 | > { |
| 161 | 150 | const serverConfig = yield* ServerConfig; |
| 162 | const { port, cwd, staticDir, devUrl, authToken, mode, host } = serverConfig; | |
| 163 | 151 | const { |
| 164 | logWebSocketEvents = parseBooleanEnv(process.env.T3CODE_LOG_WS_EVENTS) ?? Boolean(devUrl), | |
| 165 | autoBootstrapProjectFromCwd = mode === "web", | |
| 166 | } = options; | |
| 152 | port, | |
| 153 | cwd, | |
| 154 | staticDir, | |
| 155 | devUrl, | |
| 156 | authToken, | |
| 157 | host, | |
| 158 | logWebSocketEvents, | |
| 159 | autoBootstrapProjectFromCwd, | |
| 160 | } = serverConfig; | |
| 167 | 161 | |
| 168 | 162 | const gitManager = yield* GitManager; |
| 169 | 163 | const terminalManager = yield* TerminalManager; |
| ... | ... | @@ -599,6 +593,6 @@ export const createServer = Effect.fn(function* (options: ServerOptions = {}): E |
| 599 | 593 | }); |
| 600 | 594 | |
| 601 | 595 | export const ServerLive = Layer.succeed(Server, { |
| 602 | createServer, | |
| 596 | start: createServer(), | |
| 603 | 597 | stopSignal: Effect.never, |
| 604 | 598 | } satisfies ServerShape); |