core: make InstanceBootstrap into an effect (#22274)

Co-authored-by: Kit Langton <kit.langton@gmail.com>
This commit is contained in:
Brendan Allan
2026-04-13 22:16:40 +08:00
committed by GitHub
parent 3eb6508a64
commit 94f71f59a3
10 changed files with 52 additions and 28 deletions

View File

@@ -1,3 +1,5 @@
import { AppRuntime } from "@/effect/app-runtime"
const dir = process.env.OPENCODE_E2E_PROJECT_DIR ?? process.cwd() const dir = process.env.OPENCODE_E2E_PROJECT_DIR ?? process.cwd()
const title = process.env.OPENCODE_E2E_SESSION_TITLE ?? "E2E Session" const title = process.env.OPENCODE_E2E_SESSION_TITLE ?? "E2E Session"
const text = process.env.OPENCODE_E2E_MESSAGE ?? "Seeded for UI e2e" const text = process.env.OPENCODE_E2E_MESSAGE ?? "Seeded for UI e2e"
@@ -20,7 +22,7 @@ const seed = async () => {
try { try {
await Instance.provide({ await Instance.provide({
directory: dir, directory: dir,
init: InstanceBootstrap, init: () => AppRuntime.runPromise(InstanceBootstrap),
fn: async () => { fn: async () => {
await Config.waitForDependencies() await Config.waitForDependencies()
await ToolRegistry.ids() await ToolRegistry.ids()

View File

@@ -1,10 +1,11 @@
import { AppRuntime } from "@/effect/app-runtime"
import { InstanceBootstrap } from "../project/bootstrap" import { InstanceBootstrap } from "../project/bootstrap"
import { Instance } from "../project/instance" import { Instance } from "../project/instance"
export async function bootstrap<T>(directory: string, cb: () => Promise<T>) { export async function bootstrap<T>(directory: string, cb: () => Promise<T>) {
return Instance.provide({ return Instance.provide({
directory, directory,
init: InstanceBootstrap, init: () => AppRuntime.runPromise(InstanceBootstrap),
fn: async () => { fn: async () => {
try { try {
const result = await cb() const result = await cb()

View File

@@ -7,10 +7,10 @@ import { Rpc } from "@/util/rpc"
import { upgrade } from "@/cli/upgrade" import { upgrade } from "@/cli/upgrade"
import { Config } from "@/config/config" import { Config } from "@/config/config"
import { GlobalBus } from "@/bus/global" import { GlobalBus } from "@/bus/global"
import type { GlobalEvent } from "@opencode-ai/sdk/v2"
import { Flag } from "@/flag/flag" import { Flag } from "@/flag/flag"
import { writeHeapSnapshot } from "node:v8" import { writeHeapSnapshot } from "node:v8"
import { Heap } from "@/cli/heap" import { Heap } from "@/cli/heap"
import { AppRuntime } from "@/effect/app-runtime"
await Log.init({ await Log.init({
print: process.argv.includes("--print-logs"), print: process.argv.includes("--print-logs"),
@@ -74,7 +74,7 @@ export const rpc = {
async checkUpgrade(input: { directory: string }) { async checkUpgrade(input: { directory: string }) {
await Instance.provide({ await Instance.provide({
directory: input.directory, directory: input.directory,
init: InstanceBootstrap, init: () => AppRuntime.runPromise(InstanceBootstrap),
fn: async () => { fn: async () => {
await upgrade().catch(() => {}) await upgrade().catch(() => {})
}, },

View File

@@ -49,7 +49,7 @@ import { ShareNext } from "@/share/share-next"
import { SessionShare } from "@/share/session" import { SessionShare } from "@/share/session"
export const AppLayer = Layer.mergeAll( export const AppLayer = Layer.mergeAll(
Observability.layer, // Observability.layer,
AppFileSystem.defaultLayer, AppFileSystem.defaultLayer,
Bus.defaultLayer, Bus.defaultLayer,
Auth.defaultLayer, Auth.defaultLayer,
@@ -95,6 +95,6 @@ export const AppLayer = Layer.mergeAll(
Installation.defaultLayer, Installation.defaultLayer,
ShareNext.defaultLayer, ShareNext.defaultLayer,
SessionShare.defaultLayer, SessionShare.defaultLayer,
) ).pipe(Layer.provide(Observability.layer))
export const AppRuntime = ManagedRuntime.make(AppLayer, { memoMap }) export const AppRuntime = ManagedRuntime.make(AppLayer, { memoMap })

View File

@@ -1,10 +1,27 @@
import { Layer, ManagedRuntime } from "effect" import { Layer, ManagedRuntime } from "effect"
import { memoMap } from "./run-service" import { memoMap } from "./run-service"
import { Plugin } from "@/plugin"
import { LSP } from "@/lsp"
import { FileWatcher } from "@/file/watcher" import { FileWatcher } from "@/file/watcher"
import { Format } from "@/format" import { Format } from "@/format"
import { ShareNext } from "@/share/share-next" import { ShareNext } from "@/share/share-next"
import { File } from "@/file"
import { Vcs } from "@/project/vcs"
import { Snapshot } from "@/snapshot"
import { Bus } from "@/bus"
import { Observability } from "./oltp"
export const BootstrapLayer = Layer.mergeAll(Format.defaultLayer, ShareNext.defaultLayer, FileWatcher.defaultLayer) export const BootstrapLayer = Layer.mergeAll(
Plugin.defaultLayer,
ShareNext.defaultLayer,
Format.defaultLayer,
LSP.defaultLayer,
File.defaultLayer,
FileWatcher.defaultLayer,
Vcs.defaultLayer,
Snapshot.defaultLayer,
Bus.defaultLayer,
).pipe(Layer.provide(Observability.layer))
export const BootstrapRuntime = ManagedRuntime.make(BootstrapLayer, { memoMap }) export const BootstrapRuntime = ManagedRuntime.make(BootstrapLayer, { memoMap })

View File

@@ -9,24 +9,26 @@ import { Bus } from "../bus"
import { Command } from "../command" import { Command } from "../command"
import { Instance } from "./instance" import { Instance } from "./instance"
import { Log } from "@/util/log" import { Log } from "@/util/log"
import { BootstrapRuntime } from "@/effect/bootstrap-runtime"
import { FileWatcher } from "@/file/watcher" import { FileWatcher } from "@/file/watcher"
import { ShareNext } from "@/share/share-next" import { ShareNext } from "@/share/share-next"
import * as Effect from "effect/Effect"
export async function InstanceBootstrap() { export const InstanceBootstrap = Effect.gen(function* () {
Log.Default.info("bootstrapping", { directory: Instance.directory }) Log.Default.info("bootstrapping", { directory: Instance.directory })
await Plugin.init() yield* Plugin.Service.use((svc) => svc.init())
void BootstrapRuntime.runPromise(ShareNext.Service.use((svc) => svc.init())) yield* ShareNext.Service.use((svc) => svc.init()).pipe(Effect.forkDetach)
void BootstrapRuntime.runPromise(Format.Service.use((svc) => svc.init())) yield* Format.Service.use((svc) => svc.init()).pipe(Effect.forkDetach)
await LSP.init() yield* LSP.Service.use((svc) => svc.init())
File.init() yield* File.Service.use((svc) => svc.init()).pipe(Effect.forkDetach)
void BootstrapRuntime.runPromise(FileWatcher.Service.use((svc) => svc.init())) yield* FileWatcher.Service.use((svc) => svc.init()).pipe(Effect.forkDetach)
Vcs.init() yield* Vcs.Service.use((svc) => svc.init()).pipe(Effect.forkDetach)
Snapshot.init() yield* Snapshot.Service.use((svc) => svc.init()).pipe(Effect.forkDetach)
Bus.subscribe(Command.Event.Executed, async (payload) => { yield* Bus.Service.use((svc) =>
if (payload.properties.name === Command.Default.INIT) { svc.subscribeCallback(Command.Event.Executed, async (payload) => {
Project.setInitialized(Instance.project.id) if (payload.properties.name === Command.Default.INIT) {
} Project.setInitialized(Instance.project.id)
}) }
} }),
)
}).pipe(Effect.withSpan("InstanceBootstrap"))

View File

@@ -10,6 +10,7 @@ import { InstanceBootstrap } from "@/project/bootstrap"
import { Session } from "@/session" import { Session } from "@/session"
import { SessionID } from "@/session/schema" import { SessionID } from "@/session/schema"
import { WorkspaceContext } from "@/control-plane/workspace-context" import { WorkspaceContext } from "@/control-plane/workspace-context"
import { AppRuntime } from "@/effect/app-runtime"
type Rule = { method?: string; path: string; exact?: boolean; action: "local" | "forward" } type Rule = { method?: string; path: string; exact?: boolean; action: "local" | "forward" }
@@ -66,7 +67,7 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware
if (!workspaceID) { if (!workspaceID) {
return Instance.provide({ return Instance.provide({
directory, directory,
init: InstanceBootstrap, init: () => AppRuntime.runPromise(InstanceBootstrap),
async fn() { async fn() {
return next() return next()
}, },
@@ -103,7 +104,7 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware
fn: () => fn: () =>
Instance.provide({ Instance.provide({
directory: target.directory, directory: target.directory,
init: InstanceBootstrap, init: () => AppRuntime.runPromise(InstanceBootstrap),
async fn() { async fn() {
return next() return next()
}, },

View File

@@ -8,6 +8,7 @@ import { ProjectID } from "../../project/schema"
import { errors } from "../error" import { errors } from "../error"
import { lazy } from "../../util/lazy" import { lazy } from "../../util/lazy"
import { InstanceBootstrap } from "../../project/bootstrap" import { InstanceBootstrap } from "../../project/bootstrap"
import { AppRuntime } from "@/effect/app-runtime"
export const ProjectRoutes = lazy(() => export const ProjectRoutes = lazy(() =>
new Hono() new Hono()
@@ -83,7 +84,7 @@ export const ProjectRoutes = lazy(() =>
directory: dir, directory: dir,
worktree: dir, worktree: dir,
project: next, project: next,
init: InstanceBootstrap, init: () => AppRuntime.runPromise(InstanceBootstrap),
}) })
return c.json(next) return c.json(next)
}, },

View File

@@ -20,6 +20,7 @@ import { AppFileSystem } from "@/filesystem"
import { makeRuntime } from "@/effect/run-service" import { makeRuntime } from "@/effect/run-service"
import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
import { InstanceState } from "@/effect/instance-state" import { InstanceState } from "@/effect/instance-state"
import { AppRuntime } from "@/effect/app-runtime"
export namespace Worktree { export namespace Worktree {
const log = Log.create({ service: "worktree" }) const log = Log.create({ service: "worktree" })
@@ -266,7 +267,7 @@ export namespace Worktree {
const booted = yield* Effect.promise(() => const booted = yield* Effect.promise(() =>
Instance.provide({ Instance.provide({
directory: info.directory, directory: info.directory,
init: InstanceBootstrap, init: () => AppRuntime.runPromise(InstanceBootstrap),
fn: () => undefined, fn: () => undefined,
}) })
.then(() => true) .then(() => true)

View File

@@ -43,7 +43,6 @@ describe("project.initGit endpoint", () => {
worktree: tmp.path, worktree: tmp.path,
}) })
expect(reloadSpy).toHaveBeenCalledTimes(1) expect(reloadSpy).toHaveBeenCalledTimes(1)
expect(reloadSpy.mock.calls[0]?.[0]?.init).toBe(InstanceBootstrap)
expect(seen.some((evt) => evt.directory === tmp.path && evt.payload.type === "server.instance.disposed")).toBe( expect(seen.some((evt) => evt.directory === tmp.path && evt.payload.type === "server.instance.disposed")).toBe(
true, true,
) )