refactor(server): canonicalize service API (#31049)

This commit is contained in:
Dax 2026-06-06 23:27:28 -04:00 committed by GitHub
commit fe0c4f8c74
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
388 changed files with 7075 additions and 4064 deletions

View file

@ -63,7 +63,7 @@ export type Editor = {
export interface Interface {
readonly transform: State.Interface<Data, Editor>["transform"]
readonly update: (update: State.Transform<Editor>) => Effect.Effect<void, never, Scope.Scope>
readonly update: State.Interface<Data, Editor>["update"]
readonly get: (id: ID) => Effect.Effect<Info | undefined>
readonly default: () => Effect.Effect<Info | undefined>
readonly resolve: (id?: ID | string) => Effect.Effect<Info | undefined>
@ -113,10 +113,7 @@ export const layer = Layer.effect(
return Service.of({
transform: state.transform,
update: Effect.fn("AgentV2.update")(function* (update) {
const transform = yield* state.transform()
yield* transform(update)
}),
update: state.update,
get: Effect.fn("AgentV2.get")(function* (id) {
return state.get().agents.get(id)
}),

View file

@ -203,7 +203,7 @@ export const layer = Layer.effect(
event.location?.directory === location.directory && event.location.workspaceID === location.workspaceID,
),
Stream.runForEach((event) =>
state.update((catalog) => plugin.triggerFor(event.data.id, "catalog.transform", catalog, {}), "plugin.added"),
state.mutate((catalog) => plugin.triggerFor(event.data.id, "catalog.transform", catalog, {}), "plugin.added"),
),
Effect.forkIn(scope, { startImmediately: true }),
)

View file

@ -27,6 +27,7 @@ export type Editor = {
export interface Interface {
readonly transform: State.Interface<Data, Editor>["transform"]
readonly update: State.Interface<Data, Editor>["update"]
readonly get: (name: string) => Effect.Effect<Info | undefined>
readonly list: () => Effect.Effect<Info[]>
}
@ -54,6 +55,7 @@ export const layer = Layer.effect(
})
return Service.of({
update: state.update,
transform: state.transform,
get: Effect.fn("CommandV2.get")(function* (name) {
return state.get().commands.get(name)

View file

@ -6,6 +6,7 @@ import { Git } from "../git"
import { Location } from "../location"
import { ProjectV2 } from "../project"
import { SessionV2 } from "../session"
import { SessionExecution } from "../session/execution"
import { SessionEvent } from "../session/event"
import { SessionSchema } from "../session/schema"
import { AbsolutePath, RelativePath } from "../schema"
@ -124,5 +125,6 @@ export const defaultLayer = layer.pipe(
Layer.provide(Git.defaultLayer),
Layer.provide(EventV2.defaultLayer),
Layer.provide(ProjectV2.defaultLayer),
Layer.provide(SessionExecution.noopLayer),
Layer.provide(SessionV2.defaultLayer),
)

View file

@ -70,7 +70,7 @@ export const layer = Layer.effectDiscard(
return files.filter((file): file is File => file !== undefined)
})
yield* registry.contribute({
yield* registry.register({
key,
load: observe().pipe(
Effect.map((files) =>

View file

@ -425,20 +425,12 @@ export const layer = Layer.effect(
}),
)
const DefaultDatabase = Database.defaultLayer
const DefaultEvents = EventV2.layer.pipe(Layer.provide(DefaultDatabase))
const DefaultProjector = SessionProjector.layer.pipe(Layer.provide(DefaultEvents), Layer.provide(DefaultDatabase))
const DefaultStore = SessionStore.layer.pipe(Layer.provide(DefaultDatabase))
export const defaultLayer = layer.pipe(
Layer.provide(
Layer.mergeAll(
DefaultDatabase,
DefaultEvents,
DefaultProjector,
DefaultStore,
SessionExecution.noopLayer,
ProjectV2.defaultLayer,
),
),
Layer.provide(SessionExecution.noopLayer),
Layer.provide(SessionStore.defaultLayer),
Layer.provide(SessionProjector.defaultLayer),
Layer.provide(EventV2.defaultLayer),
Layer.provide(Database.defaultLayer),
Layer.provide(ProjectV2.defaultLayer),
Layer.orDie,
)

View file

@ -31,3 +31,5 @@ export const layer = Layer.effect(
})
}),
)
export const defaultLayer = layer.pipe(Layer.provide(SessionStore.defaultLayer))

View file

@ -58,3 +58,5 @@ export const layer = Layer.effect(
})
}),
)
export const defaultLayer = layer.pipe(Layer.provide(Database.defaultLayer))

View file

@ -4,7 +4,7 @@ import { Effect, Scope, Semaphore } from "effect"
import type { Draft, Objectish } from "immer"
/**
* A replayable contribution applied to an editor during rebuild.
* A replayable transform applied to an editor during rebuild.
*
* Transforms are intentionally synchronous and mutation-shaped: domain editors
* hide the draft representation while preserving concise plugin/config code.
@ -39,15 +39,17 @@ export interface Interface<State extends Objectish, Editor> {
* registration order. Closing the owning Scope removes the slot and rebuilds.
*/
readonly transform: () => Effect.Effect<(transform: Transform<Editor>) => Effect.Effect<void>, never, Scope.Scope>
/** Registers and applies a replayable transform in the current Scope. */
readonly update: (update: Transform<Editor>) => Effect.Effect<void, never, Scope.Scope>
/**
* Mutates the current materialized state directly.
* Mutates the current materialized state directly, once.
*
* This is not replayable contribution state: a later rebuild starts again
* This is not replayable transform state: a later rebuild starts again
* from `initial()` plus active transforms, so direct edits must be reserved
* for current-state adjustments that are intentionally outside the transform
* fold.
*/
readonly update: (update: (editor: Editor) => Effect.Effect<void>, reason?: string) => Effect.Effect<void>
readonly mutate: (update: (editor: Editor) => Effect.Effect<void>, reason?: string) => Effect.Effect<void>
}
export function create<State extends Objectish, Editor>(options: Options<State, Editor>): Interface<State, Editor> {
@ -69,7 +71,7 @@ export function create<State extends Objectish, Editor>(options: Options<State,
yield* commit(next)
})
return {
const result: Interface<State, Editor> = {
get: () => state,
transform: Effect.fn("State.transform")(function* () {
const scope = yield* Scope.Scope
@ -96,10 +98,15 @@ export function create<State extends Objectish, Editor>(options: Options<State,
}),
)
}),
update: Effect.fn("State.update")(function* (update, reason) {
update: Effect.fn("State.update")(function* (update) {
const transform = yield* result.transform()
yield* transform(update)
}),
mutate: Effect.fn("State.mutate")(function* (update, reason) {
const api = options.editor(state as Draft<State>)
yield* update(api)
if (options.finalize) yield* options.finalize(api, reason)
}, semaphore.withPermit),
}
return result
}

View file

@ -36,7 +36,7 @@ const builtIns = Layer.effectDiscard(
}),
])
yield* registry.contribute({ key: SystemContext.Key.make("core/builtins"), load: Effect.succeed(context) })
yield* registry.register({ key: SystemContext.Key.make("core/builtins"), load: Effect.succeed(context) })
}),
)

View file

@ -3,13 +3,13 @@ export * as SystemContextRegistry from "./registry"
import { Context, Effect, Layer, Ref, Scope } from "effect"
import { SystemContext } from "./index"
export interface Contribution {
export interface Entry {
readonly key: SystemContext.Key
readonly load: Effect.Effect<SystemContext.SystemContext>
}
export interface Interface {
readonly contribute: (contribution: Contribution) => Effect.Effect<void, never, Scope.Scope>
readonly register: (entry: Entry) => Effect.Effect<void, never, Scope.Scope>
readonly load: () => Effect.Effect<SystemContext.SystemContext>
}
@ -18,27 +18,27 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/v2
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
const contributions = yield* Ref.make<ReadonlyArray<Contribution>>([])
const entries = yield* Ref.make<ReadonlyArray<Entry>>([])
return Service.of({
contribute: Effect.fn("SystemContextRegistry.contribute")(function* (contribution) {
register: Effect.fn("SystemContextRegistry.register")(function* (entry) {
yield* Effect.acquireRelease(
Ref.modify(contributions, (current) => {
if (current.some((item) => item.key === contribution.key)) return [false, current]
return [true, [...current, contribution]]
Ref.modify(entries, (current) => {
if (current.some((item) => item.key === entry.key)) return [false, current]
return [true, [...current, entry]]
}).pipe(
Effect.flatMap((added) =>
added ? Effect.void : Effect.die(`Duplicate system context contribution key: ${contribution.key}`),
added ? Effect.void : Effect.die(`Duplicate system context entry key: ${entry.key}`),
),
Effect.as(contribution),
Effect.as(entry),
),
(entry) => Ref.update(contributions, (current) => current.filter((item) => item !== entry)),
(entry) => Ref.update(entries, (current) => current.filter((item) => item !== entry)),
)
}),
load: Effect.fn("SystemContextRegistry.load")(function* () {
const current = (yield* Ref.get(contributions)).toSorted((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0))
const current = (yield* Ref.get(entries)).toSorted((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0))
return SystemContext.combine(
yield* Effect.forEach(current, (contribution) => contribution.load, { concurrency: "unbounded" }),
yield* Effect.forEach(current, (entry) => entry.load, { concurrency: "unbounded" }),
)
}),
})

View file

@ -15,7 +15,7 @@ import { WebSearchTool } from "./websearch"
import { WriteTool } from "./write"
/**
* Composes only the shipped Location-scoped built-in tool contributions.
* Composes only the shipped Location-scoped built-in tool transforms.
* Each tool retains its implementation and focused tests independently. Dynamic
* MCP and plugin tools later use separate scoped canonical registrations, while
* provider/model filtering belongs to a future materialization phase rather
@ -25,7 +25,7 @@ import { WriteTool } from "./write"
* TODO: Port the remaining launch-follow-up leaves deliberately: edit fuzzy
* parity, task, LSP,
* repo_clone, repo_overview, plan_exit, and Rune/code mode. Keep MCP and plugin
* contributions separate from this static built-in list.
* transforms separate from this static built-in list.
*/
export const locationLayer = Layer.mergeAll(
ApplyPatchTool.layer,