Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 60 additions & 8 deletions packages/opencode/src/util/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,41 @@ export namespace Rpc {
[method: string]: (input: any) => any
}

interface SerializedError {
name: string
message: string
stack?: string
data?: Record<string, unknown>
}

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 }))
}
}
}
}
Expand All @@ -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<string, unknown>

constructor(error: SerializedError) {
super(error.message)
this.name = error.name
this.stack = error.stack
this.data = error.data
}
}

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: Error) => 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 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)
}
}
Expand All @@ -45,8 +97,8 @@ export namespace Rpc {
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 }))
})
},
Expand Down