@pythnetwork/pyth-lazer-sdk 6.2.4 → 7.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
@@ -9,58 +9,81 @@ See [contributing.md](docs/contributing/contributing.md) for information on how
9
9
  ```javascript
10
10
  import { PythLazerClient } from "@pythnetwork/pyth-lazer-sdk";
11
11
 
12
- const c = await PythLazerClient.create({
12
+ const client = await PythLazerClient.create({
13
+ // Your Lazer access token.
14
+ // The SDK sends it during the WebSocket handshake via
15
+ // `Sec-WebSocket-Protocol`.
16
+ // refer to the "Authentication" section, below.
13
17
  token: "YOUR-AUTH-TOKEN-HERE",
14
- logger: console, // Optionally log operations (to the console in this case.)
18
+ logger: console, // Optional. Customize where connection logs are routed
15
19
  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) => {
20
+ // Optional.
21
+ // Parallel redundant connections, round-robined across `urls`, to
22
+ // reduce dropped messages.
23
+ // Default is 4.
24
+ numConnections: 4,
25
+
26
+ // Optional.
27
+ // Override the stream endpoint(s). Round-robined across `numConnections`.
28
+ // Defaults to the api service instances (pyth-0/1/2.dourolabs.app).
29
+ // urls: ["wss://pyth-0.dourolabs.app/v1/prices/stream"],
30
+ onWebSocketError: (error) => {
18
31
  console.error("⛔️ WebSocket error:", error.message);
19
32
  },
20
- // Optional configuration for resilient WebSocket connections
33
+
34
+ // Optional.
35
+ // Tune the underlying resilient connections. Auth, endpoint, and logger
36
+ // are managed by the SDK and cannot be set here.
21
37
  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
38
+ heartbeatTimeoutDurationMs: 5000, // Reconnect if no message/ping within this window.
39
+ maxRetryDelayMs: 1000, // Cap on exponential reconnect backoff.
40
+ logAfterRetryCount: 10, // Log every Nth failed reconnect attempt.
25
41
  },
26
42
  },
27
43
  });
28
44
 
29
- c.addMessageListener((message) => {
45
+ client.addMessageListener((message) => {
30
46
  console.info("received the following from the Lazer stream:", message);
31
47
  });
32
48
 
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(() => {
49
+ // Fires when every connection is down at once.
50
+ // Connections keep retrying in the background
51
+ client.addAllConnectionsDownListener(() => {
36
52
  console.error("All connections are down!");
37
53
  });
38
54
 
39
- // Create and remove one or more subscriptions on the fly
40
- c.subscribe({
55
+ // Subscribe on the fly.
56
+ client.subscribe({
41
57
  type: "subscribe",
42
58
  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
59
  priceFeedIds: [1, 2, 3, 4, 5],
55
60
  properties: ["price", "exponent", "publisherCount", "confidence"],
56
- formats: ["evm"],
61
+ formats: [],
57
62
  deliveryFormat: "json",
58
63
  channel: "fixed_rate@200ms",
59
64
  parsed: true,
60
- jsonBinaryEncoding: "hex",
61
65
  });
62
66
  ```
63
67
 
68
+ ## Authentication
69
+
70
+ Pass your access token as `token` — that is the only auth step. The SDK sends it
71
+ to the server during the WebSocket handshake via the **`Sec-WebSocket-Protocol`**
72
+ header (`Sec-WebSocket-Protocol: pyth-lazer-auth, <token>`), the same way in the
73
+ browser and in Node. It is never placed in the URL as a query parameter, so the
74
+ token stays out of URLs, browser history, and proxy/server access logs — worth
75
+ knowing if you care about credential hygiene.
76
+
77
+ Connections default to the redundant Lazer api service stream instances
78
+ (`wss://pyth-{0,1,2}.dourolabs.app/v1/prices/stream`), round-robined across
79
+ `numConnections` (override via `webSocketPoolConfig.urls`). They serve parsed /
80
+ unsigned JSON updates; signed `evm` / `solana` update payloads are not served on
81
+ this stream.
82
+
83
+ > **v7 breaking change:** the token moved from `?ACCESS_TOKEN=` to
84
+ > `Sec-WebSocket-Protocol`, and the default stream moved from the router to the
85
+ > api service. See [CHANGELOG.md](CHANGELOG.md).
86
+
64
87
  ## Error callbacks and outage detection
65
88
 
66
89
  The SDK exposes two error callbacks and an "all connections down" listener that
@@ -9,17 +9,17 @@ function _export(target, all) {
9
9
  });
10
10
  }
11
11
  _export(exports, {
12
+ get AUTH_SUBPROTOCOL () {
13
+ return AUTH_SUBPROTOCOL;
14
+ },
12
15
  get DEFAULT_METADATA_SERVICE_URL () {
13
16
  return DEFAULT_METADATA_SERVICE_URL;
14
17
  },
15
18
  get DEFAULT_PRICE_SERVICE_URL () {
16
19
  return DEFAULT_PRICE_SERVICE_URL;
17
20
  },
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;
21
+ get DEFAULT_STREAM_SERVICE_URLS () {
22
+ return DEFAULT_STREAM_SERVICE_URLS;
23
23
  },
24
24
  get SOLANA_LAZER_PROGRAM_ID () {
25
25
  return SOLANA_LAZER_PROGRAM_ID;
@@ -32,5 +32,9 @@ const SOLANA_LAZER_PROGRAM_ID = "pytd2yyk641x7ak7mkaasSJVXh6YYZnC7wTmtgAyxPt";
32
32
  const SOLANA_LAZER_STORAGE_ID = "3rdJbqfnagQ4yx9HXJViD4zc4xpiSqmFsKpPuSCQVyQL";
33
33
  const DEFAULT_METADATA_SERVICE_URL = "https://pyth.dourolabs.app";
34
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";
35
+ const DEFAULT_STREAM_SERVICE_URLS = [
36
+ "wss://pyth-0.dourolabs.app/v1/prices/stream",
37
+ "wss://pyth-1.dourolabs.app/v1/prices/stream",
38
+ "wss://pyth-2.dourolabs.app/v1/prices/stream"
39
+ ];
40
+ const AUTH_SUBPROTOCOL = "pyth-lazer-auth";
@@ -2,5 +2,15 @@ export declare const SOLANA_LAZER_PROGRAM_ID = "pytd2yyk641x7ak7mkaasSJVXh6YYZnC
2
2
  export declare const SOLANA_LAZER_STORAGE_ID = "3rdJbqfnagQ4yx9HXJViD4zc4xpiSqmFsKpPuSCQVyQL";
3
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
- export declare const DEFAULT_STREAM_SERVICE_0_URL = "wss://pyth-lazer-0.dourolabs.app/v1/stream";
6
- export declare const DEFAULT_STREAM_SERVICE_1_URL = "wss://pyth-lazer-1.dourolabs.app/v1/stream";
5
+ /**
6
+ * Default stream endpoints, round-robined across the pool's connections when
7
+ * the caller does not supply their own `urls`. These are redundant instances
8
+ * of the Lazer api-service stream.
9
+ */
10
+ export declare const DEFAULT_STREAM_SERVICE_URLS: string[];
11
+ /**
12
+ * WebSocket subprotocol marker that carries the auth token during the handshake
13
+ * (used in every environment): clients send
14
+ * `Sec-WebSocket-Protocol: pyth-lazer-auth, <token>`.
15
+ */
16
+ export declare const AUTH_SUBPROTOCOL = "pyth-lazer-auth";
@@ -11,7 +11,6 @@ Object.defineProperty(exports, "ResilientWebSocket", {
11
11
  const _isomorphicws = /*#__PURE__*/ _interop_require_default(require("isomorphic-ws"));
12
12
  const _tslog = require("ts-log");
13
13
  const _protocol = require("../protocol.cjs");
14
- const _envutil = require("../util/env-util.cjs");
15
14
  function _interop_require_default(obj) {
16
15
  return obj && obj.__esModule ? obj : {
17
16
  default: obj
@@ -22,7 +21,7 @@ const DEFAULT_MAX_RETRY_DELAY_MS = 1000; // 1 second'
22
21
  const DEFAULT_LOG_AFTER_RETRY_COUNT = 10;
23
22
  class ResilientWebSocket {
24
23
  endpoint;
25
- wsOptions;
24
+ protocols;
26
25
  logger;
27
26
  heartbeatTimeoutDurationMs;
28
27
  maxRetryDelayMs;
@@ -48,7 +47,7 @@ class ResilientWebSocket {
48
47
  onTimeout;
49
48
  constructor(config){
50
49
  this.endpoint = config.endpoint;
51
- this.wsOptions = config.wsOptions;
50
+ this.protocols = config.protocols;
52
51
  this.logger = config.logger ?? _tslog.dummyLogger;
53
52
  this.heartbeatTimeoutDurationMs = config.heartbeatTimeoutDurationMs ?? DEFAULT_HEARTBEAT_TIMEOUT_DURATION_MS;
54
53
  this.maxRetryDelayMs = config.maxRetryDelayMs ?? DEFAULT_MAX_RETRY_DELAY_MS;
@@ -97,10 +96,7 @@ class ResilientWebSocket {
97
96
  // an uncaught DOMException in the browser (or equivalent in Node, Bun or Deno)
98
97
  // and potentially blow up the process (if running in Node, Bun or Deno)
99
98
  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);
99
+ this.wsClient = new _isomorphicws.default(this.endpoint, this.protocols);
104
100
  this.wsClient.addEventListener("open", ()=>{
105
101
  this.logger.info("WebSocket connection established");
106
102
  this.wsFailedAttempts = 0;
@@ -1,10 +1,10 @@
1
- import type { ClientRequestArgs } from "node:http";
2
- import type { ClientOptions, ErrorEvent } from "isomorphic-ws";
1
+ import type { ErrorEvent } from "isomorphic-ws";
3
2
  import WebSocket from "isomorphic-ws";
4
3
  import type { Logger } from "ts-log";
5
4
  export type ResilientWebSocketConfig = {
6
5
  endpoint: string;
7
- wsOptions?: ClientOptions | ClientRequestArgs | undefined;
6
+ /** WebSocket subprotocols, used to carry the auth token. */
7
+ protocols?: string[] | undefined;
8
8
  logger?: Logger;
9
9
  heartbeatTimeoutDurationMs?: number;
10
10
  maxRetryDelayMs?: number;
@@ -25,7 +25,7 @@ export type ResilientWebSocketConfig = {
25
25
  type UnsafeWebSocket = Omit<WebSocket, "terminate"> & Partial<Pick<WebSocket, "terminate">>;
26
26
  export declare class ResilientWebSocket {
27
27
  private endpoint;
28
- private wsOptions?;
28
+ private protocols?;
29
29
  private logger;
30
30
  private heartbeatTimeoutDurationMs;
31
31
  private maxRetryDelayMs;
@@ -52,7 +52,7 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
52
52
  * @param token - Authentication token to use for the connections
53
53
  * @param numConnections - Number of parallel WebSocket connections to maintain (default: 3)
54
54
  * @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
55
- */ static async create(config, token, abortSignal, logger) {
55
+ */ static async create(config, token, abortSignal, logger, deps = {}) {
56
56
  // Helper to check if aborted and throw
57
57
  const throwIfAborted = ()=>{
58
58
  if (abortSignal?.aborted) {
@@ -61,11 +61,9 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
61
61
  };
62
62
  // Check before starting
63
63
  throwIfAborted();
64
- const urls = config.urls ?? [
65
- _constants.DEFAULT_STREAM_SERVICE_0_URL,
66
- _constants.DEFAULT_STREAM_SERVICE_1_URL
67
- ];
64
+ const urls = config.urls ?? _constants.DEFAULT_STREAM_SERVICE_URLS;
68
65
  const log = logger ?? _tslog.dummyLogger;
66
+ const createConnection = deps.createConnection ?? ((rwsConfig)=>new _resilientwebsocket.ResilientWebSocket(rwsConfig));
69
67
  const pool = new WebSocketPool(log);
70
68
  try {
71
69
  const numConnections = config.numConnections ?? DEFAULT_NUM_CONNECTIONS;
@@ -80,25 +78,24 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
80
78
  }
81
79
  for(let i = 0; i < numConnections; i++){
82
80
  const baseUrl = urls[i % urls.length];
83
- const isBrowser = (0, _index1.envIsBrowserOrWorker)();
84
- const url = isBrowser ? (0, _index1.addAuthTokenToWebSocketUrl)(baseUrl, token) : baseUrl;
85
- if (!url) {
81
+ if (!baseUrl) {
86
82
  throw new Error(`URLs must not be null or empty`);
87
83
  }
88
- const wsOptions = {
89
- ...config.rwsConfig?.wsOptions,
90
- headers: isBrowser ? undefined : {
91
- Authorization: `Bearer ${token}`
92
- }
93
- };
94
- const rws = new _resilientwebsocket.ResilientWebSocket({
84
+ const rwsInit = {
95
85
  ...config.rwsConfig,
96
- endpoint: url,
86
+ // Pool-controlled fields are set last so they always win over
87
+ // anything supplied via rwsConfig (in particular, auth can never be
88
+ // overridden here).
89
+ endpoint: baseUrl,
97
90
  logger: log,
98
- wsOptions
99
- });
91
+ protocols: [
92
+ _constants.AUTH_SUBPROTOCOL,
93
+ token
94
+ ]
95
+ };
96
+ const rws = createConnection(rwsInit);
100
97
  const connectionIndex = i;
101
- const connectionEndpoint = url;
98
+ const connectionEndpoint = baseUrl;
102
99
  // If a websocket client unexpectedly disconnects, ResilientWebSocket will reestablish
103
100
  // the connection and call the onReconnect callback.
104
101
  rws.onReconnect = ()=>{
@@ -4,7 +4,21 @@ import type { Logger } from "ts-log";
4
4
  import { IsomorphicEventEmitter } from "../emitter/index.js";
5
5
  import type { Request } from "../protocol.js";
6
6
  import type { ResilientWebSocketConfig } from "./resilient-websocket.js";
7
- import { ResilientWebSocket } from "./resilient-websocket.js";
7
+ /** The subset of `ResilientWebSocket` the pool depends on. */
8
+ export type PooledConnection = {
9
+ wsUserClosed: boolean;
10
+ onReconnect: () => void;
11
+ onTimeout: () => void;
12
+ onError: (error: ErrorEvent) => void;
13
+ onMessage: (data: WebSocket.Data) => void;
14
+ send(data: string | Buffer): void;
15
+ startWebSocket(): void;
16
+ isConnected(): boolean;
17
+ isReconnecting(): boolean;
18
+ closeWebSocket(): void;
19
+ };
20
+ /** Builds a pooled connection; overridable to inject a fake in tests. */
21
+ export type ConnectionFactory = (config: ResilientWebSocketConfig) => PooledConnection;
8
22
  type WebSocketOnMessageCallback = (data: WebSocket.Data) => Promise<void>;
9
23
  export type WebSocketPoolConfig = {
10
24
  /**
@@ -32,9 +46,10 @@ export type WebSocketPoolConfig = {
32
46
  */
33
47
  onWebSocketPoolError?: (error: Error) => void;
34
48
  /**
35
- * Additional websocket configuration
49
+ * Optional tuning for the underlying resilient WebSocket connections
50
+ * (heartbeat timeout, retry backoff, and retry-log cadence).
36
51
  */
37
- rwsConfig?: Omit<ResilientWebSocketConfig, "logger" | "endpoint">;
52
+ rwsConfig?: Omit<ResilientWebSocketConfig, "endpoint" | "logger" | "protocols">;
38
53
  /**
39
54
  * Pyth URLs to use when creating a connection
40
55
  */
@@ -46,7 +61,7 @@ export type WebSocketPoolEvents = {
46
61
  };
47
62
  export declare class WebSocketPool extends IsomorphicEventEmitter<WebSocketPoolEvents> {
48
63
  private readonly logger;
49
- rwsPool: ResilientWebSocket[];
64
+ rwsPool: PooledConnection[];
50
65
  private cache;
51
66
  private subscriptions;
52
67
  private messageListeners;
@@ -66,7 +81,9 @@ export declare class WebSocketPool extends IsomorphicEventEmitter<WebSocketPoolE
66
81
  * @param numConnections - Number of parallel WebSocket connections to maintain (default: 3)
67
82
  * @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
68
83
  */
69
- static create(config: WebSocketPoolConfig, token: string, abortSignal?: AbortSignal | null | undefined, logger?: Logger): Promise<WebSocketPool>;
84
+ static create(config: WebSocketPoolConfig, token: string, abortSignal?: AbortSignal | null | undefined, logger?: Logger, deps?: {
85
+ createConnection?: ConnectionFactory;
86
+ }): Promise<WebSocketPool>;
70
87
  private emitPoolError;
71
88
  /**
72
89
  * Checks for error responses in JSON messages and throws appropriate errors
@@ -4,7 +4,6 @@ Object.defineProperty(exports, "__esModule", {
4
4
  });
5
5
  _export_star(require("./buffer-util.cjs"), exports);
6
6
  _export_star(require("./env-util.cjs"), exports);
7
- _export_star(require("./url-util.cjs"), exports);
8
7
  function _export_star(from, to) {
9
8
  Object.keys(from).forEach(function(k) {
10
9
  if (k !== "default" && !Object.prototype.hasOwnProperty.call(to, k)) {
@@ -1,3 +1,2 @@
1
1
  export * from "./buffer-util.js";
2
2
  export * from "./env-util.js";
3
- export * from "./url-util.js";
@@ -2,5 +2,15 @@ export declare const SOLANA_LAZER_PROGRAM_ID = "pytd2yyk641x7ak7mkaasSJVXh6YYZnC
2
2
  export declare const SOLANA_LAZER_STORAGE_ID = "3rdJbqfnagQ4yx9HXJViD4zc4xpiSqmFsKpPuSCQVyQL";
3
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
- export declare const DEFAULT_STREAM_SERVICE_0_URL = "wss://pyth-lazer-0.dourolabs.app/v1/stream";
6
- export declare const DEFAULT_STREAM_SERVICE_1_URL = "wss://pyth-lazer-1.dourolabs.app/v1/stream";
5
+ /**
6
+ * Default stream endpoints, round-robined across the pool's connections when
7
+ * the caller does not supply their own `urls`. These are redundant instances
8
+ * of the Lazer api-service stream.
9
+ */
10
+ export declare const DEFAULT_STREAM_SERVICE_URLS: string[];
11
+ /**
12
+ * WebSocket subprotocol marker that carries the auth token during the handshake
13
+ * (used in every environment): clients send
14
+ * `Sec-WebSocket-Protocol: pyth-lazer-auth, <token>`.
15
+ */
16
+ export declare const AUTH_SUBPROTOCOL = "pyth-lazer-auth";
@@ -2,5 +2,17 @@ export const SOLANA_LAZER_PROGRAM_ID = "pytd2yyk641x7ak7mkaasSJVXh6YYZnC7wTmtgAy
2
2
  export const SOLANA_LAZER_STORAGE_ID = "3rdJbqfnagQ4yx9HXJViD4zc4xpiSqmFsKpPuSCQVyQL";
3
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
- export const DEFAULT_STREAM_SERVICE_0_URL = "wss://pyth-lazer-0.dourolabs.app/v1/stream";
6
- export const DEFAULT_STREAM_SERVICE_1_URL = "wss://pyth-lazer-1.dourolabs.app/v1/stream";
5
+ /**
6
+ * Default stream endpoints, round-robined across the pool's connections when
7
+ * the caller does not supply their own `urls`. These are redundant instances
8
+ * of the Lazer api-service stream.
9
+ */ export const DEFAULT_STREAM_SERVICE_URLS = [
10
+ "wss://pyth-0.dourolabs.app/v1/prices/stream",
11
+ "wss://pyth-1.dourolabs.app/v1/prices/stream",
12
+ "wss://pyth-2.dourolabs.app/v1/prices/stream"
13
+ ];
14
+ /**
15
+ * WebSocket subprotocol marker that carries the auth token during the handshake
16
+ * (used in every environment): clients send
17
+ * `Sec-WebSocket-Protocol: pyth-lazer-auth, <token>`.
18
+ */ export const AUTH_SUBPROTOCOL = "pyth-lazer-auth";
@@ -1,10 +1,10 @@
1
- import type { ClientRequestArgs } from "node:http";
2
- import type { ClientOptions, ErrorEvent } from "isomorphic-ws";
1
+ import type { ErrorEvent } from "isomorphic-ws";
3
2
  import WebSocket from "isomorphic-ws";
4
3
  import type { Logger } from "ts-log";
5
4
  export type ResilientWebSocketConfig = {
6
5
  endpoint: string;
7
- wsOptions?: ClientOptions | ClientRequestArgs | undefined;
6
+ /** WebSocket subprotocols, used to carry the auth token. */
7
+ protocols?: string[] | undefined;
8
8
  logger?: Logger;
9
9
  heartbeatTimeoutDurationMs?: number;
10
10
  maxRetryDelayMs?: number;
@@ -25,7 +25,7 @@ export type ResilientWebSocketConfig = {
25
25
  type UnsafeWebSocket = Omit<WebSocket, "terminate"> & Partial<Pick<WebSocket, "terminate">>;
26
26
  export declare class ResilientWebSocket {
27
27
  private endpoint;
28
- private wsOptions?;
28
+ private protocols?;
29
29
  private logger;
30
30
  private heartbeatTimeoutDurationMs;
31
31
  private maxRetryDelayMs;
@@ -1,13 +1,12 @@
1
1
  import WebSocket from "isomorphic-ws";
2
2
  import { dummyLogger } from "ts-log";
3
3
  import { CustomSocketClosureCodes } from "../protocol.mjs";
4
- import { envIsBrowserOrWorker } from "../util/env-util.mjs";
5
4
  const DEFAULT_HEARTBEAT_TIMEOUT_DURATION_MS = 5000; // 5 seconds
6
5
  const DEFAULT_MAX_RETRY_DELAY_MS = 1000; // 1 second'
7
6
  const DEFAULT_LOG_AFTER_RETRY_COUNT = 10;
8
7
  export class ResilientWebSocket {
9
8
  endpoint;
10
- wsOptions;
9
+ protocols;
11
10
  logger;
12
11
  heartbeatTimeoutDurationMs;
13
12
  maxRetryDelayMs;
@@ -33,7 +32,7 @@ export class ResilientWebSocket {
33
32
  onTimeout;
34
33
  constructor(config){
35
34
  this.endpoint = config.endpoint;
36
- this.wsOptions = config.wsOptions;
35
+ this.protocols = config.protocols;
37
36
  this.logger = config.logger ?? dummyLogger;
38
37
  this.heartbeatTimeoutDurationMs = config.heartbeatTimeoutDurationMs ?? DEFAULT_HEARTBEAT_TIMEOUT_DURATION_MS;
39
38
  this.maxRetryDelayMs = config.maxRetryDelayMs ?? DEFAULT_MAX_RETRY_DELAY_MS;
@@ -82,10 +81,7 @@ export class ResilientWebSocket {
82
81
  // an uncaught DOMException in the browser (or equivalent in Node, Bun or Deno)
83
82
  // and potentially blow up the process (if running in Node, Bun or Deno)
84
83
  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);
84
+ this.wsClient = new WebSocket(this.endpoint, this.protocols);
89
85
  this.wsClient.addEventListener("open", ()=>{
90
86
  this.logger.info("WebSocket connection established");
91
87
  this.wsFailedAttempts = 0;
@@ -4,7 +4,21 @@ import type { Logger } from "ts-log";
4
4
  import { IsomorphicEventEmitter } from "../emitter/index.js";
5
5
  import type { Request } from "../protocol.js";
6
6
  import type { ResilientWebSocketConfig } from "./resilient-websocket.js";
7
- import { ResilientWebSocket } from "./resilient-websocket.js";
7
+ /** The subset of `ResilientWebSocket` the pool depends on. */
8
+ export type PooledConnection = {
9
+ wsUserClosed: boolean;
10
+ onReconnect: () => void;
11
+ onTimeout: () => void;
12
+ onError: (error: ErrorEvent) => void;
13
+ onMessage: (data: WebSocket.Data) => void;
14
+ send(data: string | Buffer): void;
15
+ startWebSocket(): void;
16
+ isConnected(): boolean;
17
+ isReconnecting(): boolean;
18
+ closeWebSocket(): void;
19
+ };
20
+ /** Builds a pooled connection; overridable to inject a fake in tests. */
21
+ export type ConnectionFactory = (config: ResilientWebSocketConfig) => PooledConnection;
8
22
  type WebSocketOnMessageCallback = (data: WebSocket.Data) => Promise<void>;
9
23
  export type WebSocketPoolConfig = {
10
24
  /**
@@ -32,9 +46,10 @@ export type WebSocketPoolConfig = {
32
46
  */
33
47
  onWebSocketPoolError?: (error: Error) => void;
34
48
  /**
35
- * Additional websocket configuration
49
+ * Optional tuning for the underlying resilient WebSocket connections
50
+ * (heartbeat timeout, retry backoff, and retry-log cadence).
36
51
  */
37
- rwsConfig?: Omit<ResilientWebSocketConfig, "logger" | "endpoint">;
52
+ rwsConfig?: Omit<ResilientWebSocketConfig, "endpoint" | "logger" | "protocols">;
38
53
  /**
39
54
  * Pyth URLs to use when creating a connection
40
55
  */
@@ -46,7 +61,7 @@ export type WebSocketPoolEvents = {
46
61
  };
47
62
  export declare class WebSocketPool extends IsomorphicEventEmitter<WebSocketPoolEvents> {
48
63
  private readonly logger;
49
- rwsPool: ResilientWebSocket[];
64
+ rwsPool: PooledConnection[];
50
65
  private cache;
51
66
  private subscriptions;
52
67
  private messageListeners;
@@ -66,7 +81,9 @@ export declare class WebSocketPool extends IsomorphicEventEmitter<WebSocketPoolE
66
81
  * @param numConnections - Number of parallel WebSocket connections to maintain (default: 3)
67
82
  * @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
68
83
  */
69
- static create(config: WebSocketPoolConfig, token: string, abortSignal?: AbortSignal | null | undefined, logger?: Logger): Promise<WebSocketPool>;
84
+ static create(config: WebSocketPoolConfig, token: string, abortSignal?: AbortSignal | null | undefined, logger?: Logger, deps?: {
85
+ createConnection?: ConnectionFactory;
86
+ }): Promise<WebSocketPool>;
70
87
  private emitPoolError;
71
88
  /**
72
89
  * Checks for error responses in JSON messages and throws appropriate errors
@@ -1,8 +1,8 @@
1
1
  import { TTLCache } from "@isaacs/ttlcache";
2
2
  import { dummyLogger } from "ts-log";
3
- import { DEFAULT_STREAM_SERVICE_0_URL, DEFAULT_STREAM_SERVICE_1_URL } from "../constants.mjs";
3
+ import { AUTH_SUBPROTOCOL, DEFAULT_STREAM_SERVICE_URLS } from "../constants.mjs";
4
4
  import { IsomorphicEventEmitter } from "../emitter/index.mjs";
5
- import { addAuthTokenToWebSocketUrl, bufferFromWebsocketData, envIsBrowserOrWorker } from "../util/index.mjs";
5
+ import { bufferFromWebsocketData } from "../util/index.mjs";
6
6
  import { ResilientWebSocket } from "./resilient-websocket.mjs";
7
7
  const DEFAULT_NUM_CONNECTIONS = 4;
8
8
  export class WebSocketPool extends IsomorphicEventEmitter {
@@ -42,7 +42,7 @@ export class WebSocketPool extends IsomorphicEventEmitter {
42
42
  * @param token - Authentication token to use for the connections
43
43
  * @param numConnections - Number of parallel WebSocket connections to maintain (default: 3)
44
44
  * @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
45
- */ static async create(config, token, abortSignal, logger) {
45
+ */ static async create(config, token, abortSignal, logger, deps = {}) {
46
46
  // Helper to check if aborted and throw
47
47
  const throwIfAborted = ()=>{
48
48
  if (abortSignal?.aborted) {
@@ -51,11 +51,9 @@ export class WebSocketPool extends IsomorphicEventEmitter {
51
51
  };
52
52
  // Check before starting
53
53
  throwIfAborted();
54
- const urls = config.urls ?? [
55
- DEFAULT_STREAM_SERVICE_0_URL,
56
- DEFAULT_STREAM_SERVICE_1_URL
57
- ];
54
+ const urls = config.urls ?? DEFAULT_STREAM_SERVICE_URLS;
58
55
  const log = logger ?? dummyLogger;
56
+ const createConnection = deps.createConnection ?? ((rwsConfig)=>new ResilientWebSocket(rwsConfig));
59
57
  const pool = new WebSocketPool(log);
60
58
  try {
61
59
  const numConnections = config.numConnections ?? DEFAULT_NUM_CONNECTIONS;
@@ -70,25 +68,24 @@ export class WebSocketPool extends IsomorphicEventEmitter {
70
68
  }
71
69
  for(let i = 0; i < numConnections; i++){
72
70
  const baseUrl = urls[i % urls.length];
73
- const isBrowser = envIsBrowserOrWorker();
74
- const url = isBrowser ? addAuthTokenToWebSocketUrl(baseUrl, token) : baseUrl;
75
- if (!url) {
71
+ if (!baseUrl) {
76
72
  throw new Error(`URLs must not be null or empty`);
77
73
  }
78
- const wsOptions = {
79
- ...config.rwsConfig?.wsOptions,
80
- headers: isBrowser ? undefined : {
81
- Authorization: `Bearer ${token}`
82
- }
83
- };
84
- const rws = new ResilientWebSocket({
74
+ const rwsInit = {
85
75
  ...config.rwsConfig,
86
- endpoint: url,
76
+ // Pool-controlled fields are set last so they always win over
77
+ // anything supplied via rwsConfig (in particular, auth can never be
78
+ // overridden here).
79
+ endpoint: baseUrl,
87
80
  logger: log,
88
- wsOptions
89
- });
81
+ protocols: [
82
+ AUTH_SUBPROTOCOL,
83
+ token
84
+ ]
85
+ };
86
+ const rws = createConnection(rwsInit);
90
87
  const connectionIndex = i;
91
- const connectionEndpoint = url;
88
+ const connectionEndpoint = baseUrl;
92
89
  // If a websocket client unexpectedly disconnects, ResilientWebSocket will reestablish
93
90
  // the connection and call the onReconnect callback.
94
91
  rws.onReconnect = ()=>{
@@ -1,3 +1,2 @@
1
1
  export * from "./buffer-util.js";
2
2
  export * from "./env-util.js";
3
- export * from "./url-util.js";
@@ -1,3 +1,2 @@
1
1
  export * from "./buffer-util.mjs";
2
2
  export * from "./env-util.mjs";
3
- export * from "./url-util.mjs";
package/package.json CHANGED
@@ -120,16 +120,6 @@
120
120
  "default": "./dist/cjs/util/env-util.cjs",
121
121
  "types": "./dist/cjs/util/env-util.d.ts"
122
122
  }
123
- },
124
- "./util/url-util": {
125
- "import": {
126
- "default": "./dist/esm/util/url-util.mjs",
127
- "types": "./dist/esm/util/url-util.d.ts"
128
- },
129
- "require": {
130
- "default": "./dist/cjs/util/url-util.cjs",
131
- "types": "./dist/cjs/util/url-util.d.ts"
132
- }
133
123
  }
134
124
  },
135
125
  "files": [
@@ -164,5 +154,5 @@
164
154
  },
165
155
  "type": "module",
166
156
  "types": "./dist/cjs/index.d.ts",
167
- "version": "6.2.4"
157
+ "version": "7.0.0"
168
158
  }
@@ -1,17 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", {
3
- value: true
4
- });
5
- Object.defineProperty(exports, "addAuthTokenToWebSocketUrl", {
6
- enumerable: true,
7
- get: function() {
8
- return addAuthTokenToWebSocketUrl;
9
- }
10
- });
11
- const ACCESS_TOKEN_QUERY_PARAM_KEY = "ACCESS_TOKEN";
12
- function addAuthTokenToWebSocketUrl(baseUrl, authToken) {
13
- if (!baseUrl || !authToken) return baseUrl;
14
- const parsedUrl = new URL(baseUrl);
15
- parsedUrl.searchParams.set(ACCESS_TOKEN_QUERY_PARAM_KEY, authToken);
16
- return parsedUrl.toString();
17
- }
@@ -1,8 +0,0 @@
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;
@@ -1,8 +0,0 @@
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;
@@ -1,13 +0,0 @@
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
- */ export function addAuthTokenToWebSocketUrl(baseUrl, authToken) {
9
- if (!baseUrl || !authToken) return baseUrl;
10
- const parsedUrl = new URL(baseUrl);
11
- parsedUrl.searchParams.set(ACCESS_TOKEN_QUERY_PARAM_KEY, authToken);
12
- return parsedUrl.toString();
13
- }