@pythnetwork/pyth-lazer-sdk 6.0.0 → 6.2.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.
- package/dist/cjs/client.cjs +77 -53
- package/dist/cjs/client.d.ts +36 -13
- package/dist/cjs/index.cjs +1 -1
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/protocol.cjs +3 -3
- package/dist/cjs/protocol.d.ts +3 -3
- package/dist/cjs/socket/resilient-websocket.cjs +5 -0
- package/dist/cjs/socket/resilient-websocket.d.ts +1 -0
- package/dist/cjs/socket/websocket-pool.cjs +83 -11
- package/dist/cjs/socket/websocket-pool.d.ts +22 -3
- package/dist/esm/client.d.ts +36 -13
- package/dist/esm/client.mjs +77 -53
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.mjs +1 -1
- package/dist/esm/protocol.d.ts +3 -3
- package/dist/esm/protocol.mjs +3 -3
- package/dist/esm/socket/resilient-websocket.d.ts +1 -0
- package/dist/esm/socket/resilient-websocket.mjs +5 -0
- package/dist/esm/socket/websocket-pool.d.ts +22 -3
- package/dist/esm/socket/websocket-pool.mjs +83 -11
- package/package.json +113 -123
package/dist/cjs/client.cjs
CHANGED
|
@@ -17,27 +17,20 @@ const UINT16_NUM_BYTES = 2;
|
|
|
17
17
|
const UINT32_NUM_BYTES = 4;
|
|
18
18
|
const UINT64_NUM_BYTES = 8;
|
|
19
19
|
class PythLazerClient {
|
|
20
|
-
|
|
20
|
+
logger;
|
|
21
21
|
metadataServiceUrl;
|
|
22
22
|
priceServiceUrl;
|
|
23
|
-
|
|
23
|
+
token;
|
|
24
24
|
wsp;
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
abortSignal;
|
|
26
|
+
constructor({ abortSignal, logger, metadataServiceUrl, priceServiceUrl, token, wsp }){
|
|
27
|
+
this.abortSignal = abortSignal;
|
|
28
|
+
this.logger = logger;
|
|
27
29
|
this.metadataServiceUrl = metadataServiceUrl;
|
|
28
30
|
this.priceServiceUrl = priceServiceUrl;
|
|
29
|
-
this.
|
|
31
|
+
this.token = token;
|
|
30
32
|
this.wsp = wsp;
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Gets the WebSocket pool. If the WebSocket pool is not configured, an error is thrown.
|
|
34
|
-
* @throws Error if WebSocket pool is not configured
|
|
35
|
-
* @returns The WebSocket pool
|
|
36
|
-
*/ getWebSocketPool() {
|
|
37
|
-
if (!this.wsp) {
|
|
38
|
-
throw new Error("WebSocket pool is not available. Make sure to provide webSocketPoolConfig when creating the client.");
|
|
39
|
-
}
|
|
40
|
-
return this.wsp;
|
|
33
|
+
this.bindHandlers();
|
|
41
34
|
}
|
|
42
35
|
/**
|
|
43
36
|
* Creates a new PythLazerClient instance.
|
|
@@ -48,12 +41,20 @@ class PythLazerClient {
|
|
|
48
41
|
const metadataServiceUrl = (config.metadataServiceUrl ?? _constants.DEFAULT_METADATA_SERVICE_URL).replace(/\/+$/, "");
|
|
49
42
|
const priceServiceUrl = (config.priceServiceUrl ?? _constants.DEFAULT_PRICE_SERVICE_URL).replace(/\/+$/, "");
|
|
50
43
|
const logger = config.logger ?? _tslog.dummyLogger;
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
44
|
+
// the prior API was mismatched, in that it marked a websocket pool as optional,
|
|
45
|
+
// yet all internal code on the Pyth Pro client used it and threw if it didn't exist.
|
|
46
|
+
// now, the typings indicate it's no longer optional and we don't sanity check
|
|
47
|
+
// if it's set
|
|
48
|
+
const wsp = await _websocketpool.WebSocketPool.create(config.webSocketPoolConfig, token, config.abortSignal, logger);
|
|
49
|
+
const client = new PythLazerClient({
|
|
50
|
+
abortSignal: config.abortSignal,
|
|
51
|
+
logger,
|
|
52
|
+
metadataServiceUrl,
|
|
53
|
+
priceServiceUrl,
|
|
54
|
+
token,
|
|
55
|
+
wsp
|
|
56
|
+
});
|
|
57
|
+
return client;
|
|
57
58
|
}
|
|
58
59
|
/**
|
|
59
60
|
* Adds a message listener that receives either JSON or binary responses from the WebSocket connections.
|
|
@@ -61,9 +62,8 @@ class PythLazerClient {
|
|
|
61
62
|
* @param handler - Callback function that receives the parsed message. The message can be either a JSON response
|
|
62
63
|
* or a binary response containing EVM, Solana, or parsed payload data.
|
|
63
64
|
*/ addMessageListener(handler) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (typeof data == "string") {
|
|
65
|
+
this.wsp.addMessageListener(async (data)=>{
|
|
66
|
+
if (typeof data === "string") {
|
|
67
67
|
handler({
|
|
68
68
|
type: "json",
|
|
69
69
|
value: JSON.parse(data)
|
|
@@ -74,7 +74,7 @@ class PythLazerClient {
|
|
|
74
74
|
let pos = 0;
|
|
75
75
|
const magic = buffData.subarray(pos, pos + UINT32_NUM_BYTES).readUint32LE();
|
|
76
76
|
pos += UINT32_NUM_BYTES;
|
|
77
|
-
if (magic
|
|
77
|
+
if (magic !== _protocol.BINARY_UPDATE_FORMAT_MAGIC_LE) {
|
|
78
78
|
throw new Error("binary update format magic mismatch");
|
|
79
79
|
}
|
|
80
80
|
// TODO: some uint64 values may not be representable as Number.
|
|
@@ -87,15 +87,15 @@ class PythLazerClient {
|
|
|
87
87
|
const len = buffData.subarray(pos, pos + UINT16_NUM_BYTES).readUint16BE();
|
|
88
88
|
pos += UINT16_NUM_BYTES;
|
|
89
89
|
const magic = buffData.subarray(pos, pos + UINT32_NUM_BYTES).readUint32LE();
|
|
90
|
-
if (magic
|
|
90
|
+
if (magic === _protocol.FORMAT_MAGICS_LE.EVM) {
|
|
91
91
|
value.evm = buffData.subarray(pos, pos + len);
|
|
92
|
-
} else if (magic
|
|
92
|
+
} else if (magic === _protocol.FORMAT_MAGICS_LE.SOLANA) {
|
|
93
93
|
value.solana = buffData.subarray(pos, pos + len);
|
|
94
|
-
} else if (magic
|
|
94
|
+
} else if (magic === _protocol.FORMAT_MAGICS_LE.LE_ECDSA) {
|
|
95
95
|
value.leEcdsa = buffData.subarray(pos, pos + len);
|
|
96
|
-
} else if (magic
|
|
96
|
+
} else if (magic === _protocol.FORMAT_MAGICS_LE.LE_UNSIGNED) {
|
|
97
97
|
value.leUnsigned = buffData.subarray(pos, pos + len);
|
|
98
|
-
} else if (magic
|
|
98
|
+
} else if (magic === _protocol.FORMAT_MAGICS_LE.JSON) {
|
|
99
99
|
value.parsed = JSON.parse(buffData.subarray(pos + UINT32_NUM_BYTES, pos + len).toString());
|
|
100
100
|
} else {
|
|
101
101
|
throw new Error(`unknown magic: ${magic.toString()}`);
|
|
@@ -108,43 +108,67 @@ class PythLazerClient {
|
|
|
108
108
|
});
|
|
109
109
|
});
|
|
110
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* binds any internal event handlers
|
|
113
|
+
*/ bindHandlers() {
|
|
114
|
+
this.abortSignal?.addEventListener("abort", this.abortHandler);
|
|
115
|
+
}
|
|
111
116
|
subscribe(request) {
|
|
112
|
-
const wsp = this.getWebSocketPool();
|
|
113
117
|
if (request.type !== "subscribe") {
|
|
114
118
|
throw new Error("Request must be a subscribe request");
|
|
115
119
|
}
|
|
116
|
-
wsp.addSubscription(request);
|
|
120
|
+
this.wsp.addSubscription(request);
|
|
117
121
|
}
|
|
118
122
|
unsubscribe(subscriptionId) {
|
|
119
|
-
|
|
120
|
-
wsp.removeSubscription(subscriptionId);
|
|
123
|
+
this.wsp.removeSubscription(subscriptionId);
|
|
121
124
|
}
|
|
122
125
|
send(request) {
|
|
123
|
-
|
|
124
|
-
wsp.sendRequest(request);
|
|
126
|
+
this.wsp.sendRequest(request);
|
|
125
127
|
}
|
|
126
128
|
/**
|
|
127
129
|
* Registers a handler function that will be called whenever all WebSocket connections are down or attempting to reconnect.
|
|
128
130
|
* The connections may still try to reconnect in the background. To shut down the pool, call `shutdown()`.
|
|
129
131
|
* @param handler - Function to be called when all connections are down
|
|
130
132
|
*/ addAllConnectionsDownListener(handler) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
+
this.wsp.addAllConnectionsDownListener(handler);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Registers a handler function that will be called when at least one connection is restored after all were down.
|
|
137
|
+
* @param handler - Function to be called when connection is restored
|
|
138
|
+
*/ addConnectionRestoredListener(handler) {
|
|
139
|
+
this.wsp.addConnectionRestoredListener(handler);
|
|
133
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* Registers a handler function that will be called when an individual connection times out (heartbeat timeout).
|
|
143
|
+
* @param handler - Function to be called with connection index and endpoint URL
|
|
144
|
+
*/ addConnectionTimeoutListener(handler) {
|
|
145
|
+
this.wsp.addConnectionTimeoutListener(handler);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Registers a handler function that will be called when an individual connection reconnects.
|
|
149
|
+
* @param handler - Function to be called with connection index and endpoint URL
|
|
150
|
+
*/ addConnectionReconnectListener(handler) {
|
|
151
|
+
this.wsp.addConnectionReconnectListener(handler);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* called if and only if a user provided an abort signal and it was aborted
|
|
155
|
+
*/ abortHandler = ()=>{
|
|
156
|
+
this.shutdown();
|
|
157
|
+
};
|
|
134
158
|
shutdown() {
|
|
135
|
-
|
|
136
|
-
|
|
159
|
+
// Clean up abort signal listener to prevent memory leak
|
|
160
|
+
this.abortSignal?.removeEventListener("abort", this.abortHandler);
|
|
161
|
+
this.wsp.shutdown();
|
|
137
162
|
}
|
|
138
163
|
/**
|
|
139
164
|
* Private helper method to make authenticated HTTP requests with Bearer token
|
|
140
165
|
* @param url - The URL to fetch
|
|
141
166
|
* @param options - Additional fetch options
|
|
142
167
|
* @returns Promise resolving to the fetch Response
|
|
143
|
-
*/
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
};
|
|
168
|
+
*/ authenticatedFetch(url, options = {}) {
|
|
169
|
+
// Handle all possible types of headers (Headers object, array, or plain object)
|
|
170
|
+
const headers = new Headers(options.headers);
|
|
171
|
+
headers.set("Authorization", headers.get("authorization") ?? `Bearer ${this.token}`);
|
|
148
172
|
return fetch(url, {
|
|
149
173
|
...options,
|
|
150
174
|
headers
|
|
@@ -181,15 +205,15 @@ class PythLazerClient {
|
|
|
181
205
|
try {
|
|
182
206
|
const body = JSON.stringify(params);
|
|
183
207
|
this.logger.debug("getLatestPrice", {
|
|
184
|
-
|
|
185
|
-
|
|
208
|
+
body,
|
|
209
|
+
url
|
|
186
210
|
});
|
|
187
211
|
const response = await this.authenticatedFetch(url, {
|
|
188
|
-
|
|
212
|
+
body: body,
|
|
189
213
|
headers: {
|
|
190
214
|
"Content-Type": "application/json"
|
|
191
215
|
},
|
|
192
|
-
|
|
216
|
+
method: "POST"
|
|
193
217
|
});
|
|
194
218
|
if (!response.ok) {
|
|
195
219
|
throw new Error(`HTTP error! status: ${String(response.status)} - ${await response.text()}`);
|
|
@@ -208,15 +232,15 @@ class PythLazerClient {
|
|
|
208
232
|
try {
|
|
209
233
|
const body = JSON.stringify(params);
|
|
210
234
|
this.logger.debug("getPrice", {
|
|
211
|
-
|
|
212
|
-
|
|
235
|
+
body,
|
|
236
|
+
url
|
|
213
237
|
});
|
|
214
238
|
const response = await this.authenticatedFetch(url, {
|
|
215
|
-
|
|
239
|
+
body: body,
|
|
216
240
|
headers: {
|
|
217
241
|
"Content-Type": "application/json"
|
|
218
242
|
},
|
|
219
|
-
|
|
243
|
+
method: "POST"
|
|
220
244
|
});
|
|
221
245
|
if (!response.ok) {
|
|
222
246
|
throw new Error(`HTTP error! status: ${String(response.status)} - ${await response.text()}`);
|
package/dist/cjs/client.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Logger } from "ts-log";
|
|
2
|
-
import type {
|
|
2
|
+
import type { JsonUpdate, LatestPriceRequest, ParsedPayload, PriceRequest, Request, Response, SymbolResponse, SymbolsQueryParams } from "./protocol.js";
|
|
3
3
|
import type { WebSocketPoolConfig } from "./socket/websocket-pool.js";
|
|
4
4
|
export type BinaryResponse = {
|
|
5
5
|
subscriptionId: number;
|
|
@@ -17,25 +17,25 @@ export type JsonOrBinaryResponse = {
|
|
|
17
17
|
value: BinaryResponse;
|
|
18
18
|
};
|
|
19
19
|
export type LazerClientConfig = {
|
|
20
|
+
/**
|
|
21
|
+
* if provided and the signal is detected as canceled,
|
|
22
|
+
* all active listeners and connections will be unbound and killed.
|
|
23
|
+
*/
|
|
24
|
+
abortSignal?: AbortSignal;
|
|
20
25
|
token: string;
|
|
21
26
|
metadataServiceUrl?: string;
|
|
22
27
|
priceServiceUrl?: string;
|
|
23
28
|
logger?: Logger;
|
|
24
|
-
webSocketPoolConfig
|
|
29
|
+
webSocketPoolConfig: WebSocketPoolConfig;
|
|
25
30
|
};
|
|
26
31
|
export declare class PythLazerClient {
|
|
27
|
-
private
|
|
28
|
-
private
|
|
29
|
-
private
|
|
30
|
-
private
|
|
31
|
-
private
|
|
32
|
+
private logger;
|
|
33
|
+
private metadataServiceUrl;
|
|
34
|
+
private priceServiceUrl;
|
|
35
|
+
private token;
|
|
36
|
+
private wsp;
|
|
37
|
+
private abortSignal;
|
|
32
38
|
private constructor();
|
|
33
|
-
/**
|
|
34
|
-
* Gets the WebSocket pool. If the WebSocket pool is not configured, an error is thrown.
|
|
35
|
-
* @throws Error if WebSocket pool is not configured
|
|
36
|
-
* @returns The WebSocket pool
|
|
37
|
-
*/
|
|
38
|
-
private getWebSocketPool;
|
|
39
39
|
/**
|
|
40
40
|
* Creates a new PythLazerClient instance.
|
|
41
41
|
* @param config - Configuration including token, metadata service URL, and price service URL, and WebSocket pool configuration
|
|
@@ -48,6 +48,10 @@ export declare class PythLazerClient {
|
|
|
48
48
|
* or a binary response containing EVM, Solana, or parsed payload data.
|
|
49
49
|
*/
|
|
50
50
|
addMessageListener(handler: (event: JsonOrBinaryResponse) => void): void;
|
|
51
|
+
/**
|
|
52
|
+
* binds any internal event handlers
|
|
53
|
+
*/
|
|
54
|
+
bindHandlers(): void;
|
|
51
55
|
subscribe(request: Request): void;
|
|
52
56
|
unsubscribe(subscriptionId: number): void;
|
|
53
57
|
send(request: Request): void;
|
|
@@ -57,6 +61,25 @@ export declare class PythLazerClient {
|
|
|
57
61
|
* @param handler - Function to be called when all connections are down
|
|
58
62
|
*/
|
|
59
63
|
addAllConnectionsDownListener(handler: () => void): void;
|
|
64
|
+
/**
|
|
65
|
+
* Registers a handler function that will be called when at least one connection is restored after all were down.
|
|
66
|
+
* @param handler - Function to be called when connection is restored
|
|
67
|
+
*/
|
|
68
|
+
addConnectionRestoredListener(handler: () => void): void;
|
|
69
|
+
/**
|
|
70
|
+
* Registers a handler function that will be called when an individual connection times out (heartbeat timeout).
|
|
71
|
+
* @param handler - Function to be called with connection index and endpoint URL
|
|
72
|
+
*/
|
|
73
|
+
addConnectionTimeoutListener(handler: (connectionIndex: number, endpoint: string) => void): void;
|
|
74
|
+
/**
|
|
75
|
+
* Registers a handler function that will be called when an individual connection reconnects.
|
|
76
|
+
* @param handler - Function to be called with connection index and endpoint URL
|
|
77
|
+
*/
|
|
78
|
+
addConnectionReconnectListener(handler: (connectionIndex: number, endpoint: string) => void): void;
|
|
79
|
+
/**
|
|
80
|
+
* called if and only if a user provided an abort signal and it was aborted
|
|
81
|
+
*/
|
|
82
|
+
protected abortHandler: () => void;
|
|
60
83
|
shutdown(): void;
|
|
61
84
|
/**
|
|
62
85
|
* Private helper method to make authenticated HTTP requests with Bearer token
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -3,8 +3,8 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
3
3
|
value: true
|
|
4
4
|
});
|
|
5
5
|
_export_star(require("./client.cjs"), exports);
|
|
6
|
-
_export_star(require("./protocol.cjs"), exports);
|
|
7
6
|
_export_star(require("./constants.cjs"), exports);
|
|
7
|
+
_export_star(require("./protocol.cjs"), exports);
|
|
8
8
|
function _export_star(from, to) {
|
|
9
9
|
Object.keys(from).forEach(function(k) {
|
|
10
10
|
if (k !== "default" && !Object.prototype.hasOwnProperty.call(to, k)) {
|
package/dist/cjs/index.d.ts
CHANGED
package/dist/cjs/protocol.cjs
CHANGED
|
@@ -21,11 +21,11 @@ _export(exports, {
|
|
|
21
21
|
});
|
|
22
22
|
const BINARY_UPDATE_FORMAT_MAGIC_LE = 461_928_307;
|
|
23
23
|
const FORMAT_MAGICS_LE = {
|
|
24
|
-
JSON: 3_302_625_434,
|
|
25
24
|
EVM: 2_593_727_018,
|
|
26
|
-
|
|
25
|
+
JSON: 3_302_625_434,
|
|
27
26
|
LE_ECDSA: 1_296_547_300,
|
|
28
|
-
LE_UNSIGNED: 1_499_680_012
|
|
27
|
+
LE_UNSIGNED: 1_499_680_012,
|
|
28
|
+
SOLANA: 2_182_742_457
|
|
29
29
|
};
|
|
30
30
|
var CustomSocketClosureCodes = /*#__PURE__*/ function(CustomSocketClosureCodes) {
|
|
31
31
|
CustomSocketClosureCodes[CustomSocketClosureCodes["CLIENT_TIMEOUT_BUT_RECONNECTING"] = 4000] = "CLIENT_TIMEOUT_BUT_RECONNECTING";
|
package/dist/cjs/protocol.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export type Format = "evm" | "solana" | "leEcdsa" | "leUnsigned";
|
|
|
2
2
|
export type DeliveryFormat = "json" | "binary";
|
|
3
3
|
export type JsonBinaryEncoding = "base64" | "hex";
|
|
4
4
|
export type PriceFeedProperty = "price" | "bestBidPrice" | "bestAskPrice" | "exponent" | "publisherCount" | "confidence" | "fundingRate" | "fundingTimestamp" | "fundingRateInterval" | "marketSession" | "emaPrice" | "emaConfidence" | "feedUpdateTimestamp";
|
|
5
|
-
export type Channel = "real_time" | "fixed_rate@50ms" | "fixed_rate@200ms";
|
|
5
|
+
export type Channel = "real_time" | "fixed_rate@50ms" | "fixed_rate@200ms" | "fixed_rate@1000ms";
|
|
6
6
|
export type Request = {
|
|
7
7
|
type: "subscribe";
|
|
8
8
|
subscriptionId: number;
|
|
@@ -78,11 +78,11 @@ export type Response = {
|
|
|
78
78
|
};
|
|
79
79
|
export declare const BINARY_UPDATE_FORMAT_MAGIC_LE = 461928307;
|
|
80
80
|
export declare const FORMAT_MAGICS_LE: {
|
|
81
|
-
JSON: number;
|
|
82
81
|
EVM: number;
|
|
83
|
-
|
|
82
|
+
JSON: number;
|
|
84
83
|
LE_ECDSA: number;
|
|
85
84
|
LE_UNSIGNED: number;
|
|
85
|
+
SOLANA: number;
|
|
86
86
|
};
|
|
87
87
|
export type AssetType = "crypto" | "fx" | "equity" | "metal" | "rates" | "nav" | "commodity" | "funding-rate" | "eco" | "kalshi";
|
|
88
88
|
export type SymbolsQueryParams = {
|
|
@@ -45,6 +45,7 @@ class ResilientWebSocket {
|
|
|
45
45
|
onError;
|
|
46
46
|
onMessage;
|
|
47
47
|
onReconnect;
|
|
48
|
+
onTimeout;
|
|
48
49
|
constructor(config){
|
|
49
50
|
this.endpoint = config.endpoint;
|
|
50
51
|
this.wsOptions = config.wsOptions;
|
|
@@ -62,6 +63,9 @@ class ResilientWebSocket {
|
|
|
62
63
|
this.onReconnect = ()=>{
|
|
63
64
|
// Empty function, can be set by the user.
|
|
64
65
|
};
|
|
66
|
+
this.onTimeout = ()=>{
|
|
67
|
+
// Empty function, can be set by the user.
|
|
68
|
+
};
|
|
65
69
|
}
|
|
66
70
|
send(data) {
|
|
67
71
|
this.logger.debug(`Sending message`);
|
|
@@ -129,6 +133,7 @@ class ResilientWebSocket {
|
|
|
129
133
|
this.heartbeatTimeout = setTimeout(()=>{
|
|
130
134
|
const warnMsg = "Connection timed out. Reconnecting...";
|
|
131
135
|
this.logger.warn(warnMsg);
|
|
136
|
+
this.onTimeout();
|
|
132
137
|
if (this.wsClient) {
|
|
133
138
|
if (typeof this.wsClient.terminate === "function") {
|
|
134
139
|
this.wsClient.terminate();
|
|
@@ -44,6 +44,7 @@ export declare class ResilientWebSocket {
|
|
|
44
44
|
onError: (error: ErrorEvent) => void;
|
|
45
45
|
onMessage: (data: WebSocket.Data) => void;
|
|
46
46
|
onReconnect: () => void;
|
|
47
|
+
onTimeout: () => void;
|
|
47
48
|
constructor(config: ResilientWebSocketConfig);
|
|
48
49
|
send(data: string | Buffer): void;
|
|
49
50
|
startWebSocket(): void;
|
|
@@ -10,10 +10,10 @@ Object.defineProperty(exports, "WebSocketPool", {
|
|
|
10
10
|
});
|
|
11
11
|
const _ttlcache = /*#__PURE__*/ _interop_require_default(require("@isaacs/ttlcache"));
|
|
12
12
|
const _tslog = require("ts-log");
|
|
13
|
-
const _resilientwebsocket = require("./resilient-websocket.cjs");
|
|
14
13
|
const _constants = require("../constants.cjs");
|
|
15
14
|
const _index = require("../emitter/index.cjs");
|
|
16
15
|
const _index1 = require("../util/index.cjs");
|
|
16
|
+
const _resilientwebsocket = require("./resilient-websocket.cjs");
|
|
17
17
|
function _interop_require_default(obj) {
|
|
18
18
|
return obj && obj.__esModule ? obj : {
|
|
19
19
|
default: obj
|
|
@@ -22,15 +22,20 @@ function _interop_require_default(obj) {
|
|
|
22
22
|
const DEFAULT_NUM_CONNECTIONS = 4;
|
|
23
23
|
class WebSocketPool extends _index.IsomorphicEventEmitter {
|
|
24
24
|
logger;
|
|
25
|
+
abortSignal;
|
|
25
26
|
rwsPool;
|
|
26
27
|
cache;
|
|
27
28
|
subscriptions;
|
|
28
29
|
messageListeners;
|
|
29
30
|
allConnectionsDownListeners;
|
|
31
|
+
connectionRestoredListeners;
|
|
32
|
+
connectionTimeoutListeners;
|
|
33
|
+
connectionReconnectListeners;
|
|
30
34
|
wasAllDown = true;
|
|
31
35
|
checkConnectionStatesInterval;
|
|
32
|
-
|
|
33
|
-
|
|
36
|
+
isShutdown = false;
|
|
37
|
+
constructor(logger, abortSignal){
|
|
38
|
+
super(), this.logger = logger, this.abortSignal = abortSignal;
|
|
34
39
|
this.rwsPool = [];
|
|
35
40
|
this.cache = new _ttlcache.default({
|
|
36
41
|
ttl: 1000 * 10
|
|
@@ -38,6 +43,9 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
|
|
|
38
43
|
this.subscriptions = new Map();
|
|
39
44
|
this.messageListeners = [];
|
|
40
45
|
this.allConnectionsDownListeners = [];
|
|
46
|
+
this.connectionRestoredListeners = [];
|
|
47
|
+
this.connectionTimeoutListeners = [];
|
|
48
|
+
this.connectionReconnectListeners = [];
|
|
41
49
|
// Start monitoring connection states
|
|
42
50
|
this.checkConnectionStatesInterval = setInterval(()=>{
|
|
43
51
|
this.checkConnectionStates();
|
|
@@ -50,13 +58,13 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
|
|
|
50
58
|
* @param token - Authentication token to use for the connections
|
|
51
59
|
* @param numConnections - Number of parallel WebSocket connections to maintain (default: 3)
|
|
52
60
|
* @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
|
|
53
|
-
*/ static async create(config, token, logger) {
|
|
61
|
+
*/ static async create(config, token, abortSignal, logger) {
|
|
54
62
|
const urls = config.urls ?? [
|
|
55
63
|
_constants.DEFAULT_STREAM_SERVICE_0_URL,
|
|
56
64
|
_constants.DEFAULT_STREAM_SERVICE_1_URL
|
|
57
65
|
];
|
|
58
66
|
const log = logger ?? _tslog.dummyLogger;
|
|
59
|
-
const pool = new WebSocketPool(log);
|
|
67
|
+
const pool = new WebSocketPool(log, abortSignal);
|
|
60
68
|
const numConnections = config.numConnections ?? DEFAULT_NUM_CONNECTIONS;
|
|
61
69
|
// bind a handler to capture any emitted errors and send them to the user-provided
|
|
62
70
|
// onWebSocketPoolError callback (if it is present)
|
|
@@ -83,15 +91,20 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
|
|
|
83
91
|
const rws = new _resilientwebsocket.ResilientWebSocket({
|
|
84
92
|
...config.rwsConfig,
|
|
85
93
|
endpoint: url,
|
|
86
|
-
|
|
87
|
-
|
|
94
|
+
logger: log,
|
|
95
|
+
wsOptions
|
|
88
96
|
});
|
|
97
|
+
const connectionIndex = i;
|
|
98
|
+
const connectionEndpoint = url;
|
|
89
99
|
// If a websocket client unexpectedly disconnects, ResilientWebSocket will reestablish
|
|
90
100
|
// the connection and call the onReconnect callback.
|
|
91
101
|
rws.onReconnect = ()=>{
|
|
92
|
-
if (rws.wsUserClosed) {
|
|
102
|
+
if (rws.wsUserClosed || pool.isShutdown || pool.abortSignal?.aborted) {
|
|
93
103
|
return;
|
|
94
104
|
}
|
|
105
|
+
for (const listener of pool.connectionReconnectListeners){
|
|
106
|
+
listener(connectionIndex, connectionEndpoint);
|
|
107
|
+
}
|
|
95
108
|
for (const [, request] of pool.subscriptions){
|
|
96
109
|
try {
|
|
97
110
|
rws.send(JSON.stringify(request));
|
|
@@ -100,6 +113,11 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
|
|
|
100
113
|
}
|
|
101
114
|
}
|
|
102
115
|
};
|
|
116
|
+
rws.onTimeout = ()=>{
|
|
117
|
+
for (const listener of pool.connectionTimeoutListeners){
|
|
118
|
+
listener(connectionIndex, connectionEndpoint);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
103
121
|
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
104
122
|
const onErrorHandler = config.onWebSocketError ?? config.onError;
|
|
105
123
|
if (typeof onErrorHandler === "function") {
|
|
@@ -116,6 +134,11 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
|
|
|
116
134
|
}
|
|
117
135
|
pool.logger.info(`Started WebSocketPool with ${numConnections.toString()} connections. Waiting for at least one to connect...`);
|
|
118
136
|
while(!pool.isAnyConnectionEstablished()){
|
|
137
|
+
if (pool.abortSignal?.aborted) {
|
|
138
|
+
pool.logger.warn("the WebSocket Pool's abort signal was aborted during connection. Shutting down.");
|
|
139
|
+
pool.shutdown();
|
|
140
|
+
return pool;
|
|
141
|
+
}
|
|
119
142
|
await new Promise((resolve)=>setTimeout(resolve, 100));
|
|
120
143
|
}
|
|
121
144
|
pool.logger.info(`At least one WebSocket connection is established. WebSocketPool is ready.`);
|
|
@@ -171,6 +194,10 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
|
|
|
171
194
|
}
|
|
172
195
|
};
|
|
173
196
|
sendRequest(request) {
|
|
197
|
+
if (this.isShutdown || this.abortSignal?.aborted) {
|
|
198
|
+
this.logger.warn("Cannot send request: WebSocketPool is shutdown or aborted");
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
174
201
|
for (const rws of this.rwsPool){
|
|
175
202
|
try {
|
|
176
203
|
rws.send(JSON.stringify(request));
|
|
@@ -180,6 +207,10 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
|
|
|
180
207
|
}
|
|
181
208
|
}
|
|
182
209
|
addSubscription(request) {
|
|
210
|
+
if (this.isShutdown || this.abortSignal?.aborted) {
|
|
211
|
+
this.logger.warn("Cannot add subscription: WebSocketPool is shutdown or aborted");
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
183
214
|
if (request.type !== "subscribe") {
|
|
184
215
|
this.emitPoolError(new Error("Request must be a subscribe request"));
|
|
185
216
|
return;
|
|
@@ -188,10 +219,14 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
|
|
|
188
219
|
this.sendRequest(request);
|
|
189
220
|
}
|
|
190
221
|
removeSubscription(subscriptionId) {
|
|
222
|
+
if (this.isShutdown || this.abortSignal?.aborted) {
|
|
223
|
+
this.logger.warn("Cannot remove subscription: WebSocketPool is shutdown or aborted");
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
191
226
|
this.subscriptions.delete(subscriptionId);
|
|
192
227
|
const request = {
|
|
193
|
-
|
|
194
|
-
|
|
228
|
+
subscriptionId,
|
|
229
|
+
type: "unsubscribe"
|
|
195
230
|
};
|
|
196
231
|
this.sendRequest(request);
|
|
197
232
|
}
|
|
@@ -204,6 +239,23 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
|
|
|
204
239
|
*/ addAllConnectionsDownListener(handler) {
|
|
205
240
|
this.allConnectionsDownListeners.push(handler);
|
|
206
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* Calls the handler when at least one connection is restored after all connections were down.
|
|
244
|
+
*/ addConnectionRestoredListener(handler) {
|
|
245
|
+
this.connectionRestoredListeners.push(handler);
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Calls the handler when an individual connection times out (heartbeat timeout).
|
|
249
|
+
* @param handler - Callback with connection index and endpoint URL
|
|
250
|
+
*/ addConnectionTimeoutListener(handler) {
|
|
251
|
+
this.connectionTimeoutListeners.push(handler);
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Calls the handler when an individual connection reconnects after being down.
|
|
255
|
+
* @param handler - Callback with connection index and endpoint URL
|
|
256
|
+
*/ addConnectionReconnectListener(handler) {
|
|
257
|
+
this.connectionReconnectListeners.push(handler);
|
|
258
|
+
}
|
|
207
259
|
areAllConnectionsDown() {
|
|
208
260
|
return this.rwsPool.every((ws)=>!ws.isConnected() || ws.isReconnecting());
|
|
209
261
|
}
|
|
@@ -211,6 +263,10 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
|
|
|
211
263
|
return this.rwsPool.some((ws)=>ws.isConnected());
|
|
212
264
|
}
|
|
213
265
|
checkConnectionStates() {
|
|
266
|
+
// Stop monitoring if shutdown or aborted
|
|
267
|
+
if (this.isShutdown || this.abortSignal?.aborted) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
214
270
|
const allDown = this.areAllConnectionsDown();
|
|
215
271
|
// If all connections just went down
|
|
216
272
|
if (allDown && !this.wasAllDown) {
|
|
@@ -225,12 +281,25 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
|
|
|
225
281
|
}
|
|
226
282
|
}
|
|
227
283
|
}
|
|
228
|
-
// If at least one connection was restored
|
|
284
|
+
// If at least one connection was restored after all were down
|
|
229
285
|
if (!allDown && this.wasAllDown) {
|
|
230
286
|
this.wasAllDown = false;
|
|
287
|
+
this.logger.info("At least one WebSocket connection restored");
|
|
288
|
+
for (const listener of this.connectionRestoredListeners){
|
|
289
|
+
try {
|
|
290
|
+
listener();
|
|
291
|
+
} catch (error) {
|
|
292
|
+
this.emitPoolError(error, "Connection-restored listener threw");
|
|
293
|
+
}
|
|
294
|
+
}
|
|
231
295
|
}
|
|
232
296
|
}
|
|
233
297
|
shutdown() {
|
|
298
|
+
// Prevent multiple shutdown calls
|
|
299
|
+
if (this.isShutdown) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
this.isShutdown = true;
|
|
234
303
|
for (const rws of this.rwsPool){
|
|
235
304
|
rws.closeWebSocket();
|
|
236
305
|
}
|
|
@@ -238,6 +307,9 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
|
|
|
238
307
|
this.subscriptions.clear();
|
|
239
308
|
this.messageListeners = [];
|
|
240
309
|
this.allConnectionsDownListeners = [];
|
|
310
|
+
this.connectionRestoredListeners = [];
|
|
311
|
+
this.connectionTimeoutListeners = [];
|
|
312
|
+
this.connectionReconnectListeners = [];
|
|
241
313
|
clearInterval(this.checkConnectionStatesInterval);
|
|
242
314
|
// execute all bound shutdown handlers
|
|
243
315
|
for (const shutdownHandler of this.getListeners("shutdown")){
|