@mathieuc/tradingview 2.0.5 → 3.0.0-pre

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/src/client.js ADDED
@@ -0,0 +1,280 @@
1
+ const WebSocket = require('ws');
2
+
3
+ const misc = require('./miscRequests');
4
+ const protocol = require('./protocol');
5
+
6
+ const quoteSessionGenerator = require('./quote/session');
7
+ const chartSessionGenerator = require('./chart/session');
8
+
9
+ /**
10
+ * @typedef {Object} Session
11
+ * @prop {'quote' | 'chart' | 'replay'} type Session type
12
+ * @prop {(data: {}) => null} onData When there is a data
13
+ */
14
+
15
+ /** @typedef {Object<string, Session>} SessionList Session list */
16
+
17
+ /**
18
+ * @typedef {(t: string, p: []) => null} SendPacket Send a custom packet
19
+ * @param {string} t Packet type
20
+ * @param {string[]} p Packet data
21
+ */
22
+
23
+ /**
24
+ * @typedef {Object} ClientBridge
25
+ * @prop {SessionList} sessions
26
+ * @prop {SendPacket} send
27
+ */
28
+
29
+ /**
30
+ * @typedef { 'connected' | 'disconnected'
31
+ * | 'logged' | 'ping' | 'data'
32
+ * | 'error' | 'event'
33
+ * } ClientEvent
34
+ */
35
+
36
+ module.exports = class Client {
37
+ #ws;
38
+
39
+ #logged = false;
40
+
41
+ /** If the client is logged in */
42
+ get isLogged() {
43
+ return this.#logged;
44
+ }
45
+
46
+ /** If the cient was closed */
47
+ get isOpen() {
48
+ return this.#ws.readyState === this.#ws.OPEN;
49
+ }
50
+
51
+ /** @type {SessionList} */
52
+ #sessions = {};
53
+
54
+ #callbacks = {
55
+ connected: [],
56
+ disconnected: [],
57
+ logged: [],
58
+ ping: [],
59
+ data: [],
60
+
61
+ error: [],
62
+ event: [],
63
+ };
64
+
65
+ /**
66
+ * @param {ClientEvent} ev Client event
67
+ * @param {...{}} data Packet data
68
+ */
69
+ #handleEvent(ev, ...data) {
70
+ this.#callbacks[ev].forEach((e) => e(...data));
71
+ this.#callbacks.event.forEach((e) => e(ev, ...data));
72
+ }
73
+
74
+ #handleError(...msgs) {
75
+ if (this.#callbacks.error.length === 0) console.error(...msgs);
76
+ else this.#handleEvent('error', ...msgs);
77
+ }
78
+
79
+ /**
80
+ * When client is connected
81
+ * @param {() => void} cb Callback
82
+ * @event
83
+ */
84
+ onConnected(cb) {
85
+ this.#callbacks.connected.push(cb);
86
+ }
87
+
88
+ /**
89
+ * When client is disconnected
90
+ * @param {() => void} cb Callback
91
+ * @event
92
+ */
93
+ onDisconnected(cb) {
94
+ this.#callbacks.disconnected.push(cb);
95
+ }
96
+
97
+ /**
98
+ * @typedef {Object} SocketSession
99
+ * @prop {string} session_id Socket session ID
100
+ * @prop {number} timestamp Session start timestamp
101
+ * @prop {number} timestampMs Session start milliseconds timestamp
102
+ * @prop {string} release Release
103
+ * @prop {string} studies_metadata_hash Studies metadata hash
104
+ * @prop {'json' | string} protocol Used protocol
105
+ * @prop {string} javastudies Javastudies
106
+ * @prop {number} auth_scheme_vsn Auth scheme type
107
+ * @prop {string} via Socket IP
108
+
109
+ * When client is logged
110
+ * @param {(SocketSession: SocketSession) => void} cb Callback
111
+ * @event
112
+ */
113
+ onLogged(cb) {
114
+ this.#callbacks.logged.push(cb);
115
+ }
116
+
117
+ /**
118
+ * When server is pinging the client
119
+ * @param {(i: number) => void} cb Callback
120
+ * @event
121
+ */
122
+ onPing(cb) {
123
+ this.#callbacks.ping.push(cb);
124
+ }
125
+
126
+ /**
127
+ * When unparsed data is received
128
+ * @param {(...{}) => void} cb Callback
129
+ * @event
130
+ */
131
+ onData(cb) {
132
+ this.#callbacks.data.push(cb);
133
+ }
134
+
135
+ /**
136
+ * When a client error happens
137
+ * @param {(...{}) => void} cb Callback
138
+ * @event
139
+ */
140
+ onError(cb) {
141
+ this.#callbacks.error.push(cb);
142
+ }
143
+
144
+ /**
145
+ * When a client event happens
146
+ * @param {(...{}) => void} cb Callback
147
+ * @event
148
+ */
149
+ onEvent(cb) {
150
+ this.#callbacks.event.push(cb);
151
+ }
152
+
153
+ #parsePacket(str) {
154
+ if (!this.isOpen) return;
155
+
156
+ protocol.parseWSPacket(str).forEach((packet) => {
157
+ if (global.TW_DEBUG) console.log('§90§30§107 CLIENT §0 PACKET', packet);
158
+ if (typeof packet === 'number') { // Ping
159
+ this.#ws.send(protocol.formatWSPacket(`~h~${packet}`));
160
+ this.#handleEvent('ping', packet);
161
+ return;
162
+ }
163
+
164
+ if (packet.m === 'protocol_error') { // Error
165
+ this.#handleError('Client critical error:', packet.p);
166
+ this.#ws.close();
167
+ return;
168
+ }
169
+
170
+ if (packet.m && packet.p) { // Normal packet
171
+ const parsed = {
172
+ type: packet.m,
173
+ data: packet.p,
174
+ };
175
+
176
+ const session = packet.p[0];
177
+
178
+ if (session && this.#sessions[session]) {
179
+ this.#sessions[session].onData(parsed);
180
+ return;
181
+ }
182
+ }
183
+
184
+ if (!this.#logged) {
185
+ this.#handleEvent('logged', packet);
186
+ return;
187
+ }
188
+
189
+ this.#handleEvent('data', packet);
190
+ });
191
+ }
192
+
193
+ #sendQueue = [];
194
+
195
+ /** @type {SendPacket} Send a custom packet */
196
+ send(t, p = []) {
197
+ this.#sendQueue.push(protocol.formatWSPacket({ m: t, p }));
198
+ this.sendQueue();
199
+ }
200
+
201
+ /** Send all waiting packets */
202
+ sendQueue() {
203
+ while (this.isOpen && this.#logged && this.#sendQueue.length > 0) {
204
+ const packet = this.#sendQueue.shift();
205
+ this.#ws.send(packet);
206
+ }
207
+ }
208
+
209
+ /**
210
+ * @typedef {Object} ClientOptions
211
+ * @prop {string} [token] User auth token (in 'sessionid' cookie)
212
+ * @prop {boolean} [DEBUG] Enable debug mode
213
+ */
214
+
215
+ /** Client object
216
+ * @param {ClientOptions} clientOptions TradingView client options
217
+ */
218
+ constructor(clientOptions = {}) {
219
+ if (clientOptions.DEBUG) global.TW_DEBUG = clientOptions.DEBUG;
220
+
221
+ this.#ws = new WebSocket('wss://widgetdata.tradingview.com/socket.io/websocket', {
222
+ origin: 'https://s.tradingview.com',
223
+ });
224
+
225
+ if (clientOptions.token) {
226
+ misc.getUser(clientOptions.token).then((user) => {
227
+ this.#sendQueue.unshift(protocol.formatWSPacket({
228
+ m: 'set_auth_token',
229
+ p: [user.authToken],
230
+ }));
231
+ this.#logged = true;
232
+ this.sendQueue();
233
+ }).catch((err) => {
234
+ this.#handleError('Credentials error:', err.message);
235
+ });
236
+ } else {
237
+ this.#sendQueue.unshift(protocol.formatWSPacket({
238
+ m: 'set_auth_token',
239
+ p: ['unauthorized_user_token'],
240
+ }));
241
+ this.#logged = true;
242
+ this.sendQueue();
243
+ }
244
+
245
+ this.#ws.on('open', () => {
246
+ this.#handleEvent('connected');
247
+ this.sendQueue();
248
+ });
249
+
250
+ this.#ws.on('close', () => {
251
+ this.#logged = false;
252
+ this.#handleEvent('disconnected');
253
+ });
254
+
255
+ this.#ws.on('message', (data) => this.#parsePacket(data));
256
+ }
257
+
258
+ /** @type {ClientBridge} */
259
+ #clientBridge = {
260
+ sessions: this.#sessions,
261
+ send: (t, p) => this.send(t, p),
262
+ };
263
+
264
+ /** @namespace Session */
265
+ Session = {
266
+ Quote: quoteSessionGenerator(this.#clientBridge),
267
+ Chart: chartSessionGenerator(this.#clientBridge),
268
+ };
269
+
270
+ /**
271
+ * Close the websocket connection
272
+ * @return {Promise<void>} When websocket is closed
273
+ */
274
+ end() {
275
+ return new Promise((cb) => {
276
+ this.#ws.close();
277
+ cb();
278
+ });
279
+ }
280
+ };