Skip to content

Commit 1e8345c

Browse files
kitlangtonvaur94
authored andcommitted
refactor(bus): migrate BusEvent to Effect Schema (anomalyco#24040)
(cherry picked from commit cd93533)
1 parent f6ee980 commit 1e8345c

37 files changed

Lines changed: 281 additions & 260 deletions

File tree

packages/opencode/src/bus/bus-event.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
import z from "zod"
2-
import type { ZodType } from "zod"
2+
import { Schema } from "effect"
3+
import { zodObject } from "@/util/effect-zod"
34

4-
export type Definition = ReturnType<typeof define>
5+
export type Definition<Type extends string = string, Properties extends Schema.Top = Schema.Top> = {
6+
type: Type
7+
properties: Properties
8+
}
59

610
const registry = new Map<string, Definition>()
711

8-
export function define<Type extends string, Properties extends ZodType>(type: Type, properties: Properties) {
9-
const result = {
10-
type,
11-
properties,
12-
}
12+
export function define<Type extends string, Properties extends Schema.Top>(
13+
type: Type,
14+
properties: Properties,
15+
): Definition<Type, Properties> {
16+
const result = { type, properties }
1317
registry.set(type, result)
1418
return result
1519
}
@@ -21,7 +25,7 @@ export function payloads() {
2125
return z
2226
.object({
2327
type: z.literal(type),
24-
properties: def.properties,
28+
properties: zodObject(def.properties),
2529
})
2630
.meta({
2731
ref: `Event.${def.type}`,

packages/opencode/src/bus/index.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import z from "zod"
2-
import { Effect, Exit, Layer, PubSub, Scope, Context, Stream, Schema as EffectSchema, Types } from "effect"
1+
import { Effect, Exit, Layer, PubSub, Scope, Context, Stream, Schema } from "effect"
32
import { EffectBridge } from "@/effect"
43
import { Log } from "../util"
54
import { BusEvent } from "./bus-event"
@@ -9,16 +8,12 @@ import { makeRuntime } from "@/effect/run-service"
98

109
const log = Log.create({ service: "bus" })
1110

12-
type BusProperties<D extends BusEvent.Definition = BusEvent.Definition> = D extends {
13-
effectProperties: infer Properties extends EffectSchema.Top
14-
}
15-
? Types.DeepMutable<EffectSchema.Schema.Type<Properties>>
16-
: z.infer<D["properties"]>
11+
type BusProperties<D extends BusEvent.Definition<string, Schema.Top>> = Schema.Schema.Type<D["properties"]>
1712

1813
export const InstanceDisposed = BusEvent.define(
1914
"server.instance.disposed",
20-
z.object({
21-
directory: z.string(),
15+
Schema.Struct({
16+
directory: Schema.String,
2217
}),
2318
)
2419

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { BusEvent } from "@/bus/bus-event"
22
import { SessionID } from "@/session/schema"
3-
import z from "zod"
3+
import { Schema } from "effect"
44

55
export const TuiEvent = {
6-
PromptAppend: BusEvent.define("tui.prompt.append", z.object({ text: z.string() })),
6+
PromptAppend: BusEvent.define("tui.prompt.append", Schema.Struct({ text: Schema.String })),
77
CommandExecute: BusEvent.define(
88
"tui.command.execute",
9-
z.object({
10-
command: z.union([
11-
z.enum([
9+
Schema.Struct({
10+
command: Schema.Union([
11+
Schema.Literals([
1212
"session.list",
1313
"session.new",
1414
"session.share",
@@ -26,23 +26,23 @@ export const TuiEvent = {
2626
"prompt.submit",
2727
"agent.cycle",
2828
]),
29-
z.string(),
29+
Schema.String,
3030
]),
3131
}),
3232
),
3333
ToastShow: BusEvent.define(
3434
"tui.toast.show",
35-
z.object({
36-
title: z.string().optional(),
37-
message: z.string(),
38-
variant: z.enum(["info", "success", "warning", "error"]),
39-
duration: z.number().default(5000).optional().describe("Duration in milliseconds"),
35+
Schema.Struct({
36+
title: Schema.optional(Schema.String),
37+
message: Schema.String,
38+
variant: Schema.Literals(["info", "success", "warning", "error"]),
39+
duration: Schema.optional(Schema.Number).annotate({ description: "Duration in milliseconds" }),
4040
}),
4141
),
4242
SessionSelect: BusEvent.define(
4343
"tui.session.select",
44-
z.object({
45-
sessionID: SessionID.zod.describe("Session ID to navigate to"),
44+
Schema.Struct({
45+
sessionID: SessionID.annotate({ description: "Session ID to navigate to" }),
4646
}),
4747
),
4848
}

packages/opencode/src/cli/cmd/tui/ui/toast.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ import { useTheme } from "@tui/context/theme"
44
import { useTerminalDimensions } from "@opentui/solid"
55
import { SplitBorder } from "../component/border"
66
import { TextAttributes } from "@opentui/core"
7-
import z from "zod"
7+
import { Schema } from "effect"
88
import { type TuiEvent } from "../event"
99

10-
export type ToastOptions = z.infer<typeof TuiEvent.ToastShow.properties>
10+
export type ToastOptions = Schema.Schema.Type<typeof TuiEvent.ToastShow.properties>
1111

1212
export function Toast() {
1313
const toast = useToast()

packages/opencode/src/command/index.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { InstanceState } from "@/effect"
33
import { EffectBridge } from "@/effect"
44
import type { InstanceContext } from "@/project/instance"
55
import { SessionID, MessageID } from "@/session/schema"
6-
import { Effect, Layer, Context } from "effect"
6+
import { Effect, Layer, Context, Schema } from "effect"
77
import z from "zod"
88
import { Config } from "../config"
99
import { MCP } from "../mcp"
@@ -18,11 +18,11 @@ type State = {
1818
export const Event = {
1919
Executed: BusEvent.define(
2020
"command.executed",
21-
z.object({
22-
name: z.string(),
23-
sessionID: SessionID.zod,
24-
arguments: z.string(),
25-
messageID: MessageID.zod,
21+
Schema.Struct({
22+
name: Schema.String,
23+
sessionID: SessionID,
24+
arguments: Schema.String,
25+
messageID: MessageID,
2626
}),
2727
),
2828
}

packages/opencode/src/config/config.ts

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import { Context, Duration, Effect, Exit, Fiber, Layer, Option, Schema } from "e
2525
import { EffectFlock } from "@opencode-ai/shared/util/effect-flock"
2626
import { InstanceRef } from "@/effect/instance-ref"
2727
import { zod, ZodOverride } from "@/util/effect-zod"
28-
import { NonNegativeInt, PositiveInt, withStatics } from "@/util/schema"
28+
import { NonNegativeInt, PositiveInt, withStatics, type DeepMutable } from "@/util/schema"
2929
import { ConfigAgent } from "./agent"
3030
import { ConfigCommand } from "./command"
3131
import { ConfigFormatter } from "./formatter"
@@ -265,26 +265,9 @@ export const Info = Schema.Struct({
265265
})),
266266
)
267267

268-
// Schema.Struct produces readonly types by default, but the service code
269-
// below mutates Info objects directly (e.g. `config.mode = ...`). Strip the
270-
// readonly recursively so callers get the same mutable shape zod inferred.
271-
//
272-
// `Types.DeepMutable` from effect-smol would be a drop-in, but its fallback
273-
// branch `{ -readonly [K in keyof T]: ... }` collapses `unknown` to `{}`
274-
// (since `keyof unknown = never`), which widens `Record<string, unknown>`
275-
// fields like `ConfigPlugin.Options`. The local version gates on
276-
// `extends object` so `unknown` passes through.
277-
//
278-
// Tuple branch preserves `ConfigPlugin.Spec`'s `readonly [string, Options]`
279-
// shape (otherwise the general array branch widens it to an array).
280-
type DeepMutable<T> = T extends readonly [unknown, ...unknown[]]
281-
? { -readonly [K in keyof T]: DeepMutable<T[K]> }
282-
: T extends readonly (infer U)[]
283-
? DeepMutable<U>[]
284-
: T extends object
285-
? { -readonly [K in keyof T]: DeepMutable<T[K]> }
286-
: T
287-
268+
// Uses the shared `DeepMutable` from `@/util/schema`. See the definition
269+
// there for why the local variant is needed over `Types.DeepMutable` from
270+
// effect-smol (the upstream version collapses `unknown` to `{}`).
288271
export type Info = DeepMutable<Schema.Schema.Type<typeof Info>> & {
289272
// plugin_origins is derived state, not a persisted config field. It keeps each winning plugin spec together
290273
// with the file and scope it came from so later runtime code can make location-sensitive decisions.

packages/opencode/src/control-plane/workspace.ts

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import z from "zod"
2+
import { Schema } from "effect"
23
import { setTimeout as sleep } from "node:timers/promises"
34
import { fn } from "@/util/fn"
45
import { Database, asc, eq, inArray } from "@/storage"
@@ -25,36 +26,37 @@ import { errorData } from "@/util/error"
2526
import { AppRuntime } from "@/effect/app-runtime"
2627
import { waitEvent } from "./util"
2728
import { WorkspaceContext } from "./workspace-context"
29+
import { NonNegativeInt } from "@/util/schema"
2830

2931
export const Info = WorkspaceInfo.meta({
3032
ref: "Workspace",
3133
})
3234
export type Info = z.infer<typeof Info>
3335

34-
export const ConnectionStatus = z.object({
35-
workspaceID: WorkspaceID.zod,
36-
status: z.enum(["connected", "connecting", "disconnected", "error"]),
36+
export const ConnectionStatus = Schema.Struct({
37+
workspaceID: WorkspaceID,
38+
status: Schema.Literals(["connected", "connecting", "disconnected", "error"]),
3739
})
38-
export type ConnectionStatus = z.infer<typeof ConnectionStatus>
40+
export type ConnectionStatus = Schema.Schema.Type<typeof ConnectionStatus>
3941

40-
const Restore = z.object({
41-
workspaceID: WorkspaceID.zod,
42-
sessionID: SessionID.zod,
43-
total: z.number().int().min(0),
44-
step: z.number().int().min(0),
42+
const Restore = Schema.Struct({
43+
workspaceID: WorkspaceID,
44+
sessionID: SessionID,
45+
total: NonNegativeInt,
46+
step: NonNegativeInt,
4547
})
4648

4749
export const Event = {
4850
Ready: BusEvent.define(
4951
"workspace.ready",
50-
z.object({
51-
name: z.string(),
52+
Schema.Struct({
53+
name: Schema.String,
5254
}),
5355
),
5456
Failed: BusEvent.define(
5557
"workspace.failed",
56-
z.object({
57-
message: z.string(),
58+
Schema.Struct({
59+
message: Schema.String,
5860
}),
5961
),
6062
Restore: BusEvent.define("workspace.restore", Restore),

packages/opencode/src/file/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { InstanceState } from "@/effect"
33

44
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
55
import { Git } from "@/git"
6-
import { Effect, Layer, Context, Scope } from "effect"
6+
import { Effect, Layer, Context, Schema, Scope } from "effect"
77
import * as Stream from "effect/Stream"
88
import { formatPatch, structuredPatch } from "diff"
99
import fuzzysort from "fuzzysort"
@@ -76,8 +76,8 @@ export type Content = z.infer<typeof Content>
7676
export const Event = {
7777
Edited: BusEvent.define(
7878
"file.edited",
79-
z.object({
80-
file: z.string(),
79+
Schema.Struct({
80+
file: Schema.String,
8181
}),
8282
),
8383
}

packages/opencode/src/file/watcher.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Cause, Effect, Layer, Context } from "effect"
1+
import { Cause, Effect, Layer, Context, Schema } from "effect"
22
// @ts-ignore
33
import { createWrapper } from "@parcel/watcher/wrapper"
44
import type ParcelWatcher from "@parcel/watcher"
@@ -25,9 +25,9 @@ const SUBSCRIBE_TIMEOUT_MS = 10_000
2525
export const Event = {
2626
Updated: BusEvent.define(
2727
"file.watcher.updated",
28-
z.object({
29-
file: z.string(),
30-
event: z.union([z.literal("add"), z.literal("change"), z.literal("unlink")]),
28+
Schema.Struct({
29+
file: Schema.String,
30+
event: Schema.Literals(["add", "change", "unlink"]),
3131
}),
3232
),
3333
}

packages/opencode/src/ide/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { BusEvent } from "@/bus/bus-event"
22
import z from "zod"
3+
import { Schema } from "effect"
34
import { NamedError } from "@opencode-ai/shared/util/error"
45
import { Log } from "../util"
56
import { Process } from "@/util"
@@ -17,8 +18,8 @@ const log = Log.create({ service: "ide" })
1718
export const Event = {
1819
Installed: BusEvent.define(
1920
"ide.installed",
20-
z.object({
21-
ide: z.string(),
21+
Schema.Struct({
22+
ide: Schema.String,
2223
}),
2324
),
2425
}

0 commit comments

Comments
 (0)