From 1d92186b5c5cedbf193b8e9f43c12f79dc9acb05 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Thu, 4 Jun 2026 05:59:15 +0000 Subject: [PATCH] fix(tui): reject failed rpc calls --- packages/opencode/src/util/rpc.ts | 68 ++++++++++++++++++++++--- packages/opencode/test/util/rpc.test.ts | 45 ++++++++++++++++ 2 files changed, 105 insertions(+), 8 deletions(-) create mode 100644 packages/opencode/test/util/rpc.test.ts diff --git a/packages/opencode/src/util/rpc.ts b/packages/opencode/src/util/rpc.ts index 02586ebcfc6..589e8d49790 100644 --- a/packages/opencode/src/util/rpc.ts +++ b/packages/opencode/src/util/rpc.ts @@ -2,12 +2,57 @@ type Definition = { [method: string]: (input: any) => any } +type RpcError = { + name?: string + message: string + stack?: string + props?: Record +} + +function serializeValue(value: unknown): unknown { + if (value === undefined || value === null) return value + if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value + try { + return JSON.parse(JSON.stringify(value)) + } catch { + return String(value) + } +} + +function serializeError(error: unknown): RpcError { + if (error instanceof Error) { + return { + name: error.name, + message: error.message, + stack: error.stack, + props: Object.fromEntries( + Object.getOwnPropertyNames(error).map((key) => [key, serializeValue(error[key as keyof Error])]), + ), + } + } + return { + message: String(error), + } +} + +function deserializeError(error: RpcError) { + const result = new Error(error.message) + if (error.name) result.name = error.name + if (error.stack) result.stack = error.stack + if (error.props) Object.assign(result, error.props) + return result +} + export function listen(rpc: Definition) { onmessage = async (evt) => { const parsed = JSON.parse(evt.data) if (parsed.type === "rpc.request") { - const result = await rpc[parsed.method](parsed.input) - postMessage(JSON.stringify({ type: "rpc.result", result, id: parsed.id })) + try { + const result = await rpc[parsed.method](parsed.input) + postMessage(JSON.stringify({ type: "rpc.result", result, id: parsed.id })) + } catch (error) { + postMessage(JSON.stringify({ type: "rpc.error", error: serializeError(error), id: parsed.id })) + } } } } @@ -20,15 +65,22 @@ export function client(target: { postMessage: (data: string) => void | null onmessage: ((this: Worker, ev: MessageEvent) => any) | null }) { - const pending = new Map void>() + const pending = new Map void; reject: (error: unknown) => void }>() const listeners = new Map void>>() let id = 0 target.onmessage = async (evt) => { const parsed = JSON.parse(evt.data) if (parsed.type === "rpc.result") { - const resolve = pending.get(parsed.id) - if (resolve) { - resolve(parsed.result) + const callbacks = pending.get(parsed.id) + if (callbacks) { + callbacks.resolve(parsed.result) + pending.delete(parsed.id) + } + } + if (parsed.type === "rpc.error") { + const callbacks = pending.get(parsed.id) + if (callbacks) { + callbacks.reject(deserializeError(parsed.error)) pending.delete(parsed.id) } } @@ -44,8 +96,8 @@ export function client(target: { return { call(method: Method, input: Parameters[0]): Promise> { const requestId = id++ - return new Promise((resolve) => { - pending.set(requestId, resolve) + return new Promise((resolve, reject) => { + pending.set(requestId, { resolve, reject }) target.postMessage(JSON.stringify({ type: "rpc.request", method, input, id: requestId })) }) }, diff --git a/packages/opencode/test/util/rpc.test.ts b/packages/opencode/test/util/rpc.test.ts new file mode 100644 index 00000000000..53581e49490 --- /dev/null +++ b/packages/opencode/test/util/rpc.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, test } from "bun:test" +import { Rpc } from "../../src/util/rpc" + +type Endpoint = { + postMessage(data: string): void + onmessage: ((this: Worker, ev: MessageEvent) => unknown) | null +} + +function message(data: string) { + return new MessageEvent("message", { data }) +} + +describe("util.rpc", () => { + test("rejects calls when the handler throws", async () => { + const main: Endpoint = { + onmessage: null, + postMessage(data) { + queueMicrotask(() => worker.onmessage?.call({} as Worker, message(data))) + }, + } + const worker: Endpoint = { + onmessage: null, + postMessage(data) { + queueMicrotask(() => main.onmessage?.call({} as Worker, message(data))) + }, + } + + const previousOnMessage = globalThis.onmessage + const previousPostMessage = globalThis.postMessage + try { + globalThis.postMessage = worker.postMessage + Rpc.listen({ + boom() { + throw new Error("boom") + }, + }) + worker.onmessage = globalThis.onmessage as Endpoint["onmessage"] + + await expect(Rpc.client(main).call("boom", undefined)).rejects.toThrow("boom") + } finally { + globalThis.onmessage = previousOnMessage + globalThis.postMessage = previousPostMessage + } + }) +})