@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.
Files changed (41) hide show
  1. package/dist/cjs/{client.js → client.cjs} +93 -98
  2. package/dist/cjs/constants.cjs +36 -0
  3. package/dist/cjs/emitter/index.cjs +53 -0
  4. package/dist/cjs/emitter/index.d.ts +29 -0
  5. package/dist/cjs/index.cjs +20 -0
  6. package/dist/cjs/package.json +1 -1
  7. package/dist/cjs/protocol.cjs +33 -0
  8. package/dist/cjs/protocol.d.ts +1 -1
  9. package/dist/cjs/socket/{resilient-websocket.js → resilient-websocket.cjs} +47 -48
  10. package/dist/cjs/socket/websocket-pool.cjs +253 -0
  11. package/dist/cjs/socket/websocket-pool.d.ts +37 -3
  12. package/dist/cjs/util/{buffer-util.js → buffer-util.cjs} +14 -14
  13. package/dist/cjs/util/env-util.cjs +33 -0
  14. package/dist/cjs/util/index.cjs +20 -0
  15. package/dist/cjs/util/url-util.cjs +17 -0
  16. package/dist/esm/{client.js → client.mjs} +76 -88
  17. package/dist/esm/emitter/index.d.ts +29 -0
  18. package/dist/esm/emitter/index.mjs +43 -0
  19. package/dist/esm/index.mjs +3 -0
  20. package/dist/esm/package.json +1 -1
  21. package/dist/esm/protocol.d.ts +1 -1
  22. package/dist/esm/{protocol.js → protocol.mjs} +4 -4
  23. package/dist/esm/socket/{resilient-websocket.js → resilient-websocket.mjs} +27 -36
  24. package/dist/esm/socket/websocket-pool.d.ts +37 -3
  25. package/dist/esm/socket/websocket-pool.mjs +238 -0
  26. package/dist/esm/util/{buffer-util.js → buffer-util.mjs} +3 -6
  27. package/dist/esm/util/{env-util.js → env-util.mjs} +4 -8
  28. package/dist/esm/util/index.mjs +3 -0
  29. package/dist/esm/util/{url-util.js → url-util.mjs} +2 -4
  30. package/package.json +119 -15
  31. package/dist/cjs/constants.js +0 -9
  32. package/dist/cjs/index.js +0 -19
  33. package/dist/cjs/protocol.js +0 -15
  34. package/dist/cjs/socket/websocket-pool.js +0 -201
  35. package/dist/cjs/util/env-util.js +0 -32
  36. package/dist/cjs/util/index.js +0 -19
  37. package/dist/cjs/util/url-util.js +0 -18
  38. package/dist/esm/index.js +0 -3
  39. package/dist/esm/socket/websocket-pool.js +0 -195
  40. package/dist/esm/util/index.js +0 -3
  41. /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, } from "./constants.js";
4
- import { BINARY_UPDATE_FORMAT_MAGIC_LE, FORMAT_MAGICS_LE } from "./protocol.js";
5
- import { WebSocketPool } from "./socket/websocket-pool.js";
6
- import { bufferFromWebsocketData } from "./util/buffer-util.js";
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
- * Gets the WebSocket pool. If the WebSocket pool is not configured, an error is thrown.
25
- * @throws Error if WebSocket pool is not configured
26
- * @returns The WebSocket pool
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
- * Creates a new PythLazerClient instance.
36
- * @param config - Configuration including token, metadata service URL, and price service URL, and WebSocket pool configuration
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
- * Adds a message listener that receives either JSON or binary responses from the WebSocket connections.
53
- * The listener will be called for each message received, with deduplication across redundant connections.
54
- * @param handler - Callback function that receives the parsed message. The message can be either a JSON response
55
- * or a binary response containing EVM, Solana, or parsed payload data.
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 = { subscriptionId };
80
- while (pos < buffData.length) {
81
- const len = buffData
82
- .subarray(pos, pos + UINT16_NUM_BYTES)
83
- .readUint16BE();
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({ type: "binary", value });
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
- * Registers a handler function that will be called whenever all WebSocket connections are down or attempting to reconnect.
128
- * The connections may still try to reconnect in the background. To shut down the pool, call `shutdown()`.
129
- * @param handler - Function to be called when all connections are down
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
- * Private helper method to make authenticated HTTP requests with Bearer token
141
- * @param url - The URL to fetch
142
- * @param options - Additional fetch options
143
- * @returns Promise resolving to the fetch Response
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
- * Queries the symbols endpoint to get available price feed symbols.
157
- * @param params - Optional query parameters to filter symbols
158
- * @returns Promise resolving to array of symbol information
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 (await response.json());
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
- * Queries the latest price endpoint to get current price data.
181
- * @param params - Parameters for the latest price request
182
- * @returns Promise resolving to JsonUpdate with current price data
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", { url, body });
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 (await response.json());
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
- * Queries the price endpoint to get historical price data at a specific timestamp.
207
- * @param params - Parameters for the price request including timestamp
208
- * @returns Promise resolving to JsonUpdate with price data at the specified time
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", { url, body });
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 (await response.json());
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
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./client.mjs";
2
+ export * from "./protocol.mjs";
3
+ export * from "./constants.mjs";
@@ -1 +1 @@
1
- {"type":"module"}
1
+ { "type": "module" }
@@ -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
- })(CustomSocketClosureCodes || (CustomSocketClosureCodes = {}));
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.js";
4
- import { envIsBrowserOrWorker } from "../util/env-util.js";
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
- // Empty function, can be set by the user.
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
- * Calculates the delay in milliseconds for exponential backoff based on the number of failed attempts.
168
- *
169
- * The delay increases exponentially with each attempt, starting at 20ms for the first attempt,
170
- * and is capped at maxRetryDelayMs for attempts greater than or equal to 10.
171
- *
172
- * @returns The calculated delay in milliseconds before the next retry.
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
- urls?: string[];
10
+ /**
11
+ * Maximum number of open, parallel websocket connections
12
+ * @defaultValue 3
13
+ */
10
14
  numConnections?: number;
11
- rwsConfig?: Omit<ResilientWebSocketConfig, "logger" | "endpoint">;
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
  */