@pythnetwork/pyth-lazer-sdk 0.5.0 → 1.0.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.
@@ -1,5 +1,5 @@
1
- import type { Logger } from "ts-log";
2
1
  import type { ParsedPayload, Request, Response } from "./protocol.js";
2
+ import type { WebSocketPoolConfig } from "./socket/websocket-pool.js";
3
3
  export type BinaryResponse = {
4
4
  subscriptionId: number;
5
5
  evm?: Buffer | undefined;
@@ -25,7 +25,7 @@ export declare class PythLazerClient {
25
25
  * @param numConnections - The number of parallel WebSocket connections to establish (default: 3). A higher number gives a more reliable stream. The connections will round-robin across the provided URLs.
26
26
  * @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
27
27
  */
28
- static create(urls: string[], token: string, numConnections?: number, logger?: Logger): Promise<PythLazerClient>;
28
+ static create(config: WebSocketPoolConfig): Promise<PythLazerClient>;
29
29
  /**
30
30
  * Adds a message listener that receives either JSON or binary responses from the WebSocket connections.
31
31
  * The listener will be called for each message received, with deduplication across redundant connections.
@@ -33,9 +33,9 @@ export declare class PythLazerClient {
33
33
  * or a binary response containing EVM, Solana, or parsed payload data.
34
34
  */
35
35
  addMessageListener(handler: (event: JsonOrBinaryResponse) => void): void;
36
- subscribe(request: Request): Promise<void>;
37
- unsubscribe(subscriptionId: number): Promise<void>;
38
- send(request: Request): Promise<void>;
36
+ subscribe(request: Request): void;
37
+ unsubscribe(subscriptionId: number): void;
38
+ send(request: Request): void;
39
39
  /**
40
40
  * Registers a handler function that will be called whenever all WebSocket connections are down or attempting to reconnect.
41
41
  * The connections may still try to reconnect in the background. To shut down the pool, call `shutdown()`.
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PythLazerClient = void 0;
4
- const ts_log_1 = require("ts-log");
5
4
  const protocol_js_1 = require("./protocol.js");
6
5
  const websocket_pool_js_1 = require("./socket/websocket-pool.js");
7
6
  const UINT16_NUM_BYTES = 2;
@@ -19,8 +18,8 @@ class PythLazerClient {
19
18
  * @param numConnections - The number of parallel WebSocket connections to establish (default: 3). A higher number gives a more reliable stream. The connections will round-robin across the provided URLs.
20
19
  * @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
21
20
  */
22
- static async create(urls, token, numConnections = 3, logger = ts_log_1.dummyLogger) {
23
- const wsp = await websocket_pool_js_1.WebSocketPool.create(urls, token, numConnections, logger);
21
+ static async create(config) {
22
+ const wsp = await websocket_pool_js_1.WebSocketPool.create(config);
24
23
  return new PythLazerClient(wsp);
25
24
  }
26
25
  /**
@@ -81,17 +80,17 @@ class PythLazerClient {
81
80
  }
82
81
  });
83
82
  }
84
- async subscribe(request) {
83
+ subscribe(request) {
85
84
  if (request.type !== "subscribe") {
86
85
  throw new Error("Request must be a subscribe request");
87
86
  }
88
- await this.wsp.addSubscription(request);
87
+ this.wsp.addSubscription(request);
89
88
  }
90
- async unsubscribe(subscriptionId) {
91
- await this.wsp.removeSubscription(subscriptionId);
89
+ unsubscribe(subscriptionId) {
90
+ this.wsp.removeSubscription(subscriptionId);
92
91
  }
93
- async send(request) {
94
- await this.wsp.sendRequest(request);
92
+ send(request) {
93
+ this.wsp.sendRequest(request);
95
94
  }
96
95
  /**
97
96
  * Registers a handler function that will be called whenever all WebSocket connections are down or attempting to reconnect.
@@ -2,29 +2,48 @@ import type { ClientRequestArgs } from "node:http";
2
2
  import type { ClientOptions, ErrorEvent } from "isomorphic-ws";
3
3
  import WebSocket from "isomorphic-ws";
4
4
  import type { Logger } from "ts-log";
5
- export declare class ResilientWebSocket {
5
+ export type ResilientWebSocketConfig = {
6
6
  endpoint: string;
7
+ wsOptions?: ClientOptions | ClientRequestArgs | undefined;
8
+ logger?: Logger;
9
+ heartbeatTimeoutDurationMs?: number;
10
+ maxRetryDelayMs?: number;
11
+ logAfterRetryCount?: number;
12
+ };
13
+ export declare class ResilientWebSocket {
14
+ private endpoint;
15
+ private wsOptions?;
16
+ private logger;
17
+ private heartbeatTimeoutDurationMs;
18
+ private maxRetryDelayMs;
19
+ private logAfterRetryCount;
7
20
  wsClient: undefined | WebSocket;
8
21
  wsUserClosed: boolean;
9
- private wsOptions;
10
22
  private wsFailedAttempts;
11
- private heartbeatTimeout;
12
- private logger;
13
- private connectionPromise;
14
- private resolveConnection;
15
- private rejectConnection;
23
+ private heartbeatTimeout?;
24
+ private retryTimeout?;
16
25
  private _isReconnecting;
17
- get isReconnecting(): boolean;
18
- get isConnected(): boolean;
26
+ isReconnecting(): boolean;
27
+ isConnected(): this is this & {
28
+ wsClient: WebSocket;
29
+ };
30
+ private shouldLogRetry;
19
31
  onError: (error: ErrorEvent) => void;
20
32
  onMessage: (data: WebSocket.Data) => void;
21
33
  onReconnect: () => void;
22
- constructor(endpoint: string, wsOptions?: ClientOptions | ClientRequestArgs, logger?: Logger);
23
- send(data: string | Buffer): Promise<void>;
24
- startWebSocket(): Promise<void>;
34
+ constructor(config: ResilientWebSocketConfig);
35
+ send(data: string | Buffer): void;
36
+ startWebSocket(): void;
25
37
  private resetHeartbeat;
26
- private waitForMaybeReadyWebSocket;
27
- private handleClose;
28
- private restartUnexpectedClosedWebsocket;
38
+ private handleReconnect;
29
39
  closeWebSocket(): void;
40
+ /**
41
+ * Calculates the delay in milliseconds for exponential backoff based on the number of failed attempts.
42
+ *
43
+ * The delay increases exponentially with each attempt, starting at 20ms for the first attempt,
44
+ * and is capped at maxRetryDelayMs for attempts greater than or equal to 10.
45
+ *
46
+ * @returns The calculated delay in milliseconds before the next retry.
47
+ */
48
+ private retryDelayMs;
30
49
  }
@@ -5,38 +5,49 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.ResilientWebSocket = void 0;
7
7
  const isomorphic_ws_1 = __importDefault(require("isomorphic-ws"));
8
- const HEARTBEAT_TIMEOUT_DURATION = 10_000;
9
- const CONNECTION_TIMEOUT = 5000;
8
+ const ts_log_1 = require("ts-log");
9
+ const DEFAULT_HEARTBEAT_TIMEOUT_DURATION_MS = 5000; // 5 seconds
10
+ const DEFAULT_MAX_RETRY_DELAY_MS = 1000; // 1 second'
11
+ const DEFAULT_LOG_AFTER_RETRY_COUNT = 10;
10
12
  class ResilientWebSocket {
11
13
  endpoint;
12
- wsClient;
13
- wsUserClosed;
14
14
  wsOptions;
15
+ logger;
16
+ heartbeatTimeoutDurationMs;
17
+ maxRetryDelayMs;
18
+ logAfterRetryCount;
19
+ wsClient;
20
+ wsUserClosed = false;
15
21
  wsFailedAttempts;
16
22
  heartbeatTimeout;
17
- logger;
18
- connectionPromise;
19
- resolveConnection;
20
- rejectConnection;
23
+ retryTimeout;
21
24
  _isReconnecting = false;
22
- get isReconnecting() {
25
+ isReconnecting() {
23
26
  return this._isReconnecting;
24
27
  }
25
- get isConnected() {
28
+ isConnected() {
26
29
  return this.wsClient?.readyState === isomorphic_ws_1.default.OPEN;
27
30
  }
31
+ shouldLogRetry() {
32
+ return this.wsFailedAttempts % this.logAfterRetryCount === 0;
33
+ }
28
34
  onError;
29
35
  onMessage;
30
36
  onReconnect;
31
- constructor(endpoint, wsOptions, logger) {
32
- this.endpoint = endpoint;
33
- this.wsOptions = wsOptions;
34
- this.logger = logger;
37
+ constructor(config) {
38
+ this.endpoint = config.endpoint;
39
+ this.wsOptions = config.wsOptions;
40
+ this.logger = config.logger ?? ts_log_1.dummyLogger;
41
+ this.heartbeatTimeoutDurationMs =
42
+ config.heartbeatTimeoutDurationMs ??
43
+ DEFAULT_HEARTBEAT_TIMEOUT_DURATION_MS;
44
+ this.maxRetryDelayMs = config.maxRetryDelayMs ?? DEFAULT_MAX_RETRY_DELAY_MS;
45
+ this.logAfterRetryCount =
46
+ config.logAfterRetryCount ?? DEFAULT_LOG_AFTER_RETRY_COUNT;
35
47
  this.wsFailedAttempts = 0;
36
48
  this.onError = (error) => {
37
- this.logger?.error(error.error);
49
+ void error;
38
50
  };
39
- this.wsUserClosed = true;
40
51
  this.onMessage = (data) => {
41
52
  void data;
42
53
  };
@@ -44,144 +55,117 @@ class ResilientWebSocket {
44
55
  // Empty function, can be set by the user.
45
56
  };
46
57
  }
47
- async send(data) {
48
- this.logger?.info(`Sending message`);
49
- await this.waitForMaybeReadyWebSocket();
50
- if (this.wsClient === undefined) {
51
- this.logger?.error("Couldn't connect to the websocket server. Error callback is called.");
58
+ send(data) {
59
+ this.logger.debug(`Sending message`);
60
+ if (this.isConnected()) {
61
+ this.wsClient.send(data);
52
62
  }
53
63
  else {
54
- this.wsClient.send(data);
64
+ this.logger.warn(`WebSocket to ${this.endpoint} is not connected. Cannot send message.`);
55
65
  }
56
66
  }
57
- async startWebSocket() {
67
+ startWebSocket() {
68
+ if (this.wsUserClosed) {
69
+ this.logger.error("Connection was explicitly closed by user. Will not reconnect.");
70
+ return;
71
+ }
58
72
  if (this.wsClient !== undefined) {
59
- // If there's an existing connection attempt, wait for it
60
- if (this.connectionPromise) {
61
- return this.connectionPromise;
62
- }
73
+ this.logger.info("WebSocket client already started.");
63
74
  return;
64
75
  }
65
- this.logger?.info(`Creating Web Socket client`);
66
- // Create a new promise for this connection attempt
67
- this.connectionPromise = new Promise((resolve, reject) => {
68
- this.resolveConnection = resolve;
69
- this.rejectConnection = reject;
70
- });
71
- // Set a connection timeout
72
- const timeoutId = setTimeout(() => {
73
- if (this.rejectConnection) {
74
- this.rejectConnection(new Error(`Connection timeout after ${String(CONNECTION_TIMEOUT)}ms`));
75
- }
76
- }, CONNECTION_TIMEOUT);
76
+ if (this.wsFailedAttempts == 0) {
77
+ this.logger.info(`Creating Web Socket client`);
78
+ }
79
+ if (this.retryTimeout !== undefined) {
80
+ clearTimeout(this.retryTimeout);
81
+ this.retryTimeout = undefined;
82
+ }
77
83
  this.wsClient = new isomorphic_ws_1.default(this.endpoint, this.wsOptions);
78
- this.wsUserClosed = false;
79
84
  this.wsClient.addEventListener("open", () => {
85
+ this.logger.info("WebSocket connection established");
80
86
  this.wsFailedAttempts = 0;
81
- this.resetHeartbeat();
82
- clearTimeout(timeoutId);
83
87
  this._isReconnecting = false;
84
- this.resolveConnection?.();
88
+ this.resetHeartbeat();
89
+ this.onReconnect();
90
+ });
91
+ this.wsClient.addEventListener("close", (e) => {
92
+ if (this.wsUserClosed) {
93
+ this.logger.info(`WebSocket connection to ${this.endpoint} closed by user`);
94
+ }
95
+ else {
96
+ if (this.shouldLogRetry()) {
97
+ this.logger.warn(`WebSocket connection to ${this.endpoint} closed unexpectedly: Code: ${e.code.toString()}`);
98
+ }
99
+ this.handleReconnect();
100
+ }
85
101
  });
86
102
  this.wsClient.addEventListener("error", (event) => {
87
103
  this.onError(event);
88
- if (this.rejectConnection) {
89
- this.rejectConnection(new Error("WebSocket connection failed"));
90
- }
91
104
  });
92
105
  this.wsClient.addEventListener("message", (event) => {
93
106
  this.resetHeartbeat();
94
107
  this.onMessage(event.data);
95
108
  });
96
- this.wsClient.addEventListener("close", () => {
97
- clearTimeout(timeoutId);
98
- if (this.rejectConnection) {
99
- this.rejectConnection(new Error("WebSocket closed before connecting"));
100
- }
101
- void this.handleClose();
102
- });
103
109
  if ("on" in this.wsClient) {
104
110
  this.wsClient.on("ping", () => {
105
- this.logger?.info("Ping received");
111
+ this.logger.info("Ping received");
106
112
  this.resetHeartbeat();
107
113
  });
108
114
  }
109
- return this.connectionPromise;
110
115
  }
111
116
  resetHeartbeat() {
112
117
  if (this.heartbeatTimeout !== undefined) {
113
118
  clearTimeout(this.heartbeatTimeout);
114
119
  }
115
120
  this.heartbeatTimeout = setTimeout(() => {
116
- this.logger?.warn("Connection timed out. Reconnecting...");
121
+ this.logger.warn("Connection timed out. Reconnecting...");
117
122
  this.wsClient?.terminate();
118
- void this.restartUnexpectedClosedWebsocket();
119
- }, HEARTBEAT_TIMEOUT_DURATION);
123
+ this.handleReconnect();
124
+ }, this.heartbeatTimeoutDurationMs);
120
125
  }
121
- async waitForMaybeReadyWebSocket() {
122
- let waitedTime = 0;
123
- while (this.wsClient !== undefined &&
124
- this.wsClient.readyState !== this.wsClient.OPEN) {
125
- if (waitedTime > 5000) {
126
- this.wsClient.close();
127
- return;
128
- }
129
- else {
130
- waitedTime += 10;
131
- await sleep(10);
132
- }
126
+ handleReconnect() {
127
+ if (this.wsUserClosed) {
128
+ this.logger.info("WebSocket connection closed by user, not reconnecting.");
129
+ return;
133
130
  }
134
- }
135
- async handleClose() {
136
131
  if (this.heartbeatTimeout !== undefined) {
137
132
  clearTimeout(this.heartbeatTimeout);
138
133
  }
139
- if (this.wsUserClosed) {
140
- this.logger?.info("The connection has been closed successfully.");
134
+ if (this.retryTimeout !== undefined) {
135
+ clearTimeout(this.retryTimeout);
141
136
  }
142
- else {
143
- this.wsFailedAttempts += 1;
144
- this.wsClient = undefined;
145
- this.connectionPromise = undefined;
146
- this.resolveConnection = undefined;
147
- this.rejectConnection = undefined;
148
- const waitTime = expoBackoff(this.wsFailedAttempts);
149
- this._isReconnecting = true;
150
- this.logger?.error("Connection closed unexpectedly or because of timeout. Reconnecting after " +
151
- String(waitTime) +
137
+ this.wsFailedAttempts += 1;
138
+ this.wsClient = undefined;
139
+ this._isReconnecting = true;
140
+ if (this.shouldLogRetry()) {
141
+ this.logger.error("Connection closed unexpectedly or because of timeout. Reconnecting after " +
142
+ String(this.retryDelayMs()) +
152
143
  "ms.");
153
- await sleep(waitTime);
154
- await this.restartUnexpectedClosedWebsocket();
155
144
  }
156
- }
157
- async restartUnexpectedClosedWebsocket() {
158
- if (this.wsUserClosed) {
159
- return;
160
- }
161
- await this.startWebSocket();
162
- await this.waitForMaybeReadyWebSocket();
163
- if (this.wsClient === undefined) {
164
- this.logger?.error("Couldn't reconnect to websocket. Error callback is called.");
165
- return;
166
- }
167
- this.onReconnect();
145
+ this.retryTimeout = setTimeout(() => {
146
+ this.startWebSocket();
147
+ }, this.retryDelayMs());
168
148
  }
169
149
  closeWebSocket() {
170
150
  if (this.wsClient !== undefined) {
171
- const client = this.wsClient;
151
+ this.wsClient.close();
172
152
  this.wsClient = undefined;
173
- this.connectionPromise = undefined;
174
- this.resolveConnection = undefined;
175
- this.rejectConnection = undefined;
176
- client.close();
177
153
  }
178
154
  this.wsUserClosed = true;
179
155
  }
156
+ /**
157
+ * Calculates the delay in milliseconds for exponential backoff based on the number of failed attempts.
158
+ *
159
+ * The delay increases exponentially with each attempt, starting at 20ms for the first attempt,
160
+ * and is capped at maxRetryDelayMs for attempts greater than or equal to 10.
161
+ *
162
+ * @returns The calculated delay in milliseconds before the next retry.
163
+ */
164
+ retryDelayMs() {
165
+ if (this.wsFailedAttempts >= 10) {
166
+ return this.maxRetryDelayMs;
167
+ }
168
+ return Math.min(2 ** this.wsFailedAttempts * 10, this.maxRetryDelayMs);
169
+ }
180
170
  }
181
171
  exports.ResilientWebSocket = ResilientWebSocket;
182
- async function sleep(ms) {
183
- return new Promise((resolve) => setTimeout(resolve, ms));
184
- }
185
- function expoBackoff(attempts) {
186
- return 2 ** attempts * 100;
187
- }
@@ -1,7 +1,17 @@
1
+ import type { ErrorEvent } from "isomorphic-ws";
1
2
  import WebSocket from "isomorphic-ws";
2
3
  import type { Logger } from "ts-log";
3
4
  import type { Request } from "../protocol.js";
5
+ import type { ResilientWebSocketConfig } from "./resilient-websocket.js";
4
6
  import { ResilientWebSocket } from "./resilient-websocket.js";
7
+ export type WebSocketPoolConfig = {
8
+ urls: string[];
9
+ token: string;
10
+ numConnections?: number;
11
+ logger?: Logger;
12
+ rwsConfig?: Omit<ResilientWebSocketConfig, "logger" | "endpoint">;
13
+ onError?: (error: ErrorEvent) => void;
14
+ };
5
15
  export declare class WebSocketPool {
6
16
  private readonly logger;
7
17
  rwsPool: ResilientWebSocket[];
@@ -20,7 +30,7 @@ export declare class WebSocketPool {
20
30
  * @param numConnections - Number of parallel WebSocket connections to maintain (default: 3)
21
31
  * @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
22
32
  */
23
- static create(urls: string[], token: string, numConnections?: number, logger?: Logger): Promise<WebSocketPool>;
33
+ static create(config: WebSocketPoolConfig): Promise<WebSocketPool>;
24
34
  /**
25
35
  * Checks for error responses in JSON messages and throws appropriate errors
26
36
  */
@@ -30,9 +40,9 @@ export declare class WebSocketPool {
30
40
  * multiple connections before forwarding to registered handlers
31
41
  */
32
42
  dedupeHandler: (data: WebSocket.Data) => void;
33
- sendRequest(request: Request): Promise<void>;
34
- addSubscription(request: Request): Promise<void>;
35
- removeSubscription(subscriptionId: number): Promise<void>;
43
+ sendRequest(request: Request): void;
44
+ addSubscription(request: Request): void;
45
+ removeSubscription(subscriptionId: number): void;
36
46
  addMessageListener(handler: (data: WebSocket.Data) => void): void;
37
47
  /**
38
48
  * Calls the handler if all websocket connections are currently down or in reconnecting state.
@@ -40,6 +50,7 @@ export declare class WebSocketPool {
40
50
  */
41
51
  addAllConnectionsDownListener(handler: () => void): void;
42
52
  private areAllConnectionsDown;
53
+ private isAnyConnectionEstablished;
43
54
  private checkConnectionStates;
44
55
  shutdown(): void;
45
56
  }
@@ -7,7 +7,7 @@ exports.WebSocketPool = void 0;
7
7
  const ttlcache_1 = __importDefault(require("@isaacs/ttlcache"));
8
8
  const ts_log_1 = require("ts-log");
9
9
  const resilient_websocket_js_1 = require("./resilient-websocket.js");
10
- const DEFAULT_NUM_CONNECTIONS = 3;
10
+ const DEFAULT_NUM_CONNECTIONS = 4;
11
11
  class WebSocketPool {
12
12
  logger;
13
13
  rwsPool;
@@ -17,7 +17,7 @@ class WebSocketPool {
17
17
  allConnectionsDownListeners;
18
18
  wasAllDown = true;
19
19
  checkConnectionStatesInterval;
20
- constructor(logger = ts_log_1.dummyLogger) {
20
+ constructor(logger) {
21
21
  this.logger = logger;
22
22
  this.rwsPool = [];
23
23
  this.cache = new ttlcache_1.default({ ttl: 1000 * 10 }); // TTL of 10 seconds
@@ -37,24 +37,30 @@ class WebSocketPool {
37
37
  * @param numConnections - Number of parallel WebSocket connections to maintain (default: 3)
38
38
  * @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
39
39
  */
40
- static async create(urls, token, numConnections = DEFAULT_NUM_CONNECTIONS, logger = ts_log_1.dummyLogger) {
41
- if (urls.length === 0) {
40
+ static async create(config) {
41
+ if (config.urls.length === 0) {
42
42
  throw new Error("No URLs provided");
43
43
  }
44
+ const logger = config.logger ?? ts_log_1.dummyLogger;
44
45
  const pool = new WebSocketPool(logger);
45
- // Create all websocket instances
46
- const connectionPromises = [];
46
+ const numConnections = config.numConnections ?? DEFAULT_NUM_CONNECTIONS;
47
47
  for (let i = 0; i < numConnections; i++) {
48
- const url = urls[i % urls.length];
48
+ const url = config.urls[i % config.urls.length];
49
49
  if (!url) {
50
50
  throw new Error(`URLs must not be null or empty`);
51
51
  }
52
52
  const wsOptions = {
53
+ ...config.rwsConfig?.wsOptions,
53
54
  headers: {
54
- Authorization: `Bearer ${token}`,
55
+ Authorization: `Bearer ${config.token}`,
55
56
  },
56
57
  };
57
- const rws = new resilient_websocket_js_1.ResilientWebSocket(url, wsOptions, logger);
58
+ const rws = new resilient_websocket_js_1.ResilientWebSocket({
59
+ ...config.rwsConfig,
60
+ endpoint: url,
61
+ wsOptions,
62
+ logger,
63
+ });
58
64
  // If a websocket client unexpectedly disconnects, ResilientWebSocket will reestablish
59
65
  // the connection and call the onReconnect callback.
60
66
  rws.onReconnect = () => {
@@ -63,29 +69,26 @@ class WebSocketPool {
63
69
  }
64
70
  for (const [, request] of pool.subscriptions) {
65
71
  try {
66
- void rws.send(JSON.stringify(request));
72
+ rws.send(JSON.stringify(request));
67
73
  }
68
74
  catch (error) {
69
75
  pool.logger.error("Failed to resend subscription on reconnect:", error);
70
76
  }
71
77
  }
72
78
  };
79
+ if (config.onError) {
80
+ rws.onError = config.onError;
81
+ }
73
82
  // Handle all client messages ourselves. Dedupe before sending to registered message handlers.
74
83
  rws.onMessage = pool.dedupeHandler;
75
84
  pool.rwsPool.push(rws);
76
- // Start the websocket and collect the promise
77
- connectionPromises.push(rws.startWebSocket());
78
- }
79
- // Wait for all connections to be established
80
- try {
81
- await Promise.all(connectionPromises);
85
+ rws.startWebSocket();
82
86
  }
83
- catch (error) {
84
- // If any connection fails, clean up and throw
85
- pool.shutdown();
86
- throw error;
87
+ pool.logger.info(`Started WebSocketPool with ${numConnections.toString()} connections. Waiting for at least one to connect...`);
88
+ while (!pool.isAnyConnectionEstablished()) {
89
+ await new Promise((resolve) => setTimeout(resolve, 100));
87
90
  }
88
- pool.logger.info(`Successfully established ${numConnections.toString()} redundant WebSocket connections`);
91
+ pool.logger.info(`At least one WebSocket connection is established. WebSocketPool is ready.`);
89
92
  return pool;
90
93
  }
91
94
  /**
@@ -120,32 +123,25 @@ class WebSocketPool {
120
123
  handler(data);
121
124
  }
122
125
  };
123
- async sendRequest(request) {
124
- const sendPromises = this.rwsPool.map(async (rws) => {
125
- try {
126
- await rws.send(JSON.stringify(request));
127
- }
128
- catch (error) {
129
- this.logger.error("Failed to send request:", error);
130
- throw error;
131
- }
132
- });
133
- await Promise.all(sendPromises);
126
+ sendRequest(request) {
127
+ for (const rws of this.rwsPool) {
128
+ rws.send(JSON.stringify(request));
129
+ }
134
130
  }
135
- async addSubscription(request) {
131
+ addSubscription(request) {
136
132
  if (request.type !== "subscribe") {
137
133
  throw new Error("Request must be a subscribe request");
138
134
  }
139
135
  this.subscriptions.set(request.subscriptionId, request);
140
- await this.sendRequest(request);
136
+ this.sendRequest(request);
141
137
  }
142
- async removeSubscription(subscriptionId) {
138
+ removeSubscription(subscriptionId) {
143
139
  this.subscriptions.delete(subscriptionId);
144
140
  const request = {
145
141
  type: "unsubscribe",
146
142
  subscriptionId,
147
143
  };
148
- await this.sendRequest(request);
144
+ this.sendRequest(request);
149
145
  }
150
146
  addMessageListener(handler) {
151
147
  this.messageListeners.push(handler);
@@ -158,7 +154,10 @@ class WebSocketPool {
158
154
  this.allConnectionsDownListeners.push(handler);
159
155
  }
160
156
  areAllConnectionsDown() {
161
- return this.rwsPool.every((ws) => !ws.isConnected || ws.isReconnecting);
157
+ return this.rwsPool.every((ws) => !ws.isConnected() || ws.isReconnecting());
158
+ }
159
+ isAnyConnectionEstablished() {
160
+ return this.rwsPool.some((ws) => ws.isConnected());
162
161
  }
163
162
  checkConnectionStates() {
164
163
  const allDown = this.areAllConnectionsDown();
@@ -1,5 +1,5 @@
1
- import type { Logger } from "ts-log";
2
1
  import type { ParsedPayload, Request, Response } from "./protocol.js";
2
+ import type { WebSocketPoolConfig } from "./socket/websocket-pool.js";
3
3
  export type BinaryResponse = {
4
4
  subscriptionId: number;
5
5
  evm?: Buffer | undefined;
@@ -25,7 +25,7 @@ export declare class PythLazerClient {
25
25
  * @param numConnections - The number of parallel WebSocket connections to establish (default: 3). A higher number gives a more reliable stream. The connections will round-robin across the provided URLs.
26
26
  * @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
27
27
  */
28
- static create(urls: string[], token: string, numConnections?: number, logger?: Logger): Promise<PythLazerClient>;
28
+ static create(config: WebSocketPoolConfig): Promise<PythLazerClient>;
29
29
  /**
30
30
  * Adds a message listener that receives either JSON or binary responses from the WebSocket connections.
31
31
  * The listener will be called for each message received, with deduplication across redundant connections.
@@ -33,9 +33,9 @@ export declare class PythLazerClient {
33
33
  * or a binary response containing EVM, Solana, or parsed payload data.
34
34
  */
35
35
  addMessageListener(handler: (event: JsonOrBinaryResponse) => void): void;
36
- subscribe(request: Request): Promise<void>;
37
- unsubscribe(subscriptionId: number): Promise<void>;
38
- send(request: Request): Promise<void>;
36
+ subscribe(request: Request): void;
37
+ unsubscribe(subscriptionId: number): void;
38
+ send(request: Request): void;
39
39
  /**
40
40
  * Registers a handler function that will be called whenever all WebSocket connections are down or attempting to reconnect.
41
41
  * The connections may still try to reconnect in the background. To shut down the pool, call `shutdown()`.
@@ -1,5 +1,4 @@
1
1
  import WebSocket from "isomorphic-ws";
2
- import { dummyLogger } from "ts-log";
3
2
  import { BINARY_UPDATE_FORMAT_MAGIC_LE, FORMAT_MAGICS_LE } from "./protocol.js";
4
3
  import { WebSocketPool } from "./socket/websocket-pool.js";
5
4
  const UINT16_NUM_BYTES = 2;
@@ -17,8 +16,8 @@ export class PythLazerClient {
17
16
  * @param numConnections - The number of parallel WebSocket connections to establish (default: 3). A higher number gives a more reliable stream. The connections will round-robin across the provided URLs.
18
17
  * @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
19
18
  */
20
- static async create(urls, token, numConnections = 3, logger = dummyLogger) {
21
- const wsp = await WebSocketPool.create(urls, token, numConnections, logger);
19
+ static async create(config) {
20
+ const wsp = await WebSocketPool.create(config);
22
21
  return new PythLazerClient(wsp);
23
22
  }
24
23
  /**
@@ -79,17 +78,17 @@ export class PythLazerClient {
79
78
  }
80
79
  });
81
80
  }
82
- async subscribe(request) {
81
+ subscribe(request) {
83
82
  if (request.type !== "subscribe") {
84
83
  throw new Error("Request must be a subscribe request");
85
84
  }
86
- await this.wsp.addSubscription(request);
85
+ this.wsp.addSubscription(request);
87
86
  }
88
- async unsubscribe(subscriptionId) {
89
- await this.wsp.removeSubscription(subscriptionId);
87
+ unsubscribe(subscriptionId) {
88
+ this.wsp.removeSubscription(subscriptionId);
90
89
  }
91
- async send(request) {
92
- await this.wsp.sendRequest(request);
90
+ send(request) {
91
+ this.wsp.sendRequest(request);
93
92
  }
94
93
  /**
95
94
  * Registers a handler function that will be called whenever all WebSocket connections are down or attempting to reconnect.
@@ -2,29 +2,48 @@ import type { ClientRequestArgs } from "node:http";
2
2
  import type { ClientOptions, ErrorEvent } from "isomorphic-ws";
3
3
  import WebSocket from "isomorphic-ws";
4
4
  import type { Logger } from "ts-log";
5
- export declare class ResilientWebSocket {
5
+ export type ResilientWebSocketConfig = {
6
6
  endpoint: string;
7
+ wsOptions?: ClientOptions | ClientRequestArgs | undefined;
8
+ logger?: Logger;
9
+ heartbeatTimeoutDurationMs?: number;
10
+ maxRetryDelayMs?: number;
11
+ logAfterRetryCount?: number;
12
+ };
13
+ export declare class ResilientWebSocket {
14
+ private endpoint;
15
+ private wsOptions?;
16
+ private logger;
17
+ private heartbeatTimeoutDurationMs;
18
+ private maxRetryDelayMs;
19
+ private logAfterRetryCount;
7
20
  wsClient: undefined | WebSocket;
8
21
  wsUserClosed: boolean;
9
- private wsOptions;
10
22
  private wsFailedAttempts;
11
- private heartbeatTimeout;
12
- private logger;
13
- private connectionPromise;
14
- private resolveConnection;
15
- private rejectConnection;
23
+ private heartbeatTimeout?;
24
+ private retryTimeout?;
16
25
  private _isReconnecting;
17
- get isReconnecting(): boolean;
18
- get isConnected(): boolean;
26
+ isReconnecting(): boolean;
27
+ isConnected(): this is this & {
28
+ wsClient: WebSocket;
29
+ };
30
+ private shouldLogRetry;
19
31
  onError: (error: ErrorEvent) => void;
20
32
  onMessage: (data: WebSocket.Data) => void;
21
33
  onReconnect: () => void;
22
- constructor(endpoint: string, wsOptions?: ClientOptions | ClientRequestArgs, logger?: Logger);
23
- send(data: string | Buffer): Promise<void>;
24
- startWebSocket(): Promise<void>;
34
+ constructor(config: ResilientWebSocketConfig);
35
+ send(data: string | Buffer): void;
36
+ startWebSocket(): void;
25
37
  private resetHeartbeat;
26
- private waitForMaybeReadyWebSocket;
27
- private handleClose;
28
- private restartUnexpectedClosedWebsocket;
38
+ private handleReconnect;
29
39
  closeWebSocket(): void;
40
+ /**
41
+ * Calculates the delay in milliseconds for exponential backoff based on the number of failed attempts.
42
+ *
43
+ * The delay increases exponentially with each attempt, starting at 20ms for the first attempt,
44
+ * and is capped at maxRetryDelayMs for attempts greater than or equal to 10.
45
+ *
46
+ * @returns The calculated delay in milliseconds before the next retry.
47
+ */
48
+ private retryDelayMs;
30
49
  }
@@ -1,36 +1,47 @@
1
1
  import WebSocket from "isomorphic-ws";
2
- const HEARTBEAT_TIMEOUT_DURATION = 10_000;
3
- const CONNECTION_TIMEOUT = 5000;
2
+ import { dummyLogger } from "ts-log";
3
+ const DEFAULT_HEARTBEAT_TIMEOUT_DURATION_MS = 5000; // 5 seconds
4
+ const DEFAULT_MAX_RETRY_DELAY_MS = 1000; // 1 second'
5
+ const DEFAULT_LOG_AFTER_RETRY_COUNT = 10;
4
6
  export class ResilientWebSocket {
5
7
  endpoint;
6
- wsClient;
7
- wsUserClosed;
8
8
  wsOptions;
9
+ logger;
10
+ heartbeatTimeoutDurationMs;
11
+ maxRetryDelayMs;
12
+ logAfterRetryCount;
13
+ wsClient;
14
+ wsUserClosed = false;
9
15
  wsFailedAttempts;
10
16
  heartbeatTimeout;
11
- logger;
12
- connectionPromise;
13
- resolveConnection;
14
- rejectConnection;
17
+ retryTimeout;
15
18
  _isReconnecting = false;
16
- get isReconnecting() {
19
+ isReconnecting() {
17
20
  return this._isReconnecting;
18
21
  }
19
- get isConnected() {
22
+ isConnected() {
20
23
  return this.wsClient?.readyState === WebSocket.OPEN;
21
24
  }
25
+ shouldLogRetry() {
26
+ return this.wsFailedAttempts % this.logAfterRetryCount === 0;
27
+ }
22
28
  onError;
23
29
  onMessage;
24
30
  onReconnect;
25
- constructor(endpoint, wsOptions, logger) {
26
- this.endpoint = endpoint;
27
- this.wsOptions = wsOptions;
28
- this.logger = logger;
31
+ constructor(config) {
32
+ this.endpoint = config.endpoint;
33
+ this.wsOptions = config.wsOptions;
34
+ this.logger = config.logger ?? dummyLogger;
35
+ this.heartbeatTimeoutDurationMs =
36
+ config.heartbeatTimeoutDurationMs ??
37
+ DEFAULT_HEARTBEAT_TIMEOUT_DURATION_MS;
38
+ this.maxRetryDelayMs = config.maxRetryDelayMs ?? DEFAULT_MAX_RETRY_DELAY_MS;
39
+ this.logAfterRetryCount =
40
+ config.logAfterRetryCount ?? DEFAULT_LOG_AFTER_RETRY_COUNT;
29
41
  this.wsFailedAttempts = 0;
30
42
  this.onError = (error) => {
31
- this.logger?.error(error.error);
43
+ void error;
32
44
  };
33
- this.wsUserClosed = true;
34
45
  this.onMessage = (data) => {
35
46
  void data;
36
47
  };
@@ -38,143 +49,116 @@ export class ResilientWebSocket {
38
49
  // Empty function, can be set by the user.
39
50
  };
40
51
  }
41
- async send(data) {
42
- this.logger?.info(`Sending message`);
43
- await this.waitForMaybeReadyWebSocket();
44
- if (this.wsClient === undefined) {
45
- this.logger?.error("Couldn't connect to the websocket server. Error callback is called.");
52
+ send(data) {
53
+ this.logger.debug(`Sending message`);
54
+ if (this.isConnected()) {
55
+ this.wsClient.send(data);
46
56
  }
47
57
  else {
48
- this.wsClient.send(data);
58
+ this.logger.warn(`WebSocket to ${this.endpoint} is not connected. Cannot send message.`);
49
59
  }
50
60
  }
51
- async startWebSocket() {
61
+ startWebSocket() {
62
+ if (this.wsUserClosed) {
63
+ this.logger.error("Connection was explicitly closed by user. Will not reconnect.");
64
+ return;
65
+ }
52
66
  if (this.wsClient !== undefined) {
53
- // If there's an existing connection attempt, wait for it
54
- if (this.connectionPromise) {
55
- return this.connectionPromise;
56
- }
67
+ this.logger.info("WebSocket client already started.");
57
68
  return;
58
69
  }
59
- this.logger?.info(`Creating Web Socket client`);
60
- // Create a new promise for this connection attempt
61
- this.connectionPromise = new Promise((resolve, reject) => {
62
- this.resolveConnection = resolve;
63
- this.rejectConnection = reject;
64
- });
65
- // Set a connection timeout
66
- const timeoutId = setTimeout(() => {
67
- if (this.rejectConnection) {
68
- this.rejectConnection(new Error(`Connection timeout after ${String(CONNECTION_TIMEOUT)}ms`));
69
- }
70
- }, CONNECTION_TIMEOUT);
70
+ if (this.wsFailedAttempts == 0) {
71
+ this.logger.info(`Creating Web Socket client`);
72
+ }
73
+ if (this.retryTimeout !== undefined) {
74
+ clearTimeout(this.retryTimeout);
75
+ this.retryTimeout = undefined;
76
+ }
71
77
  this.wsClient = new WebSocket(this.endpoint, this.wsOptions);
72
- this.wsUserClosed = false;
73
78
  this.wsClient.addEventListener("open", () => {
79
+ this.logger.info("WebSocket connection established");
74
80
  this.wsFailedAttempts = 0;
75
- this.resetHeartbeat();
76
- clearTimeout(timeoutId);
77
81
  this._isReconnecting = false;
78
- this.resolveConnection?.();
82
+ this.resetHeartbeat();
83
+ this.onReconnect();
84
+ });
85
+ this.wsClient.addEventListener("close", (e) => {
86
+ if (this.wsUserClosed) {
87
+ this.logger.info(`WebSocket connection to ${this.endpoint} closed by user`);
88
+ }
89
+ else {
90
+ if (this.shouldLogRetry()) {
91
+ this.logger.warn(`WebSocket connection to ${this.endpoint} closed unexpectedly: Code: ${e.code.toString()}`);
92
+ }
93
+ this.handleReconnect();
94
+ }
79
95
  });
80
96
  this.wsClient.addEventListener("error", (event) => {
81
97
  this.onError(event);
82
- if (this.rejectConnection) {
83
- this.rejectConnection(new Error("WebSocket connection failed"));
84
- }
85
98
  });
86
99
  this.wsClient.addEventListener("message", (event) => {
87
100
  this.resetHeartbeat();
88
101
  this.onMessage(event.data);
89
102
  });
90
- this.wsClient.addEventListener("close", () => {
91
- clearTimeout(timeoutId);
92
- if (this.rejectConnection) {
93
- this.rejectConnection(new Error("WebSocket closed before connecting"));
94
- }
95
- void this.handleClose();
96
- });
97
103
  if ("on" in this.wsClient) {
98
104
  this.wsClient.on("ping", () => {
99
- this.logger?.info("Ping received");
105
+ this.logger.info("Ping received");
100
106
  this.resetHeartbeat();
101
107
  });
102
108
  }
103
- return this.connectionPromise;
104
109
  }
105
110
  resetHeartbeat() {
106
111
  if (this.heartbeatTimeout !== undefined) {
107
112
  clearTimeout(this.heartbeatTimeout);
108
113
  }
109
114
  this.heartbeatTimeout = setTimeout(() => {
110
- this.logger?.warn("Connection timed out. Reconnecting...");
115
+ this.logger.warn("Connection timed out. Reconnecting...");
111
116
  this.wsClient?.terminate();
112
- void this.restartUnexpectedClosedWebsocket();
113
- }, HEARTBEAT_TIMEOUT_DURATION);
117
+ this.handleReconnect();
118
+ }, this.heartbeatTimeoutDurationMs);
114
119
  }
115
- async waitForMaybeReadyWebSocket() {
116
- let waitedTime = 0;
117
- while (this.wsClient !== undefined &&
118
- this.wsClient.readyState !== this.wsClient.OPEN) {
119
- if (waitedTime > 5000) {
120
- this.wsClient.close();
121
- return;
122
- }
123
- else {
124
- waitedTime += 10;
125
- await sleep(10);
126
- }
120
+ handleReconnect() {
121
+ if (this.wsUserClosed) {
122
+ this.logger.info("WebSocket connection closed by user, not reconnecting.");
123
+ return;
127
124
  }
128
- }
129
- async handleClose() {
130
125
  if (this.heartbeatTimeout !== undefined) {
131
126
  clearTimeout(this.heartbeatTimeout);
132
127
  }
133
- if (this.wsUserClosed) {
134
- this.logger?.info("The connection has been closed successfully.");
128
+ if (this.retryTimeout !== undefined) {
129
+ clearTimeout(this.retryTimeout);
135
130
  }
136
- else {
137
- this.wsFailedAttempts += 1;
138
- this.wsClient = undefined;
139
- this.connectionPromise = undefined;
140
- this.resolveConnection = undefined;
141
- this.rejectConnection = undefined;
142
- const waitTime = expoBackoff(this.wsFailedAttempts);
143
- this._isReconnecting = true;
144
- this.logger?.error("Connection closed unexpectedly or because of timeout. Reconnecting after " +
145
- String(waitTime) +
131
+ this.wsFailedAttempts += 1;
132
+ this.wsClient = undefined;
133
+ this._isReconnecting = true;
134
+ if (this.shouldLogRetry()) {
135
+ this.logger.error("Connection closed unexpectedly or because of timeout. Reconnecting after " +
136
+ String(this.retryDelayMs()) +
146
137
  "ms.");
147
- await sleep(waitTime);
148
- await this.restartUnexpectedClosedWebsocket();
149
138
  }
150
- }
151
- async restartUnexpectedClosedWebsocket() {
152
- if (this.wsUserClosed) {
153
- return;
154
- }
155
- await this.startWebSocket();
156
- await this.waitForMaybeReadyWebSocket();
157
- if (this.wsClient === undefined) {
158
- this.logger?.error("Couldn't reconnect to websocket. Error callback is called.");
159
- return;
160
- }
161
- this.onReconnect();
139
+ this.retryTimeout = setTimeout(() => {
140
+ this.startWebSocket();
141
+ }, this.retryDelayMs());
162
142
  }
163
143
  closeWebSocket() {
164
144
  if (this.wsClient !== undefined) {
165
- const client = this.wsClient;
145
+ this.wsClient.close();
166
146
  this.wsClient = undefined;
167
- this.connectionPromise = undefined;
168
- this.resolveConnection = undefined;
169
- this.rejectConnection = undefined;
170
- client.close();
171
147
  }
172
148
  this.wsUserClosed = true;
173
149
  }
174
- }
175
- async function sleep(ms) {
176
- return new Promise((resolve) => setTimeout(resolve, ms));
177
- }
178
- function expoBackoff(attempts) {
179
- return 2 ** attempts * 100;
150
+ /**
151
+ * Calculates the delay in milliseconds for exponential backoff based on the number of failed attempts.
152
+ *
153
+ * The delay increases exponentially with each attempt, starting at 20ms for the first attempt,
154
+ * and is capped at maxRetryDelayMs for attempts greater than or equal to 10.
155
+ *
156
+ * @returns The calculated delay in milliseconds before the next retry.
157
+ */
158
+ retryDelayMs() {
159
+ if (this.wsFailedAttempts >= 10) {
160
+ return this.maxRetryDelayMs;
161
+ }
162
+ return Math.min(2 ** this.wsFailedAttempts * 10, this.maxRetryDelayMs);
163
+ }
180
164
  }
@@ -1,7 +1,17 @@
1
+ import type { ErrorEvent } from "isomorphic-ws";
1
2
  import WebSocket from "isomorphic-ws";
2
3
  import type { Logger } from "ts-log";
3
4
  import type { Request } from "../protocol.js";
5
+ import type { ResilientWebSocketConfig } from "./resilient-websocket.js";
4
6
  import { ResilientWebSocket } from "./resilient-websocket.js";
7
+ export type WebSocketPoolConfig = {
8
+ urls: string[];
9
+ token: string;
10
+ numConnections?: number;
11
+ logger?: Logger;
12
+ rwsConfig?: Omit<ResilientWebSocketConfig, "logger" | "endpoint">;
13
+ onError?: (error: ErrorEvent) => void;
14
+ };
5
15
  export declare class WebSocketPool {
6
16
  private readonly logger;
7
17
  rwsPool: ResilientWebSocket[];
@@ -20,7 +30,7 @@ export declare class WebSocketPool {
20
30
  * @param numConnections - Number of parallel WebSocket connections to maintain (default: 3)
21
31
  * @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
22
32
  */
23
- static create(urls: string[], token: string, numConnections?: number, logger?: Logger): Promise<WebSocketPool>;
33
+ static create(config: WebSocketPoolConfig): Promise<WebSocketPool>;
24
34
  /**
25
35
  * Checks for error responses in JSON messages and throws appropriate errors
26
36
  */
@@ -30,9 +40,9 @@ export declare class WebSocketPool {
30
40
  * multiple connections before forwarding to registered handlers
31
41
  */
32
42
  dedupeHandler: (data: WebSocket.Data) => void;
33
- sendRequest(request: Request): Promise<void>;
34
- addSubscription(request: Request): Promise<void>;
35
- removeSubscription(subscriptionId: number): Promise<void>;
43
+ sendRequest(request: Request): void;
44
+ addSubscription(request: Request): void;
45
+ removeSubscription(subscriptionId: number): void;
36
46
  addMessageListener(handler: (data: WebSocket.Data) => void): void;
37
47
  /**
38
48
  * Calls the handler if all websocket connections are currently down or in reconnecting state.
@@ -40,6 +50,7 @@ export declare class WebSocketPool {
40
50
  */
41
51
  addAllConnectionsDownListener(handler: () => void): void;
42
52
  private areAllConnectionsDown;
53
+ private isAnyConnectionEstablished;
43
54
  private checkConnectionStates;
44
55
  shutdown(): void;
45
56
  }
@@ -2,7 +2,7 @@ import TTLCache from "@isaacs/ttlcache";
2
2
  import WebSocket from "isomorphic-ws";
3
3
  import { dummyLogger } from "ts-log";
4
4
  import { ResilientWebSocket } from "./resilient-websocket.js";
5
- const DEFAULT_NUM_CONNECTIONS = 3;
5
+ const DEFAULT_NUM_CONNECTIONS = 4;
6
6
  export class WebSocketPool {
7
7
  logger;
8
8
  rwsPool;
@@ -12,7 +12,7 @@ export class WebSocketPool {
12
12
  allConnectionsDownListeners;
13
13
  wasAllDown = true;
14
14
  checkConnectionStatesInterval;
15
- constructor(logger = dummyLogger) {
15
+ constructor(logger) {
16
16
  this.logger = logger;
17
17
  this.rwsPool = [];
18
18
  this.cache = new TTLCache({ ttl: 1000 * 10 }); // TTL of 10 seconds
@@ -32,24 +32,30 @@ export class WebSocketPool {
32
32
  * @param numConnections - Number of parallel WebSocket connections to maintain (default: 3)
33
33
  * @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
34
34
  */
35
- static async create(urls, token, numConnections = DEFAULT_NUM_CONNECTIONS, logger = dummyLogger) {
36
- if (urls.length === 0) {
35
+ static async create(config) {
36
+ if (config.urls.length === 0) {
37
37
  throw new Error("No URLs provided");
38
38
  }
39
+ const logger = config.logger ?? dummyLogger;
39
40
  const pool = new WebSocketPool(logger);
40
- // Create all websocket instances
41
- const connectionPromises = [];
41
+ const numConnections = config.numConnections ?? DEFAULT_NUM_CONNECTIONS;
42
42
  for (let i = 0; i < numConnections; i++) {
43
- const url = urls[i % urls.length];
43
+ const url = config.urls[i % config.urls.length];
44
44
  if (!url) {
45
45
  throw new Error(`URLs must not be null or empty`);
46
46
  }
47
47
  const wsOptions = {
48
+ ...config.rwsConfig?.wsOptions,
48
49
  headers: {
49
- Authorization: `Bearer ${token}`,
50
+ Authorization: `Bearer ${config.token}`,
50
51
  },
51
52
  };
52
- const rws = new ResilientWebSocket(url, wsOptions, logger);
53
+ const rws = new ResilientWebSocket({
54
+ ...config.rwsConfig,
55
+ endpoint: url,
56
+ wsOptions,
57
+ logger,
58
+ });
53
59
  // If a websocket client unexpectedly disconnects, ResilientWebSocket will reestablish
54
60
  // the connection and call the onReconnect callback.
55
61
  rws.onReconnect = () => {
@@ -58,29 +64,26 @@ export class WebSocketPool {
58
64
  }
59
65
  for (const [, request] of pool.subscriptions) {
60
66
  try {
61
- void rws.send(JSON.stringify(request));
67
+ rws.send(JSON.stringify(request));
62
68
  }
63
69
  catch (error) {
64
70
  pool.logger.error("Failed to resend subscription on reconnect:", error);
65
71
  }
66
72
  }
67
73
  };
74
+ if (config.onError) {
75
+ rws.onError = config.onError;
76
+ }
68
77
  // Handle all client messages ourselves. Dedupe before sending to registered message handlers.
69
78
  rws.onMessage = pool.dedupeHandler;
70
79
  pool.rwsPool.push(rws);
71
- // Start the websocket and collect the promise
72
- connectionPromises.push(rws.startWebSocket());
73
- }
74
- // Wait for all connections to be established
75
- try {
76
- await Promise.all(connectionPromises);
80
+ rws.startWebSocket();
77
81
  }
78
- catch (error) {
79
- // If any connection fails, clean up and throw
80
- pool.shutdown();
81
- throw error;
82
+ pool.logger.info(`Started WebSocketPool with ${numConnections.toString()} connections. Waiting for at least one to connect...`);
83
+ while (!pool.isAnyConnectionEstablished()) {
84
+ await new Promise((resolve) => setTimeout(resolve, 100));
82
85
  }
83
- pool.logger.info(`Successfully established ${numConnections.toString()} redundant WebSocket connections`);
86
+ pool.logger.info(`At least one WebSocket connection is established. WebSocketPool is ready.`);
84
87
  return pool;
85
88
  }
86
89
  /**
@@ -115,32 +118,25 @@ export class WebSocketPool {
115
118
  handler(data);
116
119
  }
117
120
  };
118
- async sendRequest(request) {
119
- const sendPromises = this.rwsPool.map(async (rws) => {
120
- try {
121
- await rws.send(JSON.stringify(request));
122
- }
123
- catch (error) {
124
- this.logger.error("Failed to send request:", error);
125
- throw error;
126
- }
127
- });
128
- await Promise.all(sendPromises);
121
+ sendRequest(request) {
122
+ for (const rws of this.rwsPool) {
123
+ rws.send(JSON.stringify(request));
124
+ }
129
125
  }
130
- async addSubscription(request) {
126
+ addSubscription(request) {
131
127
  if (request.type !== "subscribe") {
132
128
  throw new Error("Request must be a subscribe request");
133
129
  }
134
130
  this.subscriptions.set(request.subscriptionId, request);
135
- await this.sendRequest(request);
131
+ this.sendRequest(request);
136
132
  }
137
- async removeSubscription(subscriptionId) {
133
+ removeSubscription(subscriptionId) {
138
134
  this.subscriptions.delete(subscriptionId);
139
135
  const request = {
140
136
  type: "unsubscribe",
141
137
  subscriptionId,
142
138
  };
143
- await this.sendRequest(request);
139
+ this.sendRequest(request);
144
140
  }
145
141
  addMessageListener(handler) {
146
142
  this.messageListeners.push(handler);
@@ -153,7 +149,10 @@ export class WebSocketPool {
153
149
  this.allConnectionsDownListeners.push(handler);
154
150
  }
155
151
  areAllConnectionsDown() {
156
- return this.rwsPool.every((ws) => !ws.isConnected || ws.isReconnecting);
152
+ return this.rwsPool.every((ws) => !ws.isConnected() || ws.isReconnecting());
153
+ }
154
+ isAnyConnectionEstablished() {
155
+ return this.rwsPool.some((ws) => ws.isConnected());
157
156
  }
158
157
  checkConnectionStates() {
159
158
  const allDown = this.areAllConnectionsDown();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pythnetwork/pyth-lazer-sdk",
3
- "version": "0.5.0",
3
+ "version": "1.0.0",
4
4
  "description": "Pyth Lazer SDK",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -20,31 +20,19 @@
20
20
  "default": "./dist/cjs/index.js"
21
21
  }
22
22
  },
23
- "scripts": {
24
- "build:cjs": "tsc --project tsconfig.build.json --verbatimModuleSyntax false --module commonjs --outDir ./dist/cjs && echo '{\"type\":\"commonjs\"}' > dist/cjs/package.json",
25
- "build:esm": "tsc --project tsconfig.build.json --outDir ./dist/esm && echo '{\"type\":\"module\"}' > dist/esm/package.json",
26
- "fix:lint": "eslint --fix . --max-warnings 0",
27
- "test:lint": "eslint . --max-warnings 0",
28
- "test:types": "tsc",
29
- "test:format": "prettier --check .",
30
- "fix:format": "prettier --write .",
31
- "example": "node --loader ts-node/esm examples/index.js",
32
- "doc": "typedoc --out docs/typedoc src",
33
- "publish": "pnpm run script -- publish"
34
- },
35
23
  "devDependencies": {
36
- "@cprussin/eslint-config": "catalog:",
37
- "@cprussin/tsconfig": "catalog:",
24
+ "@cprussin/eslint-config": "^4.0.2",
25
+ "@cprussin/tsconfig": "^3.1.2",
38
26
  "@types/node": "^18.19.54",
39
27
  "@types/ws": "^8.5.12",
40
- "eslint": "catalog:",
41
- "prettier": "catalog:",
42
- "ts-node": "catalog:",
28
+ "eslint": "^9.23.0",
29
+ "prettier": "^3.5.3",
30
+ "ts-node": "^10.9.2",
43
31
  "typedoc": "^0.26.8",
44
- "typescript": "catalog:"
32
+ "typescript": "^5.8.2"
45
33
  },
46
34
  "bugs": {
47
- "url": "https://github.com/pyth-lazer-sdk/pyth-lazer-sdk/issues"
35
+ "url": "https://github.com/pyth-network/pyth-crosschain/issues"
48
36
  },
49
37
  "type": "module",
50
38
  "homepage": "https://github.com/pyth-network/pyth-crosschain/tree/main/lazer/sdk/js",
@@ -66,5 +54,15 @@
66
54
  "ts-log": "^2.2.7",
67
55
  "ws": "^8.18.0"
68
56
  },
69
- "gitHead": "da6c1185d9fce5e6ca2df4f7e6e1f5edc9b366eb"
70
- }
57
+ "scripts": {
58
+ "build:cjs": "tsc --project tsconfig.build.json --verbatimModuleSyntax false --module commonjs --outDir ./dist/cjs && echo '{\"type\":\"commonjs\"}' > dist/cjs/package.json",
59
+ "build:esm": "tsc --project tsconfig.build.json --outDir ./dist/esm && echo '{\"type\":\"module\"}' > dist/esm/package.json",
60
+ "fix:lint": "eslint --fix . --max-warnings 0",
61
+ "test:lint": "eslint . --max-warnings 0",
62
+ "test:types": "tsc",
63
+ "test:format": "prettier --check .",
64
+ "fix:format": "prettier --write .",
65
+ "example": "node --loader ts-node/esm examples/index.js",
66
+ "doc": "typedoc --out docs/typedoc src"
67
+ }
68
+ }