capnweb-auto-reconnect 0.0.1
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 +87 -0
- package/dist/index.cjs +313 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +70 -0
- package/dist/index.d.ts +70 -0
- package/dist/index.js +312 -0
- package/dist/index.js.map +1 -0
- package/package.json +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 VastBlast
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# capnweb-auto-reconnect
|
|
2
|
+
|
|
3
|
+
Small reconnecting WebSocket RPC session support for [`capnweb`](https://github.com/cloudflare/capnweb).
|
|
4
|
+
|
|
5
|
+
I built this for my personal projects and decided to publish it in case anyone else needs it.
|
|
6
|
+
|
|
7
|
+
- npm: https://www.npmjs.com/package/capnweb-auto-reconnect
|
|
8
|
+
- capnweb: https://github.com/cloudflare/capnweb
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm i capnweb-auto-reconnect
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Quick start
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { ReconnectingWebSocketRpcSession } from "capnweb-auto-reconnect";
|
|
20
|
+
|
|
21
|
+
type MyApi = {
|
|
22
|
+
square(i: number): Promise<number>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const session = new ReconnectingWebSocketRpcSession<MyApi>({
|
|
26
|
+
// `createWebSocket` must return a new socket each attempt.
|
|
27
|
+
createWebSocket: () => new WebSocket("wss://example.com/rpc"),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const rpc = await session.getRPC();
|
|
31
|
+
console.log(await rpc.square(12)); // 144
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## What it does
|
|
35
|
+
|
|
36
|
+
- Keeps a capnweb RPC session connected over WebSocket.
|
|
37
|
+
- Automatically reconnects after disconnects (configurable delay, max delay, and backoff factor).
|
|
38
|
+
- Emits connection lifecycle hooks with connection IDs (`onOpen`/`onClose`).
|
|
39
|
+
- Runs optional one-time startup logic with `onFirstInit` before the first open event.
|
|
40
|
+
- Deduplicates concurrent `start()`/`getRPC()` calls while connecting.
|
|
41
|
+
- Lets you pause/resume reconnecting with `stop()` and `start()`.
|
|
42
|
+
|
|
43
|
+
## Example: lifecycle hooks
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
const session = new ReconnectingWebSocketRpcSession({
|
|
47
|
+
createWebSocket: () => new WebSocket("wss://example.com/rpc"),
|
|
48
|
+
reconnectOptions: { delayMs: 250, maxDelayMs: 5000, backoffFactor: 2 },
|
|
49
|
+
onFirstInit: async rpc => {
|
|
50
|
+
// Runs once after first successful open.
|
|
51
|
+
await rpc.warmup?.();
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const offOpen = session.onOpen(({ connectionId, firstConnection }) => {
|
|
56
|
+
console.log("open", connectionId, { firstConnection });
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const offClose = session.onClose(({ connectionId, intentional, error }) => {
|
|
60
|
+
console.log("close", connectionId, { intentional, error });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Later, when done:
|
|
64
|
+
offOpen();
|
|
65
|
+
offClose();
|
|
66
|
+
session.stop("app shutdown");
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Example: Node + `ws`
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { WebSocket } from "ws";
|
|
73
|
+
import { ReconnectingWebSocketRpcSession } from "capnweb-auto-reconnect";
|
|
74
|
+
|
|
75
|
+
const session = new ReconnectingWebSocketRpcSession({
|
|
76
|
+
createWebSocket: () => new WebSocket("ws://127.0.0.1:8787"),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const rpc = await session.start();
|
|
80
|
+
await rpc.ping?.();
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## API notes
|
|
84
|
+
|
|
85
|
+
- `getRPC()` returns a live stub, connecting/reconnecting as needed.
|
|
86
|
+
- `start()` resumes connection attempts if you previously called `stop()`.
|
|
87
|
+
- `stop(reason?)` closes the current connection and pauses reconnecting.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
let capnweb = require("capnweb");
|
|
3
|
+
|
|
4
|
+
//#region src/helpers.ts
|
|
5
|
+
function closeEventToError(event) {
|
|
6
|
+
if (event.reason) return /* @__PURE__ */ new Error(`WebSocket closed: ${event.code} ${event.reason}`);
|
|
7
|
+
return /* @__PURE__ */ new Error(`WebSocket closed: ${event.code}`);
|
|
8
|
+
}
|
|
9
|
+
function toError(reason, fallbackMessage) {
|
|
10
|
+
if (reason instanceof Error) return reason;
|
|
11
|
+
if (typeof reason === "string" && reason.length > 0) return new Error(reason);
|
|
12
|
+
return new Error(fallbackMessage);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/reconnectingWebsocket.ts
|
|
17
|
+
const READY_STATE_OPEN = 1;
|
|
18
|
+
const READY_STATE_CLOSING = 2;
|
|
19
|
+
const READY_STATE_CLOSED = 3;
|
|
20
|
+
var ConnectionAttemptCancelledError = class extends Error {
|
|
21
|
+
constructor() {
|
|
22
|
+
super("Connection attempt was cancelled by stop or replacement.");
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
/** Reconnecting wrapper around capnweb WebSocket RPC sessions. */
|
|
26
|
+
var ReconnectingWebSocketRpcSession = class {
|
|
27
|
+
#connectPromise;
|
|
28
|
+
#activeConnection;
|
|
29
|
+
#connectionId = 0;
|
|
30
|
+
#lifecycleToken = 0;
|
|
31
|
+
#started = false;
|
|
32
|
+
#stopReason = /* @__PURE__ */ new Error("RPC session stopped.");
|
|
33
|
+
#firstInitDone = false;
|
|
34
|
+
#openedConnectionCount = 0;
|
|
35
|
+
#retryDelayWait;
|
|
36
|
+
#openListeners = /* @__PURE__ */ new Set();
|
|
37
|
+
#closeListeners = /* @__PURE__ */ new Set();
|
|
38
|
+
#reconnect;
|
|
39
|
+
#reconnectDelayMs;
|
|
40
|
+
#reconnectDelayMaxMs;
|
|
41
|
+
#reconnectBackoffFactor;
|
|
42
|
+
#nextReconnectDelayMs;
|
|
43
|
+
constructor(options) {
|
|
44
|
+
this.options = options;
|
|
45
|
+
const reconnectOptions = options.reconnectOptions;
|
|
46
|
+
this.#reconnect = reconnectOptions?.enabled ?? true;
|
|
47
|
+
this.#reconnectDelayMs = reconnectOptions?.delayMs ?? 250;
|
|
48
|
+
this.#reconnectDelayMaxMs = reconnectOptions?.maxDelayMs ?? 5e3;
|
|
49
|
+
this.#reconnectBackoffFactor = reconnectOptions?.backoffFactor ?? 2;
|
|
50
|
+
this.#nextReconnectDelayMs = this.#reconnectDelayMs;
|
|
51
|
+
if (!Number.isFinite(this.#reconnectDelayMs) || this.#reconnectDelayMs < 0) throw new RangeError("reconnectOptions.delayMs must be a finite number >= 0.");
|
|
52
|
+
if (!Number.isFinite(this.#reconnectDelayMaxMs) || this.#reconnectDelayMaxMs < this.#reconnectDelayMs) throw new RangeError("reconnectOptions.maxDelayMs must be >= reconnectOptions.delayMs.");
|
|
53
|
+
if (!Number.isFinite(this.#reconnectBackoffFactor) || this.#reconnectBackoffFactor < 1) throw new RangeError("reconnectOptions.backoffFactor must be a finite number >= 1.");
|
|
54
|
+
this.#started = true;
|
|
55
|
+
queueMicrotask(() => {
|
|
56
|
+
if (!this.#started) return;
|
|
57
|
+
this.#ensureConnected().catch(() => {});
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
/** True when `stop()` has been called and reconnecting is disabled. */
|
|
61
|
+
get isStopped() {
|
|
62
|
+
return !this.#started;
|
|
63
|
+
}
|
|
64
|
+
/** True when a fully-open RPC connection is currently available. */
|
|
65
|
+
get isConnected() {
|
|
66
|
+
const connection = this.#activeConnection;
|
|
67
|
+
return connection !== void 0 && connection.opened && !connection.closed;
|
|
68
|
+
}
|
|
69
|
+
/** Listen for each successful connection open. Returns an unsubscribe function. */
|
|
70
|
+
onOpen(listener) {
|
|
71
|
+
this.#openListeners.add(listener);
|
|
72
|
+
const connection = this.#activeConnection;
|
|
73
|
+
if (connection && connection.opened && !connection.closed) {
|
|
74
|
+
const event = {
|
|
75
|
+
connectionId: connection.id,
|
|
76
|
+
firstConnection: connection.firstConnection,
|
|
77
|
+
rpc: connection.rpc
|
|
78
|
+
};
|
|
79
|
+
try {
|
|
80
|
+
Promise.resolve(listener(event)).catch(() => {});
|
|
81
|
+
} catch {}
|
|
82
|
+
}
|
|
83
|
+
return () => this.#openListeners.delete(listener);
|
|
84
|
+
}
|
|
85
|
+
/** Listen for each opened connection close. Returns an unsubscribe function. */
|
|
86
|
+
onClose(listener) {
|
|
87
|
+
this.#closeListeners.add(listener);
|
|
88
|
+
return () => this.#closeListeners.delete(listener);
|
|
89
|
+
}
|
|
90
|
+
/** Returns a live RPC stub, starting or reconnecting if needed. */
|
|
91
|
+
async getRPC() {
|
|
92
|
+
const connection = this.#activeConnection;
|
|
93
|
+
if (connection && connection.opened && !connection.closed) return connection.rpc;
|
|
94
|
+
return this.start();
|
|
95
|
+
}
|
|
96
|
+
/** Starts (or resumes) connection attempts and resolves when ready. */
|
|
97
|
+
async start() {
|
|
98
|
+
this.#started = true;
|
|
99
|
+
return this.#ensureConnected();
|
|
100
|
+
}
|
|
101
|
+
/** Stops reconnecting and closes the active connection, if any. */
|
|
102
|
+
stop(reason = /* @__PURE__ */ new Error("RPC session was stopped by the application.")) {
|
|
103
|
+
this.#stopReason = reason;
|
|
104
|
+
this.#started = false;
|
|
105
|
+
this.#lifecycleToken++;
|
|
106
|
+
this.#interruptRetryDelay();
|
|
107
|
+
this.#nextReconnectDelayMs = this.#reconnectDelayMs;
|
|
108
|
+
const connection = this.#activeConnection;
|
|
109
|
+
if (connection) this.#disconnectConnection(connection, reason, true);
|
|
110
|
+
this.#connectPromise = void 0;
|
|
111
|
+
}
|
|
112
|
+
#ensureConnected() {
|
|
113
|
+
const connection = this.#activeConnection;
|
|
114
|
+
if (connection && connection.opened && !connection.closed) return Promise.resolve(connection.rpc);
|
|
115
|
+
if (this.#connectPromise) return this.#connectPromise;
|
|
116
|
+
const token = this.#lifecycleToken;
|
|
117
|
+
const promise = this.#connectUntilReady(token);
|
|
118
|
+
this.#connectPromise = promise;
|
|
119
|
+
const clearConnectPromise = () => {
|
|
120
|
+
if (this.#connectPromise === promise) this.#connectPromise = void 0;
|
|
121
|
+
};
|
|
122
|
+
promise.then(clearConnectPromise, clearConnectPromise);
|
|
123
|
+
return promise;
|
|
124
|
+
}
|
|
125
|
+
async #connectUntilReady(token) {
|
|
126
|
+
while (true) {
|
|
127
|
+
if (!this.#started) throw toError(this.#stopReason, "RPC session is stopped.");
|
|
128
|
+
if (token !== this.#lifecycleToken) throw new ConnectionAttemptCancelledError();
|
|
129
|
+
try {
|
|
130
|
+
const rpc = await this.#connectOnce(token);
|
|
131
|
+
if (!this.#started) throw toError(this.#stopReason, "RPC session is stopped.");
|
|
132
|
+
if (token !== this.#lifecycleToken) throw new ConnectionAttemptCancelledError();
|
|
133
|
+
const activeConnection = this.#activeConnection;
|
|
134
|
+
if (!activeConnection || activeConnection.closed || activeConnection.rpc !== rpc) throw new Error("Connection became unavailable before it was returned.");
|
|
135
|
+
this.#nextReconnectDelayMs = this.#reconnectDelayMs;
|
|
136
|
+
return rpc;
|
|
137
|
+
} catch (err) {
|
|
138
|
+
if (!this.#started) throw toError(this.#stopReason, "RPC session is stopped.");
|
|
139
|
+
if (err instanceof ConnectionAttemptCancelledError) throw err;
|
|
140
|
+
if (!this.#reconnect) throw err;
|
|
141
|
+
const delay = this.#nextReconnectDelayMs;
|
|
142
|
+
this.#nextReconnectDelayMs = Math.min(this.#reconnectDelayMaxMs, Math.ceil(this.#nextReconnectDelayMs * this.#reconnectBackoffFactor));
|
|
143
|
+
try {
|
|
144
|
+
await this.#waitForRetryDelay(delay, token);
|
|
145
|
+
} catch (waitError) {
|
|
146
|
+
if (!this.#started) throw toError(this.#stopReason, "RPC session is stopped.");
|
|
147
|
+
throw waitError;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async #connectOnce(token) {
|
|
153
|
+
const webSocket = await this.#createWebSocket();
|
|
154
|
+
if (!this.#started || token !== this.#lifecycleToken) {
|
|
155
|
+
this.#closeWebSocket(webSocket, "Connection attempt was replaced.");
|
|
156
|
+
throw new ConnectionAttemptCancelledError();
|
|
157
|
+
}
|
|
158
|
+
const rpc = (0, capnweb.newWebSocketRpcSession)(webSocket, this.options.localMain, this.options.rpcSessionOptions);
|
|
159
|
+
const connection = this.#installConnection(webSocket, rpc);
|
|
160
|
+
this.#activeConnection = connection;
|
|
161
|
+
const throwIfCancelled = () => {
|
|
162
|
+
if (this.#started && token === this.#lifecycleToken) return;
|
|
163
|
+
const error = new ConnectionAttemptCancelledError();
|
|
164
|
+
this.#disconnectConnection(connection, error, true);
|
|
165
|
+
throw error;
|
|
166
|
+
};
|
|
167
|
+
try {
|
|
168
|
+
await this.#waitUntilSocketOpen(webSocket);
|
|
169
|
+
if (connection.closed) throw new Error("WebSocket connection closed while opening.");
|
|
170
|
+
throwIfCancelled();
|
|
171
|
+
if (!this.#firstInitDone && this.options.onFirstInit) {
|
|
172
|
+
await this.options.onFirstInit(connection.rpc);
|
|
173
|
+
this.#firstInitDone = true;
|
|
174
|
+
}
|
|
175
|
+
if (connection.closed) throw new Error("WebSocket connection closed during initialization.");
|
|
176
|
+
throwIfCancelled();
|
|
177
|
+
connection.opened = true;
|
|
178
|
+
connection.firstConnection = this.#openedConnectionCount === 0;
|
|
179
|
+
this.#openedConnectionCount++;
|
|
180
|
+
this.#emitOpen({
|
|
181
|
+
connectionId: connection.id,
|
|
182
|
+
firstConnection: connection.firstConnection,
|
|
183
|
+
rpc: connection.rpc
|
|
184
|
+
});
|
|
185
|
+
if (connection.closed) throw new Error("WebSocket connection closed during open listeners.");
|
|
186
|
+
throwIfCancelled();
|
|
187
|
+
return connection.rpc;
|
|
188
|
+
} catch (err) {
|
|
189
|
+
this.#disconnectConnection(connection, err, false);
|
|
190
|
+
throw err;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
#installConnection(webSocket, rpc) {
|
|
194
|
+
const connection = {
|
|
195
|
+
id: ++this.#connectionId,
|
|
196
|
+
webSocket,
|
|
197
|
+
rpc,
|
|
198
|
+
firstConnection: false,
|
|
199
|
+
opened: false,
|
|
200
|
+
closed: false,
|
|
201
|
+
removeTransportListeners: () => {}
|
|
202
|
+
};
|
|
203
|
+
const closeListener = (event) => this.#disconnectConnection(connection, closeEventToError(event), false);
|
|
204
|
+
const errorListener = () => this.#disconnectConnection(connection, /* @__PURE__ */ new Error("WebSocket connection failed."), false);
|
|
205
|
+
webSocket.addEventListener("close", closeListener);
|
|
206
|
+
webSocket.addEventListener("error", errorListener);
|
|
207
|
+
connection.removeTransportListeners = () => {
|
|
208
|
+
webSocket.removeEventListener("close", closeListener);
|
|
209
|
+
webSocket.removeEventListener("error", errorListener);
|
|
210
|
+
};
|
|
211
|
+
rpc.onRpcBroken((error) => this.#disconnectConnection(connection, error, false));
|
|
212
|
+
return connection;
|
|
213
|
+
}
|
|
214
|
+
#disconnectConnection(connection, error, intentional) {
|
|
215
|
+
if (connection.closed) return;
|
|
216
|
+
connection.closed = true;
|
|
217
|
+
connection.removeTransportListeners();
|
|
218
|
+
if (this.#activeConnection?.id === connection.id) this.#activeConnection = void 0;
|
|
219
|
+
const wasConnected = connection.opened;
|
|
220
|
+
this.#closeWebSocket(connection.webSocket, "RPC session reconnecting.");
|
|
221
|
+
try {
|
|
222
|
+
connection.rpc[Symbol.dispose]();
|
|
223
|
+
} catch {}
|
|
224
|
+
if (wasConnected) this.#emitClose({
|
|
225
|
+
connectionId: connection.id,
|
|
226
|
+
error,
|
|
227
|
+
intentional,
|
|
228
|
+
wasConnected
|
|
229
|
+
});
|
|
230
|
+
if (wasConnected && !intentional && this.#started && this.#reconnect) this.#ensureConnected().catch(() => {});
|
|
231
|
+
}
|
|
232
|
+
#emitOpen(event) {
|
|
233
|
+
for (const listener of this.#openListeners) try {
|
|
234
|
+
Promise.resolve(listener(event)).catch(() => {});
|
|
235
|
+
} catch {}
|
|
236
|
+
}
|
|
237
|
+
#emitClose(event) {
|
|
238
|
+
for (const listener of this.#closeListeners) try {
|
|
239
|
+
Promise.resolve(listener(event)).catch(() => {});
|
|
240
|
+
} catch {}
|
|
241
|
+
}
|
|
242
|
+
async #createWebSocket() {
|
|
243
|
+
return this.options.createWebSocket();
|
|
244
|
+
}
|
|
245
|
+
async #waitUntilSocketOpen(webSocket) {
|
|
246
|
+
if (webSocket.readyState === READY_STATE_OPEN) return;
|
|
247
|
+
if (webSocket.readyState === READY_STATE_CLOSING || webSocket.readyState === READY_STATE_CLOSED) throw new Error("WebSocket is already closed.");
|
|
248
|
+
await new Promise((resolve, reject) => {
|
|
249
|
+
const openListener = () => {
|
|
250
|
+
cleanup();
|
|
251
|
+
resolve();
|
|
252
|
+
};
|
|
253
|
+
const closeListener = (event) => {
|
|
254
|
+
cleanup();
|
|
255
|
+
reject(closeEventToError(event));
|
|
256
|
+
};
|
|
257
|
+
const errorListener = () => {
|
|
258
|
+
cleanup();
|
|
259
|
+
reject(/* @__PURE__ */ new Error("WebSocket connection failed."));
|
|
260
|
+
};
|
|
261
|
+
const cleanup = () => {
|
|
262
|
+
webSocket.removeEventListener("open", openListener);
|
|
263
|
+
webSocket.removeEventListener("close", closeListener);
|
|
264
|
+
webSocket.removeEventListener("error", errorListener);
|
|
265
|
+
};
|
|
266
|
+
webSocket.addEventListener("open", openListener);
|
|
267
|
+
webSocket.addEventListener("close", closeListener);
|
|
268
|
+
webSocket.addEventListener("error", errorListener);
|
|
269
|
+
if (webSocket.readyState === READY_STATE_OPEN) {
|
|
270
|
+
cleanup();
|
|
271
|
+
resolve();
|
|
272
|
+
} else if (webSocket.readyState === READY_STATE_CLOSING || webSocket.readyState === READY_STATE_CLOSED) {
|
|
273
|
+
cleanup();
|
|
274
|
+
reject(/* @__PURE__ */ new Error("WebSocket is already closed."));
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
async #waitForRetryDelay(delayMs, token) {
|
|
279
|
+
if (delayMs <= 0) {
|
|
280
|
+
if (token !== this.#lifecycleToken) throw new ConnectionAttemptCancelledError();
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
await new Promise((resolve) => {
|
|
284
|
+
const wait = {
|
|
285
|
+
timer: setTimeout(() => {
|
|
286
|
+
if (this.#retryDelayWait !== wait) return;
|
|
287
|
+
this.#retryDelayWait = void 0;
|
|
288
|
+
wait.resolve();
|
|
289
|
+
}, delayMs),
|
|
290
|
+
resolve
|
|
291
|
+
};
|
|
292
|
+
this.#retryDelayWait = wait;
|
|
293
|
+
});
|
|
294
|
+
if (token !== this.#lifecycleToken) throw new ConnectionAttemptCancelledError();
|
|
295
|
+
}
|
|
296
|
+
#interruptRetryDelay() {
|
|
297
|
+
const wait = this.#retryDelayWait;
|
|
298
|
+
if (!wait) return;
|
|
299
|
+
this.#retryDelayWait = void 0;
|
|
300
|
+
clearTimeout(wait.timer);
|
|
301
|
+
wait.resolve();
|
|
302
|
+
}
|
|
303
|
+
#closeWebSocket(webSocket, reason) {
|
|
304
|
+
if (webSocket.readyState === READY_STATE_CLOSING || webSocket.readyState === READY_STATE_CLOSED) return;
|
|
305
|
+
try {
|
|
306
|
+
webSocket.close(3e3, reason.slice(0, 120));
|
|
307
|
+
} catch {}
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
//#endregion
|
|
312
|
+
exports.ReconnectingWebSocketRpcSession = ReconnectingWebSocketRpcSession;
|
|
313
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["#openListeners","#closeListeners","#reconnect","#reconnectDelayMs","#reconnectDelayMaxMs","#reconnectBackoffFactor","#nextReconnectDelayMs","#started","#ensureConnected","#activeConnection","#stopReason","#lifecycleToken","#interruptRetryDelay","#disconnectConnection","#connectPromise","#connectUntilReady","#connectOnce","#waitForRetryDelay","#createWebSocket","#closeWebSocket","#installConnection","#waitUntilSocketOpen","#firstInitDone","#openedConnectionCount","#emitOpen","#connectionId","#emitClose","#retryDelayWait"],"sources":["../src/helpers.ts","../src/reconnectingWebsocket.ts"],"sourcesContent":["export function closeEventToError(event: CloseEvent): Error {\n if (event.reason) return new Error(`WebSocket closed: ${event.code} ${event.reason}`);\n return new Error(`WebSocket closed: ${event.code}`);\n}\n\nexport function toError(reason: unknown, fallbackMessage: string): Error {\n if (reason instanceof Error) return reason;\n if (typeof reason === \"string\" && reason.length > 0) return new Error(reason);\n return new Error(fallbackMessage);\n}\n\nexport type MaybePromise<T> = T | Promise<T>;\n","import { newWebSocketRpcSession, type RpcSessionOptions } from \"capnweb\";\r\nimport { closeEventToError, MaybePromise, toError } from \"./helpers.js\";\r\n\r\ntype DisposableRpcStub = { [Symbol.dispose](): void, onRpcBroken(callback: (error: unknown) => void): void };\ntype RpcShape<T> = T extends object ? { [K in keyof T]: any } : Record<string, any>;\nexport type DynamicRpcStub = DisposableRpcStub & Record<string, any>;\nexport type ReconnectingWebSocketRpc<T = Record<string, never>> = RpcShape<T> & DisposableRpcStub;\ntype WebSocketSource = {\n /**\n * Return a brand-new socket for each connection attempt.\n *\n * @example\n * createWebSocket: () => new WebSocket(\"wss://api.example.com/rpc\")\n */\n createWebSocket: () => WebSocket | Promise<WebSocket>,\n};\n\nexport type ReconnectingWebSocketRpcOpenEvent<T = Record<string, never>> = {\n /** Monotonic connection id for this session instance. */\n connectionId: number,\n /** True only for the first successful connection open. */\n firstConnection: boolean,\n /** Ready RPC stub bound to this opened connection. */\n rpc: ReconnectingWebSocketRpc<T>,\n};\n\nexport type ReconnectingWebSocketRpcCloseEvent = {\n /** Connection id from the matching open event. */\n connectionId: number,\n /** Original disconnect error/cause. */\n error: unknown,\n /** True when closed by `stop()`. */\n intentional: boolean,\n /** True only if this connection reached fully-open state. */\n wasConnected: boolean,\n};\n\nexport type ReconnectingWebSocketRpcReconnectOptions = {\n /** Enable or disable automatic reconnect (default: true). */\n enabled?: boolean,\n /** First retry delay in ms (default: 250). */\n delayMs?: number,\n /** Maximum retry delay in ms (default: 5000). */\n maxDelayMs?: number,\n /** Multiplier applied after each failed attempt (default: 2). */\n backoffFactor?: number,\n};\n\nexport type ReconnectingWebSocketRpcSessionOptions<T = Record<string, never>> = WebSocketSource & {\n /** Local capnweb RPC target exposed to the remote peer. */\n localMain?: any,\n /** Options forwarded to `newWebSocketRpcSession`. */\n rpcSessionOptions?: RpcSessionOptions,\n /** Reconnect/backoff configuration. */\n reconnectOptions?: ReconnectingWebSocketRpcReconnectOptions,\n /** Runs once after the first successful socket open, before onOpen is emitted. */\n onFirstInit?: (rpc: ReconnectingWebSocketRpc<T>) => MaybePromise<void>,\n};\n\r\ntype OpenListener<T> = (event: ReconnectingWebSocketRpcOpenEvent<T>) => MaybePromise<void>;\r\ntype CloseListener = (event: ReconnectingWebSocketRpcCloseEvent) => MaybePromise<void>;\r\n\r\ntype ActiveConnection<T> = {\r\n id: number,\r\n webSocket: WebSocket,\r\n rpc: ReconnectingWebSocketRpc<T>,\r\n firstConnection: boolean,\r\n opened: boolean,\r\n closed: boolean,\r\n removeTransportListeners: () => void,\r\n};\r\n\r\nconst READY_STATE_OPEN = 1;\r\nconst READY_STATE_CLOSING = 2;\r\nconst READY_STATE_CLOSED = 3;\r\n\r\nclass ConnectionAttemptCancelledError extends Error {\n constructor() {\n super(\"Connection attempt was cancelled by stop or replacement.\");\n }\n}\n\n/** Reconnecting wrapper around capnweb WebSocket RPC sessions. */\nexport class ReconnectingWebSocketRpcSession<T = Record<string, never>> {\n #connectPromise?: Promise<ReconnectingWebSocketRpc<T>>;\r\n #activeConnection?: ActiveConnection<T>;\r\n #connectionId = 0;\r\n #lifecycleToken = 0;\r\n #started = false;\r\n #stopReason: unknown = new Error(\"RPC session stopped.\");\r\n #firstInitDone = false;\r\n #openedConnectionCount = 0;\r\n #retryDelayWait?: { timer: ReturnType<typeof setTimeout>, resolve: () => void };\r\n readonly #openListeners = new Set<OpenListener<T>>();\r\n readonly #closeListeners = new Set<CloseListener>();\r\n readonly #reconnect: boolean;\r\n readonly #reconnectDelayMs: number;\r\n readonly #reconnectDelayMaxMs: number;\r\n readonly #reconnectBackoffFactor: number;\r\n #nextReconnectDelayMs: number;\r\n\r\n constructor(readonly options: ReconnectingWebSocketRpcSessionOptions<T>) {\n const reconnectOptions = options.reconnectOptions;\r\n this.#reconnect = reconnectOptions?.enabled ?? true;\r\n this.#reconnectDelayMs = reconnectOptions?.delayMs ?? 250;\r\n this.#reconnectDelayMaxMs = reconnectOptions?.maxDelayMs ?? 5000;\r\n this.#reconnectBackoffFactor = reconnectOptions?.backoffFactor ?? 2;\r\n this.#nextReconnectDelayMs = this.#reconnectDelayMs;\r\n\r\n if (!Number.isFinite(this.#reconnectDelayMs) || this.#reconnectDelayMs < 0) throw new RangeError(\"reconnectOptions.delayMs must be a finite number >= 0.\");\r\n if (!Number.isFinite(this.#reconnectDelayMaxMs) || this.#reconnectDelayMaxMs < this.#reconnectDelayMs) throw new RangeError(\"reconnectOptions.maxDelayMs must be >= reconnectOptions.delayMs.\");\r\n if (!Number.isFinite(this.#reconnectBackoffFactor) || this.#reconnectBackoffFactor < 1) throw new RangeError(\"reconnectOptions.backoffFactor must be a finite number >= 1.\");\r\n\r\n // Start immediately after construction so onOpen/onClose hooks can drive app behavior\r\n // without requiring an initial getRPC() call.\r\n this.#started = true;\r\n queueMicrotask(() => {\r\n if (!this.#started) return;\r\n void this.#ensureConnected().catch(() => { });\r\n });\r\n }\n\n /** True when `stop()` has been called and reconnecting is disabled. */\n get isStopped(): boolean {\n return !this.#started;\n }\n\n /** True when a fully-open RPC connection is currently available. */\n get isConnected(): boolean {\n const connection = this.#activeConnection;\n return connection !== undefined && connection.opened && !connection.closed;\n }\n\n /** Listen for each successful connection open. Returns an unsubscribe function. */\n onOpen(listener: OpenListener<T>): () => void {\n this.#openListeners.add(listener);\n\r\n const connection = this.#activeConnection;\r\n if (connection && connection.opened && !connection.closed) {\r\n const event = { connectionId: connection.id, firstConnection: connection.firstConnection, rpc: connection.rpc };\r\n try {\r\n Promise.resolve(listener(event)).catch(() => { });\r\n } catch { }\r\n }\r\n\r\n return () => this.#openListeners.delete(listener);\n }\n\n /** Listen for each opened connection close. Returns an unsubscribe function. */\n onClose(listener: CloseListener): () => void {\n this.#closeListeners.add(listener);\n return () => this.#closeListeners.delete(listener);\n }\n\n /** Returns a live RPC stub, starting or reconnecting if needed. */\n async getRPC(): Promise<ReconnectingWebSocketRpc<T>> {\n const connection = this.#activeConnection;\n if (connection && connection.opened && !connection.closed) return connection.rpc;\n return this.start();\n }\n\n /** Starts (or resumes) connection attempts and resolves when ready. */\n async start(): Promise<ReconnectingWebSocketRpc<T>> {\n this.#started = true;\n return this.#ensureConnected();\n }\n\n /** Stops reconnecting and closes the active connection, if any. */\n stop(reason: unknown = new Error(\"RPC session was stopped by the application.\")): void {\n this.#stopReason = reason;\n this.#started = false;\n this.#lifecycleToken++;\n this.#interruptRetryDelay();\n this.#nextReconnectDelayMs = this.#reconnectDelayMs;\n const connection = this.#activeConnection;\n if (connection) this.#disconnectConnection(connection, reason, true);\n this.#connectPromise = undefined;\n }\n\r\n #ensureConnected(): Promise<ReconnectingWebSocketRpc<T>> {\r\n const connection = this.#activeConnection;\r\n if (connection && connection.opened && !connection.closed) return Promise.resolve(connection.rpc);\r\n if (this.#connectPromise) return this.#connectPromise;\r\n\r\n const token = this.#lifecycleToken;\r\n const promise = this.#connectUntilReady(token);\r\n this.#connectPromise = promise;\r\n // This promise is shared by concurrent getRPC() calls.\r\n // Clear it on both resolve and reject so future calls can start a fresh attempt.\r\n // The identity check avoids clearing a newer attempt from an older settled promise.\r\n const clearConnectPromise = () => { if (this.#connectPromise === promise) this.#connectPromise = undefined; };\r\n promise.then(clearConnectPromise, clearConnectPromise);\r\n return promise;\r\n }\r\n\r\n async #connectUntilReady(token: number): Promise<ReconnectingWebSocketRpc<T>> {\r\n while (true) {\r\n if (!this.#started) throw toError(this.#stopReason, \"RPC session is stopped.\");\r\n // Any stop() bumps this token and cancels prior in-flight attempts.\r\n if (token !== this.#lifecycleToken) throw new ConnectionAttemptCancelledError();\r\n\r\n try {\r\n const rpc = await this.#connectOnce(token);\r\n if (!this.#started) throw toError(this.#stopReason, \"RPC session is stopped.\");\r\n if (token !== this.#lifecycleToken) throw new ConnectionAttemptCancelledError();\r\n\r\n const activeConnection = this.#activeConnection;\r\n // Guard against races where a connection dies between setup completion and return.\r\n if (!activeConnection || activeConnection.closed || activeConnection.rpc !== rpc) throw new Error(\"Connection became unavailable before it was returned.\");\r\n\r\n this.#nextReconnectDelayMs = this.#reconnectDelayMs;\r\n return rpc;\r\n } catch (err) {\r\n if (!this.#started) throw toError(this.#stopReason, \"RPC session is stopped.\");\r\n if (err instanceof ConnectionAttemptCancelledError) throw err;\r\n if (!this.#reconnect) throw err;\r\n\r\n const delay = this.#nextReconnectDelayMs;\r\n this.#nextReconnectDelayMs = Math.min(this.#reconnectDelayMaxMs, Math.ceil(this.#nextReconnectDelayMs * this.#reconnectBackoffFactor));\r\n\r\n try {\r\n await this.#waitForRetryDelay(delay, token);\r\n } catch (waitError) {\r\n if (!this.#started) throw toError(this.#stopReason, \"RPC session is stopped.\");\r\n throw waitError;\r\n }\r\n }\r\n }\r\n }\r\n\r\n async #connectOnce(token: number): Promise<ReconnectingWebSocketRpc<T>> {\r\n const webSocket = await this.#createWebSocket();\r\n if (!this.#started || token !== this.#lifecycleToken) {\r\n this.#closeWebSocket(webSocket, \"Connection attempt was replaced.\");\r\n throw new ConnectionAttemptCancelledError();\r\n }\r\n\r\n const rpc = newWebSocketRpcSession(webSocket, this.options.localMain, this.options.rpcSessionOptions) as unknown as ReconnectingWebSocketRpc<T>;\r\n const connection = this.#installConnection(webSocket, rpc);\r\n this.#activeConnection = connection;\r\n const throwIfCancelled = () => {\r\n if (this.#started && token === this.#lifecycleToken) return;\r\n const error = new ConnectionAttemptCancelledError();\r\n this.#disconnectConnection(connection, error, true);\r\n throw error;\r\n };\r\n\r\n try {\r\n await this.#waitUntilSocketOpen(webSocket);\r\n if (connection.closed) throw new Error(\"WebSocket connection closed while opening.\");\r\n throwIfCancelled();\r\n\r\n if (!this.#firstInitDone && this.options.onFirstInit) {\r\n await this.options.onFirstInit(connection.rpc);\r\n this.#firstInitDone = true;\r\n }\r\n\r\n if (connection.closed) throw new Error(\"WebSocket connection closed during initialization.\");\r\n throwIfCancelled();\r\n\r\n // Mark as opened only when this connection is fully ready and about to emit onOpen.\r\n connection.opened = true;\r\n connection.firstConnection = this.#openedConnectionCount === 0;\r\n this.#openedConnectionCount++;\r\n // Open listeners are non-blocking; they can kick off background subscription setup.\r\n this.#emitOpen({ connectionId: connection.id, firstConnection: connection.firstConnection, rpc: connection.rpc });\r\n\r\n if (connection.closed) throw new Error(\"WebSocket connection closed during open listeners.\");\r\n throwIfCancelled();\r\n return connection.rpc;\r\n } catch (err) {\r\n this.#disconnectConnection(connection, err, false);\r\n throw err;\r\n }\r\n }\r\n\r\n #installConnection(webSocket: WebSocket, rpc: ReconnectingWebSocketRpc<T>): ActiveConnection<T> {\r\n const connection: ActiveConnection<T> = { id: ++this.#connectionId, webSocket, rpc, firstConnection: false, opened: false, closed: false, removeTransportListeners: () => { } };\r\n const closeListener = (event: CloseEvent) => this.#disconnectConnection(connection, closeEventToError(event), false);\r\n const errorListener = () => this.#disconnectConnection(connection, new Error(\"WebSocket connection failed.\"), false);\r\n\r\n webSocket.addEventListener(\"close\", closeListener);\r\n webSocket.addEventListener(\"error\", errorListener);\r\n connection.removeTransportListeners = () => {\r\n webSocket.removeEventListener(\"close\", closeListener);\r\n webSocket.removeEventListener(\"error\", errorListener);\r\n };\r\n rpc.onRpcBroken(error => this.#disconnectConnection(connection, error, false));\r\n return connection;\r\n }\r\n\r\n #disconnectConnection(connection: ActiveConnection<T>, error: unknown, intentional: boolean) {\r\n if (connection.closed) return;\r\n connection.closed = true;\r\n connection.removeTransportListeners();\r\n if (this.#activeConnection?.id === connection.id) this.#activeConnection = undefined;\r\n const wasConnected = connection.opened;\r\n\r\n this.#closeWebSocket(connection.webSocket, \"RPC session reconnecting.\");\r\n try {\r\n connection.rpc[Symbol.dispose]();\r\n } catch { }\r\n\r\n // onClose is a lifecycle signal (one close per opened connection), not a per-attempt failure signal.\r\n if (wasConnected) this.#emitClose({ connectionId: connection.id, error, intentional, wasConnected });\r\n // Unexpected disconnects trigger reconnect in the background if enabled.\r\n if (wasConnected && !intentional && this.#started && this.#reconnect) void this.#ensureConnected().catch(() => { });\r\n }\r\n\r\n #emitOpen(event: ReconnectingWebSocketRpcOpenEvent<T>): void {\r\n for (const listener of this.#openListeners) {\r\n try {\r\n // Listener failures should not bring down a healthy connection.\r\n Promise.resolve(listener(event)).catch(() => { });\r\n } catch { }\r\n }\r\n }\r\n\r\n #emitClose(event: ReconnectingWebSocketRpcCloseEvent): void {\r\n for (const listener of this.#closeListeners) {\r\n try {\r\n Promise.resolve(listener(event)).catch(() => { });\r\n } catch { }\r\n }\r\n }\r\n\r\n async #createWebSocket(): Promise<WebSocket> {\r\n return this.options.createWebSocket();\r\n }\r\n\r\n async #waitUntilSocketOpen(webSocket: WebSocket): Promise<void> {\r\n if (webSocket.readyState === READY_STATE_OPEN) return;\r\n if (webSocket.readyState === READY_STATE_CLOSING || webSocket.readyState === READY_STATE_CLOSED) throw new Error(\"WebSocket is already closed.\");\r\n\r\n await new Promise<void>((resolve, reject) => {\r\n const openListener = () => {\r\n cleanup();\r\n resolve();\r\n };\r\n const closeListener = (event: CloseEvent) => {\r\n cleanup();\r\n reject(closeEventToError(event));\r\n };\r\n const errorListener = () => {\r\n cleanup();\r\n reject(new Error(\"WebSocket connection failed.\"));\r\n };\r\n const cleanup = () => {\r\n webSocket.removeEventListener(\"open\", openListener);\r\n webSocket.removeEventListener(\"close\", closeListener);\r\n webSocket.removeEventListener(\"error\", errorListener);\r\n };\r\n\r\n webSocket.addEventListener(\"open\", openListener);\r\n webSocket.addEventListener(\"close\", closeListener);\r\n webSocket.addEventListener(\"error\", errorListener);\r\n\r\n // Re-check after listener registration to avoid missing a fast state transition.\r\n if (webSocket.readyState === READY_STATE_OPEN) {\r\n cleanup();\r\n resolve();\r\n } else if (webSocket.readyState === READY_STATE_CLOSING || webSocket.readyState === READY_STATE_CLOSED) {\r\n cleanup();\r\n reject(new Error(\"WebSocket is already closed.\"));\r\n }\r\n });\r\n }\r\n\r\n async #waitForRetryDelay(delayMs: number, token: number): Promise<void> {\r\n if (delayMs <= 0) {\r\n if (token !== this.#lifecycleToken) throw new ConnectionAttemptCancelledError();\r\n return;\r\n }\r\n\r\n await new Promise<void>((resolve) => {\r\n // Keep a single in-flight backoff wait and guard with identity checks so an old timer\r\n // can never clear or resolve a newer wait after stop/start churn.\r\n const wait = {\r\n timer: setTimeout(() => {\r\n if (this.#retryDelayWait !== wait) return;\r\n this.#retryDelayWait = undefined;\r\n wait.resolve();\r\n }, delayMs), resolve\r\n };\r\n this.#retryDelayWait = wait;\r\n });\r\n\r\n // Token might have changed while waiting (stop during backoff).\r\n if (token !== this.#lifecycleToken) throw new ConnectionAttemptCancelledError();\r\n }\r\n\r\n #interruptRetryDelay(): void {\r\n const wait = this.#retryDelayWait;\r\n if (!wait) return;\r\n this.#retryDelayWait = undefined;\r\n clearTimeout(wait.timer);\r\n wait.resolve();\r\n }\r\n\r\n #closeWebSocket(webSocket: WebSocket, reason: string): void {\r\n if (webSocket.readyState === READY_STATE_CLOSING || webSocket.readyState === READY_STATE_CLOSED) return;\r\n try {\r\n webSocket.close(3000, reason.slice(0, 120));\r\n } catch { }\r\n }\r\n}\r\n\r\n"],"mappings":"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"}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { RpcSessionOptions } from "capnweb";
|
|
2
|
+
|
|
3
|
+
//#region src/helpers.d.ts
|
|
4
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
5
|
+
//#endregion
|
|
6
|
+
//#region src/reconnectingWebsocket.d.ts
|
|
7
|
+
type DisposableRpcStub = {
|
|
8
|
+
[Symbol.dispose](): void;
|
|
9
|
+
onRpcBroken(callback: (error: unknown) => void): void;
|
|
10
|
+
};
|
|
11
|
+
type RpcShape<T> = T extends object ? { [K in keyof T]: any } : Record<string, any>;
|
|
12
|
+
type DynamicRpcStub = DisposableRpcStub & Record<string, any>;
|
|
13
|
+
type ReconnectingWebSocketRpc<T = Record<string, never>> = RpcShape<T> & DisposableRpcStub;
|
|
14
|
+
type WebSocketSource = {
|
|
15
|
+
/**
|
|
16
|
+
* Return a brand-new socket for each connection attempt.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* createWebSocket: () => new WebSocket("wss://api.example.com/rpc")
|
|
20
|
+
*/
|
|
21
|
+
createWebSocket: () => WebSocket | Promise<WebSocket>;
|
|
22
|
+
};
|
|
23
|
+
type ReconnectingWebSocketRpcOpenEvent<T = Record<string, never>> = {
|
|
24
|
+
/** Monotonic connection id for this session instance. */connectionId: number; /** True only for the first successful connection open. */
|
|
25
|
+
firstConnection: boolean; /** Ready RPC stub bound to this opened connection. */
|
|
26
|
+
rpc: ReconnectingWebSocketRpc<T>;
|
|
27
|
+
};
|
|
28
|
+
type ReconnectingWebSocketRpcCloseEvent = {
|
|
29
|
+
/** Connection id from the matching open event. */connectionId: number; /** Original disconnect error/cause. */
|
|
30
|
+
error: unknown; /** True when closed by `stop()`. */
|
|
31
|
+
intentional: boolean; /** True only if this connection reached fully-open state. */
|
|
32
|
+
wasConnected: boolean;
|
|
33
|
+
};
|
|
34
|
+
type ReconnectingWebSocketRpcReconnectOptions = {
|
|
35
|
+
/** Enable or disable automatic reconnect (default: true). */enabled?: boolean; /** First retry delay in ms (default: 250). */
|
|
36
|
+
delayMs?: number; /** Maximum retry delay in ms (default: 5000). */
|
|
37
|
+
maxDelayMs?: number; /** Multiplier applied after each failed attempt (default: 2). */
|
|
38
|
+
backoffFactor?: number;
|
|
39
|
+
};
|
|
40
|
+
type ReconnectingWebSocketRpcSessionOptions<T = Record<string, never>> = WebSocketSource & {
|
|
41
|
+
/** Local capnweb RPC target exposed to the remote peer. */localMain?: any; /** Options forwarded to `newWebSocketRpcSession`. */
|
|
42
|
+
rpcSessionOptions?: RpcSessionOptions; /** Reconnect/backoff configuration. */
|
|
43
|
+
reconnectOptions?: ReconnectingWebSocketRpcReconnectOptions; /** Runs once after the first successful socket open, before onOpen is emitted. */
|
|
44
|
+
onFirstInit?: (rpc: ReconnectingWebSocketRpc<T>) => MaybePromise<void>;
|
|
45
|
+
};
|
|
46
|
+
type OpenListener<T> = (event: ReconnectingWebSocketRpcOpenEvent<T>) => MaybePromise<void>;
|
|
47
|
+
type CloseListener = (event: ReconnectingWebSocketRpcCloseEvent) => MaybePromise<void>;
|
|
48
|
+
/** Reconnecting wrapper around capnweb WebSocket RPC sessions. */
|
|
49
|
+
declare class ReconnectingWebSocketRpcSession<T = Record<string, never>> {
|
|
50
|
+
#private;
|
|
51
|
+
readonly options: ReconnectingWebSocketRpcSessionOptions<T>;
|
|
52
|
+
constructor(options: ReconnectingWebSocketRpcSessionOptions<T>);
|
|
53
|
+
/** True when `stop()` has been called and reconnecting is disabled. */
|
|
54
|
+
get isStopped(): boolean;
|
|
55
|
+
/** True when a fully-open RPC connection is currently available. */
|
|
56
|
+
get isConnected(): boolean;
|
|
57
|
+
/** Listen for each successful connection open. Returns an unsubscribe function. */
|
|
58
|
+
onOpen(listener: OpenListener<T>): () => void;
|
|
59
|
+
/** Listen for each opened connection close. Returns an unsubscribe function. */
|
|
60
|
+
onClose(listener: CloseListener): () => void;
|
|
61
|
+
/** Returns a live RPC stub, starting or reconnecting if needed. */
|
|
62
|
+
getRPC(): Promise<ReconnectingWebSocketRpc<T>>;
|
|
63
|
+
/** Starts (or resumes) connection attempts and resolves when ready. */
|
|
64
|
+
start(): Promise<ReconnectingWebSocketRpc<T>>;
|
|
65
|
+
/** Stops reconnecting and closes the active connection, if any. */
|
|
66
|
+
stop(reason?: unknown): void;
|
|
67
|
+
}
|
|
68
|
+
//#endregion
|
|
69
|
+
export { DynamicRpcStub, ReconnectingWebSocketRpc, ReconnectingWebSocketRpcCloseEvent, ReconnectingWebSocketRpcOpenEvent, ReconnectingWebSocketRpcReconnectOptions, ReconnectingWebSocketRpcSession, ReconnectingWebSocketRpcSessionOptions };
|
|
70
|
+
//# sourceMappingURL=index.d.cts.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { RpcSessionOptions } from "capnweb";
|
|
2
|
+
|
|
3
|
+
//#region src/helpers.d.ts
|
|
4
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
5
|
+
//#endregion
|
|
6
|
+
//#region src/reconnectingWebsocket.d.ts
|
|
7
|
+
type DisposableRpcStub = {
|
|
8
|
+
[Symbol.dispose](): void;
|
|
9
|
+
onRpcBroken(callback: (error: unknown) => void): void;
|
|
10
|
+
};
|
|
11
|
+
type RpcShape<T> = T extends object ? { [K in keyof T]: any } : Record<string, any>;
|
|
12
|
+
type DynamicRpcStub = DisposableRpcStub & Record<string, any>;
|
|
13
|
+
type ReconnectingWebSocketRpc<T = Record<string, never>> = RpcShape<T> & DisposableRpcStub;
|
|
14
|
+
type WebSocketSource = {
|
|
15
|
+
/**
|
|
16
|
+
* Return a brand-new socket for each connection attempt.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* createWebSocket: () => new WebSocket("wss://api.example.com/rpc")
|
|
20
|
+
*/
|
|
21
|
+
createWebSocket: () => WebSocket | Promise<WebSocket>;
|
|
22
|
+
};
|
|
23
|
+
type ReconnectingWebSocketRpcOpenEvent<T = Record<string, never>> = {
|
|
24
|
+
/** Monotonic connection id for this session instance. */connectionId: number; /** True only for the first successful connection open. */
|
|
25
|
+
firstConnection: boolean; /** Ready RPC stub bound to this opened connection. */
|
|
26
|
+
rpc: ReconnectingWebSocketRpc<T>;
|
|
27
|
+
};
|
|
28
|
+
type ReconnectingWebSocketRpcCloseEvent = {
|
|
29
|
+
/** Connection id from the matching open event. */connectionId: number; /** Original disconnect error/cause. */
|
|
30
|
+
error: unknown; /** True when closed by `stop()`. */
|
|
31
|
+
intentional: boolean; /** True only if this connection reached fully-open state. */
|
|
32
|
+
wasConnected: boolean;
|
|
33
|
+
};
|
|
34
|
+
type ReconnectingWebSocketRpcReconnectOptions = {
|
|
35
|
+
/** Enable or disable automatic reconnect (default: true). */enabled?: boolean; /** First retry delay in ms (default: 250). */
|
|
36
|
+
delayMs?: number; /** Maximum retry delay in ms (default: 5000). */
|
|
37
|
+
maxDelayMs?: number; /** Multiplier applied after each failed attempt (default: 2). */
|
|
38
|
+
backoffFactor?: number;
|
|
39
|
+
};
|
|
40
|
+
type ReconnectingWebSocketRpcSessionOptions<T = Record<string, never>> = WebSocketSource & {
|
|
41
|
+
/** Local capnweb RPC target exposed to the remote peer. */localMain?: any; /** Options forwarded to `newWebSocketRpcSession`. */
|
|
42
|
+
rpcSessionOptions?: RpcSessionOptions; /** Reconnect/backoff configuration. */
|
|
43
|
+
reconnectOptions?: ReconnectingWebSocketRpcReconnectOptions; /** Runs once after the first successful socket open, before onOpen is emitted. */
|
|
44
|
+
onFirstInit?: (rpc: ReconnectingWebSocketRpc<T>) => MaybePromise<void>;
|
|
45
|
+
};
|
|
46
|
+
type OpenListener<T> = (event: ReconnectingWebSocketRpcOpenEvent<T>) => MaybePromise<void>;
|
|
47
|
+
type CloseListener = (event: ReconnectingWebSocketRpcCloseEvent) => MaybePromise<void>;
|
|
48
|
+
/** Reconnecting wrapper around capnweb WebSocket RPC sessions. */
|
|
49
|
+
declare class ReconnectingWebSocketRpcSession<T = Record<string, never>> {
|
|
50
|
+
#private;
|
|
51
|
+
readonly options: ReconnectingWebSocketRpcSessionOptions<T>;
|
|
52
|
+
constructor(options: ReconnectingWebSocketRpcSessionOptions<T>);
|
|
53
|
+
/** True when `stop()` has been called and reconnecting is disabled. */
|
|
54
|
+
get isStopped(): boolean;
|
|
55
|
+
/** True when a fully-open RPC connection is currently available. */
|
|
56
|
+
get isConnected(): boolean;
|
|
57
|
+
/** Listen for each successful connection open. Returns an unsubscribe function. */
|
|
58
|
+
onOpen(listener: OpenListener<T>): () => void;
|
|
59
|
+
/** Listen for each opened connection close. Returns an unsubscribe function. */
|
|
60
|
+
onClose(listener: CloseListener): () => void;
|
|
61
|
+
/** Returns a live RPC stub, starting or reconnecting if needed. */
|
|
62
|
+
getRPC(): Promise<ReconnectingWebSocketRpc<T>>;
|
|
63
|
+
/** Starts (or resumes) connection attempts and resolves when ready. */
|
|
64
|
+
start(): Promise<ReconnectingWebSocketRpc<T>>;
|
|
65
|
+
/** Stops reconnecting and closes the active connection, if any. */
|
|
66
|
+
stop(reason?: unknown): void;
|
|
67
|
+
}
|
|
68
|
+
//#endregion
|
|
69
|
+
export { DynamicRpcStub, ReconnectingWebSocketRpc, ReconnectingWebSocketRpcCloseEvent, ReconnectingWebSocketRpcOpenEvent, ReconnectingWebSocketRpcReconnectOptions, ReconnectingWebSocketRpcSession, ReconnectingWebSocketRpcSessionOptions };
|
|
70
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { newWebSocketRpcSession } from "capnweb";
|
|
2
|
+
|
|
3
|
+
//#region src/helpers.ts
|
|
4
|
+
function closeEventToError(event) {
|
|
5
|
+
if (event.reason) return /* @__PURE__ */ new Error(`WebSocket closed: ${event.code} ${event.reason}`);
|
|
6
|
+
return /* @__PURE__ */ new Error(`WebSocket closed: ${event.code}`);
|
|
7
|
+
}
|
|
8
|
+
function toError(reason, fallbackMessage) {
|
|
9
|
+
if (reason instanceof Error) return reason;
|
|
10
|
+
if (typeof reason === "string" && reason.length > 0) return new Error(reason);
|
|
11
|
+
return new Error(fallbackMessage);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
//#endregion
|
|
15
|
+
//#region src/reconnectingWebsocket.ts
|
|
16
|
+
const READY_STATE_OPEN = 1;
|
|
17
|
+
const READY_STATE_CLOSING = 2;
|
|
18
|
+
const READY_STATE_CLOSED = 3;
|
|
19
|
+
var ConnectionAttemptCancelledError = class extends Error {
|
|
20
|
+
constructor() {
|
|
21
|
+
super("Connection attempt was cancelled by stop or replacement.");
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
/** Reconnecting wrapper around capnweb WebSocket RPC sessions. */
|
|
25
|
+
var ReconnectingWebSocketRpcSession = class {
|
|
26
|
+
#connectPromise;
|
|
27
|
+
#activeConnection;
|
|
28
|
+
#connectionId = 0;
|
|
29
|
+
#lifecycleToken = 0;
|
|
30
|
+
#started = false;
|
|
31
|
+
#stopReason = /* @__PURE__ */ new Error("RPC session stopped.");
|
|
32
|
+
#firstInitDone = false;
|
|
33
|
+
#openedConnectionCount = 0;
|
|
34
|
+
#retryDelayWait;
|
|
35
|
+
#openListeners = /* @__PURE__ */ new Set();
|
|
36
|
+
#closeListeners = /* @__PURE__ */ new Set();
|
|
37
|
+
#reconnect;
|
|
38
|
+
#reconnectDelayMs;
|
|
39
|
+
#reconnectDelayMaxMs;
|
|
40
|
+
#reconnectBackoffFactor;
|
|
41
|
+
#nextReconnectDelayMs;
|
|
42
|
+
constructor(options) {
|
|
43
|
+
this.options = options;
|
|
44
|
+
const reconnectOptions = options.reconnectOptions;
|
|
45
|
+
this.#reconnect = reconnectOptions?.enabled ?? true;
|
|
46
|
+
this.#reconnectDelayMs = reconnectOptions?.delayMs ?? 250;
|
|
47
|
+
this.#reconnectDelayMaxMs = reconnectOptions?.maxDelayMs ?? 5e3;
|
|
48
|
+
this.#reconnectBackoffFactor = reconnectOptions?.backoffFactor ?? 2;
|
|
49
|
+
this.#nextReconnectDelayMs = this.#reconnectDelayMs;
|
|
50
|
+
if (!Number.isFinite(this.#reconnectDelayMs) || this.#reconnectDelayMs < 0) throw new RangeError("reconnectOptions.delayMs must be a finite number >= 0.");
|
|
51
|
+
if (!Number.isFinite(this.#reconnectDelayMaxMs) || this.#reconnectDelayMaxMs < this.#reconnectDelayMs) throw new RangeError("reconnectOptions.maxDelayMs must be >= reconnectOptions.delayMs.");
|
|
52
|
+
if (!Number.isFinite(this.#reconnectBackoffFactor) || this.#reconnectBackoffFactor < 1) throw new RangeError("reconnectOptions.backoffFactor must be a finite number >= 1.");
|
|
53
|
+
this.#started = true;
|
|
54
|
+
queueMicrotask(() => {
|
|
55
|
+
if (!this.#started) return;
|
|
56
|
+
this.#ensureConnected().catch(() => {});
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
/** True when `stop()` has been called and reconnecting is disabled. */
|
|
60
|
+
get isStopped() {
|
|
61
|
+
return !this.#started;
|
|
62
|
+
}
|
|
63
|
+
/** True when a fully-open RPC connection is currently available. */
|
|
64
|
+
get isConnected() {
|
|
65
|
+
const connection = this.#activeConnection;
|
|
66
|
+
return connection !== void 0 && connection.opened && !connection.closed;
|
|
67
|
+
}
|
|
68
|
+
/** Listen for each successful connection open. Returns an unsubscribe function. */
|
|
69
|
+
onOpen(listener) {
|
|
70
|
+
this.#openListeners.add(listener);
|
|
71
|
+
const connection = this.#activeConnection;
|
|
72
|
+
if (connection && connection.opened && !connection.closed) {
|
|
73
|
+
const event = {
|
|
74
|
+
connectionId: connection.id,
|
|
75
|
+
firstConnection: connection.firstConnection,
|
|
76
|
+
rpc: connection.rpc
|
|
77
|
+
};
|
|
78
|
+
try {
|
|
79
|
+
Promise.resolve(listener(event)).catch(() => {});
|
|
80
|
+
} catch {}
|
|
81
|
+
}
|
|
82
|
+
return () => this.#openListeners.delete(listener);
|
|
83
|
+
}
|
|
84
|
+
/** Listen for each opened connection close. Returns an unsubscribe function. */
|
|
85
|
+
onClose(listener) {
|
|
86
|
+
this.#closeListeners.add(listener);
|
|
87
|
+
return () => this.#closeListeners.delete(listener);
|
|
88
|
+
}
|
|
89
|
+
/** Returns a live RPC stub, starting or reconnecting if needed. */
|
|
90
|
+
async getRPC() {
|
|
91
|
+
const connection = this.#activeConnection;
|
|
92
|
+
if (connection && connection.opened && !connection.closed) return connection.rpc;
|
|
93
|
+
return this.start();
|
|
94
|
+
}
|
|
95
|
+
/** Starts (or resumes) connection attempts and resolves when ready. */
|
|
96
|
+
async start() {
|
|
97
|
+
this.#started = true;
|
|
98
|
+
return this.#ensureConnected();
|
|
99
|
+
}
|
|
100
|
+
/** Stops reconnecting and closes the active connection, if any. */
|
|
101
|
+
stop(reason = /* @__PURE__ */ new Error("RPC session was stopped by the application.")) {
|
|
102
|
+
this.#stopReason = reason;
|
|
103
|
+
this.#started = false;
|
|
104
|
+
this.#lifecycleToken++;
|
|
105
|
+
this.#interruptRetryDelay();
|
|
106
|
+
this.#nextReconnectDelayMs = this.#reconnectDelayMs;
|
|
107
|
+
const connection = this.#activeConnection;
|
|
108
|
+
if (connection) this.#disconnectConnection(connection, reason, true);
|
|
109
|
+
this.#connectPromise = void 0;
|
|
110
|
+
}
|
|
111
|
+
#ensureConnected() {
|
|
112
|
+
const connection = this.#activeConnection;
|
|
113
|
+
if (connection && connection.opened && !connection.closed) return Promise.resolve(connection.rpc);
|
|
114
|
+
if (this.#connectPromise) return this.#connectPromise;
|
|
115
|
+
const token = this.#lifecycleToken;
|
|
116
|
+
const promise = this.#connectUntilReady(token);
|
|
117
|
+
this.#connectPromise = promise;
|
|
118
|
+
const clearConnectPromise = () => {
|
|
119
|
+
if (this.#connectPromise === promise) this.#connectPromise = void 0;
|
|
120
|
+
};
|
|
121
|
+
promise.then(clearConnectPromise, clearConnectPromise);
|
|
122
|
+
return promise;
|
|
123
|
+
}
|
|
124
|
+
async #connectUntilReady(token) {
|
|
125
|
+
while (true) {
|
|
126
|
+
if (!this.#started) throw toError(this.#stopReason, "RPC session is stopped.");
|
|
127
|
+
if (token !== this.#lifecycleToken) throw new ConnectionAttemptCancelledError();
|
|
128
|
+
try {
|
|
129
|
+
const rpc = await this.#connectOnce(token);
|
|
130
|
+
if (!this.#started) throw toError(this.#stopReason, "RPC session is stopped.");
|
|
131
|
+
if (token !== this.#lifecycleToken) throw new ConnectionAttemptCancelledError();
|
|
132
|
+
const activeConnection = this.#activeConnection;
|
|
133
|
+
if (!activeConnection || activeConnection.closed || activeConnection.rpc !== rpc) throw new Error("Connection became unavailable before it was returned.");
|
|
134
|
+
this.#nextReconnectDelayMs = this.#reconnectDelayMs;
|
|
135
|
+
return rpc;
|
|
136
|
+
} catch (err) {
|
|
137
|
+
if (!this.#started) throw toError(this.#stopReason, "RPC session is stopped.");
|
|
138
|
+
if (err instanceof ConnectionAttemptCancelledError) throw err;
|
|
139
|
+
if (!this.#reconnect) throw err;
|
|
140
|
+
const delay = this.#nextReconnectDelayMs;
|
|
141
|
+
this.#nextReconnectDelayMs = Math.min(this.#reconnectDelayMaxMs, Math.ceil(this.#nextReconnectDelayMs * this.#reconnectBackoffFactor));
|
|
142
|
+
try {
|
|
143
|
+
await this.#waitForRetryDelay(delay, token);
|
|
144
|
+
} catch (waitError) {
|
|
145
|
+
if (!this.#started) throw toError(this.#stopReason, "RPC session is stopped.");
|
|
146
|
+
throw waitError;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
async #connectOnce(token) {
|
|
152
|
+
const webSocket = await this.#createWebSocket();
|
|
153
|
+
if (!this.#started || token !== this.#lifecycleToken) {
|
|
154
|
+
this.#closeWebSocket(webSocket, "Connection attempt was replaced.");
|
|
155
|
+
throw new ConnectionAttemptCancelledError();
|
|
156
|
+
}
|
|
157
|
+
const rpc = newWebSocketRpcSession(webSocket, this.options.localMain, this.options.rpcSessionOptions);
|
|
158
|
+
const connection = this.#installConnection(webSocket, rpc);
|
|
159
|
+
this.#activeConnection = connection;
|
|
160
|
+
const throwIfCancelled = () => {
|
|
161
|
+
if (this.#started && token === this.#lifecycleToken) return;
|
|
162
|
+
const error = new ConnectionAttemptCancelledError();
|
|
163
|
+
this.#disconnectConnection(connection, error, true);
|
|
164
|
+
throw error;
|
|
165
|
+
};
|
|
166
|
+
try {
|
|
167
|
+
await this.#waitUntilSocketOpen(webSocket);
|
|
168
|
+
if (connection.closed) throw new Error("WebSocket connection closed while opening.");
|
|
169
|
+
throwIfCancelled();
|
|
170
|
+
if (!this.#firstInitDone && this.options.onFirstInit) {
|
|
171
|
+
await this.options.onFirstInit(connection.rpc);
|
|
172
|
+
this.#firstInitDone = true;
|
|
173
|
+
}
|
|
174
|
+
if (connection.closed) throw new Error("WebSocket connection closed during initialization.");
|
|
175
|
+
throwIfCancelled();
|
|
176
|
+
connection.opened = true;
|
|
177
|
+
connection.firstConnection = this.#openedConnectionCount === 0;
|
|
178
|
+
this.#openedConnectionCount++;
|
|
179
|
+
this.#emitOpen({
|
|
180
|
+
connectionId: connection.id,
|
|
181
|
+
firstConnection: connection.firstConnection,
|
|
182
|
+
rpc: connection.rpc
|
|
183
|
+
});
|
|
184
|
+
if (connection.closed) throw new Error("WebSocket connection closed during open listeners.");
|
|
185
|
+
throwIfCancelled();
|
|
186
|
+
return connection.rpc;
|
|
187
|
+
} catch (err) {
|
|
188
|
+
this.#disconnectConnection(connection, err, false);
|
|
189
|
+
throw err;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
#installConnection(webSocket, rpc) {
|
|
193
|
+
const connection = {
|
|
194
|
+
id: ++this.#connectionId,
|
|
195
|
+
webSocket,
|
|
196
|
+
rpc,
|
|
197
|
+
firstConnection: false,
|
|
198
|
+
opened: false,
|
|
199
|
+
closed: false,
|
|
200
|
+
removeTransportListeners: () => {}
|
|
201
|
+
};
|
|
202
|
+
const closeListener = (event) => this.#disconnectConnection(connection, closeEventToError(event), false);
|
|
203
|
+
const errorListener = () => this.#disconnectConnection(connection, /* @__PURE__ */ new Error("WebSocket connection failed."), false);
|
|
204
|
+
webSocket.addEventListener("close", closeListener);
|
|
205
|
+
webSocket.addEventListener("error", errorListener);
|
|
206
|
+
connection.removeTransportListeners = () => {
|
|
207
|
+
webSocket.removeEventListener("close", closeListener);
|
|
208
|
+
webSocket.removeEventListener("error", errorListener);
|
|
209
|
+
};
|
|
210
|
+
rpc.onRpcBroken((error) => this.#disconnectConnection(connection, error, false));
|
|
211
|
+
return connection;
|
|
212
|
+
}
|
|
213
|
+
#disconnectConnection(connection, error, intentional) {
|
|
214
|
+
if (connection.closed) return;
|
|
215
|
+
connection.closed = true;
|
|
216
|
+
connection.removeTransportListeners();
|
|
217
|
+
if (this.#activeConnection?.id === connection.id) this.#activeConnection = void 0;
|
|
218
|
+
const wasConnected = connection.opened;
|
|
219
|
+
this.#closeWebSocket(connection.webSocket, "RPC session reconnecting.");
|
|
220
|
+
try {
|
|
221
|
+
connection.rpc[Symbol.dispose]();
|
|
222
|
+
} catch {}
|
|
223
|
+
if (wasConnected) this.#emitClose({
|
|
224
|
+
connectionId: connection.id,
|
|
225
|
+
error,
|
|
226
|
+
intentional,
|
|
227
|
+
wasConnected
|
|
228
|
+
});
|
|
229
|
+
if (wasConnected && !intentional && this.#started && this.#reconnect) this.#ensureConnected().catch(() => {});
|
|
230
|
+
}
|
|
231
|
+
#emitOpen(event) {
|
|
232
|
+
for (const listener of this.#openListeners) try {
|
|
233
|
+
Promise.resolve(listener(event)).catch(() => {});
|
|
234
|
+
} catch {}
|
|
235
|
+
}
|
|
236
|
+
#emitClose(event) {
|
|
237
|
+
for (const listener of this.#closeListeners) try {
|
|
238
|
+
Promise.resolve(listener(event)).catch(() => {});
|
|
239
|
+
} catch {}
|
|
240
|
+
}
|
|
241
|
+
async #createWebSocket() {
|
|
242
|
+
return this.options.createWebSocket();
|
|
243
|
+
}
|
|
244
|
+
async #waitUntilSocketOpen(webSocket) {
|
|
245
|
+
if (webSocket.readyState === READY_STATE_OPEN) return;
|
|
246
|
+
if (webSocket.readyState === READY_STATE_CLOSING || webSocket.readyState === READY_STATE_CLOSED) throw new Error("WebSocket is already closed.");
|
|
247
|
+
await new Promise((resolve, reject) => {
|
|
248
|
+
const openListener = () => {
|
|
249
|
+
cleanup();
|
|
250
|
+
resolve();
|
|
251
|
+
};
|
|
252
|
+
const closeListener = (event) => {
|
|
253
|
+
cleanup();
|
|
254
|
+
reject(closeEventToError(event));
|
|
255
|
+
};
|
|
256
|
+
const errorListener = () => {
|
|
257
|
+
cleanup();
|
|
258
|
+
reject(/* @__PURE__ */ new Error("WebSocket connection failed."));
|
|
259
|
+
};
|
|
260
|
+
const cleanup = () => {
|
|
261
|
+
webSocket.removeEventListener("open", openListener);
|
|
262
|
+
webSocket.removeEventListener("close", closeListener);
|
|
263
|
+
webSocket.removeEventListener("error", errorListener);
|
|
264
|
+
};
|
|
265
|
+
webSocket.addEventListener("open", openListener);
|
|
266
|
+
webSocket.addEventListener("close", closeListener);
|
|
267
|
+
webSocket.addEventListener("error", errorListener);
|
|
268
|
+
if (webSocket.readyState === READY_STATE_OPEN) {
|
|
269
|
+
cleanup();
|
|
270
|
+
resolve();
|
|
271
|
+
} else if (webSocket.readyState === READY_STATE_CLOSING || webSocket.readyState === READY_STATE_CLOSED) {
|
|
272
|
+
cleanup();
|
|
273
|
+
reject(/* @__PURE__ */ new Error("WebSocket is already closed."));
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
async #waitForRetryDelay(delayMs, token) {
|
|
278
|
+
if (delayMs <= 0) {
|
|
279
|
+
if (token !== this.#lifecycleToken) throw new ConnectionAttemptCancelledError();
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
await new Promise((resolve) => {
|
|
283
|
+
const wait = {
|
|
284
|
+
timer: setTimeout(() => {
|
|
285
|
+
if (this.#retryDelayWait !== wait) return;
|
|
286
|
+
this.#retryDelayWait = void 0;
|
|
287
|
+
wait.resolve();
|
|
288
|
+
}, delayMs),
|
|
289
|
+
resolve
|
|
290
|
+
};
|
|
291
|
+
this.#retryDelayWait = wait;
|
|
292
|
+
});
|
|
293
|
+
if (token !== this.#lifecycleToken) throw new ConnectionAttemptCancelledError();
|
|
294
|
+
}
|
|
295
|
+
#interruptRetryDelay() {
|
|
296
|
+
const wait = this.#retryDelayWait;
|
|
297
|
+
if (!wait) return;
|
|
298
|
+
this.#retryDelayWait = void 0;
|
|
299
|
+
clearTimeout(wait.timer);
|
|
300
|
+
wait.resolve();
|
|
301
|
+
}
|
|
302
|
+
#closeWebSocket(webSocket, reason) {
|
|
303
|
+
if (webSocket.readyState === READY_STATE_CLOSING || webSocket.readyState === READY_STATE_CLOSED) return;
|
|
304
|
+
try {
|
|
305
|
+
webSocket.close(3e3, reason.slice(0, 120));
|
|
306
|
+
} catch {}
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
//#endregion
|
|
311
|
+
export { ReconnectingWebSocketRpcSession };
|
|
312
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["#openListeners","#closeListeners","#reconnect","#reconnectDelayMs","#reconnectDelayMaxMs","#reconnectBackoffFactor","#nextReconnectDelayMs","#started","#ensureConnected","#activeConnection","#stopReason","#lifecycleToken","#interruptRetryDelay","#disconnectConnection","#connectPromise","#connectUntilReady","#connectOnce","#waitForRetryDelay","#createWebSocket","#closeWebSocket","#installConnection","#waitUntilSocketOpen","#firstInitDone","#openedConnectionCount","#emitOpen","#connectionId","#emitClose","#retryDelayWait"],"sources":["../src/helpers.ts","../src/reconnectingWebsocket.ts"],"sourcesContent":["export function closeEventToError(event: CloseEvent): Error {\n if (event.reason) return new Error(`WebSocket closed: ${event.code} ${event.reason}`);\n return new Error(`WebSocket closed: ${event.code}`);\n}\n\nexport function toError(reason: unknown, fallbackMessage: string): Error {\n if (reason instanceof Error) return reason;\n if (typeof reason === \"string\" && reason.length > 0) return new Error(reason);\n return new Error(fallbackMessage);\n}\n\nexport type MaybePromise<T> = T | Promise<T>;\n","import { newWebSocketRpcSession, type RpcSessionOptions } from \"capnweb\";\r\nimport { closeEventToError, MaybePromise, toError } from \"./helpers.js\";\r\n\r\ntype DisposableRpcStub = { [Symbol.dispose](): void, onRpcBroken(callback: (error: unknown) => void): void };\ntype RpcShape<T> = T extends object ? { [K in keyof T]: any } : Record<string, any>;\nexport type DynamicRpcStub = DisposableRpcStub & Record<string, any>;\nexport type ReconnectingWebSocketRpc<T = Record<string, never>> = RpcShape<T> & DisposableRpcStub;\ntype WebSocketSource = {\n /**\n * Return a brand-new socket for each connection attempt.\n *\n * @example\n * createWebSocket: () => new WebSocket(\"wss://api.example.com/rpc\")\n */\n createWebSocket: () => WebSocket | Promise<WebSocket>,\n};\n\nexport type ReconnectingWebSocketRpcOpenEvent<T = Record<string, never>> = {\n /** Monotonic connection id for this session instance. */\n connectionId: number,\n /** True only for the first successful connection open. */\n firstConnection: boolean,\n /** Ready RPC stub bound to this opened connection. */\n rpc: ReconnectingWebSocketRpc<T>,\n};\n\nexport type ReconnectingWebSocketRpcCloseEvent = {\n /** Connection id from the matching open event. */\n connectionId: number,\n /** Original disconnect error/cause. */\n error: unknown,\n /** True when closed by `stop()`. */\n intentional: boolean,\n /** True only if this connection reached fully-open state. */\n wasConnected: boolean,\n};\n\nexport type ReconnectingWebSocketRpcReconnectOptions = {\n /** Enable or disable automatic reconnect (default: true). */\n enabled?: boolean,\n /** First retry delay in ms (default: 250). */\n delayMs?: number,\n /** Maximum retry delay in ms (default: 5000). */\n maxDelayMs?: number,\n /** Multiplier applied after each failed attempt (default: 2). */\n backoffFactor?: number,\n};\n\nexport type ReconnectingWebSocketRpcSessionOptions<T = Record<string, never>> = WebSocketSource & {\n /** Local capnweb RPC target exposed to the remote peer. */\n localMain?: any,\n /** Options forwarded to `newWebSocketRpcSession`. */\n rpcSessionOptions?: RpcSessionOptions,\n /** Reconnect/backoff configuration. */\n reconnectOptions?: ReconnectingWebSocketRpcReconnectOptions,\n /** Runs once after the first successful socket open, before onOpen is emitted. */\n onFirstInit?: (rpc: ReconnectingWebSocketRpc<T>) => MaybePromise<void>,\n};\n\r\ntype OpenListener<T> = (event: ReconnectingWebSocketRpcOpenEvent<T>) => MaybePromise<void>;\r\ntype CloseListener = (event: ReconnectingWebSocketRpcCloseEvent) => MaybePromise<void>;\r\n\r\ntype ActiveConnection<T> = {\r\n id: number,\r\n webSocket: WebSocket,\r\n rpc: ReconnectingWebSocketRpc<T>,\r\n firstConnection: boolean,\r\n opened: boolean,\r\n closed: boolean,\r\n removeTransportListeners: () => void,\r\n};\r\n\r\nconst READY_STATE_OPEN = 1;\r\nconst READY_STATE_CLOSING = 2;\r\nconst READY_STATE_CLOSED = 3;\r\n\r\nclass ConnectionAttemptCancelledError extends Error {\n constructor() {\n super(\"Connection attempt was cancelled by stop or replacement.\");\n }\n}\n\n/** Reconnecting wrapper around capnweb WebSocket RPC sessions. */\nexport class ReconnectingWebSocketRpcSession<T = Record<string, never>> {\n #connectPromise?: Promise<ReconnectingWebSocketRpc<T>>;\r\n #activeConnection?: ActiveConnection<T>;\r\n #connectionId = 0;\r\n #lifecycleToken = 0;\r\n #started = false;\r\n #stopReason: unknown = new Error(\"RPC session stopped.\");\r\n #firstInitDone = false;\r\n #openedConnectionCount = 0;\r\n #retryDelayWait?: { timer: ReturnType<typeof setTimeout>, resolve: () => void };\r\n readonly #openListeners = new Set<OpenListener<T>>();\r\n readonly #closeListeners = new Set<CloseListener>();\r\n readonly #reconnect: boolean;\r\n readonly #reconnectDelayMs: number;\r\n readonly #reconnectDelayMaxMs: number;\r\n readonly #reconnectBackoffFactor: number;\r\n #nextReconnectDelayMs: number;\r\n\r\n constructor(readonly options: ReconnectingWebSocketRpcSessionOptions<T>) {\n const reconnectOptions = options.reconnectOptions;\r\n this.#reconnect = reconnectOptions?.enabled ?? true;\r\n this.#reconnectDelayMs = reconnectOptions?.delayMs ?? 250;\r\n this.#reconnectDelayMaxMs = reconnectOptions?.maxDelayMs ?? 5000;\r\n this.#reconnectBackoffFactor = reconnectOptions?.backoffFactor ?? 2;\r\n this.#nextReconnectDelayMs = this.#reconnectDelayMs;\r\n\r\n if (!Number.isFinite(this.#reconnectDelayMs) || this.#reconnectDelayMs < 0) throw new RangeError(\"reconnectOptions.delayMs must be a finite number >= 0.\");\r\n if (!Number.isFinite(this.#reconnectDelayMaxMs) || this.#reconnectDelayMaxMs < this.#reconnectDelayMs) throw new RangeError(\"reconnectOptions.maxDelayMs must be >= reconnectOptions.delayMs.\");\r\n if (!Number.isFinite(this.#reconnectBackoffFactor) || this.#reconnectBackoffFactor < 1) throw new RangeError(\"reconnectOptions.backoffFactor must be a finite number >= 1.\");\r\n\r\n // Start immediately after construction so onOpen/onClose hooks can drive app behavior\r\n // without requiring an initial getRPC() call.\r\n this.#started = true;\r\n queueMicrotask(() => {\r\n if (!this.#started) return;\r\n void this.#ensureConnected().catch(() => { });\r\n });\r\n }\n\n /** True when `stop()` has been called and reconnecting is disabled. */\n get isStopped(): boolean {\n return !this.#started;\n }\n\n /** True when a fully-open RPC connection is currently available. */\n get isConnected(): boolean {\n const connection = this.#activeConnection;\n return connection !== undefined && connection.opened && !connection.closed;\n }\n\n /** Listen for each successful connection open. Returns an unsubscribe function. */\n onOpen(listener: OpenListener<T>): () => void {\n this.#openListeners.add(listener);\n\r\n const connection = this.#activeConnection;\r\n if (connection && connection.opened && !connection.closed) {\r\n const event = { connectionId: connection.id, firstConnection: connection.firstConnection, rpc: connection.rpc };\r\n try {\r\n Promise.resolve(listener(event)).catch(() => { });\r\n } catch { }\r\n }\r\n\r\n return () => this.#openListeners.delete(listener);\n }\n\n /** Listen for each opened connection close. Returns an unsubscribe function. */\n onClose(listener: CloseListener): () => void {\n this.#closeListeners.add(listener);\n return () => this.#closeListeners.delete(listener);\n }\n\n /** Returns a live RPC stub, starting or reconnecting if needed. */\n async getRPC(): Promise<ReconnectingWebSocketRpc<T>> {\n const connection = this.#activeConnection;\n if (connection && connection.opened && !connection.closed) return connection.rpc;\n return this.start();\n }\n\n /** Starts (or resumes) connection attempts and resolves when ready. */\n async start(): Promise<ReconnectingWebSocketRpc<T>> {\n this.#started = true;\n return this.#ensureConnected();\n }\n\n /** Stops reconnecting and closes the active connection, if any. */\n stop(reason: unknown = new Error(\"RPC session was stopped by the application.\")): void {\n this.#stopReason = reason;\n this.#started = false;\n this.#lifecycleToken++;\n this.#interruptRetryDelay();\n this.#nextReconnectDelayMs = this.#reconnectDelayMs;\n const connection = this.#activeConnection;\n if (connection) this.#disconnectConnection(connection, reason, true);\n this.#connectPromise = undefined;\n }\n\r\n #ensureConnected(): Promise<ReconnectingWebSocketRpc<T>> {\r\n const connection = this.#activeConnection;\r\n if (connection && connection.opened && !connection.closed) return Promise.resolve(connection.rpc);\r\n if (this.#connectPromise) return this.#connectPromise;\r\n\r\n const token = this.#lifecycleToken;\r\n const promise = this.#connectUntilReady(token);\r\n this.#connectPromise = promise;\r\n // This promise is shared by concurrent getRPC() calls.\r\n // Clear it on both resolve and reject so future calls can start a fresh attempt.\r\n // The identity check avoids clearing a newer attempt from an older settled promise.\r\n const clearConnectPromise = () => { if (this.#connectPromise === promise) this.#connectPromise = undefined; };\r\n promise.then(clearConnectPromise, clearConnectPromise);\r\n return promise;\r\n }\r\n\r\n async #connectUntilReady(token: number): Promise<ReconnectingWebSocketRpc<T>> {\r\n while (true) {\r\n if (!this.#started) throw toError(this.#stopReason, \"RPC session is stopped.\");\r\n // Any stop() bumps this token and cancels prior in-flight attempts.\r\n if (token !== this.#lifecycleToken) throw new ConnectionAttemptCancelledError();\r\n\r\n try {\r\n const rpc = await this.#connectOnce(token);\r\n if (!this.#started) throw toError(this.#stopReason, \"RPC session is stopped.\");\r\n if (token !== this.#lifecycleToken) throw new ConnectionAttemptCancelledError();\r\n\r\n const activeConnection = this.#activeConnection;\r\n // Guard against races where a connection dies between setup completion and return.\r\n if (!activeConnection || activeConnection.closed || activeConnection.rpc !== rpc) throw new Error(\"Connection became unavailable before it was returned.\");\r\n\r\n this.#nextReconnectDelayMs = this.#reconnectDelayMs;\r\n return rpc;\r\n } catch (err) {\r\n if (!this.#started) throw toError(this.#stopReason, \"RPC session is stopped.\");\r\n if (err instanceof ConnectionAttemptCancelledError) throw err;\r\n if (!this.#reconnect) throw err;\r\n\r\n const delay = this.#nextReconnectDelayMs;\r\n this.#nextReconnectDelayMs = Math.min(this.#reconnectDelayMaxMs, Math.ceil(this.#nextReconnectDelayMs * this.#reconnectBackoffFactor));\r\n\r\n try {\r\n await this.#waitForRetryDelay(delay, token);\r\n } catch (waitError) {\r\n if (!this.#started) throw toError(this.#stopReason, \"RPC session is stopped.\");\r\n throw waitError;\r\n }\r\n }\r\n }\r\n }\r\n\r\n async #connectOnce(token: number): Promise<ReconnectingWebSocketRpc<T>> {\r\n const webSocket = await this.#createWebSocket();\r\n if (!this.#started || token !== this.#lifecycleToken) {\r\n this.#closeWebSocket(webSocket, \"Connection attempt was replaced.\");\r\n throw new ConnectionAttemptCancelledError();\r\n }\r\n\r\n const rpc = newWebSocketRpcSession(webSocket, this.options.localMain, this.options.rpcSessionOptions) as unknown as ReconnectingWebSocketRpc<T>;\r\n const connection = this.#installConnection(webSocket, rpc);\r\n this.#activeConnection = connection;\r\n const throwIfCancelled = () => {\r\n if (this.#started && token === this.#lifecycleToken) return;\r\n const error = new ConnectionAttemptCancelledError();\r\n this.#disconnectConnection(connection, error, true);\r\n throw error;\r\n };\r\n\r\n try {\r\n await this.#waitUntilSocketOpen(webSocket);\r\n if (connection.closed) throw new Error(\"WebSocket connection closed while opening.\");\r\n throwIfCancelled();\r\n\r\n if (!this.#firstInitDone && this.options.onFirstInit) {\r\n await this.options.onFirstInit(connection.rpc);\r\n this.#firstInitDone = true;\r\n }\r\n\r\n if (connection.closed) throw new Error(\"WebSocket connection closed during initialization.\");\r\n throwIfCancelled();\r\n\r\n // Mark as opened only when this connection is fully ready and about to emit onOpen.\r\n connection.opened = true;\r\n connection.firstConnection = this.#openedConnectionCount === 0;\r\n this.#openedConnectionCount++;\r\n // Open listeners are non-blocking; they can kick off background subscription setup.\r\n this.#emitOpen({ connectionId: connection.id, firstConnection: connection.firstConnection, rpc: connection.rpc });\r\n\r\n if (connection.closed) throw new Error(\"WebSocket connection closed during open listeners.\");\r\n throwIfCancelled();\r\n return connection.rpc;\r\n } catch (err) {\r\n this.#disconnectConnection(connection, err, false);\r\n throw err;\r\n }\r\n }\r\n\r\n #installConnection(webSocket: WebSocket, rpc: ReconnectingWebSocketRpc<T>): ActiveConnection<T> {\r\n const connection: ActiveConnection<T> = { id: ++this.#connectionId, webSocket, rpc, firstConnection: false, opened: false, closed: false, removeTransportListeners: () => { } };\r\n const closeListener = (event: CloseEvent) => this.#disconnectConnection(connection, closeEventToError(event), false);\r\n const errorListener = () => this.#disconnectConnection(connection, new Error(\"WebSocket connection failed.\"), false);\r\n\r\n webSocket.addEventListener(\"close\", closeListener);\r\n webSocket.addEventListener(\"error\", errorListener);\r\n connection.removeTransportListeners = () => {\r\n webSocket.removeEventListener(\"close\", closeListener);\r\n webSocket.removeEventListener(\"error\", errorListener);\r\n };\r\n rpc.onRpcBroken(error => this.#disconnectConnection(connection, error, false));\r\n return connection;\r\n }\r\n\r\n #disconnectConnection(connection: ActiveConnection<T>, error: unknown, intentional: boolean) {\r\n if (connection.closed) return;\r\n connection.closed = true;\r\n connection.removeTransportListeners();\r\n if (this.#activeConnection?.id === connection.id) this.#activeConnection = undefined;\r\n const wasConnected = connection.opened;\r\n\r\n this.#closeWebSocket(connection.webSocket, \"RPC session reconnecting.\");\r\n try {\r\n connection.rpc[Symbol.dispose]();\r\n } catch { }\r\n\r\n // onClose is a lifecycle signal (one close per opened connection), not a per-attempt failure signal.\r\n if (wasConnected) this.#emitClose({ connectionId: connection.id, error, intentional, wasConnected });\r\n // Unexpected disconnects trigger reconnect in the background if enabled.\r\n if (wasConnected && !intentional && this.#started && this.#reconnect) void this.#ensureConnected().catch(() => { });\r\n }\r\n\r\n #emitOpen(event: ReconnectingWebSocketRpcOpenEvent<T>): void {\r\n for (const listener of this.#openListeners) {\r\n try {\r\n // Listener failures should not bring down a healthy connection.\r\n Promise.resolve(listener(event)).catch(() => { });\r\n } catch { }\r\n }\r\n }\r\n\r\n #emitClose(event: ReconnectingWebSocketRpcCloseEvent): void {\r\n for (const listener of this.#closeListeners) {\r\n try {\r\n Promise.resolve(listener(event)).catch(() => { });\r\n } catch { }\r\n }\r\n }\r\n\r\n async #createWebSocket(): Promise<WebSocket> {\r\n return this.options.createWebSocket();\r\n }\r\n\r\n async #waitUntilSocketOpen(webSocket: WebSocket): Promise<void> {\r\n if (webSocket.readyState === READY_STATE_OPEN) return;\r\n if (webSocket.readyState === READY_STATE_CLOSING || webSocket.readyState === READY_STATE_CLOSED) throw new Error(\"WebSocket is already closed.\");\r\n\r\n await new Promise<void>((resolve, reject) => {\r\n const openListener = () => {\r\n cleanup();\r\n resolve();\r\n };\r\n const closeListener = (event: CloseEvent) => {\r\n cleanup();\r\n reject(closeEventToError(event));\r\n };\r\n const errorListener = () => {\r\n cleanup();\r\n reject(new Error(\"WebSocket connection failed.\"));\r\n };\r\n const cleanup = () => {\r\n webSocket.removeEventListener(\"open\", openListener);\r\n webSocket.removeEventListener(\"close\", closeListener);\r\n webSocket.removeEventListener(\"error\", errorListener);\r\n };\r\n\r\n webSocket.addEventListener(\"open\", openListener);\r\n webSocket.addEventListener(\"close\", closeListener);\r\n webSocket.addEventListener(\"error\", errorListener);\r\n\r\n // Re-check after listener registration to avoid missing a fast state transition.\r\n if (webSocket.readyState === READY_STATE_OPEN) {\r\n cleanup();\r\n resolve();\r\n } else if (webSocket.readyState === READY_STATE_CLOSING || webSocket.readyState === READY_STATE_CLOSED) {\r\n cleanup();\r\n reject(new Error(\"WebSocket is already closed.\"));\r\n }\r\n });\r\n }\r\n\r\n async #waitForRetryDelay(delayMs: number, token: number): Promise<void> {\r\n if (delayMs <= 0) {\r\n if (token !== this.#lifecycleToken) throw new ConnectionAttemptCancelledError();\r\n return;\r\n }\r\n\r\n await new Promise<void>((resolve) => {\r\n // Keep a single in-flight backoff wait and guard with identity checks so an old timer\r\n // can never clear or resolve a newer wait after stop/start churn.\r\n const wait = {\r\n timer: setTimeout(() => {\r\n if (this.#retryDelayWait !== wait) return;\r\n this.#retryDelayWait = undefined;\r\n wait.resolve();\r\n }, delayMs), resolve\r\n };\r\n this.#retryDelayWait = wait;\r\n });\r\n\r\n // Token might have changed while waiting (stop during backoff).\r\n if (token !== this.#lifecycleToken) throw new ConnectionAttemptCancelledError();\r\n }\r\n\r\n #interruptRetryDelay(): void {\r\n const wait = this.#retryDelayWait;\r\n if (!wait) return;\r\n this.#retryDelayWait = undefined;\r\n clearTimeout(wait.timer);\r\n wait.resolve();\r\n }\r\n\r\n #closeWebSocket(webSocket: WebSocket, reason: string): void {\r\n if (webSocket.readyState === READY_STATE_CLOSING || webSocket.readyState === READY_STATE_CLOSED) return;\r\n try {\r\n webSocket.close(3000, reason.slice(0, 120));\r\n } catch { }\r\n }\r\n}\r\n\r\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "capnweb-auto-reconnect",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Reconnecting WebSocket RPC session support for capnweb.",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"build": "tsdown",
|
|
7
|
+
"check:types": "tsgo --noEmit",
|
|
8
|
+
"lint": "oxlint --type-aware",
|
|
9
|
+
"check": "npm run lint && npm run check:types",
|
|
10
|
+
"test": "vitest run",
|
|
11
|
+
"prepublishOnly": "npm run check && npm run test && npm run build"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"capnweb",
|
|
15
|
+
"rpc",
|
|
16
|
+
"websocket",
|
|
17
|
+
"reconnect"
|
|
18
|
+
],
|
|
19
|
+
"author": "VastBlast",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"type": "module",
|
|
22
|
+
"sideEffects": false,
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/ws": "^8.18.1",
|
|
25
|
+
"@typescript/native-preview": "^7.0.0-dev.20260216.1",
|
|
26
|
+
"oxlint": "^1.48.0",
|
|
27
|
+
"oxlint-tsgolint": "^0.14.0",
|
|
28
|
+
"tsdown": "^0.20.3",
|
|
29
|
+
"typescript": "^5.9.3",
|
|
30
|
+
"vitest": "^4.0.18",
|
|
31
|
+
"ws": "^8.19.0"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"capnweb": "^0.4.0"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist"
|
|
38
|
+
],
|
|
39
|
+
"main": "./dist/index.cjs",
|
|
40
|
+
"module": "./dist/index.js",
|
|
41
|
+
"types": "./dist/index.d.cts",
|
|
42
|
+
"exports": {
|
|
43
|
+
".": {
|
|
44
|
+
"import": "./dist/index.js",
|
|
45
|
+
"require": "./dist/index.cjs"
|
|
46
|
+
},
|
|
47
|
+
"./package.json": "./package.json"
|
|
48
|
+
}
|
|
49
|
+
}
|