@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 +50 -27
- package/dist/cjs/constants.cjs +11 -7
- package/dist/cjs/constants.d.ts +12 -2
- package/dist/cjs/socket/resilient-websocket.cjs +3 -7
- package/dist/cjs/socket/resilient-websocket.d.ts +4 -4
- package/dist/cjs/socket/websocket-pool.cjs +16 -19
- package/dist/cjs/socket/websocket-pool.d.ts +22 -5
- package/dist/cjs/util/index.cjs +0 -1
- package/dist/cjs/util/index.d.ts +0 -1
- package/dist/esm/constants.d.ts +12 -2
- package/dist/esm/constants.mjs +14 -2
- package/dist/esm/socket/resilient-websocket.d.ts +4 -4
- package/dist/esm/socket/resilient-websocket.mjs +3 -7
- package/dist/esm/socket/websocket-pool.d.ts +22 -5
- package/dist/esm/socket/websocket-pool.mjs +18 -21
- package/dist/esm/util/index.d.ts +0 -1
- package/dist/esm/util/index.mjs +0 -1
- package/package.json +1 -11
- package/dist/cjs/util/url-util.cjs +0 -17
- package/dist/cjs/util/url-util.d.ts +0 -8
- package/dist/esm/util/url-util.d.ts +0 -8
- package/dist/esm/util/url-util.mjs +0 -13
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
|
|
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, //
|
|
18
|
+
logger: console, // Optional. Customize where connection logs are routed
|
|
15
19
|
webSocketPoolConfig: {
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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, //
|
|
23
|
-
maxRetryDelayMs: 1000, //
|
|
24
|
-
logAfterRetryCount: 10, //
|
|
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
|
-
|
|
45
|
+
client.addMessageListener((message) => {
|
|
30
46
|
console.info("received the following from the Lazer stream:", message);
|
|
31
47
|
});
|
|
32
48
|
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
|
|
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
|
-
//
|
|
40
|
-
|
|
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: [
|
|
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
|
package/dist/cjs/constants.cjs
CHANGED
|
@@ -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
|
|
19
|
-
return
|
|
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
|
|
36
|
-
|
|
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";
|
package/dist/cjs/constants.d.ts
CHANGED
|
@@ -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
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
99
|
-
|
|
91
|
+
protocols: [
|
|
92
|
+
_constants.AUTH_SUBPROTOCOL,
|
|
93
|
+
token
|
|
94
|
+
]
|
|
95
|
+
};
|
|
96
|
+
const rws = createConnection(rwsInit);
|
|
100
97
|
const connectionIndex = i;
|
|
101
|
-
const connectionEndpoint =
|
|
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
|
-
|
|
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
|
-
*
|
|
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" | "
|
|
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:
|
|
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
|
|
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
|
package/dist/cjs/util/index.cjs
CHANGED
|
@@ -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)) {
|
package/dist/cjs/util/index.d.ts
CHANGED
package/dist/esm/constants.d.ts
CHANGED
|
@@ -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
|
-
|
|
6
|
-
|
|
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";
|
package/dist/esm/constants.mjs
CHANGED
|
@@ -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
|
-
|
|
6
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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" | "
|
|
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:
|
|
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
|
|
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 {
|
|
3
|
+
import { AUTH_SUBPROTOCOL, DEFAULT_STREAM_SERVICE_URLS } from "../constants.mjs";
|
|
4
4
|
import { IsomorphicEventEmitter } from "../emitter/index.mjs";
|
|
5
|
-
import {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
89
|
-
|
|
81
|
+
protocols: [
|
|
82
|
+
AUTH_SUBPROTOCOL,
|
|
83
|
+
token
|
|
84
|
+
]
|
|
85
|
+
};
|
|
86
|
+
const rws = createConnection(rwsInit);
|
|
90
87
|
const connectionIndex = i;
|
|
91
|
-
const connectionEndpoint =
|
|
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 = ()=>{
|
package/dist/esm/util/index.d.ts
CHANGED
package/dist/esm/util/index.mjs
CHANGED
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": "
|
|
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
|
-
}
|