laplace-api 1.3.4 → 1.4.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/package.json
CHANGED
|
@@ -1,9 +1,49 @@
|
|
|
1
|
+
|
|
2
|
+
interface RawBISTStockLiveData {
|
|
3
|
+
_id: number;
|
|
4
|
+
symbol: string;
|
|
5
|
+
cl: number;
|
|
6
|
+
_i: string;
|
|
7
|
+
c: number;
|
|
8
|
+
d: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface RawUSStockLiveData {
|
|
12
|
+
s: string;
|
|
13
|
+
p: number;
|
|
14
|
+
t: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
1
17
|
export interface BISTStockLiveData {
|
|
18
|
+
id: number;
|
|
19
|
+
symbol: string;
|
|
20
|
+
closePrice: number;
|
|
21
|
+
tipId: string;
|
|
22
|
+
percentChange: number;
|
|
23
|
+
timestamp: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface USStockLiveData {
|
|
2
27
|
symbol: string;
|
|
3
|
-
|
|
4
|
-
|
|
28
|
+
closePrice: number;
|
|
29
|
+
timestamp: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export enum LivePriceFeed {
|
|
33
|
+
LiveBist = "live_price_tr",
|
|
34
|
+
LiveUs = "live_price_us",
|
|
35
|
+
DelayedBist = "delayed_price_tr",
|
|
36
|
+
DelayedUs = "delayed_price_us",
|
|
37
|
+
// DepthBist = "depth_tr",
|
|
5
38
|
}
|
|
6
39
|
|
|
40
|
+
type StockLiveDataType<T extends LivePriceFeed> = T extends
|
|
41
|
+
| LivePriceFeed.LiveBist
|
|
42
|
+
| LivePriceFeed.DelayedBist
|
|
43
|
+
? // | LivePriceFeed.DepthBist
|
|
44
|
+
BISTStockLiveData
|
|
45
|
+
: USStockLiveData;
|
|
46
|
+
|
|
7
47
|
export enum LogLevel {
|
|
8
48
|
Info = "info",
|
|
9
49
|
Warn = "warn",
|
|
@@ -18,12 +58,7 @@ interface WebSocketOptions {
|
|
|
18
58
|
maxReconnectDelay?: number;
|
|
19
59
|
}
|
|
20
60
|
|
|
21
|
-
type WebSocketMessageType = "heartbeat" | "error" | "warning" | "
|
|
22
|
-
|
|
23
|
-
interface WebSocketMessage<T> {
|
|
24
|
-
type: WebSocketMessageType;
|
|
25
|
-
message?: T;
|
|
26
|
-
}
|
|
61
|
+
type WebSocketMessageType = "heartbeat" | "error" | "warning" | "data";
|
|
27
62
|
|
|
28
63
|
export enum WebSocketErrorType {
|
|
29
64
|
MAX_RECONNECT_EXCEEDED = "MAX_RECONNECT_EXCEEDED",
|
|
@@ -60,7 +95,8 @@ export class LivePriceWebSocketClient {
|
|
|
60
95
|
number,
|
|
61
96
|
{
|
|
62
97
|
symbols: string[];
|
|
63
|
-
handler: (data: BISTStockLiveData) => void;
|
|
98
|
+
handler: (data: BISTStockLiveData | USStockLiveData) => void;
|
|
99
|
+
feed: LivePriceFeed;
|
|
64
100
|
}
|
|
65
101
|
>();
|
|
66
102
|
private reconnectAttempts = 0;
|
|
@@ -117,10 +153,9 @@ export class LivePriceWebSocketClient {
|
|
|
117
153
|
this.ws = new WebSocket(url);
|
|
118
154
|
this.connectPromise = this.setupWebSocket();
|
|
119
155
|
|
|
120
|
-
await this.connectPromise
|
|
156
|
+
await this.connectPromise;
|
|
121
157
|
|
|
122
158
|
this.connectPromise = null;
|
|
123
|
-
|
|
124
159
|
}
|
|
125
160
|
|
|
126
161
|
return this.ws;
|
|
@@ -191,19 +226,48 @@ export class LivePriceWebSocketClient {
|
|
|
191
226
|
this.ws.onmessage = (event) => {
|
|
192
227
|
try {
|
|
193
228
|
const rawData = JSON.parse(event.data.toString());
|
|
229
|
+
|
|
230
|
+
const feed = rawData.feed as LivePriceFeed;
|
|
194
231
|
switch (rawData.type) {
|
|
195
|
-
case "
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
if (!data) {
|
|
232
|
+
case "data":
|
|
233
|
+
const messageData = rawData.message;
|
|
234
|
+
if (!messageData) {
|
|
199
235
|
throw new WebSocketError(
|
|
200
236
|
"Price update message is empty",
|
|
201
237
|
WebSocketErrorType.MESSAGE_PARSE_ERROR
|
|
202
238
|
);
|
|
203
239
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
240
|
+
let priceData: BISTStockLiveData | USStockLiveData;
|
|
241
|
+
|
|
242
|
+
if (
|
|
243
|
+
feed === LivePriceFeed.DelayedBist ||
|
|
244
|
+
feed === LivePriceFeed.LiveBist
|
|
245
|
+
// ||
|
|
246
|
+
// feed === LivePriceFeed.DepthBist
|
|
247
|
+
) {
|
|
248
|
+
const message = messageData as RawBISTStockLiveData;
|
|
249
|
+
priceData = {
|
|
250
|
+
symbol: message?.symbol,
|
|
251
|
+
id: message?._id,
|
|
252
|
+
tipId: message?._i,
|
|
253
|
+
closePrice: message?.cl,
|
|
254
|
+
timestamp: message?.d,
|
|
255
|
+
percentChange: message?.c,
|
|
256
|
+
} as BISTStockLiveData;
|
|
257
|
+
} else {
|
|
258
|
+
const message = messageData as RawUSStockLiveData;
|
|
259
|
+
priceData = {
|
|
260
|
+
symbol: message.s,
|
|
261
|
+
closePrice: message.p,
|
|
262
|
+
timestamp: message.t,
|
|
263
|
+
} as USStockLiveData;
|
|
264
|
+
}
|
|
265
|
+
if (priceData.symbol) {
|
|
266
|
+
const handlers = this.getHandlersForSymbol(
|
|
267
|
+
priceData.symbol,
|
|
268
|
+
feed
|
|
269
|
+
);
|
|
270
|
+
handlers.forEach((handler) => handler(priceData));
|
|
207
271
|
}
|
|
208
272
|
break;
|
|
209
273
|
case "heartbeat":
|
|
@@ -226,14 +290,6 @@ export class LivePriceWebSocketClient {
|
|
|
226
290
|
});
|
|
227
291
|
}
|
|
228
292
|
|
|
229
|
-
private getActiveSymbols(): string[] {
|
|
230
|
-
const allSymbols = Array.from(this.subscriptions.values()).flatMap(
|
|
231
|
-
(sub) => sub.symbols
|
|
232
|
-
);
|
|
233
|
-
|
|
234
|
-
return [...new Set(allSymbols)];
|
|
235
|
-
}
|
|
236
|
-
|
|
237
293
|
private async attemptReconnect() {
|
|
238
294
|
const url = this.wsUrl;
|
|
239
295
|
if (!url) {
|
|
@@ -267,11 +323,28 @@ export class LivePriceWebSocketClient {
|
|
|
267
323
|
this.reconnectTimeout = setTimeout(async () => {
|
|
268
324
|
try {
|
|
269
325
|
await this.connect(url);
|
|
270
|
-
|
|
271
|
-
this.isClosed = false
|
|
272
326
|
|
|
273
|
-
|
|
274
|
-
|
|
327
|
+
this.isClosed = false;
|
|
328
|
+
|
|
329
|
+
const symbolsByFeed = new Map<LivePriceFeed, string[]>();
|
|
330
|
+
|
|
331
|
+
this.subscriptions.forEach((subscription) => {
|
|
332
|
+
const { symbols, feed } = subscription;
|
|
333
|
+
if (!symbolsByFeed.has(feed)) {
|
|
334
|
+
symbolsByFeed.set(feed, []);
|
|
335
|
+
}
|
|
336
|
+
symbols.forEach((symbol) => {
|
|
337
|
+
const currentSymbols = symbolsByFeed.get(feed) || [];
|
|
338
|
+
if (!currentSymbols.includes(symbol)) {
|
|
339
|
+
currentSymbols.push(symbol);
|
|
340
|
+
symbolsByFeed.set(feed, currentSymbols);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
symbolsByFeed.forEach((symbols, feed) => {
|
|
346
|
+
this.addSymbols(symbols, feed);
|
|
347
|
+
});
|
|
275
348
|
|
|
276
349
|
if (this.reconnectTimeout) {
|
|
277
350
|
clearTimeout(this.reconnectTimeout);
|
|
@@ -288,40 +361,50 @@ export class LivePriceWebSocketClient {
|
|
|
288
361
|
}
|
|
289
362
|
}
|
|
290
363
|
|
|
291
|
-
subscribe(
|
|
364
|
+
subscribe<F extends LivePriceFeed>(
|
|
292
365
|
symbols: string[],
|
|
293
|
-
|
|
366
|
+
feed: F,
|
|
367
|
+
handler: (data: StockLiveDataType<F>) => void
|
|
294
368
|
): () => void {
|
|
295
369
|
const subscriptionId = this.subscriptionCounter++;
|
|
296
370
|
let symbolsToAdd: string[] = [];
|
|
297
371
|
|
|
298
|
-
|
|
372
|
+
const typedHandler = (data: BISTStockLiveData | USStockLiveData) => {
|
|
373
|
+
handler(data as StockLiveDataType<F>);
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
this.subscriptions.set(subscriptionId, {
|
|
377
|
+
symbols,
|
|
378
|
+
feed,
|
|
379
|
+
handler: typedHandler,
|
|
380
|
+
});
|
|
299
381
|
|
|
300
382
|
for (const symbol of symbols) {
|
|
301
|
-
if (this.getHandlersForSymbol(symbol).length === 1) {
|
|
383
|
+
if (this.getHandlersForSymbol(symbol, feed).length === 1) {
|
|
302
384
|
symbolsToAdd.push(symbol);
|
|
303
385
|
}
|
|
304
386
|
}
|
|
305
|
-
this.addSymbols(symbolsToAdd);
|
|
387
|
+
this.addSymbols(symbolsToAdd, feed);
|
|
306
388
|
|
|
307
389
|
return () => {
|
|
308
390
|
this.subscriptions.delete(subscriptionId);
|
|
309
391
|
const symbolsForRemove = symbols.filter(
|
|
310
|
-
(s) => this.getHandlersForSymbol(s).length === 0
|
|
392
|
+
(s) => this.getHandlersForSymbol(s, feed).length === 0
|
|
311
393
|
);
|
|
312
|
-
this.removeSymbols(symbolsForRemove);
|
|
394
|
+
this.removeSymbols(symbolsForRemove, feed);
|
|
313
395
|
};
|
|
314
396
|
}
|
|
315
397
|
|
|
316
398
|
private getHandlersForSymbol(
|
|
317
|
-
symbol: string
|
|
318
|
-
|
|
399
|
+
symbol: string,
|
|
400
|
+
feed: LivePriceFeed
|
|
401
|
+
): ((data: BISTStockLiveData | USStockLiveData) => void)[] {
|
|
319
402
|
return Array.from(this.subscriptions.values())
|
|
320
|
-
.filter((s) => s.symbols.includes(symbol))
|
|
403
|
+
.filter((s) => s.symbols.includes(symbol) && s.feed === feed)
|
|
321
404
|
.map((s) => s.handler);
|
|
322
405
|
}
|
|
323
406
|
|
|
324
|
-
private async removeSymbols(symbols: string[]) {
|
|
407
|
+
private async removeSymbols(symbols: string[], feed: LivePriceFeed) {
|
|
325
408
|
if (symbols.length === 0) return;
|
|
326
409
|
|
|
327
410
|
if (!this.ws) {
|
|
@@ -332,7 +415,7 @@ export class LivePriceWebSocketClient {
|
|
|
332
415
|
}
|
|
333
416
|
|
|
334
417
|
if (this.connectPromise) {
|
|
335
|
-
await this.connectPromise
|
|
418
|
+
await this.connectPromise;
|
|
336
419
|
}
|
|
337
420
|
|
|
338
421
|
if (this.ws.readyState !== WebSocket.OPEN) {
|
|
@@ -341,15 +424,17 @@ export class LivePriceWebSocketClient {
|
|
|
341
424
|
WebSocketErrorType.WEBSOCKET_NOT_CONNECTED
|
|
342
425
|
);
|
|
343
426
|
}
|
|
427
|
+
|
|
344
428
|
this.ws.send(
|
|
345
429
|
JSON.stringify({
|
|
346
430
|
type: "unsubscribe",
|
|
347
431
|
symbols: symbols,
|
|
432
|
+
feed: feed,
|
|
348
433
|
})
|
|
349
434
|
);
|
|
350
435
|
}
|
|
351
436
|
|
|
352
|
-
private async addSymbols(symbols: string[]) {
|
|
437
|
+
private async addSymbols(symbols: string[], feed: LivePriceFeed) {
|
|
353
438
|
if (symbols.length === 0) return;
|
|
354
439
|
|
|
355
440
|
if (!this.ws) {
|
|
@@ -360,7 +445,7 @@ export class LivePriceWebSocketClient {
|
|
|
360
445
|
}
|
|
361
446
|
|
|
362
447
|
if (this.connectPromise) {
|
|
363
|
-
await this.connectPromise
|
|
448
|
+
await this.connectPromise;
|
|
364
449
|
}
|
|
365
450
|
|
|
366
451
|
if (this.ws.readyState !== WebSocket.OPEN) {
|
|
@@ -374,6 +459,7 @@ export class LivePriceWebSocketClient {
|
|
|
374
459
|
JSON.stringify({
|
|
375
460
|
type: "subscribe",
|
|
376
461
|
symbols: symbols,
|
|
462
|
+
feed: feed,
|
|
377
463
|
})
|
|
378
464
|
);
|
|
379
465
|
}
|
package/src/client/live-price.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Client } from "./client";
|
|
2
2
|
import { Region } from "./collections";
|
|
3
3
|
import { v4 as uuidv4 } from 'uuid';
|
|
4
|
+
import { LivePriceFeed } from "./live-price-web-socket";
|
|
4
5
|
|
|
5
6
|
interface WebSocketUrlResponse {
|
|
6
7
|
url: string;
|
|
@@ -8,6 +9,7 @@ interface WebSocketUrlResponse {
|
|
|
8
9
|
|
|
9
10
|
interface WebSocketUrlParams {
|
|
10
11
|
externalUserId: string;
|
|
12
|
+
feeds: LivePriceFeed[];
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
export interface BISTStockLiveData {
|
|
@@ -39,14 +41,15 @@ function getSSELivePrice<T>(
|
|
|
39
41
|
export class LivePriceClient extends Client {
|
|
40
42
|
async getWebSocketUrl(
|
|
41
43
|
externalUserId: string,
|
|
42
|
-
region: Region
|
|
44
|
+
region: Region,
|
|
45
|
+
feeds: LivePriceFeed[]
|
|
43
46
|
): Promise<string> {
|
|
44
|
-
const url = new URL(`${this["baseUrl"]}/api/
|
|
47
|
+
const url = new URL(`${this["baseUrl"]}/api/v2/ws/url`);
|
|
45
48
|
url.searchParams.append("region", region);
|
|
46
|
-
url.searchParams.append("accessLevel", "KRMD1");
|
|
47
49
|
|
|
48
50
|
const params: WebSocketUrlParams = {
|
|
49
51
|
externalUserId,
|
|
52
|
+
feeds
|
|
50
53
|
};
|
|
51
54
|
|
|
52
55
|
const response = await this.sendRequest<WebSocketUrlResponse>({
|
|
@@ -5,7 +5,9 @@ import "./client_test_suite";
|
|
|
5
5
|
import { LivePriceClient } from "../client/live-price";
|
|
6
6
|
import {
|
|
7
7
|
BISTStockLiveData,
|
|
8
|
+
LivePriceFeed,
|
|
8
9
|
LivePriceWebSocketClient,
|
|
10
|
+
USStockLiveData,
|
|
9
11
|
} from "../client/live-price-web-socket";
|
|
10
12
|
|
|
11
13
|
describe("LivePrice", () => {
|
|
@@ -28,7 +30,9 @@ describe("LivePrice", () => {
|
|
|
28
30
|
} as unknown as Logger;
|
|
29
31
|
|
|
30
32
|
livePriceUrlClient = new LivePriceClient(config, logger);
|
|
31
|
-
url = await livePriceUrlClient.getWebSocketUrl("2459", Region.Tr
|
|
33
|
+
url = await livePriceUrlClient.getWebSocketUrl("2459", Region.Tr, [
|
|
34
|
+
LivePriceFeed.LiveBist,
|
|
35
|
+
]);
|
|
32
36
|
|
|
33
37
|
ws = new LivePriceWebSocketClient({
|
|
34
38
|
enableLogging: true,
|
|
@@ -47,22 +51,28 @@ describe("LivePrice", () => {
|
|
|
47
51
|
|
|
48
52
|
describe("BIST Live Price Tests", () => {
|
|
49
53
|
const symbols = ["TUPRS", "SASA", "THYAO", "GARAN", "YKBNK"];
|
|
50
|
-
// const newSymbols = ["AKBNK", "KCHOL"];
|
|
51
54
|
|
|
52
55
|
it(
|
|
53
56
|
"should receive data for initial and updated symbols",
|
|
54
57
|
async () => {
|
|
55
58
|
const receivedData: BISTStockLiveData[] = [];
|
|
56
59
|
|
|
57
|
-
let unsubscribe: (() => void) | null =
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
let unsubscribe: (() => void) | null =
|
|
61
|
+
ws.subscribe<LivePriceFeed.LiveBist>(
|
|
62
|
+
symbols,
|
|
63
|
+
LivePriceFeed.LiveBist,
|
|
64
|
+
(data) => {
|
|
65
|
+
console.log("RECEIVED DATA", data);
|
|
66
|
+
receivedData.push(data);
|
|
67
|
+
}
|
|
68
|
+
);
|
|
61
69
|
|
|
62
70
|
await new Promise((resolve) => setTimeout(resolve, 20000));
|
|
63
71
|
|
|
64
72
|
for (const symbol of symbols) {
|
|
65
|
-
const symbolData = receivedData.filter(
|
|
73
|
+
const symbolData = receivedData.filter(
|
|
74
|
+
(data) => data.symbol === symbol
|
|
75
|
+
);
|
|
66
76
|
expect(symbolData.length).toBeGreaterThan(0);
|
|
67
77
|
}
|
|
68
78
|
|
|
@@ -70,72 +80,39 @@ describe("LivePrice", () => {
|
|
|
70
80
|
},
|
|
71
81
|
TEST_CONSTANTS.JEST_TIMEOUT
|
|
72
82
|
);
|
|
73
|
-
|
|
74
|
-
// it(
|
|
75
|
-
// "should handle multiple subscriptions for the same symbol",
|
|
76
|
-
// async () => {
|
|
77
|
-
// const symbol = "GARAN";
|
|
78
|
-
// const receivedData1: BISTStockLiveData[] = [];
|
|
79
|
-
// const receivedData2: BISTStockLiveData[] = [];
|
|
80
|
-
|
|
81
|
-
// await new Promise<void>((resolve, reject) => {
|
|
82
|
-
// const timeoutId = setTimeout(() => {
|
|
83
|
-
// reject(new Error("Test timeout: No data received"));
|
|
84
|
-
// }, TEST_CONSTANTS.MAIN_TIMEOUT).unref();
|
|
85
|
-
|
|
86
|
-
// const unsubscribe1 = ws.subscribe([symbol], (data) => {
|
|
87
|
-
// receivedData1.push(data);
|
|
88
|
-
// });
|
|
89
|
-
|
|
90
|
-
// const unsubscribe2 = ws.subscribe([symbol], (data) => {
|
|
91
|
-
// receivedData2.push(data);
|
|
92
|
-
// if (receivedData2.length >= 2) {
|
|
93
|
-
// clearTimeout(timeoutId);
|
|
94
|
-
// unsubscribe1();
|
|
95
|
-
// unsubscribe2();
|
|
96
|
-
// resolve();
|
|
97
|
-
// }
|
|
98
|
-
// });
|
|
99
|
-
// });
|
|
100
|
-
|
|
101
|
-
// expect(receivedData1.length).toBeGreaterThan(0);
|
|
102
|
-
// expect(receivedData2.length).toBeGreaterThan(0);
|
|
103
|
-
// expect(receivedData1).toEqual(receivedData2);
|
|
104
|
-
// },
|
|
105
|
-
// TEST_CONSTANTS.JEST_TIMEOUT
|
|
106
|
-
// );
|
|
107
83
|
});
|
|
108
84
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
85
|
+
//TODO: Use this test after region issue fixed
|
|
86
|
+
// describe("US Live Price Tests", () => {
|
|
87
|
+
// const symbols = ["AAPL"];
|
|
88
|
+
|
|
89
|
+
// it(
|
|
90
|
+
// "should receive data for initial and updated symbols for us",
|
|
91
|
+
// async () => {
|
|
92
|
+
// const receivedData: USStockLiveData[] = [];
|
|
93
|
+
|
|
94
|
+
// let unsubscribe: (() => void) | null =
|
|
95
|
+
// ws.subscribe<LivePriceFeed.LiveUs>(
|
|
96
|
+
// symbols,
|
|
97
|
+
// LivePriceFeed.LiveUs,
|
|
98
|
+
// (data) => {
|
|
99
|
+
// console.log("RECEIVED DATA FOR US", data);
|
|
100
|
+
// receivedData.push(data);
|
|
101
|
+
// }
|
|
102
|
+
// );
|
|
103
|
+
|
|
104
|
+
// await new Promise((resolve) => setTimeout(resolve, 20000));
|
|
105
|
+
|
|
106
|
+
// for (const symbol of symbols) {
|
|
107
|
+
// const symbolData = receivedData.filter(
|
|
108
|
+
// (data) => data.symbol === symbol
|
|
109
|
+
// );
|
|
110
|
+
// expect(symbolData.length).toBeGreaterThan(0);
|
|
111
|
+
// }
|
|
112
|
+
|
|
113
|
+
// unsubscribe();
|
|
114
|
+
// },
|
|
115
|
+
// TEST_CONSTANTS.JEST_TIMEOUT
|
|
116
|
+
// );
|
|
117
|
+
// });
|
|
141
118
|
});
|
package/src/utilities/test.env
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
BASE_URL=
|
|
2
|
-
API_KEY=
|
|
1
|
+
BASE_URL=
|
|
2
|
+
API_KEY=
|