@tchain/tcabci-read-js-client 2.7.11 → 2.7.13

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/websocket.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { byteLength, closeCode } from './validation.js'
1
2
  import { Options } from './websocketOptions.js'
2
3
  import ReconnectingWebSocket from 'reconnecting-websocket'
3
4
 
@@ -27,6 +28,9 @@ export class TWebSocket {
27
28
  _closeCallbacks = []
28
29
 
29
30
  _connectionErrorCount = 0
31
+ _timers = new Set()
32
+ _queuedMessages = 0
33
+ _queuedBytes = 0
30
34
 
31
35
  /**
32
36
  * @param {Options} options
@@ -44,43 +48,47 @@ export class TWebSocket {
44
48
  /**
45
49
  * @return {boolean}
46
50
  */
51
+ get active() {
52
+ return Boolean(this._client)
53
+ }
54
+
47
55
  get ready() {
48
56
  if (!this._client) return false
49
57
 
50
- return this._client.readyState === WebSocket.OPEN
58
+ return this._client.readyState === 1
51
59
  }
52
60
 
53
61
  /**
54
- * @return {Array<function>}
62
+ * @return {ReadonlyArray<function>}
55
63
  */
56
64
  get openListeners() {
57
- return this._openCallbacks
65
+ return Object.freeze([...this._openCallbacks])
58
66
  }
59
67
 
60
68
  /**
61
- * @return {Array<function>}
69
+ * @return {ReadonlyArray<function>}
62
70
  */
63
71
  get messageListeners() {
64
- return this._messageCallbacks
72
+ return Object.freeze([...this._messageCallbacks])
65
73
  }
66
74
 
67
75
  /**
68
- * @return {Array<function>}
76
+ * @return {ReadonlyArray<function>}
69
77
  */
70
78
  get errorListeners() {
71
- return this._errorCallbacks
79
+ return Object.freeze([...this._errorCallbacks])
72
80
  }
73
81
 
74
82
  /**
75
- * @return {Array<function>}
83
+ * @return {ReadonlyArray<function>}
76
84
  */
77
85
  get closeListeners() {
78
- return this._closeCallbacks
86
+ return Object.freeze([...this._closeCallbacks])
79
87
  }
80
88
 
81
89
  /**
82
90
  * @param {?boolean} force
83
- * @return {Awaited<TWebSocket>}
91
+ * @return {Promise<TWebSocket>}
84
92
  * @throws {Error}
85
93
  */
86
94
  async connect(force = false) {
@@ -92,31 +100,57 @@ export class TWebSocket {
92
100
 
93
101
  /**
94
102
  * @param {?number} code
95
- * @return {Awaited<TWebSocket>}
103
+ * @return {Promise<TWebSocket>}
96
104
  * @throws {Error}
97
105
  */
98
106
  async reconnect(code = null) {
99
- await this._disconnect(code)
100
- this._init(true)
107
+ this._close(code)
108
+ await this._init(true)
101
109
 
102
110
  return this
103
111
  }
104
112
 
105
113
  /**
106
- * @param {number} code
107
- * @return {Promise<void>}
114
+ * Close every socket state, clear queued callbacks and release references.
115
+ * @param {number} [code=1000]
116
+ * @returns {Promise<void>}
108
117
  */
109
118
  async disconnect(code = 1000) {
110
119
  return this._disconnect(code)
111
120
  }
112
121
 
113
122
  /**
123
+ * Send a bounded message. Offline binary buffers are rejected, not retained.
114
124
  * @param {string|ArrayBuffer|Blob|ArrayBufferView} msg
125
+ * @returns {void}
115
126
  */
116
127
  send(msg) {
117
- if (!this._client) return
128
+ if (!this._client) throw new Error('Not connected')
129
+
130
+ const size = byteLength(msg)
131
+
132
+ if (size > this._options.maxMessageBytes)
133
+ throw new Error('Message exceeds limit')
134
+
135
+ if (!this.ready && typeof msg !== 'string')
136
+ throw new Error('Binary messages require an open connection')
137
+
138
+ if (this.ready) {
139
+ if (this._client.bufferedAmount + size > this._options.maxMessageBytes)
140
+ throw new Error('Outgoing buffer exceeds byte limit')
141
+ } else if (
142
+ this._queuedMessages >= this._options.maxEnqueuedMessages ||
143
+ this._queuedBytes + size > this._options.maxMessageBytes
144
+ ) {
145
+ throw new Error('Outgoing queue exceeds limit')
146
+ }
118
147
 
119
148
  this._client.send(msg)
149
+
150
+ if (!this.ready) {
151
+ this._queuedMessages++
152
+ this._queuedBytes += size
153
+ }
120
154
  }
121
155
 
122
156
  /**
@@ -194,10 +228,12 @@ export class TWebSocket {
194
228
  * @private
195
229
  */
196
230
  _addListener(name, callback) {
231
+ if (typeof callback !== 'function') throw new TypeError('Invalid callback')
232
+
197
233
  if (this[name].length >= MaxListenerSize)
198
234
  throw new Error(`listener size must be ${MaxListenerSize}`)
199
235
 
200
- if (this[name].findIndex(v => v === callback) > -1) return this
236
+ if (this[name].findIndex((v) => v === callback) > -1) return this
201
237
 
202
238
  this[name].push(callback)
203
239
 
@@ -210,7 +246,7 @@ export class TWebSocket {
210
246
  * @private
211
247
  */
212
248
  _removeListener(name, callback) {
213
- const idx = this[name].findIndex(v => v === callback)
249
+ const idx = this[name].findIndex((v) => v === callback)
214
250
 
215
251
  if (idx > -1) {
216
252
  this[name].splice(idx, 1)
@@ -222,30 +258,43 @@ export class TWebSocket {
222
258
  _callListener(name, value) {
223
259
  if (!this[name]) return
224
260
 
261
+ if (
262
+ this._timers.size + this[name].length >
263
+ this._options.maxPendingCallbacks
264
+ ) {
265
+ this._fail(4008)
266
+
267
+ return
268
+ }
269
+
225
270
  for (const callback of this[name]) {
226
- setTimeout(
227
- (callback, value) => {
271
+ const timer = setTimeout(() => {
272
+ this._timers.delete(timer)
273
+
274
+ if (!this[name].includes(callback)) return
275
+
276
+ try {
228
277
  callback(value)
229
- },
230
- 0,
231
- callback,
232
- value
233
- )
278
+ } catch {
279
+ if (name !== '_errorCallbacks')
280
+ this._onError(new Error('Callback failed'))
281
+ }
282
+ }, 0)
283
+
284
+ this._timers.add(timer)
234
285
  }
235
286
  }
236
287
 
237
288
  /**
238
289
  * @private
239
290
  * @param {?boolean} force
240
- * @return {Awaited<TWebSocket>}
291
+ * @return {Promise<TWebSocket>}
241
292
  * @throws {Error}
242
293
  */
243
294
  async _init(force = false) {
244
- if (!force && this.ready) return this
295
+ if (!force && this._client) return this
245
296
 
246
- if (this.ready) {
247
- await this.disconnect(1000)
248
- }
297
+ if (this._client) this._close(1000)
249
298
 
250
299
  this._make()
251
300
 
@@ -261,32 +310,36 @@ export class TWebSocket {
261
310
  this._options.make()
262
311
  )
263
312
 
264
- this._openListener = e => {
313
+ this._openListener = (e) => {
314
+ this._queuedMessages = 0
315
+ this._queuedBytes = 0
265
316
  this._connectionErrorCount = 0
266
317
  this._onOpen(e)
267
318
  }
268
319
  this._client.addEventListener('open', this._openListener)
269
320
 
270
- this._closeListener = e => {
321
+ this._closeListener = (e) => {
271
322
  this._onClose(e)
272
323
  }
273
324
  this._client.addEventListener('close', this._closeListener)
274
325
 
275
- this._messageListener = e => {
326
+ this._messageListener = (e) => {
276
327
  this._onMessage(e)
277
328
  }
278
329
  this._client.addEventListener('message', this._messageListener)
279
330
 
280
- this._errorListener = e => {
331
+ this._errorListener = (e) => {
281
332
  this._onError(e)
333
+
282
334
  if (this._connectionErrorCount > 15) {
283
335
  this._onError(new Error('Internet connectivity problem!'))
284
- this._disconnect().catch(err => {
336
+ this._disconnect().catch((err) => {
285
337
  this._onError(err)
286
338
  })
287
339
 
288
340
  return
289
341
  }
342
+
290
343
  this._connectionErrorCount++
291
344
  }
292
345
  this._client.addEventListener('error', this._errorListener)
@@ -298,18 +351,39 @@ export class TWebSocket {
298
351
  * @return {Promise<void>}
299
352
  */
300
353
  async _disconnect(code = 1000) {
301
- if (!this.ready) return
354
+ this._close(code)
355
+ }
356
+
357
+ _fail(code) {
358
+ const callbacks = [...this._closeCallbacks]
302
359
 
303
360
  this._close(code)
361
+
362
+ for (const callback of callbacks) {
363
+ try {
364
+ callback({ code, reason: 'Connection limit exceeded', wasClean: false })
365
+ } catch {
366
+ /* No payload logging. */
367
+ }
368
+ }
304
369
  }
305
370
 
306
371
  _close(code = 1000) {
307
- if (!code) code = 1000
372
+ code = closeCode(code)
373
+ this._queuedMessages = 0
374
+ this._queuedBytes = 0
375
+
376
+ for (const timer of this._timers) clearTimeout(timer)
308
377
 
309
- this._client.removeEventListener('open', this._openListener)
310
- this._client.removeEventListener('close', this._closeListener)
311
- this._client.removeEventListener('message', this._messageListener)
312
- this._client.removeEventListener('error', this._errorListener)
378
+ this._timers.clear()
379
+
380
+ const client = this._client
381
+
382
+ this._client = undefined
383
+ client?.removeEventListener('open', this._openListener)
384
+ client?.removeEventListener('close', this._closeListener)
385
+ client?.removeEventListener('message', this._messageListener)
386
+ client?.removeEventListener('error', this._errorListener)
313
387
 
314
388
  this._openCallbacks.length = 0
315
389
  this._errorCallbacks.length = 0
@@ -318,10 +392,10 @@ export class TWebSocket {
318
392
 
319
393
  this._openListener = () => {}
320
394
  this._closeListener = () => {}
321
- this._closeListener = () => {}
395
+ this._errorListener = () => {}
322
396
  this._messageListener = () => {}
323
397
 
324
- this._client.close(code)
398
+ client?.close(code)
325
399
  this._connectionErrorCount = 0
326
400
  }
327
401
 
@@ -346,6 +420,18 @@ export class TWebSocket {
346
420
  * @private
347
421
  */
348
422
  _onMessage(msg) {
423
+ try {
424
+ if (byteLength(msg.data) > this._options.maxMessageBytes) {
425
+ this._fail(4009)
426
+
427
+ return
428
+ }
429
+ } catch {
430
+ this._fail(4003)
431
+
432
+ return
433
+ }
434
+
349
435
  this._callListener('_messageCallbacks', msg)
350
436
  }
351
437
 
@@ -1,40 +1,78 @@
1
+ import { WebSocketConstructor } from './types.js'
2
+
1
3
  export declare type Timeout = number
4
+
2
5
  export declare const DefaultTimeout: Timeout, LongTimeout: Timeout
3
6
 
4
7
  export declare type Port = number
8
+
5
9
  export declare const HTTPSPort: Port, HTTPPort: Port
6
10
 
7
11
  export declare class Options {
8
12
  constructor(
9
13
  host: string,
10
- timeout: Timeout,
11
- secure: boolean,
12
- port: Port,
13
- longpoll: boolean
14
+ timeout?: Timeout,
15
+ secure?: boolean,
16
+ port?: Port,
17
+ longpoll?: boolean
14
18
  )
19
+
20
+ setAllowInsecure(value: boolean): this
21
+
22
+ setMaxMessageBytes(value: number): this
23
+
24
+ get maxMessageBytes(): number
25
+
26
+ setMaxPendingCallbacks(value: number): this
27
+
28
+ get maxPendingCallbacks(): number
29
+
15
30
  check(): boolean
16
- isLongPool(): boolean
31
+
32
+ get isLongPool(): boolean
33
+
17
34
  setDebug(debug: boolean): Options
35
+
18
36
  get debug(): boolean
19
- setEndpoints(wsEndpoint: string, longpollEndpoint: string): Options
20
- endpoints(): string[]
37
+
38
+ setEndpoints(wsEndpoint?: string, longpollEndpoint?: string): Options
39
+
40
+ get endpoints(): readonly string[]
41
+
21
42
  setMaxConnectionDelay(delay: number): Options
43
+
22
44
  get maxConnectionDelay(): number
45
+
23
46
  setMinReconnectionDelay(delay: number): Options
47
+
24
48
  get minReconnectionDelay(): number
49
+
25
50
  setReconnectionDelayGrowFactor(gw: number): Options
51
+
26
52
  get reconnectionDelayGrowFactor(): number
53
+
27
54
  setMinUptime(ut: number): Options
55
+
28
56
  get minUptime(): number
57
+
29
58
  setMaxRetries(retries: number): Options
59
+
30
60
  get maxRetries(): number
61
+
31
62
  setMaxEnqueuedMessages(em: number): Options
63
+
32
64
  get maxEnqueuedMessages(): number
65
+
33
66
  setProtocols(protocols: string[]): Options
34
- get protocols(): string[]
35
- setCustomWS(customWS: WebSocket): Options
36
- get customWS(): WebSocket | undefined
67
+
68
+ get protocols(): readonly string[]
69
+
70
+ setCustomWS(customWS: WebSocketConstructor): Options
71
+
72
+ get customWS(): WebSocketConstructor | undefined
73
+
37
74
  get url(): string
75
+
38
76
  make(): {
39
77
  WebSocket?: any
40
78
  maxReconnectionDelay: number
@@ -1,9 +1,8 @@
1
- import { Debug } from '@streetbyters/js-debug'
1
+ import { CLIENT_VERSION, endpoint, positiveInteger } from './validation.js'
2
2
 
3
3
  export const HTTPSPort = 443,
4
- HTTPPort = 80
5
-
6
- export const DefaultTimeout = 7000,
4
+ HTTPPort = 80,
5
+ DefaultTimeout = 7000,
7
6
  LongTimeout = DefaultTimeout * 2
8
7
 
9
8
  export class Options {
@@ -15,14 +14,17 @@ export class Options {
15
14
  _protocols = []
16
15
  _customWS
17
16
  _endpoints = ['ws', 'longpoll']
18
- _version = `v2.7.11`
17
+ _version = CLIENT_VERSION
18
+ _allowInsecure = false
19
+ _maxMessageBytes = 16 * 1024 * 1024
20
+ _maxPendingCallbacks = 1000
19
21
 
20
22
  _maxConnectionDelay = 10000
21
23
  _minReconnectionDelay = 1000 + Math.random() * 4000
22
24
  _reconnectionDelayGrowFactor = 1.3
23
25
  _minUptime = 5000
24
26
  _maxRetries = 10
25
- _maxEnqueuedMessages = Infinity
27
+ _maxEnqueuedMessages = 100
26
28
  _startClosed = false
27
29
  _debug = false
28
30
 
@@ -56,15 +58,36 @@ export class Options {
56
58
  if (this._timeout > LongTimeout)
57
59
  throw new Error(`timeout less than LongTimeout`)
58
60
 
59
- // eslint-disable-next-line
60
- try {
61
- new URL(this.url)
61
+ positiveInteger(this._timeout, 'timeout')
62
+ endpoint(this.url, ['ws:', 'wss:'], this._allowInsecure)
62
63
 
63
- return true
64
- } catch (e) {
65
- Debug.error(e)
66
- throw e
67
- }
64
+ return true
65
+ }
66
+
67
+ setAllowInsecure(value) {
68
+ this._allowInsecure = value === true
69
+
70
+ return this
71
+ }
72
+
73
+ setMaxMessageBytes(value) {
74
+ this._maxMessageBytes = positiveInteger(value, 'maxMessageBytes')
75
+
76
+ return this
77
+ }
78
+
79
+ get maxMessageBytes() {
80
+ return this._maxMessageBytes
81
+ }
82
+
83
+ setMaxPendingCallbacks(value) {
84
+ this._maxPendingCallbacks = positiveInteger(value, 'maxPendingCallbacks')
85
+
86
+ return this
87
+ }
88
+
89
+ get maxPendingCallbacks() {
90
+ return this._maxPendingCallbacks
68
91
  }
69
92
 
70
93
  /**
@@ -108,10 +131,10 @@ export class Options {
108
131
  }
109
132
 
110
133
  /**
111
- * @return {Array<string>}
134
+ * @return {ReadonlyArray<string>}
112
135
  */
113
136
  get endpoints() {
114
- return this._endpoints
137
+ return Object.freeze([...this._endpoints])
115
138
  }
116
139
 
117
140
  /**
@@ -204,7 +227,7 @@ export class Options {
204
227
  * @return {Options}
205
228
  */
206
229
  setMaxEnqueuedMessages(em) {
207
- this._maxEnqueuedMessages = em
230
+ this._maxEnqueuedMessages = positiveInteger(em, 'maxEnqueuedMessages')
208
231
 
209
232
  return this
210
233
  }
@@ -222,18 +245,22 @@ export class Options {
222
245
  * @throws {TypeError}
223
246
  */
224
247
  setProtocols(protocols) {
225
- if (!Array.isArray(protocols))
248
+ if (
249
+ !Array.isArray(protocols) ||
250
+ protocols.some((v) => typeof v !== 'string')
251
+ )
226
252
  throw new TypeError('protocols must be a Array')
227
- this._protocols = protocols
253
+
254
+ this._protocols = [...protocols]
228
255
 
229
256
  return this
230
257
  }
231
258
 
232
259
  /**
233
- * @return {Array<string>}
260
+ * @return {ReadonlyArray<string>}
234
261
  */
235
262
  get protocols() {
236
- return this._protocols
263
+ return Object.freeze([...this._protocols])
237
264
  }
238
265
 
239
266
  /**
@@ -257,7 +284,9 @@ export class Options {
257
284
  * @return {string}
258
285
  */
259
286
  get url() {
260
- if (this._host.length > 2 && this._host.slice(0, 2) === 'ws') {
287
+ if (typeof this._host !== 'string') throw new TypeError('Invalid host')
288
+
289
+ if (/^wss?:\/\//.test(this._host)) {
261
290
  return `${this._host}${![HTTPPort, HTTPSPort].includes(this._port) ? `:${this._port}` : ''}`
262
291
  }
263
292
 
@@ -278,7 +307,8 @@ export class Options {
278
307
  maxRetries: this._maxRetries,
279
308
  maxEnqueuedMessages: this._maxEnqueuedMessages,
280
309
  startClosed: this._startClosed,
281
- debug: this._debug,
310
+ // ReconnectingWebSocket debug logs outgoing payloads. Never enable it.
311
+ debug: false,
282
312
  headers: {
283
313
  Client: `tcabaci-read-js-client/${this._version}`
284
314
  }