feat(core): add workspace domain service

This commit is contained in:
Kit Langton
2026-08-05 19:25:06 -04:00
parent 479821e621
commit 5d64dc8bfd
2 changed files with 74 additions and 0 deletions
+69
View File
@@ -1,6 +1,75 @@
export * as Workspace from "./workspace"
import { eq } from "drizzle-orm"
import { Context, Effect, Layer, Schema } from "effect"
import { Workspace } from "@opencode-ai/schema/workspace"
import { makeGlobalNode } from "@opencode-ai/util/effect/app-node"
import { Database } from "./database/database"
import { WorkspaceDriver } from "./workspace/driver"
import { WorkspaceTable } from "./workspace/sql"
export const ID = Workspace.ID
export type ID = typeof ID.Type
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("Workspace.NotFoundError", {
id: ID,
}) {}
export class Info extends Schema.Class<Info>("Workspace.Info")({
id: ID,
provider: Schema.String,
root: Schema.String,
}) {}
export interface Interface {
/** Allocate through the named driver and persist. Resolves when the Workspace is usable. */
readonly create: (input: {
readonly provider: string
}) => Effect.Effect<Info, WorkspaceDriver.Error | WorkspaceDriver.ProviderNotFoundError>
/** Metadata read; never contacts a driver. */
readonly get: (id: ID) => Effect.Effect<Info, NotFoundError>
/** Provider key and opaque binding for WorkspaceDriver.connect. Hosted Location construction only. */
readonly binding: (id: ID) => Effect.Effect<
{ readonly provider: string; readonly binding: WorkspaceDriver.Binding },
NotFoundError
>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/Workspace") {}
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const { db } = yield* Database.Service
const registry = yield* WorkspaceDriver.RegistryService
const require = Effect.fn("Workspace.require")(function* (id: ID) {
const row = yield* db.select().from(WorkspaceTable).where(eq(WorkspaceTable.id, id)).get().pipe(Effect.orDie)
if (!row) return yield* new NotFoundError({ id })
return row
})
return Service.of({
create: Effect.fn("Workspace.create")(function* (input) {
const driver = yield* registry.get(input.provider)
const id = ID.create()
const created = yield* driver.create({ workspaceID: id })
yield* db
.insert(WorkspaceTable)
.values({ id, provider: input.provider, binding: created.binding, root: created.root })
.pipe(Effect.orDie)
return new Info({ id, provider: input.provider, root: created.root })
}),
get: Effect.fn("Workspace.get")(function* (id) {
const row = yield* require(id)
return new Info({ id: row.id, provider: row.provider, root: row.root })
}),
binding: Effect.fn("Workspace.binding")(function* (id) {
const row = yield* require(id)
return { provider: row.provider, binding: row.binding }
}),
})
}),
)
export const node = makeGlobalNode({ service: Service, layer, deps: [Database.node, WorkspaceDriver.registryNode] })
+5
View File
@@ -2,6 +2,8 @@ export * as WorkspaceDriver from "./driver"
import { Context, Effect, Schema, Scope } from "effect"
import { Workspace } from "@opencode-ai/schema/workspace"
import { tags } from "@opencode-ai/util/effect/app-node"
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
import type { WorkspaceEnvironment } from "./environment"
/**
@@ -49,6 +51,9 @@ export interface Registry {
export class RegistryService extends Context.Service<RegistryService, Registry>()("@opencode/WorkspaceDriverRegistry") {}
/** Bound by Server composition (or tests); core never constructs drivers. */
export const registryNode = LayerNode.unbound(RegistryService, tags.values.global)
/** Immutable registry fixed at composition time. */
export const registry = (drivers: Readonly<Record<string, Interface>>): Registry => ({
get: (provider) => {