Skip to content

Commit 7739cc5

Browse files
authored
refactor(httpapi): fork server startup by flag (#24799)
1 parent 3fa78a8 commit 7739cc5

23 files changed

Lines changed: 190 additions & 371 deletions

packages/opencode/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ src/provider/models-snapshot.js
77
src/provider/models-snapshot.d.ts
88
script/build-*.ts
99
temporary-*.md
10+
.artifacts

packages/opencode/src/cli/cmd/generate.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,26 @@
11
import { Server } from "../../server/server"
2+
import { PublicApi } from "../../server/routes/instance/httpapi/public"
23
import type { CommandModule } from "yargs"
4+
import { OpenApi } from "effect/unstable/httpapi"
5+
6+
type Args = {
7+
httpapi: boolean
8+
}
39

410
export const GenerateCommand = {
511
command: "generate",
6-
handler: async () => {
7-
const specs = await Server.openapi()
12+
builder: (yargs) =>
13+
yargs.option("httpapi", {
14+
type: "boolean",
15+
default: false,
16+
description: "Generate OpenAPI from the experimental Effect HttpApi contract",
17+
}),
18+
handler: async (args) => {
19+
const specs = args.httpapi ? OpenApi.fromApi(PublicApi) : await Server.openapi()
820
for (const item of Object.values(specs.paths)) {
921
for (const method of ["get", "post", "put", "delete", "patch"] as const) {
1022
const operation = item[method]
1123
if (!operation?.operationId) continue
12-
// @ts-expect-error
1324
operation["x-codeSamples"] = [
1425
{
1526
lang: "js",
@@ -47,4 +58,4 @@ export const GenerateCommand = {
4758
})
4859
})
4960
},
50-
} satisfies CommandModule
61+
} satisfies CommandModule<object, Args>

packages/opencode/src/plugin/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ export const layer = Layer.effect(
127127
Authorization: `Basic ${Buffer.from(`${Flag.OPENCODE_SERVER_USERNAME ?? "opencode"}:${Flag.OPENCODE_SERVER_PASSWORD}`).toString("base64")}`,
128128
}
129129
: undefined,
130-
fetch: async (...args) => (await Server.Default()).app.fetch(...args),
130+
fetch: async (...args) => Server.Default().app.fetch(...args),
131131
})
132132
const cfg = yield* config.get()
133133
const input: PluginInput = {
Lines changed: 33 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,44 @@
11
import type { Hono } from "hono"
22
import { createBunWebSocket } from "hono/bun"
3-
import type { Adapter } from "./adapter"
3+
import type { Adapter, FetchApp, Opts } from "./adapter"
4+
5+
function listen(app: FetchApp, opts: Opts, websocket?: ReturnType<typeof createBunWebSocket>["websocket"]) {
6+
const start = (port: number) => {
7+
try {
8+
if (websocket) {
9+
return Bun.serve({ fetch: app.fetch, hostname: opts.hostname, idleTimeout: 0, websocket, port })
10+
}
11+
return Bun.serve({ fetch: app.fetch, hostname: opts.hostname, idleTimeout: 0, port })
12+
} catch {
13+
return
14+
}
15+
}
16+
const server = opts.port === 0 ? (start(4096) ?? start(0)) : start(opts.port)
17+
if (!server) {
18+
throw new Error(`Failed to start server on port ${opts.port}`)
19+
}
20+
if (!server.port) {
21+
throw new Error(`Failed to resolve server address for port ${opts.port}`)
22+
}
23+
return {
24+
port: server.port,
25+
stop(close?: boolean) {
26+
return Promise.resolve(server.stop(close))
27+
},
28+
}
29+
}
430

531
export const adapter: Adapter = {
632
create(app: Hono) {
733
const ws = createBunWebSocket()
834
return {
935
upgradeWebSocket: ws.upgradeWebSocket,
10-
async listen(opts) {
11-
const args = {
12-
fetch: app.fetch,
13-
hostname: opts.hostname,
14-
idleTimeout: 0,
15-
websocket: ws.websocket,
16-
} as const
17-
const start = (port: number) => {
18-
try {
19-
return Bun.serve({ ...args, port })
20-
} catch {
21-
return
22-
}
23-
}
24-
const server = opts.port === 0 ? (start(4096) ?? start(0)) : start(opts.port)
25-
if (!server) {
26-
throw new Error(`Failed to start server on port ${opts.port}`)
27-
}
28-
if (!server.port) {
29-
throw new Error(`Failed to resolve server address for port ${opts.port}`)
30-
}
31-
return {
32-
port: server.port,
33-
stop(close?: boolean) {
34-
return Promise.resolve(server.stop(close))
35-
},
36-
}
37-
},
36+
listen: (opts) => Promise.resolve(listen(app, opts, ws.websocket)),
37+
}
38+
},
39+
createFetch(app) {
40+
return {
41+
listen: (opts) => Promise.resolve(listen(app, opts)),
3842
}
3943
},
4044
}
Lines changed: 61 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,73 @@
11
import { createAdaptorServer, type ServerType } from "@hono/node-server"
22
import { createNodeWebSocket } from "@hono/node-ws"
33
import type { Hono } from "hono"
4-
import type { Adapter } from "./adapter"
4+
import type { Adapter, FetchApp, Opts } from "./adapter"
5+
6+
async function listen(app: FetchApp, opts: Opts, inject?: (server: ServerType) => void) {
7+
const start = (port: number) =>
8+
new Promise<ServerType>((resolve, reject) => {
9+
const server = createAdaptorServer({ fetch: app.fetch })
10+
inject?.(server)
11+
const fail = (err: Error) => {
12+
cleanup()
13+
reject(err)
14+
}
15+
const ready = () => {
16+
cleanup()
17+
resolve(server)
18+
}
19+
const cleanup = () => {
20+
server.off("error", fail)
21+
server.off("listening", ready)
22+
}
23+
server.once("error", fail)
24+
server.once("listening", ready)
25+
server.listen(port, opts.hostname)
26+
})
27+
28+
const server = opts.port === 0 ? await start(4096).catch(() => start(0)) : await start(opts.port)
29+
const addr = server.address()
30+
if (!addr || typeof addr === "string") {
31+
throw new Error(`Failed to resolve server address for port ${opts.port}`)
32+
}
33+
34+
let closing: Promise<void> | undefined
35+
return {
36+
port: addr.port,
37+
stop(close?: boolean) {
38+
closing ??= new Promise<void>((resolve, reject) => {
39+
server.close((err) => {
40+
if (err) {
41+
reject(err)
42+
return
43+
}
44+
resolve()
45+
})
46+
if (close) {
47+
if ("closeAllConnections" in server && typeof server.closeAllConnections === "function") {
48+
server.closeAllConnections()
49+
}
50+
if ("closeIdleConnections" in server && typeof server.closeIdleConnections === "function") {
51+
server.closeIdleConnections()
52+
}
53+
}
54+
})
55+
return closing
56+
},
57+
}
58+
}
559

660
export const adapter: Adapter = {
761
create(app: Hono) {
862
const ws = createNodeWebSocket({ app })
963
return {
1064
upgradeWebSocket: ws.upgradeWebSocket,
11-
async listen(opts) {
12-
const start = (port: number) =>
13-
new Promise<ServerType>((resolve, reject) => {
14-
const server = createAdaptorServer({ fetch: app.fetch })
15-
ws.injectWebSocket(server)
16-
const fail = (err: Error) => {
17-
cleanup()
18-
reject(err)
19-
}
20-
const ready = () => {
21-
cleanup()
22-
resolve(server)
23-
}
24-
const cleanup = () => {
25-
server.off("error", fail)
26-
server.off("listening", ready)
27-
}
28-
server.once("error", fail)
29-
server.once("listening", ready)
30-
server.listen(port, opts.hostname)
31-
})
32-
33-
const server = opts.port === 0 ? await start(4096).catch(() => start(0)) : await start(opts.port)
34-
const addr = server.address()
35-
if (!addr || typeof addr === "string") {
36-
throw new Error(`Failed to resolve server address for port ${opts.port}`)
37-
}
38-
39-
let closing: Promise<void> | undefined
40-
return {
41-
port: addr.port,
42-
stop(close?: boolean) {
43-
closing ??= new Promise((resolve, reject) => {
44-
server.close((err) => {
45-
if (err) {
46-
reject(err)
47-
return
48-
}
49-
resolve()
50-
})
51-
if (close) {
52-
if ("closeAllConnections" in server && typeof server.closeAllConnections === "function") {
53-
server.closeAllConnections()
54-
}
55-
if ("closeIdleConnections" in server && typeof server.closeIdleConnections === "function") {
56-
server.closeIdleConnections()
57-
}
58-
}
59-
})
60-
return closing
61-
},
62-
}
63-
},
65+
listen: (opts) => listen(app, opts, ws.injectWebSocket),
66+
}
67+
},
68+
createFetch(app) {
69+
return {
70+
listen: (opts) => listen(app, opts),
6471
}
6572
},
6673
}

packages/opencode/src/server/adapter.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import type { Hono } from "hono"
22
import type { UpgradeWebSocket } from "hono/ws"
33

4+
export type FetchApp = {
5+
fetch(request: Request): Response | Promise<Response>
6+
}
7+
48
export type Opts = {
59
port: number
610
hostname: string
@@ -18,4 +22,5 @@ export interface Runtime {
1822

1923
export interface Adapter {
2024
create(app: Hono): Runtime
25+
createFetch(app: FetchApp): Omit<Runtime, "upgradeWebSocket">
2126
}

packages/opencode/src/server/routes/instance/httpapi/server.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Effect, Layer, Schema } from "effect"
1+
import { Context, Effect, Layer, Schema } from "effect"
22
import { HttpApiBuilder } from "effect/unstable/httpapi"
33
import { HttpRouter, HttpServer, HttpServerRequest } from "effect/unstable/http"
44
import { Bus } from "@/bus"
@@ -41,6 +41,8 @@ const Headers = Schema.Struct({
4141
"x-opencode-directory": Schema.optional(Schema.String),
4242
})
4343

44+
export const context = Context.empty() as Context.Context<unknown>
45+
4446
function decode(input: string) {
4547
try {
4648
return decodeURIComponent(input)

0 commit comments

Comments
 (0)