@pythnetwork/pyth-lazer-sdk 6.2.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.
@@ -22,39 +22,64 @@ class PythLazerClient {
22
22
  priceServiceUrl;
23
23
  token;
24
24
  wsp;
25
- abortSignal;
26
- constructor({ abortSignal, logger, metadataServiceUrl, priceServiceUrl, token, wsp }){
27
- this.abortSignal = abortSignal;
25
+ constructor({ logger, metadataServiceUrl, priceServiceUrl, token, wsp }){
28
26
  this.logger = logger;
29
27
  this.metadataServiceUrl = metadataServiceUrl;
30
28
  this.priceServiceUrl = priceServiceUrl;
31
29
  this.token = token;
32
30
  this.wsp = wsp;
33
- this.bindHandlers();
34
31
  }
35
32
  /**
36
33
  * Creates a new PythLazerClient instance.
37
34
  * @param config - Configuration including token, metadata service URL, and price service URL, and WebSocket pool configuration
38
35
  */ static async create(config) {
36
+ let wsp = undefined;
37
+ let client = undefined;
38
+ const { abortSignal } = config;
39
+ const handleAbort = ()=>{
40
+ // we need to EXPLICITLY shutdown both items,
41
+ // because there is a possibility the client was undefined
42
+ // when the abort signal was called
43
+ client?.shutdown();
44
+ wsp?.shutdown();
45
+ abortSignal?.removeEventListener("abort", handleAbort);
46
+ };
47
+ const throwIfAborted = ()=>{
48
+ if (abortSignal?.aborted) {
49
+ throw new DOMException("PythLazerClient.create() was aborted", "AbortError");
50
+ }
51
+ };
52
+ abortSignal?.addEventListener("abort", handleAbort);
39
53
  const token = config.token;
40
54
  // Collect and remove trailing slash from URLs
41
55
  const metadataServiceUrl = (config.metadataServiceUrl ?? _constants.DEFAULT_METADATA_SERVICE_URL).replace(/\/+$/, "");
42
56
  const priceServiceUrl = (config.priceServiceUrl ?? _constants.DEFAULT_PRICE_SERVICE_URL).replace(/\/+$/, "");
43
57
  const logger = config.logger ?? _tslog.dummyLogger;
44
- // the prior API was mismatched, in that it marked a websocket pool as optional,
45
- // yet all internal code on the Pyth Pro client used it and threw if it didn't exist.
46
- // now, the typings indicate it's no longer optional and we don't sanity check
47
- // if it's set
48
- const wsp = await _websocketpool.WebSocketPool.create(config.webSocketPoolConfig, token, config.abortSignal, logger);
49
- const client = new PythLazerClient({
50
- abortSignal: config.abortSignal,
51
- logger,
52
- metadataServiceUrl,
53
- priceServiceUrl,
54
- token,
55
- wsp
56
- });
57
- return client;
58
+ try {
59
+ throwIfAborted();
60
+ // the prior API was mismatched, in that it marked a websocket pool as optional,
61
+ // yet all internal code on the Pyth Pro client used it and threw if it didn't exist.
62
+ // now, the typings indicate it's no longer optional and we don't sanity check
63
+ // if it's set
64
+ wsp = await _websocketpool.WebSocketPool.create(config.webSocketPoolConfig, token, abortSignal, logger);
65
+ throwIfAborted();
66
+ client = new PythLazerClient({
67
+ logger,
68
+ metadataServiceUrl,
69
+ priceServiceUrl,
70
+ token,
71
+ wsp
72
+ });
73
+ throwIfAborted();
74
+ // detach the abortSignal handler here, because we've made it!
75
+ abortSignal?.removeEventListener("abort", handleAbort);
76
+ return client;
77
+ } catch (error) {
78
+ client?.shutdown();
79
+ wsp?.shutdown();
80
+ abortSignal?.removeEventListener("abort", handleAbort);
81
+ throw error;
82
+ }
58
83
  }
59
84
  /**
60
85
  * Adds a message listener that receives either JSON or binary responses from the WebSocket connections.
@@ -108,11 +133,6 @@ class PythLazerClient {
108
133
  });
109
134
  });
110
135
  }
111
- /**
112
- * binds any internal event handlers
113
- */ bindHandlers() {
114
- this.abortSignal?.addEventListener("abort", this.abortHandler);
115
- }
116
136
  subscribe(request) {
117
137
  if (request.type !== "subscribe") {
118
138
  throw new Error("Request must be a subscribe request");
@@ -150,14 +170,7 @@ class PythLazerClient {
150
170
  */ addConnectionReconnectListener(handler) {
151
171
  this.wsp.addConnectionReconnectListener(handler);
152
172
  }
153
- /**
154
- * called if and only if a user provided an abort signal and it was aborted
155
- */ abortHandler = ()=>{
156
- this.shutdown();
157
- };
158
173
  shutdown() {
159
- // Clean up abort signal listener to prevent memory leak
160
- this.abortSignal?.removeEventListener("abort", this.abortHandler);
161
174
  this.wsp.shutdown();
162
175
  }
163
176
  /**
@@ -18,8 +18,9 @@ export type JsonOrBinaryResponse = {
18
18
  };
19
19
  export type LazerClientConfig = {
20
20
  /**
21
- * if provided and the signal is detected as canceled,
22
- * all active listeners and connections will be unbound and killed.
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'.
23
24
  */
24
25
  abortSignal?: AbortSignal;
25
26
  token: string;
@@ -34,7 +35,6 @@ export declare class PythLazerClient {
34
35
  private priceServiceUrl;
35
36
  private token;
36
37
  private wsp;
37
- private abortSignal;
38
38
  private constructor();
39
39
  /**
40
40
  * Creates a new PythLazerClient instance.
@@ -48,10 +48,6 @@ export declare class PythLazerClient {
48
48
  * or a binary response containing EVM, Solana, or parsed payload data.
49
49
  */
50
50
  addMessageListener(handler: (event: JsonOrBinaryResponse) => void): void;
51
- /**
52
- * binds any internal event handlers
53
- */
54
- bindHandlers(): void;
55
51
  subscribe(request: Request): void;
56
52
  unsubscribe(subscriptionId: number): void;
57
53
  send(request: Request): void;
@@ -76,10 +72,6 @@ export declare class PythLazerClient {
76
72
  * @param handler - Function to be called with connection index and endpoint URL
77
73
  */
78
74
  addConnectionReconnectListener(handler: (connectionIndex: number, endpoint: string) => void): void;
79
- /**
80
- * called if and only if a user provided an abort signal and it was aborted
81
- */
82
- protected abortHandler: () => void;
83
75
  shutdown(): void;
84
76
  /**
85
77
  * Private helper method to make authenticated HTTP requests with Bearer token
@@ -22,7 +22,6 @@ function _interop_require_default(obj) {
22
22
  const DEFAULT_NUM_CONNECTIONS = 4;
23
23
  class WebSocketPool extends _index.IsomorphicEventEmitter {
24
24
  logger;
25
- abortSignal;
26
25
  rwsPool;
27
26
  cache;
28
27
  subscriptions;
@@ -34,8 +33,8 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
34
33
  wasAllDown = true;
35
34
  checkConnectionStatesInterval;
36
35
  isShutdown = false;
37
- constructor(logger, abortSignal){
38
- super(), this.logger = logger, this.abortSignal = abortSignal;
36
+ constructor(logger){
37
+ super(), this.logger = logger;
39
38
  this.rwsPool = [];
40
39
  this.cache = new _ttlcache.default({
41
40
  ttl: 1000 * 10
@@ -59,90 +58,102 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
59
58
  * @param numConnections - Number of parallel WebSocket connections to maintain (default: 3)
60
59
  * @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
61
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();
62
69
  const urls = config.urls ?? [
63
70
  _constants.DEFAULT_STREAM_SERVICE_0_URL,
64
71
  _constants.DEFAULT_STREAM_SERVICE_1_URL
65
72
  ];
66
73
  const log = logger ?? _tslog.dummyLogger;
67
- const pool = new WebSocketPool(log, abortSignal);
68
- const numConnections = config.numConnections ?? DEFAULT_NUM_CONNECTIONS;
69
- // bind a handler to capture any emitted errors and send them to the user-provided
70
- // onWebSocketPoolError callback (if it is present)
71
- if (typeof config.onWebSocketPoolError === "function") {
72
- pool.on("error", config.onWebSocketPoolError);
73
- pool.once("shutdown", ()=>{
74
- // unbind all error handlers so we don't leak memory
75
- pool.off("error");
76
- });
77
- }
78
- for(let i = 0; i < numConnections; i++){
79
- const baseUrl = urls[i % urls.length];
80
- const isBrowser = (0, _index1.envIsBrowserOrWorker)();
81
- const url = isBrowser ? (0, _index1.addAuthTokenToWebSocketUrl)(baseUrl, token) : baseUrl;
82
- if (!url) {
83
- throw new Error(`URLs must not be null or empty`);
74
+ const pool = new WebSocketPool(log);
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
+ });
84
85
  }
85
- const wsOptions = {
86
- ...config.rwsConfig?.wsOptions,
87
- headers: isBrowser ? undefined : {
88
- Authorization: `Bearer ${token}`
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`);
89
92
  }
90
- };
91
- const rws = new _resilientwebsocket.ResilientWebSocket({
92
- ...config.rwsConfig,
93
- endpoint: url,
94
- logger: log,
95
- wsOptions
96
- });
97
- const connectionIndex = i;
98
- const connectionEndpoint = url;
99
- // If a websocket client unexpectedly disconnects, ResilientWebSocket will reestablish
100
- // the connection and call the onReconnect callback.
101
- rws.onReconnect = ()=>{
102
- if (rws.wsUserClosed || pool.isShutdown || pool.abortSignal?.aborted) {
103
- return;
104
- }
105
- for (const listener of pool.connectionReconnectListeners){
106
- listener(connectionIndex, connectionEndpoint);
107
- }
108
- for (const [, request] of pool.subscriptions){
109
- try {
110
- rws.send(JSON.stringify(request));
111
- } catch (error) {
112
- 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}`
113
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);
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;
114
133
  }
115
- };
116
- rws.onTimeout = ()=>{
117
- for (const listener of pool.connectionTimeoutListeners){
118
- listener(connectionIndex, connectionEndpoint);
119
- }
120
- };
121
- // eslint-disable-next-line @typescript-eslint/no-deprecated
122
- const onErrorHandler = config.onWebSocketError ?? config.onError;
123
- if (typeof onErrorHandler === "function") {
124
- 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();
125
142
  }
126
- // Handle all client messages ourselves. Dedupe before sending to registered message handlers.
127
- rws.onMessage = (data)=>{
128
- pool.dedupeHandler(data).catch((error)=>{
129
- pool.emitPoolError(error, "Error in WebSocketPool dedupeHandler");
130
- });
131
- };
132
- pool.rwsPool.push(rws);
133
- rws.startWebSocket();
134
- }
135
- pool.logger.info(`Started WebSocketPool with ${numConnections.toString()} connections. Waiting for at least one to connect...`);
136
- while(!pool.isAnyConnectionEstablished()){
137
- if (pool.abortSignal?.aborted) {
138
- pool.logger.warn("the WebSocket Pool's abort signal was aborted during connection. Shutting down.");
139
- pool.shutdown();
140
- return pool;
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));
141
147
  }
142
- await new Promise((resolve)=>setTimeout(resolve, 100));
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;
143
156
  }
144
- pool.logger.info(`At least one WebSocket connection is established. WebSocketPool is ready.`);
145
- return pool;
146
157
  }
147
158
  emitPoolError(error, context) {
148
159
  const err = error instanceof Error ? error : new Error(String(error));
@@ -194,8 +205,8 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
194
205
  }
195
206
  };
196
207
  sendRequest(request) {
197
- if (this.isShutdown || this.abortSignal?.aborted) {
198
- this.logger.warn("Cannot send request: WebSocketPool is shutdown or aborted");
208
+ if (this.isShutdown) {
209
+ this.logger.warn("Cannot send request: WebSocketPool is shutdown");
199
210
  return;
200
211
  }
201
212
  for (const rws of this.rwsPool){
@@ -207,8 +218,8 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
207
218
  }
208
219
  }
209
220
  addSubscription(request) {
210
- if (this.isShutdown || this.abortSignal?.aborted) {
211
- this.logger.warn("Cannot add subscription: WebSocketPool is shutdown or aborted");
221
+ if (this.isShutdown) {
222
+ this.logger.warn("Cannot add subscription: WebSocketPool is shutdown");
212
223
  return;
213
224
  }
214
225
  if (request.type !== "subscribe") {
@@ -219,8 +230,8 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
219
230
  this.sendRequest(request);
220
231
  }
221
232
  removeSubscription(subscriptionId) {
222
- if (this.isShutdown || this.abortSignal?.aborted) {
223
- this.logger.warn("Cannot remove subscription: WebSocketPool is shutdown or aborted");
233
+ if (this.isShutdown) {
234
+ this.logger.warn("Cannot remove subscription: WebSocketPool is shutdown");
224
235
  return;
225
236
  }
226
237
  this.subscriptions.delete(subscriptionId);
@@ -263,8 +274,8 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
263
274
  return this.rwsPool.some((ws)=>ws.isConnected());
264
275
  }
265
276
  checkConnectionStates() {
266
- // Stop monitoring if shutdown or aborted
267
- if (this.isShutdown || this.abortSignal?.aborted) {
277
+ // Stop monitoring if shutdown
278
+ if (this.isShutdown) {
268
279
  return;
269
280
  }
270
281
  const allDown = this.areAllConnectionsDown();
@@ -46,7 +46,6 @@ export type WebSocketPoolEvents = {
46
46
  };
47
47
  export declare class WebSocketPool extends IsomorphicEventEmitter<WebSocketPoolEvents> {
48
48
  private readonly logger;
49
- private readonly abortSignal?;
50
49
  rwsPool: ResilientWebSocket[];
51
50
  private cache;
52
51
  private subscriptions;
@@ -67,7 +66,7 @@ export declare class WebSocketPool extends IsomorphicEventEmitter<WebSocketPoolE
67
66
  * @param numConnections - Number of parallel WebSocket connections to maintain (default: 3)
68
67
  * @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
69
68
  */
70
- static create(config: WebSocketPoolConfig, token: string, abortSignal: AbortSignal | null | undefined, logger?: Logger): Promise<WebSocketPool>;
69
+ static create(config: WebSocketPoolConfig, token: string, abortSignal?: AbortSignal | null | undefined, logger?: Logger): Promise<WebSocketPool>;
71
70
  private emitPoolError;
72
71
  /**
73
72
  * Checks for error responses in JSON messages and throws appropriate errors
@@ -18,8 +18,9 @@ export type JsonOrBinaryResponse = {
18
18
  };
19
19
  export type LazerClientConfig = {
20
20
  /**
21
- * if provided and the signal is detected as canceled,
22
- * all active listeners and connections will be unbound and killed.
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'.
23
24
  */
24
25
  abortSignal?: AbortSignal;
25
26
  token: string;
@@ -34,7 +35,6 @@ export declare class PythLazerClient {
34
35
  private priceServiceUrl;
35
36
  private token;
36
37
  private wsp;
37
- private abortSignal;
38
38
  private constructor();
39
39
  /**
40
40
  * Creates a new PythLazerClient instance.
@@ -48,10 +48,6 @@ export declare class PythLazerClient {
48
48
  * or a binary response containing EVM, Solana, or parsed payload data.
49
49
  */
50
50
  addMessageListener(handler: (event: JsonOrBinaryResponse) => void): void;
51
- /**
52
- * binds any internal event handlers
53
- */
54
- bindHandlers(): void;
55
51
  subscribe(request: Request): void;
56
52
  unsubscribe(subscriptionId: number): void;
57
53
  send(request: Request): void;
@@ -76,10 +72,6 @@ export declare class PythLazerClient {
76
72
  * @param handler - Function to be called with connection index and endpoint URL
77
73
  */
78
74
  addConnectionReconnectListener(handler: (connectionIndex: number, endpoint: string) => void): void;
79
- /**
80
- * called if and only if a user provided an abort signal and it was aborted
81
- */
82
- protected abortHandler: () => void;
83
75
  shutdown(): void;
84
76
  /**
85
77
  * Private helper method to make authenticated HTTP requests with Bearer token
@@ -12,39 +12,64 @@ export class PythLazerClient {
12
12
  priceServiceUrl;
13
13
  token;
14
14
  wsp;
15
- abortSignal;
16
- constructor({ abortSignal, logger, metadataServiceUrl, priceServiceUrl, token, wsp }){
17
- this.abortSignal = abortSignal;
15
+ constructor({ logger, metadataServiceUrl, priceServiceUrl, token, wsp }){
18
16
  this.logger = logger;
19
17
  this.metadataServiceUrl = metadataServiceUrl;
20
18
  this.priceServiceUrl = priceServiceUrl;
21
19
  this.token = token;
22
20
  this.wsp = wsp;
23
- this.bindHandlers();
24
21
  }
25
22
  /**
26
23
  * Creates a new PythLazerClient instance.
27
24
  * @param config - Configuration including token, metadata service URL, and price service URL, and WebSocket pool configuration
28
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);
29
43
  const token = config.token;
30
44
  // Collect and remove trailing slash from URLs
31
45
  const metadataServiceUrl = (config.metadataServiceUrl ?? DEFAULT_METADATA_SERVICE_URL).replace(/\/+$/, "");
32
46
  const priceServiceUrl = (config.priceServiceUrl ?? DEFAULT_PRICE_SERVICE_URL).replace(/\/+$/, "");
33
47
  const logger = config.logger ?? dummyLogger;
34
- // the prior API was mismatched, in that it marked a websocket pool as optional,
35
- // yet all internal code on the Pyth Pro client used it and threw if it didn't exist.
36
- // now, the typings indicate it's no longer optional and we don't sanity check
37
- // if it's set
38
- const wsp = await WebSocketPool.create(config.webSocketPoolConfig, token, config.abortSignal, logger);
39
- const client = new PythLazerClient({
40
- abortSignal: config.abortSignal,
41
- logger,
42
- metadataServiceUrl,
43
- priceServiceUrl,
44
- token,
45
- wsp
46
- });
47
- return client;
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;
72
+ }
48
73
  }
49
74
  /**
50
75
  * Adds a message listener that receives either JSON or binary responses from the WebSocket connections.
@@ -98,11 +123,6 @@ export class PythLazerClient {
98
123
  });
99
124
  });
100
125
  }
101
- /**
102
- * binds any internal event handlers
103
- */ bindHandlers() {
104
- this.abortSignal?.addEventListener("abort", this.abortHandler);
105
- }
106
126
  subscribe(request) {
107
127
  if (request.type !== "subscribe") {
108
128
  throw new Error("Request must be a subscribe request");
@@ -140,14 +160,7 @@ export class PythLazerClient {
140
160
  */ addConnectionReconnectListener(handler) {
141
161
  this.wsp.addConnectionReconnectListener(handler);
142
162
  }
143
- /**
144
- * called if and only if a user provided an abort signal and it was aborted
145
- */ abortHandler = ()=>{
146
- this.shutdown();
147
- };
148
163
  shutdown() {
149
- // Clean up abort signal listener to prevent memory leak
150
- this.abortSignal?.removeEventListener("abort", this.abortHandler);
151
164
  this.wsp.shutdown();
152
165
  }
153
166
  /**
@@ -46,7 +46,6 @@ export type WebSocketPoolEvents = {
46
46
  };
47
47
  export declare class WebSocketPool extends IsomorphicEventEmitter<WebSocketPoolEvents> {
48
48
  private readonly logger;
49
- private readonly abortSignal?;
50
49
  rwsPool: ResilientWebSocket[];
51
50
  private cache;
52
51
  private subscriptions;
@@ -67,7 +66,7 @@ export declare class WebSocketPool extends IsomorphicEventEmitter<WebSocketPoolE
67
66
  * @param numConnections - Number of parallel WebSocket connections to maintain (default: 3)
68
67
  * @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
69
68
  */
70
- static create(config: WebSocketPoolConfig, token: string, abortSignal: AbortSignal | null | undefined, logger?: Logger): Promise<WebSocketPool>;
69
+ static create(config: WebSocketPoolConfig, token: string, abortSignal?: AbortSignal | null | undefined, logger?: Logger): Promise<WebSocketPool>;
71
70
  private emitPoolError;
72
71
  /**
73
72
  * Checks for error responses in JSON messages and throws appropriate errors
@@ -7,7 +7,6 @@ import { ResilientWebSocket } from "./resilient-websocket.mjs";
7
7
  const DEFAULT_NUM_CONNECTIONS = 4;
8
8
  export class WebSocketPool extends IsomorphicEventEmitter {
9
9
  logger;
10
- abortSignal;
11
10
  rwsPool;
12
11
  cache;
13
12
  subscriptions;
@@ -19,8 +18,8 @@ export class WebSocketPool extends IsomorphicEventEmitter {
19
18
  wasAllDown = true;
20
19
  checkConnectionStatesInterval;
21
20
  isShutdown = false;
22
- constructor(logger, abortSignal){
23
- super(), this.logger = logger, this.abortSignal = abortSignal;
21
+ constructor(logger){
22
+ super(), this.logger = logger;
24
23
  this.rwsPool = [];
25
24
  this.cache = new TTLCache({
26
25
  ttl: 1000 * 10
@@ -44,90 +43,102 @@ export class WebSocketPool extends IsomorphicEventEmitter {
44
43
  * @param numConnections - Number of parallel WebSocket connections to maintain (default: 3)
45
44
  * @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
46
45
  */ static async create(config, token, abortSignal, logger) {
46
+ // Helper to check if aborted and throw
47
+ const throwIfAborted = ()=>{
48
+ if (abortSignal?.aborted) {
49
+ throw new DOMException("WebSocketPool.create() was aborted", "AbortError");
50
+ }
51
+ };
52
+ // Check before starting
53
+ throwIfAborted();
47
54
  const urls = config.urls ?? [
48
55
  DEFAULT_STREAM_SERVICE_0_URL,
49
56
  DEFAULT_STREAM_SERVICE_1_URL
50
57
  ];
51
58
  const log = logger ?? dummyLogger;
52
- const pool = new WebSocketPool(log, abortSignal);
53
- const numConnections = config.numConnections ?? DEFAULT_NUM_CONNECTIONS;
54
- // bind a handler to capture any emitted errors and send them to the user-provided
55
- // onWebSocketPoolError callback (if it is present)
56
- if (typeof config.onWebSocketPoolError === "function") {
57
- pool.on("error", config.onWebSocketPoolError);
58
- pool.once("shutdown", ()=>{
59
- // unbind all error handlers so we don't leak memory
60
- pool.off("error");
61
- });
62
- }
63
- for(let i = 0; i < numConnections; i++){
64
- const baseUrl = urls[i % urls.length];
65
- const isBrowser = envIsBrowserOrWorker();
66
- const url = isBrowser ? addAuthTokenToWebSocketUrl(baseUrl, token) : baseUrl;
67
- if (!url) {
68
- throw new Error(`URLs must not be null or empty`);
59
+ const pool = new WebSocketPool(log);
60
+ try {
61
+ const numConnections = config.numConnections ?? DEFAULT_NUM_CONNECTIONS;
62
+ // bind a handler to capture any emitted errors and send them to the user-provided
63
+ // onWebSocketPoolError callback (if it is present)
64
+ if (typeof config.onWebSocketPoolError === "function") {
65
+ pool.on("error", config.onWebSocketPoolError);
66
+ pool.once("shutdown", ()=>{
67
+ // unbind all error handlers so we don't leak memory
68
+ pool.off("error");
69
+ });
69
70
  }
70
- const wsOptions = {
71
- ...config.rwsConfig?.wsOptions,
72
- headers: isBrowser ? undefined : {
73
- Authorization: `Bearer ${token}`
71
+ for(let i = 0; i < numConnections; i++){
72
+ const baseUrl = urls[i % urls.length];
73
+ const isBrowser = envIsBrowserOrWorker();
74
+ const url = isBrowser ? addAuthTokenToWebSocketUrl(baseUrl, token) : baseUrl;
75
+ if (!url) {
76
+ throw new Error(`URLs must not be null or empty`);
74
77
  }
75
- };
76
- const rws = new ResilientWebSocket({
77
- ...config.rwsConfig,
78
- endpoint: url,
79
- logger: log,
80
- wsOptions
81
- });
82
- const connectionIndex = i;
83
- const connectionEndpoint = url;
84
- // If a websocket client unexpectedly disconnects, ResilientWebSocket will reestablish
85
- // the connection and call the onReconnect callback.
86
- rws.onReconnect = ()=>{
87
- if (rws.wsUserClosed || pool.isShutdown || pool.abortSignal?.aborted) {
88
- return;
89
- }
90
- for (const listener of pool.connectionReconnectListeners){
91
- listener(connectionIndex, connectionEndpoint);
92
- }
93
- for (const [, request] of pool.subscriptions){
94
- try {
95
- rws.send(JSON.stringify(request));
96
- } catch (error) {
97
- pool.logger.error("Failed to resend subscription on reconnect:", error);
78
+ const wsOptions = {
79
+ ...config.rwsConfig?.wsOptions,
80
+ headers: isBrowser ? undefined : {
81
+ Authorization: `Bearer ${token}`
98
82
  }
83
+ };
84
+ const rws = new ResilientWebSocket({
85
+ ...config.rwsConfig,
86
+ endpoint: url,
87
+ logger: log,
88
+ wsOptions
89
+ });
90
+ const connectionIndex = i;
91
+ const connectionEndpoint = url;
92
+ // If a websocket client unexpectedly disconnects, ResilientWebSocket will reestablish
93
+ // the connection and call the onReconnect callback.
94
+ rws.onReconnect = ()=>{
95
+ if (rws.wsUserClosed || pool.isShutdown) {
96
+ return;
97
+ }
98
+ for (const listener of pool.connectionReconnectListeners){
99
+ listener(connectionIndex, connectionEndpoint);
100
+ }
101
+ for (const [, request] of pool.subscriptions){
102
+ try {
103
+ rws.send(JSON.stringify(request));
104
+ } catch (error) {
105
+ pool.logger.error("Failed to resend subscription on reconnect:", error);
106
+ }
107
+ }
108
+ };
109
+ rws.onTimeout = ()=>{
110
+ for (const listener of pool.connectionTimeoutListeners){
111
+ listener(connectionIndex, connectionEndpoint);
112
+ }
113
+ };
114
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
115
+ const onErrorHandler = config.onWebSocketError ?? config.onError;
116
+ if (typeof onErrorHandler === "function") {
117
+ rws.onError = onErrorHandler;
99
118
  }
100
- };
101
- rws.onTimeout = ()=>{
102
- for (const listener of pool.connectionTimeoutListeners){
103
- listener(connectionIndex, connectionEndpoint);
104
- }
105
- };
106
- // eslint-disable-next-line @typescript-eslint/no-deprecated
107
- const onErrorHandler = config.onWebSocketError ?? config.onError;
108
- if (typeof onErrorHandler === "function") {
109
- rws.onError = onErrorHandler;
119
+ // Handle all client messages ourselves. Dedupe before sending to registered message handlers.
120
+ rws.onMessage = (data)=>{
121
+ pool.dedupeHandler(data).catch((error)=>{
122
+ pool.emitPoolError(error, "Error in WebSocketPool dedupeHandler");
123
+ });
124
+ };
125
+ pool.rwsPool.push(rws);
126
+ rws.startWebSocket();
110
127
  }
111
- // Handle all client messages ourselves. Dedupe before sending to registered message handlers.
112
- rws.onMessage = (data)=>{
113
- pool.dedupeHandler(data).catch((error)=>{
114
- pool.emitPoolError(error, "Error in WebSocketPool dedupeHandler");
115
- });
116
- };
117
- pool.rwsPool.push(rws);
118
- rws.startWebSocket();
119
- }
120
- pool.logger.info(`Started WebSocketPool with ${numConnections.toString()} connections. Waiting for at least one to connect...`);
121
- while(!pool.isAnyConnectionEstablished()){
122
- if (pool.abortSignal?.aborted) {
123
- pool.logger.warn("the WebSocket Pool's abort signal was aborted during connection. Shutting down.");
124
- pool.shutdown();
125
- return pool;
128
+ pool.logger.info(`Started WebSocketPool with ${numConnections.toString()} connections. Waiting for at least one to connect...`);
129
+ while(!pool.isAnyConnectionEstablished() && !pool.isShutdown){
130
+ throwIfAborted();
131
+ await new Promise((resolve)=>setTimeout(resolve, 100));
126
132
  }
127
- await new Promise((resolve)=>setTimeout(resolve, 100));
133
+ // Final check after loop exits
134
+ throwIfAborted();
135
+ pool.logger.info(`At least one WebSocket connection is established. WebSocketPool is ready.`);
136
+ return pool;
137
+ } catch (error) {
138
+ // Clean up the pool if aborted during connection wait
139
+ pool.shutdown();
140
+ throw error;
128
141
  }
129
- pool.logger.info(`At least one WebSocket connection is established. WebSocketPool is ready.`);
130
- return pool;
131
142
  }
132
143
  emitPoolError(error, context) {
133
144
  const err = error instanceof Error ? error : new Error(String(error));
@@ -179,8 +190,8 @@ export class WebSocketPool extends IsomorphicEventEmitter {
179
190
  }
180
191
  };
181
192
  sendRequest(request) {
182
- if (this.isShutdown || this.abortSignal?.aborted) {
183
- this.logger.warn("Cannot send request: WebSocketPool is shutdown or aborted");
193
+ if (this.isShutdown) {
194
+ this.logger.warn("Cannot send request: WebSocketPool is shutdown");
184
195
  return;
185
196
  }
186
197
  for (const rws of this.rwsPool){
@@ -192,8 +203,8 @@ export class WebSocketPool extends IsomorphicEventEmitter {
192
203
  }
193
204
  }
194
205
  addSubscription(request) {
195
- if (this.isShutdown || this.abortSignal?.aborted) {
196
- this.logger.warn("Cannot add subscription: WebSocketPool is shutdown or aborted");
206
+ if (this.isShutdown) {
207
+ this.logger.warn("Cannot add subscription: WebSocketPool is shutdown");
197
208
  return;
198
209
  }
199
210
  if (request.type !== "subscribe") {
@@ -204,8 +215,8 @@ export class WebSocketPool extends IsomorphicEventEmitter {
204
215
  this.sendRequest(request);
205
216
  }
206
217
  removeSubscription(subscriptionId) {
207
- if (this.isShutdown || this.abortSignal?.aborted) {
208
- this.logger.warn("Cannot remove subscription: WebSocketPool is shutdown or aborted");
218
+ if (this.isShutdown) {
219
+ this.logger.warn("Cannot remove subscription: WebSocketPool is shutdown");
209
220
  return;
210
221
  }
211
222
  this.subscriptions.delete(subscriptionId);
@@ -248,8 +259,8 @@ export class WebSocketPool extends IsomorphicEventEmitter {
248
259
  return this.rwsPool.some((ws)=>ws.isConnected());
249
260
  }
250
261
  checkConnectionStates() {
251
- // Stop monitoring if shutdown or aborted
252
- if (this.isShutdown || this.abortSignal?.aborted) {
262
+ // Stop monitoring if shutdown
263
+ if (this.isShutdown) {
253
264
  return;
254
265
  }
255
266
  const allDown = this.areAllConnectionsDown();
package/package.json CHANGED
@@ -164,5 +164,5 @@
164
164
  },
165
165
  "type": "module",
166
166
  "types": "./dist/cjs/index.d.ts",
167
- "version": "6.2.0"
167
+ "version": "6.2.1"
168
168
  }