fix(httpapi): install Instance ALS for adapter Promise bridge (#25417)
This commit is contained in:
@@ -25,6 +25,7 @@ import { SessionID } from "@/session/schema"
|
|||||||
import { errorData } from "@/util/error"
|
import { errorData } from "@/util/error"
|
||||||
import { waitEvent } from "./util"
|
import { waitEvent } from "./util"
|
||||||
import { WorkspaceContext } from "./workspace-context"
|
import { WorkspaceContext } from "./workspace-context"
|
||||||
|
import { EffectBridge } from "@/effect/bridge"
|
||||||
import { NonNegativeInt, withStatics } from "@/util/schema"
|
import { NonNegativeInt, withStatics } from "@/util/schema"
|
||||||
import { zod as effectZod, zodObject } from "@/util/effect-zod"
|
import { zod as effectZod, zodObject } from "@/util/effect-zod"
|
||||||
|
|
||||||
@@ -336,7 +337,7 @@ export const layer = Layer.effect(
|
|||||||
|
|
||||||
const syncWorkspaceLoop = Effect.fn("Workspace.syncWorkspaceLoop")(function* (space: Info) {
|
const syncWorkspaceLoop = Effect.fn("Workspace.syncWorkspaceLoop")(function* (space: Info) {
|
||||||
const adapter = getAdapter(space.projectID, space.type)
|
const adapter = getAdapter(space.projectID, space.type)
|
||||||
const target = yield* Effect.promise(() => Promise.resolve(adapter.target(space)))
|
const target = yield* EffectBridge.fromPromise(() => adapter.target(space))
|
||||||
|
|
||||||
if (target.type === "local") return
|
if (target.type === "local") return
|
||||||
|
|
||||||
@@ -420,7 +421,7 @@ export const layer = Layer.effect(
|
|||||||
if (!Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) return
|
if (!Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) return
|
||||||
|
|
||||||
const adapter = getAdapter(space.projectID, space.type)
|
const adapter = getAdapter(space.projectID, space.type)
|
||||||
const target = yield* Effect.promise(() => Promise.resolve(adapter.target(space)))
|
const target = yield* EffectBridge.fromPromise(() => adapter.target(space))
|
||||||
|
|
||||||
if (target.type === "local") {
|
if (target.type === "local") {
|
||||||
setStatus(space.id, (yield* Effect.promise(() => Filesystem.exists(target.directory))) ? "connected" : "error")
|
setStatus(space.id, (yield* Effect.promise(() => Filesystem.exists(target.directory))) ? "connected" : "error")
|
||||||
@@ -459,8 +460,8 @@ export const layer = Layer.effect(
|
|||||||
const create = Effect.fn("Workspace.create")(function* (input: CreateInput) {
|
const create = Effect.fn("Workspace.create")(function* (input: CreateInput) {
|
||||||
const id = WorkspaceID.ascending(input.id)
|
const id = WorkspaceID.ascending(input.id)
|
||||||
const adapter = getAdapter(input.projectID, input.type)
|
const adapter = getAdapter(input.projectID, input.type)
|
||||||
const config = yield* Effect.promise(() =>
|
const config = yield* EffectBridge.fromPromise(() =>
|
||||||
Promise.resolve(adapter.configure({ ...input, id, name: Slug.create(), directory: null })),
|
adapter.configure({ ...input, id, name: Slug.create(), directory: null }),
|
||||||
)
|
)
|
||||||
|
|
||||||
const info: Info = {
|
const info: Info = {
|
||||||
@@ -496,7 +497,7 @@ export const layer = Layer.effect(
|
|||||||
OTEL_RESOURCE_ATTRIBUTES: process.env.OTEL_RESOURCE_ATTRIBUTES,
|
OTEL_RESOURCE_ATTRIBUTES: process.env.OTEL_RESOURCE_ATTRIBUTES,
|
||||||
}
|
}
|
||||||
|
|
||||||
yield* Effect.promise(() => adapter.create(config, env))
|
yield* EffectBridge.fromPromise(() => adapter.create(config, env))
|
||||||
yield* Effect.all(
|
yield* Effect.all(
|
||||||
[
|
[
|
||||||
waitEvent({
|
waitEvent({
|
||||||
@@ -532,7 +533,7 @@ export const layer = Layer.effect(
|
|||||||
})
|
})
|
||||||
|
|
||||||
const adapter = getAdapter(space.projectID, space.type)
|
const adapter = getAdapter(space.projectID, space.type)
|
||||||
const target = yield* Effect.promise(() => Promise.resolve(adapter.target(space)))
|
const target = yield* EffectBridge.fromPromise(() => adapter.target(space))
|
||||||
|
|
||||||
yield* sync.run(Session.Event.Updated, {
|
yield* sync.run(Session.Event.Updated, {
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
@@ -724,10 +725,10 @@ export const layer = Layer.effect(
|
|||||||
yield* stopSync(id)
|
yield* stopSync(id)
|
||||||
|
|
||||||
const info = fromRow(row)
|
const info = fromRow(row)
|
||||||
yield* Effect.catch(
|
yield* Effect.catchCause(
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const adapter = getAdapter(info.projectID, row.type)
|
const adapter = getAdapter(info.projectID, row.type)
|
||||||
yield* Effect.tryPromise(() => Promise.resolve(adapter.remove(info)))
|
yield* EffectBridge.fromPromise(() => adapter.remove(info))
|
||||||
}),
|
}),
|
||||||
() =>
|
() =>
|
||||||
Effect.sync(() => {
|
Effect.sync(() => {
|
||||||
|
|||||||
@@ -21,6 +21,25 @@ function restore<R>(instance: InstanceContext | undefined, workspace: WorkspaceI
|
|||||||
return fn()
|
return fn()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bridge from Effect into a Promise-returning JS callback while installing
|
||||||
|
* legacy `Instance.context` and `WorkspaceContext` AsyncLocalStorage for
|
||||||
|
* the duration of the callback. Effect's `InstanceRef`/`WorkspaceRef` do
|
||||||
|
* not propagate across async/await boundaries inside `Effect.promise(() =>
|
||||||
|
* async fn)` callbacks that re-enter Effect via `AppRuntime.runPromise`,
|
||||||
|
* but Node's AsyncLocalStorage does. Use this whenever an Effect crosses
|
||||||
|
* into JS that may itself spawn new Effect runtimes (workspace adapters,
|
||||||
|
* legacy plugins, etc.).
|
||||||
|
*
|
||||||
|
* Mirrors `Effect.promise` but restores legacy ALS first.
|
||||||
|
*/
|
||||||
|
export const fromPromise = <T>(fn: () => Promise<T> | T): Effect.Effect<T> =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const instance = yield* InstanceRef
|
||||||
|
const workspace = yield* WorkspaceRef
|
||||||
|
return yield* Effect.promise(() => Promise.resolve(restore(instance, workspace, () => fn())))
|
||||||
|
})
|
||||||
|
|
||||||
export function make(): Effect.Effect<Shape> {
|
export function make(): Effect.Effect<Shape> {
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
const ctx = yield* Effect.context()
|
const ctx = yield* Effect.context()
|
||||||
|
|||||||
+3
-4
@@ -2,6 +2,7 @@ import { getAdapter } from "@/control-plane/adapters"
|
|||||||
import { WorkspaceID } from "@/control-plane/schema"
|
import { WorkspaceID } from "@/control-plane/schema"
|
||||||
import type { Target } from "@/control-plane/types"
|
import type { Target } from "@/control-plane/types"
|
||||||
import { Workspace } from "@/control-plane/workspace"
|
import { Workspace } from "@/control-plane/workspace"
|
||||||
|
import { EffectBridge } from "@/effect/bridge"
|
||||||
import { Session } from "@/session/session"
|
import { Session } from "@/session/session"
|
||||||
import { HttpApiProxy } from "./proxy"
|
import { HttpApiProxy } from "./proxy"
|
||||||
import * as Fence from "@/server/fence"
|
import * as Fence from "@/server/fence"
|
||||||
@@ -79,10 +80,8 @@ function missingWorkspaceResponse(id: WorkspaceID): HttpServerResponse.HttpServe
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resolveTarget(workspace: Workspace.Info): Effect.Effect<Target> {
|
function resolveTarget(workspace: Workspace.Info): Effect.Effect<Target> {
|
||||||
return Effect.gen(function* () {
|
const adapter = getAdapter(workspace.projectID, workspace.type)
|
||||||
const adapter = yield* Effect.sync(() => getAdapter(workspace.projectID, workspace.type))
|
return EffectBridge.fromPromise(() => adapter.target(workspace))
|
||||||
return yield* Effect.promise(() => Promise.resolve(adapter.target(workspace)))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function proxyRemote(
|
function proxyRemote(
|
||||||
|
|||||||
@@ -217,6 +217,24 @@ describe("workspace HttpApi", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.live("creates a real git worktree workspace via the builtin adapter", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = true
|
||||||
|
const dir = yield* tmpdirScoped({ git: true })
|
||||||
|
|
||||||
|
const created = yield* request(WorkspacePaths.list, dir, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
body: JSON.stringify({ type: "worktree", branch: null }),
|
||||||
|
})
|
||||||
|
|
||||||
|
const body = yield* Effect.promise(() => created.text())
|
||||||
|
expect({ status: created.status, body }).toMatchObject({ status: 200 })
|
||||||
|
const workspace = JSON.parse(body) as Workspace.Info
|
||||||
|
expect(workspace).toMatchObject({ type: "worktree" })
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
it.live("documents legacy Hono accepting the TUI payload shape", () =>
|
it.live("documents legacy Hono accepting the TUI payload shape", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = true
|
Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = true
|
||||||
|
|||||||
Reference in New Issue
Block a user