@pythnetwork/pyth-lazer-sdk 6.2.1 → 6.2.3

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
@@ -60,3 +60,77 @@ c.subscribe({
60
60
  jsonBinaryEncoding: "hex",
61
61
  });
62
62
  ```
63
+
64
+ ## Error callbacks and outage detection
65
+
66
+ The SDK exposes two error callbacks and an "all connections down" listener that
67
+ serve distinct purposes. Choose what to handle based on whether you want
68
+ visibility into per-socket churn or a signal that the price stream has actually
69
+ stopped.
70
+
71
+ ### `onWebSocketError` (per-socket errors)
72
+
73
+ Passed via `webSocketPoolConfig.onWebSocketError`. Fires for individual
74
+ socket-level errors on a single underlying WebSocket — for example, a transient
75
+ `502` from one endpoint during a server upgrade or chaos test:
76
+
77
+ ```json
78
+ {
79
+ "at": "pyth#connect",
80
+ "level": "ERROR",
81
+ "error": "Unexpected server response: 502"
82
+ }
83
+ ```
84
+
85
+ **In most cases you never need to handle `onWebSocketError` — it is purely
86
+ informational.** The pool manages each socket's lifecycle (reconnect, retry,
87
+ failover across the configured URLs), so these errors are expected churn when
88
+ one endpoint hiccups. When the SDK is configured with redundant connections
89
+ (`numConnections > 1`, multiple `urls`), a single socket failing does **not**
90
+ mean the stream is dark — the other connections continue delivering messages.
91
+ Treat it as diagnostic only; don't page or alert on it.
92
+
93
+ ### `onWebSocketPoolError` (pool-level errors)
94
+
95
+ Passed via `webSocketPoolConfig.onWebSocketPoolError`. Fires for errors that
96
+ originate from the pool itself rather than a single socket — server-sent
97
+ `subscriptionError`/`error` messages, exceptions thrown by your message
98
+ listeners, send failures, and other internal errors that would otherwise
99
+ surface as unhandled rejections. Applications should listen to this and surface
100
+ it (log, alert, etc.).
101
+
102
+ ```javascript
103
+ webSocketPoolConfig: {
104
+ onWebSocketError: (error) => {
105
+ // Individual socket error — usually safe to log at info/debug.
106
+ console.warn("websocket error (single connection):", error.message);
107
+ },
108
+ onWebSocketPoolError: (error) => {
109
+ // Pool-level error — surface this.
110
+ console.error("websocket pool error:", error);
111
+ },
112
+ },
113
+ ```
114
+
115
+ ### `addAllConnectionsDownListener` (stream is dark)
116
+
117
+ Registered on the client. Fires when **every** redundant connection is down or
118
+ reconnecting at the same time — i.e., the price stream really has stopped.
119
+ This is the right signal for outage detection and paging:
120
+
121
+ ```javascript
122
+ c.addAllConnectionsDownListener(() => {
123
+ console.error("All Lazer connections are down — stream is dark.");
124
+ });
125
+ ```
126
+
127
+ A matching `addConnectionRestoredListener` fires when at least one connection
128
+ comes back after an all-down period.
129
+
130
+ ### Recommendation
131
+
132
+ For detecting real outages, handle `onWebSocketPoolError` and
133
+ `addAllConnectionsDownListener`. Do not page or panic solely on
134
+ `onWebSocketError` when the SDK is configured with redundant endpoints and
135
+ prices are still flowing — individual sockets are expected to flap during
136
+ server upgrades and chaos tests.
@@ -30,7 +30,7 @@ _export(exports, {
30
30
  });
31
31
  const SOLANA_LAZER_PROGRAM_ID = "pytd2yyk641x7ak7mkaasSJVXh6YYZnC7wTmtgAyxPt";
32
32
  const SOLANA_LAZER_STORAGE_ID = "3rdJbqfnagQ4yx9HXJViD4zc4xpiSqmFsKpPuSCQVyQL";
33
- const DEFAULT_METADATA_SERVICE_URL = "https://history.pyth-lazer.dourolabs.app/history";
33
+ const DEFAULT_METADATA_SERVICE_URL = "https://pyth.dourolabs.app";
34
34
  const DEFAULT_PRICE_SERVICE_URL = "https://pyth-lazer-0.dourolabs.app";
35
35
  const DEFAULT_STREAM_SERVICE_0_URL = "wss://pyth-lazer-0.dourolabs.app/v1/stream";
36
36
  const DEFAULT_STREAM_SERVICE_1_URL = "wss://pyth-lazer-1.dourolabs.app/v1/stream";
@@ -1,6 +1,6 @@
1
1
  export declare const SOLANA_LAZER_PROGRAM_ID = "pytd2yyk641x7ak7mkaasSJVXh6YYZnC7wTmtgAyxPt";
2
2
  export declare const SOLANA_LAZER_STORAGE_ID = "3rdJbqfnagQ4yx9HXJViD4zc4xpiSqmFsKpPuSCQVyQL";
3
- export declare const DEFAULT_METADATA_SERVICE_URL = "https://history.pyth-lazer.dourolabs.app/history";
3
+ export declare const DEFAULT_METADATA_SERVICE_URL = "https://pyth.dourolabs.app";
4
4
  export declare const DEFAULT_PRICE_SERVICE_URL = "https://pyth-lazer-0.dourolabs.app";
5
5
  export declare const DEFAULT_STREAM_SERVICE_0_URL = "wss://pyth-lazer-0.dourolabs.app/v1/stream";
6
6
  export declare const DEFAULT_STREAM_SERVICE_1_URL = "wss://pyth-lazer-1.dourolabs.app/v1/stream";
@@ -84,46 +84,68 @@ class ResilientWebSocket {
84
84
  this.logger.info("WebSocket client already started.");
85
85
  return;
86
86
  }
87
- if (this.wsFailedAttempts == 0) {
87
+ if (this.wsFailedAttempts === 0) {
88
88
  this.logger.info(`Creating Web Socket client`);
89
89
  }
90
90
  if (this.retryTimeout !== undefined) {
91
91
  clearTimeout(this.retryTimeout);
92
92
  this.retryTimeout = undefined;
93
93
  }
94
- // browser constructor supports a different 2nd argument for the constructor,
95
- // so we need to ensure it's not included if we're running in that environment:
96
- // https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket#protocols
97
- this.wsClient = new _isomorphicws.default(this.endpoint, (0, _envutil.envIsBrowserOrWorker)() ? undefined : this.wsOptions);
98
- this.wsClient.addEventListener("open", ()=>{
99
- this.logger.info("WebSocket connection established");
100
- this.wsFailedAttempts = 0;
101
- this._isReconnecting = false;
102
- this.resetHeartbeat();
103
- this.onReconnect();
104
- });
105
- this.wsClient.addEventListener("close", (e)=>{
106
- if (this.wsUserClosed) {
107
- this.logger.info(`WebSocket connection to ${this.endpoint} closed by user`);
108
- } else {
109
- if (this.shouldLogRetry()) {
110
- this.logger.warn(`WebSocket connection to ${this.endpoint} closed unexpectedly: Code: ${e.code.toString()}`);
94
+ // we wrap the new WebSocket() construction in a try / catch because,
95
+ // if one of our instances legitimately goes down or there's some network
96
+ // error that impacts connectivity, the WebSocket constructor will throw
97
+ // an uncaught DOMException in the browser (or equivalent in Node, Bun or Deno)
98
+ // and potentially blow up the process (if running in Node, Bun or Deno)
99
+ try {
100
+ // browser constructor supports a different 2nd argument for the constructor,
101
+ // so we need to ensure it's not included if we're running in that environment:
102
+ // https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket#protocols
103
+ this.wsClient = new _isomorphicws.default(this.endpoint, (0, _envutil.envIsBrowserOrWorker)() ? undefined : this.wsOptions);
104
+ this.wsClient.addEventListener("open", ()=>{
105
+ this.logger.info("WebSocket connection established");
106
+ this.wsFailedAttempts = 0;
107
+ this._isReconnecting = false;
108
+ this.resetHeartbeat();
109
+ try {
110
+ this.onReconnect();
111
+ } catch (error) {
112
+ this.logger.error("Error in onReconnect callback:", error);
111
113
  }
112
- this.handleReconnect();
113
- }
114
- });
115
- this.wsClient.addEventListener("error", (event)=>{
116
- this.onError(event);
117
- });
118
- this.wsClient.addEventListener("message", (event)=>{
119
- this.resetHeartbeat();
120
- this.onMessage(event.data);
121
- });
122
- if ("on" in this.wsClient) {
123
- this.wsClient.on("ping", ()=>{
124
- this.logger.info("Ping received");
114
+ });
115
+ this.wsClient.addEventListener("close", (e)=>{
116
+ if (this.wsUserClosed) {
117
+ this.logger.info(`WebSocket connection to ${this.endpoint} closed by user`);
118
+ } else {
119
+ if (this.shouldLogRetry()) {
120
+ this.logger.warn(`WebSocket connection to ${this.endpoint} closed unexpectedly: Code: ${e.code.toString()}`);
121
+ }
122
+ this.handleReconnect();
123
+ }
124
+ });
125
+ this.wsClient.addEventListener("error", (event)=>{
126
+ try {
127
+ this.onError(event);
128
+ } catch (error) {
129
+ this.logger.error("Error in onError callback:", error);
130
+ }
131
+ });
132
+ this.wsClient.addEventListener("message", (event)=>{
125
133
  this.resetHeartbeat();
134
+ try {
135
+ this.onMessage(event.data);
136
+ } catch (error) {
137
+ this.logger.error("Error in onMessage callback:", error);
138
+ }
126
139
  });
140
+ if (typeof this.wsClient?.on === "function") {
141
+ this.wsClient.on("ping", ()=>{
142
+ this.logger.debug("Ping received");
143
+ this.resetHeartbeat();
144
+ });
145
+ }
146
+ } catch (error) {
147
+ this.logger.error("Failed to create WebSocket client:", error);
148
+ this.handleReconnect();
127
149
  }
128
150
  }
129
151
  resetHeartbeat() {
@@ -133,7 +155,11 @@ class ResilientWebSocket {
133
155
  this.heartbeatTimeout = setTimeout(()=>{
134
156
  const warnMsg = "Connection timed out. Reconnecting...";
135
157
  this.logger.warn(warnMsg);
136
- this.onTimeout();
158
+ try {
159
+ this.onTimeout();
160
+ } catch (error) {
161
+ this.logger.error("Error in onTimeout callback:", error);
162
+ }
137
163
  if (this.wsClient) {
138
164
  if (typeof this.wsClient.terminate === "function") {
139
165
  this.wsClient.terminate();
@@ -144,7 +170,8 @@ class ResilientWebSocket {
144
170
  this.wsClient.close(_protocol.CustomSocketClosureCodes.CLIENT_TIMEOUT_BUT_RECONNECTING, warnMsg);
145
171
  }
146
172
  }
147
- this.handleReconnect();
173
+ // No direct call to handleReconnect() here
174
+ // terminate/close will trigger the 'close' event, which will call it.
148
175
  }, this.heartbeatTimeoutDurationMs);
149
176
  }
150
177
  handleReconnect() {
@@ -169,11 +196,12 @@ class ResilientWebSocket {
169
196
  }, this.retryDelayMs());
170
197
  }
171
198
  closeWebSocket() {
172
- if (this.wsClient !== undefined) {
199
+ // immediately block duplicate calls to this function
200
+ this.wsUserClosed = true;
201
+ if (typeof this.wsClient?.close === "function") {
173
202
  this.wsClient.close();
174
203
  this.wsClient = undefined;
175
204
  }
176
- this.wsUserClosed = true;
177
205
  }
178
206
  /**
179
207
  * Calculates the delay in milliseconds for exponential backoff based on the number of failed attempts.
@@ -8,17 +8,12 @@ Object.defineProperty(exports, "WebSocketPool", {
8
8
  return WebSocketPool;
9
9
  }
10
10
  });
11
- const _ttlcache = /*#__PURE__*/ _interop_require_default(require("@isaacs/ttlcache"));
11
+ const _ttlcache = require("@isaacs/ttlcache");
12
12
  const _tslog = require("ts-log");
13
13
  const _constants = require("../constants.cjs");
14
14
  const _index = require("../emitter/index.cjs");
15
15
  const _index1 = require("../util/index.cjs");
16
16
  const _resilientwebsocket = require("./resilient-websocket.cjs");
17
- function _interop_require_default(obj) {
18
- return obj && obj.__esModule ? obj : {
19
- default: obj
20
- };
21
- }
22
17
  const DEFAULT_NUM_CONNECTIONS = 4;
23
18
  class WebSocketPool extends _index.IsomorphicEventEmitter {
24
19
  logger;
@@ -36,7 +31,7 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
36
31
  constructor(logger){
37
32
  super(), this.logger = logger;
38
33
  this.rwsPool = [];
39
- this.cache = new _ttlcache.default({
34
+ this.cache = new _ttlcache.TTLCache({
40
35
  ttl: 1000 * 10
41
36
  }); // TTL of 10 seconds
42
37
  this.subscriptions = new Map();
@@ -1,6 +1,6 @@
1
1
  export declare const SOLANA_LAZER_PROGRAM_ID = "pytd2yyk641x7ak7mkaasSJVXh6YYZnC7wTmtgAyxPt";
2
2
  export declare const SOLANA_LAZER_STORAGE_ID = "3rdJbqfnagQ4yx9HXJViD4zc4xpiSqmFsKpPuSCQVyQL";
3
- export declare const DEFAULT_METADATA_SERVICE_URL = "https://history.pyth-lazer.dourolabs.app/history";
3
+ export declare const DEFAULT_METADATA_SERVICE_URL = "https://pyth.dourolabs.app";
4
4
  export declare const DEFAULT_PRICE_SERVICE_URL = "https://pyth-lazer-0.dourolabs.app";
5
5
  export declare const DEFAULT_STREAM_SERVICE_0_URL = "wss://pyth-lazer-0.dourolabs.app/v1/stream";
6
6
  export declare const DEFAULT_STREAM_SERVICE_1_URL = "wss://pyth-lazer-1.dourolabs.app/v1/stream";
@@ -1,6 +1,6 @@
1
1
  export const SOLANA_LAZER_PROGRAM_ID = "pytd2yyk641x7ak7mkaasSJVXh6YYZnC7wTmtgAyxPt";
2
2
  export const SOLANA_LAZER_STORAGE_ID = "3rdJbqfnagQ4yx9HXJViD4zc4xpiSqmFsKpPuSCQVyQL";
3
- export const DEFAULT_METADATA_SERVICE_URL = "https://history.pyth-lazer.dourolabs.app/history";
3
+ export const DEFAULT_METADATA_SERVICE_URL = "https://pyth.dourolabs.app";
4
4
  export const DEFAULT_PRICE_SERVICE_URL = "https://pyth-lazer-0.dourolabs.app";
5
5
  export const DEFAULT_STREAM_SERVICE_0_URL = "wss://pyth-lazer-0.dourolabs.app/v1/stream";
6
6
  export const DEFAULT_STREAM_SERVICE_1_URL = "wss://pyth-lazer-1.dourolabs.app/v1/stream";
@@ -69,46 +69,68 @@ export class ResilientWebSocket {
69
69
  this.logger.info("WebSocket client already started.");
70
70
  return;
71
71
  }
72
- if (this.wsFailedAttempts == 0) {
72
+ if (this.wsFailedAttempts === 0) {
73
73
  this.logger.info(`Creating Web Socket client`);
74
74
  }
75
75
  if (this.retryTimeout !== undefined) {
76
76
  clearTimeout(this.retryTimeout);
77
77
  this.retryTimeout = undefined;
78
78
  }
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);
83
- this.wsClient.addEventListener("open", ()=>{
84
- this.logger.info("WebSocket connection established");
85
- this.wsFailedAttempts = 0;
86
- this._isReconnecting = false;
87
- this.resetHeartbeat();
88
- this.onReconnect();
89
- });
90
- this.wsClient.addEventListener("close", (e)=>{
91
- if (this.wsUserClosed) {
92
- this.logger.info(`WebSocket connection to ${this.endpoint} closed by user`);
93
- } else {
94
- if (this.shouldLogRetry()) {
95
- this.logger.warn(`WebSocket connection to ${this.endpoint} closed unexpectedly: Code: ${e.code.toString()}`);
79
+ // we wrap the new WebSocket() construction in a try / catch because,
80
+ // if one of our instances legitimately goes down or there's some network
81
+ // error that impacts connectivity, the WebSocket constructor will throw
82
+ // an uncaught DOMException in the browser (or equivalent in Node, Bun or Deno)
83
+ // and potentially blow up the process (if running in Node, Bun or Deno)
84
+ try {
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 WebSocket(this.endpoint, envIsBrowserOrWorker() ? undefined : this.wsOptions);
89
+ this.wsClient.addEventListener("open", ()=>{
90
+ this.logger.info("WebSocket connection established");
91
+ this.wsFailedAttempts = 0;
92
+ this._isReconnecting = false;
93
+ this.resetHeartbeat();
94
+ try {
95
+ this.onReconnect();
96
+ } catch (error) {
97
+ this.logger.error("Error in onReconnect callback:", error);
96
98
  }
97
- this.handleReconnect();
98
- }
99
- });
100
- this.wsClient.addEventListener("error", (event)=>{
101
- this.onError(event);
102
- });
103
- this.wsClient.addEventListener("message", (event)=>{
104
- this.resetHeartbeat();
105
- this.onMessage(event.data);
106
- });
107
- if ("on" in this.wsClient) {
108
- this.wsClient.on("ping", ()=>{
109
- this.logger.info("Ping received");
99
+ });
100
+ this.wsClient.addEventListener("close", (e)=>{
101
+ if (this.wsUserClosed) {
102
+ this.logger.info(`WebSocket connection to ${this.endpoint} closed by user`);
103
+ } else {
104
+ if (this.shouldLogRetry()) {
105
+ this.logger.warn(`WebSocket connection to ${this.endpoint} closed unexpectedly: Code: ${e.code.toString()}`);
106
+ }
107
+ this.handleReconnect();
108
+ }
109
+ });
110
+ this.wsClient.addEventListener("error", (event)=>{
111
+ try {
112
+ this.onError(event);
113
+ } catch (error) {
114
+ this.logger.error("Error in onError callback:", error);
115
+ }
116
+ });
117
+ this.wsClient.addEventListener("message", (event)=>{
110
118
  this.resetHeartbeat();
119
+ try {
120
+ this.onMessage(event.data);
121
+ } catch (error) {
122
+ this.logger.error("Error in onMessage callback:", error);
123
+ }
111
124
  });
125
+ if (typeof this.wsClient?.on === "function") {
126
+ this.wsClient.on("ping", ()=>{
127
+ this.logger.debug("Ping received");
128
+ this.resetHeartbeat();
129
+ });
130
+ }
131
+ } catch (error) {
132
+ this.logger.error("Failed to create WebSocket client:", error);
133
+ this.handleReconnect();
112
134
  }
113
135
  }
114
136
  resetHeartbeat() {
@@ -118,7 +140,11 @@ export class ResilientWebSocket {
118
140
  this.heartbeatTimeout = setTimeout(()=>{
119
141
  const warnMsg = "Connection timed out. Reconnecting...";
120
142
  this.logger.warn(warnMsg);
121
- this.onTimeout();
143
+ try {
144
+ this.onTimeout();
145
+ } catch (error) {
146
+ this.logger.error("Error in onTimeout callback:", error);
147
+ }
122
148
  if (this.wsClient) {
123
149
  if (typeof this.wsClient.terminate === "function") {
124
150
  this.wsClient.terminate();
@@ -129,7 +155,8 @@ export class ResilientWebSocket {
129
155
  this.wsClient.close(CustomSocketClosureCodes.CLIENT_TIMEOUT_BUT_RECONNECTING, warnMsg);
130
156
  }
131
157
  }
132
- this.handleReconnect();
158
+ // No direct call to handleReconnect() here
159
+ // terminate/close will trigger the 'close' event, which will call it.
133
160
  }, this.heartbeatTimeoutDurationMs);
134
161
  }
135
162
  handleReconnect() {
@@ -154,11 +181,12 @@ export class ResilientWebSocket {
154
181
  }, this.retryDelayMs());
155
182
  }
156
183
  closeWebSocket() {
157
- if (this.wsClient !== undefined) {
184
+ // immediately block duplicate calls to this function
185
+ this.wsUserClosed = true;
186
+ if (typeof this.wsClient?.close === "function") {
158
187
  this.wsClient.close();
159
188
  this.wsClient = undefined;
160
189
  }
161
- this.wsUserClosed = true;
162
190
  }
163
191
  /**
164
192
  * Calculates the delay in milliseconds for exponential backoff based on the number of failed attempts.
@@ -1,4 +1,4 @@
1
- import TTLCache from "@isaacs/ttlcache";
1
+ import { TTLCache } from "@isaacs/ttlcache";
2
2
  import { dummyLogger } from "ts-log";
3
3
  import { DEFAULT_STREAM_SERVICE_0_URL, DEFAULT_STREAM_SERVICE_1_URL } from "../constants.mjs";
4
4
  import { IsomorphicEventEmitter } from "../emitter/index.mjs";
package/package.json CHANGED
@@ -1,20 +1,20 @@
1
1
  {
2
2
  "bugs": {
3
- "url": "https://github.com/pyth-network/pyth-crosschain/issues"
3
+ "url": "https://github.com/pyth-network/pyth-lazer-public/issues"
4
4
  },
5
5
  "dependencies": {
6
- "@isaacs/ttlcache": "^1.4.1",
7
- "buffer": "^6.0.3",
8
- "isomorphic-ws": "^5.0.0",
9
- "ts-log": "^2.2.7",
10
- "ws": "^8.19.0"
6
+ "@isaacs/ttlcache": "catalog:",
7
+ "buffer": "catalog:",
8
+ "isomorphic-ws": "catalog:",
9
+ "ts-log": "catalog:",
10
+ "ws": "catalog:"
11
11
  },
12
12
  "description": "Pyth Lazer SDK",
13
13
  "devDependencies": {
14
- "@cprussin/tsconfig": "^4.0.2",
15
- "@types/node": "^24.10.1",
16
- "@types/ws": "^8.18.1",
17
- "typedoc": "^0.28.17"
14
+ "@cprussin/tsconfig": "catalog:",
15
+ "@types/node": "catalog:",
16
+ "@types/ws": "catalog:",
17
+ "typedoc": "catalog:"
18
18
  },
19
19
  "engines": {
20
20
  "node": "^24.0.0"
@@ -135,7 +135,7 @@
135
135
  "files": [
136
136
  "dist/**/*"
137
137
  ],
138
- "homepage": "https://github.com/pyth-network/pyth-crosschain/tree/main/lazer/sdk/js",
138
+ "homepage": "https://github.com/pyth-network/pyth-lazer-public/tree/main/sdk/js",
139
139
  "keywords": [
140
140
  "pyth",
141
141
  "oracle"
@@ -148,9 +148,9 @@
148
148
  "access": "public"
149
149
  },
150
150
  "repository": {
151
- "directory": "lazer/sdk/js",
151
+ "directory": "sdk/js",
152
152
  "type": "git",
153
- "url": "https://github.com/pyth-network/pyth-crosschain"
153
+ "url": "https://github.com/pyth-network/pyth-lazer-public"
154
154
  },
155
155
  "scripts": {
156
156
  "build": "ts-duality --clean",
@@ -164,5 +164,5 @@
164
164
  },
165
165
  "type": "module",
166
166
  "types": "./dist/cjs/index.d.ts",
167
- "version": "6.2.1"
167
+ "version": "6.2.3"
168
168
  }