From b9de3ad370642a21ce4d6e780bc8bc1f1e124563 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 18 Mar 2026 20:57:08 -0400 Subject: [PATCH] fix(bus): tighten GlobalBus payload and BusEvent.define types Constrain BusEvent.define to ZodObject instead of ZodType so TS knows event properties are always a record. Type GlobalBus payload as { type: string; properties: Record } instead of any. Refactor watcher test to use Bus.subscribe instead of raw GlobalBus listener, removing hand-rolled event types and unnecessary casts. --- packages/opencode/src/bus/bus-event.ts | 4 ++-- packages/opencode/src/bus/global.ts | 2 +- .../opencode/src/control-plane/workspace.ts | 2 +- packages/opencode/test/file/watcher.test.ts | 20 +++++++------------ 4 files changed, 11 insertions(+), 17 deletions(-) diff --git a/packages/opencode/src/bus/bus-event.ts b/packages/opencode/src/bus/bus-event.ts index 7fe13833c8..1d9a31d4a2 100644 --- a/packages/opencode/src/bus/bus-event.ts +++ b/packages/opencode/src/bus/bus-event.ts @@ -1,5 +1,5 @@ import z from "zod" -import type { ZodType } from "zod" +import type { ZodObject, ZodRawShape } from "zod" import { Log } from "../util/log" export namespace BusEvent { @@ -9,7 +9,7 @@ export namespace BusEvent { const registry = new Map() - export function define(type: Type, properties: Properties) { + export function define>(type: Type, properties: Properties) { const result = { type, properties, diff --git a/packages/opencode/src/bus/global.ts b/packages/opencode/src/bus/global.ts index 43386dd6b2..dcc7664007 100644 --- a/packages/opencode/src/bus/global.ts +++ b/packages/opencode/src/bus/global.ts @@ -4,7 +4,7 @@ export const GlobalBus = new EventEmitter<{ event: [ { directory?: string - payload: any + payload: { type: string; properties: Record } }, ] }>() diff --git a/packages/opencode/src/control-plane/workspace.ts b/packages/opencode/src/control-plane/workspace.ts index c3c28ed605..d1f884f35a 100644 --- a/packages/opencode/src/control-plane/workspace.ts +++ b/packages/opencode/src/control-plane/workspace.ts @@ -123,7 +123,7 @@ export namespace Workspace { await parseSSE(res.body, stop, (event) => { GlobalBus.emit("event", { directory: space.id, - payload: event, + payload: event as { type: string; properties: Record }, }) }) // Wait 250ms and retry if SSE connection fails diff --git a/packages/opencode/test/file/watcher.test.ts b/packages/opencode/test/file/watcher.test.ts index 2cd27643e8..6d4c5f402f 100644 --- a/packages/opencode/test/file/watcher.test.ts +++ b/packages/opencode/test/file/watcher.test.ts @@ -5,9 +5,9 @@ import path from "path" import { Deferred, Effect, Option } from "effect" import { tmpdir } from "../fixture/fixture" import { watcherConfigLayer, withServices } from "../fixture/instance" +import { Bus } from "../../src/bus" import { FileWatcher } from "../../src/file/watcher" import { Instance } from "../../src/project/instance" -import { GlobalBus } from "../../src/bus/global" // Native @parcel/watcher bindings aren't reliably available in CI (missing on Linux, flaky on Windows) const describeWatcher = FileWatcher.hasNativeBinding() && !process.env.CI ? describe : describe.skip @@ -16,7 +16,6 @@ const describeWatcher = FileWatcher.hasNativeBinding() && !process.env.CI ? desc // Helpers // --------------------------------------------------------------------------- -type BusUpdate = { directory?: string; payload: { type: string; properties: WatcherEvent } } type WatcherEvent = { file: string; event: "add" | "change" | "unlink" } /** Run `body` with a live FileWatcher service. */ @@ -36,22 +35,17 @@ function withWatcher(directory: string, body: Effect.Effect) { function listen(directory: string, check: (evt: WatcherEvent) => boolean, hit: (evt: WatcherEvent) => void) { let done = false - function on(evt: BusUpdate) { + const unsub = Bus.subscribe(FileWatcher.Event.Updated, (evt) => { if (done) return - if (evt.directory !== directory) return - if (evt.payload.type !== FileWatcher.Event.Updated.type) return - if (!check(evt.payload.properties)) return - hit(evt.payload.properties) - } + if (!check(evt.properties)) return + hit(evt.properties) + }) - function cleanup() { + return () => { if (done) return done = true - GlobalBus.off("event", on) + unsub() } - - GlobalBus.on("event", on) - return cleanup } function wait(directory: string, check: (evt: WatcherEvent) => boolean) {