@igoforth/ws-rpc 1.0.0
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/LICENSE +21 -0
- package/README.md +446 -0
- package/dist/adapters/client.d.ts +117 -0
- package/dist/adapters/client.js +241 -0
- package/dist/adapters/cloudflare-do.d.ts +72 -0
- package/dist/adapters/cloudflare-do.js +192 -0
- package/dist/adapters/index.d.ts +13 -0
- package/dist/adapters/index.js +16 -0
- package/dist/adapters/server.d.ts +10 -0
- package/dist/adapters/server.js +122 -0
- package/dist/adapters/types.d.ts +125 -0
- package/dist/adapters/types.js +3 -0
- package/dist/codecs/cbor.d.ts +16 -0
- package/dist/codecs/cbor.js +36 -0
- package/dist/codecs/factory.d.ts +3 -0
- package/dist/codecs/factory.js +3 -0
- package/dist/codecs/index.d.ts +5 -0
- package/dist/codecs/index.js +5 -0
- package/dist/codecs/json.d.ts +4 -0
- package/dist/codecs/json.js +4 -0
- package/dist/codecs/msgpack.d.ts +16 -0
- package/dist/codecs/msgpack.js +34 -0
- package/dist/codecs-BmYG2d_U.js +0 -0
- package/dist/default-BkrMd28n.js +253 -0
- package/dist/default-xDNNMrg0.d.ts +129 -0
- package/dist/durable-MZjkvyS6.js +165 -0
- package/dist/errors-5BfreE63.js +96 -0
- package/dist/errors.d.ts +69 -0
- package/dist/errors.js +7 -0
- package/dist/factory-3ziwTuZe.js +132 -0
- package/dist/factory-C1v0AEHY.d.ts +101 -0
- package/dist/index-Be7jjS77.d.ts +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +14 -0
- package/dist/interface-C4S-WCqW.d.ts +120 -0
- package/dist/json-54Z2bIIs.d.ts +22 -0
- package/dist/json-Bshec-bZ.js +41 -0
- package/dist/memory-Bqb3KEVr.js +48 -0
- package/dist/memory-D1nGjzzH.d.ts +41 -0
- package/dist/multi-peer-BAi9yVzp.js +242 -0
- package/dist/peers/default.d.ts +8 -0
- package/dist/peers/default.js +8 -0
- package/dist/peers/durable.d.ts +136 -0
- package/dist/peers/durable.js +9 -0
- package/dist/peers/index.d.ts +10 -0
- package/dist/peers/index.js +9 -0
- package/dist/protocol-DA84zrc2.d.ts +211 -0
- package/dist/protocol-_mpoOPp6.js +192 -0
- package/dist/protocol.d.ts +6 -0
- package/dist/protocol.js +6 -0
- package/dist/reconnect-CGAA_1Gf.js +26 -0
- package/dist/reconnect-DbcN0R_1.d.ts +35 -0
- package/dist/schema-CN5HHHku.d.ts +108 -0
- package/dist/schema.d.ts +2 -0
- package/dist/schema.js +43 -0
- package/dist/server-zTjpJpoX.d.ts +209 -0
- package/dist/sql-CCjc6Bid.js +142 -0
- package/dist/sql-DPmHOeZy.d.ts +131 -0
- package/dist/storage/index.d.ts +8 -0
- package/dist/storage/index.js +7 -0
- package/dist/storage/interface.d.ts +3 -0
- package/dist/storage/interface.js +0 -0
- package/dist/storage/memory.d.ts +7 -0
- package/dist/storage/memory.js +6 -0
- package/dist/storage/sql.d.ts +7 -0
- package/dist/storage/sql.js +6 -0
- package/dist/types-Be-qmQu0.d.ts +111 -0
- package/dist/types-D_psiH09.js +13 -0
- package/dist/types.d.ts +7 -0
- package/dist/types.js +3 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.js +3 -0
- package/dist/utils/reconnect.d.ts +2 -0
- package/dist/utils/reconnect.js +3 -0
- package/package.json +156 -0
- package/src/adapters/client.ts +396 -0
- package/src/adapters/cloudflare-do.ts +346 -0
- package/src/adapters/index.ts +16 -0
- package/src/adapters/multi-peer.ts +404 -0
- package/src/adapters/server.ts +192 -0
- package/src/adapters/types.ts +202 -0
- package/src/codecs/cbor.ts +42 -0
- package/src/codecs/factory.ts +210 -0
- package/src/codecs/index.ts +30 -0
- package/src/codecs/json.ts +42 -0
- package/src/codecs/msgpack.ts +36 -0
- package/src/errors.ts +105 -0
- package/src/index.ts +102 -0
- package/src/peers/default.ts +433 -0
- package/src/peers/durable.ts +280 -0
- package/src/peers/index.ts +13 -0
- package/src/protocol.ts +306 -0
- package/src/schema.ts +167 -0
- package/src/storage/index.ts +20 -0
- package/src/storage/interface.ts +146 -0
- package/src/storage/memory.ts +84 -0
- package/src/storage/sql.ts +266 -0
- package/src/types.ts +158 -0
- package/src/utils/index.ts +9 -0
- package/src/utils/reconnect.ts +51 -0
package/package.json
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@igoforth/ws-rpc",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Bidirectional RPC over WebSocket with Zod schema validation, TypeScript inference, and Cloudflare Durable Object support",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": {
|
|
8
|
+
"name": "Ian Goforth",
|
|
9
|
+
"email": "ian.goforth@gmail.com",
|
|
10
|
+
"url": "https://ian.goforth.systems/"
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/igoforth/ws-rpc.git"
|
|
15
|
+
},
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/igoforth/ws-rpc/issues"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/igoforth/ws-rpc#readme",
|
|
20
|
+
"keywords": [
|
|
21
|
+
"rpc",
|
|
22
|
+
"websocket",
|
|
23
|
+
"bidirectional",
|
|
24
|
+
"zod",
|
|
25
|
+
"typescript",
|
|
26
|
+
"cloudflare",
|
|
27
|
+
"durable-objects",
|
|
28
|
+
"workers",
|
|
29
|
+
"json-rpc",
|
|
30
|
+
"msgpack",
|
|
31
|
+
"cbor",
|
|
32
|
+
"realtime"
|
|
33
|
+
],
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=18"
|
|
36
|
+
},
|
|
37
|
+
"main": "./dist/index.js",
|
|
38
|
+
"types": "./dist/index.d.ts",
|
|
39
|
+
"files": [
|
|
40
|
+
"dist",
|
|
41
|
+
"src",
|
|
42
|
+
"LICENSE",
|
|
43
|
+
"README.md"
|
|
44
|
+
],
|
|
45
|
+
"exports": {
|
|
46
|
+
".": {
|
|
47
|
+
"types": "./dist/index.d.ts",
|
|
48
|
+
"import": "./dist/index.js"
|
|
49
|
+
},
|
|
50
|
+
"./protocol": {
|
|
51
|
+
"types": "./dist/protocol.d.ts",
|
|
52
|
+
"import": "./dist/protocol.js"
|
|
53
|
+
},
|
|
54
|
+
"./schema": {
|
|
55
|
+
"types": "./dist/schema.d.ts",
|
|
56
|
+
"import": "./dist/schema.js"
|
|
57
|
+
},
|
|
58
|
+
"./errors": {
|
|
59
|
+
"types": "./dist/errors.d.ts",
|
|
60
|
+
"import": "./dist/errors.js"
|
|
61
|
+
},
|
|
62
|
+
"./peer": {
|
|
63
|
+
"types": "./dist/peers/index.d.ts",
|
|
64
|
+
"import": "./dist/peers/index.js"
|
|
65
|
+
},
|
|
66
|
+
"./adapters": {
|
|
67
|
+
"types": "./dist/adapters/index.d.ts",
|
|
68
|
+
"import": "./dist/adapters/index.js"
|
|
69
|
+
},
|
|
70
|
+
"./adapters/client": {
|
|
71
|
+
"types": "./dist/adapters/client.d.ts",
|
|
72
|
+
"import": "./dist/adapters/client.js"
|
|
73
|
+
},
|
|
74
|
+
"./adapters/server": {
|
|
75
|
+
"types": "./dist/adapters/server.d.ts",
|
|
76
|
+
"import": "./dist/adapters/server.js"
|
|
77
|
+
},
|
|
78
|
+
"./adapters/cloudflare-do": {
|
|
79
|
+
"types": "./dist/adapters/cloudflare-do.d.ts",
|
|
80
|
+
"import": "./dist/adapters/cloudflare-do.js"
|
|
81
|
+
},
|
|
82
|
+
"./codecs": {
|
|
83
|
+
"types": "./dist/codecs/index.d.ts",
|
|
84
|
+
"import": "./dist/codecs/index.js"
|
|
85
|
+
},
|
|
86
|
+
"./codecs/json": {
|
|
87
|
+
"types": "./dist/codecs/json.d.ts",
|
|
88
|
+
"import": "./dist/codecs/json.js"
|
|
89
|
+
},
|
|
90
|
+
"./codecs/msgpack": {
|
|
91
|
+
"types": "./dist/codecs/msgpack.d.ts",
|
|
92
|
+
"import": "./dist/codecs/msgpack.js"
|
|
93
|
+
},
|
|
94
|
+
"./codecs/cbor": {
|
|
95
|
+
"types": "./dist/codecs/cbor.d.ts",
|
|
96
|
+
"import": "./dist/codecs/cbor.js"
|
|
97
|
+
},
|
|
98
|
+
"./storage": {
|
|
99
|
+
"types": "./dist/storage/index.d.ts",
|
|
100
|
+
"import": "./dist/storage/index.js"
|
|
101
|
+
},
|
|
102
|
+
"./types": {
|
|
103
|
+
"types": "./dist/types.d.ts",
|
|
104
|
+
"import": "./dist/types.js"
|
|
105
|
+
},
|
|
106
|
+
"./utils": {
|
|
107
|
+
"types": "./dist/utils/index.d.ts",
|
|
108
|
+
"import": "./dist/utils/index.js"
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
"dependencies": {
|
|
112
|
+
"type-fest": "^5.3.1",
|
|
113
|
+
"uuid": "^13.0.0",
|
|
114
|
+
"zod": "^4.2.0"
|
|
115
|
+
},
|
|
116
|
+
"peerDependencies": {
|
|
117
|
+
"@cloudflare/actors": ">=0.0.1-beta.6",
|
|
118
|
+
"@msgpack/msgpack": "^3.1.2",
|
|
119
|
+
"cbor-x": "^1.6.0"
|
|
120
|
+
},
|
|
121
|
+
"peerDependenciesMeta": {
|
|
122
|
+
"@cloudflare/actors": {
|
|
123
|
+
"optional": true
|
|
124
|
+
},
|
|
125
|
+
"@msgpack/msgpack": {
|
|
126
|
+
"optional": true
|
|
127
|
+
},
|
|
128
|
+
"cbor-x": {
|
|
129
|
+
"optional": true
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
"devDependencies": {
|
|
133
|
+
"@biomejs/biome": "^2.3.10",
|
|
134
|
+
"@cloudflare/actors": "^0.0.1-beta.6",
|
|
135
|
+
"@cloudflare/vitest-pool-workers": "^0.11.1",
|
|
136
|
+
"@msgpack/msgpack": "^3.1.2",
|
|
137
|
+
"@types/node": "^22.10.2",
|
|
138
|
+
"@types/ws": "^8.18.1",
|
|
139
|
+
"cbor-x": "^1.6.0",
|
|
140
|
+
"tsdown": "^0.18.1",
|
|
141
|
+
"typescript": "^5.9.3",
|
|
142
|
+
"vitest": "^3.2.4",
|
|
143
|
+
"wrangler": "^4.56.0",
|
|
144
|
+
"ws": "^8.18.3"
|
|
145
|
+
},
|
|
146
|
+
"scripts": {
|
|
147
|
+
"build": "tsdown",
|
|
148
|
+
"test": "vitest run",
|
|
149
|
+
"test:watch": "vitest",
|
|
150
|
+
"bench": "vitest bench --config vitest.config.bench.ts",
|
|
151
|
+
"typecheck": "tsc --noEmit",
|
|
152
|
+
"typegen": "wrangler types",
|
|
153
|
+
"lint": "biome check .",
|
|
154
|
+
"format": "biome format --write ."
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RPC Client Adapter
|
|
3
|
+
*
|
|
4
|
+
* WebSocket client with auto-reconnect for Node.js/Bun environments.
|
|
5
|
+
* Wraps RpcPeer with connection management.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Constructor } from "type-fest";
|
|
9
|
+
import { RpcPeer } from "../peers/default.js";
|
|
10
|
+
import type { WireInput } from "../protocol.js";
|
|
11
|
+
import type {
|
|
12
|
+
Driver,
|
|
13
|
+
EventDef,
|
|
14
|
+
InferEventData,
|
|
15
|
+
Provider,
|
|
16
|
+
RpcSchema,
|
|
17
|
+
StringKeys,
|
|
18
|
+
} from "../schema.js";
|
|
19
|
+
import {
|
|
20
|
+
type IRpcOptions,
|
|
21
|
+
type IWebSocket,
|
|
22
|
+
type WebSocketOptions,
|
|
23
|
+
WebSocketReadyState,
|
|
24
|
+
} from "../types.js";
|
|
25
|
+
import {
|
|
26
|
+
calculateReconnectDelay,
|
|
27
|
+
defaultReconnectOptions,
|
|
28
|
+
type IAdapterHooks,
|
|
29
|
+
type IConnectionAdapter,
|
|
30
|
+
type ReconnectOptions,
|
|
31
|
+
} from "./types.js";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Options for creating an RpcClient
|
|
35
|
+
*/
|
|
36
|
+
export interface RpcClientOptions<
|
|
37
|
+
TLocalSchema extends RpcSchema,
|
|
38
|
+
TRemoteSchema extends RpcSchema,
|
|
39
|
+
> extends IAdapterHooks<TRemoteSchema>,
|
|
40
|
+
IRpcOptions<TLocalSchema, TRemoteSchema> {
|
|
41
|
+
/** WebSocket URL to connect to */
|
|
42
|
+
url: string;
|
|
43
|
+
/** Implementation of local methods */
|
|
44
|
+
provider: Provider<TLocalSchema>;
|
|
45
|
+
/** Auto-reconnect options (set to false to disable) */
|
|
46
|
+
reconnect?: ReconnectOptions | false;
|
|
47
|
+
/** Automatically connect when client is created (default: false) */
|
|
48
|
+
autoConnect?: boolean;
|
|
49
|
+
/** WebSocket subprotocols */
|
|
50
|
+
protocols?: string | string[];
|
|
51
|
+
/** HTTP headers for WebSocket upgrade request (Bun/Node.js only) */
|
|
52
|
+
headers?: Record<string, string>;
|
|
53
|
+
/** Custom WebSocket constructor (defaults to global WebSocket) */
|
|
54
|
+
WebSocket?: new (
|
|
55
|
+
url: string,
|
|
56
|
+
options?: string | string[] | WebSocketOptions,
|
|
57
|
+
) => IWebSocket;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Connection state
|
|
62
|
+
*/
|
|
63
|
+
export type ConnectionState =
|
|
64
|
+
| "disconnected"
|
|
65
|
+
| "connecting"
|
|
66
|
+
| "connected"
|
|
67
|
+
| "reconnecting";
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* RPC Client with auto-reconnect
|
|
71
|
+
*
|
|
72
|
+
* Manages WebSocket connection lifecycle and provides RPC capabilities.
|
|
73
|
+
*/
|
|
74
|
+
export class RpcClient<
|
|
75
|
+
TLocalSchema extends RpcSchema,
|
|
76
|
+
TRemoteSchema extends RpcSchema,
|
|
77
|
+
> implements IConnectionAdapter<TLocalSchema, TRemoteSchema>
|
|
78
|
+
{
|
|
79
|
+
readonly localSchema: TLocalSchema;
|
|
80
|
+
readonly remoteSchema: TRemoteSchema;
|
|
81
|
+
readonly provider: Provider<TLocalSchema>;
|
|
82
|
+
readonly hooks: IAdapterHooks<TRemoteSchema> = {};
|
|
83
|
+
|
|
84
|
+
private readonly url: string;
|
|
85
|
+
private readonly reconnectOptions: Required<ReconnectOptions> | false;
|
|
86
|
+
private readonly defaultTimeout: number;
|
|
87
|
+
private readonly protocols: string | string[] | undefined;
|
|
88
|
+
private readonly headers: Record<string, string> | undefined;
|
|
89
|
+
private readonly WebSocketImpl: new (
|
|
90
|
+
url: string,
|
|
91
|
+
options?: string | string[] | WebSocketOptions,
|
|
92
|
+
) => IWebSocket;
|
|
93
|
+
|
|
94
|
+
// Connection state
|
|
95
|
+
private ws: IWebSocket | null = null;
|
|
96
|
+
private peer: RpcPeer<TLocalSchema, TRemoteSchema> | null = null;
|
|
97
|
+
private _state: ConnectionState = "disconnected";
|
|
98
|
+
private reconnectAttempt = 0;
|
|
99
|
+
private reconnectTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
100
|
+
private intentionalClose = false;
|
|
101
|
+
|
|
102
|
+
constructor(options: RpcClientOptions<TLocalSchema, TRemoteSchema>) {
|
|
103
|
+
this.url = options.url;
|
|
104
|
+
this.localSchema = options.localSchema;
|
|
105
|
+
this.remoteSchema = options.remoteSchema;
|
|
106
|
+
this.provider = options.provider;
|
|
107
|
+
this.reconnectOptions =
|
|
108
|
+
options.reconnect === false
|
|
109
|
+
? false
|
|
110
|
+
: { ...defaultReconnectOptions, ...options.reconnect };
|
|
111
|
+
this.defaultTimeout = options.timeout ?? 30000;
|
|
112
|
+
this.protocols = options.protocols;
|
|
113
|
+
this.headers = options.headers;
|
|
114
|
+
this.WebSocketImpl =
|
|
115
|
+
options.WebSocket ?? (globalThis.WebSocket as Constructor<IWebSocket>);
|
|
116
|
+
|
|
117
|
+
if (options.onConnect) this.hooks.onConnect = options.onConnect;
|
|
118
|
+
if (options.onDisconnect) this.hooks.onDisconnect = options.onDisconnect;
|
|
119
|
+
if (options.onReconnect) this.hooks.onReconnect = options.onReconnect;
|
|
120
|
+
if (options.onReconnectFailed)
|
|
121
|
+
this.hooks.onReconnectFailed = options.onReconnectFailed;
|
|
122
|
+
if (options.onEvent) this.hooks.onEvent = options.onEvent;
|
|
123
|
+
|
|
124
|
+
if (options.autoConnect) {
|
|
125
|
+
void this.connect();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Current connection state
|
|
131
|
+
*/
|
|
132
|
+
get state(): ConnectionState {
|
|
133
|
+
return this._state;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Whether the client is currently connected
|
|
138
|
+
*/
|
|
139
|
+
get isConnected(): boolean {
|
|
140
|
+
return this._state === "connected" && this.peer?.isOpen === true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Get the driver for calling remote methods
|
|
145
|
+
*
|
|
146
|
+
* @returns Driver proxy for calling remote methods
|
|
147
|
+
* @throws Error if not connected
|
|
148
|
+
*/
|
|
149
|
+
get driver(): Driver<TRemoteSchema> {
|
|
150
|
+
if (!this.peer) {
|
|
151
|
+
throw new Error("Not connected - call connect() first");
|
|
152
|
+
}
|
|
153
|
+
return this.peer.driver;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Emit an event to the server (fire-and-forget)
|
|
158
|
+
*
|
|
159
|
+
* @param event - Event name from local schema
|
|
160
|
+
* @param data - Event data matching the schema
|
|
161
|
+
*/
|
|
162
|
+
emit<K extends StringKeys<TLocalSchema["events"]>>(
|
|
163
|
+
event: K,
|
|
164
|
+
data: TLocalSchema["events"] extends Record<string, EventDef>
|
|
165
|
+
? InferEventData<TLocalSchema["events"][K]>
|
|
166
|
+
: never,
|
|
167
|
+
): void {
|
|
168
|
+
if (!this.peer) {
|
|
169
|
+
console.warn(`Cannot emit event '${String(event)}': not connected`);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
this.peer.emit(event, data);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Connect to the WebSocket server
|
|
177
|
+
*
|
|
178
|
+
* @returns Promise that resolves when connected
|
|
179
|
+
* @throws Error if connection fails
|
|
180
|
+
*/
|
|
181
|
+
async connect(): Promise<void> {
|
|
182
|
+
if (this._state === "connected" || this._state === "connecting") {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
this.intentionalClose = false;
|
|
187
|
+
this._state = "connecting";
|
|
188
|
+
|
|
189
|
+
return new Promise<void>((resolve, reject) => {
|
|
190
|
+
try {
|
|
191
|
+
// Use options object when headers are present (Bun/Node.js)
|
|
192
|
+
// Fall back to protocols-only for browser compatibility
|
|
193
|
+
let wsOptions: WebSocketOptions | string | string[] | undefined;
|
|
194
|
+
if (this.headers) {
|
|
195
|
+
wsOptions = { headers: this.headers };
|
|
196
|
+
if (this.protocols) {
|
|
197
|
+
wsOptions.protocols = this.protocols;
|
|
198
|
+
}
|
|
199
|
+
} else {
|
|
200
|
+
wsOptions = this.protocols;
|
|
201
|
+
}
|
|
202
|
+
this.ws = new this.WebSocketImpl(this.url, wsOptions);
|
|
203
|
+
} catch (error) {
|
|
204
|
+
this._state = "disconnected";
|
|
205
|
+
reject(error);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const onOpen = () => {
|
|
210
|
+
cleanup();
|
|
211
|
+
this.handleOpen();
|
|
212
|
+
resolve();
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const onError = (event: unknown) => {
|
|
216
|
+
cleanup();
|
|
217
|
+
this._state = "disconnected";
|
|
218
|
+
reject(new Error(`WebSocket connection failed: ${event}`));
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const onClose = (event: unknown) => {
|
|
222
|
+
cleanup();
|
|
223
|
+
this._state = "disconnected";
|
|
224
|
+
const code =
|
|
225
|
+
typeof event === "object" && event != null && "code" in event
|
|
226
|
+
? event.code
|
|
227
|
+
: "Unknown code";
|
|
228
|
+
const reason =
|
|
229
|
+
typeof event === "object" && event != null && "reason" in event
|
|
230
|
+
? event.reason
|
|
231
|
+
: "Unknown reason";
|
|
232
|
+
reject(new Error(`WebSocket closed: ${code} ${reason}`));
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const cleanup = () => {
|
|
236
|
+
this.ws?.removeEventListener?.("open", onOpen);
|
|
237
|
+
this.ws?.removeEventListener?.("error", onError);
|
|
238
|
+
this.ws?.removeEventListener?.("close", onClose);
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
this.ws.addEventListener?.("open", onOpen);
|
|
242
|
+
this.ws.addEventListener?.("error", onError);
|
|
243
|
+
this.ws.addEventListener?.("close", onClose);
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Disconnect from the server
|
|
249
|
+
*
|
|
250
|
+
* @param code - WebSocket close code (default: 1000)
|
|
251
|
+
* @param reason - Close reason message (default: "Client disconnect")
|
|
252
|
+
*/
|
|
253
|
+
disconnect(code = 1000, reason = "Client disconnect"): void {
|
|
254
|
+
this.intentionalClose = true;
|
|
255
|
+
this.cancelReconnect();
|
|
256
|
+
|
|
257
|
+
if (this.peer) {
|
|
258
|
+
this.peer.close();
|
|
259
|
+
this.peer = null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (this.ws && this.ws.readyState !== WebSocketReadyState.CLOSED) {
|
|
263
|
+
this.ws.close(code, reason);
|
|
264
|
+
}
|
|
265
|
+
this.ws = null;
|
|
266
|
+
|
|
267
|
+
this._state = "disconnected";
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Handle WebSocket open event
|
|
272
|
+
*/
|
|
273
|
+
private handleOpen(): void {
|
|
274
|
+
if (!this.ws) return;
|
|
275
|
+
|
|
276
|
+
this._state = "connected";
|
|
277
|
+
this.reconnectAttempt = 0;
|
|
278
|
+
|
|
279
|
+
// Create RPC peer
|
|
280
|
+
this.peer = new RpcPeer({
|
|
281
|
+
ws: this.ws,
|
|
282
|
+
localSchema: this.localSchema,
|
|
283
|
+
remoteSchema: this.remoteSchema,
|
|
284
|
+
provider: this.provider,
|
|
285
|
+
onEvent: this.hooks.onEvent,
|
|
286
|
+
timeout: this.defaultTimeout,
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// Set up WebSocket event handlers
|
|
290
|
+
this.ws.onmessage = (event) => {
|
|
291
|
+
if (typeof event === "object" && event != null && "data" in event)
|
|
292
|
+
this.peer?.handleMessage(event.data as WireInput);
|
|
293
|
+
else
|
|
294
|
+
throw new Error(
|
|
295
|
+
`Received invalid event type in RpcClient.ws.onmessage ${JSON.stringify(event)}`,
|
|
296
|
+
);
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
this.ws.onclose = (event) => {
|
|
300
|
+
if (
|
|
301
|
+
typeof event === "object" &&
|
|
302
|
+
event != null &&
|
|
303
|
+
"code" in event &&
|
|
304
|
+
"reason" in event &&
|
|
305
|
+
typeof event.code === "number" &&
|
|
306
|
+
typeof event.reason === "string"
|
|
307
|
+
)
|
|
308
|
+
this.handleClose(event.code, event.reason);
|
|
309
|
+
else
|
|
310
|
+
throw new Error(
|
|
311
|
+
`Received invalid event type in RpcClient.ws.onclose ${JSON.stringify(event)}`,
|
|
312
|
+
);
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
this.ws.onerror = (event) => {
|
|
316
|
+
console.error("WebSocket error:", event);
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
this.hooks.onConnect?.();
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Handle WebSocket close event
|
|
324
|
+
*/
|
|
325
|
+
private handleClose(code: number, reason: string): void {
|
|
326
|
+
this.peer?.close();
|
|
327
|
+
this.peer = null;
|
|
328
|
+
this.ws = null;
|
|
329
|
+
|
|
330
|
+
this.hooks.onDisconnect?.(code, reason);
|
|
331
|
+
|
|
332
|
+
if (this.intentionalClose) {
|
|
333
|
+
this._state = "disconnected";
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Attempt reconnection
|
|
338
|
+
if (this.reconnectOptions !== false) {
|
|
339
|
+
this.scheduleReconnect();
|
|
340
|
+
} else {
|
|
341
|
+
this._state = "disconnected";
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Schedule a reconnection attempt
|
|
347
|
+
*/
|
|
348
|
+
private scheduleReconnect(): void {
|
|
349
|
+
if (this.reconnectOptions === false) return;
|
|
350
|
+
|
|
351
|
+
const { maxAttempts } = this.reconnectOptions;
|
|
352
|
+
if (maxAttempts > 0 && this.reconnectAttempt >= maxAttempts) {
|
|
353
|
+
this._state = "disconnected";
|
|
354
|
+
this.hooks.onReconnectFailed?.();
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
this._state = "reconnecting";
|
|
359
|
+
const delay = calculateReconnectDelay(
|
|
360
|
+
this.reconnectAttempt,
|
|
361
|
+
this.reconnectOptions,
|
|
362
|
+
);
|
|
363
|
+
this.reconnectAttempt++;
|
|
364
|
+
|
|
365
|
+
this.hooks.onReconnect?.(this.reconnectAttempt, delay);
|
|
366
|
+
|
|
367
|
+
this.reconnectTimeout = setTimeout(() => {
|
|
368
|
+
this.reconnectTimeout = null;
|
|
369
|
+
void this.attemptReconnect();
|
|
370
|
+
}, delay);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Attempt to reconnect
|
|
375
|
+
*/
|
|
376
|
+
private async attemptReconnect(): Promise<void> {
|
|
377
|
+
try {
|
|
378
|
+
await this.connect();
|
|
379
|
+
} catch {
|
|
380
|
+
// connect() failed during reconnection - schedule another attempt
|
|
381
|
+
if (!this.intentionalClose && this.reconnectOptions !== false) {
|
|
382
|
+
this.scheduleReconnect();
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Cancel any pending reconnection
|
|
389
|
+
*/
|
|
390
|
+
private cancelReconnect(): void {
|
|
391
|
+
if (this.reconnectTimeout) {
|
|
392
|
+
clearTimeout(this.reconnectTimeout);
|
|
393
|
+
this.reconnectTimeout = null;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|