import { expose, caller } from 'postmsg-rpc'; import { deserializeError, serializeError } from './serializer'; import type { PostmsgRpcOptions } from 'postmsg-rpc'; export type RPCMessageBus = { postMessage: Function; addEventListener: Function; removeEventListener: Function; }; type RPCMessage = { type: 'Message'; payload: string; }; type RPCError = { type: 'Error'; payload: Error; }; function isRPCError(data: any): data is RPCError { return data && typeof data === 'object' && data.type === 'Error'; } function getRPCOptions(messageBus: RPCMessageBus): PostmsgRpcOptions { return { addListener: messageBus.addEventListener.bind(messageBus), removeListener: messageBus.removeEventListener.bind(messageBus), postMessage(data) { return messageBus.postMessage(data); }, getMessageData(event) { return (event as { data: unknown }).data ?? event; }, }; } export const close = Symbol('@@rpc.close'); export const cancel = Symbol('@@rpc.cancel'); export type Exposed = { [k in keyof T]: T[k] & { close(): void } } & { [close]: () => void; }; export function exposeAll( obj: O, messageBus: RPCMessageBus ): Exposed { Object.entries(obj as Record).forEach(([key, val]) => { const { close } = expose( key, async (...args: unknown[]) => { try { return { type: 'Message', payload: await val(...args) }; } catch (e: any) { // If server (whatever is executing the exposed method) throws during // the execution, we want to propagate error to the client (whatever // issued the call) and re-throw there. We will do this with a special // return type. return { type: 'Error', payload: serializeError(e) }; } }, getRPCOptions(messageBus) ); val.close = close; }); Object.defineProperty(obj, close, { enumerable: false, value() { Object.values(obj as Record void }>).forEach( (fn) => { fn.close(); } ); }, }); return obj as Exposed; } export type Caller< Impl, Keys extends keyof Impl = keyof Impl > = CancelableMethods> & { [cancel]: () => void }; export function createCaller( methodNames: Extract[], messageBus: RPCMessageBus, processors: Partial< Record<(typeof methodNames)[number], (...input: any[]) => any[]> > = {} ): Caller { const obj = {}; const inflight = new Set>(); methodNames.forEach((name) => { const c = caller(name as string, getRPCOptions(messageBus)); (obj as any)[name] = async (...args: unknown[]) => { const processed = typeof processors[name] === 'function' ? processors[name]?.(...args) : args; const promise = c(...(processed as any[])); inflight.add(promise); const result = (await promise) as RPCError | RPCMessage; inflight.delete(promise); if (isRPCError(result)) throw deserializeError(result.payload); return result.payload; }; }); Object.defineProperty(obj, cancel, { enumerable: false, value() { for (const cancelable of inflight) { cancelable.cancel(); inflight.delete(cancelable); } }, }); return obj as Caller; }