@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
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import "../factory-3ziwTuZe.js";
|
|
2
|
+
import "../json-Bshec-bZ.js";
|
|
3
|
+
import "../codecs-BmYG2d_U.js";
|
|
4
|
+
import "../protocol-_mpoOPp6.js";
|
|
5
|
+
import "../errors-5BfreE63.js";
|
|
6
|
+
import { t as WebSocketReadyState } from "../types-D_psiH09.js";
|
|
7
|
+
import { n as defaultReconnectOptions, t as calculateReconnectDelay } from "../reconnect-CGAA_1Gf.js";
|
|
8
|
+
import { t as RpcPeer } from "../default-BkrMd28n.js";
|
|
9
|
+
import "./types.js";
|
|
10
|
+
|
|
11
|
+
//#region src/adapters/client.ts
|
|
12
|
+
/**
|
|
13
|
+
* RPC Client with auto-reconnect
|
|
14
|
+
*
|
|
15
|
+
* Manages WebSocket connection lifecycle and provides RPC capabilities.
|
|
16
|
+
*/
|
|
17
|
+
var RpcClient = class {
|
|
18
|
+
localSchema;
|
|
19
|
+
remoteSchema;
|
|
20
|
+
provider;
|
|
21
|
+
hooks = {};
|
|
22
|
+
url;
|
|
23
|
+
reconnectOptions;
|
|
24
|
+
defaultTimeout;
|
|
25
|
+
protocols;
|
|
26
|
+
headers;
|
|
27
|
+
WebSocketImpl;
|
|
28
|
+
ws = null;
|
|
29
|
+
peer = null;
|
|
30
|
+
_state = "disconnected";
|
|
31
|
+
reconnectAttempt = 0;
|
|
32
|
+
reconnectTimeout = null;
|
|
33
|
+
intentionalClose = false;
|
|
34
|
+
constructor(options) {
|
|
35
|
+
this.url = options.url;
|
|
36
|
+
this.localSchema = options.localSchema;
|
|
37
|
+
this.remoteSchema = options.remoteSchema;
|
|
38
|
+
this.provider = options.provider;
|
|
39
|
+
this.reconnectOptions = options.reconnect === false ? false : {
|
|
40
|
+
...defaultReconnectOptions,
|
|
41
|
+
...options.reconnect
|
|
42
|
+
};
|
|
43
|
+
this.defaultTimeout = options.timeout ?? 3e4;
|
|
44
|
+
this.protocols = options.protocols;
|
|
45
|
+
this.headers = options.headers;
|
|
46
|
+
this.WebSocketImpl = options.WebSocket ?? globalThis.WebSocket;
|
|
47
|
+
if (options.onConnect) this.hooks.onConnect = options.onConnect;
|
|
48
|
+
if (options.onDisconnect) this.hooks.onDisconnect = options.onDisconnect;
|
|
49
|
+
if (options.onReconnect) this.hooks.onReconnect = options.onReconnect;
|
|
50
|
+
if (options.onReconnectFailed) this.hooks.onReconnectFailed = options.onReconnectFailed;
|
|
51
|
+
if (options.onEvent) this.hooks.onEvent = options.onEvent;
|
|
52
|
+
if (options.autoConnect) this.connect();
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Current connection state
|
|
56
|
+
*/
|
|
57
|
+
get state() {
|
|
58
|
+
return this._state;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Whether the client is currently connected
|
|
62
|
+
*/
|
|
63
|
+
get isConnected() {
|
|
64
|
+
return this._state === "connected" && this.peer?.isOpen === true;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Get the driver for calling remote methods
|
|
68
|
+
*
|
|
69
|
+
* @returns Driver proxy for calling remote methods
|
|
70
|
+
* @throws Error if not connected
|
|
71
|
+
*/
|
|
72
|
+
get driver() {
|
|
73
|
+
if (!this.peer) throw new Error("Not connected - call connect() first");
|
|
74
|
+
return this.peer.driver;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Emit an event to the server (fire-and-forget)
|
|
78
|
+
*
|
|
79
|
+
* @param event - Event name from local schema
|
|
80
|
+
* @param data - Event data matching the schema
|
|
81
|
+
*/
|
|
82
|
+
emit(event, data) {
|
|
83
|
+
if (!this.peer) {
|
|
84
|
+
console.warn(`Cannot emit event '${String(event)}': not connected`);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
this.peer.emit(event, data);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Connect to the WebSocket server
|
|
91
|
+
*
|
|
92
|
+
* @returns Promise that resolves when connected
|
|
93
|
+
* @throws Error if connection fails
|
|
94
|
+
*/
|
|
95
|
+
async connect() {
|
|
96
|
+
if (this._state === "connected" || this._state === "connecting") return;
|
|
97
|
+
this.intentionalClose = false;
|
|
98
|
+
this._state = "connecting";
|
|
99
|
+
return new Promise((resolve, reject) => {
|
|
100
|
+
try {
|
|
101
|
+
let wsOptions;
|
|
102
|
+
if (this.headers) {
|
|
103
|
+
wsOptions = { headers: this.headers };
|
|
104
|
+
if (this.protocols) wsOptions.protocols = this.protocols;
|
|
105
|
+
} else wsOptions = this.protocols;
|
|
106
|
+
this.ws = new this.WebSocketImpl(this.url, wsOptions);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
this._state = "disconnected";
|
|
109
|
+
reject(error);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const onOpen = () => {
|
|
113
|
+
cleanup();
|
|
114
|
+
this.handleOpen();
|
|
115
|
+
resolve();
|
|
116
|
+
};
|
|
117
|
+
const onError = (event) => {
|
|
118
|
+
cleanup();
|
|
119
|
+
this._state = "disconnected";
|
|
120
|
+
reject(/* @__PURE__ */ new Error(`WebSocket connection failed: ${event}`));
|
|
121
|
+
};
|
|
122
|
+
const onClose = (event) => {
|
|
123
|
+
cleanup();
|
|
124
|
+
this._state = "disconnected";
|
|
125
|
+
const code = typeof event === "object" && event != null && "code" in event ? event.code : "Unknown code";
|
|
126
|
+
const reason = typeof event === "object" && event != null && "reason" in event ? event.reason : "Unknown reason";
|
|
127
|
+
reject(/* @__PURE__ */ new Error(`WebSocket closed: ${code} ${reason}`));
|
|
128
|
+
};
|
|
129
|
+
const cleanup = () => {
|
|
130
|
+
this.ws?.removeEventListener?.("open", onOpen);
|
|
131
|
+
this.ws?.removeEventListener?.("error", onError);
|
|
132
|
+
this.ws?.removeEventListener?.("close", onClose);
|
|
133
|
+
};
|
|
134
|
+
this.ws.addEventListener?.("open", onOpen);
|
|
135
|
+
this.ws.addEventListener?.("error", onError);
|
|
136
|
+
this.ws.addEventListener?.("close", onClose);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Disconnect from the server
|
|
141
|
+
*
|
|
142
|
+
* @param code - WebSocket close code (default: 1000)
|
|
143
|
+
* @param reason - Close reason message (default: "Client disconnect")
|
|
144
|
+
*/
|
|
145
|
+
disconnect(code = 1e3, reason = "Client disconnect") {
|
|
146
|
+
this.intentionalClose = true;
|
|
147
|
+
this.cancelReconnect();
|
|
148
|
+
if (this.peer) {
|
|
149
|
+
this.peer.close();
|
|
150
|
+
this.peer = null;
|
|
151
|
+
}
|
|
152
|
+
if (this.ws && this.ws.readyState !== WebSocketReadyState.CLOSED) this.ws.close(code, reason);
|
|
153
|
+
this.ws = null;
|
|
154
|
+
this._state = "disconnected";
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Handle WebSocket open event
|
|
158
|
+
*/
|
|
159
|
+
handleOpen() {
|
|
160
|
+
if (!this.ws) return;
|
|
161
|
+
this._state = "connected";
|
|
162
|
+
this.reconnectAttempt = 0;
|
|
163
|
+
this.peer = new RpcPeer({
|
|
164
|
+
ws: this.ws,
|
|
165
|
+
localSchema: this.localSchema,
|
|
166
|
+
remoteSchema: this.remoteSchema,
|
|
167
|
+
provider: this.provider,
|
|
168
|
+
onEvent: this.hooks.onEvent,
|
|
169
|
+
timeout: this.defaultTimeout
|
|
170
|
+
});
|
|
171
|
+
this.ws.onmessage = (event) => {
|
|
172
|
+
if (typeof event === "object" && event != null && "data" in event) this.peer?.handleMessage(event.data);
|
|
173
|
+
else throw new Error(`Received invalid event type in RpcClient.ws.onmessage ${JSON.stringify(event)}`);
|
|
174
|
+
};
|
|
175
|
+
this.ws.onclose = (event) => {
|
|
176
|
+
if (typeof event === "object" && event != null && "code" in event && "reason" in event && typeof event.code === "number" && typeof event.reason === "string") this.handleClose(event.code, event.reason);
|
|
177
|
+
else throw new Error(`Received invalid event type in RpcClient.ws.onclose ${JSON.stringify(event)}`);
|
|
178
|
+
};
|
|
179
|
+
this.ws.onerror = (event) => {
|
|
180
|
+
console.error("WebSocket error:", event);
|
|
181
|
+
};
|
|
182
|
+
this.hooks.onConnect?.();
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Handle WebSocket close event
|
|
186
|
+
*/
|
|
187
|
+
handleClose(code, reason) {
|
|
188
|
+
this.peer?.close();
|
|
189
|
+
this.peer = null;
|
|
190
|
+
this.ws = null;
|
|
191
|
+
this.hooks.onDisconnect?.(code, reason);
|
|
192
|
+
if (this.intentionalClose) {
|
|
193
|
+
this._state = "disconnected";
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (this.reconnectOptions !== false) this.scheduleReconnect();
|
|
197
|
+
else this._state = "disconnected";
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Schedule a reconnection attempt
|
|
201
|
+
*/
|
|
202
|
+
scheduleReconnect() {
|
|
203
|
+
if (this.reconnectOptions === false) return;
|
|
204
|
+
const { maxAttempts } = this.reconnectOptions;
|
|
205
|
+
if (maxAttempts > 0 && this.reconnectAttempt >= maxAttempts) {
|
|
206
|
+
this._state = "disconnected";
|
|
207
|
+
this.hooks.onReconnectFailed?.();
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
this._state = "reconnecting";
|
|
211
|
+
const delay = calculateReconnectDelay(this.reconnectAttempt, this.reconnectOptions);
|
|
212
|
+
this.reconnectAttempt++;
|
|
213
|
+
this.hooks.onReconnect?.(this.reconnectAttempt, delay);
|
|
214
|
+
this.reconnectTimeout = setTimeout(() => {
|
|
215
|
+
this.reconnectTimeout = null;
|
|
216
|
+
this.attemptReconnect();
|
|
217
|
+
}, delay);
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Attempt to reconnect
|
|
221
|
+
*/
|
|
222
|
+
async attemptReconnect() {
|
|
223
|
+
try {
|
|
224
|
+
await this.connect();
|
|
225
|
+
} catch {
|
|
226
|
+
if (!this.intentionalClose && this.reconnectOptions !== false) this.scheduleReconnect();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Cancel any pending reconnection
|
|
231
|
+
*/
|
|
232
|
+
cancelReconnect() {
|
|
233
|
+
if (this.reconnectTimeout) {
|
|
234
|
+
clearTimeout(this.reconnectTimeout);
|
|
235
|
+
this.reconnectTimeout = null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
//#endregion
|
|
241
|
+
export { RpcClient };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { m as RpcSchema, p as Provider } from "../schema-CN5HHHku.js";
|
|
2
|
+
import "../factory-C1v0AEHY.js";
|
|
3
|
+
import "../json-54Z2bIIs.js";
|
|
4
|
+
import "../index-Be7jjS77.js";
|
|
5
|
+
import "../protocol-DA84zrc2.js";
|
|
6
|
+
import { a as IRpcOptions } from "../types-Be-qmQu0.js";
|
|
7
|
+
import { t as RpcPeer } from "../default-xDNNMrg0.js";
|
|
8
|
+
import { IMultiAdapterHooks, IMultiConnectionAdapter } from "./types.js";
|
|
9
|
+
import { Constructor } from "type-fest";
|
|
10
|
+
import { Actor } from "@cloudflare/actors";
|
|
11
|
+
|
|
12
|
+
//#region src/adapters/cloudflare-do.d.ts
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Extended hooks for Durable Object adapter
|
|
16
|
+
*/
|
|
17
|
+
interface IDOHooks<TLocalSchema extends RpcSchema, TRemoteSchema extends RpcSchema> extends IMultiAdapterHooks<TLocalSchema, TRemoteSchema> {
|
|
18
|
+
/** Called when a peer is recreated after hibernation */
|
|
19
|
+
onPeerRecreated?(peer: RpcPeer<TLocalSchema, TRemoteSchema>, ws: WebSocket): void;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Constructor type for the RPC mixin result.
|
|
23
|
+
*
|
|
24
|
+
* Subclasses must implement methods from TLocalSchema on `this`.
|
|
25
|
+
* Runtime enforces this when methods are called via RPC.
|
|
26
|
+
*/
|
|
27
|
+
type RpcActorConstructor<TBase extends Constructor<Actor<unknown>>, TLocalSchema extends RpcSchema, TRemoteSchema extends RpcSchema> = {
|
|
28
|
+
new (...args: ConstructorParameters<TBase>): InstanceType<TBase> & IMultiConnectionAdapter<TLocalSchema, TRemoteSchema>;
|
|
29
|
+
} & Omit<TBase, "new">;
|
|
30
|
+
/**
|
|
31
|
+
* Create a mixin that adds RPC capabilities to a Durable Object Actor.
|
|
32
|
+
*
|
|
33
|
+
* The resulting class requires implementation of all methods defined in
|
|
34
|
+
* `localSchema`. TypeScript enforces this at compile time.
|
|
35
|
+
*
|
|
36
|
+
* @param Base - The Actor class to extend
|
|
37
|
+
* @param options - RPC configuration including local/remote schemas and timeout
|
|
38
|
+
* @returns A new class with RPC capabilities mixed in
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```ts
|
|
42
|
+
* const ServerSchema = {
|
|
43
|
+
* methods: {
|
|
44
|
+
* getData: method({
|
|
45
|
+
* input: z.object({}),
|
|
46
|
+
* output: z.object({ data: z.array(z.string()) }),
|
|
47
|
+
* }),
|
|
48
|
+
* },
|
|
49
|
+
* events: {},
|
|
50
|
+
* } as const;
|
|
51
|
+
*
|
|
52
|
+
* class MyDO extends withRpc(Actor, {
|
|
53
|
+
* localSchema: ServerSchema,
|
|
54
|
+
* remoteSchema: ClientSchema,
|
|
55
|
+
* }) {
|
|
56
|
+
* // Required: implement methods from ServerSchema
|
|
57
|
+
* async getData() {
|
|
58
|
+
* return { data: this.dataList };
|
|
59
|
+
* }
|
|
60
|
+
*
|
|
61
|
+
* // Call methods on connected clients
|
|
62
|
+
* async notifyClients() {
|
|
63
|
+
* const results = await this.driver.clientMethod({ info: "update" });
|
|
64
|
+
* }
|
|
65
|
+
* }
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
declare function withRpc<TLocalSchema extends RpcSchema, TRemoteSchema extends RpcSchema, TEnv, TBase extends Constructor<Actor<TEnv>> & {
|
|
69
|
+
prototype: Provider<TLocalSchema>;
|
|
70
|
+
}>(Base: TBase, options: IRpcOptions<TLocalSchema, TRemoteSchema>): RpcActorConstructor<TBase, TLocalSchema, TRemoteSchema>;
|
|
71
|
+
//#endregion
|
|
72
|
+
export { IDOHooks, RpcActorConstructor, withRpc };
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import "../factory-3ziwTuZe.js";
|
|
2
|
+
import "../json-Bshec-bZ.js";
|
|
3
|
+
import "../codecs-BmYG2d_U.js";
|
|
4
|
+
import "../protocol-_mpoOPp6.js";
|
|
5
|
+
import "../errors-5BfreE63.js";
|
|
6
|
+
import { t as SqlPendingCallStorage } from "../sql-CCjc6Bid.js";
|
|
7
|
+
import "../default-BkrMd28n.js";
|
|
8
|
+
import { n as createDurableRpcPeerFactory } from "../durable-MZjkvyS6.js";
|
|
9
|
+
import { t as MultiPeerBase } from "../multi-peer-BAi9yVzp.js";
|
|
10
|
+
|
|
11
|
+
//#region src/adapters/cloudflare-do.ts
|
|
12
|
+
/**
|
|
13
|
+
* Concrete MultiPeerBase for Durable Objects using native WebSocket
|
|
14
|
+
*/
|
|
15
|
+
var DOMultiPeer = class extends MultiPeerBase {
|
|
16
|
+
hooks;
|
|
17
|
+
_createPeer;
|
|
18
|
+
constructor(options) {
|
|
19
|
+
super(options);
|
|
20
|
+
this.hooks = options.hooks ?? {};
|
|
21
|
+
this._createPeer = createDurableRpcPeerFactory({
|
|
22
|
+
actor: options.actor,
|
|
23
|
+
storage: options.storage,
|
|
24
|
+
...options.durableTimeout != null && { durableTimeout: options.durableTimeout }
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Create an RPC peer for a WebSocket connection.
|
|
29
|
+
* Override to use the actor instance as provider via closure.
|
|
30
|
+
*/
|
|
31
|
+
createPeerWithProvider(ws, provider) {
|
|
32
|
+
const peer = this._createPeer({
|
|
33
|
+
ws,
|
|
34
|
+
localSchema: this.localSchema,
|
|
35
|
+
remoteSchema: this.remoteSchema,
|
|
36
|
+
provider,
|
|
37
|
+
onEvent: (event, data) => {
|
|
38
|
+
this.hooks.onEvent?.(peer, event, data);
|
|
39
|
+
},
|
|
40
|
+
timeout: this.timeout
|
|
41
|
+
});
|
|
42
|
+
return peer;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Get or create RPC peer for a WebSocket
|
|
46
|
+
* Handles lazy recreation after hibernation.
|
|
47
|
+
*/
|
|
48
|
+
getOrCreatePeer(ws, provider, isHibernationRecovery = false) {
|
|
49
|
+
let peer = this.getPeerFor(ws);
|
|
50
|
+
if (!peer) {
|
|
51
|
+
peer = this.createPeerWithProvider(ws, provider);
|
|
52
|
+
this.addPeer(ws, peer);
|
|
53
|
+
if (isHibernationRecovery) this.hooks.onPeerRecreated?.(peer, ws);
|
|
54
|
+
}
|
|
55
|
+
return peer;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Connect a new WebSocket and create its peer
|
|
59
|
+
*/
|
|
60
|
+
connectPeer(ws, provider) {
|
|
61
|
+
const peer = this.createPeerWithProvider(ws, provider);
|
|
62
|
+
this.addPeer(ws, peer);
|
|
63
|
+
return peer;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Disconnect a WebSocket and remove its peer
|
|
67
|
+
*/
|
|
68
|
+
disconnectPeer(ws) {
|
|
69
|
+
this.removePeer(ws);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Handle an error on a WebSocket
|
|
73
|
+
*/
|
|
74
|
+
handleError(ws, error) {
|
|
75
|
+
const peer = this.getPeerFor(ws);
|
|
76
|
+
if (peer) {
|
|
77
|
+
peer.close();
|
|
78
|
+
this.removePeer(ws);
|
|
79
|
+
}
|
|
80
|
+
this.hooks.onError?.(peer ?? null, error);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Create a mixin that adds RPC capabilities to a Durable Object Actor.
|
|
85
|
+
*
|
|
86
|
+
* The resulting class requires implementation of all methods defined in
|
|
87
|
+
* `localSchema`. TypeScript enforces this at compile time.
|
|
88
|
+
*
|
|
89
|
+
* @param Base - The Actor class to extend
|
|
90
|
+
* @param options - RPC configuration including local/remote schemas and timeout
|
|
91
|
+
* @returns A new class with RPC capabilities mixed in
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* ```ts
|
|
95
|
+
* const ServerSchema = {
|
|
96
|
+
* methods: {
|
|
97
|
+
* getData: method({
|
|
98
|
+
* input: z.object({}),
|
|
99
|
+
* output: z.object({ data: z.array(z.string()) }),
|
|
100
|
+
* }),
|
|
101
|
+
* },
|
|
102
|
+
* events: {},
|
|
103
|
+
* } as const;
|
|
104
|
+
*
|
|
105
|
+
* class MyDO extends withRpc(Actor, {
|
|
106
|
+
* localSchema: ServerSchema,
|
|
107
|
+
* remoteSchema: ClientSchema,
|
|
108
|
+
* }) {
|
|
109
|
+
* // Required: implement methods from ServerSchema
|
|
110
|
+
* async getData() {
|
|
111
|
+
* return { data: this.dataList };
|
|
112
|
+
* }
|
|
113
|
+
*
|
|
114
|
+
* // Call methods on connected clients
|
|
115
|
+
* async notifyClients() {
|
|
116
|
+
* const results = await this.driver.clientMethod({ info: "update" });
|
|
117
|
+
* }
|
|
118
|
+
* }
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
function withRpc(Base, options) {
|
|
122
|
+
return class RpcActor extends Base {
|
|
123
|
+
__rpc = null;
|
|
124
|
+
/**
|
|
125
|
+
* Internal RPC manager (lazily initialized)
|
|
126
|
+
*
|
|
127
|
+
* Handles peer management, message routing, and durable storage.
|
|
128
|
+
* Uses SQL storage from the Durable Object for hibernation-safe calls.
|
|
129
|
+
*
|
|
130
|
+
* @throws Error if DurableObjectStorage is not available
|
|
131
|
+
*/
|
|
132
|
+
get _rpc() {
|
|
133
|
+
if (!this.storage.raw) throw new Error("DurableObjectStorage not present in actor `raw`");
|
|
134
|
+
return this.__rpc ??= new DOMultiPeer({
|
|
135
|
+
actor: this,
|
|
136
|
+
storage: new SqlPendingCallStorage(this.storage.raw.sql),
|
|
137
|
+
localSchema: options.localSchema,
|
|
138
|
+
remoteSchema: options.remoteSchema,
|
|
139
|
+
provider: this,
|
|
140
|
+
...options.timeout !== void 0 && { timeout: options.timeout }
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Driver for calling methods on connected clients
|
|
145
|
+
*/
|
|
146
|
+
get driver() {
|
|
147
|
+
return this._rpc.driver;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Emit an event to connected clients
|
|
151
|
+
*
|
|
152
|
+
* @param event - Event name from local schema
|
|
153
|
+
* @param data - Event data matching the schema
|
|
154
|
+
* @param ids - Optional array of peer IDs to emit to (broadcasts to all if omitted)
|
|
155
|
+
*/
|
|
156
|
+
emit(event, data, ids) {
|
|
157
|
+
this._rpc.emit(event, data, ids);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Get the number of connected peers
|
|
161
|
+
*/
|
|
162
|
+
getConnectionCount() {
|
|
163
|
+
return this._rpc.getConnectionCount();
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Get the IDs of all connected peers
|
|
167
|
+
*/
|
|
168
|
+
getConnectionIds() {
|
|
169
|
+
return this._rpc.getConnectionIds();
|
|
170
|
+
}
|
|
171
|
+
/** Called by Actor when WebSocket connects */
|
|
172
|
+
onWebSocketConnect(ws, _request) {
|
|
173
|
+
this._rpc.connectPeer(ws, this);
|
|
174
|
+
}
|
|
175
|
+
/** Called by Actor when WebSocket message received (handles hibernation recovery) */
|
|
176
|
+
onWebSocketMessage(ws, message) {
|
|
177
|
+
const existingPeer = this._rpc.getPeerFor(ws);
|
|
178
|
+
this._rpc.getOrCreatePeer(ws, this, !existingPeer).handleMessage(message);
|
|
179
|
+
}
|
|
180
|
+
/** Called by Actor when WebSocket disconnects */
|
|
181
|
+
onWebSocketDisconnect(ws) {
|
|
182
|
+
this._rpc.disconnectPeer(ws);
|
|
183
|
+
}
|
|
184
|
+
/** Called by Actor when WebSocket error occurs */
|
|
185
|
+
onWebSocketError(ws, error) {
|
|
186
|
+
this._rpc.handleError(ws, error);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
//#endregion
|
|
192
|
+
export { withRpc };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import "../schema-CN5HHHku.js";
|
|
2
|
+
import "../factory-C1v0AEHY.js";
|
|
3
|
+
import "../json-54Z2bIIs.js";
|
|
4
|
+
import "../index-Be7jjS77.js";
|
|
5
|
+
import "../protocol-DA84zrc2.js";
|
|
6
|
+
import "../types-Be-qmQu0.js";
|
|
7
|
+
import "../default-xDNNMrg0.js";
|
|
8
|
+
import { n as calculateReconnectDelay, r as defaultReconnectOptions, t as ReconnectOptions } from "../reconnect-DbcN0R_1.js";
|
|
9
|
+
import { IAdapterHooks, IConnectionAdapter, IMultiAdapterHooks, IMultiConnectionAdapter, MultiCallOptions, MultiCallResult, MultiDriver } from "./types.js";
|
|
10
|
+
import { ConnectionState, RpcClient, RpcClientOptions } from "./client.js";
|
|
11
|
+
import { RpcActorConstructor, withRpc } from "./cloudflare-do.js";
|
|
12
|
+
import { n as RpcServerOptions, r as MultiPeerBase, t as RpcServer } from "../server-zTjpJpoX.js";
|
|
13
|
+
export { type ConnectionState, IAdapterHooks, IConnectionAdapter, IMultiAdapterHooks, IMultiConnectionAdapter, MultiCallOptions, MultiCallResult, MultiDriver, MultiPeerBase, ReconnectOptions, type RpcActorConstructor, RpcClient, type RpcClientOptions, RpcServer, type RpcServerOptions, calculateReconnectDelay, defaultReconnectOptions, withRpc };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import "../factory-3ziwTuZe.js";
|
|
2
|
+
import "../json-Bshec-bZ.js";
|
|
3
|
+
import "../codecs-BmYG2d_U.js";
|
|
4
|
+
import "../protocol-_mpoOPp6.js";
|
|
5
|
+
import "../errors-5BfreE63.js";
|
|
6
|
+
import "../sql-CCjc6Bid.js";
|
|
7
|
+
import { n as defaultReconnectOptions, t as calculateReconnectDelay } from "../reconnect-CGAA_1Gf.js";
|
|
8
|
+
import "../default-BkrMd28n.js";
|
|
9
|
+
import "../durable-MZjkvyS6.js";
|
|
10
|
+
import "./types.js";
|
|
11
|
+
import { RpcClient } from "./client.js";
|
|
12
|
+
import { t as MultiPeerBase } from "../multi-peer-BAi9yVzp.js";
|
|
13
|
+
import { withRpc } from "./cloudflare-do.js";
|
|
14
|
+
import { RpcServer } from "./server.js";
|
|
15
|
+
|
|
16
|
+
export { MultiPeerBase, RpcClient, RpcServer, calculateReconnectDelay, defaultReconnectOptions, withRpc };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import "../schema-CN5HHHku.js";
|
|
2
|
+
import "../factory-C1v0AEHY.js";
|
|
3
|
+
import "../json-54Z2bIIs.js";
|
|
4
|
+
import "../index-Be7jjS77.js";
|
|
5
|
+
import "../protocol-DA84zrc2.js";
|
|
6
|
+
import "../types-Be-qmQu0.js";
|
|
7
|
+
import "../default-xDNNMrg0.js";
|
|
8
|
+
import "./types.js";
|
|
9
|
+
import { n as RpcServerOptions, t as RpcServer } from "../server-zTjpJpoX.js";
|
|
10
|
+
export { RpcServer, RpcServerOptions };
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import "../factory-3ziwTuZe.js";
|
|
2
|
+
import "../json-Bshec-bZ.js";
|
|
3
|
+
import "../codecs-BmYG2d_U.js";
|
|
4
|
+
import "../protocol-_mpoOPp6.js";
|
|
5
|
+
import "../errors-5BfreE63.js";
|
|
6
|
+
import { t as WebSocketReadyState } from "../types-D_psiH09.js";
|
|
7
|
+
import { t as RpcPeer } from "../default-BkrMd28n.js";
|
|
8
|
+
import { t as MultiPeerBase } from "../multi-peer-BAi9yVzp.js";
|
|
9
|
+
|
|
10
|
+
//#region src/adapters/server.ts
|
|
11
|
+
/**
|
|
12
|
+
* RPC Server Adapter
|
|
13
|
+
*
|
|
14
|
+
* Manages RpcPeer instances for incoming WebSocket connections.
|
|
15
|
+
* Works with Node.js `ws`, Bun's native WebSocket, or any compatible server.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* RPC Server
|
|
19
|
+
*
|
|
20
|
+
* Manages WebSocket server and client connections with RPC capabilities.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```typescript
|
|
24
|
+
* import { WebSocketServer } from "ws";
|
|
25
|
+
* import { RpcServer } from "@igoforth/ws-rpc/adapters/server";
|
|
26
|
+
*
|
|
27
|
+
* const server = new RpcServer({
|
|
28
|
+
* wss: { port: 8080 },
|
|
29
|
+
* WebSocketServer,
|
|
30
|
+
* localSchema: ServerSchema,
|
|
31
|
+
* remoteSchema: ClientSchema,
|
|
32
|
+
* provider: {
|
|
33
|
+
* getUser: async ({ id }) => ({ name: "John", email: "john@example.com" }),
|
|
34
|
+
* },
|
|
35
|
+
* hooks: {
|
|
36
|
+
* onConnect: (peer) => {
|
|
37
|
+
* console.log(`Client ${peer.id} connected`);
|
|
38
|
+
* peer.driver.ping({}).then(console.log);
|
|
39
|
+
* },
|
|
40
|
+
* onDisconnect: (peer) => console.log(`Client ${peer.id} disconnected`),
|
|
41
|
+
* },
|
|
42
|
+
* });
|
|
43
|
+
*
|
|
44
|
+
* // Emit to all clients
|
|
45
|
+
* server.emit("orderUpdated", { orderId: "123", status: "shipped" });
|
|
46
|
+
*
|
|
47
|
+
* // Graceful shutdown
|
|
48
|
+
* process.on("SIGTERM", () => server.close());
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
var RpcServer = class extends MultiPeerBase {
|
|
52
|
+
wss;
|
|
53
|
+
constructor(options) {
|
|
54
|
+
super({
|
|
55
|
+
localSchema: options.localSchema,
|
|
56
|
+
remoteSchema: options.remoteSchema,
|
|
57
|
+
provider: options.provider,
|
|
58
|
+
...options.timeout !== void 0 && { timeout: options.timeout },
|
|
59
|
+
...options.hooks !== void 0 && { hooks: options.hooks }
|
|
60
|
+
});
|
|
61
|
+
if ("on" in options.wss && typeof options.wss.on === "function") this.wss = options.wss;
|
|
62
|
+
else {
|
|
63
|
+
if (!options.WebSocketServer) throw new Error("WebSocketServer constructor required when passing options");
|
|
64
|
+
this.wss = new options.WebSocketServer(options.wss);
|
|
65
|
+
}
|
|
66
|
+
this.wss.on("connection", (ws) => this.handleConnection(ws));
|
|
67
|
+
this.wss.on("error", (error) => this.hooks.onError?.(null, error));
|
|
68
|
+
this.wss.on("close", () => this.hooks.onClose?.());
|
|
69
|
+
}
|
|
70
|
+
handleConnection(ws) {
|
|
71
|
+
const peer = new RpcPeer({
|
|
72
|
+
ws,
|
|
73
|
+
localSchema: this.localSchema,
|
|
74
|
+
remoteSchema: this.remoteSchema,
|
|
75
|
+
provider: this.provider,
|
|
76
|
+
timeout: this.timeout,
|
|
77
|
+
onEvent: (event, data) => {
|
|
78
|
+
this.hooks.onEvent?.(peer, event, data);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
this.addPeer(ws, peer);
|
|
82
|
+
ws.onmessage = (event) => {
|
|
83
|
+
if (typeof event === "object" && event != null && "data" in event && (typeof event.data === "string" || event.data instanceof ArrayBuffer)) peer.handleMessage(event.data);
|
|
84
|
+
};
|
|
85
|
+
ws.onclose = () => {
|
|
86
|
+
this.removePeer(ws);
|
|
87
|
+
};
|
|
88
|
+
ws.onerror = (event) => {
|
|
89
|
+
const error = event instanceof Error ? event : /* @__PURE__ */ new Error(`WebSocket error for peer ${peer.id}`);
|
|
90
|
+
this.hooks.onError?.(peer, error);
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Close a peer connection with WebSocket close code/reason
|
|
95
|
+
*
|
|
96
|
+
* @param id - Peer ID to close
|
|
97
|
+
* @param code - WebSocket close code (default: 1000)
|
|
98
|
+
* @param reason - Close reason message (default: "Server disconnect")
|
|
99
|
+
* @returns true if peer was found and closed, false otherwise
|
|
100
|
+
*/
|
|
101
|
+
closePeer(id, code = 1e3, reason = "Server disconnect") {
|
|
102
|
+
const entry = this.findPeerEntry(id);
|
|
103
|
+
if (entry) {
|
|
104
|
+
if (entry.connection.readyState !== WebSocketReadyState.CLOSED) entry.connection.close(code, reason);
|
|
105
|
+
return super.closePeer(id);
|
|
106
|
+
}
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Close the server and all client connections
|
|
111
|
+
*
|
|
112
|
+
* @param callback - Optional callback invoked when server is closed
|
|
113
|
+
*/
|
|
114
|
+
close(callback) {
|
|
115
|
+
for (const entry of this.getOpenEntries()) if (entry.connection.readyState !== WebSocketReadyState.CLOSED) entry.connection.close(1001, "Server shutdown");
|
|
116
|
+
this.closeAll();
|
|
117
|
+
this.wss.close(callback);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
//#endregion
|
|
122
|
+
export { RpcServer };
|