refactor(lifecycle): bootstrap as pure orchestration (#25510)
This commit is contained in:
@@ -123,7 +123,9 @@ export const layer = Layer.effect(
|
|||||||
const cfgIgnores = cfg.watcher?.ignore ?? []
|
const cfgIgnores = cfg.watcher?.ignore ?? []
|
||||||
|
|
||||||
if (yield* Flag.OPENCODE_EXPERIMENTAL_FILEWATCHER) {
|
if (yield* Flag.OPENCODE_EXPERIMENTAL_FILEWATCHER) {
|
||||||
yield* subscribe(ctx.directory, [...FileIgnore.PATTERNS, ...cfgIgnores, ...protecteds(ctx.directory)])
|
yield* Effect.forkScoped(
|
||||||
|
subscribe(ctx.directory, [...FileIgnore.PATTERNS, ...cfgIgnores, ...protecteds(ctx.directory)]),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ctx.project.vcs === "git") {
|
if (ctx.project.vcs === "git") {
|
||||||
@@ -135,7 +137,7 @@ export const layer = Layer.effect(
|
|||||||
const ignore = (yield* Effect.promise(() => readdir(vcsDir).catch(() => []))).filter(
|
const ignore = (yield* Effect.promise(() => readdir(vcsDir).catch(() => []))).filter(
|
||||||
(entry) => entry !== "HEAD",
|
(entry) => entry !== "HEAD",
|
||||||
)
|
)
|
||||||
yield* subscribe(vcsDir, ignore)
|
yield* Effect.forkScoped(subscribe(vcsDir, ignore))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { Snapshot } from "../snapshot"
|
|||||||
import * as Project from "./project"
|
import * as Project from "./project"
|
||||||
import * as Vcs from "./vcs"
|
import * as Vcs from "./vcs"
|
||||||
import { Bus } from "../bus"
|
import { Bus } from "../bus"
|
||||||
import { Command } from "../command"
|
|
||||||
import { InstanceState } from "@/effect/instance-state"
|
import { InstanceState } from "@/effect/instance-state"
|
||||||
import { FileWatcher } from "@/file/watcher"
|
import { FileWatcher } from "@/file/watcher"
|
||||||
import { ShareNext } from "@/share/share-next"
|
import { ShareNext } from "@/share/share-next"
|
||||||
@@ -23,13 +22,13 @@ export const layer = Layer.effect(
|
|||||||
// Yield each bootstrap dep at layer init so `run` itself has R = never.
|
// Yield each bootstrap dep at layer init so `run` itself has R = never.
|
||||||
// InstanceStore imports only the lightweight tag from bootstrap-service.ts,
|
// InstanceStore imports only the lightweight tag from bootstrap-service.ts,
|
||||||
// so it can depend on bootstrap without importing this implementation graph.
|
// so it can depend on bootstrap without importing this implementation graph.
|
||||||
const bus = yield* Bus.Service
|
|
||||||
const config = yield* Config.Service
|
const config = yield* Config.Service
|
||||||
const file = yield* File.Service
|
const file = yield* File.Service
|
||||||
const fileWatcher = yield* FileWatcher.Service
|
const fileWatcher = yield* FileWatcher.Service
|
||||||
const format = yield* Format.Service
|
const format = yield* Format.Service
|
||||||
const lsp = yield* LSP.Service
|
const lsp = yield* LSP.Service
|
||||||
const plugin = yield* Plugin.Service
|
const plugin = yield* Plugin.Service
|
||||||
|
const project = yield* Project.Service
|
||||||
const shareNext = yield* ShareNext.Service
|
const shareNext = yield* ShareNext.Service
|
||||||
const snapshot = yield* Snapshot.Service
|
const snapshot = yield* Snapshot.Service
|
||||||
const vcs = yield* Vcs.Service
|
const vcs = yield* Vcs.Service
|
||||||
@@ -41,16 +40,13 @@ export const layer = Layer.effect(
|
|||||||
yield* config.get()
|
yield* config.get()
|
||||||
// Plugin can mutate config so it has to be initialized before anything else.
|
// Plugin can mutate config so it has to be initialized before anything else.
|
||||||
yield* plugin.init()
|
yield* plugin.init()
|
||||||
yield* Effect.all(
|
// Each service self-manages its own slow work via Effect.forkScoped against
|
||||||
[lsp, shareNext, format, file, fileWatcher, vcs, snapshot].map((s) => Effect.forkDetach(s.init())),
|
// its per-instance state scope. We just await materialization here.
|
||||||
|
yield* Effect.forEach(
|
||||||
|
[lsp, shareNext, format, file, fileWatcher, vcs, snapshot, project],
|
||||||
|
(s) => s.init().pipe(Effect.catchCause((cause) => Effect.logWarning("init failed", { cause }))),
|
||||||
|
{ concurrency: "unbounded", discard: true },
|
||||||
).pipe(Effect.withSpan("InstanceBootstrap.init"))
|
).pipe(Effect.withSpan("InstanceBootstrap.init"))
|
||||||
|
|
||||||
const projectID = ctx.project.id
|
|
||||||
yield* bus.subscribeCallback(Command.Event.Executed, async (payload) => {
|
|
||||||
if (payload.properties.name === Command.Default.INIT) {
|
|
||||||
Project.setInitialized(projectID)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}).pipe(Effect.withSpan("InstanceBootstrap"))
|
}).pipe(Effect.withSpan("InstanceBootstrap"))
|
||||||
|
|
||||||
return Service.of({ run })
|
return Service.of({ run })
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ import { BusEvent } from "@/bus/bus-event"
|
|||||||
import { GlobalBus } from "@/bus/global"
|
import { GlobalBus } from "@/bus/global"
|
||||||
import { which } from "../util/which"
|
import { which } from "../util/which"
|
||||||
import { ProjectID } from "./schema"
|
import { ProjectID } from "./schema"
|
||||||
|
import { Bus } from "@/bus"
|
||||||
|
import { Command } from "@/command"
|
||||||
|
import { InstanceState } from "@/effect/instance-state"
|
||||||
import { Effect, Layer, Path, Scope, Context, Stream, Types, Schema } from "effect"
|
import { Effect, Layer, Path, Scope, Context, Stream, Types, Schema } from "effect"
|
||||||
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
||||||
import { NodePath } from "@effect/platform-node"
|
import { NodePath } from "@effect/platform-node"
|
||||||
@@ -108,6 +111,12 @@ export type UpdatePayload = Types.DeepMutable<Schema.Schema.Type<typeof UpdatePa
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export interface Interface {
|
export interface Interface {
|
||||||
|
/**
|
||||||
|
* Per-instance setup. Subscribes to the `/init` slash command for the
|
||||||
|
* current instance and stamps the project's initialized timestamp when it
|
||||||
|
* fires. Subscription lifetime is tied to the per-instance state scope.
|
||||||
|
*/
|
||||||
|
readonly init: () => Effect.Effect<void>
|
||||||
readonly fromDirectory: (directory: string) => Effect.Effect<{ project: Info; sandbox: string }>
|
readonly fromDirectory: (directory: string) => Effect.Effect<{ project: Info; sandbox: string }>
|
||||||
readonly discover: (input: Info) => Effect.Effect<void>
|
readonly discover: (input: Info) => Effect.Effect<void>
|
||||||
readonly list: () => Effect.Effect<Info[]>
|
readonly list: () => Effect.Effect<Info[]>
|
||||||
@@ -127,13 +136,14 @@ type GitResult = { code: number; text: string; stderr: string }
|
|||||||
export const layer: Layer.Layer<
|
export const layer: Layer.Layer<
|
||||||
Service,
|
Service,
|
||||||
never,
|
never,
|
||||||
AppFileSystem.Service | Path.Path | ChildProcessSpawner.ChildProcessSpawner
|
AppFileSystem.Service | Path.Path | ChildProcessSpawner.ChildProcessSpawner | Bus.Service
|
||||||
> = Layer.effect(
|
> = Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const fs = yield* AppFileSystem.Service
|
const fs = yield* AppFileSystem.Service
|
||||||
const pathSvc = yield* Path.Path
|
const pathSvc = yield* Path.Path
|
||||||
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
|
||||||
|
const bus = yield* Bus.Service
|
||||||
|
|
||||||
const git = Effect.fnUntraced(
|
const git = Effect.fnUntraced(
|
||||||
function* (args: string[], opts?: { cwd?: string }) {
|
function* (args: string[], opts?: { cwd?: string }) {
|
||||||
@@ -417,6 +427,21 @@ export const layer: Layer.Layer<
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const initState = yield* InstanceState.make(
|
||||||
|
Effect.fn("Project.initState")(function* (ctx) {
|
||||||
|
yield* bus.subscribe(Command.Event.Executed).pipe(
|
||||||
|
Stream.runForEach((payload) =>
|
||||||
|
payload.properties.name === Command.Default.INIT ? setInitialized(ctx.project.id) : Effect.void,
|
||||||
|
),
|
||||||
|
Effect.forkScoped,
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const init = Effect.fn("Project.init")(function* () {
|
||||||
|
yield* InstanceState.get(initState)
|
||||||
|
})
|
||||||
|
|
||||||
const sandboxes = Effect.fn("Project.sandboxes")(function* (id: ProjectID) {
|
const sandboxes = Effect.fn("Project.sandboxes")(function* (id: ProjectID) {
|
||||||
const row = yield* db((d) => d.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get())
|
const row = yield* db((d) => d.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get())
|
||||||
if (!row) return []
|
if (!row) return []
|
||||||
@@ -466,6 +491,7 @@ export const layer: Layer.Layer<
|
|||||||
})
|
})
|
||||||
|
|
||||||
return Service.of({
|
return Service.of({
|
||||||
|
init,
|
||||||
fromDirectory,
|
fromDirectory,
|
||||||
discover,
|
discover,
|
||||||
list,
|
list,
|
||||||
@@ -481,6 +507,7 @@ export const layer: Layer.Layer<
|
|||||||
)
|
)
|
||||||
|
|
||||||
export const defaultLayer = layer.pipe(
|
export const defaultLayer = layer.pipe(
|
||||||
|
Layer.provide(Bus.defaultLayer),
|
||||||
Layer.provide(CrossSpawnSpawner.defaultLayer),
|
Layer.provide(CrossSpawnSpawner.defaultLayer),
|
||||||
Layer.provide(AppFileSystem.defaultLayer),
|
Layer.provide(AppFileSystem.defaultLayer),
|
||||||
Layer.provide(NodePath.layer),
|
Layer.provide(NodePath.layer),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { Bus } from "@/bus"
|
||||||
import { Project } from "@/project/project"
|
import { Project } from "@/project/project"
|
||||||
import * as Log from "@opencode-ai/core/util/log"
|
import * as Log from "@opencode-ai/core/util/log"
|
||||||
import { $ } from "bun"
|
import { $ } from "bun"
|
||||||
@@ -63,6 +64,7 @@ function mockGitFailure(failArg: string) {
|
|||||||
function projectLayerWithFailure(failArg: string) {
|
function projectLayerWithFailure(failArg: string) {
|
||||||
return Project.layer.pipe(
|
return Project.layer.pipe(
|
||||||
Layer.provide(mockGitFailure(failArg)),
|
Layer.provide(mockGitFailure(failArg)),
|
||||||
|
Layer.provide(Bus.defaultLayer),
|
||||||
Layer.provide(AppFileSystem.defaultLayer),
|
Layer.provide(AppFileSystem.defaultLayer),
|
||||||
Layer.provide(NodePath.layer),
|
Layer.provide(NodePath.layer),
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user