Skip to content

Commit 5242a1c

Browse files
authored
fix(httpapi): install Instance ALS for adapter Promise bridge (#25417)
1 parent 075f876 commit 5242a1c

4 files changed

Lines changed: 49 additions & 12 deletions

File tree

packages/opencode/src/control-plane/workspace.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { SessionID } from "@/session/schema"
2525
import { errorData } from "@/util/error"
2626
import { waitEvent } from "./util"
2727
import { WorkspaceContext } from "./workspace-context"
28+
import { EffectBridge } from "@/effect/bridge"
2829
import { NonNegativeInt, withStatics } from "@/util/schema"
2930
import { zod as effectZod, zodObject } from "@/util/effect-zod"
3031

@@ -336,7 +337,7 @@ export const layer = Layer.effect(
336337

337338
const syncWorkspaceLoop = Effect.fn("Workspace.syncWorkspaceLoop")(function* (space: Info) {
338339
const adapter = getAdapter(space.projectID, space.type)
339-
const target = yield* Effect.promise(() => Promise.resolve(adapter.target(space)))
340+
const target = yield* EffectBridge.fromPromise(() => adapter.target(space))
340341

341342
if (target.type === "local") return
342343

@@ -420,7 +421,7 @@ export const layer = Layer.effect(
420421
if (!Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) return
421422

422423
const adapter = getAdapter(space.projectID, space.type)
423-
const target = yield* Effect.promise(() => Promise.resolve(adapter.target(space)))
424+
const target = yield* EffectBridge.fromPromise(() => adapter.target(space))
424425

425426
if (target.type === "local") {
426427
setStatus(space.id, (yield* Effect.promise(() => Filesystem.exists(target.directory))) ? "connected" : "error")
@@ -459,8 +460,8 @@ export const layer = Layer.effect(
459460
const create = Effect.fn("Workspace.create")(function* (input: CreateInput) {
460461
const id = WorkspaceID.ascending(input.id)
461462
const adapter = getAdapter(input.projectID, input.type)
462-
const config = yield* Effect.promise(() =>
463-
Promise.resolve(adapter.configure({ ...input, id, name: Slug.create(), directory: null })),
463+
const config = yield* EffectBridge.fromPromise(() =>
464+
adapter.configure({ ...input, id, name: Slug.create(), directory: null }),
464465
)
465466

466467
const info: Info = {
@@ -496,7 +497,7 @@ export const layer = Layer.effect(
496497
OTEL_RESOURCE_ATTRIBUTES: process.env.OTEL_RESOURCE_ATTRIBUTES,
497498
}
498499

499-
yield* Effect.promise(() => adapter.create(config, env))
500+
yield* EffectBridge.fromPromise(() => adapter.create(config, env))
500501
yield* Effect.all(
501502
[
502503
waitEvent({
@@ -532,7 +533,7 @@ export const layer = Layer.effect(
532533
})
533534

534535
const adapter = getAdapter(space.projectID, space.type)
535-
const target = yield* Effect.promise(() => Promise.resolve(adapter.target(space)))
536+
const target = yield* EffectBridge.fromPromise(() => adapter.target(space))
536537

537538
yield* sync.run(Session.Event.Updated, {
538539
sessionID: input.sessionID,
@@ -724,10 +725,10 @@ export const layer = Layer.effect(
724725
yield* stopSync(id)
725726

726727
const info = fromRow(row)
727-
yield* Effect.catch(
728+
yield* Effect.catchCause(
728729
Effect.gen(function* () {
729730
const adapter = getAdapter(info.projectID, row.type)
730-
yield* Effect.tryPromise(() => Promise.resolve(adapter.remove(info)))
731+
yield* EffectBridge.fromPromise(() => adapter.remove(info))
731732
}),
732733
() =>
733734
Effect.sync(() => {

packages/opencode/src/effect/bridge.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,25 @@ function restore<R>(instance: InstanceContext | undefined, workspace: WorkspaceI
2121
return fn()
2222
}
2323

24+
/**
25+
* Bridge from Effect into a Promise-returning JS callback while installing
26+
* legacy `Instance.context` and `WorkspaceContext` AsyncLocalStorage for
27+
* the duration of the callback. Effect's `InstanceRef`/`WorkspaceRef` do
28+
* not propagate across async/await boundaries inside `Effect.promise(() =>
29+
* async fn)` callbacks that re-enter Effect via `AppRuntime.runPromise`,
30+
* but Node's AsyncLocalStorage does. Use this whenever an Effect crosses
31+
* into JS that may itself spawn new Effect runtimes (workspace adapters,
32+
* legacy plugins, etc.).
33+
*
34+
* Mirrors `Effect.promise` but restores legacy ALS first.
35+
*/
36+
export const fromPromise = <T>(fn: () => Promise<T> | T): Effect.Effect<T> =>
37+
Effect.gen(function* () {
38+
const instance = yield* InstanceRef
39+
const workspace = yield* WorkspaceRef
40+
return yield* Effect.promise(() => Promise.resolve(restore(instance, workspace, () => fn())))
41+
})
42+
2443
export function make(): Effect.Effect<Shape> {
2544
return Effect.gen(function* () {
2645
const ctx = yield* Effect.context()

packages/opencode/src/server/routes/instance/httpapi/middleware/workspace-routing.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { getAdapter } from "@/control-plane/adapters"
22
import { WorkspaceID } from "@/control-plane/schema"
33
import type { Target } from "@/control-plane/types"
44
import { Workspace } from "@/control-plane/workspace"
5+
import { EffectBridge } from "@/effect/bridge"
56
import { Session } from "@/session/session"
67
import { HttpApiProxy } from "./proxy"
78
import * as Fence from "@/server/fence"
@@ -79,10 +80,8 @@ function missingWorkspaceResponse(id: WorkspaceID): HttpServerResponse.HttpServe
7980
}
8081

8182
function resolveTarget(workspace: Workspace.Info): Effect.Effect<Target> {
82-
return Effect.gen(function* () {
83-
const adapter = yield* Effect.sync(() => getAdapter(workspace.projectID, workspace.type))
84-
return yield* Effect.promise(() => Promise.resolve(adapter.target(workspace)))
85-
})
83+
const adapter = getAdapter(workspace.projectID, workspace.type)
84+
return EffectBridge.fromPromise(() => adapter.target(workspace))
8685
}
8786

8887
function proxyRemote(

packages/opencode/test/server/httpapi-workspace.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,24 @@ describe("workspace HttpApi", () => {
217217
}),
218218
)
219219

220+
it.live("creates a real git worktree workspace via the builtin adapter", () =>
221+
Effect.gen(function* () {
222+
Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = true
223+
const dir = yield* tmpdirScoped({ git: true })
224+
225+
const created = yield* request(WorkspacePaths.list, dir, {
226+
method: "POST",
227+
headers: { "content-type": "application/json" },
228+
body: JSON.stringify({ type: "worktree", branch: null }),
229+
})
230+
231+
const body = yield* Effect.promise(() => created.text())
232+
expect({ status: created.status, body }).toMatchObject({ status: 200 })
233+
const workspace = JSON.parse(body) as Workspace.Info
234+
expect(workspace).toMatchObject({ type: "worktree" })
235+
}),
236+
)
237+
220238
it.live("documents legacy Hono accepting the TUI payload shape", () =>
221239
Effect.gen(function* () {
222240
Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = true

0 commit comments

Comments
 (0)