@pythnetwork/pyth-lazer-sdk 6.0.0 → 6.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.
@@ -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
@@ -27,8 +27,12 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
27
27
  subscriptions;
28
28
  messageListeners;
29
29
  allConnectionsDownListeners;
30
+ connectionRestoredListeners;
31
+ connectionTimeoutListeners;
32
+ connectionReconnectListeners;
30
33
  wasAllDown = true;
31
34
  checkConnectionStatesInterval;
35
+ isShutdown = false;
32
36
  constructor(logger){
33
37
  super(), this.logger = logger;
34
38
  this.rwsPool = [];
@@ -38,6 +42,9 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
38
42
  this.subscriptions = new Map();
39
43
  this.messageListeners = [];
40
44
  this.allConnectionsDownListeners = [];
45
+ this.connectionRestoredListeners = [];
46
+ this.connectionTimeoutListeners = [];
47
+ this.connectionReconnectListeners = [];
41
48
  // Start monitoring connection states
42
49
  this.checkConnectionStatesInterval = setInterval(()=>{
43
50
  this.checkConnectionStates();
@@ -50,76 +57,103 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
50
57
  * @param token - Authentication token to use for the connections
51
58
  * @param numConnections - Number of parallel WebSocket connections to maintain (default: 3)
52
59
  * @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) {
60
+ */ static async create(config, token, abortSignal, logger) {
61
+ // Helper to check if aborted and throw
62
+ const throwIfAborted = ()=>{
63
+ if (abortSignal?.aborted) {
64
+ throw new DOMException("WebSocketPool.create() was aborted", "AbortError");
65
+ }
66
+ };
67
+ // Check before starting
68
+ throwIfAborted();
54
69
  const urls = config.urls ?? [
55
70
  _constants.DEFAULT_STREAM_SERVICE_0_URL,
56
71
  _constants.DEFAULT_STREAM_SERVICE_1_URL
57
72
  ];
58
73
  const log = logger ?? _tslog.dummyLogger;
59
74
  const pool = new WebSocketPool(log);
60
- const numConnections = config.numConnections ?? DEFAULT_NUM_CONNECTIONS;
61
- // bind a handler to capture any emitted errors and send them to the user-provided
62
- // onWebSocketPoolError callback (if it is present)
63
- if (typeof config.onWebSocketPoolError === "function") {
64
- pool.on("error", config.onWebSocketPoolError);
65
- pool.once("shutdown", ()=>{
66
- // unbind all error handlers so we don't leak memory
67
- pool.off("error");
68
- });
69
- }
70
- for(let i = 0; i < numConnections; i++){
71
- const baseUrl = urls[i % urls.length];
72
- const isBrowser = (0, _index1.envIsBrowserOrWorker)();
73
- const url = isBrowser ? (0, _index1.addAuthTokenToWebSocketUrl)(baseUrl, token) : baseUrl;
74
- if (!url) {
75
- throw new Error(`URLs must not be null or empty`);
75
+ try {
76
+ const numConnections = config.numConnections ?? DEFAULT_NUM_CONNECTIONS;
77
+ // bind a handler to capture any emitted errors and send them to the user-provided
78
+ // onWebSocketPoolError callback (if it is present)
79
+ if (typeof config.onWebSocketPoolError === "function") {
80
+ pool.on("error", config.onWebSocketPoolError);
81
+ pool.once("shutdown", ()=>{
82
+ // unbind all error handlers so we don't leak memory
83
+ pool.off("error");
84
+ });
76
85
  }
77
- const wsOptions = {
78
- ...config.rwsConfig?.wsOptions,
79
- headers: isBrowser ? undefined : {
80
- Authorization: `Bearer ${token}`
81
- }
82
- };
83
- const rws = new _resilientwebsocket.ResilientWebSocket({
84
- ...config.rwsConfig,
85
- endpoint: url,
86
- wsOptions,
87
- logger: log
88
- });
89
- // If a websocket client unexpectedly disconnects, ResilientWebSocket will reestablish
90
- // the connection and call the onReconnect callback.
91
- rws.onReconnect = ()=>{
92
- if (rws.wsUserClosed) {
93
- return;
86
+ for(let i = 0; i < numConnections; i++){
87
+ const baseUrl = urls[i % urls.length];
88
+ const isBrowser = (0, _index1.envIsBrowserOrWorker)();
89
+ const url = isBrowser ? (0, _index1.addAuthTokenToWebSocketUrl)(baseUrl, token) : baseUrl;
90
+ if (!url) {
91
+ throw new Error(`URLs must not be null or empty`);
94
92
  }
95
- for (const [, request] of pool.subscriptions){
96
- try {
97
- rws.send(JSON.stringify(request));
98
- } catch (error) {
99
- pool.logger.error("Failed to resend subscription on reconnect:", error);
93
+ const wsOptions = {
94
+ ...config.rwsConfig?.wsOptions,
95
+ headers: isBrowser ? undefined : {
96
+ Authorization: `Bearer ${token}`
97
+ }
98
+ };
99
+ const rws = new _resilientwebsocket.ResilientWebSocket({
100
+ ...config.rwsConfig,
101
+ endpoint: url,
102
+ logger: log,
103
+ wsOptions
104
+ });
105
+ const connectionIndex = i;
106
+ const connectionEndpoint = url;
107
+ // If a websocket client unexpectedly disconnects, ResilientWebSocket will reestablish
108
+ // the connection and call the onReconnect callback.
109
+ rws.onReconnect = ()=>{
110
+ if (rws.wsUserClosed || pool.isShutdown) {
111
+ return;
112
+ }
113
+ for (const listener of pool.connectionReconnectListeners){
114
+ listener(connectionIndex, connectionEndpoint);
100
115
  }
116
+ for (const [, request] of pool.subscriptions){
117
+ try {
118
+ rws.send(JSON.stringify(request));
119
+ } catch (error) {
120
+ pool.logger.error("Failed to resend subscription on reconnect:", error);
121
+ }
122
+ }
123
+ };
124
+ rws.onTimeout = ()=>{
125
+ for (const listener of pool.connectionTimeoutListeners){
126
+ listener(connectionIndex, connectionEndpoint);
127
+ }
128
+ };
129
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
130
+ const onErrorHandler = config.onWebSocketError ?? config.onError;
131
+ if (typeof onErrorHandler === "function") {
132
+ rws.onError = onErrorHandler;
101
133
  }
102
- };
103
- // eslint-disable-next-line @typescript-eslint/no-deprecated
104
- const onErrorHandler = config.onWebSocketError ?? config.onError;
105
- if (typeof onErrorHandler === "function") {
106
- rws.onError = onErrorHandler;
134
+ // Handle all client messages ourselves. Dedupe before sending to registered message handlers.
135
+ rws.onMessage = (data)=>{
136
+ pool.dedupeHandler(data).catch((error)=>{
137
+ pool.emitPoolError(error, "Error in WebSocketPool dedupeHandler");
138
+ });
139
+ };
140
+ pool.rwsPool.push(rws);
141
+ rws.startWebSocket();
107
142
  }
108
- // Handle all client messages ourselves. Dedupe before sending to registered message handlers.
109
- rws.onMessage = (data)=>{
110
- pool.dedupeHandler(data).catch((error)=>{
111
- pool.emitPoolError(error, "Error in WebSocketPool dedupeHandler");
112
- });
113
- };
114
- pool.rwsPool.push(rws);
115
- rws.startWebSocket();
116
- }
117
- pool.logger.info(`Started WebSocketPool with ${numConnections.toString()} connections. Waiting for at least one to connect...`);
118
- while(!pool.isAnyConnectionEstablished()){
119
- await new Promise((resolve)=>setTimeout(resolve, 100));
143
+ pool.logger.info(`Started WebSocketPool with ${numConnections.toString()} connections. Waiting for at least one to connect...`);
144
+ while(!pool.isAnyConnectionEstablished() && !pool.isShutdown){
145
+ throwIfAborted();
146
+ await new Promise((resolve)=>setTimeout(resolve, 100));
147
+ }
148
+ // Final check after loop exits
149
+ throwIfAborted();
150
+ pool.logger.info(`At least one WebSocket connection is established. WebSocketPool is ready.`);
151
+ return pool;
152
+ } catch (error) {
153
+ // Clean up the pool if aborted during connection wait
154
+ pool.shutdown();
155
+ throw error;
120
156
  }
121
- pool.logger.info(`At least one WebSocket connection is established. WebSocketPool is ready.`);
122
- return pool;
123
157
  }
124
158
  emitPoolError(error, context) {
125
159
  const err = error instanceof Error ? error : new Error(String(error));
@@ -171,6 +205,10 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
171
205
  }
172
206
  };
173
207
  sendRequest(request) {
208
+ if (this.isShutdown) {
209
+ this.logger.warn("Cannot send request: WebSocketPool is shutdown");
210
+ return;
211
+ }
174
212
  for (const rws of this.rwsPool){
175
213
  try {
176
214
  rws.send(JSON.stringify(request));
@@ -180,6 +218,10 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
180
218
  }
181
219
  }
182
220
  addSubscription(request) {
221
+ if (this.isShutdown) {
222
+ this.logger.warn("Cannot add subscription: WebSocketPool is shutdown");
223
+ return;
224
+ }
183
225
  if (request.type !== "subscribe") {
184
226
  this.emitPoolError(new Error("Request must be a subscribe request"));
185
227
  return;
@@ -188,10 +230,14 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
188
230
  this.sendRequest(request);
189
231
  }
190
232
  removeSubscription(subscriptionId) {
233
+ if (this.isShutdown) {
234
+ this.logger.warn("Cannot remove subscription: WebSocketPool is shutdown");
235
+ return;
236
+ }
191
237
  this.subscriptions.delete(subscriptionId);
192
238
  const request = {
193
- type: "unsubscribe",
194
- subscriptionId
239
+ subscriptionId,
240
+ type: "unsubscribe"
195
241
  };
196
242
  this.sendRequest(request);
197
243
  }
@@ -204,6 +250,23 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
204
250
  */ addAllConnectionsDownListener(handler) {
205
251
  this.allConnectionsDownListeners.push(handler);
206
252
  }
253
+ /**
254
+ * Calls the handler when at least one connection is restored after all connections were down.
255
+ */ addConnectionRestoredListener(handler) {
256
+ this.connectionRestoredListeners.push(handler);
257
+ }
258
+ /**
259
+ * Calls the handler when an individual connection times out (heartbeat timeout).
260
+ * @param handler - Callback with connection index and endpoint URL
261
+ */ addConnectionTimeoutListener(handler) {
262
+ this.connectionTimeoutListeners.push(handler);
263
+ }
264
+ /**
265
+ * Calls the handler when an individual connection reconnects after being down.
266
+ * @param handler - Callback with connection index and endpoint URL
267
+ */ addConnectionReconnectListener(handler) {
268
+ this.connectionReconnectListeners.push(handler);
269
+ }
207
270
  areAllConnectionsDown() {
208
271
  return this.rwsPool.every((ws)=>!ws.isConnected() || ws.isReconnecting());
209
272
  }
@@ -211,6 +274,10 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
211
274
  return this.rwsPool.some((ws)=>ws.isConnected());
212
275
  }
213
276
  checkConnectionStates() {
277
+ // Stop monitoring if shutdown
278
+ if (this.isShutdown) {
279
+ return;
280
+ }
214
281
  const allDown = this.areAllConnectionsDown();
215
282
  // If all connections just went down
216
283
  if (allDown && !this.wasAllDown) {
@@ -225,12 +292,25 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
225
292
  }
226
293
  }
227
294
  }
228
- // If at least one connection was restored
295
+ // If at least one connection was restored after all were down
229
296
  if (!allDown && this.wasAllDown) {
230
297
  this.wasAllDown = false;
298
+ this.logger.info("At least one WebSocket connection restored");
299
+ for (const listener of this.connectionRestoredListeners){
300
+ try {
301
+ listener();
302
+ } catch (error) {
303
+ this.emitPoolError(error, "Connection-restored listener threw");
304
+ }
305
+ }
231
306
  }
232
307
  }
233
308
  shutdown() {
309
+ // Prevent multiple shutdown calls
310
+ if (this.isShutdown) {
311
+ return;
312
+ }
313
+ this.isShutdown = true;
234
314
  for (const rws of this.rwsPool){
235
315
  rws.closeWebSocket();
236
316
  }
@@ -238,6 +318,9 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
238
318
  this.subscriptions.clear();
239
319
  this.messageListeners = [];
240
320
  this.allConnectionsDownListeners = [];
321
+ this.connectionRestoredListeners = [];
322
+ this.connectionTimeoutListeners = [];
323
+ this.connectionReconnectListeners = [];
241
324
  clearInterval(this.checkConnectionStatesInterval);
242
325
  // execute all bound shutdown handlers
243
326
  for (const shutdownHandler of this.getListeners("shutdown")){
@@ -1,10 +1,10 @@
1
+ import type WebSocket from "isomorphic-ws";
1
2
  import type { ErrorEvent } from "isomorphic-ws";
2
- import WebSocket from "isomorphic-ws";
3
3
  import type { Logger } from "ts-log";
4
+ import { IsomorphicEventEmitter } from "../emitter/index.js";
4
5
  import type { Request } from "../protocol.js";
5
6
  import type { ResilientWebSocketConfig } from "./resilient-websocket.js";
6
7
  import { ResilientWebSocket } from "./resilient-websocket.js";
7
- import { IsomorphicEventEmitter } from "../emitter/index.js";
8
8
  type WebSocketOnMessageCallback = (data: WebSocket.Data) => Promise<void>;
9
9
  export type WebSocketPoolConfig = {
10
10
  /**
@@ -51,8 +51,12 @@ export declare class WebSocketPool extends IsomorphicEventEmitter<WebSocketPoolE
51
51
  private subscriptions;
52
52
  private messageListeners;
53
53
  private allConnectionsDownListeners;
54
+ private connectionRestoredListeners;
55
+ private connectionTimeoutListeners;
56
+ private connectionReconnectListeners;
54
57
  private wasAllDown;
55
58
  private checkConnectionStatesInterval;
59
+ private isShutdown;
56
60
  private constructor();
57
61
  /**
58
62
  * Creates a new WebSocketPool instance that uses multiple redundant WebSocket connections for reliability.
@@ -62,7 +66,7 @@ export declare class WebSocketPool extends IsomorphicEventEmitter<WebSocketPoolE
62
66
  * @param numConnections - Number of parallel WebSocket connections to maintain (default: 3)
63
67
  * @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
64
68
  */
65
- static create(config: WebSocketPoolConfig, token: string, logger?: Logger): Promise<WebSocketPool>;
69
+ static create(config: WebSocketPoolConfig, token: string, abortSignal?: AbortSignal | null | undefined, logger?: Logger): Promise<WebSocketPool>;
66
70
  private emitPoolError;
67
71
  /**
68
72
  * Checks for error responses in JSON messages and throws appropriate errors
@@ -83,6 +87,20 @@ export declare class WebSocketPool extends IsomorphicEventEmitter<WebSocketPoolE
83
87
  * The connections may still try to reconnect in the background.
84
88
  */
85
89
  addAllConnectionsDownListener(handler: () => void): void;
90
+ /**
91
+ * Calls the handler when at least one connection is restored after all connections were down.
92
+ */
93
+ addConnectionRestoredListener(handler: () => void): void;
94
+ /**
95
+ * Calls the handler when an individual connection times out (heartbeat timeout).
96
+ * @param handler - Callback with connection index and endpoint URL
97
+ */
98
+ addConnectionTimeoutListener(handler: (connectionIndex: number, endpoint: string) => void): void;
99
+ /**
100
+ * Calls the handler when an individual connection reconnects after being down.
101
+ * @param handler - Callback with connection index and endpoint URL
102
+ */
103
+ addConnectionReconnectListener(handler: (connectionIndex: number, endpoint: string) => void): void;
86
104
  private areAllConnectionsDown;
87
105
  private isAnyConnectionEstablished;
88
106
  private checkConnectionStates;
@@ -1,5 +1,5 @@
1
1
  import type { Logger } from "ts-log";
2
- import type { ParsedPayload, Request, Response, SymbolResponse, SymbolsQueryParams, LatestPriceRequest, PriceRequest, JsonUpdate } from "./protocol.js";
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, allows aborting the client creation process.
22
+ * Once the client is successfully created, this signal has no effect on the client's lifecycle.
23
+ * When aborted, the promise will reject with a DOMException with name 'AbortError'.
24
+ */
25
+ abortSignal?: AbortSignal;
20
26
  token: string;
21
27
  metadataServiceUrl?: string;
22
28
  priceServiceUrl?: string;
23
29
  logger?: Logger;
24
- webSocketPoolConfig?: WebSocketPoolConfig;
30
+ webSocketPoolConfig: WebSocketPoolConfig;
25
31
  };
26
32
  export declare class PythLazerClient {
27
- private readonly token;
28
- private readonly metadataServiceUrl;
29
- private readonly priceServiceUrl;
30
- private readonly logger;
31
- private readonly wsp?;
33
+ private logger;
34
+ private metadataServiceUrl;
35
+ private priceServiceUrl;
36
+ private token;
37
+ private wsp;
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
@@ -57,6 +57,21 @@ export declare class PythLazerClient {
57
57
  * @param handler - Function to be called when all connections are down
58
58
  */
59
59
  addAllConnectionsDownListener(handler: () => void): void;
60
+ /**
61
+ * Registers a handler function that will be called when at least one connection is restored after all were down.
62
+ * @param handler - Function to be called when connection is restored
63
+ */
64
+ addConnectionRestoredListener(handler: () => void): void;
65
+ /**
66
+ * Registers a handler function that will be called when an individual connection times out (heartbeat timeout).
67
+ * @param handler - Function to be called with connection index and endpoint URL
68
+ */
69
+ addConnectionTimeoutListener(handler: (connectionIndex: number, endpoint: string) => void): void;
70
+ /**
71
+ * Registers a handler function that will be called when an individual connection reconnects.
72
+ * @param handler - Function to be called with connection index and endpoint URL
73
+ */
74
+ addConnectionReconnectListener(handler: (connectionIndex: number, endpoint: string) => void): void;
60
75
  shutdown(): void;
61
76
  /**
62
77
  * Private helper method to make authenticated HTTP requests with Bearer token
@@ -7,43 +7,69 @@ const UINT16_NUM_BYTES = 2;
7
7
  const UINT32_NUM_BYTES = 4;
8
8
  const UINT64_NUM_BYTES = 8;
9
9
  export class PythLazerClient {
10
- token;
10
+ logger;
11
11
  metadataServiceUrl;
12
12
  priceServiceUrl;
13
- logger;
13
+ token;
14
14
  wsp;
15
- constructor(token, metadataServiceUrl, priceServiceUrl, logger, wsp){
16
- this.token = token;
15
+ constructor({ logger, metadataServiceUrl, priceServiceUrl, token, wsp }){
16
+ this.logger = logger;
17
17
  this.metadataServiceUrl = metadataServiceUrl;
18
18
  this.priceServiceUrl = priceServiceUrl;
19
- this.logger = logger;
19
+ this.token = token;
20
20
  this.wsp = wsp;
21
21
  }
22
22
  /**
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() {
27
- if (!this.wsp) {
28
- throw new Error("WebSocket pool is not available. Make sure to provide webSocketPoolConfig when creating the client.");
29
- }
30
- return this.wsp;
31
- }
32
- /**
33
23
  * Creates a new PythLazerClient instance.
34
24
  * @param config - Configuration including token, metadata service URL, and price service URL, and WebSocket pool configuration
35
25
  */ static async create(config) {
26
+ let wsp = undefined;
27
+ let client = undefined;
28
+ const { abortSignal } = config;
29
+ const handleAbort = ()=>{
30
+ // we need to EXPLICITLY shutdown both items,
31
+ // because there is a possibility the client was undefined
32
+ // when the abort signal was called
33
+ client?.shutdown();
34
+ wsp?.shutdown();
35
+ abortSignal?.removeEventListener("abort", handleAbort);
36
+ };
37
+ const throwIfAborted = ()=>{
38
+ if (abortSignal?.aborted) {
39
+ throw new DOMException("PythLazerClient.create() was aborted", "AbortError");
40
+ }
41
+ };
42
+ abortSignal?.addEventListener("abort", handleAbort);
36
43
  const token = config.token;
37
44
  // Collect and remove trailing slash from URLs
38
45
  const metadataServiceUrl = (config.metadataServiceUrl ?? DEFAULT_METADATA_SERVICE_URL).replace(/\/+$/, "");
39
46
  const priceServiceUrl = (config.priceServiceUrl ?? DEFAULT_PRICE_SERVICE_URL).replace(/\/+$/, "");
40
47
  const logger = config.logger ?? dummyLogger;
41
- // If webSocketPoolConfig is provided, create a WebSocket pool and block until at least one connection is established.
42
- let wsp;
43
- if (config.webSocketPoolConfig) {
44
- wsp = await WebSocketPool.create(config.webSocketPoolConfig, token, logger);
48
+ try {
49
+ throwIfAborted();
50
+ // the prior API was mismatched, in that it marked a websocket pool as optional,
51
+ // yet all internal code on the Pyth Pro client used it and threw if it didn't exist.
52
+ // now, the typings indicate it's no longer optional and we don't sanity check
53
+ // if it's set
54
+ wsp = await WebSocketPool.create(config.webSocketPoolConfig, token, abortSignal, logger);
55
+ throwIfAborted();
56
+ client = new PythLazerClient({
57
+ logger,
58
+ metadataServiceUrl,
59
+ priceServiceUrl,
60
+ token,
61
+ wsp
62
+ });
63
+ throwIfAborted();
64
+ // detach the abortSignal handler here, because we've made it!
65
+ abortSignal?.removeEventListener("abort", handleAbort);
66
+ return client;
67
+ } catch (error) {
68
+ client?.shutdown();
69
+ wsp?.shutdown();
70
+ abortSignal?.removeEventListener("abort", handleAbort);
71
+ throw error;
45
72
  }
46
- return new PythLazerClient(token, metadataServiceUrl, priceServiceUrl, logger, wsp);
47
73
  }
48
74
  /**
49
75
  * Adds a message listener that receives either JSON or binary responses from the WebSocket connections.
@@ -51,9 +77,8 @@ export class PythLazerClient {
51
77
  * @param handler - Callback function that receives the parsed message. The message can be either a JSON response
52
78
  * or a binary response containing EVM, Solana, or parsed payload data.
53
79
  */ addMessageListener(handler) {
54
- const wsp = this.getWebSocketPool();
55
- wsp.addMessageListener(async (data)=>{
56
- if (typeof data == "string") {
80
+ this.wsp.addMessageListener(async (data)=>{
81
+ if (typeof data === "string") {
57
82
  handler({
58
83
  type: "json",
59
84
  value: JSON.parse(data)
@@ -64,7 +89,7 @@ export class PythLazerClient {
64
89
  let pos = 0;
65
90
  const magic = buffData.subarray(pos, pos + UINT32_NUM_BYTES).readUint32LE();
66
91
  pos += UINT32_NUM_BYTES;
67
- if (magic != BINARY_UPDATE_FORMAT_MAGIC_LE) {
92
+ if (magic !== BINARY_UPDATE_FORMAT_MAGIC_LE) {
68
93
  throw new Error("binary update format magic mismatch");
69
94
  }
70
95
  // TODO: some uint64 values may not be representable as Number.
@@ -77,15 +102,15 @@ export class PythLazerClient {
77
102
  const len = buffData.subarray(pos, pos + UINT16_NUM_BYTES).readUint16BE();
78
103
  pos += UINT16_NUM_BYTES;
79
104
  const magic = buffData.subarray(pos, pos + UINT32_NUM_BYTES).readUint32LE();
80
- if (magic == FORMAT_MAGICS_LE.EVM) {
105
+ if (magic === FORMAT_MAGICS_LE.EVM) {
81
106
  value.evm = buffData.subarray(pos, pos + len);
82
- } else if (magic == FORMAT_MAGICS_LE.SOLANA) {
107
+ } else if (magic === FORMAT_MAGICS_LE.SOLANA) {
83
108
  value.solana = buffData.subarray(pos, pos + len);
84
- } else if (magic == FORMAT_MAGICS_LE.LE_ECDSA) {
109
+ } else if (magic === FORMAT_MAGICS_LE.LE_ECDSA) {
85
110
  value.leEcdsa = buffData.subarray(pos, pos + len);
86
- } else if (magic == FORMAT_MAGICS_LE.LE_UNSIGNED) {
111
+ } else if (magic === FORMAT_MAGICS_LE.LE_UNSIGNED) {
87
112
  value.leUnsigned = buffData.subarray(pos, pos + len);
88
- } else if (magic == FORMAT_MAGICS_LE.JSON) {
113
+ } else if (magic === FORMAT_MAGICS_LE.JSON) {
89
114
  value.parsed = JSON.parse(buffData.subarray(pos + UINT32_NUM_BYTES, pos + len).toString());
90
115
  } else {
91
116
  throw new Error(`unknown magic: ${magic.toString()}`);
@@ -99,42 +124,54 @@ export class PythLazerClient {
99
124
  });
100
125
  }
101
126
  subscribe(request) {
102
- const wsp = this.getWebSocketPool();
103
127
  if (request.type !== "subscribe") {
104
128
  throw new Error("Request must be a subscribe request");
105
129
  }
106
- wsp.addSubscription(request);
130
+ this.wsp.addSubscription(request);
107
131
  }
108
132
  unsubscribe(subscriptionId) {
109
- const wsp = this.getWebSocketPool();
110
- wsp.removeSubscription(subscriptionId);
133
+ this.wsp.removeSubscription(subscriptionId);
111
134
  }
112
135
  send(request) {
113
- const wsp = this.getWebSocketPool();
114
- wsp.sendRequest(request);
136
+ this.wsp.sendRequest(request);
115
137
  }
116
138
  /**
117
139
  * Registers a handler function that will be called whenever all WebSocket connections are down or attempting to reconnect.
118
140
  * The connections may still try to reconnect in the background. To shut down the pool, call `shutdown()`.
119
141
  * @param handler - Function to be called when all connections are down
120
142
  */ addAllConnectionsDownListener(handler) {
121
- const wsp = this.getWebSocketPool();
122
- wsp.addAllConnectionsDownListener(handler);
143
+ this.wsp.addAllConnectionsDownListener(handler);
144
+ }
145
+ /**
146
+ * Registers a handler function that will be called when at least one connection is restored after all were down.
147
+ * @param handler - Function to be called when connection is restored
148
+ */ addConnectionRestoredListener(handler) {
149
+ this.wsp.addConnectionRestoredListener(handler);
150
+ }
151
+ /**
152
+ * Registers a handler function that will be called when an individual connection times out (heartbeat timeout).
153
+ * @param handler - Function to be called with connection index and endpoint URL
154
+ */ addConnectionTimeoutListener(handler) {
155
+ this.wsp.addConnectionTimeoutListener(handler);
156
+ }
157
+ /**
158
+ * Registers a handler function that will be called when an individual connection reconnects.
159
+ * @param handler - Function to be called with connection index and endpoint URL
160
+ */ addConnectionReconnectListener(handler) {
161
+ this.wsp.addConnectionReconnectListener(handler);
123
162
  }
124
163
  shutdown() {
125
- const wsp = this.getWebSocketPool();
126
- wsp.shutdown();
164
+ this.wsp.shutdown();
127
165
  }
128
166
  /**
129
167
  * Private helper method to make authenticated HTTP requests with Bearer token
130
168
  * @param url - The URL to fetch
131
169
  * @param options - Additional fetch options
132
170
  * @returns Promise resolving to the fetch Response
133
- */ async authenticatedFetch(url, options = {}) {
134
- const headers = {
135
- Authorization: `Bearer ${this.token}`,
136
- ...options.headers
137
- };
171
+ */ authenticatedFetch(url, options = {}) {
172
+ // Handle all possible types of headers (Headers object, array, or plain object)
173
+ const headers = new Headers(options.headers);
174
+ headers.set("Authorization", headers.get("authorization") ?? `Bearer ${this.token}`);
138
175
  return fetch(url, {
139
176
  ...options,
140
177
  headers
@@ -171,15 +208,15 @@ export class PythLazerClient {
171
208
  try {
172
209
  const body = JSON.stringify(params);
173
210
  this.logger.debug("getLatestPrice", {
174
- url,
175
- body
211
+ body,
212
+ url
176
213
  });
177
214
  const response = await this.authenticatedFetch(url, {
178
- method: "POST",
215
+ body: body,
179
216
  headers: {
180
217
  "Content-Type": "application/json"
181
218
  },
182
- body: body
219
+ method: "POST"
183
220
  });
184
221
  if (!response.ok) {
185
222
  throw new Error(`HTTP error! status: ${String(response.status)} - ${await response.text()}`);
@@ -198,15 +235,15 @@ export class PythLazerClient {
198
235
  try {
199
236
  const body = JSON.stringify(params);
200
237
  this.logger.debug("getPrice", {
201
- url,
202
- body
238
+ body,
239
+ url
203
240
  });
204
241
  const response = await this.authenticatedFetch(url, {
205
- method: "POST",
242
+ body: body,
206
243
  headers: {
207
244
  "Content-Type": "application/json"
208
245
  },
209
- body: body
246
+ method: "POST"
210
247
  });
211
248
  if (!response.ok) {
212
249
  throw new Error(`HTTP error! status: ${String(response.status)} - ${await response.text()}`);