@pythnetwork/pyth-lazer-sdk 5.2.1 → 6.2.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.
@@ -17,27 +17,20 @@ const UINT16_NUM_BYTES = 2;
17
17
  const UINT32_NUM_BYTES = 4;
18
18
  const UINT64_NUM_BYTES = 8;
19
19
  class PythLazerClient {
20
- token;
20
+ logger;
21
21
  metadataServiceUrl;
22
22
  priceServiceUrl;
23
- logger;
23
+ token;
24
24
  wsp;
25
- constructor(token, metadataServiceUrl, priceServiceUrl, logger, wsp){
26
- this.token = token;
25
+ abortSignal;
26
+ constructor({ abortSignal, logger, metadataServiceUrl, priceServiceUrl, token, wsp }){
27
+ this.abortSignal = abortSignal;
28
+ this.logger = logger;
27
29
  this.metadataServiceUrl = metadataServiceUrl;
28
30
  this.priceServiceUrl = priceServiceUrl;
29
- this.logger = logger;
31
+ this.token = token;
30
32
  this.wsp = wsp;
31
- }
32
- /**
33
- * Gets the WebSocket pool. If the WebSocket pool is not configured, an error is thrown.
34
- * @throws Error if WebSocket pool is not configured
35
- * @returns The WebSocket pool
36
- */ getWebSocketPool() {
37
- if (!this.wsp) {
38
- throw new Error("WebSocket pool is not available. Make sure to provide webSocketPoolConfig when creating the client.");
39
- }
40
- return this.wsp;
33
+ this.bindHandlers();
41
34
  }
42
35
  /**
43
36
  * Creates a new PythLazerClient instance.
@@ -48,12 +41,20 @@ class PythLazerClient {
48
41
  const metadataServiceUrl = (config.metadataServiceUrl ?? _constants.DEFAULT_METADATA_SERVICE_URL).replace(/\/+$/, "");
49
42
  const priceServiceUrl = (config.priceServiceUrl ?? _constants.DEFAULT_PRICE_SERVICE_URL).replace(/\/+$/, "");
50
43
  const logger = config.logger ?? _tslog.dummyLogger;
51
- // If webSocketPoolConfig is provided, create a WebSocket pool and block until at least one connection is established.
52
- let wsp;
53
- if (config.webSocketPoolConfig) {
54
- wsp = await _websocketpool.WebSocketPool.create(config.webSocketPoolConfig, token, logger);
55
- }
56
- return new PythLazerClient(token, metadataServiceUrl, priceServiceUrl, logger, wsp);
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;
57
58
  }
58
59
  /**
59
60
  * Adds a message listener that receives either JSON or binary responses from the WebSocket connections.
@@ -61,9 +62,8 @@ class PythLazerClient {
61
62
  * @param handler - Callback function that receives the parsed message. The message can be either a JSON response
62
63
  * or a binary response containing EVM, Solana, or parsed payload data.
63
64
  */ addMessageListener(handler) {
64
- const wsp = this.getWebSocketPool();
65
- wsp.addMessageListener(async (data)=>{
66
- if (typeof data == "string") {
65
+ this.wsp.addMessageListener(async (data)=>{
66
+ if (typeof data === "string") {
67
67
  handler({
68
68
  type: "json",
69
69
  value: JSON.parse(data)
@@ -74,7 +74,7 @@ class PythLazerClient {
74
74
  let pos = 0;
75
75
  const magic = buffData.subarray(pos, pos + UINT32_NUM_BYTES).readUint32LE();
76
76
  pos += UINT32_NUM_BYTES;
77
- if (magic != _protocol.BINARY_UPDATE_FORMAT_MAGIC_LE) {
77
+ if (magic !== _protocol.BINARY_UPDATE_FORMAT_MAGIC_LE) {
78
78
  throw new Error("binary update format magic mismatch");
79
79
  }
80
80
  // TODO: some uint64 values may not be representable as Number.
@@ -87,15 +87,15 @@ class PythLazerClient {
87
87
  const len = buffData.subarray(pos, pos + UINT16_NUM_BYTES).readUint16BE();
88
88
  pos += UINT16_NUM_BYTES;
89
89
  const magic = buffData.subarray(pos, pos + UINT32_NUM_BYTES).readUint32LE();
90
- if (magic == _protocol.FORMAT_MAGICS_LE.EVM) {
90
+ if (magic === _protocol.FORMAT_MAGICS_LE.EVM) {
91
91
  value.evm = buffData.subarray(pos, pos + len);
92
- } else if (magic == _protocol.FORMAT_MAGICS_LE.SOLANA) {
92
+ } else if (magic === _protocol.FORMAT_MAGICS_LE.SOLANA) {
93
93
  value.solana = buffData.subarray(pos, pos + len);
94
- } else if (magic == _protocol.FORMAT_MAGICS_LE.LE_ECDSA) {
94
+ } else if (magic === _protocol.FORMAT_MAGICS_LE.LE_ECDSA) {
95
95
  value.leEcdsa = buffData.subarray(pos, pos + len);
96
- } else if (magic == _protocol.FORMAT_MAGICS_LE.LE_UNSIGNED) {
96
+ } else if (magic === _protocol.FORMAT_MAGICS_LE.LE_UNSIGNED) {
97
97
  value.leUnsigned = buffData.subarray(pos, pos + len);
98
- } else if (magic == _protocol.FORMAT_MAGICS_LE.JSON) {
98
+ } else if (magic === _protocol.FORMAT_MAGICS_LE.JSON) {
99
99
  value.parsed = JSON.parse(buffData.subarray(pos + UINT32_NUM_BYTES, pos + len).toString());
100
100
  } else {
101
101
  throw new Error(`unknown magic: ${magic.toString()}`);
@@ -108,43 +108,67 @@ class PythLazerClient {
108
108
  });
109
109
  });
110
110
  }
111
+ /**
112
+ * binds any internal event handlers
113
+ */ bindHandlers() {
114
+ this.abortSignal?.addEventListener("abort", this.abortHandler);
115
+ }
111
116
  subscribe(request) {
112
- const wsp = this.getWebSocketPool();
113
117
  if (request.type !== "subscribe") {
114
118
  throw new Error("Request must be a subscribe request");
115
119
  }
116
- wsp.addSubscription(request);
120
+ this.wsp.addSubscription(request);
117
121
  }
118
122
  unsubscribe(subscriptionId) {
119
- const wsp = this.getWebSocketPool();
120
- wsp.removeSubscription(subscriptionId);
123
+ this.wsp.removeSubscription(subscriptionId);
121
124
  }
122
125
  send(request) {
123
- const wsp = this.getWebSocketPool();
124
- wsp.sendRequest(request);
126
+ this.wsp.sendRequest(request);
125
127
  }
126
128
  /**
127
129
  * Registers a handler function that will be called whenever all WebSocket connections are down or attempting to reconnect.
128
130
  * The connections may still try to reconnect in the background. To shut down the pool, call `shutdown()`.
129
131
  * @param handler - Function to be called when all connections are down
130
132
  */ addAllConnectionsDownListener(handler) {
131
- const wsp = this.getWebSocketPool();
132
- wsp.addAllConnectionsDownListener(handler);
133
+ this.wsp.addAllConnectionsDownListener(handler);
134
+ }
135
+ /**
136
+ * Registers a handler function that will be called when at least one connection is restored after all were down.
137
+ * @param handler - Function to be called when connection is restored
138
+ */ addConnectionRestoredListener(handler) {
139
+ this.wsp.addConnectionRestoredListener(handler);
133
140
  }
141
+ /**
142
+ * Registers a handler function that will be called when an individual connection times out (heartbeat timeout).
143
+ * @param handler - Function to be called with connection index and endpoint URL
144
+ */ addConnectionTimeoutListener(handler) {
145
+ this.wsp.addConnectionTimeoutListener(handler);
146
+ }
147
+ /**
148
+ * Registers a handler function that will be called when an individual connection reconnects.
149
+ * @param handler - Function to be called with connection index and endpoint URL
150
+ */ addConnectionReconnectListener(handler) {
151
+ this.wsp.addConnectionReconnectListener(handler);
152
+ }
153
+ /**
154
+ * called if and only if a user provided an abort signal and it was aborted
155
+ */ abortHandler = ()=>{
156
+ this.shutdown();
157
+ };
134
158
  shutdown() {
135
- const wsp = this.getWebSocketPool();
136
- wsp.shutdown();
159
+ // Clean up abort signal listener to prevent memory leak
160
+ this.abortSignal?.removeEventListener("abort", this.abortHandler);
161
+ this.wsp.shutdown();
137
162
  }
138
163
  /**
139
164
  * Private helper method to make authenticated HTTP requests with Bearer token
140
165
  * @param url - The URL to fetch
141
166
  * @param options - Additional fetch options
142
167
  * @returns Promise resolving to the fetch Response
143
- */ async authenticatedFetch(url, options = {}) {
144
- const headers = {
145
- Authorization: `Bearer ${this.token}`,
146
- ...options.headers
147
- };
168
+ */ authenticatedFetch(url, options = {}) {
169
+ // Handle all possible types of headers (Headers object, array, or plain object)
170
+ const headers = new Headers(options.headers);
171
+ headers.set("Authorization", headers.get("authorization") ?? `Bearer ${this.token}`);
148
172
  return fetch(url, {
149
173
  ...options,
150
174
  headers
@@ -181,15 +205,15 @@ class PythLazerClient {
181
205
  try {
182
206
  const body = JSON.stringify(params);
183
207
  this.logger.debug("getLatestPrice", {
184
- url,
185
- body
208
+ body,
209
+ url
186
210
  });
187
211
  const response = await this.authenticatedFetch(url, {
188
- method: "POST",
212
+ body: body,
189
213
  headers: {
190
214
  "Content-Type": "application/json"
191
215
  },
192
- body: body
216
+ method: "POST"
193
217
  });
194
218
  if (!response.ok) {
195
219
  throw new Error(`HTTP error! status: ${String(response.status)} - ${await response.text()}`);
@@ -208,15 +232,15 @@ class PythLazerClient {
208
232
  try {
209
233
  const body = JSON.stringify(params);
210
234
  this.logger.debug("getPrice", {
211
- url,
212
- body
235
+ body,
236
+ url
213
237
  });
214
238
  const response = await this.authenticatedFetch(url, {
215
- method: "POST",
239
+ body: body,
216
240
  headers: {
217
241
  "Content-Type": "application/json"
218
242
  },
219
- body: body
243
+ method: "POST"
220
244
  });
221
245
  if (!response.ok) {
222
246
  throw new Error(`HTTP error! status: ${String(response.status)} - ${await response.text()}`);
@@ -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 and the signal is detected as canceled,
22
+ * all active listeners and connections will be unbound and killed.
23
+ */
24
+ abortSignal?: AbortSignal;
20
25
  token: string;
21
26
  metadataServiceUrl?: string;
22
27
  priceServiceUrl?: string;
23
28
  logger?: Logger;
24
- webSocketPoolConfig?: WebSocketPoolConfig;
29
+ webSocketPoolConfig: WebSocketPoolConfig;
25
30
  };
26
31
  export declare class PythLazerClient {
27
- private readonly token;
28
- private readonly metadataServiceUrl;
29
- private readonly priceServiceUrl;
30
- private readonly logger;
31
- private readonly wsp?;
32
+ private logger;
33
+ private metadataServiceUrl;
34
+ private priceServiceUrl;
35
+ private token;
36
+ private wsp;
37
+ private abortSignal;
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
@@ -48,6 +48,10 @@ 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;
51
55
  subscribe(request: Request): void;
52
56
  unsubscribe(subscriptionId: number): void;
53
57
  send(request: Request): void;
@@ -57,6 +61,25 @@ export declare class PythLazerClient {
57
61
  * @param handler - Function to be called when all connections are down
58
62
  */
59
63
  addAllConnectionsDownListener(handler: () => void): void;
64
+ /**
65
+ * Registers a handler function that will be called when at least one connection is restored after all were down.
66
+ * @param handler - Function to be called when connection is restored
67
+ */
68
+ addConnectionRestoredListener(handler: () => void): void;
69
+ /**
70
+ * Registers a handler function that will be called when an individual connection times out (heartbeat timeout).
71
+ * @param handler - Function to be called with connection index and endpoint URL
72
+ */
73
+ addConnectionTimeoutListener(handler: (connectionIndex: number, endpoint: string) => void): void;
74
+ /**
75
+ * Registers a handler function that will be called when an individual connection reconnects.
76
+ * @param handler - Function to be called with connection index and endpoint URL
77
+ */
78
+ 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;
60
83
  shutdown(): void;
61
84
  /**
62
85
  * Private helper method to make authenticated HTTP requests with Bearer token
@@ -3,8 +3,8 @@ Object.defineProperty(exports, "__esModule", {
3
3
  value: true
4
4
  });
5
5
  _export_star(require("./client.cjs"), exports);
6
- _export_star(require("./protocol.cjs"), exports);
7
6
  _export_star(require("./constants.cjs"), exports);
7
+ _export_star(require("./protocol.cjs"), exports);
8
8
  function _export_star(from, to) {
9
9
  Object.keys(from).forEach(function(k) {
10
10
  if (k !== "default" && !Object.prototype.hasOwnProperty.call(to, k)) {
@@ -1,3 +1,3 @@
1
1
  export * from "./client.js";
2
- export * from "./protocol.js";
3
2
  export * from "./constants.js";
3
+ export * from "./protocol.js";
@@ -21,11 +21,11 @@ _export(exports, {
21
21
  });
22
22
  const BINARY_UPDATE_FORMAT_MAGIC_LE = 461_928_307;
23
23
  const FORMAT_MAGICS_LE = {
24
- JSON: 3_302_625_434,
25
24
  EVM: 2_593_727_018,
26
- SOLANA: 2_182_742_457,
25
+ JSON: 3_302_625_434,
27
26
  LE_ECDSA: 1_296_547_300,
28
- LE_UNSIGNED: 1_499_680_012
27
+ LE_UNSIGNED: 1_499_680_012,
28
+ SOLANA: 2_182_742_457
29
29
  };
30
30
  var CustomSocketClosureCodes = /*#__PURE__*/ function(CustomSocketClosureCodes) {
31
31
  CustomSocketClosureCodes[CustomSocketClosureCodes["CLIENT_TIMEOUT_BUT_RECONNECTING"] = 4000] = "CLIENT_TIMEOUT_BUT_RECONNECTING";
@@ -1,18 +1,19 @@
1
1
  export type Format = "evm" | "solana" | "leEcdsa" | "leUnsigned";
2
2
  export type DeliveryFormat = "json" | "binary";
3
3
  export type JsonBinaryEncoding = "base64" | "hex";
4
- export type PriceFeedProperty = "price" | "bestBidPrice" | "bestAskPrice" | "exponent" | "publisherCount" | "confidence" | "fundingRate" | "fundingTimestamp" | "fundingRateInterval";
5
- export type Channel = "real_time" | "fixed_rate@50ms" | "fixed_rate@200ms";
4
+ export type PriceFeedProperty = "price" | "bestBidPrice" | "bestAskPrice" | "exponent" | "publisherCount" | "confidence" | "fundingRate" | "fundingTimestamp" | "fundingRateInterval" | "marketSession" | "emaPrice" | "emaConfidence" | "feedUpdateTimestamp";
5
+ export type Channel = "real_time" | "fixed_rate@50ms" | "fixed_rate@200ms" | "fixed_rate@1000ms";
6
6
  export type Request = {
7
7
  type: "subscribe";
8
8
  subscriptionId: number;
9
- priceFeedIds: number[];
9
+ priceFeedIds?: number[] | undefined;
10
+ symbols?: string[] | undefined;
10
11
  properties: PriceFeedProperty[];
11
12
  formats: Format[];
12
- deliveryFormat?: DeliveryFormat;
13
- jsonBinaryEncoding?: JsonBinaryEncoding;
14
- parsed?: boolean;
15
- ignoreInvalidFeedIds?: boolean;
13
+ deliveryFormat?: DeliveryFormat | undefined;
14
+ jsonBinaryEncoding?: JsonBinaryEncoding | undefined;
15
+ parsed?: boolean | undefined;
16
+ ignoreInvalidFeedIds?: boolean | undefined;
16
17
  channel: Channel;
17
18
  } | {
18
19
  type: "unsubscribe";
@@ -25,7 +26,14 @@ export type ParsedFeedPayload = {
25
26
  bestAskPrice?: string | undefined;
26
27
  publisherCount?: number | undefined;
27
28
  exponent?: number | undefined;
28
- confidence?: string | undefined;
29
+ confidence?: number | undefined;
30
+ fundingRate?: number | undefined;
31
+ fundingTimestamp?: number | undefined;
32
+ fundingRateInterval?: number | undefined;
33
+ marketSession?: string | undefined;
34
+ emaPrice?: string | undefined;
35
+ emaConfidence?: number | undefined;
36
+ feedUpdateTimestamp?: number | undefined;
29
37
  };
30
38
  export type ParsedPayload = {
31
39
  timestampUs: string;
@@ -70,13 +78,13 @@ export type Response = {
70
78
  };
71
79
  export declare const BINARY_UPDATE_FORMAT_MAGIC_LE = 461928307;
72
80
  export declare const FORMAT_MAGICS_LE: {
73
- JSON: number;
74
81
  EVM: number;
75
- SOLANA: number;
82
+ JSON: number;
76
83
  LE_ECDSA: number;
77
84
  LE_UNSIGNED: number;
85
+ SOLANA: number;
78
86
  };
79
- export type AssetType = "crypto" | "fx" | "equity" | "metal" | "rates" | "nav" | "commodity" | "funding-rate";
87
+ export type AssetType = "crypto" | "fx" | "equity" | "metal" | "rates" | "nav" | "commodity" | "funding-rate" | "eco" | "kalshi";
80
88
  export type SymbolsQueryParams = {
81
89
  query?: string;
82
90
  asset_type?: AssetType;
@@ -45,6 +45,7 @@ class ResilientWebSocket {
45
45
  onError;
46
46
  onMessage;
47
47
  onReconnect;
48
+ onTimeout;
48
49
  constructor(config){
49
50
  this.endpoint = config.endpoint;
50
51
  this.wsOptions = config.wsOptions;
@@ -62,6 +63,9 @@ class ResilientWebSocket {
62
63
  this.onReconnect = ()=>{
63
64
  // Empty function, can be set by the user.
64
65
  };
66
+ this.onTimeout = ()=>{
67
+ // Empty function, can be set by the user.
68
+ };
65
69
  }
66
70
  send(data) {
67
71
  this.logger.debug(`Sending message`);
@@ -129,6 +133,7 @@ class ResilientWebSocket {
129
133
  this.heartbeatTimeout = setTimeout(()=>{
130
134
  const warnMsg = "Connection timed out. Reconnecting...";
131
135
  this.logger.warn(warnMsg);
136
+ this.onTimeout();
132
137
  if (this.wsClient) {
133
138
  if (typeof this.wsClient.terminate === "function") {
134
139
  this.wsClient.terminate();
@@ -44,6 +44,7 @@ export declare class ResilientWebSocket {
44
44
  onError: (error: ErrorEvent) => void;
45
45
  onMessage: (data: WebSocket.Data) => void;
46
46
  onReconnect: () => void;
47
+ onTimeout: () => void;
47
48
  constructor(config: ResilientWebSocketConfig);
48
49
  send(data: string | Buffer): void;
49
50
  startWebSocket(): void;
@@ -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
@@ -22,15 +22,20 @@ function _interop_require_default(obj) {
22
22
  const DEFAULT_NUM_CONNECTIONS = 4;
23
23
  class WebSocketPool extends _index.IsomorphicEventEmitter {
24
24
  logger;
25
+ abortSignal;
25
26
  rwsPool;
26
27
  cache;
27
28
  subscriptions;
28
29
  messageListeners;
29
30
  allConnectionsDownListeners;
31
+ connectionRestoredListeners;
32
+ connectionTimeoutListeners;
33
+ connectionReconnectListeners;
30
34
  wasAllDown = true;
31
35
  checkConnectionStatesInterval;
32
- constructor(logger){
33
- super(), this.logger = logger;
36
+ isShutdown = false;
37
+ constructor(logger, abortSignal){
38
+ super(), this.logger = logger, this.abortSignal = abortSignal;
34
39
  this.rwsPool = [];
35
40
  this.cache = new _ttlcache.default({
36
41
  ttl: 1000 * 10
@@ -38,6 +43,9 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
38
43
  this.subscriptions = new Map();
39
44
  this.messageListeners = [];
40
45
  this.allConnectionsDownListeners = [];
46
+ this.connectionRestoredListeners = [];
47
+ this.connectionTimeoutListeners = [];
48
+ this.connectionReconnectListeners = [];
41
49
  // Start monitoring connection states
42
50
  this.checkConnectionStatesInterval = setInterval(()=>{
43
51
  this.checkConnectionStates();
@@ -50,13 +58,13 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
50
58
  * @param token - Authentication token to use for the connections
51
59
  * @param numConnections - Number of parallel WebSocket connections to maintain (default: 3)
52
60
  * @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) {
61
+ */ static async create(config, token, abortSignal, logger) {
54
62
  const urls = config.urls ?? [
55
63
  _constants.DEFAULT_STREAM_SERVICE_0_URL,
56
64
  _constants.DEFAULT_STREAM_SERVICE_1_URL
57
65
  ];
58
66
  const log = logger ?? _tslog.dummyLogger;
59
- const pool = new WebSocketPool(log);
67
+ const pool = new WebSocketPool(log, abortSignal);
60
68
  const numConnections = config.numConnections ?? DEFAULT_NUM_CONNECTIONS;
61
69
  // bind a handler to capture any emitted errors and send them to the user-provided
62
70
  // onWebSocketPoolError callback (if it is present)
@@ -83,15 +91,20 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
83
91
  const rws = new _resilientwebsocket.ResilientWebSocket({
84
92
  ...config.rwsConfig,
85
93
  endpoint: url,
86
- wsOptions,
87
- logger: log
94
+ logger: log,
95
+ wsOptions
88
96
  });
97
+ const connectionIndex = i;
98
+ const connectionEndpoint = url;
89
99
  // If a websocket client unexpectedly disconnects, ResilientWebSocket will reestablish
90
100
  // the connection and call the onReconnect callback.
91
101
  rws.onReconnect = ()=>{
92
- if (rws.wsUserClosed) {
102
+ if (rws.wsUserClosed || pool.isShutdown || pool.abortSignal?.aborted) {
93
103
  return;
94
104
  }
105
+ for (const listener of pool.connectionReconnectListeners){
106
+ listener(connectionIndex, connectionEndpoint);
107
+ }
95
108
  for (const [, request] of pool.subscriptions){
96
109
  try {
97
110
  rws.send(JSON.stringify(request));
@@ -100,6 +113,11 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
100
113
  }
101
114
  }
102
115
  };
116
+ rws.onTimeout = ()=>{
117
+ for (const listener of pool.connectionTimeoutListeners){
118
+ listener(connectionIndex, connectionEndpoint);
119
+ }
120
+ };
103
121
  // eslint-disable-next-line @typescript-eslint/no-deprecated
104
122
  const onErrorHandler = config.onWebSocketError ?? config.onError;
105
123
  if (typeof onErrorHandler === "function") {
@@ -116,6 +134,11 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
116
134
  }
117
135
  pool.logger.info(`Started WebSocketPool with ${numConnections.toString()} connections. Waiting for at least one to connect...`);
118
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;
141
+ }
119
142
  await new Promise((resolve)=>setTimeout(resolve, 100));
120
143
  }
121
144
  pool.logger.info(`At least one WebSocket connection is established. WebSocketPool is ready.`);
@@ -171,6 +194,10 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
171
194
  }
172
195
  };
173
196
  sendRequest(request) {
197
+ if (this.isShutdown || this.abortSignal?.aborted) {
198
+ this.logger.warn("Cannot send request: WebSocketPool is shutdown or aborted");
199
+ return;
200
+ }
174
201
  for (const rws of this.rwsPool){
175
202
  try {
176
203
  rws.send(JSON.stringify(request));
@@ -180,6 +207,10 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
180
207
  }
181
208
  }
182
209
  addSubscription(request) {
210
+ if (this.isShutdown || this.abortSignal?.aborted) {
211
+ this.logger.warn("Cannot add subscription: WebSocketPool is shutdown or aborted");
212
+ return;
213
+ }
183
214
  if (request.type !== "subscribe") {
184
215
  this.emitPoolError(new Error("Request must be a subscribe request"));
185
216
  return;
@@ -188,10 +219,14 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
188
219
  this.sendRequest(request);
189
220
  }
190
221
  removeSubscription(subscriptionId) {
222
+ if (this.isShutdown || this.abortSignal?.aborted) {
223
+ this.logger.warn("Cannot remove subscription: WebSocketPool is shutdown or aborted");
224
+ return;
225
+ }
191
226
  this.subscriptions.delete(subscriptionId);
192
227
  const request = {
193
- type: "unsubscribe",
194
- subscriptionId
228
+ subscriptionId,
229
+ type: "unsubscribe"
195
230
  };
196
231
  this.sendRequest(request);
197
232
  }
@@ -204,6 +239,23 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
204
239
  */ addAllConnectionsDownListener(handler) {
205
240
  this.allConnectionsDownListeners.push(handler);
206
241
  }
242
+ /**
243
+ * Calls the handler when at least one connection is restored after all connections were down.
244
+ */ addConnectionRestoredListener(handler) {
245
+ this.connectionRestoredListeners.push(handler);
246
+ }
247
+ /**
248
+ * Calls the handler when an individual connection times out (heartbeat timeout).
249
+ * @param handler - Callback with connection index and endpoint URL
250
+ */ addConnectionTimeoutListener(handler) {
251
+ this.connectionTimeoutListeners.push(handler);
252
+ }
253
+ /**
254
+ * Calls the handler when an individual connection reconnects after being down.
255
+ * @param handler - Callback with connection index and endpoint URL
256
+ */ addConnectionReconnectListener(handler) {
257
+ this.connectionReconnectListeners.push(handler);
258
+ }
207
259
  areAllConnectionsDown() {
208
260
  return this.rwsPool.every((ws)=>!ws.isConnected() || ws.isReconnecting());
209
261
  }
@@ -211,6 +263,10 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
211
263
  return this.rwsPool.some((ws)=>ws.isConnected());
212
264
  }
213
265
  checkConnectionStates() {
266
+ // Stop monitoring if shutdown or aborted
267
+ if (this.isShutdown || this.abortSignal?.aborted) {
268
+ return;
269
+ }
214
270
  const allDown = this.areAllConnectionsDown();
215
271
  // If all connections just went down
216
272
  if (allDown && !this.wasAllDown) {
@@ -225,12 +281,25 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
225
281
  }
226
282
  }
227
283
  }
228
- // If at least one connection was restored
284
+ // If at least one connection was restored after all were down
229
285
  if (!allDown && this.wasAllDown) {
230
286
  this.wasAllDown = false;
287
+ this.logger.info("At least one WebSocket connection restored");
288
+ for (const listener of this.connectionRestoredListeners){
289
+ try {
290
+ listener();
291
+ } catch (error) {
292
+ this.emitPoolError(error, "Connection-restored listener threw");
293
+ }
294
+ }
231
295
  }
232
296
  }
233
297
  shutdown() {
298
+ // Prevent multiple shutdown calls
299
+ if (this.isShutdown) {
300
+ return;
301
+ }
302
+ this.isShutdown = true;
234
303
  for (const rws of this.rwsPool){
235
304
  rws.closeWebSocket();
236
305
  }
@@ -238,6 +307,9 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
238
307
  this.subscriptions.clear();
239
308
  this.messageListeners = [];
240
309
  this.allConnectionsDownListeners = [];
310
+ this.connectionRestoredListeners = [];
311
+ this.connectionTimeoutListeners = [];
312
+ this.connectionReconnectListeners = [];
241
313
  clearInterval(this.checkConnectionStatesInterval);
242
314
  // execute all bound shutdown handlers
243
315
  for (const shutdownHandler of this.getListeners("shutdown")){