@jusi/light-im-sdk 0.1.0-alpha.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 John Shao
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,65 @@
1
+ # @jusi/light-im-sdk
2
+
3
+ TypeScript SDK for [jusi-light-im](https://github.com/John-Shao/jusi-light-im) — a lightweight self-hosted IM service designed to complement [we-meet](https://github.com/John-Shao/we-meet) and similar real-time collaboration apps.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @jusi/light-im-sdk
9
+ ```
10
+
11
+ Supports modern browsers + Node 18+ (ESM and CJS bundles ship side-by-side).
12
+
13
+ ## Usage
14
+
15
+ ```ts
16
+ import { Client } from '@jusi/light-im-sdk'
17
+
18
+ const client = new Client({
19
+ baseURL: 'https://im.example.com',
20
+ tokenProvider: () =>
21
+ fetch('/api/im/token', { method: 'POST', credentials: 'include' })
22
+ .then((r) => r.json())
23
+ .then((d) => d.token),
24
+ })
25
+
26
+ await client.connect()
27
+ client.onMessage((m) => console.log('msg', m))
28
+ const ack = await client.sendText('cid-XYZ', 'hello')
29
+
30
+ const convs = await client.listConversations()
31
+ const { messages } = await client.loadHistory('cid-XYZ')
32
+ await client.markRead('cid-XYZ', ack.seq)
33
+ ```
34
+
35
+ State machine:
36
+
37
+ ```
38
+ disconnected → connecting → connected ⇄ reconnecting
39
+
40
+ auth_failed (terminal until explicit connect())
41
+ ```
42
+
43
+ ## Companion server
44
+
45
+ This SDK targets [jusi-light-im](https://github.com/John-Shao/jusi-light-im) — a single Go binary you can host alongside any Postgres + Redis pair.
46
+
47
+ | Capability | API surface |
48
+ |---|---|
49
+ | REST | `GET /v1/conversations`, `GET /v1/messages`, `POST /v1/read` |
50
+ | WebSocket | `GET /v1/ws?token=<jwt>` (text frames, JSON) |
51
+ | Server-side admin | HMAC-signed `/admin/*` for bridge integrations |
52
+
53
+ The `tokenProvider` you pass to `Client` is expected to return a bearer JWT minted by the server's `POST /admin/tokens/issue` endpoint (typically called server-side from your host app).
54
+
55
+ ## Scripts
56
+
57
+ | Command | Purpose |
58
+ |---|---|
59
+ | `npm run build` | Bundle CJS + ESM + `.d.ts` to `dist/` via tsup |
60
+ | `npm test` | Unit tests via Vitest |
61
+ | `npm run typecheck` | `tsc --noEmit` |
62
+
63
+ ## License
64
+
65
+ [MIT](https://github.com/John-Shao/jusi-light-im/blob/main/LICENSE) © 2026 John Shao
package/dist/index.cjs ADDED
@@ -0,0 +1,608 @@
1
+ 'use strict';
2
+
3
+ // src/types.ts
4
+ var FrameType = {
5
+ Echo: "echo",
6
+ System: "system",
7
+ Msg: "msg",
8
+ Ack: "ack",
9
+ Read: "read"
10
+ };
11
+
12
+ // src/errors.ts
13
+ var IMError = class extends Error {
14
+ constructor(message, cause) {
15
+ super(message, cause !== void 0 ? { cause } : void 0);
16
+ this.name = "IMError";
17
+ }
18
+ };
19
+ var AuthError = class extends IMError {
20
+ constructor(message, cause) {
21
+ super(message, cause);
22
+ this.name = "AuthError";
23
+ }
24
+ };
25
+ var NetworkError = class extends IMError {
26
+ constructor(message, cause) {
27
+ super(message, cause);
28
+ this.name = "NetworkError";
29
+ }
30
+ };
31
+ var ProtocolError = class extends IMError {
32
+ constructor(message, cause) {
33
+ super(message, cause);
34
+ this.name = "ProtocolError";
35
+ }
36
+ };
37
+ var TimeoutError = class extends IMError {
38
+ constructor(message, cause) {
39
+ super(message, cause);
40
+ this.name = "TimeoutError";
41
+ }
42
+ };
43
+
44
+ // src/events.ts
45
+ var Emitter = class {
46
+ listeners = /* @__PURE__ */ new Set();
47
+ on(cb) {
48
+ this.listeners.add(cb);
49
+ return () => {
50
+ this.listeners.delete(cb);
51
+ };
52
+ }
53
+ emit(event) {
54
+ for (const cb of this.listeners) {
55
+ try {
56
+ cb(event);
57
+ } catch (e) {
58
+ console.error("[jusi-light-im-sdk] listener threw", e);
59
+ }
60
+ }
61
+ }
62
+ clear() {
63
+ this.listeners.clear();
64
+ }
65
+ get size() {
66
+ return this.listeners.size;
67
+ }
68
+ };
69
+
70
+ // src/backoff.ts
71
+ var Backoff = class _Backoff {
72
+ attempt = 0;
73
+ opts;
74
+ constructor(opts) {
75
+ this.opts = opts;
76
+ }
77
+ static create(opts = {}) {
78
+ return new _Backoff({
79
+ baseMs: opts.baseMs ?? 1e3,
80
+ maxMs: opts.maxMs ?? 3e4,
81
+ factor: opts.factor ?? 2,
82
+ jitter: opts.jitter ?? 0.3,
83
+ random: opts.random ?? Math.random
84
+ });
85
+ }
86
+ /**
87
+ * Compute the next delay (in ms) and increment the internal attempt counter.
88
+ *
89
+ * Caller is responsible for sleeping; this method just returns the number.
90
+ */
91
+ next() {
92
+ const exp = this.opts.baseMs * Math.pow(this.opts.factor, this.attempt);
93
+ const raw = Math.min(exp, this.opts.maxMs);
94
+ this.attempt += 1;
95
+ if (this.opts.jitter <= 0) return raw;
96
+ const lo = 1 - this.opts.jitter;
97
+ const span = this.opts.jitter * 2;
98
+ const factor = lo + this.opts.random() * span;
99
+ return Math.max(0, Math.round(raw * factor));
100
+ }
101
+ /** Reset attempts to 0; the next call to next() yields baseMs again. */
102
+ reset() {
103
+ this.attempt = 0;
104
+ }
105
+ /** Number of times next() has been called since the last reset. */
106
+ get attempts() {
107
+ return this.attempt;
108
+ }
109
+ };
110
+
111
+ // src/rest.ts
112
+ var RestClient = class {
113
+ baseURL;
114
+ tokenProvider;
115
+ fetchImpl;
116
+ cachedToken = null;
117
+ constructor(opts) {
118
+ if (!opts.baseURL) throw new Error("RestClient: baseURL required");
119
+ if (!opts.tokenProvider) throw new Error("RestClient: tokenProvider required");
120
+ this.baseURL = opts.baseURL.replace(/\/$/, "");
121
+ this.tokenProvider = opts.tokenProvider;
122
+ this.fetchImpl = opts.fetch ?? ((input, init) => globalThis.fetch(input, init));
123
+ }
124
+ /** Drop the cached token; next request will call tokenProvider again. */
125
+ invalidateToken() {
126
+ this.cachedToken = null;
127
+ }
128
+ // ---- public endpoints ----
129
+ async listConversations() {
130
+ return this.request("GET", "/v1/conversations");
131
+ }
132
+ async listMessages(cid, opts = {}) {
133
+ if (!cid) throw new Error("listMessages: cid required");
134
+ const params = new URLSearchParams({ cid });
135
+ if (opts.beforeSeq !== void 0) params.set("before_seq", String(opts.beforeSeq));
136
+ if (opts.limit !== void 0) params.set("limit", String(opts.limit));
137
+ return this.request("GET", `/v1/messages?${params.toString()}`);
138
+ }
139
+ async markRead(cid, seq) {
140
+ if (!cid) throw new Error("markRead: cid required");
141
+ if (!Number.isFinite(seq) || seq <= 0) throw new Error("markRead: positive seq required");
142
+ await this.request("POST", "/v1/read", { cid, seq });
143
+ }
144
+ // ---- internals ----
145
+ async getToken() {
146
+ if (this.cachedToken) return this.cachedToken;
147
+ try {
148
+ const t = await this.tokenProvider();
149
+ if (!t) throw new Error("tokenProvider returned empty token");
150
+ this.cachedToken = t;
151
+ return t;
152
+ } catch (e) {
153
+ throw new AuthError("tokenProvider failed", e);
154
+ }
155
+ }
156
+ async request(method, path, body) {
157
+ const url = this.baseURL + path;
158
+ const res = await this.doFetch(url, method, body, await this.getToken());
159
+ if (res.status === 401) {
160
+ this.invalidateToken();
161
+ const fresh = await this.getToken();
162
+ const res2 = await this.doFetch(url, method, body, fresh);
163
+ if (res2.status === 401) {
164
+ throw new AuthError("unauthorized after token refresh");
165
+ }
166
+ return this.parseBody(res2);
167
+ }
168
+ return this.parseBody(res);
169
+ }
170
+ async doFetch(url, method, body, token) {
171
+ const headers = {
172
+ Authorization: `Bearer ${token}`
173
+ };
174
+ let payload;
175
+ if (body !== void 0) {
176
+ headers["Content-Type"] = "application/json";
177
+ payload = JSON.stringify(body);
178
+ }
179
+ try {
180
+ return await this.fetchImpl(url, { method, headers, body: payload });
181
+ } catch (e) {
182
+ throw new NetworkError(`${method} ${url} failed`, e);
183
+ }
184
+ }
185
+ async parseBody(res) {
186
+ if (res.status === 204) {
187
+ return void 0;
188
+ }
189
+ if (res.status >= 500) {
190
+ throw new NetworkError(`${res.status} ${res.statusText}`);
191
+ }
192
+ if (res.status >= 400) {
193
+ const detail = await res.text().catch(() => "");
194
+ throw new ProtocolError(`${res.status} ${res.statusText}: ${detail}`);
195
+ }
196
+ try {
197
+ return await res.json();
198
+ } catch (e) {
199
+ throw new ProtocolError("response not valid JSON", e);
200
+ }
201
+ }
202
+ };
203
+
204
+ // src/ws.ts
205
+ var DEFAULT_ACK_TIMEOUT_MS = 1e4;
206
+ var WsClient = class {
207
+ baseURL;
208
+ wsFactory;
209
+ ackTimeoutMs;
210
+ ws = null;
211
+ pendingAcks = /* @__PURE__ */ new Map();
212
+ idCounter = 0;
213
+ openEmit = new Emitter();
214
+ closeEmit = new Emitter();
215
+ frameEmit = new Emitter();
216
+ errorEmit = new Emitter();
217
+ constructor(opts) {
218
+ if (!opts.url) throw new Error("WsClient: url required");
219
+ this.baseURL = opts.url;
220
+ this.wsFactory = opts.wsFactory ?? ((u) => new WebSocket(u));
221
+ this.ackTimeoutMs = opts.ackTimeoutMs ?? DEFAULT_ACK_TIMEOUT_MS;
222
+ }
223
+ /** True when the underlying socket is OPEN. */
224
+ get isOpen() {
225
+ return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
226
+ }
227
+ /**
228
+ * Open a WebSocket with the given token appended as ?token=. Resolves when the
229
+ * native onopen fires; rejects on transport error (does NOT wait for the
230
+ * server-side "connected" system frame — that's Client's higher-level concern).
231
+ */
232
+ connect(token) {
233
+ return new Promise((resolve, reject) => {
234
+ const sep = this.baseURL.includes("?") ? "&" : "?";
235
+ const url = this.baseURL + sep + "token=" + encodeURIComponent(token);
236
+ let ws;
237
+ try {
238
+ ws = this.wsFactory(url);
239
+ } catch (e) {
240
+ reject(new NetworkError("WebSocket constructor threw", e));
241
+ return;
242
+ }
243
+ this.ws = ws;
244
+ let settled = false;
245
+ const settle = (err) => {
246
+ if (settled) return;
247
+ settled = true;
248
+ if (err) reject(err);
249
+ else resolve();
250
+ };
251
+ ws.onopen = () => {
252
+ this.openEmit.emit();
253
+ settle();
254
+ };
255
+ ws.onmessage = (e) => this.handleMessage(e.data);
256
+ ws.onerror = (_e) => {
257
+ const err = new NetworkError("ws error event");
258
+ this.errorEmit.emit(err);
259
+ settle(err);
260
+ };
261
+ ws.onclose = (e) => {
262
+ this.failPendingAcks(new NetworkError("connection closed"));
263
+ this.closeEmit.emit({ code: e.code, reason: e.reason });
264
+ settle(new NetworkError(`closed before open (code ${e.code})`));
265
+ };
266
+ });
267
+ }
268
+ /** Send any frame. Throws NetworkError if the socket is not open. */
269
+ send(frame) {
270
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
271
+ throw new NetworkError("send: ws not open");
272
+ }
273
+ let serialised;
274
+ try {
275
+ serialised = JSON.stringify(frame);
276
+ } catch (e) {
277
+ throw new ProtocolError("frame not JSON-serialisable", e);
278
+ }
279
+ this.ws.send(serialised);
280
+ }
281
+ /**
282
+ * Send a TypeMsg frame and resolve when the matching ack arrives (matched on
283
+ * client_msg_id). Rejects with TimeoutError if no ack within ackTimeoutMs, or
284
+ * with NetworkError if the connection drops first.
285
+ */
286
+ sendMsg(payload) {
287
+ const cmid = payload.client_msg_id ?? this.generateClientMsgId();
288
+ const out = { ...payload, client_msg_id: cmid };
289
+ return new Promise((resolve, reject) => {
290
+ const timer = setTimeout(() => {
291
+ this.pendingAcks.delete(cmid);
292
+ reject(new TimeoutError(`ack timeout for ${cmid}`));
293
+ }, this.ackTimeoutMs);
294
+ this.pendingAcks.set(cmid, { resolve, reject, timer });
295
+ try {
296
+ this.send({ type: FrameType.Msg, payload: out });
297
+ } catch (e) {
298
+ clearTimeout(timer);
299
+ this.pendingAcks.delete(cmid);
300
+ reject(e);
301
+ }
302
+ });
303
+ }
304
+ /** Close the socket and reject any pending acks. Idempotent. */
305
+ disconnect(code = 1e3, reason = "client disconnect") {
306
+ this.failPendingAcks(new IMError("disconnected"));
307
+ if (this.ws && this.ws.readyState !== WebSocket.CLOSED) {
308
+ try {
309
+ this.ws.close(code, reason);
310
+ } catch {
311
+ }
312
+ }
313
+ this.ws = null;
314
+ }
315
+ // ---- subscriptions ----
316
+ onOpen(cb) {
317
+ return this.openEmit.on(cb);
318
+ }
319
+ onClose(cb) {
320
+ return this.closeEmit.on(cb);
321
+ }
322
+ onFrame(cb) {
323
+ return this.frameEmit.on(cb);
324
+ }
325
+ onError(cb) {
326
+ return this.errorEmit.on(cb);
327
+ }
328
+ // ---- internals ----
329
+ handleMessage(data) {
330
+ let raw;
331
+ if (typeof data === "string") {
332
+ raw = data;
333
+ } else if (data instanceof ArrayBuffer) {
334
+ raw = new TextDecoder().decode(new Uint8Array(data));
335
+ } else {
336
+ this.errorEmit.emit(new ProtocolError("unsupported ws data type"));
337
+ return;
338
+ }
339
+ let frame;
340
+ try {
341
+ frame = JSON.parse(raw);
342
+ } catch (e) {
343
+ this.errorEmit.emit(new ProtocolError("bad json frame", e));
344
+ return;
345
+ }
346
+ if (frame.type === FrameType.Ack && frame.payload) {
347
+ const ack = frame.payload;
348
+ const cmid = ack.client_msg_id;
349
+ if (cmid && this.pendingAcks.has(cmid)) {
350
+ const p = this.pendingAcks.get(cmid);
351
+ clearTimeout(p.timer);
352
+ this.pendingAcks.delete(cmid);
353
+ p.resolve(ack);
354
+ return;
355
+ }
356
+ }
357
+ this.frameEmit.emit(frame);
358
+ }
359
+ failPendingAcks(err) {
360
+ for (const [, p] of this.pendingAcks) {
361
+ clearTimeout(p.timer);
362
+ p.reject(err);
363
+ }
364
+ this.pendingAcks.clear();
365
+ }
366
+ generateClientMsgId() {
367
+ try {
368
+ const g = globalThis;
369
+ if (g.crypto && typeof g.crypto.randomUUID === "function") {
370
+ return g.crypto.randomUUID();
371
+ }
372
+ } catch {
373
+ }
374
+ this.idCounter += 1;
375
+ return "cm-" + this.idCounter.toString(36);
376
+ }
377
+ };
378
+
379
+ // src/client.ts
380
+ var Client = class {
381
+ opts;
382
+ rest;
383
+ wsURL;
384
+ backoff;
385
+ stateEmit = new Emitter();
386
+ msgEmit = new Emitter();
387
+ readEmit = new Emitter();
388
+ systemEmit = new Emitter();
389
+ state_ = "disconnected";
390
+ ws = null;
391
+ currentToken = null;
392
+ reconnectTimer = null;
393
+ explicitlyDisconnected = false;
394
+ constructor(opts) {
395
+ if (!opts.baseURL) throw new Error("Client: baseURL required");
396
+ if (!opts.tokenProvider) throw new Error("Client: tokenProvider required");
397
+ this.opts = opts;
398
+ this.rest = new RestClient({
399
+ baseURL: opts.baseURL,
400
+ tokenProvider: opts.tokenProvider,
401
+ fetch: opts.fetch
402
+ });
403
+ this.wsURL = opts.wsURL ?? this.deriveWsURL(opts.baseURL);
404
+ this.backoff = Backoff.create(opts.backoff);
405
+ if (opts.onStateChange) this.stateEmit.on(opts.onStateChange);
406
+ }
407
+ // ---- public API ----
408
+ get state() {
409
+ return this.state_;
410
+ }
411
+ async connect() {
412
+ this.explicitlyDisconnected = false;
413
+ if (this.state_ === "connected" || this.state_ === "connecting") return;
414
+ await this.openOnce(
415
+ /*allowRefresh=*/
416
+ true
417
+ );
418
+ }
419
+ disconnect() {
420
+ this.explicitlyDisconnected = true;
421
+ this.cancelReconnect();
422
+ if (this.ws) {
423
+ this.ws.disconnect();
424
+ this.ws = null;
425
+ }
426
+ this.transitionTo("disconnected");
427
+ }
428
+ async sendText(cid, body, opts = {}) {
429
+ if (!cid || !body) throw new Error("sendText: cid and body required");
430
+ if (!this.ws || !this.ws.isOpen) {
431
+ throw new NetworkError("sendText: not connected");
432
+ }
433
+ return this.ws.sendMsg({
434
+ cid,
435
+ body,
436
+ content_type: opts.contentType,
437
+ client_msg_id: opts.clientMsgId
438
+ });
439
+ }
440
+ async loadHistory(cid, opts = {}) {
441
+ return this.rest.listMessages(cid, opts);
442
+ }
443
+ async markRead(cid, seq) {
444
+ if (this.ws && this.ws.isOpen) {
445
+ const payload = { cid, seq };
446
+ this.ws.send({ type: FrameType.Read, payload });
447
+ return;
448
+ }
449
+ await this.rest.markRead(cid, seq);
450
+ }
451
+ async listConversations() {
452
+ return this.rest.listConversations();
453
+ }
454
+ // ---- event subscriptions ----
455
+ onMessage(cb) {
456
+ return this.msgEmit.on(cb);
457
+ }
458
+ onRead(cb) {
459
+ return this.readEmit.on(cb);
460
+ }
461
+ onSystem(cb) {
462
+ return this.systemEmit.on(cb);
463
+ }
464
+ onStateChange(cb) {
465
+ return this.stateEmit.on(cb);
466
+ }
467
+ // ---- internals ----
468
+ deriveWsURL(baseURL) {
469
+ const trimmed = baseURL.replace(/\/$/, "");
470
+ return trimmed.replace(/^http/, "ws") + "/v1/ws";
471
+ }
472
+ transitionTo(next) {
473
+ if (this.state_ === next) return;
474
+ this.state_ = next;
475
+ this.stateEmit.emit(next);
476
+ }
477
+ async fetchToken(forceRefresh) {
478
+ if (this.currentToken && !forceRefresh) return this.currentToken;
479
+ let tok;
480
+ try {
481
+ tok = await this.opts.tokenProvider();
482
+ } catch (e) {
483
+ throw new AuthError("tokenProvider failed", e);
484
+ }
485
+ if (!tok) throw new AuthError("tokenProvider returned empty token");
486
+ this.currentToken = tok;
487
+ return tok;
488
+ }
489
+ /**
490
+ * Try to open one WebSocket. On 401-style failure, retries once with a fresh token
491
+ * (when allowRefresh is true). Updates state.
492
+ */
493
+ async openOnce(allowRefresh) {
494
+ this.transitionTo("connecting");
495
+ let token;
496
+ try {
497
+ token = await this.fetchToken(false);
498
+ } catch (e) {
499
+ this.transitionTo("auth_failed");
500
+ throw e;
501
+ }
502
+ try {
503
+ await this.openWith(token);
504
+ this.backoff.reset();
505
+ this.transitionTo("connected");
506
+ } catch (e) {
507
+ if (allowRefresh && this.looksLikeAuthFailure(e)) {
508
+ try {
509
+ token = await this.fetchToken(true);
510
+ } catch (refreshErr) {
511
+ this.transitionTo("auth_failed");
512
+ throw refreshErr;
513
+ }
514
+ try {
515
+ await this.openWith(token);
516
+ this.backoff.reset();
517
+ this.transitionTo("connected");
518
+ return;
519
+ } catch (e2) {
520
+ this.transitionTo("auth_failed");
521
+ throw e2;
522
+ }
523
+ }
524
+ throw e;
525
+ }
526
+ }
527
+ looksLikeAuthFailure(e) {
528
+ if (e instanceof NetworkError && /code 1008|1006/.test(e.message)) return true;
529
+ if (e instanceof AuthError) return true;
530
+ return false;
531
+ }
532
+ openWith(token) {
533
+ const ws = new WsClient({
534
+ url: this.wsURL,
535
+ wsFactory: this.opts.wsFactory,
536
+ ackTimeoutMs: this.opts.ackTimeoutMs
537
+ });
538
+ this.ws = ws;
539
+ ws.onFrame((f) => this.dispatchFrame(f));
540
+ ws.onClose((info) => this.handleClose(info));
541
+ return ws.connect(token);
542
+ }
543
+ dispatchFrame(f) {
544
+ switch (f.type) {
545
+ case FrameType.Msg:
546
+ if (f.payload) this.msgEmit.emit(f.payload);
547
+ break;
548
+ case FrameType.Read:
549
+ if (f.payload) this.readEmit.emit(f.payload);
550
+ break;
551
+ case FrameType.System:
552
+ if (f.payload) this.systemEmit.emit(f.payload);
553
+ break;
554
+ }
555
+ }
556
+ handleClose(_info) {
557
+ if (this.explicitlyDisconnected) {
558
+ return;
559
+ }
560
+ this.ws = null;
561
+ if (this.state_ === "auth_failed") {
562
+ return;
563
+ }
564
+ this.transitionTo("reconnecting");
565
+ this.scheduleReconnect();
566
+ }
567
+ scheduleReconnect() {
568
+ this.cancelReconnect();
569
+ const delay = this.backoff.next();
570
+ this.reconnectTimer = setTimeout(() => {
571
+ this.reconnectTimer = null;
572
+ void this.tryReconnect();
573
+ }, delay);
574
+ }
575
+ async tryReconnect() {
576
+ if (this.explicitlyDisconnected) return;
577
+ try {
578
+ await this.openOnce(
579
+ /*allowRefresh=*/
580
+ true
581
+ );
582
+ } catch (e) {
583
+ if (this.state_ === "auth_failed" || this.explicitlyDisconnected) return;
584
+ this.transitionTo("reconnecting");
585
+ this.scheduleReconnect();
586
+ }
587
+ }
588
+ cancelReconnect() {
589
+ if (this.reconnectTimer !== null) {
590
+ clearTimeout(this.reconnectTimer);
591
+ this.reconnectTimer = null;
592
+ }
593
+ }
594
+ };
595
+
596
+ exports.AuthError = AuthError;
597
+ exports.Backoff = Backoff;
598
+ exports.Client = Client;
599
+ exports.Emitter = Emitter;
600
+ exports.FrameType = FrameType;
601
+ exports.IMError = IMError;
602
+ exports.NetworkError = NetworkError;
603
+ exports.ProtocolError = ProtocolError;
604
+ exports.RestClient = RestClient;
605
+ exports.TimeoutError = TimeoutError;
606
+ exports.WsClient = WsClient;
607
+ //# sourceMappingURL=index.cjs.map
608
+ //# sourceMappingURL=index.cjs.map