From d0e58c5b26fe62c0753d9ee6c26b0917bf60b1e6 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Sun, 28 Dec 2025 21:44:59 +0100 Subject: [PATCH 1/4] feat(plugin-bridge): add RPC bridge --- .../version-plan-1766951301102.md | 5 + packages/plugin-bridge/README.md | 95 +++++++++ packages/plugin-bridge/src/index.ts | 2 + packages/plugin-bridge/src/rpc.test.ts | 162 +++++++++++++++ packages/plugin-bridge/src/rpc.ts | 191 ++++++++++++++++++ packages/plugin-bridge/vite.config.ts | 4 + .../plugin-development/plugin-development.md | 106 +++++++++- 7 files changed, 561 insertions(+), 4 deletions(-) create mode 100644 .nx/version-plans/version-plan-1766951301102.md create mode 100644 packages/plugin-bridge/src/rpc.test.ts create mode 100644 packages/plugin-bridge/src/rpc.ts diff --git a/.nx/version-plans/version-plan-1766951301102.md b/.nx/version-plans/version-plan-1766951301102.md new file mode 100644 index 00000000..2cb5f76e --- /dev/null +++ b/.nx/version-plans/version-plan-1766951301102.md @@ -0,0 +1,5 @@ +--- +"@rozenite/plugin-bridge": minor +--- + +Introduced `createRozeniteRPCBridge` for symmetrical bi-directional RPC communication between App and DevTools. This enables type-safe, contract-first method calls across the bridge. \ No newline at end of file diff --git a/packages/plugin-bridge/README.md b/packages/plugin-bridge/README.md index 78b9a30e..907cfc97 100644 --- a/packages/plugin-bridge/README.md +++ b/packages/plugin-bridge/README.md @@ -101,3 +101,98 @@ Like the project? ⚛️ [Join the team](https://callstack.com/careers/?utm_camp [prs-welcome]: https://github.com/callstackincubator/rozenite/blob/main/CONTRIBUTING.md [chat-badge]: https://img.shields.io/discord/426714625279524876.svg?style=for-the-badge [chat]: https://discord.gg/xgGt7KAjxv + +## RPC Bridge + +The `createRozeniteRPCBridge` function allows you to set up a symmetrical bi-directional RPC bridge between the App and the DevTools. + +### Usage + +1. **Define Protocols** + +```typescript +// Shared types +export type AppProtocol = { + getAppVersion(): Promise; + logMessage(msg: string): Promise; +}; + +export type DevToolsProtocol = { + refresh(): Promise; + showNotification(text: string): Promise; +}; +``` + +2. **Initialize in App** + +```typescript +import { createRozeniteRPCBridge, getRozeniteDevToolsClient } from '@rozenite/plugin-bridge'; +import { AppProtocol, DevToolsProtocol } from './protocols'; + +async function setupAppBridge() { + const client = await getRozeniteDevToolsClient('my-plugin'); + + // Implement local handlers + const localHandlers: AppProtocol = { + async getAppVersion() { + return '1.0.0'; + }, + async logMessage(msg) { + console.log('App Log:', msg); + } + }; + + // Create Bridge + const devTools = createRozeniteRPCBridge( + { + send: (msg) => client.send('rpc', msg), + onMessage: (listener) => client.onMessage('rpc', listener), + }, + localHandlers + ); + + // Call remote method + await devTools.showNotification('App connected!'); +} +``` + +3. **Initialize in DevTools** + +```typescript +import { useEffect } from 'react'; +import { createRozeniteRPCBridge, useRozeniteDevToolsClient } from '@rozenite/plugin-bridge'; +import { AppProtocol, DevToolsProtocol } from './protocols'; + +function MyPlugin() { + const client = useRozeniteDevToolsClient({ pluginId: 'my-plugin' }); + + useEffect(() => { + if (!client) return; + + const localHandlers: DevToolsProtocol = { + async refresh() { + console.log('Refreshing...'); + }, + async showNotification(text) { + alert(text); + return true; + } + }; + + const app = createRozeniteRPCBridge( + { + send: (msg) => client.send('rpc', msg), + onMessage: (listener) => client.onMessage('rpc', listener), + }, + localHandlers + ); + + // Call remote method + app.getAppVersion().then(version => console.log('App Version:', version)); + + }, [client]); + + return
My Plugin
; +} +``` + diff --git a/packages/plugin-bridge/src/index.ts b/packages/plugin-bridge/src/index.ts index b4a094d6..51e322ec 100644 --- a/packages/plugin-bridge/src/index.ts +++ b/packages/plugin-bridge/src/index.ts @@ -4,3 +4,5 @@ export type { Subscription } from './types'; export type { UseRozeniteDevToolsClientOptions } from './useRozeniteDevToolsClient'; export { getRozeniteDevToolsClient } from './client'; export { UnsupportedPlatformError } from './errors'; +export { createRozeniteRPCBridge } from './rpc'; +export type { Transport as RozeniteRPCTransport } from './rpc'; diff --git a/packages/plugin-bridge/src/rpc.test.ts b/packages/plugin-bridge/src/rpc.test.ts new file mode 100644 index 00000000..5912bbd5 --- /dev/null +++ b/packages/plugin-bridge/src/rpc.test.ts @@ -0,0 +1,162 @@ +import { describe, it, expect, vi } from 'vitest'; +import { createRozeniteRPCBridge } from './rpc'; +import type { RozeniteRPCTransport } from './index'; + +describe('createRozeniteRPCBridge', () => { + const createMockTransport = () => { + let listener: (message: unknown) => void = () => {}; + return { + send: vi.fn(), + onMessage: vi.fn((l) => { + listener = l; + }), + // Helper to simulate receiving a message + emit: (message: unknown) => listener(message), + }; + }; + + type Local = { + add(a: number, b: number): Promise; + getError(): Promise; + }; + + type Remote = { + multiply(a: number, b: number): Promise; + }; + + it('should call remote methods via proxy and receive results', async () => { + const transport = createMockTransport(); + const localHandlers: Local = { + add: async (a, b) => a + b, + getError: async () => { + throw new Error('Local error'); + }, + }; + + const bridge = createRozeniteRPCBridge( + transport as unknown as RozeniteRPCTransport, + localHandlers + ); + + // Call remote method + const promise = bridge.multiply(2, 3); + + // Verify request was sent + expect(transport.send).toHaveBeenCalledWith( + expect.objectContaining({ + jsonrpc: '2.0', + method: 'multiply', + params: [2, 3], + id: expect.any(String), + }) + ); + + // Simulate response + const lastCall = transport.send.mock.calls[0][0] as any; + transport.emit({ + jsonrpc: '2.0', + id: lastCall.id, + result: 6, + }); + + const result = await promise; + expect(result).toBe(6); + }); + + it('should handle incoming requests and send responses', async () => { + const transport = createMockTransport(); + const localHandlers: Local = { + add: async (a, b) => a + b, + getError: async () => { + throw new Error('Local error'); + }, + }; + + createRozeniteRPCBridge( + transport as unknown as RozeniteRPCTransport, + localHandlers + ); + + // Simulate incoming request + transport.emit({ + jsonrpc: '2.0', + id: '123', + method: 'add', + params: [10, 20], + }); + + // Wait for promise resolution in handler + await vi.waitFor(() => { + expect(transport.send).toHaveBeenCalledWith({ + jsonrpc: '2.0', + id: '123', + result: 30, + }); + }); + }); + + it('should handle errors across the bridge', async () => { + const transport = createMockTransport(); + const localHandlers: Local = { + add: async (a, b) => a + b, + getError: async () => { + throw new Error('Remote crash'); + }, + }; + + const bridge = createRozeniteRPCBridge( + transport as unknown as RozeniteRPCTransport, + localHandlers + ); + + // Test remote error (received from other side) + const promise = bridge.multiply(5, 5); + const lastCall = transport.send.mock.calls[0][0] as any; + + transport.emit({ + jsonrpc: '2.0', + id: lastCall.id, + error: { message: 'Method failed' }, + }); + + await expect(promise).rejects.toThrow('Method failed'); + + // Test local error (sent to other side) + transport.emit({ + jsonrpc: '2.0', + id: '456', + method: 'getError', + params: [], + }); + + await vi.waitFor(() => { + expect(transport.send).toHaveBeenCalledWith( + expect.objectContaining({ + jsonrpc: '2.0', + id: '456', + error: expect.objectContaining({ + message: 'Remote crash', + }), + }) + ); + }); + }); + + it('should ignore non-RPC messages', async () => { + const transport = createMockTransport(); + const localHandlers = { + ping: vi.fn(), + }; + + createRozeniteRPCBridge( + transport as unknown as RozeniteRPCTransport, + localHandlers + ); + + // Send invalid message + transport.emit({ type: 'not-rpc', data: {} }); + + expect(localHandlers.ping).not.toHaveBeenCalled(); + expect(transport.send).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/plugin-bridge/src/rpc.ts b/packages/plugin-bridge/src/rpc.ts new file mode 100644 index 00000000..e9ce2565 --- /dev/null +++ b/packages/plugin-bridge/src/rpc.ts @@ -0,0 +1,191 @@ +import { Subscription } from './types'; + +export type Transport = { + send(message: unknown): void; + onMessage(listener: (message: unknown) => void): Subscription | void; +}; + +export type RPCRequest = { + jsonrpc: '2.0'; + id: string; + method: string; + params: unknown[]; +}; + +export type RPCResponseSuccess = { + jsonrpc: '2.0'; + id: string; + result: unknown; +}; + +export type RPCResponseError = { + jsonrpc: '2.0'; + id: string; + error: { + message: string; + code?: number; + data?: unknown; + }; +}; + +export type RPCResponse = RPCResponseSuccess | RPCResponseError; + +type PendingPromise = { + resolve: (value: unknown) => void; + reject: (reason?: unknown) => void; +}; + +// Error serialization helper +function serializeError(error: unknown) { + if (error instanceof Error) { + return { + message: error.message, + data: { + stack: error.stack, + name: error.name, + // Copy other properties + ...Object.getOwnPropertyNames(error).reduce((acc, key) => { + // @ts-ignore + acc[key] = error[key]; + return acc; + }, {} as Record) + }, + }; + } + return { + message: String(error), + }; +} + +// Simple ID generator +function generateId(): string { + return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); +} + +function isRPCRequest(message: any): message is RPCRequest { + return ( + typeof message === 'object' && + message !== null && + message.jsonrpc === '2.0' && + typeof message.method === 'string' && + typeof message.id === 'string' && + Array.isArray(message.params) + ); +} + +function isRPCResponse(message: any): message is RPCResponse { + return ( + typeof message === 'object' && + message !== null && + message.jsonrpc === '2.0' && + typeof message.id === 'string' && + ('result' in message || 'error' in message) + ); +} + +/** + * Creates a Symmetrical Bi-Directional RPC Bridge. + * + * @param transport - The transport layer to send/receive messages. + * @param localHandlers - The implementation of the local methods exposed to the other side. + * @returns A Proxy object representing the remote interface. + */ +export function createRozeniteRPCBridge( + transport: Transport, + localHandlers: LocalHandlers +): RemoteInterface { + const pendingPromises = new Map(); + + // Message Handler + transport.onMessage((message: unknown) => { + if (isRPCRequest(message)) { + // It's a request: Execute local handler + const { id, method, params } = message; + // @ts-ignore + const handler = localHandlers[method]; + + if (typeof handler === 'function') { + try { + const result = handler(...params); + // Handle sync and async results + Promise.resolve(result) + .then((res) => { + const response: RPCResponseSuccess = { + jsonrpc: '2.0', + id, + result: res, + }; + transport.send(response); + }) + .catch((err) => { + const response: RPCResponseError = { + jsonrpc: '2.0', + id, + error: serializeError(err), + }; + transport.send(response); + }); + } catch (err) { + const response: RPCResponseError = { + jsonrpc: '2.0', + id, + error: serializeError(err), + }; + transport.send(response); + } + } else { + // Method not found + const response: RPCResponseError = { + jsonrpc: '2.0', + id, + error: { message: `Method ${method} not found` }, + }; + transport.send(response); + } + } else if (isRPCResponse(message)) { + // It's a response: Resolve/Reject pending promise + const { id } = message; + const pending = pendingPromises.get(id); + if (pending) { + pendingPromises.delete(id); + if ('error' in message && message.error) { + // Reconstruct error + const errData = (message as RPCResponseError).error; + const error = new Error(errData.message); + if (errData.data && typeof errData.data === 'object') { + Object.assign(error, errData.data); + } + pending.reject(error); + } else { + pending.resolve((message as RPCResponseSuccess).result); + } + } + } + // Ignore other messages (opt-in approach) + }); + + // Proxy Mechanism + return new Proxy({} as RemoteInterface, { + get: (target, prop) => { + if (typeof prop === 'string') { + // Return a function that sends the RPC request + return (...args: unknown[]) => { + return new Promise((resolve, reject) => { + const id = generateId(); + pendingPromises.set(id, { resolve, reject }); + + const request: RPCRequest = { + jsonrpc: '2.0', + id, + method: prop, + params: args, + }; + + transport.send(request); + }); + }; + } + return Reflect.get(target, prop); + }, + }); +} diff --git a/packages/plugin-bridge/vite.config.ts b/packages/plugin-bridge/vite.config.ts index 2dea06f9..32ed412a 100644 --- a/packages/plugin-bridge/vite.config.ts +++ b/packages/plugin-bridge/vite.config.ts @@ -12,6 +12,10 @@ export default defineConfig({ root: __dirname, cacheDir: '../../node_modules/.vite/communication', base: './', + test: { + globals: true, + environment: 'node', + }, build: { lib: { entry: resolve(__dirname, 'src/index.ts'), diff --git a/website/src/docs/plugin-development/plugin-development.md b/website/src/docs/plugin-development/plugin-development.md index 38b24c79..ad051a70 100644 --- a/website/src/docs/plugin-development/plugin-development.md +++ b/website/src/docs/plugin-development/plugin-development.md @@ -127,7 +127,7 @@ import React, { useEffect, useState } from 'react'; import { useRozeniteDevToolsClient } from '@rozenite/plugin-bridge'; // Define type-safe event map -interface PluginEvents { +type PluginEvents = { 'user-data': { id: string; name: string; @@ -136,7 +136,7 @@ interface PluginEvents { 'request-user-data': { type: 'userInfo'; }; -} +}; export default function MyPanel() { const client = useRozeniteDevToolsClient({ @@ -185,6 +185,104 @@ export default function MyPanel() { } ``` +### RPC Bridge + +For more complex interactions, Rozenite provides a symmetrical bi-directional RPC bridge. This allows you to call methods on the other side as if they were local asynchronous functions. + +#### 1. Define Protocols + +First, define the interfaces for both the App and DevTools sides. These should be shared between your App and DevTools code. + +```typescript +// protocols.ts +export type AppProtocol = { + getAppVersion(): Promise; + logMessage(msg: string): Promise; +}; + +export type DevToolsProtocol = { + refresh(): Promise; + showNotification(text: string): Promise; +}; +``` + +#### 2. Initialize in App (React Native) + +In your `react-native.ts`, set up the bridge and implement the `AppProtocol`. + +```typescript title="react-native.ts" +import { createRozeniteRPCBridge, DevToolsPluginClient } from '@rozenite/plugin-bridge'; +import { AppProtocol, DevToolsProtocol } from './protocols'; + +export default function setupPlugin(client: DevToolsPluginClient) { + // Implement local handlers for AppProtocol + const localHandlers: AppProtocol = { + async getAppVersion() { + return '1.0.0'; + }, + async logMessage(msg) { + console.log('App Log:', msg); + } + }; + + // Create Bridge + const devTools = createRozeniteRPCBridge( + { + send: (msg) => client.send('rpc', msg), + onMessage: (listener) => client.onMessage('rpc', listener), + }, + localHandlers + ); + + // You can now call methods on the DevTools side + // devTools.showNotification('App connected!'); +} +``` + +#### 3. Initialize in DevTools (Panel) + +In your panel component, set up the bridge and implement the `DevToolsProtocol`. + +```typescript title="src/my-panel.tsx" +import React, { useEffect } from 'react'; +import { createRozeniteRPCBridge, useRozeniteDevToolsClient } from '@rozenite/plugin-bridge'; +import { AppProtocol, DevToolsProtocol } from './protocols'; + +export default function MyPanel() { + const client = useRozeniteDevToolsClient({ pluginId: 'my-plugin' }); + + useEffect(() => { + if (!client) return; + + // Implement local handlers for DevToolsProtocol + const localHandlers: DevToolsProtocol = { + async refresh() { + console.log('Refreshing...'); + }, + async showNotification(text) { + alert(text); + return true; + } + }; + + // Create Bridge + const app = createRozeniteRPCBridge( + { + send: (msg) => client.send('rpc', msg), + onMessage: (listener) => client.onMessage('rpc', listener), + }, + localHandlers + ); + + // Call remote method on App + app.getAppVersion().then(version => console.log('App Version:', version)); + + }, [client]); + + return
My Panel
; +} +``` + ## Step 4: React Native Integration Add React Native functionality by creating a `react-native.ts` file. You can use React Native APIs and libraries to enhance your plugin: @@ -194,7 +292,7 @@ import { DevToolsPluginClient } from '@rozenite/plugin-bridge'; import { Platform, Dimensions } from 'react-native'; // Use the same type-safe event map -interface PluginEvents { +type PluginEvents = { 'user-data': { id: string; name: string; @@ -203,7 +301,7 @@ interface PluginEvents { 'request-user-data': { type: 'userInfo'; }; -} +}; export default function setupPlugin( client: DevToolsPluginClient From 326b9351b87eb0648cbcbb43b330cfcd393d35c6 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Sun, 28 Dec 2025 21:56:36 +0100 Subject: [PATCH 2/4] feat(plugin-bridge): add timeout support --- packages/plugin-bridge/README.md | 3 +- packages/plugin-bridge/src/index.ts | 1 + packages/plugin-bridge/src/rpc.test.ts | 22 ++++++++++ packages/plugin-bridge/src/rpc.ts | 40 +++++++++++++++++-- .../plugin-development/plugin-development.md | 3 +- 5 files changed, 64 insertions(+), 5 deletions(-) diff --git a/packages/plugin-bridge/README.md b/packages/plugin-bridge/README.md index 907cfc97..ebf7a1f8 100644 --- a/packages/plugin-bridge/README.md +++ b/packages/plugin-bridge/README.md @@ -148,7 +148,8 @@ async function setupAppBridge() { send: (msg) => client.send('rpc', msg), onMessage: (listener) => client.onMessage('rpc', listener), }, - localHandlers + localHandlers, + { timeout: 30000 } // Optional: Timeout in ms (default: 60000) ); // Call remote method diff --git a/packages/plugin-bridge/src/index.ts b/packages/plugin-bridge/src/index.ts index 51e322ec..29d38be3 100644 --- a/packages/plugin-bridge/src/index.ts +++ b/packages/plugin-bridge/src/index.ts @@ -6,3 +6,4 @@ export { getRozeniteDevToolsClient } from './client'; export { UnsupportedPlatformError } from './errors'; export { createRozeniteRPCBridge } from './rpc'; export type { Transport as RozeniteRPCTransport } from './rpc'; +export type { RPCBridgeOptions } from './rpc'; diff --git a/packages/plugin-bridge/src/rpc.test.ts b/packages/plugin-bridge/src/rpc.test.ts index 5912bbd5..e34cd252 100644 --- a/packages/plugin-bridge/src/rpc.test.ts +++ b/packages/plugin-bridge/src/rpc.test.ts @@ -159,4 +159,26 @@ describe('createRozeniteRPCBridge', () => { expect(localHandlers.ping).not.toHaveBeenCalled(); expect(transport.send).not.toHaveBeenCalled(); }); + + it('should timeout if no response is received', async () => { + const transport = createMockTransport(); + const localHandlers = {}; + + vi.useFakeTimers(); + + const bridge = createRozeniteRPCBridge( + transport as unknown as RozeniteRPCTransport, + localHandlers, + { timeout: 1000 } + ); + + const promise = bridge.multiply(1, 2); + + // Fast-forward time + vi.advanceTimersByTime(1001); + + await expect(promise).rejects.toThrow('RPC Timeout: Request multiply timed out after 1000ms'); + + vi.useRealTimers(); + }); }); diff --git a/packages/plugin-bridge/src/rpc.ts b/packages/plugin-bridge/src/rpc.ts index e9ce2565..d501609a 100644 --- a/packages/plugin-bridge/src/rpc.ts +++ b/packages/plugin-bridge/src/rpc.ts @@ -30,6 +30,16 @@ export type RPCResponseError = { export type RPCResponse = RPCResponseSuccess | RPCResponseError; +export type RPCBridgeOptions = { + /** + * Timeout in milliseconds for RPC requests. + * If a response is not received within this time, the promise will be rejected. + * Set to 0 to disable timeout. + * @default 60000 (60 seconds) + */ + timeout?: number; +}; + type PendingPromise = { resolve: (value: unknown) => void; reject: (reason?: unknown) => void; @@ -88,13 +98,16 @@ function isRPCResponse(message: any): message is RPCResponse { * * @param transport - The transport layer to send/receive messages. * @param localHandlers - The implementation of the local methods exposed to the other side. + * @param options - Configuration options. * @returns A Proxy object representing the remote interface. */ export function createRozeniteRPCBridge( transport: Transport, - localHandlers: LocalHandlers + localHandlers: LocalHandlers, + options?: RPCBridgeOptions ): RemoteInterface { const pendingPromises = new Map(); + const timeoutMs = options?.timeout ?? 60000; // Message Handler transport.onMessage((message: unknown) => { @@ -172,7 +185,28 @@ export function createRozeniteRPCBridge { return new Promise((resolve, reject) => { const id = generateId(); - pendingPromises.set(id, { resolve, reject }); + + let timer: ReturnType | undefined; + + if (timeoutMs > 0) { + timer = setTimeout(() => { + if (pendingPromises.has(id)) { + pendingPromises.delete(id); + reject(new Error(`RPC Timeout: Request ${prop} timed out after ${timeoutMs}ms`)); + } + }, timeoutMs); + } + + pendingPromises.set(id, { + resolve: (val) => { + if (timer) clearTimeout(timer); + resolve(val); + }, + reject: (err) => { + if (timer) clearTimeout(timer); + reject(err); + } + }); const request: RPCRequest = { jsonrpc: '2.0', @@ -188,4 +222,4 @@ export function createRozeniteRPCBridge client.send('rpc', msg), onMessage: (listener) => client.onMessage('rpc', listener), }, - localHandlers + localHandlers, + { timeout: 30000 } // Optional: Timeout in ms (default: 60000) ); // You can now call methods on the DevTools side From 2908968eafdfff230dba0eb6d7308d3eba7d907b Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Wed, 7 Jan 2026 10:04:21 +0100 Subject: [PATCH 3/4] fix: lint errors --- packages/plugin-bridge/src/rpc.test.ts | 5 ++++- packages/plugin-bridge/src/rpc.ts | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/plugin-bridge/src/rpc.test.ts b/packages/plugin-bridge/src/rpc.test.ts index e34cd252..43ba6a49 100644 --- a/packages/plugin-bridge/src/rpc.test.ts +++ b/packages/plugin-bridge/src/rpc.test.ts @@ -4,7 +4,10 @@ import type { RozeniteRPCTransport } from './index'; describe('createRozeniteRPCBridge', () => { const createMockTransport = () => { - let listener: (message: unknown) => void = () => {}; + let listener: (message: unknown) => void = () => { + // Empty listener to avoid warnings + }; + return { send: vi.fn(), onMessage: vi.fn((l) => { diff --git a/packages/plugin-bridge/src/rpc.ts b/packages/plugin-bridge/src/rpc.ts index d501609a..eeeab70c 100644 --- a/packages/plugin-bridge/src/rpc.ts +++ b/packages/plugin-bridge/src/rpc.ts @@ -55,7 +55,7 @@ function serializeError(error: unknown) { name: error.name, // Copy other properties ...Object.getOwnPropertyNames(error).reduce((acc, key) => { - // @ts-ignore + // @ts-expect-error - error is an instance of Error acc[key] = error[key]; return acc; }, {} as Record) @@ -114,7 +114,7 @@ export function createRozeniteRPCBridge Date: Wed, 7 Jan 2026 10:11:30 +0100 Subject: [PATCH 4/4] refactor: use arrow functions --- packages/plugin-bridge/src/rpc.ts | 110 ++++++++++++++++-------------- 1 file changed, 60 insertions(+), 50 deletions(-) diff --git a/packages/plugin-bridge/src/rpc.ts b/packages/plugin-bridge/src/rpc.ts index eeeab70c..093227f6 100644 --- a/packages/plugin-bridge/src/rpc.ts +++ b/packages/plugin-bridge/src/rpc.ts @@ -46,66 +46,73 @@ type PendingPromise = { }; // Error serialization helper -function serializeError(error: unknown) { +const serializeError = (error: unknown) => { if (error instanceof Error) { return { message: error.message, - data: { - stack: error.stack, + data: { + stack: error.stack, name: error.name, // Copy other properties ...Object.getOwnPropertyNames(error).reduce((acc, key) => { - // @ts-expect-error - error is an instance of Error - acc[key] = error[key]; - return acc; - }, {} as Record) + acc[key] = (error as any)[key]; + return acc; + }, {} as Record), }, }; } return { message: String(error), }; -} +}; // Simple ID generator -function generateId(): string { - return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); -} +const generateId = (): string => { + return ( + Math.random().toString(36).substring(2, 15) + + Math.random().toString(36).substring(2, 15) + ); +}; -function isRPCRequest(message: any): message is RPCRequest { +const isRPCRequest = (message: unknown): message is RPCRequest => { + const msg = message as any; return ( - typeof message === 'object' && - message !== null && - message.jsonrpc === '2.0' && - typeof message.method === 'string' && - typeof message.id === 'string' && - Array.isArray(message.params) + typeof msg === 'object' && + msg !== null && + msg.jsonrpc === '2.0' && + typeof msg.method === 'string' && + typeof msg.id === 'string' && + Array.isArray(msg.params) ); -} +}; -function isRPCResponse(message: any): message is RPCResponse { +const isRPCResponse = (message: unknown): message is RPCResponse => { + const msg = message as any; return ( - typeof message === 'object' && - message !== null && - message.jsonrpc === '2.0' && - typeof message.id === 'string' && - ('result' in message || 'error' in message) + typeof msg === 'object' && + msg !== null && + msg.jsonrpc === '2.0' && + typeof msg.id === 'string' && + ('result' in msg || 'error' in msg) ); -} +}; /** * Creates a Symmetrical Bi-Directional RPC Bridge. - * + * * @param transport - The transport layer to send/receive messages. * @param localHandlers - The implementation of the local methods exposed to the other side. * @param options - Configuration options. * @returns A Proxy object representing the remote interface. */ -export function createRozeniteRPCBridge( +export const createRozeniteRPCBridge = < + LocalHandlers extends object, + RemoteInterface extends object +>( transport: Transport, localHandlers: LocalHandlers, options?: RPCBridgeOptions -): RemoteInterface { +): RemoteInterface => { const pendingPromises = new Map(); const timeoutMs = options?.timeout ?? 60000; @@ -114,8 +121,7 @@ export function createRozeniteRPCBridge { return new Promise((resolve, reject) => { const id = generateId(); - + let timer: ReturnType | undefined; if (timeoutMs > 0) { timer = setTimeout(() => { if (pendingPromises.has(id)) { pendingPromises.delete(id); - reject(new Error(`RPC Timeout: Request ${prop} timed out after ${timeoutMs}ms`)); + reject( + new Error( + `RPC Timeout: Request ${prop} timed out after ${timeoutMs}ms` + ) + ); } }, timeoutMs); } - pendingPromises.set(id, { + pendingPromises.set(id, { resolve: (val) => { if (timer) clearTimeout(timer); resolve(val); - }, + }, reject: (err) => { if (timer) clearTimeout(timer); reject(err); - } + }, }); const request: RPCRequest = { @@ -222,4 +232,4 @@ export function createRozeniteRPCBridge