@mathieuc/tradingview 3.0.0 → 3.3.0

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.
Files changed (51) hide show
  1. package/.eslintrc.js +6 -0
  2. package/README.md +29 -3
  3. package/docs/DOCS.md +3 -0
  4. package/examples/AllPrivateIndicators.js +4 -4
  5. package/examples/BuiltInIndicator.js +1 -1
  6. package/examples/CustomChartType.js +1 -0
  7. package/examples/Errors.js +147 -153
  8. package/examples/FakeReplayMode.js +41 -0
  9. package/examples/FromToData.js +29 -0
  10. package/examples/GetDrawings.js +26 -0
  11. package/examples/GraphicIndicator.js +7 -5
  12. package/examples/PinePermManage.js +57 -0
  13. package/examples/ReplayMode.js +97 -0
  14. package/examples/UserLogin.js +16 -0
  15. package/main.js +2 -0
  16. package/package.json +6 -9
  17. package/src/FormData.js +36 -0
  18. package/src/chart/session.js +205 -24
  19. package/src/chart/study.js +68 -68
  20. package/src/classes/BuiltInIndicator.js +7 -1
  21. package/src/classes/PineIndicator.js +7 -7
  22. package/src/classes/PinePermManager.js +143 -0
  23. package/src/client.js +15 -10
  24. package/src/miscRequests.js +144 -101
  25. package/src/protocol.js +7 -1
  26. package/src/quote/session.js +1 -0
  27. package/src/request.js +37 -0
  28. package/src/types.js +6 -2
  29. package/src/utils.js +6 -0
  30. package/test.js +58 -147
  31. package/tests/1. Search.js +18 -0
  32. package/tests/2. QuoteSession.js +54 -0
  33. package/tests/3. SimpleChart.js +61 -0
  34. package/tests/4. Indicators.js +100 -0
  35. package/tests/5. BuiltInIndicator.js +41 -0
  36. package/tests/6. FakeReplayMode.js +59 -0
  37. package/tests/7. ReplayMode.js +51 -0
  38. package/tests/8. CustomChartTypes.js +115 -0
  39. package/tests/9. AllErrors.js +146 -0
  40. package/.github/FUNDING.yml +0 -1
  41. package/examples/_RenkoIndicator.js +0 -41
  42. package/tests/alerts/discordWH.js +0 -76
  43. package/tests/alerts/drawings.js +0 -41
  44. package/tests/alerts/envLoader.js +0 -11
  45. package/tests/alerts/manualAlert.js +0 -77
  46. package/tests/alerts/sentNotifs.js +0 -16
  47. package/tests/alerts/strategyAlert.js +0 -94
  48. package/tests/analysis.js +0 -13
  49. package/tests/indicator.js +0 -42
  50. package/tests/prices.js +0 -33
  51. package/tests/strategy.js +0 -56
@@ -16,11 +16,11 @@ function getInputs(options) {
16
16
  if (options.pineId) pineInputs.pineId = options.pineId;
17
17
  if (options.pineVersion) pineInputs.pineVersion = options.pineVersion;
18
18
 
19
- Object.keys(options.inputs).forEach((inputID) => {
19
+ Object.keys(options.inputs).forEach((inputID, n) => {
20
20
  const input = options.inputs[inputID];
21
21
 
22
22
  pineInputs[inputID] = {
23
- v: input.value,
23
+ v: (input.type !== 'color') ? input.value : n,
24
24
  f: input.isFake,
25
25
  t: input.type,
26
26
  };
@@ -35,80 +35,80 @@ function getInputs(options) {
35
35
  /**
36
36
  * @typedef {Object} TradeReport Trade report
37
37
 
38
- * @property {Object} entry Trade entry
39
- * @property {string} entry.name Trade name
40
- * @property {'long' | 'short'} entry.type Entry type (long/short)
41
- * @property {number} entry.value Entry price value
42
- * @property {number} entry.time Entry timestamp
43
-
44
- * @property {Object} exit Trade exit
45
- * @property {'' | string} exit.name Trade name ('' if false exit)
46
- * @property {number} exit.value Exit price value
47
- * @property {number} exit.time Exit timestamp
48
-
49
- * @property {number} quantity Trade quantity
50
- * @property {RelAbsValue} profit Trade profit
51
- * @property {RelAbsValue} cumulative Trade cummulative profit
52
- * @property {RelAbsValue} runup Trade run-up
53
- * @property {RelAbsValue} drawdown Trade drawdown
38
+ * @prop {Object} entry Trade entry
39
+ * @prop {string} entry.name Trade name
40
+ * @prop {'long' | 'short'} entry.type Entry type (long/short)
41
+ * @prop {number} entry.value Entry price value
42
+ * @prop {number} entry.time Entry timestamp
43
+
44
+ * @prop {Object} exit Trade exit
45
+ * @prop {'' | string} exit.name Trade name ('' if false exit)
46
+ * @prop {number} exit.value Exit price value
47
+ * @prop {number} exit.time Exit timestamp
48
+
49
+ * @prop {number} quantity Trade quantity
50
+ * @prop {RelAbsValue} profit Trade profit
51
+ * @prop {RelAbsValue} cumulative Trade cummulative profit
52
+ * @prop {RelAbsValue} runup Trade run-up
53
+ * @prop {RelAbsValue} drawdown Trade drawdown
54
54
  */
55
55
 
56
56
  /**
57
57
  * @typedef {Object} PerfReport
58
- * @property {number} avgBarsInTrade Average bars in trade
59
- * @property {number} avgBarsInWinTrade Average bars in winning trade
60
- * @property {number} avgBarsInLossTrade Average bars in losing trade
61
- * @property {number} avgTrade Average trade gain
62
- * @property {number} avgTradePercent Average trade performace
63
- * @property {number} avgLosTrade Average losing trade gain
64
- * @property {number} avgLosTradePercent Average losing trade performace
65
- * @property {number} avgWinTrade Average winning trade gain
66
- * @property {number} avgWinTradePercent Average winning trade performace
67
- * @property {number} commissionPaid Commission paid
68
- * @property {number} grossLoss Gross loss value
69
- * @property {number} grossLossPercent Gross loss percent
70
- * @property {number} grossProfit Gross profit
71
- * @property {number} grossProfitPercent Gross profit percent
72
- * @property {number} largestLosTrade Largest losing trade gain
73
- * @property {number} largestLosTradePercent Largent losing trade performance (percentage)
74
- * @property {number} largestWinTrade Largest winning trade gain
75
- * @property {number} largestWinTradePercent Largest winning trade performance (percentage)
76
- * @property {number} marginCalls Margin calls
77
- * @property {number} maxContractsHeld Max Contracts Held
78
- * @property {number} netProfit Net profit
79
- * @property {number} netProfitPercent Net performance (percentage)
80
- * @property {number} numberOfLosingTrades Number of losing trades
81
- * @property {number} numberOfWiningTrades Number of winning trades
82
- * @property {number} percentProfitable Strategy winrate
83
- * @property {number} profitFactor Profit factor
84
- * @property {number} ratioAvgWinAvgLoss Ratio Average Win / Average Loss
85
- * @property {number} totalOpenTrades Total open trades
86
- * @property {number} totalTrades Total trades
58
+ * @prop {number} avgBarsInTrade Average bars in trade
59
+ * @prop {number} avgBarsInWinTrade Average bars in winning trade
60
+ * @prop {number} avgBarsInLossTrade Average bars in losing trade
61
+ * @prop {number} avgTrade Average trade gain
62
+ * @prop {number} avgTradePercent Average trade performace
63
+ * @prop {number} avgLosTrade Average losing trade gain
64
+ * @prop {number} avgLosTradePercent Average losing trade performace
65
+ * @prop {number} avgWinTrade Average winning trade gain
66
+ * @prop {number} avgWinTradePercent Average winning trade performace
67
+ * @prop {number} commissionPaid Commission paid
68
+ * @prop {number} grossLoss Gross loss value
69
+ * @prop {number} grossLossPercent Gross loss percent
70
+ * @prop {number} grossProfit Gross profit
71
+ * @prop {number} grossProfitPercent Gross profit percent
72
+ * @prop {number} largestLosTrade Largest losing trade gain
73
+ * @prop {number} largestLosTradePercent Largent losing trade performance (percentage)
74
+ * @prop {number} largestWinTrade Largest winning trade gain
75
+ * @prop {number} largestWinTradePercent Largest winning trade performance (percentage)
76
+ * @prop {number} marginCalls Margin calls
77
+ * @prop {number} maxContractsHeld Max Contracts Held
78
+ * @prop {number} netProfit Net profit
79
+ * @prop {number} netProfitPercent Net performance (percentage)
80
+ * @prop {number} numberOfLosingTrades Number of losing trades
81
+ * @prop {number} numberOfWiningTrades Number of winning trades
82
+ * @prop {number} percentProfitable Strategy winrate
83
+ * @prop {number} profitFactor Profit factor
84
+ * @prop {number} ratioAvgWinAvgLoss Ratio Average Win / Average Loss
85
+ * @prop {number} totalOpenTrades Total open trades
86
+ * @prop {number} totalTrades Total trades
87
87
  */
88
88
 
89
89
  /**
90
90
  * @typedef {Object} StrategyReport
91
- * @property {'EUR' | 'USD' | 'JPY' | '' | 'CHF'} [currency] Selected currency
92
- * @property {TradeReport[]} trades Trade list starting by the last
93
- * @property {Object} history History Chart value
94
- * @property {number[]} [history.buyHold] Buy hold values
95
- * @property {number[]} [history.buyHoldPercent] Buy hold percent values
96
- * @property {number[]} [history.drawDown] Drawdown values
97
- * @property {number[]} [history.drawDownPercent] Drawdown percent values
98
- * @property {number[]} [history.equity] Equity values
99
- * @property {number[]} [history.equityPercent] Equity percent values
100
- * @property {Object} performance Strategy performance
101
- * @property {PerfReport} [performance.all] Strategy long/short performances
102
- * @property {PerfReport} [performance.long] Strategy long performances
103
- * @property {PerfReport} [performance.short] Strategy short performances
104
- * @property {number} [performance.buyHoldReturn] Strategy Buy & Hold Return
105
- * @property {number} [performance.buyHoldReturnPercent] Strategy Buy & Hold Return percent
106
- * @property {number} [performance.maxDrawDown] Strategy max drawdown
107
- * @property {number} [performance.maxDrawDownPercent] Strategy max drawdown percent
108
- * @property {number} [performance.openPL] Strategy Open P&L (Profit And Loss)
109
- * @property {number} [performance.openPLPercent] Strategy Open P&L (Profit And Loss) percent
110
- * @property {number} [performance.sharpeRatio] Strategy Sharpe Ratio
111
- * @property {number} [performance.sortinoRatio] Strategy Sortino Ratio
91
+ * @prop {'EUR' | 'USD' | 'JPY' | '' | 'CHF'} [currency] Selected currency
92
+ * @prop {TradeReport[]} trades Trade list starting by the last
93
+ * @prop {Object} history History Chart value
94
+ * @prop {number[]} [history.buyHold] Buy hold values
95
+ * @prop {number[]} [history.buyHoldPercent] Buy hold percent values
96
+ * @prop {number[]} [history.drawDown] Drawdown values
97
+ * @prop {number[]} [history.drawDownPercent] Drawdown percent values
98
+ * @prop {number[]} [history.equity] Equity values
99
+ * @prop {number[]} [history.equityPercent] Equity percent values
100
+ * @prop {Object} performance Strategy performance
101
+ * @prop {PerfReport} [performance.all] Strategy long/short performances
102
+ * @prop {PerfReport} [performance.long] Strategy long performances
103
+ * @prop {PerfReport} [performance.short] Strategy short performances
104
+ * @prop {number} [performance.buyHoldReturn] Strategy Buy & Hold Return
105
+ * @prop {number} [performance.buyHoldReturnPercent] Strategy Buy & Hold Return percent
106
+ * @prop {number} [performance.maxDrawDown] Strategy max drawdown
107
+ * @prop {number} [performance.maxDrawDownPercent] Strategy max drawdown percent
108
+ * @prop {number} [performance.openPL] Strategy Open P&L (Profit And Loss)
109
+ * @prop {number} [performance.openPLPercent] Strategy Open P&L (Profit And Loss) percent
110
+ * @prop {number} [performance.sharpeRatio] Strategy Sharpe Ratio
111
+ * @prop {number} [performance.sortinoRatio] Strategy Sortino Ratio
112
112
  */
113
113
 
114
114
  /**
@@ -1,5 +1,6 @@
1
1
  /**
2
- * @typedef {'VbPFixed@tv-basicstudies-139!'
2
+ * @typedef {'Volume@tv-basicstudies-144'
3
+ * | 'VbPFixed@tv-basicstudies-139!'
3
4
  * | 'VbPFixed@tv-volumebyprice-53!'
4
5
  * | 'VbPSessions@tv-volumebyprice-53'
5
6
  * | 'VbPSessionsRough@tv-volumebyprice-53!'
@@ -16,6 +17,10 @@
16
17
  */
17
18
 
18
19
  const defaultValues = {
20
+ 'Volume@tv-basicstudies-144': {
21
+ length: 20,
22
+ col_prev_close: false,
23
+ },
19
24
  'VbPFixed@tv-basicstudies-139!': {
20
25
  rowsLayout: 'Number Of Rows',
21
26
  rows: 24,
@@ -63,6 +68,7 @@ const defaultValues = {
63
68
  },
64
69
  };
65
70
 
71
+ /** @class */
66
72
  module.exports = class BuiltInIndicator {
67
73
  /** @type {BuiltInIndicatorType} */
68
74
  #type;
@@ -4,7 +4,9 @@
4
4
  * @property {string} inline Input inline name
5
5
  * @property {string} [internalID] Input internal ID
6
6
  * @property {string} [tooltip] Input tooltip
7
- * @property {'text' | 'source' | 'integer' | 'float' | 'resolution' | 'bool'} type Input type
7
+ * @property {'text' | 'source' | 'integer'
8
+ * | 'float' | 'resolution' | 'bool' | 'color'
9
+ * } type Input type
8
10
  * @property {string | number | boolean} value Input default value
9
11
  * @property {boolean} isHidden If the input is hidden
10
12
  * @property {boolean} isFake If the input is fake
@@ -26,6 +28,8 @@
26
28
  * @typedef {'Script@tv-scripting-101!'
27
29
  * | 'StrategyScript@tv-scripting-101!'} IndicatorType Indicator type
28
30
  */
31
+
32
+ /** @class */
29
33
  module.exports = class PineIndicator {
30
34
  #options;
31
35
 
@@ -113,12 +117,8 @@ module.exports = class PineIndicator {
113
117
  text: 'String',
114
118
  };
115
119
 
116
- if (
117
- (input.type === 'bool' && typeof value !== 'boolean')
118
- || (input.type === 'integer' && typeof value !== 'number')
119
- || (input.type === 'float' && typeof value !== 'number')
120
- || (input.type === 'text' && typeof value !== 'string')
121
- ) {
120
+ // eslint-disable-next-line valid-typeof
121
+ if (types[input.type] && typeof value !== types[input.type].toLowerCase()) {
122
122
  throw new Error(`Input '${input.name}' (${propI}) must be a ${types[input.type]} !`);
123
123
  }
124
124
 
@@ -0,0 +1,143 @@
1
+ const request = require('../request');
2
+ const FormData = require('../FormData');
3
+
4
+ /**
5
+ * @typedef {Object} AuthorizationUser
6
+ * @prop {id} id User id
7
+ * @prop {string} username User's username
8
+ * @prop {string} userpic User's profile picture URL
9
+ * @prop {string} expiration Authorization expiration date
10
+ * @prop {string} created Authorization creation date
11
+ */
12
+
13
+ /** @class */
14
+ class PinePermManager {
15
+ sessionId;
16
+
17
+ pineId;
18
+
19
+ /**
20
+ * Creates a PinePermManager instance
21
+ * @param {string} sessionId Token from sessionid cookie
22
+ * @param {string} pineId Indicator ID (Like: PUB;XXXXXXXXXXXXXXXXXXXXX)
23
+ */
24
+ constructor(sessionId, pineId) {
25
+ if (!sessionId) throw new Error('Please provide a SessionID');
26
+ if (!pineId) throw new Error('Please provide a PineID');
27
+ this.pineId = pineId;
28
+ this.sessionId = sessionId;
29
+ }
30
+
31
+ /**
32
+ * Get list of authorized users
33
+ * @param {number} limit Fetching limit
34
+ * @param {'user__username'
35
+ * | '-user__username'
36
+ * | 'created' | 'created'
37
+ * | 'expiration,user__username'
38
+ * | '-expiration,user__username'
39
+ * } order Fetching order
40
+ * @returns {AuthorizationUser[]}
41
+ */
42
+ async getUsers(limit = 10, order = '-created') {
43
+ const { data } = await request({
44
+ method: 'POST',
45
+ host: 'www.tradingview.com',
46
+ path: `/pine_perm/list_users/?limit=${limit}&order_by=${order}`,
47
+ headers: {
48
+ origin: 'https://www.tradingview.com',
49
+ 'Content-Type': 'application/x-www-form-urlencoded',
50
+ cookie: `sessionid=${this.sessionId}`,
51
+ },
52
+ }, false, `pine_id=${this.pineId.replace(/;/g, '%3B')}`);
53
+
54
+ if (!data.results) throw new Error('Wrong sessionId or pineId');
55
+
56
+ return data.results;
57
+ }
58
+
59
+ /**
60
+ * Adds an user to the authorized list
61
+ * @param {string} username User's username
62
+ * @param {Date} [expiration] Expiration date
63
+ * @returns {'ok' | 'exists' | null}
64
+ */
65
+ async addUser(username, expiration = null) {
66
+ const formData = new FormData();
67
+ formData.append('pine_id', this.pineId);
68
+ formData.append('username_recip', username);
69
+ if (expiration && expiration instanceof Date) {
70
+ formData.append('expiration', expiration.toString());
71
+ }
72
+
73
+ const { data } = await request({
74
+ method: 'POST',
75
+ host: 'www.tradingview.com',
76
+ path: '/pine_perm/add/',
77
+ headers: {
78
+ origin: 'https://www.tradingview.com',
79
+ 'Content-Type': `multipart/form-data; boundary=${formData.boundary}`,
80
+ cookie: `sessionid=${this.sessionId}`,
81
+ },
82
+ }, false, formData.toString());
83
+
84
+ if (!data.status) throw new Error('Wrong sessionId or pineId');
85
+ return data.status;
86
+ }
87
+
88
+ /**
89
+ * Modify an authorization expiration date
90
+ * @param {string} username User's username
91
+ * @param {Date} [expiration] New expiration date
92
+ * @returns {'ok' | null}
93
+ */
94
+ async modifyExpiration(username, expiration = null) {
95
+ const formData = new FormData();
96
+ formData.append('pine_id', this.pineId);
97
+ formData.append('username_recip', username);
98
+ if (expiration && expiration instanceof Date) {
99
+ formData.append('expiration', expiration.toISOString());
100
+ }
101
+
102
+ const { data } = await request({
103
+ method: 'POST',
104
+ host: 'www.tradingview.com',
105
+ path: '/pine_perm/modify_user_expiration/',
106
+ headers: {
107
+ origin: 'https://www.tradingview.com',
108
+ 'Content-Type': `multipart/form-data; boundary=${formData.boundary}`,
109
+ cookie: `sessionid=${this.sessionId}`,
110
+ },
111
+ }, false, formData.toString());
112
+
113
+ if (!data.status) throw new Error('Wrong sessionId or pineId');
114
+ return data.status;
115
+ }
116
+
117
+ /**
118
+ * Removes an user to the authorized list
119
+ * @param {string} username User's username
120
+ * @returns {'ok' | null}
121
+ */
122
+ async removeUser(username) {
123
+ const formData = new FormData();
124
+ formData.append('pine_id', this.pineId);
125
+ formData.append('username_recip', username);
126
+
127
+ const { data } = await request({
128
+ method: 'POST',
129
+ host: 'www.tradingview.com',
130
+ path: '/pine_perm/remove/',
131
+ headers: {
132
+ origin: 'https://www.tradingview.com',
133
+ 'Content-Type': `multipart/form-data; boundary=${formData.boundary}`,
134
+ cookie: `sessionid=${this.sessionId}`,
135
+ },
136
+ }, false, formData.toString());
137
+
138
+ if (!data.status) throw new Error('Wrong sessionId or pineId');
139
+ return data.status;
140
+ }
141
+ }
142
+
143
+ module.exports = PinePermManager;
package/src/client.js CHANGED
@@ -15,9 +15,10 @@ const chartSessionGenerator = require('./chart/session');
15
15
  /** @typedef {Object<string, Session>} SessionList Session list */
16
16
 
17
17
  /**
18
- * @typedef {(t: string, p: []) => null} SendPacket Send a custom packet
18
+ * @callback SendPacket Send a custom packet
19
19
  * @param {string} t Packet type
20
20
  * @param {string[]} p Packet data
21
+ * @returns {void}
21
22
  */
22
23
 
23
24
  /**
@@ -33,6 +34,7 @@ const chartSessionGenerator = require('./chart/session');
33
34
  * } ClientEvent
34
35
  */
35
36
 
37
+ /** @class */
36
38
  module.exports = class Client {
37
39
  #ws;
38
40
 
@@ -79,7 +81,7 @@ module.exports = class Client {
79
81
  /**
80
82
  * When client is connected
81
83
  * @param {() => void} cb Callback
82
- * @event
84
+ * @event onConnected
83
85
  */
84
86
  onConnected(cb) {
85
87
  this.#callbacks.connected.push(cb);
@@ -88,7 +90,7 @@ module.exports = class Client {
88
90
  /**
89
91
  * When client is disconnected
90
92
  * @param {() => void} cb Callback
91
- * @event
93
+ * @event onDisconnected
92
94
  */
93
95
  onDisconnected(cb) {
94
96
  this.#callbacks.disconnected.push(cb);
@@ -105,10 +107,12 @@ module.exports = class Client {
105
107
  * @prop {string} javastudies Javastudies
106
108
  * @prop {number} auth_scheme_vsn Auth scheme type
107
109
  * @prop {string} via Socket IP
110
+ */
108
111
 
112
+ /**
109
113
  * When client is logged
110
114
  * @param {(SocketSession: SocketSession) => void} cb Callback
111
- * @event
115
+ * @event onLogged
112
116
  */
113
117
  onLogged(cb) {
114
118
  this.#callbacks.logged.push(cb);
@@ -117,7 +121,7 @@ module.exports = class Client {
117
121
  /**
118
122
  * When server is pinging the client
119
123
  * @param {(i: number) => void} cb Callback
120
- * @event
124
+ * @event onPing
121
125
  */
122
126
  onPing(cb) {
123
127
  this.#callbacks.ping.push(cb);
@@ -126,7 +130,7 @@ module.exports = class Client {
126
130
  /**
127
131
  * When unparsed data is received
128
132
  * @param {(...{}) => void} cb Callback
129
- * @event
133
+ * @event onData
130
134
  */
131
135
  onData(cb) {
132
136
  this.#callbacks.data.push(cb);
@@ -135,7 +139,7 @@ module.exports = class Client {
135
139
  /**
136
140
  * When a client error happens
137
141
  * @param {(...{}) => void} cb Callback
138
- * @event
142
+ * @event onError
139
143
  */
140
144
  onError(cb) {
141
145
  this.#callbacks.error.push(cb);
@@ -144,7 +148,7 @@ module.exports = class Client {
144
148
  /**
145
149
  * When a client event happens
146
150
  * @param {(...{}) => void} cb Callback
147
- * @event
151
+ * @event onEvent
148
152
  */
149
153
  onEvent(cb) {
150
154
  this.#callbacks.event.push(cb);
@@ -203,6 +207,7 @@ module.exports = class Client {
203
207
  while (this.isOpen && this.#logged && this.#sendQueue.length > 0) {
204
208
  const packet = this.#sendQueue.shift();
205
209
  this.#ws.send(packet);
210
+ if (global.TW_DEBUG) console.log('§90§30§107 > §0', packet);
206
211
  }
207
212
  }
208
213
 
@@ -218,7 +223,7 @@ module.exports = class Client {
218
223
  constructor(clientOptions = {}) {
219
224
  if (clientOptions.DEBUG) global.TW_DEBUG = clientOptions.DEBUG;
220
225
 
221
- this.#ws = new WebSocket('wss://widgetdata.tradingview.com/socket.io/websocket', {
226
+ this.#ws = new WebSocket('wss://data.tradingview.com/socket.io/websocket', {
222
227
  origin: 'https://s.tradingview.com',
223
228
  });
224
229
 
@@ -273,7 +278,7 @@ module.exports = class Client {
273
278
  */
274
279
  end() {
275
280
  return new Promise((cb) => {
276
- this.#ws.close();
281
+ if (this.#ws.readyState) this.#ws.close();
277
282
  cb();
278
283
  });
279
284
  }