@pythnetwork/pyth-lazer-sdk 0.5.1 → 2.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/dist/cjs/client.d.ts +5 -5
- package/dist/cjs/client.js +8 -9
- package/dist/cjs/constants.d.ts +2 -3
- package/dist/cjs/constants.js +3 -4
- package/dist/cjs/index.d.ts +0 -1
- package/dist/cjs/index.js +0 -1
- package/dist/cjs/socket/resilient-websocket.d.ts +34 -15
- package/dist/cjs/socket/resilient-websocket.js +93 -109
- package/dist/cjs/socket/websocket-pool.d.ts +15 -4
- package/dist/cjs/socket/websocket-pool.js +36 -37
- package/dist/esm/client.d.ts +5 -5
- package/dist/esm/client.js +8 -9
- package/dist/esm/constants.d.ts +2 -3
- package/dist/esm/constants.js +2 -3
- package/dist/esm/index.d.ts +0 -1
- package/dist/esm/index.js +0 -1
- package/dist/esm/socket/resilient-websocket.d.ts +34 -15
- package/dist/esm/socket/resilient-websocket.js +93 -109
- package/dist/esm/socket/websocket-pool.d.ts +15 -4
- package/dist/esm/socket/websocket-pool.js +36 -37
- package/package.json +1 -3
- package/dist/cjs/ed25519.d.ts +0 -2
- package/dist/cjs/ed25519.js +0 -79
- package/dist/esm/ed25519.d.ts +0 -2
- package/dist/esm/ed25519.js +0 -42
package/dist/cjs/client.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { Logger } from "ts-log";
|
|
2
1
|
import type { ParsedPayload, Request, Response } from "./protocol.js";
|
|
2
|
+
import type { WebSocketPoolConfig } from "./socket/websocket-pool.js";
|
|
3
3
|
export type BinaryResponse = {
|
|
4
4
|
subscriptionId: number;
|
|
5
5
|
evm?: Buffer | undefined;
|
|
@@ -25,7 +25,7 @@ export declare class PythLazerClient {
|
|
|
25
25
|
* @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.
|
|
26
26
|
* @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
|
|
27
27
|
*/
|
|
28
|
-
static create(
|
|
28
|
+
static create(config: WebSocketPoolConfig): Promise<PythLazerClient>;
|
|
29
29
|
/**
|
|
30
30
|
* Adds a message listener that receives either JSON or binary responses from the WebSocket connections.
|
|
31
31
|
* The listener will be called for each message received, with deduplication across redundant connections.
|
|
@@ -33,9 +33,9 @@ export declare class PythLazerClient {
|
|
|
33
33
|
* or a binary response containing EVM, Solana, or parsed payload data.
|
|
34
34
|
*/
|
|
35
35
|
addMessageListener(handler: (event: JsonOrBinaryResponse) => void): void;
|
|
36
|
-
subscribe(request: Request):
|
|
37
|
-
unsubscribe(subscriptionId: number):
|
|
38
|
-
send(request: Request):
|
|
36
|
+
subscribe(request: Request): void;
|
|
37
|
+
unsubscribe(subscriptionId: number): void;
|
|
38
|
+
send(request: Request): void;
|
|
39
39
|
/**
|
|
40
40
|
* Registers a handler function that will be called whenever all WebSocket connections are down or attempting to reconnect.
|
|
41
41
|
* The connections may still try to reconnect in the background. To shut down the pool, call `shutdown()`.
|
package/dist/cjs/client.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.PythLazerClient = void 0;
|
|
4
|
-
const ts_log_1 = require("ts-log");
|
|
5
4
|
const protocol_js_1 = require("./protocol.js");
|
|
6
5
|
const websocket_pool_js_1 = require("./socket/websocket-pool.js");
|
|
7
6
|
const UINT16_NUM_BYTES = 2;
|
|
@@ -19,8 +18,8 @@ class PythLazerClient {
|
|
|
19
18
|
* @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.
|
|
20
19
|
* @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
|
|
21
20
|
*/
|
|
22
|
-
static async create(
|
|
23
|
-
const wsp = await websocket_pool_js_1.WebSocketPool.create(
|
|
21
|
+
static async create(config) {
|
|
22
|
+
const wsp = await websocket_pool_js_1.WebSocketPool.create(config);
|
|
24
23
|
return new PythLazerClient(wsp);
|
|
25
24
|
}
|
|
26
25
|
/**
|
|
@@ -81,17 +80,17 @@ class PythLazerClient {
|
|
|
81
80
|
}
|
|
82
81
|
});
|
|
83
82
|
}
|
|
84
|
-
|
|
83
|
+
subscribe(request) {
|
|
85
84
|
if (request.type !== "subscribe") {
|
|
86
85
|
throw new Error("Request must be a subscribe request");
|
|
87
86
|
}
|
|
88
|
-
|
|
87
|
+
this.wsp.addSubscription(request);
|
|
89
88
|
}
|
|
90
|
-
|
|
91
|
-
|
|
89
|
+
unsubscribe(subscriptionId) {
|
|
90
|
+
this.wsp.removeSubscription(subscriptionId);
|
|
92
91
|
}
|
|
93
|
-
|
|
94
|
-
|
|
92
|
+
send(request) {
|
|
93
|
+
this.wsp.sendRequest(request);
|
|
95
94
|
}
|
|
96
95
|
/**
|
|
97
96
|
* Registers a handler function that will be called whenever all WebSocket connections are down or attempting to reconnect.
|
package/dist/cjs/constants.d.ts
CHANGED
|
@@ -1,3 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
export declare const
|
|
3
|
-
export declare const SOLANA_STORAGE_ID: PublicKey;
|
|
1
|
+
export declare const SOLANA_LAZER_PROGRAM_ID = "pytd2yyk641x7ak7mkaasSJVXh6YYZnC7wTmtgAyxPt";
|
|
2
|
+
export declare const SOLANA_LAZER_STORAGE_ID = "3rdJbqfnagQ4yx9HXJViD4zc4xpiSqmFsKpPuSCQVyQL";
|
package/dist/cjs/constants.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
4
|
-
|
|
5
|
-
exports.
|
|
6
|
-
exports.SOLANA_STORAGE_ID = new web3_js_1.PublicKey("3rdJbqfnagQ4yx9HXJViD4zc4xpiSqmFsKpPuSCQVyQL");
|
|
3
|
+
exports.SOLANA_LAZER_STORAGE_ID = exports.SOLANA_LAZER_PROGRAM_ID = void 0;
|
|
4
|
+
exports.SOLANA_LAZER_PROGRAM_ID = "pytd2yyk641x7ak7mkaasSJVXh6YYZnC7wTmtgAyxPt";
|
|
5
|
+
exports.SOLANA_LAZER_STORAGE_ID = "3rdJbqfnagQ4yx9HXJViD4zc4xpiSqmFsKpPuSCQVyQL";
|
package/dist/cjs/index.d.ts
CHANGED
package/dist/cjs/index.js
CHANGED
|
@@ -16,5 +16,4 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./client.js"), exports);
|
|
18
18
|
__exportStar(require("./protocol.js"), exports);
|
|
19
|
-
__exportStar(require("./ed25519.js"), exports);
|
|
20
19
|
__exportStar(require("./constants.js"), exports);
|
|
@@ -2,29 +2,48 @@ import type { ClientRequestArgs } from "node:http";
|
|
|
2
2
|
import type { ClientOptions, ErrorEvent } from "isomorphic-ws";
|
|
3
3
|
import WebSocket from "isomorphic-ws";
|
|
4
4
|
import type { Logger } from "ts-log";
|
|
5
|
-
export
|
|
5
|
+
export type ResilientWebSocketConfig = {
|
|
6
6
|
endpoint: string;
|
|
7
|
+
wsOptions?: ClientOptions | ClientRequestArgs | undefined;
|
|
8
|
+
logger?: Logger;
|
|
9
|
+
heartbeatTimeoutDurationMs?: number;
|
|
10
|
+
maxRetryDelayMs?: number;
|
|
11
|
+
logAfterRetryCount?: number;
|
|
12
|
+
};
|
|
13
|
+
export declare class ResilientWebSocket {
|
|
14
|
+
private endpoint;
|
|
15
|
+
private wsOptions?;
|
|
16
|
+
private logger;
|
|
17
|
+
private heartbeatTimeoutDurationMs;
|
|
18
|
+
private maxRetryDelayMs;
|
|
19
|
+
private logAfterRetryCount;
|
|
7
20
|
wsClient: undefined | WebSocket;
|
|
8
21
|
wsUserClosed: boolean;
|
|
9
|
-
private wsOptions;
|
|
10
22
|
private wsFailedAttempts;
|
|
11
|
-
private heartbeatTimeout
|
|
12
|
-
private
|
|
13
|
-
private connectionPromise;
|
|
14
|
-
private resolveConnection;
|
|
15
|
-
private rejectConnection;
|
|
23
|
+
private heartbeatTimeout?;
|
|
24
|
+
private retryTimeout?;
|
|
16
25
|
private _isReconnecting;
|
|
17
|
-
|
|
18
|
-
|
|
26
|
+
isReconnecting(): boolean;
|
|
27
|
+
isConnected(): this is this & {
|
|
28
|
+
wsClient: WebSocket;
|
|
29
|
+
};
|
|
30
|
+
private shouldLogRetry;
|
|
19
31
|
onError: (error: ErrorEvent) => void;
|
|
20
32
|
onMessage: (data: WebSocket.Data) => void;
|
|
21
33
|
onReconnect: () => void;
|
|
22
|
-
constructor(
|
|
23
|
-
send(data: string | Buffer):
|
|
24
|
-
startWebSocket():
|
|
34
|
+
constructor(config: ResilientWebSocketConfig);
|
|
35
|
+
send(data: string | Buffer): void;
|
|
36
|
+
startWebSocket(): void;
|
|
25
37
|
private resetHeartbeat;
|
|
26
|
-
private
|
|
27
|
-
private handleClose;
|
|
28
|
-
private restartUnexpectedClosedWebsocket;
|
|
38
|
+
private handleReconnect;
|
|
29
39
|
closeWebSocket(): void;
|
|
40
|
+
/**
|
|
41
|
+
* Calculates the delay in milliseconds for exponential backoff based on the number of failed attempts.
|
|
42
|
+
*
|
|
43
|
+
* The delay increases exponentially with each attempt, starting at 20ms for the first attempt,
|
|
44
|
+
* and is capped at maxRetryDelayMs for attempts greater than or equal to 10.
|
|
45
|
+
*
|
|
46
|
+
* @returns The calculated delay in milliseconds before the next retry.
|
|
47
|
+
*/
|
|
48
|
+
private retryDelayMs;
|
|
30
49
|
}
|
|
@@ -5,38 +5,49 @@ 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
|
-
const
|
|
9
|
-
const
|
|
8
|
+
const ts_log_1 = require("ts-log");
|
|
9
|
+
const DEFAULT_HEARTBEAT_TIMEOUT_DURATION_MS = 5000; // 5 seconds
|
|
10
|
+
const DEFAULT_MAX_RETRY_DELAY_MS = 1000; // 1 second'
|
|
11
|
+
const DEFAULT_LOG_AFTER_RETRY_COUNT = 10;
|
|
10
12
|
class ResilientWebSocket {
|
|
11
13
|
endpoint;
|
|
12
|
-
wsClient;
|
|
13
|
-
wsUserClosed;
|
|
14
14
|
wsOptions;
|
|
15
|
+
logger;
|
|
16
|
+
heartbeatTimeoutDurationMs;
|
|
17
|
+
maxRetryDelayMs;
|
|
18
|
+
logAfterRetryCount;
|
|
19
|
+
wsClient;
|
|
20
|
+
wsUserClosed = false;
|
|
15
21
|
wsFailedAttempts;
|
|
16
22
|
heartbeatTimeout;
|
|
17
|
-
|
|
18
|
-
connectionPromise;
|
|
19
|
-
resolveConnection;
|
|
20
|
-
rejectConnection;
|
|
23
|
+
retryTimeout;
|
|
21
24
|
_isReconnecting = false;
|
|
22
|
-
|
|
25
|
+
isReconnecting() {
|
|
23
26
|
return this._isReconnecting;
|
|
24
27
|
}
|
|
25
|
-
|
|
28
|
+
isConnected() {
|
|
26
29
|
return this.wsClient?.readyState === isomorphic_ws_1.default.OPEN;
|
|
27
30
|
}
|
|
31
|
+
shouldLogRetry() {
|
|
32
|
+
return this.wsFailedAttempts % this.logAfterRetryCount === 0;
|
|
33
|
+
}
|
|
28
34
|
onError;
|
|
29
35
|
onMessage;
|
|
30
36
|
onReconnect;
|
|
31
|
-
constructor(
|
|
32
|
-
this.endpoint = endpoint;
|
|
33
|
-
this.wsOptions = wsOptions;
|
|
34
|
-
this.logger = logger;
|
|
37
|
+
constructor(config) {
|
|
38
|
+
this.endpoint = config.endpoint;
|
|
39
|
+
this.wsOptions = config.wsOptions;
|
|
40
|
+
this.logger = config.logger ?? ts_log_1.dummyLogger;
|
|
41
|
+
this.heartbeatTimeoutDurationMs =
|
|
42
|
+
config.heartbeatTimeoutDurationMs ??
|
|
43
|
+
DEFAULT_HEARTBEAT_TIMEOUT_DURATION_MS;
|
|
44
|
+
this.maxRetryDelayMs = config.maxRetryDelayMs ?? DEFAULT_MAX_RETRY_DELAY_MS;
|
|
45
|
+
this.logAfterRetryCount =
|
|
46
|
+
config.logAfterRetryCount ?? DEFAULT_LOG_AFTER_RETRY_COUNT;
|
|
35
47
|
this.wsFailedAttempts = 0;
|
|
36
48
|
this.onError = (error) => {
|
|
37
|
-
|
|
49
|
+
void error;
|
|
38
50
|
};
|
|
39
|
-
this.wsUserClosed = true;
|
|
40
51
|
this.onMessage = (data) => {
|
|
41
52
|
void data;
|
|
42
53
|
};
|
|
@@ -44,144 +55,117 @@ class ResilientWebSocket {
|
|
|
44
55
|
// Empty function, can be set by the user.
|
|
45
56
|
};
|
|
46
57
|
}
|
|
47
|
-
|
|
48
|
-
this.logger
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
this.logger?.error("Couldn't connect to the websocket server. Error callback is called.");
|
|
58
|
+
send(data) {
|
|
59
|
+
this.logger.debug(`Sending message`);
|
|
60
|
+
if (this.isConnected()) {
|
|
61
|
+
this.wsClient.send(data);
|
|
52
62
|
}
|
|
53
63
|
else {
|
|
54
|
-
this.
|
|
64
|
+
this.logger.warn(`WebSocket to ${this.endpoint} is not connected. Cannot send message.`);
|
|
55
65
|
}
|
|
56
66
|
}
|
|
57
|
-
|
|
67
|
+
startWebSocket() {
|
|
68
|
+
if (this.wsUserClosed) {
|
|
69
|
+
this.logger.error("Connection was explicitly closed by user. Will not reconnect.");
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
58
72
|
if (this.wsClient !== undefined) {
|
|
59
|
-
|
|
60
|
-
if (this.connectionPromise) {
|
|
61
|
-
return this.connectionPromise;
|
|
62
|
-
}
|
|
73
|
+
this.logger.info("WebSocket client already started.");
|
|
63
74
|
return;
|
|
64
75
|
}
|
|
65
|
-
this.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
this.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const timeoutId = setTimeout(() => {
|
|
73
|
-
if (this.rejectConnection) {
|
|
74
|
-
this.rejectConnection(new Error(`Connection timeout after ${String(CONNECTION_TIMEOUT)}ms`));
|
|
75
|
-
}
|
|
76
|
-
}, CONNECTION_TIMEOUT);
|
|
76
|
+
if (this.wsFailedAttempts == 0) {
|
|
77
|
+
this.logger.info(`Creating Web Socket client`);
|
|
78
|
+
}
|
|
79
|
+
if (this.retryTimeout !== undefined) {
|
|
80
|
+
clearTimeout(this.retryTimeout);
|
|
81
|
+
this.retryTimeout = undefined;
|
|
82
|
+
}
|
|
77
83
|
this.wsClient = new isomorphic_ws_1.default(this.endpoint, this.wsOptions);
|
|
78
|
-
this.wsUserClosed = false;
|
|
79
84
|
this.wsClient.addEventListener("open", () => {
|
|
85
|
+
this.logger.info("WebSocket connection established");
|
|
80
86
|
this.wsFailedAttempts = 0;
|
|
81
|
-
this.resetHeartbeat();
|
|
82
|
-
clearTimeout(timeoutId);
|
|
83
87
|
this._isReconnecting = false;
|
|
84
|
-
this.
|
|
88
|
+
this.resetHeartbeat();
|
|
89
|
+
this.onReconnect();
|
|
90
|
+
});
|
|
91
|
+
this.wsClient.addEventListener("close", (e) => {
|
|
92
|
+
if (this.wsUserClosed) {
|
|
93
|
+
this.logger.info(`WebSocket connection to ${this.endpoint} closed by user`);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
if (this.shouldLogRetry()) {
|
|
97
|
+
this.logger.warn(`WebSocket connection to ${this.endpoint} closed unexpectedly: Code: ${e.code.toString()}`);
|
|
98
|
+
}
|
|
99
|
+
this.handleReconnect();
|
|
100
|
+
}
|
|
85
101
|
});
|
|
86
102
|
this.wsClient.addEventListener("error", (event) => {
|
|
87
103
|
this.onError(event);
|
|
88
|
-
if (this.rejectConnection) {
|
|
89
|
-
this.rejectConnection(new Error("WebSocket connection failed"));
|
|
90
|
-
}
|
|
91
104
|
});
|
|
92
105
|
this.wsClient.addEventListener("message", (event) => {
|
|
93
106
|
this.resetHeartbeat();
|
|
94
107
|
this.onMessage(event.data);
|
|
95
108
|
});
|
|
96
|
-
this.wsClient.addEventListener("close", () => {
|
|
97
|
-
clearTimeout(timeoutId);
|
|
98
|
-
if (this.rejectConnection) {
|
|
99
|
-
this.rejectConnection(new Error("WebSocket closed before connecting"));
|
|
100
|
-
}
|
|
101
|
-
void this.handleClose();
|
|
102
|
-
});
|
|
103
109
|
if ("on" in this.wsClient) {
|
|
104
110
|
this.wsClient.on("ping", () => {
|
|
105
|
-
this.logger
|
|
111
|
+
this.logger.info("Ping received");
|
|
106
112
|
this.resetHeartbeat();
|
|
107
113
|
});
|
|
108
114
|
}
|
|
109
|
-
return this.connectionPromise;
|
|
110
115
|
}
|
|
111
116
|
resetHeartbeat() {
|
|
112
117
|
if (this.heartbeatTimeout !== undefined) {
|
|
113
118
|
clearTimeout(this.heartbeatTimeout);
|
|
114
119
|
}
|
|
115
120
|
this.heartbeatTimeout = setTimeout(() => {
|
|
116
|
-
this.logger
|
|
121
|
+
this.logger.warn("Connection timed out. Reconnecting...");
|
|
117
122
|
this.wsClient?.terminate();
|
|
118
|
-
|
|
119
|
-
},
|
|
123
|
+
this.handleReconnect();
|
|
124
|
+
}, this.heartbeatTimeoutDurationMs);
|
|
120
125
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (waitedTime > 5000) {
|
|
126
|
-
this.wsClient.close();
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
else {
|
|
130
|
-
waitedTime += 10;
|
|
131
|
-
await sleep(10);
|
|
132
|
-
}
|
|
126
|
+
handleReconnect() {
|
|
127
|
+
if (this.wsUserClosed) {
|
|
128
|
+
this.logger.info("WebSocket connection closed by user, not reconnecting.");
|
|
129
|
+
return;
|
|
133
130
|
}
|
|
134
|
-
}
|
|
135
|
-
async handleClose() {
|
|
136
131
|
if (this.heartbeatTimeout !== undefined) {
|
|
137
132
|
clearTimeout(this.heartbeatTimeout);
|
|
138
133
|
}
|
|
139
|
-
if (this.
|
|
140
|
-
this.
|
|
134
|
+
if (this.retryTimeout !== undefined) {
|
|
135
|
+
clearTimeout(this.retryTimeout);
|
|
141
136
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
this.
|
|
147
|
-
|
|
148
|
-
const waitTime = expoBackoff(this.wsFailedAttempts);
|
|
149
|
-
this._isReconnecting = true;
|
|
150
|
-
this.logger?.error("Connection closed unexpectedly or because of timeout. Reconnecting after " +
|
|
151
|
-
String(waitTime) +
|
|
137
|
+
this.wsFailedAttempts += 1;
|
|
138
|
+
this.wsClient = undefined;
|
|
139
|
+
this._isReconnecting = true;
|
|
140
|
+
if (this.shouldLogRetry()) {
|
|
141
|
+
this.logger.error("Connection closed unexpectedly or because of timeout. Reconnecting after " +
|
|
142
|
+
String(this.retryDelayMs()) +
|
|
152
143
|
"ms.");
|
|
153
|
-
await sleep(waitTime);
|
|
154
|
-
await this.restartUnexpectedClosedWebsocket();
|
|
155
144
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
await this.startWebSocket();
|
|
162
|
-
await this.waitForMaybeReadyWebSocket();
|
|
163
|
-
if (this.wsClient === undefined) {
|
|
164
|
-
this.logger?.error("Couldn't reconnect to websocket. Error callback is called.");
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
this.onReconnect();
|
|
145
|
+
this.retryTimeout = setTimeout(() => {
|
|
146
|
+
this.startWebSocket();
|
|
147
|
+
}, this.retryDelayMs());
|
|
168
148
|
}
|
|
169
149
|
closeWebSocket() {
|
|
170
150
|
if (this.wsClient !== undefined) {
|
|
171
|
-
|
|
151
|
+
this.wsClient.close();
|
|
172
152
|
this.wsClient = undefined;
|
|
173
|
-
this.connectionPromise = undefined;
|
|
174
|
-
this.resolveConnection = undefined;
|
|
175
|
-
this.rejectConnection = undefined;
|
|
176
|
-
client.close();
|
|
177
153
|
}
|
|
178
154
|
this.wsUserClosed = true;
|
|
179
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* Calculates the delay in milliseconds for exponential backoff based on the number of failed attempts.
|
|
158
|
+
*
|
|
159
|
+
* The delay increases exponentially with each attempt, starting at 20ms for the first attempt,
|
|
160
|
+
* and is capped at maxRetryDelayMs for attempts greater than or equal to 10.
|
|
161
|
+
*
|
|
162
|
+
* @returns The calculated delay in milliseconds before the next retry.
|
|
163
|
+
*/
|
|
164
|
+
retryDelayMs() {
|
|
165
|
+
if (this.wsFailedAttempts >= 10) {
|
|
166
|
+
return this.maxRetryDelayMs;
|
|
167
|
+
}
|
|
168
|
+
return Math.min(2 ** this.wsFailedAttempts * 10, this.maxRetryDelayMs);
|
|
169
|
+
}
|
|
180
170
|
}
|
|
181
171
|
exports.ResilientWebSocket = ResilientWebSocket;
|
|
182
|
-
async function sleep(ms) {
|
|
183
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
184
|
-
}
|
|
185
|
-
function expoBackoff(attempts) {
|
|
186
|
-
return 2 ** attempts * 100;
|
|
187
|
-
}
|
|
@@ -1,7 +1,17 @@
|
|
|
1
|
+
import type { ErrorEvent } from "isomorphic-ws";
|
|
1
2
|
import WebSocket from "isomorphic-ws";
|
|
2
3
|
import type { Logger } from "ts-log";
|
|
3
4
|
import type { Request } from "../protocol.js";
|
|
5
|
+
import type { ResilientWebSocketConfig } from "./resilient-websocket.js";
|
|
4
6
|
import { ResilientWebSocket } from "./resilient-websocket.js";
|
|
7
|
+
export type WebSocketPoolConfig = {
|
|
8
|
+
urls: string[];
|
|
9
|
+
token: string;
|
|
10
|
+
numConnections?: number;
|
|
11
|
+
logger?: Logger;
|
|
12
|
+
rwsConfig?: Omit<ResilientWebSocketConfig, "logger" | "endpoint">;
|
|
13
|
+
onError?: (error: ErrorEvent) => void;
|
|
14
|
+
};
|
|
5
15
|
export declare class WebSocketPool {
|
|
6
16
|
private readonly logger;
|
|
7
17
|
rwsPool: ResilientWebSocket[];
|
|
@@ -20,7 +30,7 @@ export declare class WebSocketPool {
|
|
|
20
30
|
* @param numConnections - Number of parallel WebSocket connections to maintain (default: 3)
|
|
21
31
|
* @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
|
|
22
32
|
*/
|
|
23
|
-
static create(
|
|
33
|
+
static create(config: WebSocketPoolConfig): Promise<WebSocketPool>;
|
|
24
34
|
/**
|
|
25
35
|
* Checks for error responses in JSON messages and throws appropriate errors
|
|
26
36
|
*/
|
|
@@ -30,9 +40,9 @@ export declare class WebSocketPool {
|
|
|
30
40
|
* multiple connections before forwarding to registered handlers
|
|
31
41
|
*/
|
|
32
42
|
dedupeHandler: (data: WebSocket.Data) => void;
|
|
33
|
-
sendRequest(request: Request):
|
|
34
|
-
addSubscription(request: Request):
|
|
35
|
-
removeSubscription(subscriptionId: number):
|
|
43
|
+
sendRequest(request: Request): void;
|
|
44
|
+
addSubscription(request: Request): void;
|
|
45
|
+
removeSubscription(subscriptionId: number): void;
|
|
36
46
|
addMessageListener(handler: (data: WebSocket.Data) => void): void;
|
|
37
47
|
/**
|
|
38
48
|
* Calls the handler if all websocket connections are currently down or in reconnecting state.
|
|
@@ -40,6 +50,7 @@ export declare class WebSocketPool {
|
|
|
40
50
|
*/
|
|
41
51
|
addAllConnectionsDownListener(handler: () => void): void;
|
|
42
52
|
private areAllConnectionsDown;
|
|
53
|
+
private isAnyConnectionEstablished;
|
|
43
54
|
private checkConnectionStates;
|
|
44
55
|
shutdown(): void;
|
|
45
56
|
}
|
|
@@ -7,7 +7,7 @@ exports.WebSocketPool = void 0;
|
|
|
7
7
|
const ttlcache_1 = __importDefault(require("@isaacs/ttlcache"));
|
|
8
8
|
const ts_log_1 = require("ts-log");
|
|
9
9
|
const resilient_websocket_js_1 = require("./resilient-websocket.js");
|
|
10
|
-
const DEFAULT_NUM_CONNECTIONS =
|
|
10
|
+
const DEFAULT_NUM_CONNECTIONS = 4;
|
|
11
11
|
class WebSocketPool {
|
|
12
12
|
logger;
|
|
13
13
|
rwsPool;
|
|
@@ -17,7 +17,7 @@ class WebSocketPool {
|
|
|
17
17
|
allConnectionsDownListeners;
|
|
18
18
|
wasAllDown = true;
|
|
19
19
|
checkConnectionStatesInterval;
|
|
20
|
-
constructor(logger
|
|
20
|
+
constructor(logger) {
|
|
21
21
|
this.logger = logger;
|
|
22
22
|
this.rwsPool = [];
|
|
23
23
|
this.cache = new ttlcache_1.default({ ttl: 1000 * 10 }); // TTL of 10 seconds
|
|
@@ -37,24 +37,30 @@ class WebSocketPool {
|
|
|
37
37
|
* @param numConnections - Number of parallel WebSocket connections to maintain (default: 3)
|
|
38
38
|
* @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
|
|
39
39
|
*/
|
|
40
|
-
static async create(
|
|
41
|
-
if (urls.length === 0) {
|
|
40
|
+
static async create(config) {
|
|
41
|
+
if (config.urls.length === 0) {
|
|
42
42
|
throw new Error("No URLs provided");
|
|
43
43
|
}
|
|
44
|
+
const logger = config.logger ?? ts_log_1.dummyLogger;
|
|
44
45
|
const pool = new WebSocketPool(logger);
|
|
45
|
-
|
|
46
|
-
const connectionPromises = [];
|
|
46
|
+
const numConnections = config.numConnections ?? DEFAULT_NUM_CONNECTIONS;
|
|
47
47
|
for (let i = 0; i < numConnections; i++) {
|
|
48
|
-
const url = urls[i % urls.length];
|
|
48
|
+
const url = config.urls[i % config.urls.length];
|
|
49
49
|
if (!url) {
|
|
50
50
|
throw new Error(`URLs must not be null or empty`);
|
|
51
51
|
}
|
|
52
52
|
const wsOptions = {
|
|
53
|
+
...config.rwsConfig?.wsOptions,
|
|
53
54
|
headers: {
|
|
54
|
-
Authorization: `Bearer ${token}`,
|
|
55
|
+
Authorization: `Bearer ${config.token}`,
|
|
55
56
|
},
|
|
56
57
|
};
|
|
57
|
-
const rws = new resilient_websocket_js_1.ResilientWebSocket(
|
|
58
|
+
const rws = new resilient_websocket_js_1.ResilientWebSocket({
|
|
59
|
+
...config.rwsConfig,
|
|
60
|
+
endpoint: url,
|
|
61
|
+
wsOptions,
|
|
62
|
+
logger,
|
|
63
|
+
});
|
|
58
64
|
// If a websocket client unexpectedly disconnects, ResilientWebSocket will reestablish
|
|
59
65
|
// the connection and call the onReconnect callback.
|
|
60
66
|
rws.onReconnect = () => {
|
|
@@ -63,29 +69,26 @@ class WebSocketPool {
|
|
|
63
69
|
}
|
|
64
70
|
for (const [, request] of pool.subscriptions) {
|
|
65
71
|
try {
|
|
66
|
-
|
|
72
|
+
rws.send(JSON.stringify(request));
|
|
67
73
|
}
|
|
68
74
|
catch (error) {
|
|
69
75
|
pool.logger.error("Failed to resend subscription on reconnect:", error);
|
|
70
76
|
}
|
|
71
77
|
}
|
|
72
78
|
};
|
|
79
|
+
if (config.onError) {
|
|
80
|
+
rws.onError = config.onError;
|
|
81
|
+
}
|
|
73
82
|
// Handle all client messages ourselves. Dedupe before sending to registered message handlers.
|
|
74
83
|
rws.onMessage = pool.dedupeHandler;
|
|
75
84
|
pool.rwsPool.push(rws);
|
|
76
|
-
|
|
77
|
-
connectionPromises.push(rws.startWebSocket());
|
|
78
|
-
}
|
|
79
|
-
// Wait for all connections to be established
|
|
80
|
-
try {
|
|
81
|
-
await Promise.all(connectionPromises);
|
|
85
|
+
rws.startWebSocket();
|
|
82
86
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
throw error;
|
|
87
|
+
pool.logger.info(`Started WebSocketPool with ${numConnections.toString()} connections. Waiting for at least one to connect...`);
|
|
88
|
+
while (!pool.isAnyConnectionEstablished()) {
|
|
89
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
87
90
|
}
|
|
88
|
-
pool.logger.info(`
|
|
91
|
+
pool.logger.info(`At least one WebSocket connection is established. WebSocketPool is ready.`);
|
|
89
92
|
return pool;
|
|
90
93
|
}
|
|
91
94
|
/**
|
|
@@ -120,32 +123,25 @@ class WebSocketPool {
|
|
|
120
123
|
handler(data);
|
|
121
124
|
}
|
|
122
125
|
};
|
|
123
|
-
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
catch (error) {
|
|
129
|
-
this.logger.error("Failed to send request:", error);
|
|
130
|
-
throw error;
|
|
131
|
-
}
|
|
132
|
-
});
|
|
133
|
-
await Promise.all(sendPromises);
|
|
126
|
+
sendRequest(request) {
|
|
127
|
+
for (const rws of this.rwsPool) {
|
|
128
|
+
rws.send(JSON.stringify(request));
|
|
129
|
+
}
|
|
134
130
|
}
|
|
135
|
-
|
|
131
|
+
addSubscription(request) {
|
|
136
132
|
if (request.type !== "subscribe") {
|
|
137
133
|
throw new Error("Request must be a subscribe request");
|
|
138
134
|
}
|
|
139
135
|
this.subscriptions.set(request.subscriptionId, request);
|
|
140
|
-
|
|
136
|
+
this.sendRequest(request);
|
|
141
137
|
}
|
|
142
|
-
|
|
138
|
+
removeSubscription(subscriptionId) {
|
|
143
139
|
this.subscriptions.delete(subscriptionId);
|
|
144
140
|
const request = {
|
|
145
141
|
type: "unsubscribe",
|
|
146
142
|
subscriptionId,
|
|
147
143
|
};
|
|
148
|
-
|
|
144
|
+
this.sendRequest(request);
|
|
149
145
|
}
|
|
150
146
|
addMessageListener(handler) {
|
|
151
147
|
this.messageListeners.push(handler);
|
|
@@ -158,7 +154,10 @@ class WebSocketPool {
|
|
|
158
154
|
this.allConnectionsDownListeners.push(handler);
|
|
159
155
|
}
|
|
160
156
|
areAllConnectionsDown() {
|
|
161
|
-
return this.rwsPool.every((ws) => !ws.isConnected || ws.isReconnecting);
|
|
157
|
+
return this.rwsPool.every((ws) => !ws.isConnected() || ws.isReconnecting());
|
|
158
|
+
}
|
|
159
|
+
isAnyConnectionEstablished() {
|
|
160
|
+
return this.rwsPool.some((ws) => ws.isConnected());
|
|
162
161
|
}
|
|
163
162
|
checkConnectionStates() {
|
|
164
163
|
const allDown = this.areAllConnectionsDown();
|