@roeehrl/tinode-sdk 0.25.1-sqlite.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.
@@ -0,0 +1,537 @@
1
+ /**
2
+ * @file Abstraction layer for websocket and long polling connections.
3
+ *
4
+ * @copyright 2015-2025 Tinode LLC.
5
+ */
6
+ 'use strict';
7
+
8
+ import CommError from './comm-error.js';
9
+ import {
10
+ BACKOFF_BASE,
11
+ BACKOFF_MAX_ITER,
12
+ BACKOFF_JITTER
13
+ } from './config.js';
14
+ import {
15
+ jsonParseHelper
16
+ } from './utils.js';
17
+
18
+
19
+ let WebSocketProvider;
20
+ let XHRProvider;
21
+
22
+ // Error code to return in case of a network problem.
23
+ const NETWORK_ERROR = 503;
24
+ const NETWORK_ERROR_TEXT = "Connection failed";
25
+
26
+ // Error code to return when user disconnected from server.
27
+ const NETWORK_USER = 418;
28
+ const NETWORK_USER_TEXT = "Disconnected by client";
29
+
30
+ // Helper function for creating an endpoint URL.
31
+ function makeBaseUrl(host, protocol, version, apiKey) {
32
+ let url = null;
33
+
34
+ if (['http', 'https', 'ws', 'wss'].includes(protocol)) {
35
+ url = `${protocol}://${host}`;
36
+ if (url.charAt(url.length - 1) !== '/') {
37
+ url += '/';
38
+ }
39
+ url += 'v' + version + '/channels';
40
+ if (['http', 'https'].includes(protocol)) {
41
+ // Long polling endpoint ends with "lp", i.e.
42
+ // '/v0/channels/lp' vs just '/v0/channels' for ws
43
+ url += '/lp';
44
+ }
45
+ url += '?apikey=' + apiKey;
46
+ }
47
+ return url;
48
+ }
49
+
50
+ /**
51
+ * An abstraction for a websocket or a long polling connection.
52
+ *
53
+ * @class Connection
54
+ * @memberof Tinode
55
+
56
+ * @param {Object} config - configuration parameters.
57
+ * @param {string} config.host - Host name and optional port number to connect to.
58
+ * @param {string} config.apiKey - API key generated by <code>keygen</code>.
59
+ * @param {string} config.transport - Network transport to use, either <code>"ws"<code>/<code>"wss"</code> for websocket or
60
+ * <code>lp</code> for long polling.
61
+ * @param {boolean} config.secure - Use Secure WebSocket if <code>true</code>.
62
+ * @param {string} version_ - Major value of the protocol version, e.g. '0' in '0.17.1'.
63
+ * @param {boolean} autoreconnect_ - If connection is lost, try to reconnect automatically.
64
+ */
65
+ export default class Connection {
66
+ // Logger, does nothing by default.
67
+ static #log = _ => {};
68
+
69
+ #boffTimer = null;
70
+ #boffIteration = 0;
71
+ #boffClosed = false; // Indicator if the socket was manually closed - don't autoreconnect if true.
72
+
73
+ // Websocket.
74
+ #socket = null;
75
+
76
+ host;
77
+ secure;
78
+ apiKey;
79
+
80
+ version;
81
+ autoreconnect;
82
+
83
+ initialized;
84
+
85
+ // (config.host, config.apiKey, config.transport, config.secure), PROTOCOL_VERSION, true
86
+ constructor(config, version_, autoreconnect_) {
87
+ this.host = config.host;
88
+ this.secure = config.secure;
89
+ this.apiKey = config.apiKey;
90
+
91
+ this.version = version_;
92
+ this.autoreconnect = autoreconnect_;
93
+
94
+ if (config.transport === 'lp') {
95
+ // explicit request to use long polling
96
+ this.#init_lp();
97
+ this.initialized = 'lp';
98
+ } else if (config.transport === 'ws') {
99
+ // explicit request to use web socket
100
+ // if websockets are not available, horrible things will happen
101
+ this.#init_ws();
102
+ this.initialized = 'ws';
103
+ }
104
+
105
+ if (!this.initialized) {
106
+ // Invalid or undefined network transport.
107
+ Connection.#log("Unknown or invalid network transport. Running under Node? Call 'Tinode.setNetworkProviders()'.");
108
+ throw new Error("Unknown or invalid network transport. Running under Node? Call 'Tinode.setNetworkProviders()'.");
109
+ }
110
+ }
111
+
112
+ /**
113
+ * To use Connection in a non browser context, supply WebSocket and XMLHttpRequest providers.
114
+ * @static
115
+ * @memberof Connection
116
+ * @param wsProvider WebSocket provider, e.g. for nodeJS , <code>require('ws')</code>.
117
+ * @param xhrProvider XMLHttpRequest provider, e.g. for node <code>require('xhr')</code>.
118
+ */
119
+ static setNetworkProviders(wsProvider, xhrProvider) {
120
+ WebSocketProvider = wsProvider;
121
+ XHRProvider = xhrProvider;
122
+ }
123
+
124
+ /**
125
+ * Assign a non-default logger.
126
+ * @static
127
+ * @memberof Connection
128
+ * @param {function} l variadic logging function.
129
+ */
130
+ static set logger(l) {
131
+ Connection.#log = l;
132
+ }
133
+
134
+ /**
135
+ * Initiate a new connection
136
+ * @memberof Tinode.Connection#
137
+ * @param {string} host_ Host name to connect to; if <code>null</code> the old host name will be used.
138
+ * @param {boolean} force Force new connection even if one already exists.
139
+ * @return {Promise} Promise resolved/rejected when the connection call completes, resolution is called without
140
+ * parameters, rejection passes the {Error} as parameter.
141
+ */
142
+ connect(host_, force) {
143
+ return Promise.reject(null);
144
+ }
145
+
146
+ /**
147
+ * Try to restore a network connection, also reset backoff.
148
+ * @memberof Tinode.Connection#
149
+ *
150
+ * @param {boolean} force - reconnect even if there is a live connection already.
151
+ */
152
+ reconnect(force) {}
153
+
154
+ /**
155
+ * Terminate the network connection
156
+ * @memberof Tinode.Connection#
157
+ */
158
+ disconnect() {}
159
+
160
+ /**
161
+ * Send a string to the server.
162
+ * @memberof Tinode.Connection#
163
+ *
164
+ * @param {string} msg - String to send.
165
+ * @throws Throws an exception if the underlying connection is not live.
166
+ */
167
+ sendText(msg) {}
168
+
169
+ /**
170
+ * Check if connection is alive.
171
+ * @memberof Tinode.Connection#
172
+ * @returns {boolean} <code>true</code> if connection is live, <code>false</code> otherwise.
173
+ */
174
+ isConnected() {
175
+ return false;
176
+ }
177
+
178
+ /**
179
+ * Get the name of the current network transport.
180
+ * @memberof Tinode.Connection#
181
+ * @returns {string} name of the transport such as <code>"ws"</code> or <code>"lp"</code>.
182
+ */
183
+ transport() {
184
+ return this.initialized;
185
+ }
186
+
187
+ /**
188
+ * Send network probe to check if connection is indeed live.
189
+ * @memberof Tinode.Connection#
190
+ */
191
+ probe() {
192
+ this.sendText('1');
193
+ }
194
+
195
+ /**
196
+ * Reset autoreconnect counter to zero.
197
+ * @memberof Tinode.Connection#
198
+ */
199
+ backoffReset() {
200
+ this.#boffReset();
201
+ }
202
+
203
+ // Backoff implementation - reconnect after a timeout.
204
+ #boffReconnect() {
205
+ // Clear timer
206
+ clearTimeout(this.#boffTimer);
207
+ // Calculate when to fire the reconnect attempt
208
+ const timeout = BACKOFF_BASE * (Math.pow(2, this.#boffIteration) * (1.0 + BACKOFF_JITTER * Math.random()));
209
+ // Update iteration counter for future use
210
+ this.#boffIteration = (this.#boffIteration >= BACKOFF_MAX_ITER ? this.#boffIteration : this.#boffIteration + 1);
211
+ if (this.onAutoreconnectIteration) {
212
+ this.onAutoreconnectIteration(timeout);
213
+ }
214
+
215
+ this.#boffTimer = setTimeout(_ => {
216
+ Connection.#log(`Reconnecting, iter=${this.#boffIteration}, timeout=${timeout}`);
217
+ // Maybe the socket was closed while we waited for the timer?
218
+ if (!this.#boffClosed) {
219
+ const prom = this.connect();
220
+ if (this.onAutoreconnectIteration) {
221
+ this.onAutoreconnectIteration(0, prom);
222
+ } else {
223
+ // Suppress error if it's not used.
224
+ prom.catch(_ => {
225
+ /* do nothing */
226
+ });
227
+ }
228
+ } else if (this.onAutoreconnectIteration) {
229
+ this.onAutoreconnectIteration(-1);
230
+ }
231
+ }, timeout);
232
+ }
233
+
234
+ // Terminate auto-reconnect process.
235
+ #boffStop() {
236
+ clearTimeout(this.#boffTimer);
237
+ this.#boffTimer = null;
238
+ }
239
+
240
+ // Reset auto-reconnect iteration counter.
241
+ #boffReset() {
242
+ this.#boffIteration = 0;
243
+ }
244
+
245
+ // Initialization for long polling.
246
+ #init_lp() {
247
+ const XDR_UNSENT = 0; // Client has been created. open() not called yet.
248
+ const XDR_OPENED = 1; // open() has been called.
249
+ const XDR_HEADERS_RECEIVED = 2; // send() has been called, and headers and status are available.
250
+ const XDR_LOADING = 3; // Downloading; responseText holds partial data.
251
+ const XDR_DONE = 4; // The operation is complete.
252
+
253
+ // Fully composed endpoint URL, with API key & SID
254
+ let _lpURL = null;
255
+
256
+ let _poller = null;
257
+ let _sender = null;
258
+
259
+ let lp_sender = (url_) => {
260
+ const sender = new XHRProvider();
261
+ sender.onreadystatechange = (evt) => {
262
+ if (sender.readyState == XDR_DONE && sender.status >= 400) {
263
+ // Some sort of error response
264
+ throw new CommError("LP sender failed", sender.status);
265
+ }
266
+ };
267
+
268
+ sender.open('POST', url_, true);
269
+ return sender;
270
+ }
271
+
272
+ let lp_poller = (url_, resolve, reject) => {
273
+ let poller = new XHRProvider();
274
+ let promiseCompleted = false;
275
+
276
+ poller.onreadystatechange = evt => {
277
+ if (poller.readyState == XDR_DONE) {
278
+ if (poller.status == 201) { // 201 == HTTP.Created, get SID
279
+ let pkt = JSON.parse(poller.responseText, jsonParseHelper);
280
+ _lpURL = url_ + '&sid=' + pkt.ctrl.params.sid;
281
+ poller = lp_poller(_lpURL);
282
+ poller.send(null);
283
+ if (this.onOpen) {
284
+ this.onOpen();
285
+ }
286
+
287
+ if (resolve) {
288
+ promiseCompleted = true;
289
+ resolve();
290
+ }
291
+
292
+ if (this.autoreconnect) {
293
+ this.#boffStop();
294
+ }
295
+ } else if (poller.status > 0 && poller.status < 400) { // 0 = network error; 400 = HTTP.BadRequest
296
+ if (this.onMessage) {
297
+ this.onMessage(poller.responseText);
298
+ }
299
+ poller = lp_poller(_lpURL);
300
+ poller.send(null);
301
+ } else {
302
+ // Don't throw an error here, gracefully handle server errors
303
+ if (reject && !promiseCompleted) {
304
+ promiseCompleted = true;
305
+ reject(poller.responseText);
306
+ }
307
+ if (this.onMessage && poller.responseText) {
308
+ this.onMessage(poller.responseText);
309
+ }
310
+ if (this.onDisconnect) {
311
+ const code = poller.status || (this.#boffClosed ? NETWORK_USER : NETWORK_ERROR);
312
+ const text = poller.responseText || (this.#boffClosed ? NETWORK_USER_TEXT : NETWORK_ERROR_TEXT);
313
+ this.onDisconnect(new CommError(text, code), code);
314
+ }
315
+
316
+ // Polling has stopped. Indicate it by setting poller to null.
317
+ poller = null;
318
+ if (!this.#boffClosed && this.autoreconnect) {
319
+ this.#boffReconnect();
320
+ }
321
+ }
322
+ }
323
+ };
324
+ // Using POST to avoid caching response by service worker.
325
+ poller.open('POST', url_, true);
326
+ return poller;
327
+ }
328
+
329
+ this.connect = (host_, force) => {
330
+ this.#boffClosed = false;
331
+
332
+ if (_poller) {
333
+ if (!force) {
334
+ return Promise.resolve();
335
+ }
336
+ _poller.onreadystatechange = undefined;
337
+ _poller.abort();
338
+ _poller = null;
339
+ }
340
+
341
+ if (host_) {
342
+ this.host = host_;
343
+ }
344
+
345
+ return new Promise((resolve, reject) => {
346
+ const url = makeBaseUrl(this.host, this.secure ? 'https' : 'http', this.version, this.apiKey);
347
+ Connection.#log("LP connecting to:", url);
348
+ _poller = lp_poller(url, resolve, reject);
349
+ _poller.send(null);
350
+ }).catch(err => {
351
+ Connection.#log("LP connection failed:", err);
352
+ });
353
+ };
354
+
355
+ this.reconnect = force => {
356
+ this.#boffStop();
357
+ this.connect(null, force);
358
+ };
359
+
360
+ this.disconnect = _ => {
361
+ this.#boffClosed = true;
362
+ this.#boffStop();
363
+
364
+ if (_sender) {
365
+ _sender.onreadystatechange = undefined;
366
+ _sender.abort();
367
+ _sender = null;
368
+ }
369
+ if (_poller) {
370
+ _poller.onreadystatechange = undefined;
371
+ _poller.abort();
372
+ _poller = null;
373
+ }
374
+
375
+ if (this.onDisconnect) {
376
+ this.onDisconnect(new CommError(NETWORK_USER_TEXT, NETWORK_USER), NETWORK_USER);
377
+ }
378
+ // Ensure it's reconstructed
379
+ _lpURL = null;
380
+ };
381
+
382
+ this.sendText = (msg) => {
383
+ _sender = lp_sender(_lpURL);
384
+ if (_sender && (_sender.readyState == XDR_OPENED)) {
385
+ _sender.send(msg);
386
+ } else {
387
+ throw new Error("Long poller failed to connect");
388
+ }
389
+ };
390
+
391
+ this.isConnected = _ => {
392
+ return (_poller && true);
393
+ };
394
+ }
395
+
396
+ // Initialization for Websocket
397
+ #init_ws() {
398
+ this.connect = (host_, force) => {
399
+ this.#boffClosed = false;
400
+
401
+ if (this.#socket) {
402
+ if (!force && this.#socket.readyState == this.#socket.OPEN) {
403
+ // Issue a probe request to be sure the connection is live.
404
+ // This is a non-blocking call.
405
+ this.probe();
406
+ return Promise.resolve();
407
+ }
408
+ this.#socket.close();
409
+ this.#socket = null;
410
+ }
411
+
412
+ if (host_) {
413
+ this.host = host_;
414
+ }
415
+
416
+ return new Promise((resolve, reject) => {
417
+ const url = makeBaseUrl(this.host, this.secure ? 'wss' : 'ws', this.version, this.apiKey);
418
+
419
+ Connection.#log("WS connecting to: ", url);
420
+
421
+ // It throws when the server is not accessible but the exception cannot be caught:
422
+ // https://stackoverflow.com/questions/31002592/javascript-doesnt-catch-error-in-websocket-instantiation/31003057
423
+ const conn = new WebSocketProvider(url);
424
+
425
+ conn.onerror = err => {
426
+ reject(err);
427
+ };
428
+
429
+ conn.onopen = _ => {
430
+ if (this.autoreconnect) {
431
+ this.#boffStop();
432
+ }
433
+
434
+ if (this.onOpen) {
435
+ this.onOpen();
436
+ }
437
+
438
+ resolve();
439
+ };
440
+
441
+ conn.onclose = _ => {
442
+ this.#socket = null;
443
+
444
+ if (this.onDisconnect) {
445
+ const code = this.#boffClosed ? NETWORK_USER : NETWORK_ERROR;
446
+ this.onDisconnect(new CommError(this.#boffClosed ? NETWORK_USER_TEXT : NETWORK_ERROR_TEXT, code), code);
447
+ }
448
+
449
+ if (!this.#boffClosed && this.autoreconnect) {
450
+ this.#boffReconnect();
451
+ }
452
+ };
453
+
454
+ conn.onmessage = evt => {
455
+ if (this.onMessage) {
456
+ this.onMessage(evt.data);
457
+ }
458
+ };
459
+
460
+ this.#socket = conn;
461
+ });
462
+ }
463
+
464
+ this.reconnect = force => {
465
+ this.#boffStop();
466
+ this.connect(null, force);
467
+ };
468
+
469
+ this.disconnect = _ => {
470
+ this.#boffClosed = true;
471
+ this.#boffStop();
472
+
473
+ if (!this.#socket) {
474
+ return;
475
+ }
476
+ this.#socket.close();
477
+ this.#socket = null;
478
+ };
479
+
480
+ this.sendText = msg => {
481
+ if (this.#socket && (this.#socket.readyState == this.#socket.OPEN)) {
482
+ this.#socket.send(msg);
483
+ } else {
484
+ throw new Error("Websocket is not connected");
485
+ }
486
+ };
487
+
488
+ this.isConnected = _ => {
489
+ return (this.#socket && (this.#socket.readyState == this.#socket.OPEN));
490
+ };
491
+ }
492
+
493
+ // Callbacks:
494
+
495
+ /**
496
+ * A callback to pass incoming messages to. See {@link Tinode.Connection#onMessage}.
497
+ * @callback Tinode.Connection.OnMessage
498
+ * @memberof Tinode.Connection
499
+ * @param {string} message - Message to process.
500
+ */
501
+ onMessage = undefined;
502
+
503
+ /**
504
+ * A callback for reporting a dropped connection.
505
+ * @type {function}
506
+ * @memberof Tinode.Connection#
507
+ */
508
+ onDisconnect = undefined;
509
+
510
+ /**
511
+ * A callback called when the connection is ready to be used for sending. For websockets it's socket open,
512
+ * for long polling it's <code>readyState=1</code> (OPENED)
513
+ * @type {function}
514
+ * @memberof Tinode.Connection#
515
+ */
516
+ onOpen = undefined;
517
+
518
+ /**
519
+ * A callback to notify of reconnection attempts. See {@link Tinode.Connection#onAutoreconnectIteration}.
520
+ * @memberof Tinode.Connection
521
+ * @callback AutoreconnectIterationType
522
+ * @param {string} timeout - time till the next reconnect attempt in milliseconds. <code>-1</code> means reconnect was skipped.
523
+ * @param {Promise} promise resolved or rejected when the reconnect attemp completes.
524
+ *
525
+ */
526
+ /**
527
+ * A callback to inform when the next attampt to reconnect will happen and to receive connection promise.
528
+ * @memberof Tinode.Connection#
529
+ * @type {Tinode.Connection.AutoreconnectIterationType}
530
+ */
531
+ onAutoreconnectIteration = undefined;
532
+ }
533
+
534
+ Connection.NETWORK_ERROR = NETWORK_ERROR;
535
+ Connection.NETWORK_ERROR_TEXT = NETWORK_ERROR_TEXT;
536
+ Connection.NETWORK_USER = NETWORK_USER;
537
+ Connection.NETWORK_USER_TEXT = NETWORK_USER_TEXT;