mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-06 20:33:22 +00:00
fix(tui): reject failed rpc calls
This commit is contained in:
parent
c0bc020ad6
commit
1d92186b5c
2 changed files with 105 additions and 8 deletions
|
|
@ -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
packages/opencode/test/util/rpc.test.ts
Normal file
45
packages/opencode/test/util/rpc.test.ts
Normal 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
|
||||
}
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue