refactor(server): centralize server options
This commit is contained in:
parent
f5a487ffcd
commit
ace77a5cf7
4 changed files with 74 additions and 67 deletions
|
|
@ -60,30 +60,31 @@ const processEffect = Effect.fnUntraced(function* (options: Options) {
|
||||||
: randomBytes(32).toString("base64url")
|
: randomBytes(32).toString("base64url")
|
||||||
if (!password) return yield* Effect.fail(new Error("Missing server password"))
|
if (!password) return yield* Effect.fail(new Error("Missing server password"))
|
||||||
const instanceID = randomUUID()
|
const instanceID = randomUUID()
|
||||||
const server = yield* start({
|
const server = yield* start(
|
||||||
hostname,
|
{
|
||||||
port: Option.fromNullishOr(port),
|
hostname,
|
||||||
password,
|
port,
|
||||||
instanceID,
|
password,
|
||||||
database: {
|
database: {
|
||||||
path: process.env.OPENCODE_DB,
|
path: process.env.OPENCODE_DB,
|
||||||
|
},
|
||||||
|
models: {
|
||||||
|
url: process.env.OPENCODE_MODELS_URL,
|
||||||
|
file: process.env.OPENCODE_MODELS_PATH,
|
||||||
|
fetch: !["1", "true"].includes(process.env.OPENCODE_DISABLE_MODELS_FETCH?.toLowerCase() ?? ""),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
models: {
|
serviceOptions === undefined
|
||||||
url: process.env.OPENCODE_MODELS_URL,
|
? undefined
|
||||||
file: process.env.OPENCODE_MODELS_PATH,
|
: {
|
||||||
fetch: !["1", "true"].includes(process.env.OPENCODE_DISABLE_MODELS_FETCH?.toLowerCase() ?? ""),
|
instanceID,
|
||||||
},
|
onListen: (address, shutdown) =>
|
||||||
service:
|
Effect.gen(function* () {
|
||||||
serviceOptions === undefined
|
if (!config.password) yield* ServiceConfig.password(password)
|
||||||
? undefined
|
return yield* register(address, password, instanceID, serviceOptions.file, shutdown)
|
||||||
: {
|
}),
|
||||||
onListen: (address, shutdown) =>
|
},
|
||||||
Effect.gen(function* () {
|
).pipe(
|
||||||
if (!config.password) yield* ServiceConfig.password(password)
|
|
||||||
return yield* register(address, password, instanceID, serviceOptions.file, shutdown)
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}).pipe(
|
|
||||||
Effect.provide(Logger.layer([], { mergeWithExisting: false })),
|
Effect.provide(Logger.layer([], { mergeWithExisting: false })),
|
||||||
Effect.catch((error) => {
|
Effect.catch((error) => {
|
||||||
if (serviceOptions === undefined || port === undefined || !addressInUse(error)) return Effect.fail(error)
|
if (serviceOptions === undefined || port === undefined || !addressInUse(error)) return Effect.fail(error)
|
||||||
|
|
|
||||||
10
packages/server/src/options.ts
Normal file
10
packages/server/src/options.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import type { Database } from "@opencode-ai/core/database/database"
|
||||||
|
import type { ModelsDev } from "@opencode-ai/core/models-dev"
|
||||||
|
|
||||||
|
export interface ServerOptions {
|
||||||
|
readonly hostname?: string
|
||||||
|
readonly port?: number
|
||||||
|
readonly password?: string
|
||||||
|
readonly database?: Database.Options
|
||||||
|
readonly models?: ModelsDev.Options
|
||||||
|
}
|
||||||
|
|
@ -1,14 +1,13 @@
|
||||||
export * as ServerProcess from "./process"
|
export * as ServerProcess from "./process"
|
||||||
|
|
||||||
import { NodeHttpServer, NodeHttpServerRequest } from "@effect/platform-node"
|
import { NodeHttpServer, NodeHttpServerRequest } from "@effect/platform-node"
|
||||||
import { Database } from "@opencode-ai/core/database/database"
|
|
||||||
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||||
import { ModelsDev } from "@opencode-ai/core/models-dev"
|
|
||||||
import { SessionRestart } from "@opencode-ai/core/session/execution/restart"
|
import { SessionRestart } from "@opencode-ai/core/session/execution/restart"
|
||||||
import { ServiceStatus } from "@opencode-ai/protocol/groups/health"
|
import { ServiceStatus } from "@opencode-ai/protocol/groups/health"
|
||||||
import { hasPtyConnectTicketURL } from "@opencode-ai/protocol/groups/pty"
|
import { hasPtyConnectTicketURL } from "@opencode-ai/protocol/groups/pty"
|
||||||
import { Cause, Context, Deferred, Effect, Exit, Layer, Option, Ref, Schema, Scope } from "effect"
|
import { Cause, Context, Deferred, Effect, Exit, Layer, Option, Ref, Schema, Scope } from "effect"
|
||||||
import { HttpMiddleware, HttpRouter, HttpServer, HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
|
import { HttpMiddleware, HttpRouter, HttpServer, HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
|
||||||
|
import { randomUUID } from "node:crypto"
|
||||||
import { createServer } from "node:http"
|
import { createServer } from "node:http"
|
||||||
import { ServerAuth } from "./auth"
|
import { ServerAuth } from "./auth"
|
||||||
import { authorizedRequest } from "./middleware/authorization"
|
import { authorizedRequest } from "./middleware/authorization"
|
||||||
|
|
@ -16,20 +15,14 @@ import { withoutParentSpan } from "./request-tracing"
|
||||||
import { createRoutes } from "./routes"
|
import { createRoutes } from "./routes"
|
||||||
import { ServerInfo } from "./server-info"
|
import { ServerInfo } from "./server-info"
|
||||||
import { Status } from "./service-status"
|
import { Status } from "./service-status"
|
||||||
|
import type { ServerOptions } from "./options"
|
||||||
|
|
||||||
export type Options<E = never, R = never> = {
|
export interface Lifecycle<E = never, R = never> {
|
||||||
readonly hostname: string
|
|
||||||
readonly port: Option.Option<number>
|
|
||||||
readonly password: string
|
|
||||||
readonly instanceID: string
|
readonly instanceID: string
|
||||||
readonly database?: Database.Options
|
readonly onListen: (
|
||||||
readonly models?: ModelsDev.Options
|
address: HttpServer.Address,
|
||||||
readonly service?: {
|
shutdown: Effect.Effect<void>,
|
||||||
readonly onListen: (
|
) => Effect.Effect<Effect.Effect<void>, E, R>
|
||||||
address: HttpServer.Address,
|
|
||||||
shutdown: Effect.Effect<void>,
|
|
||||||
) => Effect.Effect<Effect.Effect<void>, E, R>
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type App = Effect.Effect<
|
type App = Effect.Effect<
|
||||||
|
|
@ -38,21 +31,27 @@ type App = Effect.Effect<
|
||||||
HttpServerRequest.HttpServerRequest | Scope.Scope
|
HttpServerRequest.HttpServerRequest | Scope.Scope
|
||||||
>
|
>
|
||||||
|
|
||||||
export const start = Effect.fn("ServerProcess.start")(function* <E, R>(options: Options<E, R>) {
|
export const start = Effect.fn("ServerProcess.start")(function* <E, R>(
|
||||||
if (!options.password) return yield* Effect.fail(new Error("Missing server password"))
|
options: ServerOptions,
|
||||||
|
lifecycle?: Lifecycle<E, R>,
|
||||||
|
) {
|
||||||
|
const password = options.password
|
||||||
|
if (!password) return yield* Effect.fail(new Error("Missing server password"))
|
||||||
|
const hostname = options.hostname ?? "127.0.0.1"
|
||||||
|
const port = Option.fromNullishOr(options.port)
|
||||||
const shutdown = yield* Deferred.make<void>()
|
const shutdown = yield* Deferred.make<void>()
|
||||||
const status = yield* Status.make({
|
const status = yield* Status.make({
|
||||||
instanceID: options.instanceID,
|
instanceID: lifecycle?.instanceID ?? randomUUID(),
|
||||||
managed: options.service !== undefined,
|
managed: lifecycle !== undefined,
|
||||||
})
|
})
|
||||||
const bound = yield* listen(options)
|
const bound = yield* listen({ hostname, port })
|
||||||
const application = yield* Ref.make(Option.none<App>())
|
const application = yield* Ref.make(Option.none<App>())
|
||||||
// Request fibers may continue inbound trace context, but must not inherit the server startup parent.
|
// Request fibers may continue inbound trace context, but must not inherit the server startup parent.
|
||||||
yield* bound.http
|
yield* bound.http
|
||||||
.serve(dispatch(options.password, status, application, shutdown), HttpMiddleware.logger)
|
.serve(dispatch(password, status, application, shutdown), HttpMiddleware.logger)
|
||||||
.pipe(withoutParentSpan)
|
.pipe(withoutParentSpan)
|
||||||
if (options.service)
|
if (lifecycle)
|
||||||
yield* options.service.onListen(bound.http.address, Deferred.succeed(shutdown, undefined).pipe(Effect.asVoid)).pipe(
|
yield* lifecycle.onListen(bound.http.address, Deferred.succeed(shutdown, undefined).pipe(Effect.asVoid)).pipe(
|
||||||
Effect.flatMap((cleanup) =>
|
Effect.flatMap((cleanup) =>
|
||||||
Effect.addFinalizer(() => Scope.close(bound.scope, Exit.void).pipe(Effect.andThen(cleanup))),
|
Effect.addFinalizer(() => Scope.close(bound.scope, Exit.void).pipe(Effect.andThen(cleanup))),
|
||||||
),
|
),
|
||||||
|
|
@ -70,20 +69,21 @@ export const start = Effect.fn("ServerProcess.start")(function* <E, R>(options:
|
||||||
|
|
||||||
const boot = Effect.gen(function* () {
|
const boot = Effect.gen(function* () {
|
||||||
const context = yield* Layer.buildWithScope(
|
const context = yield* Layer.buildWithScope(
|
||||||
createRoutes({
|
createRoutes(
|
||||||
password: options.password,
|
{
|
||||||
serviceURLs: () => {
|
...options,
|
||||||
|
password,
|
||||||
|
},
|
||||||
|
() => {
|
||||||
const address = bound.server.address()
|
const address = bound.server.address()
|
||||||
if (address === null || typeof address === "string") return []
|
if (address === null || typeof address === "string") return []
|
||||||
const host = address.family === "IPv6" ? `[${address.address}]` : address.address
|
const host = address.family === "IPv6" ? `[${address.address}]` : address.address
|
||||||
return ServerInfo.connectionURLs(`http://${host}:${address.port}`, options.hostname)
|
return ServerInfo.connectionURLs(`http://${host}:${address.port}`, hostname)
|
||||||
},
|
},
|
||||||
database: options.database,
|
).pipe(Layer.provide(NodeHttpServer.layerHttpServices)),
|
||||||
models: options.models,
|
|
||||||
}).pipe(Layer.provide(NodeHttpServer.layerHttpServices)),
|
|
||||||
applicationScope,
|
applicationScope,
|
||||||
)
|
)
|
||||||
if (options.service) {
|
if (lifecycle) {
|
||||||
yield* installRestartContinuity(Context.get(context, SessionRestart.Service)).pipe(
|
yield* installRestartContinuity(Context.get(context, SessionRestart.Service)).pipe(
|
||||||
Effect.provideService(Scope.Scope, applicationScope),
|
Effect.provideService(Scope.Scope, applicationScope),
|
||||||
)
|
)
|
||||||
|
|
@ -93,7 +93,7 @@ export const start = Effect.fn("ServerProcess.start")(function* <E, R>(options:
|
||||||
return { address: bound.http.address, shutdown: Deferred.await(shutdown) }
|
return { address: bound.http.address, shutdown: Deferred.await(shutdown) }
|
||||||
}).pipe(
|
}).pipe(
|
||||||
Effect.catchCause((cause) => {
|
Effect.catchCause((cause) => {
|
||||||
if (!options.service || Cause.hasInterruptsOnly(cause)) return Effect.failCause(cause)
|
if (!lifecycle || Cause.hasInterruptsOnly(cause)) return Effect.failCause(cause)
|
||||||
return status.fail.pipe(
|
return status.fail.pipe(
|
||||||
Effect.andThen(
|
Effect.andThen(
|
||||||
Scope.close(applicationScope, Exit.failCause(cause)).pipe(
|
Scope.close(applicationScope, Exit.failCause(cause)).pipe(
|
||||||
|
|
@ -107,7 +107,7 @@ export const start = Effect.fn("ServerProcess.start")(function* <E, R>(options:
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
if (!options.service) return yield* boot
|
if (!lifecycle) return yield* boot
|
||||||
return yield* Effect.raceFirst(boot, Deferred.await(shutdown).pipe(Effect.andThen(Effect.interrupt)))
|
return yield* Effect.raceFirst(boot, Deferred.await(shutdown).pipe(Effect.andThen(Effect.interrupt)))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ import { layer } from "./location"
|
||||||
import { formLocationLayer } from "./middleware/form-location"
|
import { formLocationLayer } from "./middleware/form-location"
|
||||||
import { sessionLocationLayer } from "./middleware/session-location"
|
import { sessionLocationLayer } from "./middleware/session-location"
|
||||||
import { ServerInfo } from "./server-info"
|
import { ServerInfo } from "./server-info"
|
||||||
|
import type { ServerOptions } from "./options"
|
||||||
|
|
||||||
const applicationServices = LayerNode.group([
|
const applicationServices = LayerNode.group([
|
||||||
Database.node,
|
Database.node,
|
||||||
|
|
@ -52,29 +53,24 @@ const applicationServices = LayerNode.group([
|
||||||
SessionRestart.node,
|
SessionRestart.node,
|
||||||
])
|
])
|
||||||
|
|
||||||
export interface Options {
|
export function createRoutes(options: ServerOptions = {}, serviceURLs: () => ReadonlyArray<string> = () => []) {
|
||||||
readonly password?: string
|
|
||||||
readonly serviceURLs?: () => ReadonlyArray<string>
|
|
||||||
readonly database?: Database.Options
|
|
||||||
readonly models?: ModelsDev.Options
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createRoutes(options: Options = {}) {
|
|
||||||
return makeRoutes(
|
return makeRoutes(
|
||||||
options.password
|
options.password
|
||||||
? ServerAuth.Config.configLayer({ username: "opencode", password: Option.some(options.password) })
|
? ServerAuth.Config.configLayer({ username: "opencode", password: Option.some(options.password) })
|
||||||
: ServerAuth.Config.layer,
|
: ServerAuth.Config.layer,
|
||||||
options,
|
options,
|
||||||
|
serviceURLs,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createEmbeddedRoutes(options: Pick<Options, "database" | "models"> = {}) {
|
export function createEmbeddedRoutes(options: ServerOptions = {}) {
|
||||||
return makeRoutes(ServerAuth.Config.configLayer({ username: "opencode", password: Option.none() }), options)
|
return makeRoutes(ServerAuth.Config.configLayer({ username: "opencode", password: Option.none() }), options, () => [])
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeRoutes<AuthError, AuthServices>(
|
function makeRoutes<AuthError, AuthServices>(
|
||||||
auth: Layer.Layer<ServerAuth.Config, AuthError, AuthServices>,
|
auth: Layer.Layer<ServerAuth.Config, AuthError, AuthServices>,
|
||||||
options: Pick<Options, "database" | "models" | "serviceURLs">,
|
options: ServerOptions,
|
||||||
|
serviceURLs: () => ReadonlyArray<string>,
|
||||||
) {
|
) {
|
||||||
const pluginRuntimeCell = PluginRuntime.makeCell()
|
const pluginRuntimeCell = PluginRuntime.makeCell()
|
||||||
const replacements: LayerNode.Replacements = [
|
const replacements: LayerNode.Replacements = [
|
||||||
|
|
@ -98,7 +94,7 @@ function makeRoutes<AuthError, AuthServices>(
|
||||||
const services = Layer.succeedContext(context)
|
const services = Layer.succeedContext(context)
|
||||||
const requestServices = Layer.merge(
|
const requestServices = Layer.merge(
|
||||||
Layer.succeedContext(Context.pick(PermissionSaved.Service, Project.Service, WellKnown.Service)(context)),
|
Layer.succeedContext(Context.pick(PermissionSaved.Service, Project.Service, WellKnown.Service)(context)),
|
||||||
ServerInfo.layer(options.serviceURLs ?? (() => [])),
|
ServerInfo.layer(serviceURLs),
|
||||||
)
|
)
|
||||||
return HttpApiBuilder.layer(Api, { openapiPath: "/openapi.json" }).pipe(
|
return HttpApiBuilder.layer(Api, { openapiPath: "/openapi.json" }).pipe(
|
||||||
Layer.provide(handlers.pipe(Layer.provide(services))),
|
Layer.provide(handlers.pipe(Layer.provide(services))),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue