@nsky/rpc-bridge 0.0.1-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +187 -0
- package/dist/index.d.mts +187 -0
- package/dist/index.mjs +1 -0
- package/package.json +31 -0
package/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# @nsky/rpc-bridge
|
|
2
|
+
|
|
3
|
+
Transport-agnostic JSON-RPC 2.0 bridge for code that needs the same business API across Web Workers, browser extensions, Electron, and WebSocket-backed runtimes.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
// 服务的接口定义(供客户端类型约束使用)
|
|
9
|
+
export interface CryptoService {
|
|
10
|
+
encrypt(text: string): Promise<string>;
|
|
11
|
+
decrypt(text: string): Promise<string>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// 纯净的服务实现(不依赖任何平台API)
|
|
15
|
+
export class CryptoService {
|
|
16
|
+
async encrypt(text) {
|
|
17
|
+
// 模拟加密耗时任务
|
|
18
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 1000));
|
|
19
|
+
return `[Encrypted:${text}]`;
|
|
20
|
+
}
|
|
21
|
+
async decrypt(cipher) {
|
|
22
|
+
return cipher.replace("[Encrypted:", "").replace("]", "");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
On the service side:
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { BridgeServer, WorkerTransport } from "@nsky/rpc-bridge";
|
|
31
|
+
|
|
32
|
+
// 1.初始化适配器
|
|
33
|
+
const transport = new WorkerTransport(self);
|
|
34
|
+
|
|
35
|
+
// 2.启动 RPC 服务端
|
|
36
|
+
const server = new BridgeServer(transport);
|
|
37
|
+
|
|
38
|
+
// 3.注册服务
|
|
39
|
+
server.registerService("CryptoService", new CryptoService());
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
On the client side:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { BridgeClient, BridgeServer, WorkerTransport } from "@nsky/rpc-bridge";
|
|
46
|
+
|
|
47
|
+
// 1. 初始化 Web Worker
|
|
48
|
+
const worker = new Worker("worker.js");
|
|
49
|
+
|
|
50
|
+
// 2. 初始化客户端适配器
|
|
51
|
+
const client = new BridgeClient(new WorkerTransport(worker));
|
|
52
|
+
|
|
53
|
+
// 3. 获取 CryptoService 代理
|
|
54
|
+
const cryptoService = client.getService<CryptoService>("CryptoService");
|
|
55
|
+
|
|
56
|
+
// 4. 像调用本地方法一样调用(实际上是跨线程异步调用)
|
|
57
|
+
const result = await cryptoService.encrypt("Hello World");
|
|
58
|
+
console.log(result);
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Transports
|
|
62
|
+
|
|
63
|
+
- `WorkerTransport` wraps postMessage-style Worker endpoints.
|
|
64
|
+
- `ChromeTransport` wraps `chrome.runtime.sendMessage` and `runtime.onMessage`.
|
|
65
|
+
- `ElectronTransport` wraps Electron IPC channels.
|
|
66
|
+
- `WebSocketTransport` serializes RPC messages over JSON WebSocket frames.
|
|
67
|
+
|
|
68
|
+
All transports implement the same `RpcTransport` interface, so application code only swaps the adapter at initialization time.
|
|
69
|
+
|
|
70
|
+
## JSON-RPC 2.0 Messages
|
|
71
|
+
|
|
72
|
+
`BridgeClient` sends JSON-RPC 2.0 request objects:
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"jsonrpc": "2.0",
|
|
77
|
+
"method": "CryptoService.encrypt",
|
|
78
|
+
"params": ["Hello World"],
|
|
79
|
+
"id": "request-1"
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`BridgeServer` returns JSON-RPC success and error responses, including standard error codes such as `-32600` for invalid requests and `-32601` for missing methods. Notifications omit `id` and do not receive responses.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e={PARSE_ERROR:-32700,INVALID_REQUEST:-32600,METHOD_NOT_FOUND:-32601,INVALID_PARAMS:-32602,INTERNAL_ERROR:-32603,TIMEOUT:-32e3,UNAUTHORIZED:-32001,FORBIDDEN:-32002,NOT_FOUND:-32003};var t=class extends Error{code;data;constructor(e){super(e.message),this.name=`JsonRpcError`,this.code=e.code,this.data=e.data}};function n(e){return p(e)&&Object.hasOwn(e,`id`)&&v(e.id)&&(h(e)||g(e))}function r(e){return m(e)&&Object.hasOwn(e,`id`)&&v(e.id)}function i(e){return m(e)&&!Object.hasOwn(e,`id`)}function a(e,t,n,r){return{jsonrpc:`2.0`,method:`${e}.${t}`,params:n,id:r}}function o(e,t){return{jsonrpc:`2.0`,result:t===void 0?null:t,id:e}}function s(e,t,n,r){return{jsonrpc:`2.0`,error:r===void 0?{code:t,message:n}:{code:t,message:n,data:r},id:e}}function c(){let e=Math.random().toString(36).slice(2,11);return`${Date.now().toString(36)}-${e}`}function l(e){let t=e.lastIndexOf(`.`);if(!(t<=0||t===e.length-1))return{serviceName:e.slice(0,t),methodName:e.slice(t+1)}}function u(e){return e===void 0?[]:Array.isArray(e)?e:[e]}function d(e){return e instanceof Error?{name:e.name,message:e.message,stack:e.stack}:String(e)}function f(e){return new t(e)}function p(e){return typeof e==`object`&&!!e&&!Array.isArray(e)&&e.jsonrpc===`2.0`}function m(e){return p(e)&&typeof e.method==`string`&&(e.params===void 0||_(e.params))}function h(e){return p(e)&&Object.hasOwn(e,`result`)&&!Object.hasOwn(e,`error`)}function g(e){if(!p(e)||!Object.hasOwn(e,`error`))return!1;let t=e.error;return typeof t==`object`&&!!t&&!Array.isArray(t)&&typeof t.code==`number`&&typeof t.message==`string`}function _(e){return Array.isArray(e)||y(e)}function v(e){return typeof e==`string`||typeof e==`number`||e===null}function y(e){return typeof e==`object`&&!!e&&!Array.isArray(e)&&Object.getPrototypeOf(e)===Object.prototype}var b=class{transport;idFactory;pendingRequests=new Map;timeoutMs;unsubscribe;constructor(e,t={}){this.transport=e,this.timeoutMs=t.timeoutMs??15e3,this.idFactory=t.idFactory??c;let r=this.transport.listen(e=>{n(e)&&this.resolveResponse(e)});this.unsubscribe=typeof r==`function`?r:void 0}get pendingCount(){return this.pendingRequests.size}getService(e){return new Proxy({},{get:(t,n)=>{if(!(typeof n!=`string`||n===`then`))return(...t)=>this.call(e,n,t)}})}destroy(e=`RPC client destroyed.`){this.unsubscribe?.();for(let[t,n]of this.pendingRequests)clearTimeout(n.timer),n.reject(Error(e)),this.pendingRequests.delete(t)}call(e,t,n){let r=this.idFactory();return new Promise((i,o)=>{let s=setTimeout(()=>{this.pendingRequests.delete(r),o(Error(`RPC request to ${e}.${t} timed out after ${this.timeoutMs}ms.`))},this.timeoutMs);this.pendingRequests.set(r,{serviceName:e,method:t,resolve:i,reject:o,timer:s}),this.transport.send(a(e,t,n,r))})}resolveResponse(e){if(e.id===null)return;let t=this.pendingRequests.get(e.id);if(t){if(clearTimeout(t.timer),this.pendingRequests.delete(e.id),e.error){t.reject(f(e.error));return}t.resolve(e.result)}}},x=class t{transport;services=new Map;unsubscribe;static ERR_INVALID_REQUEST=s(null,e.INVALID_REQUEST,`Invalid Request`);constructor(e){this.transport=e;let t=this.transport.listen(e=>{this.handleMessage(e)});this.unsubscribe=typeof t==`function`?t:void 0}registerService(e,t){this.services.set(e,t)}unregisterService(e){return this.services.delete(e)}destroy(){this.services.clear(),this.unsubscribe?.()}async handleMessage(e){if(Array.isArray(e)){await this.handleBatch(e);return}let t=await this.handleSingle(e);t&&await this.transport.send(t)}async handleBatch(e){if(e.length===0){await this.transport.send(t.ERR_INVALID_REQUEST);return}let n=(await Promise.all(e.map(e=>this.handleSingle(e)))).filter(e=>e!==void 0);n.length>0&&await this.transport.send(n)}async handleSingle(n){let a=r(n),c=i(n);if(!a&&!c)return t.ERR_INVALID_REQUEST;let f=a?n.id:void 0;try{let e=l(n.method);if(!e||n.method.startsWith(`rpc.`))return S(n.method,f);let t=this.services.get(e.serviceName);if(!t)return S(n.method,f);let r=t[e.methodName];if(typeof r!=`function`)return S(n.method,f);let i=await r(...u(n.params));return f===void 0?void 0:o(f,i)}catch(t){return f===void 0?void 0:s(f,e.INTERNAL_ERROR,`Internal error`,d(t))}}};function S(t,n){if(n!==void 0)return s(n,e.METHOD_NOT_FOUND,`Method not found`,{method:t})}var C=class{runtime;constructor(e=w()){if(!e?.runtime)throw Error(`Chrome runtime API is not available.`);this.runtime=e.runtime}send(e){this.runtime.sendMessage(e,()=>{this.runtime.lastError})}listen(e){let t=t=>{e(t)};return this.runtime.onMessage.addListener(t),()=>{this.runtime.onMessage.removeListener?.(t)}}};function w(){return globalThis.chrome}var T=class{ipc;channel;webContents;constructor(e,t={}){this.ipc=e,this.channel=t.channel??`rpc-channel`,this.webContents=t.webContents}send(e){if(this.webContents){this.webContents.send(this.channel,e);return}if(!this.ipc.send)throw Error(`Electron ipc sender is not available.`);this.ipc.send(this.channel,e)}listen(e){let t=(t,n)=>{e(n??t)};return this.ipc.on(this.channel,t),()=>{this.ipc.removeListener?.(this.channel,t)}}},E=class{socket;constructor(e){this.socket=e}send(e){this.socket.send(JSON.stringify(e))}listen(e){let t=t=>{e(typeof t.data==`string`?JSON.parse(t.data):t.data)};return this.socket.addEventListener(`message`,t),()=>{this.socket.removeEventListener?.(`message`,t)}}},D=class{target;options;constructor(e,t={}){this.target=e,this.options=t}send(e,t=this.options.transfer){if(t){this.target.postMessage(e,t);return}this.target.postMessage(e)}listen(e){let t=t=>{e(t.data)};if(this.target.addEventListener)return this.target.addEventListener(`message`,t),this.target.start?.(),()=>{this.target.removeEventListener?.(`message`,t)};let n=this.target.onmessage;return this.target.onmessage=e=>{n?.(e),t(e)},this.target.start?.(),()=>{this.target.onmessage=n}}};exports.BridgeClient=b,exports.BridgeServer=x,exports.ChromeTransport=C,exports.ElectronTransport=T,exports.RPCError=t,exports.RPC_ERROR_CODE=e,exports.RPC_VERSION=`2.0`,exports.WebSocketTransport=E,exports.WorkerTransport=D;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
declare const RPC_VERSION = "2.0";
|
|
3
|
+
/**
|
|
4
|
+
* 标准错误码
|
|
5
|
+
*/
|
|
6
|
+
declare const RPC_ERROR_CODE: {
|
|
7
|
+
readonly PARSE_ERROR: -32700;
|
|
8
|
+
readonly INVALID_REQUEST: -32600;
|
|
9
|
+
readonly METHOD_NOT_FOUND: -32601;
|
|
10
|
+
readonly INVALID_PARAMS: -32602;
|
|
11
|
+
readonly INTERNAL_ERROR: -32603;
|
|
12
|
+
readonly TIMEOUT: -32000;
|
|
13
|
+
readonly UNAUTHORIZED: -32001;
|
|
14
|
+
readonly FORBIDDEN: -32002;
|
|
15
|
+
readonly NOT_FOUND: -32003;
|
|
16
|
+
};
|
|
17
|
+
type RPCId = string | number | null;
|
|
18
|
+
type RPCParams = readonly unknown[] | Record<string, unknown>;
|
|
19
|
+
type RPCPayload = RPCMessage | readonly RPCMessage[];
|
|
20
|
+
type RPCMessage = RPCRequest | RPCResponse | RPCNotification;
|
|
21
|
+
interface RPCRequest {
|
|
22
|
+
readonly jsonrpc: typeof RPC_VERSION;
|
|
23
|
+
readonly method: string;
|
|
24
|
+
readonly params?: RPCParams;
|
|
25
|
+
readonly id: RPCId;
|
|
26
|
+
}
|
|
27
|
+
interface RPCNotification {
|
|
28
|
+
readonly jsonrpc: typeof RPC_VERSION;
|
|
29
|
+
readonly method: string;
|
|
30
|
+
readonly params?: RPCParams;
|
|
31
|
+
}
|
|
32
|
+
type RPCResponse = RPCSuccessResponse | RPCErrorResponse;
|
|
33
|
+
interface RPCSuccessResponse {
|
|
34
|
+
readonly jsonrpc: typeof RPC_VERSION;
|
|
35
|
+
readonly result: unknown;
|
|
36
|
+
readonly id: RPCId;
|
|
37
|
+
}
|
|
38
|
+
interface RPCErrorResponse {
|
|
39
|
+
readonly jsonrpc: typeof RPC_VERSION;
|
|
40
|
+
readonly error: RPCErrorObject;
|
|
41
|
+
readonly id: RPCId;
|
|
42
|
+
}
|
|
43
|
+
interface RPCErrorObject {
|
|
44
|
+
readonly code: number;
|
|
45
|
+
readonly message: string;
|
|
46
|
+
readonly data?: unknown;
|
|
47
|
+
}
|
|
48
|
+
type RPCListener = (message: unknown) => void | Promise<void>;
|
|
49
|
+
interface RPCTransport {
|
|
50
|
+
/**
|
|
51
|
+
* 发送 JSON-RPC 2.0 消息
|
|
52
|
+
* @param {RPCPayload} message
|
|
53
|
+
*/
|
|
54
|
+
send(message: RPCPayload): void | Promise<void>;
|
|
55
|
+
/**
|
|
56
|
+
* 监听 JSON-RPC 2.0 消息
|
|
57
|
+
* @param {RPCListener} callback (message) => void
|
|
58
|
+
*/
|
|
59
|
+
listen(callback: RPCListener): void | (() => void);
|
|
60
|
+
}
|
|
61
|
+
type RPCHandler = (...args: any[]) => any;
|
|
62
|
+
type RPCService = object;
|
|
63
|
+
type RPCServiceProxy<TService extends object> = { [TMethod in keyof TService as TService[TMethod] extends RPCHandler ? TMethod : never]: TService[TMethod] extends ((...args: infer TArgs) => infer TResult) ? (...args: TArgs) => Promise<Awaited<TResult>> : never };
|
|
64
|
+
declare class RPCError extends Error {
|
|
65
|
+
readonly code: number;
|
|
66
|
+
readonly data: unknown;
|
|
67
|
+
constructor(error: RPCErrorObject);
|
|
68
|
+
}
|
|
69
|
+
//#endregion
|
|
70
|
+
//#region src/client.d.ts
|
|
71
|
+
interface BridgeClientOptions {
|
|
72
|
+
readonly timeoutMs?: number;
|
|
73
|
+
readonly idFactory?: () => Exclude<RPCId, null>;
|
|
74
|
+
}
|
|
75
|
+
declare class BridgeClient {
|
|
76
|
+
private readonly transport;
|
|
77
|
+
private readonly idFactory;
|
|
78
|
+
private readonly pendingRequests;
|
|
79
|
+
private readonly timeoutMs;
|
|
80
|
+
private readonly unsubscribe;
|
|
81
|
+
constructor(transport: RPCTransport, options?: BridgeClientOptions);
|
|
82
|
+
get pendingCount(): number;
|
|
83
|
+
getService<TService extends object = RPCService>(serviceName: string): RPCServiceProxy<TService>;
|
|
84
|
+
destroy(reason?: string): void;
|
|
85
|
+
private call;
|
|
86
|
+
private resolveResponse;
|
|
87
|
+
}
|
|
88
|
+
//#endregion
|
|
89
|
+
//#region src/server.d.ts
|
|
90
|
+
declare class BridgeServer {
|
|
91
|
+
private readonly transport;
|
|
92
|
+
private readonly services;
|
|
93
|
+
private readonly unsubscribe;
|
|
94
|
+
static readonly ERR_INVALID_REQUEST: RPCErrorResponse;
|
|
95
|
+
constructor(transport: RPCTransport);
|
|
96
|
+
registerService(name: string, service: RPCService): void;
|
|
97
|
+
unregisterService(name: string): boolean;
|
|
98
|
+
destroy(): void;
|
|
99
|
+
private handleMessage;
|
|
100
|
+
private handleBatch;
|
|
101
|
+
private handleSingle;
|
|
102
|
+
}
|
|
103
|
+
//#endregion
|
|
104
|
+
//#region src/transports/chrome.d.ts
|
|
105
|
+
interface ChromeRuntimeLastError {
|
|
106
|
+
readonly message?: string;
|
|
107
|
+
}
|
|
108
|
+
interface ChromeRuntimeMessageEvent {
|
|
109
|
+
addListener(listener: (message: unknown) => void): void;
|
|
110
|
+
removeListener?(listener: (message: unknown) => void): void;
|
|
111
|
+
}
|
|
112
|
+
interface ChromeRuntimeLike {
|
|
113
|
+
readonly lastError?: ChromeRuntimeLastError;
|
|
114
|
+
readonly onMessage: ChromeRuntimeMessageEvent;
|
|
115
|
+
sendMessage(message: unknown, callback?: () => void): void;
|
|
116
|
+
}
|
|
117
|
+
interface ChromeLike {
|
|
118
|
+
readonly runtime: ChromeRuntimeLike;
|
|
119
|
+
}
|
|
120
|
+
declare class ChromeTransport implements RPCTransport {
|
|
121
|
+
private readonly runtime;
|
|
122
|
+
constructor(chromeApi?: ChromeLike | undefined);
|
|
123
|
+
send(message: RPCPayload): void;
|
|
124
|
+
listen(callback: RPCListener): () => void;
|
|
125
|
+
}
|
|
126
|
+
//#endregion
|
|
127
|
+
//#region src/transports/electron.d.ts
|
|
128
|
+
interface ElectronIpcLike {
|
|
129
|
+
send?(channel: string, message: RPCPayload): void;
|
|
130
|
+
on(channel: string, listener: (event: unknown, message?: RPCPayload) => void): void;
|
|
131
|
+
removeListener?(channel: string, listener: (event: unknown, message?: RPCPayload) => void): void;
|
|
132
|
+
}
|
|
133
|
+
interface ElectronWebContentsLike {
|
|
134
|
+
send(channel: string, message: RPCPayload): void;
|
|
135
|
+
}
|
|
136
|
+
interface ElectronTransportOptions {
|
|
137
|
+
readonly channel?: string;
|
|
138
|
+
readonly webContents?: ElectronWebContentsLike;
|
|
139
|
+
}
|
|
140
|
+
declare class ElectronTransport implements RPCTransport {
|
|
141
|
+
private readonly ipc;
|
|
142
|
+
private readonly channel;
|
|
143
|
+
private readonly webContents;
|
|
144
|
+
constructor(ipc: ElectronIpcLike, options?: ElectronTransportOptions);
|
|
145
|
+
send(message: RPCPayload): void;
|
|
146
|
+
listen(callback: RPCListener): () => void;
|
|
147
|
+
}
|
|
148
|
+
//#endregion
|
|
149
|
+
//#region src/transports/websocket.d.ts
|
|
150
|
+
interface WebSocketMessageEvent {
|
|
151
|
+
readonly data: unknown;
|
|
152
|
+
}
|
|
153
|
+
interface WebSocketLike {
|
|
154
|
+
send(data: string): void;
|
|
155
|
+
addEventListener(type: "message", listener: (event: WebSocketMessageEvent) => void): void;
|
|
156
|
+
removeEventListener?(type: "message", listener: (event: WebSocketMessageEvent) => void): void;
|
|
157
|
+
}
|
|
158
|
+
declare class WebSocketTransport implements RPCTransport {
|
|
159
|
+
private readonly socket;
|
|
160
|
+
constructor(socket: WebSocketLike);
|
|
161
|
+
send(message: RPCPayload): void;
|
|
162
|
+
listen(callback: RPCListener): () => void;
|
|
163
|
+
}
|
|
164
|
+
//#endregion
|
|
165
|
+
//#region src/transports/worker.d.ts
|
|
166
|
+
interface WorkerMessageEvent {
|
|
167
|
+
readonly data: unknown;
|
|
168
|
+
}
|
|
169
|
+
interface WorkerEndpoint {
|
|
170
|
+
onmessage?: ((event: WorkerMessageEvent) => void) | null;
|
|
171
|
+
postMessage(message: unknown, transfer?: readonly Transferable[]): void;
|
|
172
|
+
addEventListener?(type: "message", listener: (event: WorkerMessageEvent) => void): void;
|
|
173
|
+
removeEventListener?(type: "message", listener: (event: WorkerMessageEvent) => void): void;
|
|
174
|
+
start?(): void;
|
|
175
|
+
}
|
|
176
|
+
interface WorkerTransportOptions {
|
|
177
|
+
readonly transfer?: readonly Transferable[];
|
|
178
|
+
}
|
|
179
|
+
declare class WorkerTransport implements RPCTransport {
|
|
180
|
+
private readonly target;
|
|
181
|
+
private readonly options;
|
|
182
|
+
constructor(target: WorkerEndpoint, options?: WorkerTransportOptions);
|
|
183
|
+
send(message: RPCPayload, transfer?: readonly Transferable[] | undefined): void;
|
|
184
|
+
listen(callback: RPCListener): () => void;
|
|
185
|
+
}
|
|
186
|
+
//#endregion
|
|
187
|
+
export { BridgeClient, BridgeClientOptions, BridgeServer, ChromeLike, ChromeRuntimeLike, ChromeTransport, ElectronIpcLike, ElectronTransport, ElectronTransportOptions, ElectronWebContentsLike, RPCError, RPCErrorObject, RPCErrorResponse, RPCHandler, RPCId, RPCListener, RPCMessage, RPCNotification, RPCParams, RPCPayload, RPCRequest, RPCResponse, RPCService, RPCServiceProxy, RPCSuccessResponse, RPCTransport, RPC_ERROR_CODE, RPC_VERSION, WebSocketLike, WebSocketMessageEvent, WebSocketTransport, WorkerEndpoint, WorkerMessageEvent, WorkerTransport, WorkerTransportOptions };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
declare const RPC_VERSION = "2.0";
|
|
3
|
+
/**
|
|
4
|
+
* 标准错误码
|
|
5
|
+
*/
|
|
6
|
+
declare const RPC_ERROR_CODE: {
|
|
7
|
+
readonly PARSE_ERROR: -32700;
|
|
8
|
+
readonly INVALID_REQUEST: -32600;
|
|
9
|
+
readonly METHOD_NOT_FOUND: -32601;
|
|
10
|
+
readonly INVALID_PARAMS: -32602;
|
|
11
|
+
readonly INTERNAL_ERROR: -32603;
|
|
12
|
+
readonly TIMEOUT: -32000;
|
|
13
|
+
readonly UNAUTHORIZED: -32001;
|
|
14
|
+
readonly FORBIDDEN: -32002;
|
|
15
|
+
readonly NOT_FOUND: -32003;
|
|
16
|
+
};
|
|
17
|
+
type RPCId = string | number | null;
|
|
18
|
+
type RPCParams = readonly unknown[] | Record<string, unknown>;
|
|
19
|
+
type RPCPayload = RPCMessage | readonly RPCMessage[];
|
|
20
|
+
type RPCMessage = RPCRequest | RPCResponse | RPCNotification;
|
|
21
|
+
interface RPCRequest {
|
|
22
|
+
readonly jsonrpc: typeof RPC_VERSION;
|
|
23
|
+
readonly method: string;
|
|
24
|
+
readonly params?: RPCParams;
|
|
25
|
+
readonly id: RPCId;
|
|
26
|
+
}
|
|
27
|
+
interface RPCNotification {
|
|
28
|
+
readonly jsonrpc: typeof RPC_VERSION;
|
|
29
|
+
readonly method: string;
|
|
30
|
+
readonly params?: RPCParams;
|
|
31
|
+
}
|
|
32
|
+
type RPCResponse = RPCSuccessResponse | RPCErrorResponse;
|
|
33
|
+
interface RPCSuccessResponse {
|
|
34
|
+
readonly jsonrpc: typeof RPC_VERSION;
|
|
35
|
+
readonly result: unknown;
|
|
36
|
+
readonly id: RPCId;
|
|
37
|
+
}
|
|
38
|
+
interface RPCErrorResponse {
|
|
39
|
+
readonly jsonrpc: typeof RPC_VERSION;
|
|
40
|
+
readonly error: RPCErrorObject;
|
|
41
|
+
readonly id: RPCId;
|
|
42
|
+
}
|
|
43
|
+
interface RPCErrorObject {
|
|
44
|
+
readonly code: number;
|
|
45
|
+
readonly message: string;
|
|
46
|
+
readonly data?: unknown;
|
|
47
|
+
}
|
|
48
|
+
type RPCListener = (message: unknown) => void | Promise<void>;
|
|
49
|
+
interface RPCTransport {
|
|
50
|
+
/**
|
|
51
|
+
* 发送 JSON-RPC 2.0 消息
|
|
52
|
+
* @param {RPCPayload} message
|
|
53
|
+
*/
|
|
54
|
+
send(message: RPCPayload): void | Promise<void>;
|
|
55
|
+
/**
|
|
56
|
+
* 监听 JSON-RPC 2.0 消息
|
|
57
|
+
* @param {RPCListener} callback (message) => void
|
|
58
|
+
*/
|
|
59
|
+
listen(callback: RPCListener): void | (() => void);
|
|
60
|
+
}
|
|
61
|
+
type RPCHandler = (...args: any[]) => any;
|
|
62
|
+
type RPCService = object;
|
|
63
|
+
type RPCServiceProxy<TService extends object> = { [TMethod in keyof TService as TService[TMethod] extends RPCHandler ? TMethod : never]: TService[TMethod] extends ((...args: infer TArgs) => infer TResult) ? (...args: TArgs) => Promise<Awaited<TResult>> : never };
|
|
64
|
+
declare class RPCError extends Error {
|
|
65
|
+
readonly code: number;
|
|
66
|
+
readonly data: unknown;
|
|
67
|
+
constructor(error: RPCErrorObject);
|
|
68
|
+
}
|
|
69
|
+
//#endregion
|
|
70
|
+
//#region src/client.d.ts
|
|
71
|
+
interface BridgeClientOptions {
|
|
72
|
+
readonly timeoutMs?: number;
|
|
73
|
+
readonly idFactory?: () => Exclude<RPCId, null>;
|
|
74
|
+
}
|
|
75
|
+
declare class BridgeClient {
|
|
76
|
+
private readonly transport;
|
|
77
|
+
private readonly idFactory;
|
|
78
|
+
private readonly pendingRequests;
|
|
79
|
+
private readonly timeoutMs;
|
|
80
|
+
private readonly unsubscribe;
|
|
81
|
+
constructor(transport: RPCTransport, options?: BridgeClientOptions);
|
|
82
|
+
get pendingCount(): number;
|
|
83
|
+
getService<TService extends object = RPCService>(serviceName: string): RPCServiceProxy<TService>;
|
|
84
|
+
destroy(reason?: string): void;
|
|
85
|
+
private call;
|
|
86
|
+
private resolveResponse;
|
|
87
|
+
}
|
|
88
|
+
//#endregion
|
|
89
|
+
//#region src/server.d.ts
|
|
90
|
+
declare class BridgeServer {
|
|
91
|
+
private readonly transport;
|
|
92
|
+
private readonly services;
|
|
93
|
+
private readonly unsubscribe;
|
|
94
|
+
static readonly ERR_INVALID_REQUEST: RPCErrorResponse;
|
|
95
|
+
constructor(transport: RPCTransport);
|
|
96
|
+
registerService(name: string, service: RPCService): void;
|
|
97
|
+
unregisterService(name: string): boolean;
|
|
98
|
+
destroy(): void;
|
|
99
|
+
private handleMessage;
|
|
100
|
+
private handleBatch;
|
|
101
|
+
private handleSingle;
|
|
102
|
+
}
|
|
103
|
+
//#endregion
|
|
104
|
+
//#region src/transports/chrome.d.ts
|
|
105
|
+
interface ChromeRuntimeLastError {
|
|
106
|
+
readonly message?: string;
|
|
107
|
+
}
|
|
108
|
+
interface ChromeRuntimeMessageEvent {
|
|
109
|
+
addListener(listener: (message: unknown) => void): void;
|
|
110
|
+
removeListener?(listener: (message: unknown) => void): void;
|
|
111
|
+
}
|
|
112
|
+
interface ChromeRuntimeLike {
|
|
113
|
+
readonly lastError?: ChromeRuntimeLastError;
|
|
114
|
+
readonly onMessage: ChromeRuntimeMessageEvent;
|
|
115
|
+
sendMessage(message: unknown, callback?: () => void): void;
|
|
116
|
+
}
|
|
117
|
+
interface ChromeLike {
|
|
118
|
+
readonly runtime: ChromeRuntimeLike;
|
|
119
|
+
}
|
|
120
|
+
declare class ChromeTransport implements RPCTransport {
|
|
121
|
+
private readonly runtime;
|
|
122
|
+
constructor(chromeApi?: ChromeLike | undefined);
|
|
123
|
+
send(message: RPCPayload): void;
|
|
124
|
+
listen(callback: RPCListener): () => void;
|
|
125
|
+
}
|
|
126
|
+
//#endregion
|
|
127
|
+
//#region src/transports/electron.d.ts
|
|
128
|
+
interface ElectronIpcLike {
|
|
129
|
+
send?(channel: string, message: RPCPayload): void;
|
|
130
|
+
on(channel: string, listener: (event: unknown, message?: RPCPayload) => void): void;
|
|
131
|
+
removeListener?(channel: string, listener: (event: unknown, message?: RPCPayload) => void): void;
|
|
132
|
+
}
|
|
133
|
+
interface ElectronWebContentsLike {
|
|
134
|
+
send(channel: string, message: RPCPayload): void;
|
|
135
|
+
}
|
|
136
|
+
interface ElectronTransportOptions {
|
|
137
|
+
readonly channel?: string;
|
|
138
|
+
readonly webContents?: ElectronWebContentsLike;
|
|
139
|
+
}
|
|
140
|
+
declare class ElectronTransport implements RPCTransport {
|
|
141
|
+
private readonly ipc;
|
|
142
|
+
private readonly channel;
|
|
143
|
+
private readonly webContents;
|
|
144
|
+
constructor(ipc: ElectronIpcLike, options?: ElectronTransportOptions);
|
|
145
|
+
send(message: RPCPayload): void;
|
|
146
|
+
listen(callback: RPCListener): () => void;
|
|
147
|
+
}
|
|
148
|
+
//#endregion
|
|
149
|
+
//#region src/transports/websocket.d.ts
|
|
150
|
+
interface WebSocketMessageEvent {
|
|
151
|
+
readonly data: unknown;
|
|
152
|
+
}
|
|
153
|
+
interface WebSocketLike {
|
|
154
|
+
send(data: string): void;
|
|
155
|
+
addEventListener(type: "message", listener: (event: WebSocketMessageEvent) => void): void;
|
|
156
|
+
removeEventListener?(type: "message", listener: (event: WebSocketMessageEvent) => void): void;
|
|
157
|
+
}
|
|
158
|
+
declare class WebSocketTransport implements RPCTransport {
|
|
159
|
+
private readonly socket;
|
|
160
|
+
constructor(socket: WebSocketLike);
|
|
161
|
+
send(message: RPCPayload): void;
|
|
162
|
+
listen(callback: RPCListener): () => void;
|
|
163
|
+
}
|
|
164
|
+
//#endregion
|
|
165
|
+
//#region src/transports/worker.d.ts
|
|
166
|
+
interface WorkerMessageEvent {
|
|
167
|
+
readonly data: unknown;
|
|
168
|
+
}
|
|
169
|
+
interface WorkerEndpoint {
|
|
170
|
+
onmessage?: ((event: WorkerMessageEvent) => void) | null;
|
|
171
|
+
postMessage(message: unknown, transfer?: readonly Transferable[]): void;
|
|
172
|
+
addEventListener?(type: "message", listener: (event: WorkerMessageEvent) => void): void;
|
|
173
|
+
removeEventListener?(type: "message", listener: (event: WorkerMessageEvent) => void): void;
|
|
174
|
+
start?(): void;
|
|
175
|
+
}
|
|
176
|
+
interface WorkerTransportOptions {
|
|
177
|
+
readonly transfer?: readonly Transferable[];
|
|
178
|
+
}
|
|
179
|
+
declare class WorkerTransport implements RPCTransport {
|
|
180
|
+
private readonly target;
|
|
181
|
+
private readonly options;
|
|
182
|
+
constructor(target: WorkerEndpoint, options?: WorkerTransportOptions);
|
|
183
|
+
send(message: RPCPayload, transfer?: readonly Transferable[] | undefined): void;
|
|
184
|
+
listen(callback: RPCListener): () => void;
|
|
185
|
+
}
|
|
186
|
+
//#endregion
|
|
187
|
+
export { BridgeClient, BridgeClientOptions, BridgeServer, ChromeLike, ChromeRuntimeLike, ChromeTransport, ElectronIpcLike, ElectronTransport, ElectronTransportOptions, ElectronWebContentsLike, RPCError, RPCErrorObject, RPCErrorResponse, RPCHandler, RPCId, RPCListener, RPCMessage, RPCNotification, RPCParams, RPCPayload, RPCRequest, RPCResponse, RPCService, RPCServiceProxy, RPCSuccessResponse, RPCTransport, RPC_ERROR_CODE, RPC_VERSION, WebSocketLike, WebSocketMessageEvent, WebSocketTransport, WorkerEndpoint, WorkerMessageEvent, WorkerTransport, WorkerTransportOptions };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const e=`2.0`,t={PARSE_ERROR:-32700,INVALID_REQUEST:-32600,METHOD_NOT_FOUND:-32601,INVALID_PARAMS:-32602,INTERNAL_ERROR:-32603,TIMEOUT:-32e3,UNAUTHORIZED:-32001,FORBIDDEN:-32002,NOT_FOUND:-32003};var n=class extends Error{code;data;constructor(e){super(e.message),this.name=`JsonRpcError`,this.code=e.code,this.data=e.data}};function r(e){return m(e)&&Object.hasOwn(e,`id`)&&y(e.id)&&(g(e)||_(e))}function i(e){return h(e)&&Object.hasOwn(e,`id`)&&y(e.id)}function a(e){return h(e)&&!Object.hasOwn(e,`id`)}function o(e,t,n,r){return{jsonrpc:`2.0`,method:`${e}.${t}`,params:n,id:r}}function s(e,t){return{jsonrpc:`2.0`,result:t===void 0?null:t,id:e}}function c(e,t,n,r){return{jsonrpc:`2.0`,error:r===void 0?{code:t,message:n}:{code:t,message:n,data:r},id:e}}function l(){let e=Math.random().toString(36).slice(2,11);return`${Date.now().toString(36)}-${e}`}function u(e){let t=e.lastIndexOf(`.`);if(!(t<=0||t===e.length-1))return{serviceName:e.slice(0,t),methodName:e.slice(t+1)}}function d(e){return e===void 0?[]:Array.isArray(e)?e:[e]}function f(e){return e instanceof Error?{name:e.name,message:e.message,stack:e.stack}:String(e)}function p(e){return new n(e)}function m(e){return typeof e==`object`&&!!e&&!Array.isArray(e)&&e.jsonrpc===`2.0`}function h(e){return m(e)&&typeof e.method==`string`&&(e.params===void 0||v(e.params))}function g(e){return m(e)&&Object.hasOwn(e,`result`)&&!Object.hasOwn(e,`error`)}function _(e){if(!m(e)||!Object.hasOwn(e,`error`))return!1;let t=e.error;return typeof t==`object`&&!!t&&!Array.isArray(t)&&typeof t.code==`number`&&typeof t.message==`string`}function v(e){return Array.isArray(e)||b(e)}function y(e){return typeof e==`string`||typeof e==`number`||e===null}function b(e){return typeof e==`object`&&!!e&&!Array.isArray(e)&&Object.getPrototypeOf(e)===Object.prototype}var x=class{transport;idFactory;pendingRequests=new Map;timeoutMs;unsubscribe;constructor(e,t={}){this.transport=e,this.timeoutMs=t.timeoutMs??15e3,this.idFactory=t.idFactory??l;let n=this.transport.listen(e=>{r(e)&&this.resolveResponse(e)});this.unsubscribe=typeof n==`function`?n:void 0}get pendingCount(){return this.pendingRequests.size}getService(e){return new Proxy({},{get:(t,n)=>{if(!(typeof n!=`string`||n===`then`))return(...t)=>this.call(e,n,t)}})}destroy(e=`RPC client destroyed.`){this.unsubscribe?.();for(let[t,n]of this.pendingRequests)clearTimeout(n.timer),n.reject(Error(e)),this.pendingRequests.delete(t)}call(e,t,n){let r=this.idFactory();return new Promise((i,a)=>{let s=setTimeout(()=>{this.pendingRequests.delete(r),a(Error(`RPC request to ${e}.${t} timed out after ${this.timeoutMs}ms.`))},this.timeoutMs);this.pendingRequests.set(r,{serviceName:e,method:t,resolve:i,reject:a,timer:s}),this.transport.send(o(e,t,n,r))})}resolveResponse(e){if(e.id===null)return;let t=this.pendingRequests.get(e.id);if(t){if(clearTimeout(t.timer),this.pendingRequests.delete(e.id),e.error){t.reject(p(e.error));return}t.resolve(e.result)}}},S=class e{transport;services=new Map;unsubscribe;static ERR_INVALID_REQUEST=c(null,t.INVALID_REQUEST,`Invalid Request`);constructor(e){this.transport=e;let t=this.transport.listen(e=>{this.handleMessage(e)});this.unsubscribe=typeof t==`function`?t:void 0}registerService(e,t){this.services.set(e,t)}unregisterService(e){return this.services.delete(e)}destroy(){this.services.clear(),this.unsubscribe?.()}async handleMessage(e){if(Array.isArray(e)){await this.handleBatch(e);return}let t=await this.handleSingle(e);t&&await this.transport.send(t)}async handleBatch(t){if(t.length===0){await this.transport.send(e.ERR_INVALID_REQUEST);return}let n=(await Promise.all(t.map(e=>this.handleSingle(e)))).filter(e=>e!==void 0);n.length>0&&await this.transport.send(n)}async handleSingle(n){let r=i(n),o=a(n);if(!r&&!o)return e.ERR_INVALID_REQUEST;let l=r?n.id:void 0;try{let e=u(n.method);if(!e||n.method.startsWith(`rpc.`))return C(n.method,l);let t=this.services.get(e.serviceName);if(!t)return C(n.method,l);let r=t[e.methodName];if(typeof r!=`function`)return C(n.method,l);let i=await r(...d(n.params));return l===void 0?void 0:s(l,i)}catch(e){return l===void 0?void 0:c(l,t.INTERNAL_ERROR,`Internal error`,f(e))}}};function C(e,n){if(n!==void 0)return c(n,t.METHOD_NOT_FOUND,`Method not found`,{method:e})}var w=class{runtime;constructor(e=T()){if(!e?.runtime)throw Error(`Chrome runtime API is not available.`);this.runtime=e.runtime}send(e){this.runtime.sendMessage(e,()=>{this.runtime.lastError})}listen(e){let t=t=>{e(t)};return this.runtime.onMessage.addListener(t),()=>{this.runtime.onMessage.removeListener?.(t)}}};function T(){return globalThis.chrome}var E=class{ipc;channel;webContents;constructor(e,t={}){this.ipc=e,this.channel=t.channel??`rpc-channel`,this.webContents=t.webContents}send(e){if(this.webContents){this.webContents.send(this.channel,e);return}if(!this.ipc.send)throw Error(`Electron ipc sender is not available.`);this.ipc.send(this.channel,e)}listen(e){let t=(t,n)=>{e(n??t)};return this.ipc.on(this.channel,t),()=>{this.ipc.removeListener?.(this.channel,t)}}},D=class{socket;constructor(e){this.socket=e}send(e){this.socket.send(JSON.stringify(e))}listen(e){let t=t=>{e(typeof t.data==`string`?JSON.parse(t.data):t.data)};return this.socket.addEventListener(`message`,t),()=>{this.socket.removeEventListener?.(`message`,t)}}},O=class{target;options;constructor(e,t={}){this.target=e,this.options=t}send(e,t=this.options.transfer){if(t){this.target.postMessage(e,t);return}this.target.postMessage(e)}listen(e){let t=t=>{e(t.data)};if(this.target.addEventListener)return this.target.addEventListener(`message`,t),this.target.start?.(),()=>{this.target.removeEventListener?.(`message`,t)};let n=this.target.onmessage;return this.target.onmessage=e=>{n?.(e),t(e)},this.target.start?.(),()=>{this.target.onmessage=n}}};export{x as BridgeClient,S as BridgeServer,w as ChromeTransport,E as ElectronTransport,n as RPCError,t as RPC_ERROR_CODE,e as RPC_VERSION,D as WebSocketTransport,O as WorkerTransport};
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nsky/rpc-bridge",
|
|
3
|
+
"version": "0.0.1-beta",
|
|
4
|
+
"description": "Transport-agnostic RPC bridge for workers, extensions, Electron, and WebSocket runtimes.",
|
|
5
|
+
"files": [
|
|
6
|
+
"dist",
|
|
7
|
+
"README.md"
|
|
8
|
+
],
|
|
9
|
+
"type": "module",
|
|
10
|
+
"sideEffects": false,
|
|
11
|
+
"main": "./dist/index.cjs",
|
|
12
|
+
"module": "./dist/index.mjs",
|
|
13
|
+
"types": "./dist/index.d.mts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./src/index.d.mts",
|
|
17
|
+
"import": "./dist/index.mjs",
|
|
18
|
+
"require": "./dist/index.cjs",
|
|
19
|
+
"development": "./src/index.ts",
|
|
20
|
+
"production": "./src/index.ts",
|
|
21
|
+
"default": "./dist/index.mjs"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "pnpm exec tsdown",
|
|
26
|
+
"dev": "pnpm exec tsdown --watch",
|
|
27
|
+
"publint": "publint",
|
|
28
|
+
"test": "vitest run",
|
|
29
|
+
"typecheck": "tsc --noEmit"
|
|
30
|
+
}
|
|
31
|
+
}
|