@voltkit-rtc/client 0.7.0 → 0.7.2
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/CHANGELOG.md +99 -0
- package/README.md +39 -2
- package/dist/cjs/broadcast.d.ts +21 -2
- package/dist/cjs/broadcast.d.ts.map +1 -1
- package/dist/cjs/broadcast.js +75 -11
- package/dist/cjs/broadcast.js.map +1 -1
- package/dist/cjs/chat.d.ts +13 -0
- package/dist/cjs/chat.d.ts.map +1 -1
- package/dist/cjs/chat.js +65 -9
- package/dist/cjs/chat.js.map +1 -1
- package/dist/cjs/index.d.ts +78 -3
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +269 -61
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/broadcast.d.ts +21 -2
- package/dist/esm/broadcast.d.ts.map +1 -1
- package/dist/esm/broadcast.js +75 -11
- package/dist/esm/broadcast.js.map +1 -1
- package/dist/esm/chat.d.ts +13 -0
- package/dist/esm/chat.d.ts.map +1 -1
- package/dist/esm/chat.js +65 -9
- package/dist/esm/chat.js.map +1 -1
- package/dist/esm/index.d.ts +78 -3
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +269 -61
- package/dist/esm/index.js.map +1 -1
- package/package.json +1 -1
- package/src/broadcast.ts +68 -11
- package/src/chat.ts +63 -8
- package/src/index.ts +266 -56
package/src/chat.ts
CHANGED
|
@@ -93,6 +93,8 @@ interface Pending {
|
|
|
93
93
|
resolve: (v: unknown) => void;
|
|
94
94
|
reject: (e: Error) => void;
|
|
95
95
|
timer: ReturnType<typeof setTimeout>;
|
|
96
|
+
/** The socket it was sent on: only that socket can answer it. */
|
|
97
|
+
ws: WebSocket;
|
|
96
98
|
}
|
|
97
99
|
|
|
98
100
|
const RPC_TIMEOUT_MS = 15_000;
|
|
@@ -112,6 +114,11 @@ export class ChatClient {
|
|
|
112
114
|
private token: string;
|
|
113
115
|
private joinResult: ChatJoinResult | null = null;
|
|
114
116
|
private closedByUs = false;
|
|
117
|
+
/**
|
|
118
|
+
* Bumped by close(). A connect() under way when it ran -- the app's, or a
|
|
119
|
+
* reconnect's -- sees the change after its await and stops.
|
|
120
|
+
*/
|
|
121
|
+
private lifecycle = 0;
|
|
115
122
|
private reconnecting = false;
|
|
116
123
|
private reconnectAttempt = 0;
|
|
117
124
|
/** Set by the first successful join. Only a session that has joined is reconnected. */
|
|
@@ -147,15 +154,23 @@ export class ChatClient {
|
|
|
147
154
|
}
|
|
148
155
|
|
|
149
156
|
async connect(): Promise<ChatJoinResult> {
|
|
157
|
+
const lifecycle = this.lifecycle;
|
|
150
158
|
let result: ChatJoinResult;
|
|
159
|
+
// The socket this call opens. A later connect() replaces it, and this
|
|
160
|
+
// call's failure must then leave the newer socket alone.
|
|
161
|
+
let socket: WebSocket | null = null;
|
|
151
162
|
try {
|
|
152
|
-
|
|
163
|
+
const opening = this.openSocket();
|
|
164
|
+
socket = this.ws;
|
|
165
|
+
await opening;
|
|
153
166
|
result = (await this.rpc('join', { token: this.token })) as ChatJoinResult;
|
|
167
|
+
if (lifecycle !== this.lifecycle) throw new Error('the chat was closed while connecting');
|
|
154
168
|
} catch (err) {
|
|
155
169
|
// Never joined: the app has the error. Left open, the socket would be
|
|
156
170
|
// closed by the server's join deadline, and that would start
|
|
157
171
|
// reconnecting a session that never began.
|
|
158
|
-
|
|
172
|
+
const superseded = this.ws !== null && this.ws !== socket;
|
|
173
|
+
if (!this.joinedOnce && !superseded) this.detachSocket();
|
|
159
174
|
throw err;
|
|
160
175
|
}
|
|
161
176
|
// Joined: the next drop gets every reconnect attempt again. Not when the
|
|
@@ -203,16 +218,30 @@ export class ChatClient {
|
|
|
203
218
|
});
|
|
204
219
|
}
|
|
205
220
|
|
|
206
|
-
private detachSocket(): void {
|
|
221
|
+
private detachSocket(reason = 'superseded', why = 'the connection was replaced'): void {
|
|
207
222
|
const ws = this.ws;
|
|
208
223
|
this.ws = null;
|
|
209
|
-
if (ws
|
|
224
|
+
if (!ws) return;
|
|
225
|
+
if (ws.readyState <= WebSocket.OPEN) {
|
|
210
226
|
try {
|
|
211
|
-
ws.close(1000,
|
|
227
|
+
ws.close(1000, reason);
|
|
212
228
|
} catch {
|
|
213
229
|
/* already closing */
|
|
214
230
|
}
|
|
215
231
|
}
|
|
232
|
+
// Its replies are no longer read: what waits on it fails now instead of
|
|
233
|
+
// waiting out its timeout. Never replayed.
|
|
234
|
+
this.failPending(ws, new Error(why));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* A reconnect that ends the session lets go of its socket: a refused join
|
|
239
|
+
* leaves it open, and it would otherwise stay attached, and sending, after
|
|
240
|
+
* closed.
|
|
241
|
+
*/
|
|
242
|
+
private releaseSocket(): void {
|
|
243
|
+
this.detachSocket('gave up', 'the chat has ended');
|
|
244
|
+
this.failAllPending(new Error('the chat has ended'));
|
|
216
245
|
}
|
|
217
246
|
|
|
218
247
|
/** Single-flight, with backoff: the same shape as the room client's. */
|
|
@@ -224,6 +253,7 @@ export class ChatClient {
|
|
|
224
253
|
this.reconnectAttempt += 1;
|
|
225
254
|
if (this.reconnectAttempt > MAX_RECONNECT_ATTEMPTS) {
|
|
226
255
|
this.emit('error', new Error(`gave up after ${MAX_RECONNECT_ATTEMPTS} reconnect attempts`));
|
|
256
|
+
this.releaseSocket();
|
|
227
257
|
this.closedByUs = true;
|
|
228
258
|
this.emit('closed', 'gave-up');
|
|
229
259
|
return;
|
|
@@ -235,11 +265,15 @@ export class ChatClient {
|
|
|
235
265
|
await this.connect();
|
|
236
266
|
return;
|
|
237
267
|
} catch (err) {
|
|
268
|
+
// Closed while this attempt was under way, and that has been
|
|
269
|
+
// reported: nothing follows closed.
|
|
270
|
+
if (this.closedByUs) return;
|
|
238
271
|
this.emit('error', err as Error);
|
|
239
272
|
// A token that is wrong will be wrong next time too, and an account
|
|
240
273
|
// that ran out stays out until topped up: the registry's terminal
|
|
241
274
|
// codes, shared with StreamClient. Retrying only adds noise.
|
|
242
275
|
if (err instanceof ChatRpcError && isTerminalError(err.code)) {
|
|
276
|
+
this.releaseSocket();
|
|
243
277
|
this.closedByUs = true;
|
|
244
278
|
this.emit('closed', 'gave-up');
|
|
245
279
|
return;
|
|
@@ -252,12 +286,20 @@ export class ChatClient {
|
|
|
252
286
|
}
|
|
253
287
|
|
|
254
288
|
close(): void {
|
|
255
|
-
|
|
289
|
+
// Whatever socket is held is released even when the session has already
|
|
290
|
+
// ended: one opened by a connect() after an earlier close() would
|
|
291
|
+
// otherwise be out of reach. `closed` is still reported once.
|
|
292
|
+
const wasOpen = !this.closedByUs;
|
|
256
293
|
this.closedByUs = true;
|
|
294
|
+
this.lifecycle += 1;
|
|
257
295
|
const ws = this.ws;
|
|
258
296
|
this.ws = null;
|
|
259
297
|
ws?.close(1000, 'client closed');
|
|
260
|
-
|
|
298
|
+
// Detached first, so the socket's own close handler no longer fails what
|
|
299
|
+
// was waiting on it: done here, or a join or a send in flight would wait
|
|
300
|
+
// out its 15 s timeout.
|
|
301
|
+
this.failAllPending(new Error('the chat was closed'));
|
|
302
|
+
if (wasOpen) this.emit('closed', 'left');
|
|
261
303
|
}
|
|
262
304
|
|
|
263
305
|
private rpc(method: string, params: unknown = {}): Promise<unknown> {
|
|
@@ -271,11 +313,21 @@ export class ChatClient {
|
|
|
271
313
|
this.pending.delete(id);
|
|
272
314
|
reject(new Error(`${method} timed out`));
|
|
273
315
|
}, RPC_TIMEOUT_MS);
|
|
274
|
-
this.pending.set(id, { resolve, reject, timer });
|
|
316
|
+
this.pending.set(id, { resolve, reject, timer, ws: this.ws });
|
|
275
317
|
this.ws.send(JSON.stringify({ id, method, params }));
|
|
276
318
|
});
|
|
277
319
|
}
|
|
278
320
|
|
|
321
|
+
/** Fail what was sent on `ws` and is still waiting. */
|
|
322
|
+
private failPending(ws: WebSocket, err: Error): void {
|
|
323
|
+
for (const [id, p] of this.pending) {
|
|
324
|
+
if (p.ws !== ws) continue;
|
|
325
|
+
clearTimeout(p.timer);
|
|
326
|
+
this.pending.delete(id);
|
|
327
|
+
p.reject(err);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
279
331
|
private failAllPending(err: Error): void {
|
|
280
332
|
for (const p of this.pending.values()) {
|
|
281
333
|
clearTimeout(p.timer);
|
|
@@ -291,6 +343,9 @@ export class ChatClient {
|
|
|
291
343
|
} catch {
|
|
292
344
|
return;
|
|
293
345
|
}
|
|
346
|
+
// Valid JSON that is not a message object ("null" would throw below) is
|
|
347
|
+
// ignored like any other frame that is not a message.
|
|
348
|
+
if (typeof msg !== 'object' || msg === null) return;
|
|
294
349
|
if (typeof msg.id === 'number') {
|
|
295
350
|
const p = this.pending.get(msg.id);
|
|
296
351
|
if (!p) return;
|