@pythnetwork/pyth-lazer-sdk 0.2.0 → 0.3.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/dist/cjs/client.d.ts +16 -4
- package/dist/cjs/client.js +22 -4
- package/dist/cjs/constants.d.ts +3 -0
- package/dist/cjs/constants.js +6 -0
- package/dist/cjs/index.d.ts +1 -0
- package/dist/cjs/index.js +1 -0
- package/dist/cjs/socket/{resilient-web-socket.d.ts → resilient-websocket.d.ts} +7 -16
- package/dist/cjs/socket/{resilient-web-socket.js → resilient-websocket.js} +46 -20
- package/dist/cjs/socket/{web-socket-pool.d.ts → websocket-pool.d.ts} +10 -21
- package/dist/cjs/socket/{web-socket-pool.js → websocket-pool.js} +62 -48
- package/dist/esm/client.d.ts +16 -4
- package/dist/esm/client.js +22 -4
- package/dist/esm/constants.d.ts +3 -0
- package/dist/esm/constants.js +3 -0
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/socket/{resilient-web-socket.d.ts → resilient-websocket.d.ts} +7 -16
- package/dist/esm/socket/{resilient-web-socket.js → resilient-websocket.js} +46 -20
- package/dist/esm/socket/{web-socket-pool.d.ts → websocket-pool.d.ts} +10 -21
- package/dist/esm/socket/{web-socket-pool.js → websocket-pool.js} +61 -47
- package/package.json +2 -2
package/dist/cjs/client.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { type Logger } from "ts-log";
|
|
2
2
|
import { type ParsedPayload, type Request, type Response } from "./protocol.js";
|
|
3
|
-
import { WebSocketPool } from "./socket/web-socket-pool.js";
|
|
4
3
|
export type BinaryResponse = {
|
|
5
4
|
subscriptionId: number;
|
|
6
5
|
evm?: Buffer | undefined;
|
|
@@ -15,18 +14,31 @@ export type JsonOrBinaryResponse = {
|
|
|
15
14
|
value: BinaryResponse;
|
|
16
15
|
};
|
|
17
16
|
export declare class PythLazerClient {
|
|
18
|
-
wsp
|
|
17
|
+
private readonly wsp;
|
|
18
|
+
private constructor();
|
|
19
19
|
/**
|
|
20
20
|
* Creates a new PythLazerClient instance.
|
|
21
21
|
* @param urls - List of WebSocket URLs of the Pyth Lazer service
|
|
22
22
|
* @param token - The access token for authentication
|
|
23
|
-
* @param numConnections - The number of parallel WebSocket connections to establish (default: 3). A higher number gives a more reliable stream.
|
|
23
|
+
* @param numConnections - The number of parallel WebSocket connections to establish (default: 3). A higher number gives a more reliable stream. The connections will round-robin across the provided URLs.
|
|
24
24
|
* @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
|
|
25
25
|
*/
|
|
26
|
-
|
|
26
|
+
static create(urls: string[], token: string, numConnections?: number, logger?: Logger): Promise<PythLazerClient>;
|
|
27
|
+
/**
|
|
28
|
+
* Adds a message listener that receives either JSON or binary responses from the WebSocket connections.
|
|
29
|
+
* The listener will be called for each message received, with deduplication across redundant connections.
|
|
30
|
+
* @param handler - Callback function that receives the parsed message. The message can be either a JSON response
|
|
31
|
+
* or a binary response containing EVM, Solana, or parsed payload data.
|
|
32
|
+
*/
|
|
27
33
|
addMessageListener(handler: (event: JsonOrBinaryResponse) => void): void;
|
|
28
34
|
subscribe(request: Request): Promise<void>;
|
|
29
35
|
unsubscribe(subscriptionId: number): Promise<void>;
|
|
30
36
|
send(request: Request): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Registers a handler function that will be called whenever all WebSocket connections are down or attempting to reconnect.
|
|
39
|
+
* The connections may still try to reconnect in the background. To shut down the pool, call `shutdown()`.
|
|
40
|
+
* @param handler - Function to be called when all connections are down
|
|
41
|
+
*/
|
|
42
|
+
addAllConnectionsDownListener(handler: () => void): void;
|
|
31
43
|
shutdown(): void;
|
|
32
44
|
}
|
package/dist/cjs/client.js
CHANGED
|
@@ -3,22 +3,32 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.PythLazerClient = void 0;
|
|
4
4
|
const ts_log_1 = require("ts-log");
|
|
5
5
|
const protocol_js_1 = require("./protocol.js");
|
|
6
|
-
const
|
|
6
|
+
const websocket_pool_js_1 = require("./socket/websocket-pool.js");
|
|
7
7
|
const UINT16_NUM_BYTES = 2;
|
|
8
8
|
const UINT32_NUM_BYTES = 4;
|
|
9
9
|
const UINT64_NUM_BYTES = 8;
|
|
10
10
|
class PythLazerClient {
|
|
11
11
|
wsp;
|
|
12
|
+
constructor(wsp) {
|
|
13
|
+
this.wsp = wsp;
|
|
14
|
+
}
|
|
12
15
|
/**
|
|
13
16
|
* Creates a new PythLazerClient instance.
|
|
14
17
|
* @param urls - List of WebSocket URLs of the Pyth Lazer service
|
|
15
18
|
* @param token - The access token for authentication
|
|
16
|
-
* @param numConnections - The number of parallel WebSocket connections to establish (default: 3). A higher number gives a more reliable stream.
|
|
19
|
+
* @param numConnections - The number of parallel WebSocket connections to establish (default: 3). A higher number gives a more reliable stream. The connections will round-robin across the provided URLs.
|
|
17
20
|
* @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
|
|
18
21
|
*/
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
static async create(urls, token, numConnections = 3, logger = ts_log_1.dummyLogger) {
|
|
23
|
+
const wsp = await websocket_pool_js_1.WebSocketPool.create(urls, token, numConnections, logger);
|
|
24
|
+
return new PythLazerClient(wsp);
|
|
21
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Adds a message listener that receives either JSON or binary responses from the WebSocket connections.
|
|
28
|
+
* The listener will be called for each message received, with deduplication across redundant connections.
|
|
29
|
+
* @param handler - Callback function that receives the parsed message. The message can be either a JSON response
|
|
30
|
+
* or a binary response containing EVM, Solana, or parsed payload data.
|
|
31
|
+
*/
|
|
22
32
|
addMessageListener(handler) {
|
|
23
33
|
this.wsp.addMessageListener((data) => {
|
|
24
34
|
if (typeof data == "string") {
|
|
@@ -77,6 +87,14 @@ class PythLazerClient {
|
|
|
77
87
|
async send(request) {
|
|
78
88
|
await this.wsp.sendRequest(request);
|
|
79
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Registers a handler function that will be called whenever all WebSocket connections are down or attempting to reconnect.
|
|
92
|
+
* The connections may still try to reconnect in the background. To shut down the pool, call `shutdown()`.
|
|
93
|
+
* @param handler - Function to be called when all connections are down
|
|
94
|
+
*/
|
|
95
|
+
addAllConnectionsDownListener(handler) {
|
|
96
|
+
this.wsp.addAllConnectionsDownListener(handler);
|
|
97
|
+
}
|
|
80
98
|
shutdown() {
|
|
81
99
|
this.wsp.shutdown();
|
|
82
100
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SOLANA_STORAGE_ID = exports.SOLANA_LAZER_PROGRAM_ID = void 0;
|
|
4
|
+
const web3_js_1 = require("@solana/web3.js");
|
|
5
|
+
exports.SOLANA_LAZER_PROGRAM_ID = new web3_js_1.PublicKey("pytd2yyk641x7ak7mkaasSJVXh6YYZnC7wTmtgAyxPt");
|
|
6
|
+
exports.SOLANA_STORAGE_ID = new web3_js_1.PublicKey("3rdJbqfnagQ4yx9HXJViD4zc4xpiSqmFsKpPuSCQVyQL");
|
package/dist/cjs/index.d.ts
CHANGED
package/dist/cjs/index.js
CHANGED
|
@@ -1,16 +1,6 @@
|
|
|
1
1
|
import type { ClientRequestArgs } from "node:http";
|
|
2
2
|
import WebSocket, { type ClientOptions, type ErrorEvent } from "isomorphic-ws";
|
|
3
3
|
import type { Logger } from "ts-log";
|
|
4
|
-
/**
|
|
5
|
-
* This class wraps websocket to provide a resilient web socket client.
|
|
6
|
-
*
|
|
7
|
-
* It will reconnect if connection fails with exponential backoff. Also, it will reconnect
|
|
8
|
-
* if it receives no ping request or regular message from server within a while as indication
|
|
9
|
-
* of timeout (assuming the server sends either regularly).
|
|
10
|
-
*
|
|
11
|
-
* This class also logs events if logger is given and by replacing onError method you can handle
|
|
12
|
-
* connection errors yourself (e.g: do not retry and close the connection).
|
|
13
|
-
*/
|
|
14
4
|
export declare class ResilientWebSocket {
|
|
15
5
|
endpoint: string;
|
|
16
6
|
wsClient: undefined | WebSocket;
|
|
@@ -19,17 +9,18 @@ export declare class ResilientWebSocket {
|
|
|
19
9
|
private wsFailedAttempts;
|
|
20
10
|
private heartbeatTimeout;
|
|
21
11
|
private logger;
|
|
12
|
+
private connectionPromise;
|
|
13
|
+
private resolveConnection;
|
|
14
|
+
private rejectConnection;
|
|
15
|
+
private _isReconnecting;
|
|
16
|
+
get isReconnecting(): boolean;
|
|
17
|
+
get isConnected(): boolean;
|
|
22
18
|
onError: (error: ErrorEvent) => void;
|
|
23
19
|
onMessage: (data: WebSocket.Data) => void;
|
|
24
20
|
onReconnect: () => void;
|
|
25
21
|
constructor(endpoint: string, wsOptions?: ClientOptions | ClientRequestArgs, logger?: Logger);
|
|
26
22
|
send(data: string | Buffer): Promise<void>;
|
|
27
|
-
startWebSocket(): void
|
|
28
|
-
/**
|
|
29
|
-
* Reset the heartbeat timeout. This is called when we receive any message (ping or regular)
|
|
30
|
-
* from the server. If we don't receive any message within HEARTBEAT_TIMEOUT_DURATION,
|
|
31
|
-
* we assume the connection is dead and reconnect.
|
|
32
|
-
*/
|
|
23
|
+
startWebSocket(): Promise<void>;
|
|
33
24
|
private resetHeartbeat;
|
|
34
25
|
private waitForMaybeReadyWebSocket;
|
|
35
26
|
private handleClose;
|
|
@@ -5,18 +5,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.ResilientWebSocket = void 0;
|
|
7
7
|
const isomorphic_ws_1 = __importDefault(require("isomorphic-ws"));
|
|
8
|
-
// Reconnect with expo backoff if we don't get a message or ping for 10 seconds
|
|
9
8
|
const HEARTBEAT_TIMEOUT_DURATION = 10_000;
|
|
10
|
-
|
|
11
|
-
* This class wraps websocket to provide a resilient web socket client.
|
|
12
|
-
*
|
|
13
|
-
* It will reconnect if connection fails with exponential backoff. Also, it will reconnect
|
|
14
|
-
* if it receives no ping request or regular message from server within a while as indication
|
|
15
|
-
* of timeout (assuming the server sends either regularly).
|
|
16
|
-
*
|
|
17
|
-
* This class also logs events if logger is given and by replacing onError method you can handle
|
|
18
|
-
* connection errors yourself (e.g: do not retry and close the connection).
|
|
19
|
-
*/
|
|
9
|
+
const CONNECTION_TIMEOUT = 5000;
|
|
20
10
|
class ResilientWebSocket {
|
|
21
11
|
endpoint;
|
|
22
12
|
wsClient;
|
|
@@ -25,6 +15,16 @@ class ResilientWebSocket {
|
|
|
25
15
|
wsFailedAttempts;
|
|
26
16
|
heartbeatTimeout;
|
|
27
17
|
logger;
|
|
18
|
+
connectionPromise;
|
|
19
|
+
resolveConnection;
|
|
20
|
+
rejectConnection;
|
|
21
|
+
_isReconnecting = false;
|
|
22
|
+
get isReconnecting() {
|
|
23
|
+
return this._isReconnecting;
|
|
24
|
+
}
|
|
25
|
+
get isConnected() {
|
|
26
|
+
return this.wsClient?.readyState === isomorphic_ws_1.default.OPEN;
|
|
27
|
+
}
|
|
28
28
|
onError;
|
|
29
29
|
onMessage;
|
|
30
30
|
onReconnect;
|
|
@@ -54,41 +54,60 @@ class ResilientWebSocket {
|
|
|
54
54
|
this.wsClient.send(data);
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
-
startWebSocket() {
|
|
57
|
+
async startWebSocket() {
|
|
58
58
|
if (this.wsClient !== undefined) {
|
|
59
|
+
// If there's an existing connection attempt, wait for it
|
|
60
|
+
if (this.connectionPromise) {
|
|
61
|
+
return this.connectionPromise;
|
|
62
|
+
}
|
|
59
63
|
return;
|
|
60
64
|
}
|
|
61
65
|
this.logger?.info(`Creating Web Socket client`);
|
|
66
|
+
// Create a new promise for this connection attempt
|
|
67
|
+
this.connectionPromise = new Promise((resolve, reject) => {
|
|
68
|
+
this.resolveConnection = resolve;
|
|
69
|
+
this.rejectConnection = reject;
|
|
70
|
+
});
|
|
71
|
+
// Set a connection timeout
|
|
72
|
+
const timeoutId = setTimeout(() => {
|
|
73
|
+
if (this.rejectConnection) {
|
|
74
|
+
this.rejectConnection(new Error(`Connection timeout after ${String(CONNECTION_TIMEOUT)}ms`));
|
|
75
|
+
}
|
|
76
|
+
}, CONNECTION_TIMEOUT);
|
|
62
77
|
this.wsClient = new isomorphic_ws_1.default(this.endpoint, this.wsOptions);
|
|
63
78
|
this.wsUserClosed = false;
|
|
64
79
|
this.wsClient.addEventListener("open", () => {
|
|
65
80
|
this.wsFailedAttempts = 0;
|
|
66
81
|
this.resetHeartbeat();
|
|
82
|
+
clearTimeout(timeoutId);
|
|
83
|
+
this._isReconnecting = false;
|
|
84
|
+
this.resolveConnection?.();
|
|
67
85
|
});
|
|
68
86
|
this.wsClient.addEventListener("error", (event) => {
|
|
69
87
|
this.onError(event);
|
|
88
|
+
if (this.rejectConnection) {
|
|
89
|
+
this.rejectConnection(new Error("WebSocket connection failed"));
|
|
90
|
+
}
|
|
70
91
|
});
|
|
71
92
|
this.wsClient.addEventListener("message", (event) => {
|
|
72
93
|
this.resetHeartbeat();
|
|
73
94
|
this.onMessage(event.data);
|
|
74
95
|
});
|
|
75
96
|
this.wsClient.addEventListener("close", () => {
|
|
97
|
+
clearTimeout(timeoutId);
|
|
98
|
+
if (this.rejectConnection) {
|
|
99
|
+
this.rejectConnection(new Error("WebSocket closed before connecting"));
|
|
100
|
+
}
|
|
76
101
|
void this.handleClose();
|
|
77
102
|
});
|
|
78
|
-
// Handle ping events if supported (Node.js only)
|
|
79
103
|
if ("on" in this.wsClient) {
|
|
80
|
-
// Ping handler is undefined in browser side
|
|
81
104
|
this.wsClient.on("ping", () => {
|
|
82
105
|
this.logger?.info("Ping received");
|
|
83
106
|
this.resetHeartbeat();
|
|
84
107
|
});
|
|
85
108
|
}
|
|
109
|
+
return this.connectionPromise;
|
|
86
110
|
}
|
|
87
|
-
/**
|
|
88
|
-
* Reset the heartbeat timeout. This is called when we receive any message (ping or regular)
|
|
89
|
-
* from the server. If we don't receive any message within HEARTBEAT_TIMEOUT_DURATION,
|
|
90
|
-
* we assume the connection is dead and reconnect.
|
|
91
|
-
*/
|
|
92
111
|
resetHeartbeat() {
|
|
93
112
|
if (this.heartbeatTimeout !== undefined) {
|
|
94
113
|
clearTimeout(this.heartbeatTimeout);
|
|
@@ -123,7 +142,11 @@ class ResilientWebSocket {
|
|
|
123
142
|
else {
|
|
124
143
|
this.wsFailedAttempts += 1;
|
|
125
144
|
this.wsClient = undefined;
|
|
145
|
+
this.connectionPromise = undefined;
|
|
146
|
+
this.resolveConnection = undefined;
|
|
147
|
+
this.rejectConnection = undefined;
|
|
126
148
|
const waitTime = expoBackoff(this.wsFailedAttempts);
|
|
149
|
+
this._isReconnecting = true;
|
|
127
150
|
this.logger?.error("Connection closed unexpectedly or because of timeout. Reconnecting after " +
|
|
128
151
|
String(waitTime) +
|
|
129
152
|
"ms.");
|
|
@@ -135,7 +158,7 @@ class ResilientWebSocket {
|
|
|
135
158
|
if (this.wsUserClosed) {
|
|
136
159
|
return;
|
|
137
160
|
}
|
|
138
|
-
this.startWebSocket();
|
|
161
|
+
await this.startWebSocket();
|
|
139
162
|
await this.waitForMaybeReadyWebSocket();
|
|
140
163
|
if (this.wsClient === undefined) {
|
|
141
164
|
this.logger?.error("Couldn't reconnect to websocket. Error callback is called.");
|
|
@@ -147,6 +170,9 @@ class ResilientWebSocket {
|
|
|
147
170
|
if (this.wsClient !== undefined) {
|
|
148
171
|
const client = this.wsClient;
|
|
149
172
|
this.wsClient = undefined;
|
|
173
|
+
this.connectionPromise = undefined;
|
|
174
|
+
this.resolveConnection = undefined;
|
|
175
|
+
this.rejectConnection = undefined;
|
|
150
176
|
client.close();
|
|
151
177
|
}
|
|
152
178
|
this.wsUserClosed = true;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import WebSocket from "isomorphic-ws";
|
|
2
2
|
import { type Logger } from "ts-log";
|
|
3
|
-
import { ResilientWebSocket } from "./resilient-
|
|
3
|
+
import { ResilientWebSocket } from "./resilient-websocket.js";
|
|
4
4
|
import type { Request } from "../protocol.js";
|
|
5
5
|
export declare class WebSocketPool {
|
|
6
6
|
private readonly logger;
|
|
@@ -8,6 +8,9 @@ export declare class WebSocketPool {
|
|
|
8
8
|
private cache;
|
|
9
9
|
private subscriptions;
|
|
10
10
|
private messageListeners;
|
|
11
|
+
private allConnectionsDownListeners;
|
|
12
|
+
private wasAllDown;
|
|
13
|
+
private constructor();
|
|
11
14
|
/**
|
|
12
15
|
* Creates a new WebSocketPool instance that uses multiple redundant WebSocket connections for reliability.
|
|
13
16
|
* Usage semantics are similar to using a regular WebSocket client.
|
|
@@ -16,7 +19,7 @@ export declare class WebSocketPool {
|
|
|
16
19
|
* @param numConnections - Number of parallel WebSocket connections to maintain (default: 3)
|
|
17
20
|
* @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
|
|
18
21
|
*/
|
|
19
|
-
|
|
22
|
+
static create(urls: string[], token: string, numConnections?: number, logger?: Logger): Promise<WebSocketPool>;
|
|
20
23
|
/**
|
|
21
24
|
* Checks for error responses in JSON messages and throws appropriate errors
|
|
22
25
|
*/
|
|
@@ -26,30 +29,16 @@ export declare class WebSocketPool {
|
|
|
26
29
|
* multiple connections before forwarding to registered handlers
|
|
27
30
|
*/
|
|
28
31
|
dedupeHandler: (data: WebSocket.Data) => void;
|
|
29
|
-
/**
|
|
30
|
-
* Sends a message to all websockets in the pool
|
|
31
|
-
* @param request - The request to send
|
|
32
|
-
*/
|
|
33
32
|
sendRequest(request: Request): Promise<void>;
|
|
34
|
-
/**
|
|
35
|
-
* Adds a subscription by sending a subscribe request to all websockets in the pool
|
|
36
|
-
* and storing it for replay on reconnection
|
|
37
|
-
* @param request - The subscription request to send
|
|
38
|
-
*/
|
|
39
33
|
addSubscription(request: Request): Promise<void>;
|
|
40
|
-
/**
|
|
41
|
-
* Removes a subscription by sending an unsubscribe request to all websockets in the pool
|
|
42
|
-
* and removing it from stored subscriptions
|
|
43
|
-
* @param subscriptionId - The ID of the subscription to remove
|
|
44
|
-
*/
|
|
45
34
|
removeSubscription(subscriptionId: number): Promise<void>;
|
|
46
|
-
/**
|
|
47
|
-
* Adds a message handler function to receive websocket messages
|
|
48
|
-
* @param handler - Function that will be called with each received message
|
|
49
|
-
*/
|
|
50
35
|
addMessageListener(handler: (data: WebSocket.Data) => void): void;
|
|
51
36
|
/**
|
|
52
|
-
*
|
|
37
|
+
* Calls the handler if all websocket connections are currently down or in reconnecting state.
|
|
38
|
+
* The connections may still try to reconnect in the background.
|
|
53
39
|
*/
|
|
40
|
+
addAllConnectionsDownListener(handler: () => void): void;
|
|
41
|
+
private areAllConnectionsDown;
|
|
42
|
+
private checkConnectionStates;
|
|
54
43
|
shutdown(): void;
|
|
55
44
|
}
|
|
@@ -6,8 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.WebSocketPool = void 0;
|
|
7
7
|
const ttlcache_1 = __importDefault(require("@isaacs/ttlcache"));
|
|
8
8
|
const ts_log_1 = require("ts-log");
|
|
9
|
-
const
|
|
10
|
-
// Number of redundant parallel WebSocket connections
|
|
9
|
+
const resilient_websocket_js_1 = require("./resilient-websocket.js");
|
|
11
10
|
const DEFAULT_NUM_CONNECTIONS = 3;
|
|
12
11
|
class WebSocketPool {
|
|
13
12
|
logger;
|
|
@@ -15,6 +14,20 @@ class WebSocketPool {
|
|
|
15
14
|
cache;
|
|
16
15
|
subscriptions; // id -> subscription Request
|
|
17
16
|
messageListeners;
|
|
17
|
+
allConnectionsDownListeners;
|
|
18
|
+
wasAllDown = true;
|
|
19
|
+
constructor(logger = ts_log_1.dummyLogger) {
|
|
20
|
+
this.logger = logger;
|
|
21
|
+
this.rwsPool = [];
|
|
22
|
+
this.cache = new ttlcache_1.default({ ttl: 1000 * 10 }); // TTL of 10 seconds
|
|
23
|
+
this.subscriptions = new Map();
|
|
24
|
+
this.messageListeners = [];
|
|
25
|
+
this.allConnectionsDownListeners = [];
|
|
26
|
+
// Start monitoring connection states
|
|
27
|
+
setInterval(() => {
|
|
28
|
+
this.checkConnectionStates();
|
|
29
|
+
}, 100);
|
|
30
|
+
}
|
|
18
31
|
/**
|
|
19
32
|
* Creates a new WebSocketPool instance that uses multiple redundant WebSocket connections for reliability.
|
|
20
33
|
* Usage semantics are similar to using a regular WebSocket client.
|
|
@@ -23,18 +36,13 @@ class WebSocketPool {
|
|
|
23
36
|
* @param numConnections - Number of parallel WebSocket connections to maintain (default: 3)
|
|
24
37
|
* @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
|
|
25
38
|
*/
|
|
26
|
-
|
|
27
|
-
this.logger = logger;
|
|
39
|
+
static async create(urls, token, numConnections = DEFAULT_NUM_CONNECTIONS, logger = ts_log_1.dummyLogger) {
|
|
28
40
|
if (urls.length === 0) {
|
|
29
41
|
throw new Error("No URLs provided");
|
|
30
42
|
}
|
|
31
|
-
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
this.cache = new ttlcache_1.default({ ttl: 1000 * 10 }); // TTL of 10 seconds
|
|
35
|
-
this.rwsPool = [];
|
|
36
|
-
this.subscriptions = new Map();
|
|
37
|
-
this.messageListeners = [];
|
|
43
|
+
const pool = new WebSocketPool(logger);
|
|
44
|
+
// Create all websocket instances
|
|
45
|
+
const connectionPromises = [];
|
|
38
46
|
for (let i = 0; i < numConnections; i++) {
|
|
39
47
|
const url = urls[i % urls.length];
|
|
40
48
|
if (!url) {
|
|
@@ -45,33 +53,39 @@ class WebSocketPool {
|
|
|
45
53
|
Authorization: `Bearer ${token}`,
|
|
46
54
|
},
|
|
47
55
|
};
|
|
48
|
-
const rws = new
|
|
56
|
+
const rws = new resilient_websocket_js_1.ResilientWebSocket(url, wsOptions, logger);
|
|
49
57
|
// If a websocket client unexpectedly disconnects, ResilientWebSocket will reestablish
|
|
50
58
|
// the connection and call the onReconnect callback.
|
|
51
|
-
// When we reconnect, replay all subscription messages to resume the data stream.
|
|
52
59
|
rws.onReconnect = () => {
|
|
53
60
|
if (rws.wsUserClosed) {
|
|
54
61
|
return;
|
|
55
62
|
}
|
|
56
|
-
for (const [, request] of
|
|
63
|
+
for (const [, request] of pool.subscriptions) {
|
|
57
64
|
try {
|
|
58
65
|
void rws.send(JSON.stringify(request));
|
|
59
66
|
}
|
|
60
67
|
catch (error) {
|
|
61
|
-
|
|
68
|
+
pool.logger.error("Failed to resend subscription on reconnect:", error);
|
|
62
69
|
}
|
|
63
70
|
}
|
|
64
71
|
};
|
|
65
72
|
// Handle all client messages ourselves. Dedupe before sending to registered message handlers.
|
|
66
|
-
rws.onMessage =
|
|
67
|
-
|
|
73
|
+
rws.onMessage = pool.dedupeHandler;
|
|
74
|
+
pool.rwsPool.push(rws);
|
|
75
|
+
// Start the websocket and collect the promise
|
|
76
|
+
connectionPromises.push(rws.startWebSocket());
|
|
68
77
|
}
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
78
|
+
// Wait for all connections to be established
|
|
79
|
+
try {
|
|
80
|
+
await Promise.all(connectionPromises);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
// If any connection fails, clean up and throw
|
|
84
|
+
pool.shutdown();
|
|
85
|
+
throw error;
|
|
73
86
|
}
|
|
74
|
-
|
|
87
|
+
pool.logger.info(`Successfully established ${numConnections.toString()} redundant WebSocket connections`);
|
|
88
|
+
return pool;
|
|
75
89
|
}
|
|
76
90
|
/**
|
|
77
91
|
* Checks for error responses in JSON messages and throws appropriate errors
|
|
@@ -90,19 +104,14 @@ class WebSocketPool {
|
|
|
90
104
|
* multiple connections before forwarding to registered handlers
|
|
91
105
|
*/
|
|
92
106
|
dedupeHandler = (data) => {
|
|
93
|
-
// For string data, use the whole string as the cache key. This avoids expensive JSON parsing during deduping.
|
|
94
|
-
// For binary data, use the hex string representation as the cache key
|
|
95
107
|
const cacheKey = typeof data === "string"
|
|
96
108
|
? data
|
|
97
109
|
: Buffer.from(data).toString("hex");
|
|
98
|
-
// If we've seen this exact message recently, drop it
|
|
99
110
|
if (this.cache.has(cacheKey)) {
|
|
100
111
|
this.logger.debug("Dropping duplicate message");
|
|
101
112
|
return;
|
|
102
113
|
}
|
|
103
|
-
// Haven't seen this message, cache it and forward to handlers
|
|
104
114
|
this.cache.set(cacheKey, true);
|
|
105
|
-
// Check for errors in JSON responses
|
|
106
115
|
if (typeof data === "string") {
|
|
107
116
|
this.handleErrorMessages(data);
|
|
108
117
|
}
|
|
@@ -110,28 +119,18 @@ class WebSocketPool {
|
|
|
110
119
|
handler(data);
|
|
111
120
|
}
|
|
112
121
|
};
|
|
113
|
-
/**
|
|
114
|
-
* Sends a message to all websockets in the pool
|
|
115
|
-
* @param request - The request to send
|
|
116
|
-
*/
|
|
117
122
|
async sendRequest(request) {
|
|
118
|
-
// Send to all websockets in the pool
|
|
119
123
|
const sendPromises = this.rwsPool.map(async (rws) => {
|
|
120
124
|
try {
|
|
121
125
|
await rws.send(JSON.stringify(request));
|
|
122
126
|
}
|
|
123
127
|
catch (error) {
|
|
124
128
|
this.logger.error("Failed to send request:", error);
|
|
125
|
-
throw error;
|
|
129
|
+
throw error;
|
|
126
130
|
}
|
|
127
131
|
});
|
|
128
132
|
await Promise.all(sendPromises);
|
|
129
133
|
}
|
|
130
|
-
/**
|
|
131
|
-
* Adds a subscription by sending a subscribe request to all websockets in the pool
|
|
132
|
-
* and storing it for replay on reconnection
|
|
133
|
-
* @param request - The subscription request to send
|
|
134
|
-
*/
|
|
135
134
|
async addSubscription(request) {
|
|
136
135
|
if (request.type !== "subscribe") {
|
|
137
136
|
throw new Error("Request must be a subscribe request");
|
|
@@ -139,11 +138,6 @@ class WebSocketPool {
|
|
|
139
138
|
this.subscriptions.set(request.subscriptionId, request);
|
|
140
139
|
await this.sendRequest(request);
|
|
141
140
|
}
|
|
142
|
-
/**
|
|
143
|
-
* Removes a subscription by sending an unsubscribe request to all websockets in the pool
|
|
144
|
-
* and removing it from stored subscriptions
|
|
145
|
-
* @param subscriptionId - The ID of the subscription to remove
|
|
146
|
-
*/
|
|
147
141
|
async removeSubscription(subscriptionId) {
|
|
148
142
|
this.subscriptions.delete(subscriptionId);
|
|
149
143
|
const request = {
|
|
@@ -152,16 +146,35 @@ class WebSocketPool {
|
|
|
152
146
|
};
|
|
153
147
|
await this.sendRequest(request);
|
|
154
148
|
}
|
|
155
|
-
/**
|
|
156
|
-
* Adds a message handler function to receive websocket messages
|
|
157
|
-
* @param handler - Function that will be called with each received message
|
|
158
|
-
*/
|
|
159
149
|
addMessageListener(handler) {
|
|
160
150
|
this.messageListeners.push(handler);
|
|
161
151
|
}
|
|
162
152
|
/**
|
|
163
|
-
*
|
|
153
|
+
* Calls the handler if all websocket connections are currently down or in reconnecting state.
|
|
154
|
+
* The connections may still try to reconnect in the background.
|
|
164
155
|
*/
|
|
156
|
+
addAllConnectionsDownListener(handler) {
|
|
157
|
+
this.allConnectionsDownListeners.push(handler);
|
|
158
|
+
}
|
|
159
|
+
areAllConnectionsDown() {
|
|
160
|
+
return this.rwsPool.every((ws) => !ws.isConnected || ws.isReconnecting);
|
|
161
|
+
}
|
|
162
|
+
checkConnectionStates() {
|
|
163
|
+
const allDown = this.areAllConnectionsDown();
|
|
164
|
+
// If all connections just went down
|
|
165
|
+
if (allDown && !this.wasAllDown) {
|
|
166
|
+
this.wasAllDown = true;
|
|
167
|
+
this.logger.error("All WebSocket connections are down or reconnecting");
|
|
168
|
+
// Notify all listeners
|
|
169
|
+
for (const listener of this.allConnectionsDownListeners) {
|
|
170
|
+
listener();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// If at least one connection was restored
|
|
174
|
+
if (!allDown && this.wasAllDown) {
|
|
175
|
+
this.wasAllDown = false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
165
178
|
shutdown() {
|
|
166
179
|
for (const rws of this.rwsPool) {
|
|
167
180
|
rws.closeWebSocket();
|
|
@@ -169,6 +182,7 @@ class WebSocketPool {
|
|
|
169
182
|
this.rwsPool = [];
|
|
170
183
|
this.subscriptions.clear();
|
|
171
184
|
this.messageListeners = [];
|
|
185
|
+
this.allConnectionsDownListeners = [];
|
|
172
186
|
}
|
|
173
187
|
}
|
|
174
188
|
exports.WebSocketPool = WebSocketPool;
|
package/dist/esm/client.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { type Logger } from "ts-log";
|
|
2
2
|
import { type ParsedPayload, type Request, type Response } from "./protocol.js";
|
|
3
|
-
import { WebSocketPool } from "./socket/web-socket-pool.js";
|
|
4
3
|
export type BinaryResponse = {
|
|
5
4
|
subscriptionId: number;
|
|
6
5
|
evm?: Buffer | undefined;
|
|
@@ -15,18 +14,31 @@ export type JsonOrBinaryResponse = {
|
|
|
15
14
|
value: BinaryResponse;
|
|
16
15
|
};
|
|
17
16
|
export declare class PythLazerClient {
|
|
18
|
-
wsp
|
|
17
|
+
private readonly wsp;
|
|
18
|
+
private constructor();
|
|
19
19
|
/**
|
|
20
20
|
* Creates a new PythLazerClient instance.
|
|
21
21
|
* @param urls - List of WebSocket URLs of the Pyth Lazer service
|
|
22
22
|
* @param token - The access token for authentication
|
|
23
|
-
* @param numConnections - The number of parallel WebSocket connections to establish (default: 3). A higher number gives a more reliable stream.
|
|
23
|
+
* @param numConnections - The number of parallel WebSocket connections to establish (default: 3). A higher number gives a more reliable stream. The connections will round-robin across the provided URLs.
|
|
24
24
|
* @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
|
|
25
25
|
*/
|
|
26
|
-
|
|
26
|
+
static create(urls: string[], token: string, numConnections?: number, logger?: Logger): Promise<PythLazerClient>;
|
|
27
|
+
/**
|
|
28
|
+
* Adds a message listener that receives either JSON or binary responses from the WebSocket connections.
|
|
29
|
+
* The listener will be called for each message received, with deduplication across redundant connections.
|
|
30
|
+
* @param handler - Callback function that receives the parsed message. The message can be either a JSON response
|
|
31
|
+
* or a binary response containing EVM, Solana, or parsed payload data.
|
|
32
|
+
*/
|
|
27
33
|
addMessageListener(handler: (event: JsonOrBinaryResponse) => void): void;
|
|
28
34
|
subscribe(request: Request): Promise<void>;
|
|
29
35
|
unsubscribe(subscriptionId: number): Promise<void>;
|
|
30
36
|
send(request: Request): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Registers a handler function that will be called whenever all WebSocket connections are down or attempting to reconnect.
|
|
39
|
+
* The connections may still try to reconnect in the background. To shut down the pool, call `shutdown()`.
|
|
40
|
+
* @param handler - Function to be called when all connections are down
|
|
41
|
+
*/
|
|
42
|
+
addAllConnectionsDownListener(handler: () => void): void;
|
|
31
43
|
shutdown(): void;
|
|
32
44
|
}
|
package/dist/esm/client.js
CHANGED
|
@@ -1,22 +1,32 @@
|
|
|
1
1
|
import WebSocket from "isomorphic-ws";
|
|
2
2
|
import { dummyLogger } from "ts-log";
|
|
3
3
|
import { BINARY_UPDATE_FORMAT_MAGIC, EVM_FORMAT_MAGIC, PARSED_FORMAT_MAGIC, SOLANA_FORMAT_MAGIC_BE, } from "./protocol.js";
|
|
4
|
-
import { WebSocketPool } from "./socket/
|
|
4
|
+
import { WebSocketPool } from "./socket/websocket-pool.js";
|
|
5
5
|
const UINT16_NUM_BYTES = 2;
|
|
6
6
|
const UINT32_NUM_BYTES = 4;
|
|
7
7
|
const UINT64_NUM_BYTES = 8;
|
|
8
8
|
export class PythLazerClient {
|
|
9
9
|
wsp;
|
|
10
|
+
constructor(wsp) {
|
|
11
|
+
this.wsp = wsp;
|
|
12
|
+
}
|
|
10
13
|
/**
|
|
11
14
|
* Creates a new PythLazerClient instance.
|
|
12
15
|
* @param urls - List of WebSocket URLs of the Pyth Lazer service
|
|
13
16
|
* @param token - The access token for authentication
|
|
14
|
-
* @param numConnections - The number of parallel WebSocket connections to establish (default: 3). A higher number gives a more reliable stream.
|
|
17
|
+
* @param numConnections - The number of parallel WebSocket connections to establish (default: 3). A higher number gives a more reliable stream. The connections will round-robin across the provided URLs.
|
|
15
18
|
* @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
|
|
16
19
|
*/
|
|
17
|
-
|
|
18
|
-
|
|
20
|
+
static async create(urls, token, numConnections = 3, logger = dummyLogger) {
|
|
21
|
+
const wsp = await WebSocketPool.create(urls, token, numConnections, logger);
|
|
22
|
+
return new PythLazerClient(wsp);
|
|
19
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Adds a message listener that receives either JSON or binary responses from the WebSocket connections.
|
|
26
|
+
* The listener will be called for each message received, with deduplication across redundant connections.
|
|
27
|
+
* @param handler - Callback function that receives the parsed message. The message can be either a JSON response
|
|
28
|
+
* or a binary response containing EVM, Solana, or parsed payload data.
|
|
29
|
+
*/
|
|
20
30
|
addMessageListener(handler) {
|
|
21
31
|
this.wsp.addMessageListener((data) => {
|
|
22
32
|
if (typeof data == "string") {
|
|
@@ -75,6 +85,14 @@ export class PythLazerClient {
|
|
|
75
85
|
async send(request) {
|
|
76
86
|
await this.wsp.sendRequest(request);
|
|
77
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Registers a handler function that will be called whenever all WebSocket connections are down or attempting to reconnect.
|
|
90
|
+
* The connections may still try to reconnect in the background. To shut down the pool, call `shutdown()`.
|
|
91
|
+
* @param handler - Function to be called when all connections are down
|
|
92
|
+
*/
|
|
93
|
+
addAllConnectionsDownListener(handler) {
|
|
94
|
+
this.wsp.addAllConnectionsDownListener(handler);
|
|
95
|
+
}
|
|
78
96
|
shutdown() {
|
|
79
97
|
this.wsp.shutdown();
|
|
80
98
|
}
|
package/dist/esm/index.d.ts
CHANGED
package/dist/esm/index.js
CHANGED
|
@@ -1,16 +1,6 @@
|
|
|
1
1
|
import type { ClientRequestArgs } from "node:http";
|
|
2
2
|
import WebSocket, { type ClientOptions, type ErrorEvent } from "isomorphic-ws";
|
|
3
3
|
import type { Logger } from "ts-log";
|
|
4
|
-
/**
|
|
5
|
-
* This class wraps websocket to provide a resilient web socket client.
|
|
6
|
-
*
|
|
7
|
-
* It will reconnect if connection fails with exponential backoff. Also, it will reconnect
|
|
8
|
-
* if it receives no ping request or regular message from server within a while as indication
|
|
9
|
-
* of timeout (assuming the server sends either regularly).
|
|
10
|
-
*
|
|
11
|
-
* This class also logs events if logger is given and by replacing onError method you can handle
|
|
12
|
-
* connection errors yourself (e.g: do not retry and close the connection).
|
|
13
|
-
*/
|
|
14
4
|
export declare class ResilientWebSocket {
|
|
15
5
|
endpoint: string;
|
|
16
6
|
wsClient: undefined | WebSocket;
|
|
@@ -19,17 +9,18 @@ export declare class ResilientWebSocket {
|
|
|
19
9
|
private wsFailedAttempts;
|
|
20
10
|
private heartbeatTimeout;
|
|
21
11
|
private logger;
|
|
12
|
+
private connectionPromise;
|
|
13
|
+
private resolveConnection;
|
|
14
|
+
private rejectConnection;
|
|
15
|
+
private _isReconnecting;
|
|
16
|
+
get isReconnecting(): boolean;
|
|
17
|
+
get isConnected(): boolean;
|
|
22
18
|
onError: (error: ErrorEvent) => void;
|
|
23
19
|
onMessage: (data: WebSocket.Data) => void;
|
|
24
20
|
onReconnect: () => void;
|
|
25
21
|
constructor(endpoint: string, wsOptions?: ClientOptions | ClientRequestArgs, logger?: Logger);
|
|
26
22
|
send(data: string | Buffer): Promise<void>;
|
|
27
|
-
startWebSocket(): void
|
|
28
|
-
/**
|
|
29
|
-
* Reset the heartbeat timeout. This is called when we receive any message (ping or regular)
|
|
30
|
-
* from the server. If we don't receive any message within HEARTBEAT_TIMEOUT_DURATION,
|
|
31
|
-
* we assume the connection is dead and reconnect.
|
|
32
|
-
*/
|
|
23
|
+
startWebSocket(): Promise<void>;
|
|
33
24
|
private resetHeartbeat;
|
|
34
25
|
private waitForMaybeReadyWebSocket;
|
|
35
26
|
private handleClose;
|
|
@@ -1,16 +1,6 @@
|
|
|
1
1
|
import WebSocket, {} from "isomorphic-ws";
|
|
2
|
-
// Reconnect with expo backoff if we don't get a message or ping for 10 seconds
|
|
3
2
|
const HEARTBEAT_TIMEOUT_DURATION = 10_000;
|
|
4
|
-
|
|
5
|
-
* This class wraps websocket to provide a resilient web socket client.
|
|
6
|
-
*
|
|
7
|
-
* It will reconnect if connection fails with exponential backoff. Also, it will reconnect
|
|
8
|
-
* if it receives no ping request or regular message from server within a while as indication
|
|
9
|
-
* of timeout (assuming the server sends either regularly).
|
|
10
|
-
*
|
|
11
|
-
* This class also logs events if logger is given and by replacing onError method you can handle
|
|
12
|
-
* connection errors yourself (e.g: do not retry and close the connection).
|
|
13
|
-
*/
|
|
3
|
+
const CONNECTION_TIMEOUT = 5000;
|
|
14
4
|
export class ResilientWebSocket {
|
|
15
5
|
endpoint;
|
|
16
6
|
wsClient;
|
|
@@ -19,6 +9,16 @@ export class ResilientWebSocket {
|
|
|
19
9
|
wsFailedAttempts;
|
|
20
10
|
heartbeatTimeout;
|
|
21
11
|
logger;
|
|
12
|
+
connectionPromise;
|
|
13
|
+
resolveConnection;
|
|
14
|
+
rejectConnection;
|
|
15
|
+
_isReconnecting = false;
|
|
16
|
+
get isReconnecting() {
|
|
17
|
+
return this._isReconnecting;
|
|
18
|
+
}
|
|
19
|
+
get isConnected() {
|
|
20
|
+
return this.wsClient?.readyState === WebSocket.OPEN;
|
|
21
|
+
}
|
|
22
22
|
onError;
|
|
23
23
|
onMessage;
|
|
24
24
|
onReconnect;
|
|
@@ -48,41 +48,60 @@ export class ResilientWebSocket {
|
|
|
48
48
|
this.wsClient.send(data);
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
-
startWebSocket() {
|
|
51
|
+
async startWebSocket() {
|
|
52
52
|
if (this.wsClient !== undefined) {
|
|
53
|
+
// If there's an existing connection attempt, wait for it
|
|
54
|
+
if (this.connectionPromise) {
|
|
55
|
+
return this.connectionPromise;
|
|
56
|
+
}
|
|
53
57
|
return;
|
|
54
58
|
}
|
|
55
59
|
this.logger?.info(`Creating Web Socket client`);
|
|
60
|
+
// Create a new promise for this connection attempt
|
|
61
|
+
this.connectionPromise = new Promise((resolve, reject) => {
|
|
62
|
+
this.resolveConnection = resolve;
|
|
63
|
+
this.rejectConnection = reject;
|
|
64
|
+
});
|
|
65
|
+
// Set a connection timeout
|
|
66
|
+
const timeoutId = setTimeout(() => {
|
|
67
|
+
if (this.rejectConnection) {
|
|
68
|
+
this.rejectConnection(new Error(`Connection timeout after ${String(CONNECTION_TIMEOUT)}ms`));
|
|
69
|
+
}
|
|
70
|
+
}, CONNECTION_TIMEOUT);
|
|
56
71
|
this.wsClient = new WebSocket(this.endpoint, this.wsOptions);
|
|
57
72
|
this.wsUserClosed = false;
|
|
58
73
|
this.wsClient.addEventListener("open", () => {
|
|
59
74
|
this.wsFailedAttempts = 0;
|
|
60
75
|
this.resetHeartbeat();
|
|
76
|
+
clearTimeout(timeoutId);
|
|
77
|
+
this._isReconnecting = false;
|
|
78
|
+
this.resolveConnection?.();
|
|
61
79
|
});
|
|
62
80
|
this.wsClient.addEventListener("error", (event) => {
|
|
63
81
|
this.onError(event);
|
|
82
|
+
if (this.rejectConnection) {
|
|
83
|
+
this.rejectConnection(new Error("WebSocket connection failed"));
|
|
84
|
+
}
|
|
64
85
|
});
|
|
65
86
|
this.wsClient.addEventListener("message", (event) => {
|
|
66
87
|
this.resetHeartbeat();
|
|
67
88
|
this.onMessage(event.data);
|
|
68
89
|
});
|
|
69
90
|
this.wsClient.addEventListener("close", () => {
|
|
91
|
+
clearTimeout(timeoutId);
|
|
92
|
+
if (this.rejectConnection) {
|
|
93
|
+
this.rejectConnection(new Error("WebSocket closed before connecting"));
|
|
94
|
+
}
|
|
70
95
|
void this.handleClose();
|
|
71
96
|
});
|
|
72
|
-
// Handle ping events if supported (Node.js only)
|
|
73
97
|
if ("on" in this.wsClient) {
|
|
74
|
-
// Ping handler is undefined in browser side
|
|
75
98
|
this.wsClient.on("ping", () => {
|
|
76
99
|
this.logger?.info("Ping received");
|
|
77
100
|
this.resetHeartbeat();
|
|
78
101
|
});
|
|
79
102
|
}
|
|
103
|
+
return this.connectionPromise;
|
|
80
104
|
}
|
|
81
|
-
/**
|
|
82
|
-
* Reset the heartbeat timeout. This is called when we receive any message (ping or regular)
|
|
83
|
-
* from the server. If we don't receive any message within HEARTBEAT_TIMEOUT_DURATION,
|
|
84
|
-
* we assume the connection is dead and reconnect.
|
|
85
|
-
*/
|
|
86
105
|
resetHeartbeat() {
|
|
87
106
|
if (this.heartbeatTimeout !== undefined) {
|
|
88
107
|
clearTimeout(this.heartbeatTimeout);
|
|
@@ -117,7 +136,11 @@ export class ResilientWebSocket {
|
|
|
117
136
|
else {
|
|
118
137
|
this.wsFailedAttempts += 1;
|
|
119
138
|
this.wsClient = undefined;
|
|
139
|
+
this.connectionPromise = undefined;
|
|
140
|
+
this.resolveConnection = undefined;
|
|
141
|
+
this.rejectConnection = undefined;
|
|
120
142
|
const waitTime = expoBackoff(this.wsFailedAttempts);
|
|
143
|
+
this._isReconnecting = true;
|
|
121
144
|
this.logger?.error("Connection closed unexpectedly or because of timeout. Reconnecting after " +
|
|
122
145
|
String(waitTime) +
|
|
123
146
|
"ms.");
|
|
@@ -129,7 +152,7 @@ export class ResilientWebSocket {
|
|
|
129
152
|
if (this.wsUserClosed) {
|
|
130
153
|
return;
|
|
131
154
|
}
|
|
132
|
-
this.startWebSocket();
|
|
155
|
+
await this.startWebSocket();
|
|
133
156
|
await this.waitForMaybeReadyWebSocket();
|
|
134
157
|
if (this.wsClient === undefined) {
|
|
135
158
|
this.logger?.error("Couldn't reconnect to websocket. Error callback is called.");
|
|
@@ -141,6 +164,9 @@ export class ResilientWebSocket {
|
|
|
141
164
|
if (this.wsClient !== undefined) {
|
|
142
165
|
const client = this.wsClient;
|
|
143
166
|
this.wsClient = undefined;
|
|
167
|
+
this.connectionPromise = undefined;
|
|
168
|
+
this.resolveConnection = undefined;
|
|
169
|
+
this.rejectConnection = undefined;
|
|
144
170
|
client.close();
|
|
145
171
|
}
|
|
146
172
|
this.wsUserClosed = true;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import WebSocket from "isomorphic-ws";
|
|
2
2
|
import { type Logger } from "ts-log";
|
|
3
|
-
import { ResilientWebSocket } from "./resilient-
|
|
3
|
+
import { ResilientWebSocket } from "./resilient-websocket.js";
|
|
4
4
|
import type { Request } from "../protocol.js";
|
|
5
5
|
export declare class WebSocketPool {
|
|
6
6
|
private readonly logger;
|
|
@@ -8,6 +8,9 @@ export declare class WebSocketPool {
|
|
|
8
8
|
private cache;
|
|
9
9
|
private subscriptions;
|
|
10
10
|
private messageListeners;
|
|
11
|
+
private allConnectionsDownListeners;
|
|
12
|
+
private wasAllDown;
|
|
13
|
+
private constructor();
|
|
11
14
|
/**
|
|
12
15
|
* Creates a new WebSocketPool instance that uses multiple redundant WebSocket connections for reliability.
|
|
13
16
|
* Usage semantics are similar to using a regular WebSocket client.
|
|
@@ -16,7 +19,7 @@ export declare class WebSocketPool {
|
|
|
16
19
|
* @param numConnections - Number of parallel WebSocket connections to maintain (default: 3)
|
|
17
20
|
* @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
|
|
18
21
|
*/
|
|
19
|
-
|
|
22
|
+
static create(urls: string[], token: string, numConnections?: number, logger?: Logger): Promise<WebSocketPool>;
|
|
20
23
|
/**
|
|
21
24
|
* Checks for error responses in JSON messages and throws appropriate errors
|
|
22
25
|
*/
|
|
@@ -26,30 +29,16 @@ export declare class WebSocketPool {
|
|
|
26
29
|
* multiple connections before forwarding to registered handlers
|
|
27
30
|
*/
|
|
28
31
|
dedupeHandler: (data: WebSocket.Data) => void;
|
|
29
|
-
/**
|
|
30
|
-
* Sends a message to all websockets in the pool
|
|
31
|
-
* @param request - The request to send
|
|
32
|
-
*/
|
|
33
32
|
sendRequest(request: Request): Promise<void>;
|
|
34
|
-
/**
|
|
35
|
-
* Adds a subscription by sending a subscribe request to all websockets in the pool
|
|
36
|
-
* and storing it for replay on reconnection
|
|
37
|
-
* @param request - The subscription request to send
|
|
38
|
-
*/
|
|
39
33
|
addSubscription(request: Request): Promise<void>;
|
|
40
|
-
/**
|
|
41
|
-
* Removes a subscription by sending an unsubscribe request to all websockets in the pool
|
|
42
|
-
* and removing it from stored subscriptions
|
|
43
|
-
* @param subscriptionId - The ID of the subscription to remove
|
|
44
|
-
*/
|
|
45
34
|
removeSubscription(subscriptionId: number): Promise<void>;
|
|
46
|
-
/**
|
|
47
|
-
* Adds a message handler function to receive websocket messages
|
|
48
|
-
* @param handler - Function that will be called with each received message
|
|
49
|
-
*/
|
|
50
35
|
addMessageListener(handler: (data: WebSocket.Data) => void): void;
|
|
51
36
|
/**
|
|
52
|
-
*
|
|
37
|
+
* Calls the handler if all websocket connections are currently down or in reconnecting state.
|
|
38
|
+
* The connections may still try to reconnect in the background.
|
|
53
39
|
*/
|
|
40
|
+
addAllConnectionsDownListener(handler: () => void): void;
|
|
41
|
+
private areAllConnectionsDown;
|
|
42
|
+
private checkConnectionStates;
|
|
54
43
|
shutdown(): void;
|
|
55
44
|
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import TTLCache from "@isaacs/ttlcache";
|
|
2
2
|
import WebSocket from "isomorphic-ws";
|
|
3
3
|
import { dummyLogger } from "ts-log";
|
|
4
|
-
import { ResilientWebSocket } from "./resilient-
|
|
5
|
-
// Number of redundant parallel WebSocket connections
|
|
4
|
+
import { ResilientWebSocket } from "./resilient-websocket.js";
|
|
6
5
|
const DEFAULT_NUM_CONNECTIONS = 3;
|
|
7
6
|
export class WebSocketPool {
|
|
8
7
|
logger;
|
|
@@ -10,6 +9,20 @@ export class WebSocketPool {
|
|
|
10
9
|
cache;
|
|
11
10
|
subscriptions; // id -> subscription Request
|
|
12
11
|
messageListeners;
|
|
12
|
+
allConnectionsDownListeners;
|
|
13
|
+
wasAllDown = true;
|
|
14
|
+
constructor(logger = dummyLogger) {
|
|
15
|
+
this.logger = logger;
|
|
16
|
+
this.rwsPool = [];
|
|
17
|
+
this.cache = new TTLCache({ ttl: 1000 * 10 }); // TTL of 10 seconds
|
|
18
|
+
this.subscriptions = new Map();
|
|
19
|
+
this.messageListeners = [];
|
|
20
|
+
this.allConnectionsDownListeners = [];
|
|
21
|
+
// Start monitoring connection states
|
|
22
|
+
setInterval(() => {
|
|
23
|
+
this.checkConnectionStates();
|
|
24
|
+
}, 100);
|
|
25
|
+
}
|
|
13
26
|
/**
|
|
14
27
|
* Creates a new WebSocketPool instance that uses multiple redundant WebSocket connections for reliability.
|
|
15
28
|
* Usage semantics are similar to using a regular WebSocket client.
|
|
@@ -18,18 +31,13 @@ export class WebSocketPool {
|
|
|
18
31
|
* @param numConnections - Number of parallel WebSocket connections to maintain (default: 3)
|
|
19
32
|
* @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
|
|
20
33
|
*/
|
|
21
|
-
|
|
22
|
-
this.logger = logger;
|
|
34
|
+
static async create(urls, token, numConnections = DEFAULT_NUM_CONNECTIONS, logger = dummyLogger) {
|
|
23
35
|
if (urls.length === 0) {
|
|
24
36
|
throw new Error("No URLs provided");
|
|
25
37
|
}
|
|
26
|
-
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
this.cache = new TTLCache({ ttl: 1000 * 10 }); // TTL of 10 seconds
|
|
30
|
-
this.rwsPool = [];
|
|
31
|
-
this.subscriptions = new Map();
|
|
32
|
-
this.messageListeners = [];
|
|
38
|
+
const pool = new WebSocketPool(logger);
|
|
39
|
+
// Create all websocket instances
|
|
40
|
+
const connectionPromises = [];
|
|
33
41
|
for (let i = 0; i < numConnections; i++) {
|
|
34
42
|
const url = urls[i % urls.length];
|
|
35
43
|
if (!url) {
|
|
@@ -43,30 +51,36 @@ export class WebSocketPool {
|
|
|
43
51
|
const rws = new ResilientWebSocket(url, wsOptions, logger);
|
|
44
52
|
// If a websocket client unexpectedly disconnects, ResilientWebSocket will reestablish
|
|
45
53
|
// the connection and call the onReconnect callback.
|
|
46
|
-
// When we reconnect, replay all subscription messages to resume the data stream.
|
|
47
54
|
rws.onReconnect = () => {
|
|
48
55
|
if (rws.wsUserClosed) {
|
|
49
56
|
return;
|
|
50
57
|
}
|
|
51
|
-
for (const [, request] of
|
|
58
|
+
for (const [, request] of pool.subscriptions) {
|
|
52
59
|
try {
|
|
53
60
|
void rws.send(JSON.stringify(request));
|
|
54
61
|
}
|
|
55
62
|
catch (error) {
|
|
56
|
-
|
|
63
|
+
pool.logger.error("Failed to resend subscription on reconnect:", error);
|
|
57
64
|
}
|
|
58
65
|
}
|
|
59
66
|
};
|
|
60
67
|
// Handle all client messages ourselves. Dedupe before sending to registered message handlers.
|
|
61
|
-
rws.onMessage =
|
|
62
|
-
|
|
68
|
+
rws.onMessage = pool.dedupeHandler;
|
|
69
|
+
pool.rwsPool.push(rws);
|
|
70
|
+
// Start the websocket and collect the promise
|
|
71
|
+
connectionPromises.push(rws.startWebSocket());
|
|
63
72
|
}
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
73
|
+
// Wait for all connections to be established
|
|
74
|
+
try {
|
|
75
|
+
await Promise.all(connectionPromises);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
// If any connection fails, clean up and throw
|
|
79
|
+
pool.shutdown();
|
|
80
|
+
throw error;
|
|
68
81
|
}
|
|
69
|
-
|
|
82
|
+
pool.logger.info(`Successfully established ${numConnections.toString()} redundant WebSocket connections`);
|
|
83
|
+
return pool;
|
|
70
84
|
}
|
|
71
85
|
/**
|
|
72
86
|
* Checks for error responses in JSON messages and throws appropriate errors
|
|
@@ -85,19 +99,14 @@ export class WebSocketPool {
|
|
|
85
99
|
* multiple connections before forwarding to registered handlers
|
|
86
100
|
*/
|
|
87
101
|
dedupeHandler = (data) => {
|
|
88
|
-
// For string data, use the whole string as the cache key. This avoids expensive JSON parsing during deduping.
|
|
89
|
-
// For binary data, use the hex string representation as the cache key
|
|
90
102
|
const cacheKey = typeof data === "string"
|
|
91
103
|
? data
|
|
92
104
|
: Buffer.from(data).toString("hex");
|
|
93
|
-
// If we've seen this exact message recently, drop it
|
|
94
105
|
if (this.cache.has(cacheKey)) {
|
|
95
106
|
this.logger.debug("Dropping duplicate message");
|
|
96
107
|
return;
|
|
97
108
|
}
|
|
98
|
-
// Haven't seen this message, cache it and forward to handlers
|
|
99
109
|
this.cache.set(cacheKey, true);
|
|
100
|
-
// Check for errors in JSON responses
|
|
101
110
|
if (typeof data === "string") {
|
|
102
111
|
this.handleErrorMessages(data);
|
|
103
112
|
}
|
|
@@ -105,28 +114,18 @@ export class WebSocketPool {
|
|
|
105
114
|
handler(data);
|
|
106
115
|
}
|
|
107
116
|
};
|
|
108
|
-
/**
|
|
109
|
-
* Sends a message to all websockets in the pool
|
|
110
|
-
* @param request - The request to send
|
|
111
|
-
*/
|
|
112
117
|
async sendRequest(request) {
|
|
113
|
-
// Send to all websockets in the pool
|
|
114
118
|
const sendPromises = this.rwsPool.map(async (rws) => {
|
|
115
119
|
try {
|
|
116
120
|
await rws.send(JSON.stringify(request));
|
|
117
121
|
}
|
|
118
122
|
catch (error) {
|
|
119
123
|
this.logger.error("Failed to send request:", error);
|
|
120
|
-
throw error;
|
|
124
|
+
throw error;
|
|
121
125
|
}
|
|
122
126
|
});
|
|
123
127
|
await Promise.all(sendPromises);
|
|
124
128
|
}
|
|
125
|
-
/**
|
|
126
|
-
* Adds a subscription by sending a subscribe request to all websockets in the pool
|
|
127
|
-
* and storing it for replay on reconnection
|
|
128
|
-
* @param request - The subscription request to send
|
|
129
|
-
*/
|
|
130
129
|
async addSubscription(request) {
|
|
131
130
|
if (request.type !== "subscribe") {
|
|
132
131
|
throw new Error("Request must be a subscribe request");
|
|
@@ -134,11 +133,6 @@ export class WebSocketPool {
|
|
|
134
133
|
this.subscriptions.set(request.subscriptionId, request);
|
|
135
134
|
await this.sendRequest(request);
|
|
136
135
|
}
|
|
137
|
-
/**
|
|
138
|
-
* Removes a subscription by sending an unsubscribe request to all websockets in the pool
|
|
139
|
-
* and removing it from stored subscriptions
|
|
140
|
-
* @param subscriptionId - The ID of the subscription to remove
|
|
141
|
-
*/
|
|
142
136
|
async removeSubscription(subscriptionId) {
|
|
143
137
|
this.subscriptions.delete(subscriptionId);
|
|
144
138
|
const request = {
|
|
@@ -147,16 +141,35 @@ export class WebSocketPool {
|
|
|
147
141
|
};
|
|
148
142
|
await this.sendRequest(request);
|
|
149
143
|
}
|
|
150
|
-
/**
|
|
151
|
-
* Adds a message handler function to receive websocket messages
|
|
152
|
-
* @param handler - Function that will be called with each received message
|
|
153
|
-
*/
|
|
154
144
|
addMessageListener(handler) {
|
|
155
145
|
this.messageListeners.push(handler);
|
|
156
146
|
}
|
|
157
147
|
/**
|
|
158
|
-
*
|
|
148
|
+
* Calls the handler if all websocket connections are currently down or in reconnecting state.
|
|
149
|
+
* The connections may still try to reconnect in the background.
|
|
159
150
|
*/
|
|
151
|
+
addAllConnectionsDownListener(handler) {
|
|
152
|
+
this.allConnectionsDownListeners.push(handler);
|
|
153
|
+
}
|
|
154
|
+
areAllConnectionsDown() {
|
|
155
|
+
return this.rwsPool.every((ws) => !ws.isConnected || ws.isReconnecting);
|
|
156
|
+
}
|
|
157
|
+
checkConnectionStates() {
|
|
158
|
+
const allDown = this.areAllConnectionsDown();
|
|
159
|
+
// If all connections just went down
|
|
160
|
+
if (allDown && !this.wasAllDown) {
|
|
161
|
+
this.wasAllDown = true;
|
|
162
|
+
this.logger.error("All WebSocket connections are down or reconnecting");
|
|
163
|
+
// Notify all listeners
|
|
164
|
+
for (const listener of this.allConnectionsDownListeners) {
|
|
165
|
+
listener();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// If at least one connection was restored
|
|
169
|
+
if (!allDown && this.wasAllDown) {
|
|
170
|
+
this.wasAllDown = false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
160
173
|
shutdown() {
|
|
161
174
|
for (const rws of this.rwsPool) {
|
|
162
175
|
rws.closeWebSocket();
|
|
@@ -164,5 +177,6 @@ export class WebSocketPool {
|
|
|
164
177
|
this.rwsPool = [];
|
|
165
178
|
this.subscriptions.clear();
|
|
166
179
|
this.messageListeners = [];
|
|
180
|
+
this.allConnectionsDownListeners = [];
|
|
167
181
|
}
|
|
168
182
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pythnetwork/pyth-lazer-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Pyth Lazer SDK",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -67,5 +67,5 @@
|
|
|
67
67
|
"ts-log": "^2.2.7",
|
|
68
68
|
"ws": "^8.18.0"
|
|
69
69
|
},
|
|
70
|
-
"gitHead": "
|
|
70
|
+
"gitHead": "1c6530498edb3112be05a4f7e44a3973310df8ed"
|
|
71
71
|
}
|