@pythnetwork/pyth-lazer-sdk 6.0.0 → 6.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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";
@@ -1,3 +1,3 @@
1
1
  export * from "./client.mjs";
2
- export * from "./protocol.mjs";
3
2
  export * from "./constants.mjs";
3
+ export * from "./protocol.mjs";
@@ -2,7 +2,7 @@ export type Format = "evm" | "solana" | "leEcdsa" | "leUnsigned";
2
2
  export type DeliveryFormat = "json" | "binary";
3
3
  export type JsonBinaryEncoding = "base64" | "hex";
4
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";
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;
@@ -78,11 +78,11 @@ export type Response = {
78
78
  };
79
79
  export declare const BINARY_UPDATE_FORMAT_MAGIC_LE = 461928307;
80
80
  export declare const FORMAT_MAGICS_LE: {
81
- JSON: number;
82
81
  EVM: number;
83
- SOLANA: number;
82
+ JSON: number;
84
83
  LE_ECDSA: number;
85
84
  LE_UNSIGNED: number;
85
+ SOLANA: number;
86
86
  };
87
87
  export type AssetType = "crypto" | "fx" | "equity" | "metal" | "rates" | "nav" | "commodity" | "funding-rate" | "eco" | "kalshi";
88
88
  export type SymbolsQueryParams = {
@@ -1,10 +1,10 @@
1
1
  export const BINARY_UPDATE_FORMAT_MAGIC_LE = 461_928_307;
2
2
  export const FORMAT_MAGICS_LE = {
3
- JSON: 3_302_625_434,
4
3
  EVM: 2_593_727_018,
5
- SOLANA: 2_182_742_457,
4
+ JSON: 3_302_625_434,
6
5
  LE_ECDSA: 1_296_547_300,
7
- LE_UNSIGNED: 1_499_680_012
6
+ LE_UNSIGNED: 1_499_680_012,
7
+ SOLANA: 2_182_742_457
8
8
  };
9
9
  export var CustomSocketClosureCodes = /*#__PURE__*/ function(CustomSocketClosureCodes) {
10
10
  CustomSocketClosureCodes[CustomSocketClosureCodes["CLIENT_TIMEOUT_BUT_RECONNECTING"] = 4000] = "CLIENT_TIMEOUT_BUT_RECONNECTING";
@@ -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;
@@ -30,6 +30,7 @@ export class ResilientWebSocket {
30
30
  onError;
31
31
  onMessage;
32
32
  onReconnect;
33
+ onTimeout;
33
34
  constructor(config){
34
35
  this.endpoint = config.endpoint;
35
36
  this.wsOptions = config.wsOptions;
@@ -47,6 +48,9 @@ export class ResilientWebSocket {
47
48
  this.onReconnect = ()=>{
48
49
  // Empty function, can be set by the user.
49
50
  };
51
+ this.onTimeout = ()=>{
52
+ // Empty function, can be set by the user.
53
+ };
50
54
  }
51
55
  send(data) {
52
56
  this.logger.debug(`Sending message`);
@@ -114,6 +118,7 @@ export class ResilientWebSocket {
114
118
  this.heartbeatTimeout = setTimeout(()=>{
115
119
  const warnMsg = "Connection timed out. Reconnecting...";
116
120
  this.logger.warn(warnMsg);
121
+ this.onTimeout();
117
122
  if (this.wsClient) {
118
123
  if (typeof this.wsClient.terminate === "function") {
119
124
  this.wsClient.terminate();
@@ -1,10 +1,10 @@
1
+ import type WebSocket from "isomorphic-ws";
1
2
  import type { ErrorEvent } from "isomorphic-ws";
2
- import WebSocket from "isomorphic-ws";
3
3
  import type { Logger } from "ts-log";
4
+ import { IsomorphicEventEmitter } from "../emitter/index.js";
4
5
  import type { Request } from "../protocol.js";
5
6
  import type { ResilientWebSocketConfig } from "./resilient-websocket.js";
6
7
  import { ResilientWebSocket } from "./resilient-websocket.js";
7
- import { IsomorphicEventEmitter } from "../emitter/index.js";
8
8
  type WebSocketOnMessageCallback = (data: WebSocket.Data) => Promise<void>;
9
9
  export type WebSocketPoolConfig = {
10
10
  /**
@@ -51,8 +51,12 @@ export declare class WebSocketPool extends IsomorphicEventEmitter<WebSocketPoolE
51
51
  private subscriptions;
52
52
  private messageListeners;
53
53
  private allConnectionsDownListeners;
54
+ private connectionRestoredListeners;
55
+ private connectionTimeoutListeners;
56
+ private connectionReconnectListeners;
54
57
  private wasAllDown;
55
58
  private checkConnectionStatesInterval;
59
+ private isShutdown;
56
60
  private constructor();
57
61
  /**
58
62
  * Creates a new WebSocketPool instance that uses multiple redundant WebSocket connections for reliability.
@@ -62,7 +66,7 @@ export declare class WebSocketPool extends IsomorphicEventEmitter<WebSocketPoolE
62
66
  * @param numConnections - Number of parallel WebSocket connections to maintain (default: 3)
63
67
  * @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
64
68
  */
65
- static create(config: WebSocketPoolConfig, token: string, logger?: Logger): Promise<WebSocketPool>;
69
+ static create(config: WebSocketPoolConfig, token: string, abortSignal?: AbortSignal | null | undefined, logger?: Logger): Promise<WebSocketPool>;
66
70
  private emitPoolError;
67
71
  /**
68
72
  * Checks for error responses in JSON messages and throws appropriate errors
@@ -83,6 +87,20 @@ export declare class WebSocketPool extends IsomorphicEventEmitter<WebSocketPoolE
83
87
  * The connections may still try to reconnect in the background.
84
88
  */
85
89
  addAllConnectionsDownListener(handler: () => void): void;
90
+ /**
91
+ * Calls the handler when at least one connection is restored after all connections were down.
92
+ */
93
+ addConnectionRestoredListener(handler: () => void): void;
94
+ /**
95
+ * Calls the handler when an individual connection times out (heartbeat timeout).
96
+ * @param handler - Callback with connection index and endpoint URL
97
+ */
98
+ addConnectionTimeoutListener(handler: (connectionIndex: number, endpoint: string) => void): void;
99
+ /**
100
+ * Calls the handler when an individual connection reconnects after being down.
101
+ * @param handler - Callback with connection index and endpoint URL
102
+ */
103
+ addConnectionReconnectListener(handler: (connectionIndex: number, endpoint: string) => void): void;
86
104
  private areAllConnectionsDown;
87
105
  private isAnyConnectionEstablished;
88
106
  private checkConnectionStates;
@@ -1,9 +1,9 @@
1
1
  import TTLCache from "@isaacs/ttlcache";
2
2
  import { dummyLogger } from "ts-log";
3
- import { ResilientWebSocket } from "./resilient-websocket.mjs";
4
3
  import { DEFAULT_STREAM_SERVICE_0_URL, DEFAULT_STREAM_SERVICE_1_URL } from "../constants.mjs";
5
4
  import { IsomorphicEventEmitter } from "../emitter/index.mjs";
6
5
  import { addAuthTokenToWebSocketUrl, bufferFromWebsocketData, envIsBrowserOrWorker } from "../util/index.mjs";
6
+ import { ResilientWebSocket } from "./resilient-websocket.mjs";
7
7
  const DEFAULT_NUM_CONNECTIONS = 4;
8
8
  export class WebSocketPool extends IsomorphicEventEmitter {
9
9
  logger;
@@ -12,8 +12,12 @@ export class WebSocketPool extends IsomorphicEventEmitter {
12
12
  subscriptions;
13
13
  messageListeners;
14
14
  allConnectionsDownListeners;
15
+ connectionRestoredListeners;
16
+ connectionTimeoutListeners;
17
+ connectionReconnectListeners;
15
18
  wasAllDown = true;
16
19
  checkConnectionStatesInterval;
20
+ isShutdown = false;
17
21
  constructor(logger){
18
22
  super(), this.logger = logger;
19
23
  this.rwsPool = [];
@@ -23,6 +27,9 @@ export class WebSocketPool extends IsomorphicEventEmitter {
23
27
  this.subscriptions = new Map();
24
28
  this.messageListeners = [];
25
29
  this.allConnectionsDownListeners = [];
30
+ this.connectionRestoredListeners = [];
31
+ this.connectionTimeoutListeners = [];
32
+ this.connectionReconnectListeners = [];
26
33
  // Start monitoring connection states
27
34
  this.checkConnectionStatesInterval = setInterval(()=>{
28
35
  this.checkConnectionStates();
@@ -35,76 +42,103 @@ export class WebSocketPool extends IsomorphicEventEmitter {
35
42
  * @param token - Authentication token to use for the connections
36
43
  * @param numConnections - Number of parallel WebSocket connections to maintain (default: 3)
37
44
  * @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
38
- */ static async create(config, token, logger) {
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();
39
54
  const urls = config.urls ?? [
40
55
  DEFAULT_STREAM_SERVICE_0_URL,
41
56
  DEFAULT_STREAM_SERVICE_1_URL
42
57
  ];
43
58
  const log = logger ?? dummyLogger;
44
59
  const pool = new WebSocketPool(log);
45
- const numConnections = config.numConnections ?? DEFAULT_NUM_CONNECTIONS;
46
- // bind a handler to capture any emitted errors and send them to the user-provided
47
- // onWebSocketPoolError callback (if it is present)
48
- if (typeof config.onWebSocketPoolError === "function") {
49
- pool.on("error", config.onWebSocketPoolError);
50
- pool.once("shutdown", ()=>{
51
- // unbind all error handlers so we don't leak memory
52
- pool.off("error");
53
- });
54
- }
55
- for(let i = 0; i < numConnections; i++){
56
- const baseUrl = urls[i % urls.length];
57
- const isBrowser = envIsBrowserOrWorker();
58
- const url = isBrowser ? addAuthTokenToWebSocketUrl(baseUrl, token) : baseUrl;
59
- if (!url) {
60
- throw new Error(`URLs must not be null or empty`);
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
+ });
61
70
  }
62
- const wsOptions = {
63
- ...config.rwsConfig?.wsOptions,
64
- headers: isBrowser ? undefined : {
65
- Authorization: `Bearer ${token}`
66
- }
67
- };
68
- const rws = new ResilientWebSocket({
69
- ...config.rwsConfig,
70
- endpoint: url,
71
- wsOptions,
72
- logger: log
73
- });
74
- // If a websocket client unexpectedly disconnects, ResilientWebSocket will reestablish
75
- // the connection and call the onReconnect callback.
76
- rws.onReconnect = ()=>{
77
- if (rws.wsUserClosed) {
78
- return;
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`);
79
77
  }
80
- for (const [, request] of pool.subscriptions){
81
- try {
82
- rws.send(JSON.stringify(request));
83
- } catch (error) {
84
- 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}`
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);
85
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;
86
118
  }
87
- };
88
- // eslint-disable-next-line @typescript-eslint/no-deprecated
89
- const onErrorHandler = config.onWebSocketError ?? config.onError;
90
- if (typeof onErrorHandler === "function") {
91
- 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();
92
127
  }
93
- // Handle all client messages ourselves. Dedupe before sending to registered message handlers.
94
- rws.onMessage = (data)=>{
95
- pool.dedupeHandler(data).catch((error)=>{
96
- pool.emitPoolError(error, "Error in WebSocketPool dedupeHandler");
97
- });
98
- };
99
- pool.rwsPool.push(rws);
100
- rws.startWebSocket();
101
- }
102
- pool.logger.info(`Started WebSocketPool with ${numConnections.toString()} connections. Waiting for at least one to connect...`);
103
- while(!pool.isAnyConnectionEstablished()){
104
- await new Promise((resolve)=>setTimeout(resolve, 100));
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));
132
+ }
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;
105
141
  }
106
- pool.logger.info(`At least one WebSocket connection is established. WebSocketPool is ready.`);
107
- return pool;
108
142
  }
109
143
  emitPoolError(error, context) {
110
144
  const err = error instanceof Error ? error : new Error(String(error));
@@ -156,6 +190,10 @@ export class WebSocketPool extends IsomorphicEventEmitter {
156
190
  }
157
191
  };
158
192
  sendRequest(request) {
193
+ if (this.isShutdown) {
194
+ this.logger.warn("Cannot send request: WebSocketPool is shutdown");
195
+ return;
196
+ }
159
197
  for (const rws of this.rwsPool){
160
198
  try {
161
199
  rws.send(JSON.stringify(request));
@@ -165,6 +203,10 @@ export class WebSocketPool extends IsomorphicEventEmitter {
165
203
  }
166
204
  }
167
205
  addSubscription(request) {
206
+ if (this.isShutdown) {
207
+ this.logger.warn("Cannot add subscription: WebSocketPool is shutdown");
208
+ return;
209
+ }
168
210
  if (request.type !== "subscribe") {
169
211
  this.emitPoolError(new Error("Request must be a subscribe request"));
170
212
  return;
@@ -173,10 +215,14 @@ export class WebSocketPool extends IsomorphicEventEmitter {
173
215
  this.sendRequest(request);
174
216
  }
175
217
  removeSubscription(subscriptionId) {
218
+ if (this.isShutdown) {
219
+ this.logger.warn("Cannot remove subscription: WebSocketPool is shutdown");
220
+ return;
221
+ }
176
222
  this.subscriptions.delete(subscriptionId);
177
223
  const request = {
178
- type: "unsubscribe",
179
- subscriptionId
224
+ subscriptionId,
225
+ type: "unsubscribe"
180
226
  };
181
227
  this.sendRequest(request);
182
228
  }
@@ -189,6 +235,23 @@ export class WebSocketPool extends IsomorphicEventEmitter {
189
235
  */ addAllConnectionsDownListener(handler) {
190
236
  this.allConnectionsDownListeners.push(handler);
191
237
  }
238
+ /**
239
+ * Calls the handler when at least one connection is restored after all connections were down.
240
+ */ addConnectionRestoredListener(handler) {
241
+ this.connectionRestoredListeners.push(handler);
242
+ }
243
+ /**
244
+ * Calls the handler when an individual connection times out (heartbeat timeout).
245
+ * @param handler - Callback with connection index and endpoint URL
246
+ */ addConnectionTimeoutListener(handler) {
247
+ this.connectionTimeoutListeners.push(handler);
248
+ }
249
+ /**
250
+ * Calls the handler when an individual connection reconnects after being down.
251
+ * @param handler - Callback with connection index and endpoint URL
252
+ */ addConnectionReconnectListener(handler) {
253
+ this.connectionReconnectListeners.push(handler);
254
+ }
192
255
  areAllConnectionsDown() {
193
256
  return this.rwsPool.every((ws)=>!ws.isConnected() || ws.isReconnecting());
194
257
  }
@@ -196,6 +259,10 @@ export class WebSocketPool extends IsomorphicEventEmitter {
196
259
  return this.rwsPool.some((ws)=>ws.isConnected());
197
260
  }
198
261
  checkConnectionStates() {
262
+ // Stop monitoring if shutdown
263
+ if (this.isShutdown) {
264
+ return;
265
+ }
199
266
  const allDown = this.areAllConnectionsDown();
200
267
  // If all connections just went down
201
268
  if (allDown && !this.wasAllDown) {
@@ -210,12 +277,25 @@ export class WebSocketPool extends IsomorphicEventEmitter {
210
277
  }
211
278
  }
212
279
  }
213
- // If at least one connection was restored
280
+ // If at least one connection was restored after all were down
214
281
  if (!allDown && this.wasAllDown) {
215
282
  this.wasAllDown = false;
283
+ this.logger.info("At least one WebSocket connection restored");
284
+ for (const listener of this.connectionRestoredListeners){
285
+ try {
286
+ listener();
287
+ } catch (error) {
288
+ this.emitPoolError(error, "Connection-restored listener threw");
289
+ }
290
+ }
216
291
  }
217
292
  }
218
293
  shutdown() {
294
+ // Prevent multiple shutdown calls
295
+ if (this.isShutdown) {
296
+ return;
297
+ }
298
+ this.isShutdown = true;
219
299
  for (const rws of this.rwsPool){
220
300
  rws.closeWebSocket();
221
301
  }
@@ -223,6 +303,9 @@ export class WebSocketPool extends IsomorphicEventEmitter {
223
303
  this.subscriptions.clear();
224
304
  this.messageListeners = [];
225
305
  this.allConnectionsDownListeners = [];
306
+ this.connectionRestoredListeners = [];
307
+ this.connectionTimeoutListeners = [];
308
+ this.connectionReconnectListeners = [];
226
309
  clearInterval(this.checkConnectionStatesInterval);
227
310
  // execute all bound shutdown handlers
228
311
  for (const shutdownHandler of this.getListeners("shutdown")){