Compare commits

...

1 Commits

Author SHA1 Message Date
opencode-agent[bot] 1d92186b5c fix(tui): reject failed rpc calls 2026-06-04 05:59:15 +00:00
2 changed files with 105 additions and 8 deletions
+60 -8
View File
@@ -2,12 +2,57 @@ type Definition = {
[method: string]: (input: any) => any
}
type RpcError = {
name?: string
message: string
stack?: string
props?: Record<string, unknown>
}
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<T extends Definition>(target: {
postMessage: (data: string) => void | null
onmessage: ((this: Worker, ev: MessageEvent<any>) => any) | null
}) {
const pending = new Map<number, (result: any) => void>()
const pending = new Map<number, { resolve: (result: any) => void; reject: (error: unknown) => void }>()
const listeners = new Map<string, Set<(data: any) => 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<T extends Definition>(target: {
return {
call<Method extends keyof T>(method: Method, input: Parameters<T[Method]>[0]): Promise<ReturnType<T[Method]>> {
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 }))
})
},
+45
View File
@@ -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<string>) => 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
}
})
})