serve: type listener transports explicitly
This commit is contained in:
parent
971fe35e81
commit
a2008478f9
3 changed files with 53 additions and 20 deletions
|
|
@ -25,10 +25,12 @@ export const ServeCommand = effectCmd({
|
|||
if (conflicts.length) yield* fail(`--socket cannot be used with ${conflicts.join(", ")}`)
|
||||
}
|
||||
const opts = yield* resolveNetworkOptions(args)
|
||||
const server = yield* Effect.promise(() =>
|
||||
Server.listen(args.socket ? { ...opts, socket: resolveSocketPath(args.socket) } : opts),
|
||||
)
|
||||
if (server.socket) {
|
||||
const server = args.socket
|
||||
? yield* Effect.promise(() =>
|
||||
Server.listen({ type: "socket", socket: resolveSocketPath(args.socket), cors: opts.cors }),
|
||||
)
|
||||
: yield* Effect.promise(() => Server.listen(opts))
|
||||
if (server.type === "socket") {
|
||||
console.log(`opencode server listening on socket ${server.socket}`)
|
||||
yield* Effect.never
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,35 +19,55 @@ globalThis.AI_SDK_LOG_WARNINGS = false
|
|||
|
||||
const log = Log.create({ service: "server" })
|
||||
|
||||
export type Listener = {
|
||||
export type TcpListener = {
|
||||
type: "tcp"
|
||||
hostname: string
|
||||
port: number
|
||||
url: URL
|
||||
socket?: string
|
||||
stop: (close?: boolean) => Promise<void>
|
||||
}
|
||||
|
||||
export type SocketListener = {
|
||||
type: "socket"
|
||||
socket: string
|
||||
url: URL
|
||||
stop: (close?: boolean) => Promise<void>
|
||||
}
|
||||
|
||||
export type Listener = TcpListener | SocketListener
|
||||
|
||||
type ServerApp = {
|
||||
fetch(request: Request): Response | Promise<Response>
|
||||
request(input: string | URL | Request, init?: RequestInit): Response | Promise<Response>
|
||||
}
|
||||
|
||||
type ListenOptions = CorsOptions & {
|
||||
export type TcpListenOptions = CorsOptions & {
|
||||
type?: "tcp"
|
||||
port: number
|
||||
hostname: string
|
||||
socket?: string
|
||||
mdns?: boolean
|
||||
mdnsDomain?: string
|
||||
}
|
||||
|
||||
export type SocketListenOptions = CorsOptions & {
|
||||
type: "socket"
|
||||
socket: string
|
||||
}
|
||||
|
||||
export type ListenOptions = TcpListenOptions | SocketListenOptions
|
||||
type ListenerState = {
|
||||
scope: Scope.Scope
|
||||
server: Context.Service.Shape<typeof HttpServer.HttpServer>
|
||||
http: ListenerServer
|
||||
websockets: WebSocketTracker.Interface
|
||||
}
|
||||
type EffectListener = Omit<Listener, "stop"> & {
|
||||
type EffectTcpListener = Omit<TcpListener, "stop"> & {
|
||||
stop: (close?: boolean) => Effect.Effect<void>
|
||||
}
|
||||
type EffectSocketListener = Omit<SocketListener, "stop"> & {
|
||||
stop: (close?: boolean) => Effect.Effect<void>
|
||||
}
|
||||
type EffectListener = EffectTcpListener | EffectSocketListener
|
||||
|
||||
interface ListenerServer {
|
||||
readonly closeAll: Effect.Effect<void>
|
||||
|
|
@ -74,21 +94,32 @@ export async function openapi() {
|
|||
|
||||
export let url: URL
|
||||
|
||||
export function listen(opts: TcpListenOptions): Promise<TcpListener>
|
||||
export function listen(opts: SocketListenOptions): Promise<SocketListener>
|
||||
export async function listen(opts: ListenOptions): Promise<Listener> {
|
||||
const listener = await Effect.runPromise(listenEffect(opts))
|
||||
const stop = (close?: boolean) => Effect.runPromiseExit(listener.stop(close)).then(() => undefined)
|
||||
if (listener.type === "socket") {
|
||||
return {
|
||||
type: "socket" as const,
|
||||
socket: listener.socket,
|
||||
url: listener.url,
|
||||
stop,
|
||||
}
|
||||
}
|
||||
return {
|
||||
type: "tcp" as const,
|
||||
hostname: listener.hostname,
|
||||
port: listener.port,
|
||||
url: listener.url,
|
||||
socket: listener.socket,
|
||||
stop: (close?: boolean) => Effect.runPromiseExit(listener.stop(close)).then(() => undefined),
|
||||
stop,
|
||||
}
|
||||
}
|
||||
|
||||
const listenEffect: (opts: ListenOptions) => Effect.Effect<EffectListener, unknown> = Effect.fn("Server.listen")(
|
||||
function* (opts: ListenOptions) {
|
||||
const state = yield* startWithPortFallback(opts)
|
||||
if (opts.socket) return yield* makeSocketListener(state, opts.socket)
|
||||
if (opts.type === "socket") return yield* makeSocketListener(state, opts.socket)
|
||||
|
||||
const address = yield* tcpAddress(state)
|
||||
const listenerUrl = makeURL(opts.hostname, address.port)
|
||||
|
|
@ -97,6 +128,7 @@ const listenEffect: (opts: ListenOptions) => Effect.Effect<EffectListener, unkno
|
|||
const unpublishMdns = yield* setupMdns(opts, address.port, state.scope)
|
||||
|
||||
return {
|
||||
type: "tcp" as const,
|
||||
hostname: opts.hostname,
|
||||
port: address.port,
|
||||
url: listenerUrl,
|
||||
|
|
@ -123,7 +155,7 @@ function listenerLayer(opts: ListenOptions, port: number) {
|
|||
}
|
||||
|
||||
function startWithPortFallback(opts: ListenOptions) {
|
||||
if (opts.socket) return startListener(opts, 0)
|
||||
if (opts.type === "socket") return startListener(opts, 0)
|
||||
if (opts.port !== 0) return startListener(opts, opts.port)
|
||||
// Match the legacy listener port-resolution behavior: explicit `0` prefers
|
||||
// 4096 first, then any free port.
|
||||
|
|
@ -152,8 +184,7 @@ function makeSocketListener(state: ListenerState, socket: string) {
|
|||
url = listenerUrl
|
||||
|
||||
return {
|
||||
hostname: "localhost",
|
||||
port: 0,
|
||||
type: "socket" as const,
|
||||
socket,
|
||||
url: listenerUrl,
|
||||
stop: yield* makeStop(state, Effect.void),
|
||||
|
|
@ -176,7 +207,7 @@ function makeURL(hostname: string, port: number) {
|
|||
return result
|
||||
}
|
||||
|
||||
function setupMdns(opts: ListenOptions, port: number, scope: Scope.Scope) {
|
||||
function setupMdns(opts: TcpListenOptions, port: number, scope: Scope.Scope) {
|
||||
return Effect.gen(function* () {
|
||||
const publish =
|
||||
opts.mdns && port && opts.hostname !== "127.0.0.1" && opts.hostname !== "localhost" && opts.hostname !== "::1"
|
||||
|
|
@ -237,7 +268,7 @@ function serverLayer(opts: ListenOptions, port: number) {
|
|||
}
|
||||
|
||||
function nodeListenOptions(opts: ListenOptions, port: number) {
|
||||
if (opts.socket) return { path: opts.socket, gracefulShutdownTimeout: "1 second" as const }
|
||||
if (opts.type === "socket") return { path: opts.socket, gracefulShutdownTimeout: "1 second" as const }
|
||||
return { port, host: opts.hostname, gracefulShutdownTimeout: "1 second" as const }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ async function expectSocketRejected(url: URL, init?: { headers?: Record<string,
|
|||
)
|
||||
}
|
||||
|
||||
function stop(listener: Awaited<ReturnType<typeof startListener>>, label: string) {
|
||||
function stop(listener: { stop(close?: boolean): Promise<void> }, label: string) {
|
||||
return withTimeout(listener.stop(true), 10_000, label)
|
||||
}
|
||||
|
||||
|
|
@ -293,9 +293,9 @@ describe("HttpApi Server.listen", () => {
|
|||
process.platform === "win32"
|
||||
? `\\\\.\\pipe\\opencode-test-${process.pid}-${Date.now()}`
|
||||
: path.join(tmp.path, "opencode.sock")
|
||||
const listener = await Server.listen({ hostname: "127.0.0.1", port: 0, socket })
|
||||
const listener = await Server.listen({ type: "socket", socket })
|
||||
try {
|
||||
expect(listener.port).toBe(0)
|
||||
expect(listener.type).toBe("socket")
|
||||
expect(listener.socket).toBe(socket)
|
||||
|
||||
const response = await requestSocketRoot(socket)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue