@pythnetwork/pyth-lazer-sdk 4.0.0 → 5.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.
Files changed (45) hide show
  1. package/README.md +57 -0
  2. package/dist/cjs/client.cjs +229 -0
  3. package/dist/cjs/constants.cjs +36 -0
  4. package/dist/cjs/index.cjs +20 -0
  5. package/dist/cjs/package.json +1 -1
  6. package/dist/cjs/protocol.cjs +33 -0
  7. package/dist/cjs/protocol.d.ts +4 -1
  8. package/dist/cjs/socket/{resilient-websocket.js → resilient-websocket.cjs} +60 -45
  9. package/dist/cjs/socket/resilient-websocket.d.ts +15 -1
  10. package/dist/cjs/socket/{websocket-pool.js → websocket-pool.cjs} +77 -64
  11. package/dist/cjs/socket/websocket-pool.d.ts +5 -2
  12. package/dist/cjs/util/buffer-util.cjs +33 -0
  13. package/dist/cjs/util/buffer-util.d.ts +7 -0
  14. package/dist/cjs/util/env-util.cjs +33 -0
  15. package/dist/cjs/util/env-util.d.ts +17 -0
  16. package/dist/cjs/util/index.cjs +20 -0
  17. package/dist/cjs/util/index.d.ts +3 -0
  18. package/dist/cjs/util/url-util.cjs +17 -0
  19. package/dist/cjs/util/url-util.d.ts +8 -0
  20. package/dist/esm/client.mjs +219 -0
  21. package/dist/esm/index.mjs +3 -0
  22. package/dist/esm/package.json +1 -1
  23. package/dist/esm/protocol.d.ts +4 -1
  24. package/dist/esm/protocol.mjs +12 -0
  25. package/dist/esm/socket/resilient-websocket.d.ts +15 -1
  26. package/dist/esm/socket/{resilient-websocket.js → resilient-websocket.mjs} +42 -35
  27. package/dist/esm/socket/websocket-pool.d.ts +5 -2
  28. package/dist/esm/socket/{websocket-pool.js → websocket-pool.mjs} +58 -54
  29. package/dist/esm/util/buffer-util.d.ts +7 -0
  30. package/dist/esm/util/buffer-util.mjs +27 -0
  31. package/dist/esm/util/env-util.d.ts +17 -0
  32. package/dist/esm/util/env-util.mjs +23 -0
  33. package/dist/esm/util/index.d.ts +3 -0
  34. package/dist/esm/util/index.mjs +3 -0
  35. package/dist/esm/util/url-util.d.ts +8 -0
  36. package/dist/esm/util/url-util.mjs +13 -0
  37. package/package.json +111 -15
  38. package/dist/cjs/client.js +0 -236
  39. package/dist/cjs/constants.js +0 -9
  40. package/dist/cjs/index.js +0 -19
  41. package/dist/cjs/protocol.js +0 -11
  42. package/dist/esm/client.js +0 -230
  43. package/dist/esm/index.js +0 -3
  44. package/dist/esm/protocol.js +0 -8
  45. /package/dist/esm/{constants.js → constants.mjs} +0 -0
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
+ ```
@@ -0,0 +1,229 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", {
3
+ value: true
4
+ });
5
+ Object.defineProperty(exports, "PythLazerClient", {
6
+ enumerable: true,
7
+ get: function() {
8
+ return PythLazerClient;
9
+ }
10
+ });
11
+ const _tslog = require("ts-log");
12
+ const _constants = require("./constants.cjs");
13
+ const _protocol = require("./protocol.cjs");
14
+ const _websocketpool = require("./socket/websocket-pool.cjs");
15
+ const _bufferutil = require("./util/buffer-util.cjs");
16
+ const UINT16_NUM_BYTES = 2;
17
+ const UINT32_NUM_BYTES = 4;
18
+ const UINT64_NUM_BYTES = 8;
19
+ class PythLazerClient {
20
+ token;
21
+ metadataServiceUrl;
22
+ priceServiceUrl;
23
+ logger;
24
+ wsp;
25
+ constructor(token, metadataServiceUrl, priceServiceUrl, logger, wsp){
26
+ this.token = token;
27
+ this.metadataServiceUrl = metadataServiceUrl;
28
+ this.priceServiceUrl = priceServiceUrl;
29
+ this.logger = logger;
30
+ 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;
41
+ }
42
+ /**
43
+ * Creates a new PythLazerClient instance.
44
+ * @param config - Configuration including token, metadata service URL, and price service URL, and WebSocket pool configuration
45
+ */ static async create(config) {
46
+ const token = config.token;
47
+ // Collect and remove trailing slash from URLs
48
+ const metadataServiceUrl = (config.metadataServiceUrl ?? _constants.DEFAULT_METADATA_SERVICE_URL).replace(/\/+$/, "");
49
+ const priceServiceUrl = (config.priceServiceUrl ?? _constants.DEFAULT_PRICE_SERVICE_URL).replace(/\/+$/, "");
50
+ 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);
57
+ }
58
+ /**
59
+ * Adds a message listener that receives either JSON or binary responses from the WebSocket connections.
60
+ * The listener will be called for each message received, with deduplication across redundant connections.
61
+ * @param handler - Callback function that receives the parsed message. The message can be either a JSON response
62
+ * or a binary response containing EVM, Solana, or parsed payload data.
63
+ */ addMessageListener(handler) {
64
+ const wsp = this.getWebSocketPool();
65
+ wsp.addMessageListener(async (data)=>{
66
+ if (typeof data == "string") {
67
+ handler({
68
+ type: "json",
69
+ value: JSON.parse(data)
70
+ });
71
+ return;
72
+ }
73
+ const buffData = await (0, _bufferutil.bufferFromWebsocketData)(data);
74
+ let pos = 0;
75
+ const magic = buffData.subarray(pos, pos + UINT32_NUM_BYTES).readUint32LE();
76
+ pos += UINT32_NUM_BYTES;
77
+ if (magic != _protocol.BINARY_UPDATE_FORMAT_MAGIC_LE) {
78
+ throw new Error("binary update format magic mismatch");
79
+ }
80
+ // TODO: some uint64 values may not be representable as Number.
81
+ const subscriptionId = Number(buffData.subarray(pos, pos + UINT64_NUM_BYTES).readBigInt64BE());
82
+ pos += UINT64_NUM_BYTES;
83
+ const value = {
84
+ subscriptionId
85
+ };
86
+ while(pos < buffData.length){
87
+ const len = buffData.subarray(pos, pos + UINT16_NUM_BYTES).readUint16BE();
88
+ pos += UINT16_NUM_BYTES;
89
+ const magic = buffData.subarray(pos, pos + UINT32_NUM_BYTES).readUint32LE();
90
+ if (magic == _protocol.FORMAT_MAGICS_LE.EVM) {
91
+ value.evm = buffData.subarray(pos, pos + len);
92
+ } else if (magic == _protocol.FORMAT_MAGICS_LE.SOLANA) {
93
+ value.solana = buffData.subarray(pos, pos + len);
94
+ } else if (magic == _protocol.FORMAT_MAGICS_LE.LE_ECDSA) {
95
+ value.leEcdsa = buffData.subarray(pos, pos + len);
96
+ } else if (magic == _protocol.FORMAT_MAGICS_LE.LE_UNSIGNED) {
97
+ value.leUnsigned = buffData.subarray(pos, pos + len);
98
+ } else if (magic == _protocol.FORMAT_MAGICS_LE.JSON) {
99
+ value.parsed = JSON.parse(buffData.subarray(pos + UINT32_NUM_BYTES, pos + len).toString());
100
+ } else {
101
+ throw new Error(`unknown magic: ${magic.toString()}`);
102
+ }
103
+ pos += len;
104
+ }
105
+ handler({
106
+ type: "binary",
107
+ value
108
+ });
109
+ });
110
+ }
111
+ subscribe(request) {
112
+ const wsp = this.getWebSocketPool();
113
+ if (request.type !== "subscribe") {
114
+ throw new Error("Request must be a subscribe request");
115
+ }
116
+ wsp.addSubscription(request);
117
+ }
118
+ unsubscribe(subscriptionId) {
119
+ const wsp = this.getWebSocketPool();
120
+ wsp.removeSubscription(subscriptionId);
121
+ }
122
+ send(request) {
123
+ const wsp = this.getWebSocketPool();
124
+ wsp.sendRequest(request);
125
+ }
126
+ /**
127
+ * Registers a handler function that will be called whenever all WebSocket connections are down or attempting to reconnect.
128
+ * The connections may still try to reconnect in the background. To shut down the pool, call `shutdown()`.
129
+ * @param handler - Function to be called when all connections are down
130
+ */ addAllConnectionsDownListener(handler) {
131
+ const wsp = this.getWebSocketPool();
132
+ wsp.addAllConnectionsDownListener(handler);
133
+ }
134
+ shutdown() {
135
+ const wsp = this.getWebSocketPool();
136
+ wsp.shutdown();
137
+ }
138
+ /**
139
+ * Private helper method to make authenticated HTTP requests with Bearer token
140
+ * @param url - The URL to fetch
141
+ * @param options - Additional fetch options
142
+ * @returns Promise resolving to the fetch Response
143
+ */ async authenticatedFetch(url, options = {}) {
144
+ const headers = {
145
+ Authorization: `Bearer ${this.token}`,
146
+ ...options.headers
147
+ };
148
+ return fetch(url, {
149
+ ...options,
150
+ headers
151
+ });
152
+ }
153
+ /**
154
+ * Queries the symbols endpoint to get available price feed symbols.
155
+ * @param params - Optional query parameters to filter symbols
156
+ * @returns Promise resolving to array of symbol information
157
+ */ async getSymbols(params) {
158
+ const url = new URL(`${this.metadataServiceUrl}/v1/symbols`);
159
+ if (params?.query) {
160
+ url.searchParams.set("query", params.query);
161
+ }
162
+ if (params?.asset_type) {
163
+ url.searchParams.set("asset_type", params.asset_type);
164
+ }
165
+ try {
166
+ const response = await this.authenticatedFetch(url.toString());
167
+ if (!response.ok) {
168
+ throw new Error(`HTTP error! status: ${String(response.status)} - ${await response.text()}`);
169
+ }
170
+ return await response.json();
171
+ } catch (error) {
172
+ throw new Error(`Failed to fetch symbols: ${error instanceof Error ? error.message : String(error)}`);
173
+ }
174
+ }
175
+ /**
176
+ * Queries the latest price endpoint to get current price data.
177
+ * @param params - Parameters for the latest price request
178
+ * @returns Promise resolving to JsonUpdate with current price data
179
+ */ async getLatestPrice(params) {
180
+ const url = `${this.priceServiceUrl}/v1/latest_price`;
181
+ try {
182
+ const body = JSON.stringify(params);
183
+ this.logger.debug("getLatestPrice", {
184
+ url,
185
+ body
186
+ });
187
+ const response = await this.authenticatedFetch(url, {
188
+ method: "POST",
189
+ headers: {
190
+ "Content-Type": "application/json"
191
+ },
192
+ body: body
193
+ });
194
+ if (!response.ok) {
195
+ throw new Error(`HTTP error! status: ${String(response.status)} - ${await response.text()}`);
196
+ }
197
+ return await response.json();
198
+ } catch (error) {
199
+ throw new Error(`Failed to fetch latest price: ${error instanceof Error ? error.message : String(error)}`);
200
+ }
201
+ }
202
+ /**
203
+ * Queries the price endpoint to get historical price data at a specific timestamp.
204
+ * @param params - Parameters for the price request including timestamp
205
+ * @returns Promise resolving to JsonUpdate with price data at the specified time
206
+ */ async getPrice(params) {
207
+ const url = `${this.priceServiceUrl}/v1/price`;
208
+ try {
209
+ const body = JSON.stringify(params);
210
+ this.logger.debug("getPrice", {
211
+ url,
212
+ body
213
+ });
214
+ const response = await this.authenticatedFetch(url, {
215
+ method: "POST",
216
+ headers: {
217
+ "Content-Type": "application/json"
218
+ },
219
+ body: body
220
+ });
221
+ if (!response.ok) {
222
+ throw new Error(`HTTP error! status: ${String(response.status)} - ${await response.text()}`);
223
+ }
224
+ return await response.json();
225
+ } catch (error) {
226
+ throw new Error(`Failed to fetch price: ${error instanceof Error ? error.message : String(error)}`);
227
+ }
228
+ }
229
+ }
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", {
3
+ value: true
4
+ });
5
+ function _export(target, all) {
6
+ for(var name in all)Object.defineProperty(target, name, {
7
+ enumerable: true,
8
+ get: Object.getOwnPropertyDescriptor(all, name).get
9
+ });
10
+ }
11
+ _export(exports, {
12
+ get DEFAULT_METADATA_SERVICE_URL () {
13
+ return DEFAULT_METADATA_SERVICE_URL;
14
+ },
15
+ get DEFAULT_PRICE_SERVICE_URL () {
16
+ return DEFAULT_PRICE_SERVICE_URL;
17
+ },
18
+ get DEFAULT_STREAM_SERVICE_0_URL () {
19
+ return DEFAULT_STREAM_SERVICE_0_URL;
20
+ },
21
+ get DEFAULT_STREAM_SERVICE_1_URL () {
22
+ return DEFAULT_STREAM_SERVICE_1_URL;
23
+ },
24
+ get SOLANA_LAZER_PROGRAM_ID () {
25
+ return SOLANA_LAZER_PROGRAM_ID;
26
+ },
27
+ get SOLANA_LAZER_STORAGE_ID () {
28
+ return SOLANA_LAZER_STORAGE_ID;
29
+ }
30
+ });
31
+ const SOLANA_LAZER_PROGRAM_ID = "pytd2yyk641x7ak7mkaasSJVXh6YYZnC7wTmtgAyxPt";
32
+ const SOLANA_LAZER_STORAGE_ID = "3rdJbqfnagQ4yx9HXJViD4zc4xpiSqmFsKpPuSCQVyQL";
33
+ const DEFAULT_METADATA_SERVICE_URL = "https://history.pyth-lazer.dourolabs.app/history";
34
+ const DEFAULT_PRICE_SERVICE_URL = "https://pyth-lazer-0.dourolabs.app";
35
+ const DEFAULT_STREAM_SERVICE_0_URL = "wss://pyth-lazer-0.dourolabs.app/v1/stream";
36
+ const DEFAULT_STREAM_SERVICE_1_URL = "wss://pyth-lazer-1.dourolabs.app/v1/stream";
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", {
3
+ value: true
4
+ });
5
+ _export_star(require("./client.cjs"), exports);
6
+ _export_star(require("./protocol.cjs"), exports);
7
+ _export_star(require("./constants.cjs"), exports);
8
+ function _export_star(from, to) {
9
+ Object.keys(from).forEach(function(k) {
10
+ if (k !== "default" && !Object.prototype.hasOwnProperty.call(to, k)) {
11
+ Object.defineProperty(to, k, {
12
+ enumerable: true,
13
+ get: function() {
14
+ return from[k];
15
+ }
16
+ });
17
+ }
18
+ });
19
+ return from;
20
+ }
@@ -1 +1 @@
1
- {"type":"commonjs"}
1
+ { "type": "commonjs" }
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", {
3
+ value: true
4
+ });
5
+ function _export(target, all) {
6
+ for(var name in all)Object.defineProperty(target, name, {
7
+ enumerable: true,
8
+ get: Object.getOwnPropertyDescriptor(all, name).get
9
+ });
10
+ }
11
+ _export(exports, {
12
+ get BINARY_UPDATE_FORMAT_MAGIC_LE () {
13
+ return BINARY_UPDATE_FORMAT_MAGIC_LE;
14
+ },
15
+ get CustomSocketClosureCodes () {
16
+ return CustomSocketClosureCodes;
17
+ },
18
+ get FORMAT_MAGICS_LE () {
19
+ return FORMAT_MAGICS_LE;
20
+ }
21
+ });
22
+ const BINARY_UPDATE_FORMAT_MAGIC_LE = 461_928_307;
23
+ const FORMAT_MAGICS_LE = {
24
+ JSON: 3_302_625_434,
25
+ EVM: 2_593_727_018,
26
+ SOLANA: 2_182_742_457,
27
+ LE_ECDSA: 1_296_547_300,
28
+ LE_UNSIGNED: 1_499_680_012
29
+ };
30
+ var CustomSocketClosureCodes = /*#__PURE__*/ function(CustomSocketClosureCodes) {
31
+ CustomSocketClosureCodes[CustomSocketClosureCodes["CLIENT_TIMEOUT_BUT_RECONNECTING"] = 4000] = "CLIENT_TIMEOUT_BUT_RECONNECTING";
32
+ return CustomSocketClosureCodes;
33
+ }({});
@@ -1,7 +1,7 @@
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";
4
+ export type PriceFeedProperty = "price" | "bestBidPrice" | "bestAskPrice" | "exponent" | "publisherCount" | "confidence" | "fundingRate" | "fundingTimestamp" | "fundingRateInterval";
5
5
  export type Channel = "real_time" | "fixed_rate@50ms" | "fixed_rate@200ms";
6
6
  export type Request = {
7
7
  type: "subscribe";
@@ -122,3 +122,6 @@ export type JsonUpdate = {
122
122
  leEcdsa?: JsonBinaryData;
123
123
  leUnsigned?: JsonBinaryData;
124
124
  };
125
+ export declare enum CustomSocketClosureCodes {
126
+ CLIENT_TIMEOUT_BUT_RECONNECTING = 4000
127
+ }
@@ -1,11 +1,22 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.ResilientWebSocket = void 0;
7
- const isomorphic_ws_1 = __importDefault(require("isomorphic-ws"));
8
- const ts_log_1 = require("ts-log");
2
+ Object.defineProperty(exports, "__esModule", {
3
+ value: true
4
+ });
5
+ Object.defineProperty(exports, "ResilientWebSocket", {
6
+ enumerable: true,
7
+ get: function() {
8
+ return ResilientWebSocket;
9
+ }
10
+ });
11
+ const _isomorphicws = /*#__PURE__*/ _interop_require_default(require("isomorphic-ws"));
12
+ const _tslog = require("ts-log");
13
+ const _protocol = require("../protocol.cjs");
14
+ const _envutil = require("../util/env-util.cjs");
15
+ function _interop_require_default(obj) {
16
+ return obj && obj.__esModule ? obj : {
17
+ default: obj
18
+ };
19
+ }
9
20
  const DEFAULT_HEARTBEAT_TIMEOUT_DURATION_MS = 5000; // 5 seconds
10
21
  const DEFAULT_MAX_RETRY_DELAY_MS = 1000; // 1 second'
11
22
  const DEFAULT_LOG_AFTER_RETRY_COUNT = 10;
@@ -26,7 +37,7 @@ class ResilientWebSocket {
26
37
  return this._isReconnecting;
27
38
  }
28
39
  isConnected() {
29
- return this.wsClient?.readyState === isomorphic_ws_1.default.OPEN;
40
+ return this.wsClient?.readyState === _isomorphicws.default.OPEN;
30
41
  }
31
42
  shouldLogRetry() {
32
43
  return this.wsFailedAttempts % this.logAfterRetryCount === 0;
@@ -34,33 +45,29 @@ class ResilientWebSocket {
34
45
  onError;
35
46
  onMessage;
36
47
  onReconnect;
37
- constructor(config) {
48
+ constructor(config){
38
49
  this.endpoint = config.endpoint;
39
50
  this.wsOptions = config.wsOptions;
40
- this.logger = config.logger ?? ts_log_1.dummyLogger;
41
- this.heartbeatTimeoutDurationMs =
42
- config.heartbeatTimeoutDurationMs ??
43
- DEFAULT_HEARTBEAT_TIMEOUT_DURATION_MS;
51
+ this.logger = config.logger ?? _tslog.dummyLogger;
52
+ this.heartbeatTimeoutDurationMs = config.heartbeatTimeoutDurationMs ?? DEFAULT_HEARTBEAT_TIMEOUT_DURATION_MS;
44
53
  this.maxRetryDelayMs = config.maxRetryDelayMs ?? DEFAULT_MAX_RETRY_DELAY_MS;
45
- this.logAfterRetryCount =
46
- config.logAfterRetryCount ?? DEFAULT_LOG_AFTER_RETRY_COUNT;
54
+ this.logAfterRetryCount = config.logAfterRetryCount ?? DEFAULT_LOG_AFTER_RETRY_COUNT;
47
55
  this.wsFailedAttempts = 0;
48
- this.onError = (error) => {
56
+ this.onError = (error)=>{
49
57
  void error;
50
58
  };
51
- this.onMessage = (data) => {
59
+ this.onMessage = (data)=>{
52
60
  void data;
53
61
  };
54
- this.onReconnect = () => {
55
- // Empty function, can be set by the user.
62
+ this.onReconnect = ()=>{
63
+ // Empty function, can be set by the user.
56
64
  };
57
65
  }
58
66
  send(data) {
59
67
  this.logger.debug(`Sending message`);
60
68
  if (this.isConnected()) {
61
69
  this.wsClient.send(data);
62
- }
63
- else {
70
+ } else {
64
71
  this.logger.warn(`WebSocket to ${this.endpoint} is not connected. Cannot send message.`);
65
72
  }
66
73
  }
@@ -80,34 +87,36 @@ class ResilientWebSocket {
80
87
  clearTimeout(this.retryTimeout);
81
88
  this.retryTimeout = undefined;
82
89
  }
83
- this.wsClient = new isomorphic_ws_1.default(this.endpoint, this.wsOptions);
84
- this.wsClient.addEventListener("open", () => {
90
+ // browser constructor supports a different 2nd argument for the constructor,
91
+ // so we need to ensure it's not included if we're running in that environment:
92
+ // https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket#protocols
93
+ this.wsClient = new _isomorphicws.default(this.endpoint, (0, _envutil.envIsBrowserOrWorker)() ? undefined : this.wsOptions);
94
+ this.wsClient.addEventListener("open", ()=>{
85
95
  this.logger.info("WebSocket connection established");
86
96
  this.wsFailedAttempts = 0;
87
97
  this._isReconnecting = false;
88
98
  this.resetHeartbeat();
89
99
  this.onReconnect();
90
100
  });
91
- this.wsClient.addEventListener("close", (e) => {
101
+ this.wsClient.addEventListener("close", (e)=>{
92
102
  if (this.wsUserClosed) {
93
103
  this.logger.info(`WebSocket connection to ${this.endpoint} closed by user`);
94
- }
95
- else {
104
+ } else {
96
105
  if (this.shouldLogRetry()) {
97
106
  this.logger.warn(`WebSocket connection to ${this.endpoint} closed unexpectedly: Code: ${e.code.toString()}`);
98
107
  }
99
108
  this.handleReconnect();
100
109
  }
101
110
  });
102
- this.wsClient.addEventListener("error", (event) => {
111
+ this.wsClient.addEventListener("error", (event)=>{
103
112
  this.onError(event);
104
113
  });
105
- this.wsClient.addEventListener("message", (event) => {
114
+ this.wsClient.addEventListener("message", (event)=>{
106
115
  this.resetHeartbeat();
107
116
  this.onMessage(event.data);
108
117
  });
109
118
  if ("on" in this.wsClient) {
110
- this.wsClient.on("ping", () => {
119
+ this.wsClient.on("ping", ()=>{
111
120
  this.logger.info("Ping received");
112
121
  this.resetHeartbeat();
113
122
  });
@@ -117,9 +126,19 @@ class ResilientWebSocket {
117
126
  if (this.heartbeatTimeout !== undefined) {
118
127
  clearTimeout(this.heartbeatTimeout);
119
128
  }
120
- this.heartbeatTimeout = setTimeout(() => {
121
- this.logger.warn("Connection timed out. Reconnecting...");
122
- this.wsClient?.terminate();
129
+ this.heartbeatTimeout = setTimeout(()=>{
130
+ const warnMsg = "Connection timed out. Reconnecting...";
131
+ this.logger.warn(warnMsg);
132
+ if (this.wsClient) {
133
+ if (typeof this.wsClient.terminate === "function") {
134
+ this.wsClient.terminate();
135
+ } else {
136
+ // terminate is an implementation detail of the node-friendly
137
+ // https://www.npmjs.com/package/ws package, but is not a native WebSocket API,
138
+ // so we have to use the close method
139
+ this.wsClient.close(_protocol.CustomSocketClosureCodes.CLIENT_TIMEOUT_BUT_RECONNECTING, warnMsg);
140
+ }
141
+ }
123
142
  this.handleReconnect();
124
143
  }, this.heartbeatTimeoutDurationMs);
125
144
  }
@@ -138,11 +157,9 @@ class ResilientWebSocket {
138
157
  this.wsClient = undefined;
139
158
  this._isReconnecting = true;
140
159
  if (this.shouldLogRetry()) {
141
- this.logger.error("Connection closed unexpectedly or because of timeout. Reconnecting after " +
142
- String(this.retryDelayMs()) +
143
- "ms.");
160
+ this.logger.error("Connection closed unexpectedly or because of timeout. Reconnecting after " + String(this.retryDelayMs()) + "ms.");
144
161
  }
145
- this.retryTimeout = setTimeout(() => {
162
+ this.retryTimeout = setTimeout(()=>{
146
163
  this.startWebSocket();
147
164
  }, this.retryDelayMs());
148
165
  }
@@ -154,18 +171,16 @@ class ResilientWebSocket {
154
171
  this.wsUserClosed = true;
155
172
  }
156
173
  /**
157
- * Calculates the delay in milliseconds for exponential backoff based on the number of failed attempts.
158
- *
159
- * The delay increases exponentially with each attempt, starting at 20ms for the first attempt,
160
- * and is capped at maxRetryDelayMs for attempts greater than or equal to 10.
161
- *
162
- * @returns The calculated delay in milliseconds before the next retry.
163
- */
164
- retryDelayMs() {
174
+ * Calculates the delay in milliseconds for exponential backoff based on the number of failed attempts.
175
+ *
176
+ * The delay increases exponentially with each attempt, starting at 20ms for the first attempt,
177
+ * and is capped at maxRetryDelayMs for attempts greater than or equal to 10.
178
+ *
179
+ * @returns The calculated delay in milliseconds before the next retry.
180
+ */ retryDelayMs() {
165
181
  if (this.wsFailedAttempts >= 10) {
166
182
  return this.maxRetryDelayMs;
167
183
  }
168
184
  return Math.min(2 ** this.wsFailedAttempts * 10, this.maxRetryDelayMs);
169
185
  }
170
186
  }
171
- exports.ResilientWebSocket = ResilientWebSocket;
@@ -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 {};