@pythnetwork/pyth-lazer-sdk 5.0.0 → 5.2.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/dist/cjs/{client.js → client.cjs} +93 -98
- package/dist/cjs/constants.cjs +36 -0
- package/dist/cjs/emitter/index.cjs +53 -0
- package/dist/cjs/emitter/index.d.ts +29 -0
- package/dist/cjs/index.cjs +20 -0
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/protocol.cjs +33 -0
- package/dist/cjs/protocol.d.ts +1 -1
- package/dist/cjs/socket/{resilient-websocket.js → resilient-websocket.cjs} +47 -48
- package/dist/cjs/socket/websocket-pool.cjs +253 -0
- package/dist/cjs/socket/websocket-pool.d.ts +37 -3
- package/dist/cjs/util/{buffer-util.js → buffer-util.cjs} +14 -14
- package/dist/cjs/util/env-util.cjs +33 -0
- package/dist/cjs/util/index.cjs +20 -0
- package/dist/cjs/util/url-util.cjs +17 -0
- package/dist/esm/{client.js → client.mjs} +76 -88
- package/dist/esm/emitter/index.d.ts +29 -0
- package/dist/esm/emitter/index.mjs +43 -0
- package/dist/esm/index.mjs +3 -0
- package/dist/esm/package.json +1 -1
- package/dist/esm/protocol.d.ts +1 -1
- package/dist/esm/{protocol.js → protocol.mjs} +4 -4
- package/dist/esm/socket/{resilient-websocket.js → resilient-websocket.mjs} +27 -36
- package/dist/esm/socket/websocket-pool.d.ts +37 -3
- package/dist/esm/socket/websocket-pool.mjs +238 -0
- package/dist/esm/util/{buffer-util.js → buffer-util.mjs} +3 -6
- package/dist/esm/util/{env-util.js → env-util.mjs} +4 -8
- package/dist/esm/util/index.mjs +3 -0
- package/dist/esm/util/{url-util.js → url-util.mjs} +2 -4
- package/package.json +119 -15
- package/dist/cjs/constants.js +0 -9
- package/dist/cjs/index.js +0 -19
- package/dist/cjs/protocol.js +0 -15
- package/dist/cjs/socket/websocket-pool.js +0 -201
- package/dist/cjs/util/env-util.js +0 -32
- package/dist/cjs/util/index.js +0 -19
- package/dist/cjs/util/url-util.js +0 -18
- package/dist/esm/index.js +0 -3
- package/dist/esm/socket/websocket-pool.js +0 -195
- package/dist/esm/util/index.js +0 -3
- /package/dist/esm/{constants.js → constants.mjs} +0 -0
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import WebSocket from "isomorphic-ws";
|
|
2
1
|
import { dummyLogger } from "ts-log";
|
|
3
|
-
import { DEFAULT_METADATA_SERVICE_URL, DEFAULT_PRICE_SERVICE_URL
|
|
4
|
-
import { BINARY_UPDATE_FORMAT_MAGIC_LE, FORMAT_MAGICS_LE } from "./protocol.
|
|
5
|
-
import { WebSocketPool } from "./socket/websocket-pool.
|
|
6
|
-
import { bufferFromWebsocketData } from "./util/buffer-util.
|
|
2
|
+
import { DEFAULT_METADATA_SERVICE_URL, DEFAULT_PRICE_SERVICE_URL } from "./constants.mjs";
|
|
3
|
+
import { BINARY_UPDATE_FORMAT_MAGIC_LE, FORMAT_MAGICS_LE } from "./protocol.mjs";
|
|
4
|
+
import { WebSocketPool } from "./socket/websocket-pool.mjs";
|
|
5
|
+
import { bufferFromWebsocketData } from "./util/buffer-util.mjs";
|
|
7
6
|
const UINT16_NUM_BYTES = 2;
|
|
8
7
|
const UINT32_NUM_BYTES = 4;
|
|
9
8
|
const UINT64_NUM_BYTES = 8;
|
|
@@ -13,7 +12,7 @@ export class PythLazerClient {
|
|
|
13
12
|
priceServiceUrl;
|
|
14
13
|
logger;
|
|
15
14
|
wsp;
|
|
16
|
-
constructor(token, metadataServiceUrl, priceServiceUrl, logger, wsp)
|
|
15
|
+
constructor(token, metadataServiceUrl, priceServiceUrl, logger, wsp){
|
|
17
16
|
this.token = token;
|
|
18
17
|
this.metadataServiceUrl = metadataServiceUrl;
|
|
19
18
|
this.priceServiceUrl = priceServiceUrl;
|
|
@@ -21,21 +20,19 @@ export class PythLazerClient {
|
|
|
21
20
|
this.wsp = wsp;
|
|
22
21
|
}
|
|
23
22
|
/**
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
getWebSocketPool() {
|
|
23
|
+
* Gets the WebSocket pool. If the WebSocket pool is not configured, an error is thrown.
|
|
24
|
+
* @throws Error if WebSocket pool is not configured
|
|
25
|
+
* @returns The WebSocket pool
|
|
26
|
+
*/ getWebSocketPool() {
|
|
29
27
|
if (!this.wsp) {
|
|
30
28
|
throw new Error("WebSocket pool is not available. Make sure to provide webSocketPoolConfig when creating the client.");
|
|
31
29
|
}
|
|
32
30
|
return this.wsp;
|
|
33
31
|
}
|
|
34
32
|
/**
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
static async create(config) {
|
|
33
|
+
* Creates a new PythLazerClient instance.
|
|
34
|
+
* @param config - Configuration including token, metadata service URL, and price service URL, and WebSocket pool configuration
|
|
35
|
+
*/ static async create(config) {
|
|
39
36
|
const token = config.token;
|
|
40
37
|
// Collect and remove trailing slash from URLs
|
|
41
38
|
const metadataServiceUrl = (config.metadataServiceUrl ?? DEFAULT_METADATA_SERVICE_URL).replace(/\/+$/, "");
|
|
@@ -49,26 +46,23 @@ export class PythLazerClient {
|
|
|
49
46
|
return new PythLazerClient(token, metadataServiceUrl, priceServiceUrl, logger, wsp);
|
|
50
47
|
}
|
|
51
48
|
/**
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
addMessageListener(handler) {
|
|
49
|
+
* Adds a message listener that receives either JSON or binary responses from the WebSocket connections.
|
|
50
|
+
* The listener will be called for each message received, with deduplication across redundant connections.
|
|
51
|
+
* @param handler - Callback function that receives the parsed message. The message can be either a JSON response
|
|
52
|
+
* or a binary response containing EVM, Solana, or parsed payload data.
|
|
53
|
+
*/ addMessageListener(handler) {
|
|
58
54
|
const wsp = this.getWebSocketPool();
|
|
59
|
-
wsp.addMessageListener(async (data)
|
|
55
|
+
wsp.addMessageListener(async (data)=>{
|
|
60
56
|
if (typeof data == "string") {
|
|
61
57
|
handler({
|
|
62
58
|
type: "json",
|
|
63
|
-
value: JSON.parse(data)
|
|
59
|
+
value: JSON.parse(data)
|
|
64
60
|
});
|
|
65
61
|
return;
|
|
66
62
|
}
|
|
67
63
|
const buffData = await bufferFromWebsocketData(data);
|
|
68
64
|
let pos = 0;
|
|
69
|
-
const magic = buffData
|
|
70
|
-
.subarray(pos, pos + UINT32_NUM_BYTES)
|
|
71
|
-
.readUint32LE();
|
|
65
|
+
const magic = buffData.subarray(pos, pos + UINT32_NUM_BYTES).readUint32LE();
|
|
72
66
|
pos += UINT32_NUM_BYTES;
|
|
73
67
|
if (magic != BINARY_UPDATE_FORMAT_MAGIC_LE) {
|
|
74
68
|
throw new Error("binary update format magic mismatch");
|
|
@@ -76,36 +70,32 @@ export class PythLazerClient {
|
|
|
76
70
|
// TODO: some uint64 values may not be representable as Number.
|
|
77
71
|
const subscriptionId = Number(buffData.subarray(pos, pos + UINT64_NUM_BYTES).readBigInt64BE());
|
|
78
72
|
pos += UINT64_NUM_BYTES;
|
|
79
|
-
const value = {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
73
|
+
const value = {
|
|
74
|
+
subscriptionId
|
|
75
|
+
};
|
|
76
|
+
while(pos < buffData.length){
|
|
77
|
+
const len = buffData.subarray(pos, pos + UINT16_NUM_BYTES).readUint16BE();
|
|
84
78
|
pos += UINT16_NUM_BYTES;
|
|
85
|
-
const magic = buffData
|
|
86
|
-
.subarray(pos, pos + UINT32_NUM_BYTES)
|
|
87
|
-
.readUint32LE();
|
|
79
|
+
const magic = buffData.subarray(pos, pos + UINT32_NUM_BYTES).readUint32LE();
|
|
88
80
|
if (magic == FORMAT_MAGICS_LE.EVM) {
|
|
89
81
|
value.evm = buffData.subarray(pos, pos + len);
|
|
90
|
-
}
|
|
91
|
-
else if (magic == FORMAT_MAGICS_LE.SOLANA) {
|
|
82
|
+
} else if (magic == FORMAT_MAGICS_LE.SOLANA) {
|
|
92
83
|
value.solana = buffData.subarray(pos, pos + len);
|
|
93
|
-
}
|
|
94
|
-
else if (magic == FORMAT_MAGICS_LE.LE_ECDSA) {
|
|
84
|
+
} else if (magic == FORMAT_MAGICS_LE.LE_ECDSA) {
|
|
95
85
|
value.leEcdsa = buffData.subarray(pos, pos + len);
|
|
96
|
-
}
|
|
97
|
-
else if (magic == FORMAT_MAGICS_LE.LE_UNSIGNED) {
|
|
86
|
+
} else if (magic == FORMAT_MAGICS_LE.LE_UNSIGNED) {
|
|
98
87
|
value.leUnsigned = buffData.subarray(pos, pos + len);
|
|
99
|
-
}
|
|
100
|
-
else if (magic == FORMAT_MAGICS_LE.JSON) {
|
|
88
|
+
} else if (magic == FORMAT_MAGICS_LE.JSON) {
|
|
101
89
|
value.parsed = JSON.parse(buffData.subarray(pos + UINT32_NUM_BYTES, pos + len).toString());
|
|
102
|
-
}
|
|
103
|
-
else {
|
|
90
|
+
} else {
|
|
104
91
|
throw new Error(`unknown magic: ${magic.toString()}`);
|
|
105
92
|
}
|
|
106
93
|
pos += len;
|
|
107
94
|
}
|
|
108
|
-
handler({
|
|
95
|
+
handler({
|
|
96
|
+
type: "binary",
|
|
97
|
+
value
|
|
98
|
+
});
|
|
109
99
|
});
|
|
110
100
|
}
|
|
111
101
|
subscribe(request) {
|
|
@@ -124,11 +114,10 @@ export class PythLazerClient {
|
|
|
124
114
|
wsp.sendRequest(request);
|
|
125
115
|
}
|
|
126
116
|
/**
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
addAllConnectionsDownListener(handler) {
|
|
117
|
+
* Registers a handler function that will be called whenever all WebSocket connections are down or attempting to reconnect.
|
|
118
|
+
* The connections may still try to reconnect in the background. To shut down the pool, call `shutdown()`.
|
|
119
|
+
* @param handler - Function to be called when all connections are down
|
|
120
|
+
*/ addAllConnectionsDownListener(handler) {
|
|
132
121
|
const wsp = this.getWebSocketPool();
|
|
133
122
|
wsp.addAllConnectionsDownListener(handler);
|
|
134
123
|
}
|
|
@@ -137,27 +126,25 @@ export class PythLazerClient {
|
|
|
137
126
|
wsp.shutdown();
|
|
138
127
|
}
|
|
139
128
|
/**
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
async authenticatedFetch(url, options = {}) {
|
|
129
|
+
* Private helper method to make authenticated HTTP requests with Bearer token
|
|
130
|
+
* @param url - The URL to fetch
|
|
131
|
+
* @param options - Additional fetch options
|
|
132
|
+
* @returns Promise resolving to the fetch Response
|
|
133
|
+
*/ async authenticatedFetch(url, options = {}) {
|
|
146
134
|
const headers = {
|
|
147
135
|
Authorization: `Bearer ${this.token}`,
|
|
148
|
-
...options.headers
|
|
136
|
+
...options.headers
|
|
149
137
|
};
|
|
150
138
|
return fetch(url, {
|
|
151
139
|
...options,
|
|
152
|
-
headers
|
|
140
|
+
headers
|
|
153
141
|
});
|
|
154
142
|
}
|
|
155
143
|
/**
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
async getSymbols(params) {
|
|
144
|
+
* Queries the symbols endpoint to get available price feed symbols.
|
|
145
|
+
* @param params - Optional query parameters to filter symbols
|
|
146
|
+
* @returns Promise resolving to array of symbol information
|
|
147
|
+
*/ async getSymbols(params) {
|
|
161
148
|
const url = new URL(`${this.metadataServiceUrl}/v1/symbols`);
|
|
162
149
|
if (params?.query) {
|
|
163
150
|
url.searchParams.set("query", params.query);
|
|
@@ -170,61 +157,62 @@ export class PythLazerClient {
|
|
|
170
157
|
if (!response.ok) {
|
|
171
158
|
throw new Error(`HTTP error! status: ${String(response.status)} - ${await response.text()}`);
|
|
172
159
|
}
|
|
173
|
-
return
|
|
174
|
-
}
|
|
175
|
-
catch (error) {
|
|
160
|
+
return await response.json();
|
|
161
|
+
} catch (error) {
|
|
176
162
|
throw new Error(`Failed to fetch symbols: ${error instanceof Error ? error.message : String(error)}`);
|
|
177
163
|
}
|
|
178
164
|
}
|
|
179
165
|
/**
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
async getLatestPrice(params) {
|
|
166
|
+
* Queries the latest price endpoint to get current price data.
|
|
167
|
+
* @param params - Parameters for the latest price request
|
|
168
|
+
* @returns Promise resolving to JsonUpdate with current price data
|
|
169
|
+
*/ async getLatestPrice(params) {
|
|
185
170
|
const url = `${this.priceServiceUrl}/v1/latest_price`;
|
|
186
171
|
try {
|
|
187
172
|
const body = JSON.stringify(params);
|
|
188
|
-
this.logger.debug("getLatestPrice", {
|
|
173
|
+
this.logger.debug("getLatestPrice", {
|
|
174
|
+
url,
|
|
175
|
+
body
|
|
176
|
+
});
|
|
189
177
|
const response = await this.authenticatedFetch(url, {
|
|
190
178
|
method: "POST",
|
|
191
179
|
headers: {
|
|
192
|
-
"Content-Type": "application/json"
|
|
180
|
+
"Content-Type": "application/json"
|
|
193
181
|
},
|
|
194
|
-
body: body
|
|
182
|
+
body: body
|
|
195
183
|
});
|
|
196
184
|
if (!response.ok) {
|
|
197
185
|
throw new Error(`HTTP error! status: ${String(response.status)} - ${await response.text()}`);
|
|
198
186
|
}
|
|
199
|
-
return
|
|
200
|
-
}
|
|
201
|
-
catch (error) {
|
|
187
|
+
return await response.json();
|
|
188
|
+
} catch (error) {
|
|
202
189
|
throw new Error(`Failed to fetch latest price: ${error instanceof Error ? error.message : String(error)}`);
|
|
203
190
|
}
|
|
204
191
|
}
|
|
205
192
|
/**
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
async getPrice(params) {
|
|
193
|
+
* Queries the price endpoint to get historical price data at a specific timestamp.
|
|
194
|
+
* @param params - Parameters for the price request including timestamp
|
|
195
|
+
* @returns Promise resolving to JsonUpdate with price data at the specified time
|
|
196
|
+
*/ async getPrice(params) {
|
|
211
197
|
const url = `${this.priceServiceUrl}/v1/price`;
|
|
212
198
|
try {
|
|
213
199
|
const body = JSON.stringify(params);
|
|
214
|
-
this.logger.debug("getPrice", {
|
|
200
|
+
this.logger.debug("getPrice", {
|
|
201
|
+
url,
|
|
202
|
+
body
|
|
203
|
+
});
|
|
215
204
|
const response = await this.authenticatedFetch(url, {
|
|
216
205
|
method: "POST",
|
|
217
206
|
headers: {
|
|
218
|
-
"Content-Type": "application/json"
|
|
207
|
+
"Content-Type": "application/json"
|
|
219
208
|
},
|
|
220
|
-
body: body
|
|
209
|
+
body: body
|
|
221
210
|
});
|
|
222
211
|
if (!response.ok) {
|
|
223
212
|
throw new Error(`HTTP error! status: ${String(response.status)} - ${await response.text()}`);
|
|
224
213
|
}
|
|
225
|
-
return
|
|
226
|
-
}
|
|
227
|
-
catch (error) {
|
|
214
|
+
return await response.json();
|
|
215
|
+
} catch (error) {
|
|
228
216
|
throw new Error(`Failed to fetch price: ${error instanceof Error ? error.message : String(error)}`);
|
|
229
217
|
}
|
|
230
218
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* T defines the "Event Map".
|
|
3
|
+
* Example: `{ 'data': (payload: string) => void; 'error': (err: Error) => void; }`
|
|
4
|
+
*/
|
|
5
|
+
export declare abstract class IsomorphicEventEmitter<T extends Record<string, (...args: any[]) => void>> {
|
|
6
|
+
private listeners;
|
|
7
|
+
/**
|
|
8
|
+
* Register a callback for a specific event.
|
|
9
|
+
*/
|
|
10
|
+
on<K extends keyof T>(eventName: K, callback: T[K]): void;
|
|
11
|
+
/**
|
|
12
|
+
* Registers a callback for a specific event that
|
|
13
|
+
* will only be executed a single time i.e. the first occurence.
|
|
14
|
+
* After this, the handler will be automatically removed and cleaned up.
|
|
15
|
+
*/
|
|
16
|
+
once<K extends keyof T>(eventName: K, callback: T[K]): void;
|
|
17
|
+
/**
|
|
18
|
+
* Remove a callback from a specific event.
|
|
19
|
+
* If no specific callback is specified when off() is called,
|
|
20
|
+
* ALL event handler callbacks for the given eventName will be removed
|
|
21
|
+
* at once.
|
|
22
|
+
*/
|
|
23
|
+
off<K extends keyof T>(eventName: K, callback?: T[K]): void;
|
|
24
|
+
/**
|
|
25
|
+
* Protected method to retrieve listeners for internal triggering.
|
|
26
|
+
* This allows the child class to decide how/when to execute them.
|
|
27
|
+
*/
|
|
28
|
+
protected getListeners<K extends keyof T>(eventName: K): T[K][];
|
|
29
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* T defines the "Event Map".
|
|
3
|
+
* Example: `{ 'data': (payload: string) => void; 'error': (err: Error) => void; }`
|
|
4
|
+
*/ export class IsomorphicEventEmitter {
|
|
5
|
+
listeners = new Map();
|
|
6
|
+
/**
|
|
7
|
+
* Register a callback for a specific event.
|
|
8
|
+
*/ on(eventName, callback) {
|
|
9
|
+
const currentListeners = this.listeners.get(eventName) ?? [];
|
|
10
|
+
this.listeners.set(eventName, [
|
|
11
|
+
...currentListeners,
|
|
12
|
+
callback
|
|
13
|
+
]);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Registers a callback for a specific event that
|
|
17
|
+
* will only be executed a single time i.e. the first occurence.
|
|
18
|
+
* After this, the handler will be automatically removed and cleaned up.
|
|
19
|
+
*/ once(eventName, callback) {
|
|
20
|
+
const wrappedCallback = (...args)=>{
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
22
|
+
callback(...args);
|
|
23
|
+
this.off(eventName, wrappedCallback);
|
|
24
|
+
};
|
|
25
|
+
this.on(eventName, wrappedCallback);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Remove a callback from a specific event.
|
|
29
|
+
* If no specific callback is specified when off() is called,
|
|
30
|
+
* ALL event handler callbacks for the given eventName will be removed
|
|
31
|
+
* at once.
|
|
32
|
+
*/ off(eventName, callback) {
|
|
33
|
+
const cbIsFunc = typeof callback === "function";
|
|
34
|
+
const currentListeners = this.listeners.get(eventName) ?? [];
|
|
35
|
+
this.listeners.set(eventName, currentListeners.filter((cb)=>cbIsFunc && cb !== callback));
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Protected method to retrieve listeners for internal triggering.
|
|
39
|
+
* This allows the child class to decide how/when to execute them.
|
|
40
|
+
*/ getListeners(eventName) {
|
|
41
|
+
return this.listeners.get(eventName) ?? [];
|
|
42
|
+
}
|
|
43
|
+
}
|
package/dist/esm/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"type":"module"}
|
|
1
|
+
{ "type": "module" }
|
package/dist/esm/protocol.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export type Format = "evm" | "solana" | "leEcdsa" | "leUnsigned";
|
|
2
2
|
export type DeliveryFormat = "json" | "binary";
|
|
3
3
|
export type JsonBinaryEncoding = "base64" | "hex";
|
|
4
|
-
export type PriceFeedProperty = "price" | "bestBidPrice" | "bestAskPrice" | "exponent" | "publisherCount" | "confidence";
|
|
4
|
+
export type PriceFeedProperty = "price" | "bestBidPrice" | "bestAskPrice" | "exponent" | "publisherCount" | "confidence" | "fundingRate" | "fundingTimestamp" | "fundingRateInterval";
|
|
5
5
|
export type Channel = "real_time" | "fixed_rate@50ms" | "fixed_rate@200ms";
|
|
6
6
|
export type Request = {
|
|
7
7
|
type: "subscribe";
|
|
@@ -4,9 +4,9 @@ export const FORMAT_MAGICS_LE = {
|
|
|
4
4
|
EVM: 2_593_727_018,
|
|
5
5
|
SOLANA: 2_182_742_457,
|
|
6
6
|
LE_ECDSA: 1_296_547_300,
|
|
7
|
-
LE_UNSIGNED: 1_499_680_012
|
|
7
|
+
LE_UNSIGNED: 1_499_680_012
|
|
8
8
|
};
|
|
9
|
-
export var CustomSocketClosureCodes
|
|
10
|
-
(function (CustomSocketClosureCodes) {
|
|
9
|
+
export var CustomSocketClosureCodes = /*#__PURE__*/ function(CustomSocketClosureCodes) {
|
|
11
10
|
CustomSocketClosureCodes[CustomSocketClosureCodes["CLIENT_TIMEOUT_BUT_RECONNECTING"] = 4000] = "CLIENT_TIMEOUT_BUT_RECONNECTING";
|
|
12
|
-
|
|
11
|
+
return CustomSocketClosureCodes;
|
|
12
|
+
}({});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import WebSocket from "isomorphic-ws";
|
|
2
2
|
import { dummyLogger } from "ts-log";
|
|
3
|
-
import { CustomSocketClosureCodes } from "../protocol.
|
|
4
|
-
import { envIsBrowserOrWorker } from "../util/env-util.
|
|
3
|
+
import { CustomSocketClosureCodes } from "../protocol.mjs";
|
|
4
|
+
import { envIsBrowserOrWorker } from "../util/env-util.mjs";
|
|
5
5
|
const DEFAULT_HEARTBEAT_TIMEOUT_DURATION_MS = 5000; // 5 seconds
|
|
6
6
|
const DEFAULT_MAX_RETRY_DELAY_MS = 1000; // 1 second'
|
|
7
7
|
const DEFAULT_LOG_AFTER_RETRY_COUNT = 10;
|
|
@@ -30,33 +30,29 @@ export class ResilientWebSocket {
|
|
|
30
30
|
onError;
|
|
31
31
|
onMessage;
|
|
32
32
|
onReconnect;
|
|
33
|
-
constructor(config)
|
|
33
|
+
constructor(config){
|
|
34
34
|
this.endpoint = config.endpoint;
|
|
35
35
|
this.wsOptions = config.wsOptions;
|
|
36
36
|
this.logger = config.logger ?? dummyLogger;
|
|
37
|
-
this.heartbeatTimeoutDurationMs =
|
|
38
|
-
config.heartbeatTimeoutDurationMs ??
|
|
39
|
-
DEFAULT_HEARTBEAT_TIMEOUT_DURATION_MS;
|
|
37
|
+
this.heartbeatTimeoutDurationMs = config.heartbeatTimeoutDurationMs ?? DEFAULT_HEARTBEAT_TIMEOUT_DURATION_MS;
|
|
40
38
|
this.maxRetryDelayMs = config.maxRetryDelayMs ?? DEFAULT_MAX_RETRY_DELAY_MS;
|
|
41
|
-
this.logAfterRetryCount =
|
|
42
|
-
config.logAfterRetryCount ?? DEFAULT_LOG_AFTER_RETRY_COUNT;
|
|
39
|
+
this.logAfterRetryCount = config.logAfterRetryCount ?? DEFAULT_LOG_AFTER_RETRY_COUNT;
|
|
43
40
|
this.wsFailedAttempts = 0;
|
|
44
|
-
this.onError = (error)
|
|
41
|
+
this.onError = (error)=>{
|
|
45
42
|
void error;
|
|
46
43
|
};
|
|
47
|
-
this.onMessage = (data)
|
|
44
|
+
this.onMessage = (data)=>{
|
|
48
45
|
void data;
|
|
49
46
|
};
|
|
50
|
-
this.onReconnect = ()
|
|
51
|
-
|
|
47
|
+
this.onReconnect = ()=>{
|
|
48
|
+
// Empty function, can be set by the user.
|
|
52
49
|
};
|
|
53
50
|
}
|
|
54
51
|
send(data) {
|
|
55
52
|
this.logger.debug(`Sending message`);
|
|
56
53
|
if (this.isConnected()) {
|
|
57
54
|
this.wsClient.send(data);
|
|
58
|
-
}
|
|
59
|
-
else {
|
|
55
|
+
} else {
|
|
60
56
|
this.logger.warn(`WebSocket to ${this.endpoint} is not connected. Cannot send message.`);
|
|
61
57
|
}
|
|
62
58
|
}
|
|
@@ -80,33 +76,32 @@ export class ResilientWebSocket {
|
|
|
80
76
|
// so we need to ensure it's not included if we're running in that environment:
|
|
81
77
|
// https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket#protocols
|
|
82
78
|
this.wsClient = new WebSocket(this.endpoint, envIsBrowserOrWorker() ? undefined : this.wsOptions);
|
|
83
|
-
this.wsClient.addEventListener("open", ()
|
|
79
|
+
this.wsClient.addEventListener("open", ()=>{
|
|
84
80
|
this.logger.info("WebSocket connection established");
|
|
85
81
|
this.wsFailedAttempts = 0;
|
|
86
82
|
this._isReconnecting = false;
|
|
87
83
|
this.resetHeartbeat();
|
|
88
84
|
this.onReconnect();
|
|
89
85
|
});
|
|
90
|
-
this.wsClient.addEventListener("close", (e)
|
|
86
|
+
this.wsClient.addEventListener("close", (e)=>{
|
|
91
87
|
if (this.wsUserClosed) {
|
|
92
88
|
this.logger.info(`WebSocket connection to ${this.endpoint} closed by user`);
|
|
93
|
-
}
|
|
94
|
-
else {
|
|
89
|
+
} else {
|
|
95
90
|
if (this.shouldLogRetry()) {
|
|
96
91
|
this.logger.warn(`WebSocket connection to ${this.endpoint} closed unexpectedly: Code: ${e.code.toString()}`);
|
|
97
92
|
}
|
|
98
93
|
this.handleReconnect();
|
|
99
94
|
}
|
|
100
95
|
});
|
|
101
|
-
this.wsClient.addEventListener("error", (event)
|
|
96
|
+
this.wsClient.addEventListener("error", (event)=>{
|
|
102
97
|
this.onError(event);
|
|
103
98
|
});
|
|
104
|
-
this.wsClient.addEventListener("message", (event)
|
|
99
|
+
this.wsClient.addEventListener("message", (event)=>{
|
|
105
100
|
this.resetHeartbeat();
|
|
106
101
|
this.onMessage(event.data);
|
|
107
102
|
});
|
|
108
103
|
if ("on" in this.wsClient) {
|
|
109
|
-
this.wsClient.on("ping", ()
|
|
104
|
+
this.wsClient.on("ping", ()=>{
|
|
110
105
|
this.logger.info("Ping received");
|
|
111
106
|
this.resetHeartbeat();
|
|
112
107
|
});
|
|
@@ -116,14 +111,13 @@ export class ResilientWebSocket {
|
|
|
116
111
|
if (this.heartbeatTimeout !== undefined) {
|
|
117
112
|
clearTimeout(this.heartbeatTimeout);
|
|
118
113
|
}
|
|
119
|
-
this.heartbeatTimeout = setTimeout(()
|
|
114
|
+
this.heartbeatTimeout = setTimeout(()=>{
|
|
120
115
|
const warnMsg = "Connection timed out. Reconnecting...";
|
|
121
116
|
this.logger.warn(warnMsg);
|
|
122
117
|
if (this.wsClient) {
|
|
123
118
|
if (typeof this.wsClient.terminate === "function") {
|
|
124
119
|
this.wsClient.terminate();
|
|
125
|
-
}
|
|
126
|
-
else {
|
|
120
|
+
} else {
|
|
127
121
|
// terminate is an implementation detail of the node-friendly
|
|
128
122
|
// https://www.npmjs.com/package/ws package, but is not a native WebSocket API,
|
|
129
123
|
// so we have to use the close method
|
|
@@ -148,11 +142,9 @@ export class ResilientWebSocket {
|
|
|
148
142
|
this.wsClient = undefined;
|
|
149
143
|
this._isReconnecting = true;
|
|
150
144
|
if (this.shouldLogRetry()) {
|
|
151
|
-
this.logger.error("Connection closed unexpectedly or because of timeout. Reconnecting after " +
|
|
152
|
-
String(this.retryDelayMs()) +
|
|
153
|
-
"ms.");
|
|
145
|
+
this.logger.error("Connection closed unexpectedly or because of timeout. Reconnecting after " + String(this.retryDelayMs()) + "ms.");
|
|
154
146
|
}
|
|
155
|
-
this.retryTimeout = setTimeout(()
|
|
147
|
+
this.retryTimeout = setTimeout(()=>{
|
|
156
148
|
this.startWebSocket();
|
|
157
149
|
}, this.retryDelayMs());
|
|
158
150
|
}
|
|
@@ -164,14 +156,13 @@ export class ResilientWebSocket {
|
|
|
164
156
|
this.wsUserClosed = true;
|
|
165
157
|
}
|
|
166
158
|
/**
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
retryDelayMs() {
|
|
159
|
+
* Calculates the delay in milliseconds for exponential backoff based on the number of failed attempts.
|
|
160
|
+
*
|
|
161
|
+
* The delay increases exponentially with each attempt, starting at 20ms for the first attempt,
|
|
162
|
+
* and is capped at maxRetryDelayMs for attempts greater than or equal to 10.
|
|
163
|
+
*
|
|
164
|
+
* @returns The calculated delay in milliseconds before the next retry.
|
|
165
|
+
*/ retryDelayMs() {
|
|
175
166
|
if (this.wsFailedAttempts >= 10) {
|
|
176
167
|
return this.maxRetryDelayMs;
|
|
177
168
|
}
|
|
@@ -4,14 +4,47 @@ import type { Logger } from "ts-log";
|
|
|
4
4
|
import type { Request } from "../protocol.js";
|
|
5
5
|
import type { ResilientWebSocketConfig } from "./resilient-websocket.js";
|
|
6
6
|
import { ResilientWebSocket } from "./resilient-websocket.js";
|
|
7
|
+
import { IsomorphicEventEmitter } from "../emitter/index.js";
|
|
7
8
|
type WebSocketOnMessageCallback = (data: WebSocket.Data) => void | Promise<void>;
|
|
8
9
|
export type WebSocketPoolConfig = {
|
|
9
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Maximum number of open, parallel websocket connections
|
|
12
|
+
* @defaultValue 3
|
|
13
|
+
*/
|
|
10
14
|
numConnections?: number;
|
|
11
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Callback that will be executed whenever an error
|
|
17
|
+
* message or an error event occurs on an individual WebSocket connection
|
|
18
|
+
*
|
|
19
|
+
* @deprecated use onWebSocketError() instead
|
|
20
|
+
*/
|
|
12
21
|
onError?: (error: ErrorEvent) => void;
|
|
22
|
+
/**
|
|
23
|
+
* Callback that will be executed whenever an error
|
|
24
|
+
* message or an error event occurs on an individual WebSocket connection
|
|
25
|
+
*/
|
|
26
|
+
onWebSocketError?: (error: ErrorEvent) => void;
|
|
27
|
+
/**
|
|
28
|
+
* Callback that will be executed whenever an error occurs
|
|
29
|
+
* directly within the WebSocket pool. These can typically
|
|
30
|
+
* be errors that would normally manifest as "unhandledRejection" or "uncaughtException"
|
|
31
|
+
* errors.
|
|
32
|
+
*/
|
|
33
|
+
onWebSocketPoolError?: (error: Error) => void;
|
|
34
|
+
/**
|
|
35
|
+
* Additional websocket configuration
|
|
36
|
+
*/
|
|
37
|
+
rwsConfig?: Omit<ResilientWebSocketConfig, "logger" | "endpoint">;
|
|
38
|
+
/**
|
|
39
|
+
* Pyth URLs to use when creating a connection
|
|
40
|
+
*/
|
|
41
|
+
urls?: string[];
|
|
42
|
+
};
|
|
43
|
+
export type WebSocketPoolEvents = {
|
|
44
|
+
error: (error: Error) => void;
|
|
45
|
+
shutdown: () => void;
|
|
13
46
|
};
|
|
14
|
-
export declare class WebSocketPool {
|
|
47
|
+
export declare class WebSocketPool extends IsomorphicEventEmitter<WebSocketPoolEvents> {
|
|
15
48
|
private readonly logger;
|
|
16
49
|
rwsPool: ResilientWebSocket[];
|
|
17
50
|
private cache;
|
|
@@ -30,6 +63,7 @@ export declare class WebSocketPool {
|
|
|
30
63
|
* @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
|
|
31
64
|
*/
|
|
32
65
|
static create(config: WebSocketPoolConfig, token: string, logger?: Logger): Promise<WebSocketPool>;
|
|
66
|
+
private emitPoolError;
|
|
33
67
|
/**
|
|
34
68
|
* Checks for error responses in JSON messages and throws appropriate errors
|
|
35
69
|
*/
|