diff --git a/packages/opencode/src/util/rpc.ts b/packages/opencode/src/util/rpc.ts index ebd8be40e45..dc0125afbcf 100644 --- a/packages/opencode/src/util/rpc.ts +++ b/packages/opencode/src/util/rpc.ts @@ -3,12 +3,41 @@ export namespace Rpc { [method: string]: (input: any) => any } + interface SerializedError { + name: string + message: string + stack?: string + data?: Record + } + + function serializeError(err: unknown): SerializedError { + if (err instanceof Error) { + const serialized: SerializedError = { + name: err.name, + message: err.message, + stack: err.stack, + } + if ("toObject" in err && typeof err.toObject === "function") { + serialized.data = err.toObject().data + } + return serialized + } + return { + name: "Error", + message: String(err), + } + } + 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 (err) { + postMessage(JSON.stringify({ type: "rpc.error", error: serializeError(err), id: parsed.id })) + } } } } @@ -17,19 +46,42 @@ export namespace Rpc { postMessage(JSON.stringify({ type: "rpc.event", event, data })) } + /** + * Error class that reconstructs a remote error in a way that is compatible + * with NamedError.isInstance checks. The `name` and `data` properties match + * the original error, allowing FormatError to handle it correctly. + */ + export class RemoteError extends Error { + readonly data?: Record + + constructor(error: SerializedError) { + super(error.message) + this.name = error.name + this.stack = error.stack + this.data = error.data + } + } + 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: Error) => 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 handler = pending.get(parsed.id) + if (handler) { + handler.resolve(parsed.result) + pending.delete(parsed.id) + } + } + if (parsed.type === "rpc.error") { + const handler = pending.get(parsed.id) + if (handler) { + handler.reject(new RemoteError(parsed.error)) pending.delete(parsed.id) } } @@ -45,8 +97,8 @@ export namespace Rpc { 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 })) }) },