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