@pythnetwork/pyth-lazer-sdk 3.0.0 → 5.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.
package/README.md CHANGED
@@ -3,3 +3,60 @@
3
3
  ## Contributing & Development
4
4
 
5
5
  See [contributing.md](docs/contributing/contributing.md) for information on how to develop or contribute to this project!
6
+
7
+ ## How to use
8
+
9
+ ```javascript
10
+ import { PythLazerClient } from "@pythnetwork/pyth-lazer-sdk";
11
+
12
+ const c = await PythLazerClient.create({
13
+ token: "YOUR-AUTH-TOKEN-HERE",
14
+ logger: console, // Optionally log operations (to the console in this case.)
15
+ webSocketPoolConfig: {
16
+ numConnections: 4, // Optionally specify number of parallel redundant connections to reduce the chance of dropped messages. The connections will round-robin across the provided URLs. Default is 4.
17
+ onError: (error) => {
18
+ console.error("⛔️ WebSocket error:", error.message);
19
+ },
20
+ // Optional configuration for resilient WebSocket connections
21
+ rwsConfig: {
22
+ heartbeatTimeoutDurationMs: 5000, // Optional heartbeat timeout duration in milliseconds
23
+ maxRetryDelayMs: 1000, // Optional maximum retry delay in milliseconds
24
+ logAfterRetryCount: 10, // Optional log after how many retries
25
+ },
26
+ },
27
+ });
28
+
29
+ c.addMessageListener((message) => {
30
+ console.info("received the following from the Lazer stream:", message);
31
+ });
32
+
33
+ // Monitor for all connections in the pool being down simultaneously (e.g. if the internet goes down)
34
+ // The connections may still try to reconnect in the background. To shut down the client completely, call shutdown().
35
+ c.addAllConnectionsDownListener(() => {
36
+ console.error("All connections are down!");
37
+ });
38
+
39
+ // Create and remove one or more subscriptions on the fly
40
+ c.subscribe({
41
+ type: "subscribe",
42
+ subscriptionId: 1,
43
+ priceFeedIds: [1, 2],
44
+ properties: ["price"],
45
+ formats: ["solana"],
46
+ deliveryFormat: "binary",
47
+ channel: "fixed_rate@200ms",
48
+ parsed: false,
49
+ jsonBinaryEncoding: "base64",
50
+ });
51
+ c.subscribe({
52
+ type: "subscribe",
53
+ subscriptionId: 2,
54
+ priceFeedIds: [1, 2, 3, 4, 5],
55
+ properties: ["price", "exponent", "publisherCount", "confidence"],
56
+ formats: ["evm"],
57
+ deliveryFormat: "json",
58
+ channel: "fixed_rate@200ms",
59
+ parsed: true,
60
+ jsonBinaryEncoding: "hex",
61
+ });
62
+ ```
@@ -70,17 +70,17 @@ export declare class PythLazerClient {
70
70
  * @param params - Optional query parameters to filter symbols
71
71
  * @returns Promise resolving to array of symbol information
72
72
  */
73
- get_symbols(params?: SymbolsQueryParams): Promise<SymbolResponse[]>;
73
+ getSymbols(params?: SymbolsQueryParams): Promise<SymbolResponse[]>;
74
74
  /**
75
75
  * Queries the latest price endpoint to get current price data.
76
76
  * @param params - Parameters for the latest price request
77
77
  * @returns Promise resolving to JsonUpdate with current price data
78
78
  */
79
- get_latest_price(params: LatestPriceRequest): Promise<JsonUpdate>;
79
+ getLatestPrice(params: LatestPriceRequest): Promise<JsonUpdate>;
80
80
  /**
81
81
  * Queries the price endpoint to get historical price data at a specific timestamp.
82
82
  * @param params - Parameters for the price request including timestamp
83
83
  * @returns Promise resolving to JsonUpdate with price data at the specified time
84
84
  */
85
- get_price(params: PriceRequest): Promise<JsonUpdate>;
85
+ getPrice(params: PriceRequest): Promise<JsonUpdate>;
86
86
  }
@@ -1,14 +1,11 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
3
  exports.PythLazerClient = void 0;
7
- const cross_fetch_1 = __importDefault(require("cross-fetch"));
8
4
  const ts_log_1 = require("ts-log");
9
5
  const constants_js_1 = require("./constants.js");
10
6
  const protocol_js_1 = require("./protocol.js");
11
7
  const websocket_pool_js_1 = require("./socket/websocket-pool.js");
8
+ const buffer_util_js_1 = require("./util/buffer-util.js");
12
9
  const UINT16_NUM_BYTES = 2;
13
10
  const UINT32_NUM_BYTES = 4;
14
11
  const UINT64_NUM_BYTES = 8;
@@ -61,55 +58,56 @@ class PythLazerClient {
61
58
  */
62
59
  addMessageListener(handler) {
63
60
  const wsp = this.getWebSocketPool();
64
- wsp.addMessageListener((data) => {
61
+ wsp.addMessageListener(async (data) => {
65
62
  if (typeof data == "string") {
66
63
  handler({
67
64
  type: "json",
68
65
  value: JSON.parse(data),
69
66
  });
67
+ return;
70
68
  }
71
- else if (Buffer.isBuffer(data)) {
72
- let pos = 0;
73
- const magic = data.subarray(pos, pos + UINT32_NUM_BYTES).readUint32LE();
74
- pos += UINT32_NUM_BYTES;
75
- if (magic != protocol_js_1.BINARY_UPDATE_FORMAT_MAGIC_LE) {
76
- throw new Error("binary update format magic mismatch");
69
+ const buffData = await (0, buffer_util_js_1.bufferFromWebsocketData)(data);
70
+ let pos = 0;
71
+ const magic = buffData
72
+ .subarray(pos, pos + UINT32_NUM_BYTES)
73
+ .readUint32LE();
74
+ pos += UINT32_NUM_BYTES;
75
+ if (magic != protocol_js_1.BINARY_UPDATE_FORMAT_MAGIC_LE) {
76
+ throw new Error("binary update format magic mismatch");
77
+ }
78
+ // TODO: some uint64 values may not be representable as Number.
79
+ const subscriptionId = Number(buffData.subarray(pos, pos + UINT64_NUM_BYTES).readBigInt64BE());
80
+ pos += UINT64_NUM_BYTES;
81
+ const value = { subscriptionId };
82
+ while (pos < buffData.length) {
83
+ const len = buffData
84
+ .subarray(pos, pos + UINT16_NUM_BYTES)
85
+ .readUint16BE();
86
+ pos += UINT16_NUM_BYTES;
87
+ const magic = buffData
88
+ .subarray(pos, pos + UINT32_NUM_BYTES)
89
+ .readUint32LE();
90
+ if (magic == protocol_js_1.FORMAT_MAGICS_LE.EVM) {
91
+ value.evm = buffData.subarray(pos, pos + len);
77
92
  }
78
- // TODO: some uint64 values may not be representable as Number.
79
- const subscriptionId = Number(data.subarray(pos, pos + UINT64_NUM_BYTES).readBigInt64BE());
80
- pos += UINT64_NUM_BYTES;
81
- const value = { subscriptionId };
82
- while (pos < data.length) {
83
- const len = data.subarray(pos, pos + UINT16_NUM_BYTES).readUint16BE();
84
- pos += UINT16_NUM_BYTES;
85
- const magic = data
86
- .subarray(pos, pos + UINT32_NUM_BYTES)
87
- .readUint32LE();
88
- if (magic == protocol_js_1.FORMAT_MAGICS_LE.EVM) {
89
- value.evm = data.subarray(pos, pos + len);
90
- }
91
- else if (magic == protocol_js_1.FORMAT_MAGICS_LE.SOLANA) {
92
- value.solana = data.subarray(pos, pos + len);
93
- }
94
- else if (magic == protocol_js_1.FORMAT_MAGICS_LE.LE_ECDSA) {
95
- value.leEcdsa = data.subarray(pos, pos + len);
96
- }
97
- else if (magic == protocol_js_1.FORMAT_MAGICS_LE.LE_UNSIGNED) {
98
- value.leUnsigned = data.subarray(pos, pos + len);
99
- }
100
- else if (magic == protocol_js_1.FORMAT_MAGICS_LE.JSON) {
101
- value.parsed = JSON.parse(data.subarray(pos + UINT32_NUM_BYTES, pos + len).toString());
102
- }
103
- else {
104
- throw new Error("unknown magic: " + magic.toString());
105
- }
106
- pos += len;
93
+ else if (magic == protocol_js_1.FORMAT_MAGICS_LE.SOLANA) {
94
+ value.solana = buffData.subarray(pos, pos + len);
107
95
  }
108
- handler({ type: "binary", value });
109
- }
110
- else {
111
- throw new TypeError("unexpected event data type");
96
+ else if (magic == protocol_js_1.FORMAT_MAGICS_LE.LE_ECDSA) {
97
+ value.leEcdsa = buffData.subarray(pos, pos + len);
98
+ }
99
+ else if (magic == protocol_js_1.FORMAT_MAGICS_LE.LE_UNSIGNED) {
100
+ value.leUnsigned = buffData.subarray(pos, pos + len);
101
+ }
102
+ else if (magic == protocol_js_1.FORMAT_MAGICS_LE.JSON) {
103
+ value.parsed = JSON.parse(buffData.subarray(pos + UINT32_NUM_BYTES, pos + len).toString());
104
+ }
105
+ else {
106
+ throw new Error(`unknown magic: ${magic.toString()}`);
107
+ }
108
+ pos += len;
112
109
  }
110
+ handler({ type: "binary", value });
113
111
  });
114
112
  }
115
113
  subscribe(request) {
@@ -151,7 +149,7 @@ class PythLazerClient {
151
149
  Authorization: `Bearer ${this.token}`,
152
150
  ...options.headers,
153
151
  };
154
- return (0, cross_fetch_1.default)(url, {
152
+ return fetch(url, {
155
153
  ...options,
156
154
  headers,
157
155
  });
@@ -161,7 +159,7 @@ class PythLazerClient {
161
159
  * @param params - Optional query parameters to filter symbols
162
160
  * @returns Promise resolving to array of symbol information
163
161
  */
164
- async get_symbols(params) {
162
+ async getSymbols(params) {
165
163
  const url = new URL(`${this.metadataServiceUrl}/v1/symbols`);
166
164
  if (params?.query) {
167
165
  url.searchParams.set("query", params.query);
@@ -185,11 +183,11 @@ class PythLazerClient {
185
183
  * @param params - Parameters for the latest price request
186
184
  * @returns Promise resolving to JsonUpdate with current price data
187
185
  */
188
- async get_latest_price(params) {
186
+ async getLatestPrice(params) {
189
187
  const url = `${this.priceServiceUrl}/v1/latest_price`;
190
188
  try {
191
189
  const body = JSON.stringify(params);
192
- this.logger.debug("get_latest_price", { url, body });
190
+ this.logger.debug("getLatestPrice", { url, body });
193
191
  const response = await this.authenticatedFetch(url, {
194
192
  method: "POST",
195
193
  headers: {
@@ -211,11 +209,11 @@ class PythLazerClient {
211
209
  * @param params - Parameters for the price request including timestamp
212
210
  * @returns Promise resolving to JsonUpdate with price data at the specified time
213
211
  */
214
- async get_price(params) {
212
+ async getPrice(params) {
215
213
  const url = `${this.priceServiceUrl}/v1/price`;
216
214
  try {
217
215
  const body = JSON.stringify(params);
218
- this.logger.debug("get_price", { url, body });
216
+ this.logger.debug("getPrice", { url, body });
219
217
  const response = await this.authenticatedFetch(url, {
220
218
  method: "POST",
221
219
  headers: {
@@ -37,6 +37,7 @@ export type JsonBinaryData = {
37
37
  };
38
38
  export type InvalidFeedSubscriptionDetails = {
39
39
  unknownIds: number[];
40
+ unknownSymbols: string[];
40
41
  unsupportedChannels: number[];
41
42
  unstable: number[];
42
43
  };
@@ -121,3 +122,6 @@ export type JsonUpdate = {
121
122
  leEcdsa?: JsonBinaryData;
122
123
  leUnsigned?: JsonBinaryData;
123
124
  };
125
+ export declare enum CustomSocketClosureCodes {
126
+ CLIENT_TIMEOUT_BUT_RECONNECTING = 4000
127
+ }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.FORMAT_MAGICS_LE = exports.BINARY_UPDATE_FORMAT_MAGIC_LE = void 0;
3
+ exports.CustomSocketClosureCodes = exports.FORMAT_MAGICS_LE = exports.BINARY_UPDATE_FORMAT_MAGIC_LE = void 0;
4
4
  exports.BINARY_UPDATE_FORMAT_MAGIC_LE = 461_928_307;
5
5
  exports.FORMAT_MAGICS_LE = {
6
6
  JSON: 3_302_625_434,
@@ -9,3 +9,7 @@ exports.FORMAT_MAGICS_LE = {
9
9
  LE_ECDSA: 1_296_547_300,
10
10
  LE_UNSIGNED: 1_499_680_012,
11
11
  };
12
+ var CustomSocketClosureCodes;
13
+ (function (CustomSocketClosureCodes) {
14
+ CustomSocketClosureCodes[CustomSocketClosureCodes["CLIENT_TIMEOUT_BUT_RECONNECTING"] = 4000] = "CLIENT_TIMEOUT_BUT_RECONNECTING";
15
+ })(CustomSocketClosureCodes || (exports.CustomSocketClosureCodes = CustomSocketClosureCodes = {}));
@@ -10,6 +10,19 @@ export type ResilientWebSocketConfig = {
10
10
  maxRetryDelayMs?: number;
11
11
  logAfterRetryCount?: number;
12
12
  };
13
+ /**
14
+ * the isomorphic-ws package ships with some slightly-erroneous typings.
15
+ * namely, it returns a WebSocket with typings that indicate the "terminate()" function
16
+ * is available on all platforms.
17
+ * Given that, under the hood, it is using the globalThis.WebSocket class, if it's available,
18
+ * and falling back to using the https://www.npmjs.com/package/ws package, this
19
+ * means there are API differences between the native WebSocket (the one in a web browser)
20
+ * and the server-side version from the "ws" package.
21
+ *
22
+ * This type creates a WebSocket type reference we use to indicate the unknown
23
+ * nature of the env in which is code is run.
24
+ */
25
+ type UnsafeWebSocket = Omit<WebSocket, "terminate"> & Partial<Pick<WebSocket, "terminate">>;
13
26
  export declare class ResilientWebSocket {
14
27
  private endpoint;
15
28
  private wsOptions?;
@@ -17,7 +30,7 @@ export declare class ResilientWebSocket {
17
30
  private heartbeatTimeoutDurationMs;
18
31
  private maxRetryDelayMs;
19
32
  private logAfterRetryCount;
20
- wsClient: undefined | WebSocket;
33
+ wsClient: UnsafeWebSocket | undefined;
21
34
  wsUserClosed: boolean;
22
35
  private wsFailedAttempts;
23
36
  private heartbeatTimeout?;
@@ -47,3 +60,4 @@ export declare class ResilientWebSocket {
47
60
  */
48
61
  private retryDelayMs;
49
62
  }
63
+ export {};
@@ -6,6 +6,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.ResilientWebSocket = void 0;
7
7
  const isomorphic_ws_1 = __importDefault(require("isomorphic-ws"));
8
8
  const ts_log_1 = require("ts-log");
9
+ const protocol_js_1 = require("../protocol.js");
10
+ const env_util_js_1 = require("../util/env-util.js");
9
11
  const DEFAULT_HEARTBEAT_TIMEOUT_DURATION_MS = 5000; // 5 seconds
10
12
  const DEFAULT_MAX_RETRY_DELAY_MS = 1000; // 1 second'
11
13
  const DEFAULT_LOG_AFTER_RETRY_COUNT = 10;
@@ -80,7 +82,10 @@ class ResilientWebSocket {
80
82
  clearTimeout(this.retryTimeout);
81
83
  this.retryTimeout = undefined;
82
84
  }
83
- this.wsClient = new isomorphic_ws_1.default(this.endpoint, this.wsOptions);
85
+ // browser constructor supports a different 2nd argument for the constructor,
86
+ // so we need to ensure it's not included if we're running in that environment:
87
+ // https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket#protocols
88
+ this.wsClient = new isomorphic_ws_1.default(this.endpoint, (0, env_util_js_1.envIsBrowserOrWorker)() ? undefined : this.wsOptions);
84
89
  this.wsClient.addEventListener("open", () => {
85
90
  this.logger.info("WebSocket connection established");
86
91
  this.wsFailedAttempts = 0;
@@ -118,8 +123,19 @@ class ResilientWebSocket {
118
123
  clearTimeout(this.heartbeatTimeout);
119
124
  }
120
125
  this.heartbeatTimeout = setTimeout(() => {
121
- this.logger.warn("Connection timed out. Reconnecting...");
122
- this.wsClient?.terminate();
126
+ const warnMsg = "Connection timed out. Reconnecting...";
127
+ this.logger.warn(warnMsg);
128
+ if (this.wsClient) {
129
+ if (typeof this.wsClient.terminate === "function") {
130
+ this.wsClient.terminate();
131
+ }
132
+ else {
133
+ // terminate is an implementation detail of the node-friendly
134
+ // https://www.npmjs.com/package/ws package, but is not a native WebSocket API,
135
+ // so we have to use the close method
136
+ this.wsClient.close(protocol_js_1.CustomSocketClosureCodes.CLIENT_TIMEOUT_BUT_RECONNECTING, warnMsg);
137
+ }
138
+ }
123
139
  this.handleReconnect();
124
140
  }, this.heartbeatTimeoutDurationMs);
125
141
  }
@@ -4,6 +4,7 @@ import type { Logger } from "ts-log";
4
4
  import type { Request } from "../protocol.js";
5
5
  import type { ResilientWebSocketConfig } from "./resilient-websocket.js";
6
6
  import { ResilientWebSocket } from "./resilient-websocket.js";
7
+ type WebSocketOnMessageCallback = (data: WebSocket.Data) => void | Promise<void>;
7
8
  export type WebSocketPoolConfig = {
8
9
  urls?: string[];
9
10
  numConnections?: number;
@@ -33,15 +34,16 @@ export declare class WebSocketPool {
33
34
  * Checks for error responses in JSON messages and throws appropriate errors
34
35
  */
35
36
  private handleErrorMessages;
37
+ private constructCacheKeyFromWebsocketData;
36
38
  /**
37
39
  * Handles incoming websocket messages by deduplicating identical messages received across
38
40
  * multiple connections before forwarding to registered handlers
39
41
  */
40
- dedupeHandler: (data: WebSocket.Data) => void;
42
+ dedupeHandler: (data: WebSocket.Data) => Promise<void>;
41
43
  sendRequest(request: Request): void;
42
44
  addSubscription(request: Request): void;
43
45
  removeSubscription(subscriptionId: number): void;
44
- addMessageListener(handler: (data: WebSocket.Data) => void): void;
46
+ addMessageListener(handler: WebSocketOnMessageCallback): void;
45
47
  /**
46
48
  * Calls the handler if all websocket connections are currently down or in reconnecting state.
47
49
  * The connections may still try to reconnect in the background.
@@ -52,3 +54,4 @@ export declare class WebSocketPool {
52
54
  private checkConnectionStates;
53
55
  shutdown(): void;
54
56
  }
57
+ export {};
@@ -8,6 +8,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
10
  const constants_js_1 = require("../constants.js");
11
+ const index_js_1 = require("../util/index.js");
11
12
  const DEFAULT_NUM_CONNECTIONS = 4;
12
13
  class WebSocketPool {
13
14
  logger;
@@ -47,15 +48,17 @@ class WebSocketPool {
47
48
  const pool = new WebSocketPool(log);
48
49
  const numConnections = config.numConnections ?? DEFAULT_NUM_CONNECTIONS;
49
50
  for (let i = 0; i < numConnections; i++) {
50
- const url = urls[i % urls.length];
51
+ const baseUrl = urls[i % urls.length];
52
+ const isBrowser = (0, index_js_1.envIsBrowserOrWorker)();
53
+ const url = isBrowser
54
+ ? (0, index_js_1.addAuthTokenToWebSocketUrl)(baseUrl, token)
55
+ : baseUrl;
51
56
  if (!url) {
52
57
  throw new Error(`URLs must not be null or empty`);
53
58
  }
54
59
  const wsOptions = {
55
60
  ...config.rwsConfig?.wsOptions,
56
- headers: {
57
- Authorization: `Bearer ${token}`,
58
- },
61
+ headers: isBrowser ? undefined : { Authorization: `Bearer ${token}` },
59
62
  };
60
63
  const rws = new resilient_websocket_js_1.ResilientWebSocket({
61
64
  ...config.rwsConfig,
@@ -82,7 +85,12 @@ class WebSocketPool {
82
85
  rws.onError = config.onError;
83
86
  }
84
87
  // Handle all client messages ourselves. Dedupe before sending to registered message handlers.
85
- rws.onMessage = pool.dedupeHandler;
88
+ rws.onMessage = (data) => {
89
+ pool.dedupeHandler(data).catch((error) => {
90
+ const errMsg = `An error occurred in the WebSocket pool's dedupeHandler: ${error instanceof Error ? error.message : String(error)}`;
91
+ throw new Error(errMsg);
92
+ });
93
+ };
86
94
  pool.rwsPool.push(rws);
87
95
  rws.startWebSocket();
88
96
  }
@@ -105,14 +113,18 @@ class WebSocketPool {
105
113
  throw new Error(`Error: ${message.error}`);
106
114
  }
107
115
  }
116
+ async constructCacheKeyFromWebsocketData(data) {
117
+ if (typeof data === "string")
118
+ return data;
119
+ const buff = await (0, index_js_1.bufferFromWebsocketData)(data);
120
+ return buff.toString("hex");
121
+ }
108
122
  /**
109
123
  * Handles incoming websocket messages by deduplicating identical messages received across
110
124
  * multiple connections before forwarding to registered handlers
111
125
  */
112
- dedupeHandler = (data) => {
113
- const cacheKey = typeof data === "string"
114
- ? data
115
- : Buffer.from(data).toString("hex");
126
+ dedupeHandler = async (data) => {
127
+ const cacheKey = await this.constructCacheKeyFromWebsocketData(data);
116
128
  if (this.cache.has(cacheKey)) {
117
129
  this.logger.debug("Dropping duplicate message");
118
130
  return;
@@ -121,9 +133,7 @@ class WebSocketPool {
121
133
  if (typeof data === "string") {
122
134
  this.handleErrorMessages(data);
123
135
  }
124
- for (const handler of this.messageListeners) {
125
- handler(data);
126
- }
136
+ await Promise.all(this.messageListeners.map((handler) => handler(data)));
127
137
  };
128
138
  sendRequest(request) {
129
139
  for (const rws of this.rwsPool) {
@@ -0,0 +1,7 @@
1
+ import type { Data } from "isomorphic-ws";
2
+ /**
3
+ * given a relatively unknown websocket frame data object,
4
+ * returns a valid Buffer instance that is safe to use
5
+ * isomorphically in any JS runtime environment
6
+ */
7
+ export declare function bufferFromWebsocketData(data: Data): Promise<Buffer>;
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.bufferFromWebsocketData = bufferFromWebsocketData;
4
+ // the linting rules don't allow importing anything that might clash with
5
+ // a global, top-level import. we disable this rule because we need this
6
+ // imported from our installed dependency
7
+ // eslint-disable-next-line unicorn/prefer-node-protocol
8
+ const buffer_1 = require("buffer");
9
+ const BufferClassToUse = "Buffer" in globalThis ? globalThis.Buffer : buffer_1.Buffer;
10
+ /**
11
+ * given a relatively unknown websocket frame data object,
12
+ * returns a valid Buffer instance that is safe to use
13
+ * isomorphically in any JS runtime environment
14
+ */
15
+ async function bufferFromWebsocketData(data) {
16
+ if (typeof data === "string") {
17
+ return BufferClassToUse.from(new TextEncoder().encode(data).buffer);
18
+ }
19
+ if (data instanceof BufferClassToUse)
20
+ return data;
21
+ if (data instanceof Blob) {
22
+ // let the uncaught promise exception bubble up if there's an issue
23
+ return BufferClassToUse.from(await data.arrayBuffer());
24
+ }
25
+ if (data instanceof ArrayBuffer)
26
+ return BufferClassToUse.from(data);
27
+ if (Array.isArray(data)) {
28
+ // an array of buffers is highly unlikely, but it is a possibility
29
+ // indicated by the WebSocket Data interface
30
+ return BufferClassToUse.concat(data);
31
+ }
32
+ return data;
33
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Detects if this code is running within any Service or WebWorker context.
3
+ * @returns true if in a worker of some kind, false if otherwise
4
+ */
5
+ export declare function envIsServiceOrWebWorker(): boolean;
6
+ /**
7
+ * Detects if the code is running in a regular DOM or Web Worker context.
8
+ * @returns true if running in a DOM or Web Worker context, false if running in Node.js
9
+ */
10
+ export declare function envIsBrowser(): boolean;
11
+ /**
12
+ * a convenience method that returns whether or not
13
+ * this code is executing in some type of browser-centric environment
14
+ *
15
+ * @returns true if in the browser's main UI thread or in a worker, false if otherwise
16
+ */
17
+ export declare function envIsBrowserOrWorker(): boolean;
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.envIsServiceOrWebWorker = envIsServiceOrWebWorker;
4
+ exports.envIsBrowser = envIsBrowser;
5
+ exports.envIsBrowserOrWorker = envIsBrowserOrWorker;
6
+ // we create this local-only type, which has assertions made to indicate
7
+ // that we do not know and cannot guarantee which JS environment we are in
8
+ const g = globalThis;
9
+ /**
10
+ * Detects if this code is running within any Service or WebWorker context.
11
+ * @returns true if in a worker of some kind, false if otherwise
12
+ */
13
+ function envIsServiceOrWebWorker() {
14
+ return (typeof WorkerGlobalScope !== "undefined" &&
15
+ g.self instanceof WorkerGlobalScope);
16
+ }
17
+ /**
18
+ * Detects if the code is running in a regular DOM or Web Worker context.
19
+ * @returns true if running in a DOM or Web Worker context, false if running in Node.js
20
+ */
21
+ function envIsBrowser() {
22
+ return g.window !== undefined;
23
+ }
24
+ /**
25
+ * a convenience method that returns whether or not
26
+ * this code is executing in some type of browser-centric environment
27
+ *
28
+ * @returns true if in the browser's main UI thread or in a worker, false if otherwise
29
+ */
30
+ function envIsBrowserOrWorker() {
31
+ return envIsServiceOrWebWorker() || envIsBrowser();
32
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./buffer-util.js";
2
+ export * from "./env-util.js";
3
+ export * from "./url-util.js";
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./buffer-util.js"), exports);
18
+ __exportStar(require("./env-util.js"), exports);
19
+ __exportStar(require("./url-util.js"), exports);
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Given a URL to a hosted lazer stream service and a possible auth token,
3
+ * appends the auth token as a query parameter and returns the URL with the token
4
+ * contained within.
5
+ * If the URL provided is nullish, it is returned as-is (in the same nullish format).
6
+ * If the token is nullish, the baseUrl given is returned, instead.
7
+ */
8
+ export declare function addAuthTokenToWebSocketUrl(baseUrl: string | null | undefined, authToken: string | null | undefined): string | null | undefined;
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.addAuthTokenToWebSocketUrl = addAuthTokenToWebSocketUrl;
4
+ const ACCESS_TOKEN_QUERY_PARAM_KEY = "ACCESS_TOKEN";
5
+ /**
6
+ * Given a URL to a hosted lazer stream service and a possible auth token,
7
+ * appends the auth token as a query parameter and returns the URL with the token
8
+ * contained within.
9
+ * If the URL provided is nullish, it is returned as-is (in the same nullish format).
10
+ * If the token is nullish, the baseUrl given is returned, instead.
11
+ */
12
+ function addAuthTokenToWebSocketUrl(baseUrl, authToken) {
13
+ if (!baseUrl || !authToken)
14
+ return baseUrl;
15
+ const parsedUrl = new URL(baseUrl);
16
+ parsedUrl.searchParams.set(ACCESS_TOKEN_QUERY_PARAM_KEY, authToken);
17
+ return parsedUrl.toString();
18
+ }
@@ -70,17 +70,17 @@ export declare class PythLazerClient {
70
70
  * @param params - Optional query parameters to filter symbols
71
71
  * @returns Promise resolving to array of symbol information
72
72
  */
73
- get_symbols(params?: SymbolsQueryParams): Promise<SymbolResponse[]>;
73
+ getSymbols(params?: SymbolsQueryParams): Promise<SymbolResponse[]>;
74
74
  /**
75
75
  * Queries the latest price endpoint to get current price data.
76
76
  * @param params - Parameters for the latest price request
77
77
  * @returns Promise resolving to JsonUpdate with current price data
78
78
  */
79
- get_latest_price(params: LatestPriceRequest): Promise<JsonUpdate>;
79
+ getLatestPrice(params: LatestPriceRequest): Promise<JsonUpdate>;
80
80
  /**
81
81
  * Queries the price endpoint to get historical price data at a specific timestamp.
82
82
  * @param params - Parameters for the price request including timestamp
83
83
  * @returns Promise resolving to JsonUpdate with price data at the specified time
84
84
  */
85
- get_price(params: PriceRequest): Promise<JsonUpdate>;
85
+ getPrice(params: PriceRequest): Promise<JsonUpdate>;
86
86
  }
@@ -1,9 +1,9 @@
1
- import fetch from "cross-fetch";
2
1
  import WebSocket from "isomorphic-ws";
3
2
  import { dummyLogger } from "ts-log";
4
3
  import { DEFAULT_METADATA_SERVICE_URL, DEFAULT_PRICE_SERVICE_URL, } from "./constants.js";
5
4
  import { BINARY_UPDATE_FORMAT_MAGIC_LE, FORMAT_MAGICS_LE } from "./protocol.js";
6
5
  import { WebSocketPool } from "./socket/websocket-pool.js";
6
+ import { bufferFromWebsocketData } from "./util/buffer-util.js";
7
7
  const UINT16_NUM_BYTES = 2;
8
8
  const UINT32_NUM_BYTES = 4;
9
9
  const UINT64_NUM_BYTES = 8;
@@ -56,55 +56,56 @@ export class PythLazerClient {
56
56
  */
57
57
  addMessageListener(handler) {
58
58
  const wsp = this.getWebSocketPool();
59
- wsp.addMessageListener((data) => {
59
+ wsp.addMessageListener(async (data) => {
60
60
  if (typeof data == "string") {
61
61
  handler({
62
62
  type: "json",
63
63
  value: JSON.parse(data),
64
64
  });
65
+ return;
65
66
  }
66
- else if (Buffer.isBuffer(data)) {
67
- let pos = 0;
68
- const magic = data.subarray(pos, pos + UINT32_NUM_BYTES).readUint32LE();
69
- pos += UINT32_NUM_BYTES;
70
- if (magic != BINARY_UPDATE_FORMAT_MAGIC_LE) {
71
- throw new Error("binary update format magic mismatch");
67
+ const buffData = await bufferFromWebsocketData(data);
68
+ let pos = 0;
69
+ const magic = buffData
70
+ .subarray(pos, pos + UINT32_NUM_BYTES)
71
+ .readUint32LE();
72
+ pos += UINT32_NUM_BYTES;
73
+ if (magic != BINARY_UPDATE_FORMAT_MAGIC_LE) {
74
+ throw new Error("binary update format magic mismatch");
75
+ }
76
+ // TODO: some uint64 values may not be representable as Number.
77
+ const subscriptionId = Number(buffData.subarray(pos, pos + UINT64_NUM_BYTES).readBigInt64BE());
78
+ pos += UINT64_NUM_BYTES;
79
+ const value = { subscriptionId };
80
+ while (pos < buffData.length) {
81
+ const len = buffData
82
+ .subarray(pos, pos + UINT16_NUM_BYTES)
83
+ .readUint16BE();
84
+ pos += UINT16_NUM_BYTES;
85
+ const magic = buffData
86
+ .subarray(pos, pos + UINT32_NUM_BYTES)
87
+ .readUint32LE();
88
+ if (magic == FORMAT_MAGICS_LE.EVM) {
89
+ value.evm = buffData.subarray(pos, pos + len);
72
90
  }
73
- // TODO: some uint64 values may not be representable as Number.
74
- const subscriptionId = Number(data.subarray(pos, pos + UINT64_NUM_BYTES).readBigInt64BE());
75
- pos += UINT64_NUM_BYTES;
76
- const value = { subscriptionId };
77
- while (pos < data.length) {
78
- const len = data.subarray(pos, pos + UINT16_NUM_BYTES).readUint16BE();
79
- pos += UINT16_NUM_BYTES;
80
- const magic = data
81
- .subarray(pos, pos + UINT32_NUM_BYTES)
82
- .readUint32LE();
83
- if (magic == FORMAT_MAGICS_LE.EVM) {
84
- value.evm = data.subarray(pos, pos + len);
85
- }
86
- else if (magic == FORMAT_MAGICS_LE.SOLANA) {
87
- value.solana = data.subarray(pos, pos + len);
88
- }
89
- else if (magic == FORMAT_MAGICS_LE.LE_ECDSA) {
90
- value.leEcdsa = data.subarray(pos, pos + len);
91
- }
92
- else if (magic == FORMAT_MAGICS_LE.LE_UNSIGNED) {
93
- value.leUnsigned = data.subarray(pos, pos + len);
94
- }
95
- else if (magic == FORMAT_MAGICS_LE.JSON) {
96
- value.parsed = JSON.parse(data.subarray(pos + UINT32_NUM_BYTES, pos + len).toString());
97
- }
98
- else {
99
- throw new Error("unknown magic: " + magic.toString());
100
- }
101
- pos += len;
91
+ else if (magic == FORMAT_MAGICS_LE.SOLANA) {
92
+ value.solana = buffData.subarray(pos, pos + len);
102
93
  }
103
- handler({ type: "binary", value });
104
- }
105
- else {
106
- throw new TypeError("unexpected event data type");
94
+ else if (magic == FORMAT_MAGICS_LE.LE_ECDSA) {
95
+ value.leEcdsa = buffData.subarray(pos, pos + len);
96
+ }
97
+ else if (magic == FORMAT_MAGICS_LE.LE_UNSIGNED) {
98
+ value.leUnsigned = buffData.subarray(pos, pos + len);
99
+ }
100
+ else if (magic == FORMAT_MAGICS_LE.JSON) {
101
+ value.parsed = JSON.parse(buffData.subarray(pos + UINT32_NUM_BYTES, pos + len).toString());
102
+ }
103
+ else {
104
+ throw new Error(`unknown magic: ${magic.toString()}`);
105
+ }
106
+ pos += len;
107
107
  }
108
+ handler({ type: "binary", value });
108
109
  });
109
110
  }
110
111
  subscribe(request) {
@@ -156,7 +157,7 @@ export class PythLazerClient {
156
157
  * @param params - Optional query parameters to filter symbols
157
158
  * @returns Promise resolving to array of symbol information
158
159
  */
159
- async get_symbols(params) {
160
+ async getSymbols(params) {
160
161
  const url = new URL(`${this.metadataServiceUrl}/v1/symbols`);
161
162
  if (params?.query) {
162
163
  url.searchParams.set("query", params.query);
@@ -180,11 +181,11 @@ export class PythLazerClient {
180
181
  * @param params - Parameters for the latest price request
181
182
  * @returns Promise resolving to JsonUpdate with current price data
182
183
  */
183
- async get_latest_price(params) {
184
+ async getLatestPrice(params) {
184
185
  const url = `${this.priceServiceUrl}/v1/latest_price`;
185
186
  try {
186
187
  const body = JSON.stringify(params);
187
- this.logger.debug("get_latest_price", { url, body });
188
+ this.logger.debug("getLatestPrice", { url, body });
188
189
  const response = await this.authenticatedFetch(url, {
189
190
  method: "POST",
190
191
  headers: {
@@ -206,11 +207,11 @@ export class PythLazerClient {
206
207
  * @param params - Parameters for the price request including timestamp
207
208
  * @returns Promise resolving to JsonUpdate with price data at the specified time
208
209
  */
209
- async get_price(params) {
210
+ async getPrice(params) {
210
211
  const url = `${this.priceServiceUrl}/v1/price`;
211
212
  try {
212
213
  const body = JSON.stringify(params);
213
- this.logger.debug("get_price", { url, body });
214
+ this.logger.debug("getPrice", { url, body });
214
215
  const response = await this.authenticatedFetch(url, {
215
216
  method: "POST",
216
217
  headers: {
@@ -37,6 +37,7 @@ export type JsonBinaryData = {
37
37
  };
38
38
  export type InvalidFeedSubscriptionDetails = {
39
39
  unknownIds: number[];
40
+ unknownSymbols: string[];
40
41
  unsupportedChannels: number[];
41
42
  unstable: number[];
42
43
  };
@@ -121,3 +122,6 @@ export type JsonUpdate = {
121
122
  leEcdsa?: JsonBinaryData;
122
123
  leUnsigned?: JsonBinaryData;
123
124
  };
125
+ export declare enum CustomSocketClosureCodes {
126
+ CLIENT_TIMEOUT_BUT_RECONNECTING = 4000
127
+ }
@@ -6,3 +6,7 @@ export const FORMAT_MAGICS_LE = {
6
6
  LE_ECDSA: 1_296_547_300,
7
7
  LE_UNSIGNED: 1_499_680_012,
8
8
  };
9
+ export var CustomSocketClosureCodes;
10
+ (function (CustomSocketClosureCodes) {
11
+ CustomSocketClosureCodes[CustomSocketClosureCodes["CLIENT_TIMEOUT_BUT_RECONNECTING"] = 4000] = "CLIENT_TIMEOUT_BUT_RECONNECTING";
12
+ })(CustomSocketClosureCodes || (CustomSocketClosureCodes = {}));
@@ -10,6 +10,19 @@ export type ResilientWebSocketConfig = {
10
10
  maxRetryDelayMs?: number;
11
11
  logAfterRetryCount?: number;
12
12
  };
13
+ /**
14
+ * the isomorphic-ws package ships with some slightly-erroneous typings.
15
+ * namely, it returns a WebSocket with typings that indicate the "terminate()" function
16
+ * is available on all platforms.
17
+ * Given that, under the hood, it is using the globalThis.WebSocket class, if it's available,
18
+ * and falling back to using the https://www.npmjs.com/package/ws package, this
19
+ * means there are API differences between the native WebSocket (the one in a web browser)
20
+ * and the server-side version from the "ws" package.
21
+ *
22
+ * This type creates a WebSocket type reference we use to indicate the unknown
23
+ * nature of the env in which is code is run.
24
+ */
25
+ type UnsafeWebSocket = Omit<WebSocket, "terminate"> & Partial<Pick<WebSocket, "terminate">>;
13
26
  export declare class ResilientWebSocket {
14
27
  private endpoint;
15
28
  private wsOptions?;
@@ -17,7 +30,7 @@ export declare class ResilientWebSocket {
17
30
  private heartbeatTimeoutDurationMs;
18
31
  private maxRetryDelayMs;
19
32
  private logAfterRetryCount;
20
- wsClient: undefined | WebSocket;
33
+ wsClient: UnsafeWebSocket | undefined;
21
34
  wsUserClosed: boolean;
22
35
  private wsFailedAttempts;
23
36
  private heartbeatTimeout?;
@@ -47,3 +60,4 @@ export declare class ResilientWebSocket {
47
60
  */
48
61
  private retryDelayMs;
49
62
  }
63
+ export {};
@@ -1,5 +1,7 @@
1
1
  import WebSocket from "isomorphic-ws";
2
2
  import { dummyLogger } from "ts-log";
3
+ import { CustomSocketClosureCodes } from "../protocol.js";
4
+ import { envIsBrowserOrWorker } from "../util/env-util.js";
3
5
  const DEFAULT_HEARTBEAT_TIMEOUT_DURATION_MS = 5000; // 5 seconds
4
6
  const DEFAULT_MAX_RETRY_DELAY_MS = 1000; // 1 second'
5
7
  const DEFAULT_LOG_AFTER_RETRY_COUNT = 10;
@@ -74,7 +76,10 @@ export class ResilientWebSocket {
74
76
  clearTimeout(this.retryTimeout);
75
77
  this.retryTimeout = undefined;
76
78
  }
77
- this.wsClient = new WebSocket(this.endpoint, this.wsOptions);
79
+ // browser constructor supports a different 2nd argument for the constructor,
80
+ // so we need to ensure it's not included if we're running in that environment:
81
+ // https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket#protocols
82
+ this.wsClient = new WebSocket(this.endpoint, envIsBrowserOrWorker() ? undefined : this.wsOptions);
78
83
  this.wsClient.addEventListener("open", () => {
79
84
  this.logger.info("WebSocket connection established");
80
85
  this.wsFailedAttempts = 0;
@@ -112,8 +117,19 @@ export class ResilientWebSocket {
112
117
  clearTimeout(this.heartbeatTimeout);
113
118
  }
114
119
  this.heartbeatTimeout = setTimeout(() => {
115
- this.logger.warn("Connection timed out. Reconnecting...");
116
- this.wsClient?.terminate();
120
+ const warnMsg = "Connection timed out. Reconnecting...";
121
+ this.logger.warn(warnMsg);
122
+ if (this.wsClient) {
123
+ if (typeof this.wsClient.terminate === "function") {
124
+ this.wsClient.terminate();
125
+ }
126
+ else {
127
+ // terminate is an implementation detail of the node-friendly
128
+ // https://www.npmjs.com/package/ws package, but is not a native WebSocket API,
129
+ // so we have to use the close method
130
+ this.wsClient.close(CustomSocketClosureCodes.CLIENT_TIMEOUT_BUT_RECONNECTING, warnMsg);
131
+ }
132
+ }
117
133
  this.handleReconnect();
118
134
  }, this.heartbeatTimeoutDurationMs);
119
135
  }
@@ -4,6 +4,7 @@ import type { Logger } from "ts-log";
4
4
  import type { Request } from "../protocol.js";
5
5
  import type { ResilientWebSocketConfig } from "./resilient-websocket.js";
6
6
  import { ResilientWebSocket } from "./resilient-websocket.js";
7
+ type WebSocketOnMessageCallback = (data: WebSocket.Data) => void | Promise<void>;
7
8
  export type WebSocketPoolConfig = {
8
9
  urls?: string[];
9
10
  numConnections?: number;
@@ -33,15 +34,16 @@ export declare class WebSocketPool {
33
34
  * Checks for error responses in JSON messages and throws appropriate errors
34
35
  */
35
36
  private handleErrorMessages;
37
+ private constructCacheKeyFromWebsocketData;
36
38
  /**
37
39
  * Handles incoming websocket messages by deduplicating identical messages received across
38
40
  * multiple connections before forwarding to registered handlers
39
41
  */
40
- dedupeHandler: (data: WebSocket.Data) => void;
42
+ dedupeHandler: (data: WebSocket.Data) => Promise<void>;
41
43
  sendRequest(request: Request): void;
42
44
  addSubscription(request: Request): void;
43
45
  removeSubscription(subscriptionId: number): void;
44
- addMessageListener(handler: (data: WebSocket.Data) => void): void;
46
+ addMessageListener(handler: WebSocketOnMessageCallback): void;
45
47
  /**
46
48
  * Calls the handler if all websocket connections are currently down or in reconnecting state.
47
49
  * The connections may still try to reconnect in the background.
@@ -52,3 +54,4 @@ export declare class WebSocketPool {
52
54
  private checkConnectionStates;
53
55
  shutdown(): void;
54
56
  }
57
+ export {};
@@ -3,6 +3,7 @@ import WebSocket from "isomorphic-ws";
3
3
  import { dummyLogger } from "ts-log";
4
4
  import { ResilientWebSocket } from "./resilient-websocket.js";
5
5
  import { DEFAULT_STREAM_SERVICE_0_URL, DEFAULT_STREAM_SERVICE_1_URL, } from "../constants.js";
6
+ import { addAuthTokenToWebSocketUrl, bufferFromWebsocketData, envIsBrowserOrWorker, } from "../util/index.js";
6
7
  const DEFAULT_NUM_CONNECTIONS = 4;
7
8
  export class WebSocketPool {
8
9
  logger;
@@ -42,15 +43,17 @@ export class WebSocketPool {
42
43
  const pool = new WebSocketPool(log);
43
44
  const numConnections = config.numConnections ?? DEFAULT_NUM_CONNECTIONS;
44
45
  for (let i = 0; i < numConnections; i++) {
45
- const url = urls[i % urls.length];
46
+ const baseUrl = urls[i % urls.length];
47
+ const isBrowser = envIsBrowserOrWorker();
48
+ const url = isBrowser
49
+ ? addAuthTokenToWebSocketUrl(baseUrl, token)
50
+ : baseUrl;
46
51
  if (!url) {
47
52
  throw new Error(`URLs must not be null or empty`);
48
53
  }
49
54
  const wsOptions = {
50
55
  ...config.rwsConfig?.wsOptions,
51
- headers: {
52
- Authorization: `Bearer ${token}`,
53
- },
56
+ headers: isBrowser ? undefined : { Authorization: `Bearer ${token}` },
54
57
  };
55
58
  const rws = new ResilientWebSocket({
56
59
  ...config.rwsConfig,
@@ -77,7 +80,12 @@ export class WebSocketPool {
77
80
  rws.onError = config.onError;
78
81
  }
79
82
  // Handle all client messages ourselves. Dedupe before sending to registered message handlers.
80
- rws.onMessage = pool.dedupeHandler;
83
+ rws.onMessage = (data) => {
84
+ pool.dedupeHandler(data).catch((error) => {
85
+ const errMsg = `An error occurred in the WebSocket pool's dedupeHandler: ${error instanceof Error ? error.message : String(error)}`;
86
+ throw new Error(errMsg);
87
+ });
88
+ };
81
89
  pool.rwsPool.push(rws);
82
90
  rws.startWebSocket();
83
91
  }
@@ -100,14 +108,18 @@ export class WebSocketPool {
100
108
  throw new Error(`Error: ${message.error}`);
101
109
  }
102
110
  }
111
+ async constructCacheKeyFromWebsocketData(data) {
112
+ if (typeof data === "string")
113
+ return data;
114
+ const buff = await bufferFromWebsocketData(data);
115
+ return buff.toString("hex");
116
+ }
103
117
  /**
104
118
  * Handles incoming websocket messages by deduplicating identical messages received across
105
119
  * multiple connections before forwarding to registered handlers
106
120
  */
107
- dedupeHandler = (data) => {
108
- const cacheKey = typeof data === "string"
109
- ? data
110
- : Buffer.from(data).toString("hex");
121
+ dedupeHandler = async (data) => {
122
+ const cacheKey = await this.constructCacheKeyFromWebsocketData(data);
111
123
  if (this.cache.has(cacheKey)) {
112
124
  this.logger.debug("Dropping duplicate message");
113
125
  return;
@@ -116,9 +128,7 @@ export class WebSocketPool {
116
128
  if (typeof data === "string") {
117
129
  this.handleErrorMessages(data);
118
130
  }
119
- for (const handler of this.messageListeners) {
120
- handler(data);
121
- }
131
+ await Promise.all(this.messageListeners.map((handler) => handler(data)));
122
132
  };
123
133
  sendRequest(request) {
124
134
  for (const rws of this.rwsPool) {
@@ -0,0 +1,7 @@
1
+ import type { Data } from "isomorphic-ws";
2
+ /**
3
+ * given a relatively unknown websocket frame data object,
4
+ * returns a valid Buffer instance that is safe to use
5
+ * isomorphically in any JS runtime environment
6
+ */
7
+ export declare function bufferFromWebsocketData(data: Data): Promise<Buffer>;
@@ -0,0 +1,30 @@
1
+ // the linting rules don't allow importing anything that might clash with
2
+ // a global, top-level import. we disable this rule because we need this
3
+ // imported from our installed dependency
4
+ // eslint-disable-next-line unicorn/prefer-node-protocol
5
+ import { Buffer as BrowserBuffer } from "buffer";
6
+ const BufferClassToUse = "Buffer" in globalThis ? globalThis.Buffer : BrowserBuffer;
7
+ /**
8
+ * given a relatively unknown websocket frame data object,
9
+ * returns a valid Buffer instance that is safe to use
10
+ * isomorphically in any JS runtime environment
11
+ */
12
+ export async function bufferFromWebsocketData(data) {
13
+ if (typeof data === "string") {
14
+ return BufferClassToUse.from(new TextEncoder().encode(data).buffer);
15
+ }
16
+ if (data instanceof BufferClassToUse)
17
+ return data;
18
+ if (data instanceof Blob) {
19
+ // let the uncaught promise exception bubble up if there's an issue
20
+ return BufferClassToUse.from(await data.arrayBuffer());
21
+ }
22
+ if (data instanceof ArrayBuffer)
23
+ return BufferClassToUse.from(data);
24
+ if (Array.isArray(data)) {
25
+ // an array of buffers is highly unlikely, but it is a possibility
26
+ // indicated by the WebSocket Data interface
27
+ return BufferClassToUse.concat(data);
28
+ }
29
+ return data;
30
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Detects if this code is running within any Service or WebWorker context.
3
+ * @returns true if in a worker of some kind, false if otherwise
4
+ */
5
+ export declare function envIsServiceOrWebWorker(): boolean;
6
+ /**
7
+ * Detects if the code is running in a regular DOM or Web Worker context.
8
+ * @returns true if running in a DOM or Web Worker context, false if running in Node.js
9
+ */
10
+ export declare function envIsBrowser(): boolean;
11
+ /**
12
+ * a convenience method that returns whether or not
13
+ * this code is executing in some type of browser-centric environment
14
+ *
15
+ * @returns true if in the browser's main UI thread or in a worker, false if otherwise
16
+ */
17
+ export declare function envIsBrowserOrWorker(): boolean;
@@ -0,0 +1,27 @@
1
+ // we create this local-only type, which has assertions made to indicate
2
+ // that we do not know and cannot guarantee which JS environment we are in
3
+ const g = globalThis;
4
+ /**
5
+ * Detects if this code is running within any Service or WebWorker context.
6
+ * @returns true if in a worker of some kind, false if otherwise
7
+ */
8
+ export function envIsServiceOrWebWorker() {
9
+ return (typeof WorkerGlobalScope !== "undefined" &&
10
+ g.self instanceof WorkerGlobalScope);
11
+ }
12
+ /**
13
+ * Detects if the code is running in a regular DOM or Web Worker context.
14
+ * @returns true if running in a DOM or Web Worker context, false if running in Node.js
15
+ */
16
+ export function envIsBrowser() {
17
+ return g.window !== undefined;
18
+ }
19
+ /**
20
+ * a convenience method that returns whether or not
21
+ * this code is executing in some type of browser-centric environment
22
+ *
23
+ * @returns true if in the browser's main UI thread or in a worker, false if otherwise
24
+ */
25
+ export function envIsBrowserOrWorker() {
26
+ return envIsServiceOrWebWorker() || envIsBrowser();
27
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./buffer-util.js";
2
+ export * from "./env-util.js";
3
+ export * from "./url-util.js";
@@ -0,0 +1,3 @@
1
+ export * from "./buffer-util.js";
2
+ export * from "./env-util.js";
3
+ export * from "./url-util.js";
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Given a URL to a hosted lazer stream service and a possible auth token,
3
+ * appends the auth token as a query parameter and returns the URL with the token
4
+ * contained within.
5
+ * If the URL provided is nullish, it is returned as-is (in the same nullish format).
6
+ * If the token is nullish, the baseUrl given is returned, instead.
7
+ */
8
+ export declare function addAuthTokenToWebSocketUrl(baseUrl: string | null | undefined, authToken: string | null | undefined): string | null | undefined;
@@ -0,0 +1,15 @@
1
+ const ACCESS_TOKEN_QUERY_PARAM_KEY = "ACCESS_TOKEN";
2
+ /**
3
+ * Given a URL to a hosted lazer stream service and a possible auth token,
4
+ * appends the auth token as a query parameter and returns the URL with the token
5
+ * contained within.
6
+ * If the URL provided is nullish, it is returned as-is (in the same nullish format).
7
+ * If the token is nullish, the baseUrl given is returned, instead.
8
+ */
9
+ export function addAuthTokenToWebSocketUrl(baseUrl, authToken) {
10
+ if (!baseUrl || !authToken)
11
+ return baseUrl;
12
+ const parsedUrl = new URL(baseUrl);
13
+ parsedUrl.searchParams.set(ACCESS_TOKEN_QUERY_PARAM_KEY, authToken);
14
+ return parsedUrl.toString();
15
+ }
package/package.json CHANGED
@@ -1,7 +1,10 @@
1
1
  {
2
2
  "name": "@pythnetwork/pyth-lazer-sdk",
3
- "version": "3.0.0",
3
+ "version": "5.0.0",
4
4
  "description": "Pyth Lazer SDK",
5
+ "engines": {
6
+ "node": ">=22"
7
+ },
5
8
  "publishConfig": {
6
9
  "access": "public"
7
10
  },
@@ -48,7 +51,7 @@
48
51
  "license": "Apache-2.0",
49
52
  "dependencies": {
50
53
  "@isaacs/ttlcache": "^1.4.1",
51
- "cross-fetch": "^4.0.0",
54
+ "buffer": "^6.0.3",
52
55
  "isomorphic-ws": "^5.0.0",
53
56
  "ts-log": "^2.2.7",
54
57
  "ws": "^8.18.0"