@pythnetwork/pyth-lazer-sdk 5.0.0 → 5.2.1
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.js → client.cjs} +93 -98
- package/dist/cjs/constants.cjs +36 -0
- package/dist/cjs/emitter/index.cjs +53 -0
- package/dist/cjs/emitter/index.d.ts +29 -0
- package/dist/cjs/index.cjs +20 -0
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/protocol.cjs +33 -0
- package/dist/cjs/protocol.d.ts +1 -1
- package/dist/cjs/socket/{resilient-websocket.js → resilient-websocket.cjs} +47 -48
- package/dist/cjs/socket/websocket-pool.cjs +253 -0
- package/dist/cjs/socket/websocket-pool.d.ts +37 -3
- package/dist/cjs/util/{buffer-util.js → buffer-util.cjs} +14 -14
- package/dist/cjs/util/env-util.cjs +33 -0
- package/dist/cjs/util/index.cjs +20 -0
- package/dist/cjs/util/url-util.cjs +17 -0
- package/dist/esm/{client.js → client.mjs} +76 -88
- package/dist/esm/emitter/index.d.ts +29 -0
- package/dist/esm/emitter/index.mjs +43 -0
- package/dist/esm/index.mjs +3 -0
- package/dist/esm/package.json +1 -1
- package/dist/esm/protocol.d.ts +1 -1
- package/dist/esm/{protocol.js → protocol.mjs} +4 -4
- package/dist/esm/socket/{resilient-websocket.js → resilient-websocket.mjs} +27 -36
- package/dist/esm/socket/websocket-pool.d.ts +37 -3
- package/dist/esm/socket/websocket-pool.mjs +238 -0
- package/dist/esm/util/{buffer-util.js → buffer-util.mjs} +3 -6
- package/dist/esm/util/{env-util.js → env-util.mjs} +4 -8
- package/dist/esm/util/index.mjs +3 -0
- package/dist/esm/util/{url-util.js → url-util.mjs} +2 -4
- package/package.json +119 -15
- package/dist/cjs/constants.js +0 -9
- package/dist/cjs/index.js +0 -19
- package/dist/cjs/protocol.js +0 -15
- package/dist/cjs/socket/websocket-pool.js +0 -201
- package/dist/cjs/util/env-util.js +0 -32
- package/dist/cjs/util/index.js +0 -19
- package/dist/cjs/util/url-util.js +0 -18
- package/dist/esm/index.js +0 -3
- package/dist/esm/socket/websocket-pool.js +0 -195
- package/dist/esm/util/index.js +0 -3
- /package/dist/esm/{constants.js → constants.mjs} +0 -0
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
Object.defineProperty(exports, "ResilientWebSocket", {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
get: function() {
|
|
8
|
+
return ResilientWebSocket;
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
const _isomorphicws = /*#__PURE__*/ _interop_require_default(require("isomorphic-ws"));
|
|
12
|
+
const _tslog = require("ts-log");
|
|
13
|
+
const _protocol = require("../protocol.cjs");
|
|
14
|
+
const _envutil = require("../util/env-util.cjs");
|
|
15
|
+
function _interop_require_default(obj) {
|
|
16
|
+
return obj && obj.__esModule ? obj : {
|
|
17
|
+
default: obj
|
|
18
|
+
};
|
|
19
|
+
}
|
|
11
20
|
const DEFAULT_HEARTBEAT_TIMEOUT_DURATION_MS = 5000; // 5 seconds
|
|
12
21
|
const DEFAULT_MAX_RETRY_DELAY_MS = 1000; // 1 second'
|
|
13
22
|
const DEFAULT_LOG_AFTER_RETRY_COUNT = 10;
|
|
@@ -28,7 +37,7 @@ class ResilientWebSocket {
|
|
|
28
37
|
return this._isReconnecting;
|
|
29
38
|
}
|
|
30
39
|
isConnected() {
|
|
31
|
-
return this.wsClient?.readyState ===
|
|
40
|
+
return this.wsClient?.readyState === _isomorphicws.default.OPEN;
|
|
32
41
|
}
|
|
33
42
|
shouldLogRetry() {
|
|
34
43
|
return this.wsFailedAttempts % this.logAfterRetryCount === 0;
|
|
@@ -36,33 +45,29 @@ class ResilientWebSocket {
|
|
|
36
45
|
onError;
|
|
37
46
|
onMessage;
|
|
38
47
|
onReconnect;
|
|
39
|
-
constructor(config)
|
|
48
|
+
constructor(config){
|
|
40
49
|
this.endpoint = config.endpoint;
|
|
41
50
|
this.wsOptions = config.wsOptions;
|
|
42
|
-
this.logger = config.logger ??
|
|
43
|
-
this.heartbeatTimeoutDurationMs =
|
|
44
|
-
config.heartbeatTimeoutDurationMs ??
|
|
45
|
-
DEFAULT_HEARTBEAT_TIMEOUT_DURATION_MS;
|
|
51
|
+
this.logger = config.logger ?? _tslog.dummyLogger;
|
|
52
|
+
this.heartbeatTimeoutDurationMs = config.heartbeatTimeoutDurationMs ?? DEFAULT_HEARTBEAT_TIMEOUT_DURATION_MS;
|
|
46
53
|
this.maxRetryDelayMs = config.maxRetryDelayMs ?? DEFAULT_MAX_RETRY_DELAY_MS;
|
|
47
|
-
this.logAfterRetryCount =
|
|
48
|
-
config.logAfterRetryCount ?? DEFAULT_LOG_AFTER_RETRY_COUNT;
|
|
54
|
+
this.logAfterRetryCount = config.logAfterRetryCount ?? DEFAULT_LOG_AFTER_RETRY_COUNT;
|
|
49
55
|
this.wsFailedAttempts = 0;
|
|
50
|
-
this.onError = (error)
|
|
56
|
+
this.onError = (error)=>{
|
|
51
57
|
void error;
|
|
52
58
|
};
|
|
53
|
-
this.onMessage = (data)
|
|
59
|
+
this.onMessage = (data)=>{
|
|
54
60
|
void data;
|
|
55
61
|
};
|
|
56
|
-
this.onReconnect = ()
|
|
57
|
-
|
|
62
|
+
this.onReconnect = ()=>{
|
|
63
|
+
// Empty function, can be set by the user.
|
|
58
64
|
};
|
|
59
65
|
}
|
|
60
66
|
send(data) {
|
|
61
67
|
this.logger.debug(`Sending message`);
|
|
62
68
|
if (this.isConnected()) {
|
|
63
69
|
this.wsClient.send(data);
|
|
64
|
-
}
|
|
65
|
-
else {
|
|
70
|
+
} else {
|
|
66
71
|
this.logger.warn(`WebSocket to ${this.endpoint} is not connected. Cannot send message.`);
|
|
67
72
|
}
|
|
68
73
|
}
|
|
@@ -85,34 +90,33 @@ class ResilientWebSocket {
|
|
|
85
90
|
// browser constructor supports a different 2nd argument for the constructor,
|
|
86
91
|
// so we need to ensure it's not included if we're running in that environment:
|
|
87
92
|
// https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket#protocols
|
|
88
|
-
this.wsClient = new
|
|
89
|
-
this.wsClient.addEventListener("open", ()
|
|
93
|
+
this.wsClient = new _isomorphicws.default(this.endpoint, (0, _envutil.envIsBrowserOrWorker)() ? undefined : this.wsOptions);
|
|
94
|
+
this.wsClient.addEventListener("open", ()=>{
|
|
90
95
|
this.logger.info("WebSocket connection established");
|
|
91
96
|
this.wsFailedAttempts = 0;
|
|
92
97
|
this._isReconnecting = false;
|
|
93
98
|
this.resetHeartbeat();
|
|
94
99
|
this.onReconnect();
|
|
95
100
|
});
|
|
96
|
-
this.wsClient.addEventListener("close", (e)
|
|
101
|
+
this.wsClient.addEventListener("close", (e)=>{
|
|
97
102
|
if (this.wsUserClosed) {
|
|
98
103
|
this.logger.info(`WebSocket connection to ${this.endpoint} closed by user`);
|
|
99
|
-
}
|
|
100
|
-
else {
|
|
104
|
+
} else {
|
|
101
105
|
if (this.shouldLogRetry()) {
|
|
102
106
|
this.logger.warn(`WebSocket connection to ${this.endpoint} closed unexpectedly: Code: ${e.code.toString()}`);
|
|
103
107
|
}
|
|
104
108
|
this.handleReconnect();
|
|
105
109
|
}
|
|
106
110
|
});
|
|
107
|
-
this.wsClient.addEventListener("error", (event)
|
|
111
|
+
this.wsClient.addEventListener("error", (event)=>{
|
|
108
112
|
this.onError(event);
|
|
109
113
|
});
|
|
110
|
-
this.wsClient.addEventListener("message", (event)
|
|
114
|
+
this.wsClient.addEventListener("message", (event)=>{
|
|
111
115
|
this.resetHeartbeat();
|
|
112
116
|
this.onMessage(event.data);
|
|
113
117
|
});
|
|
114
118
|
if ("on" in this.wsClient) {
|
|
115
|
-
this.wsClient.on("ping", ()
|
|
119
|
+
this.wsClient.on("ping", ()=>{
|
|
116
120
|
this.logger.info("Ping received");
|
|
117
121
|
this.resetHeartbeat();
|
|
118
122
|
});
|
|
@@ -122,18 +126,17 @@ class ResilientWebSocket {
|
|
|
122
126
|
if (this.heartbeatTimeout !== undefined) {
|
|
123
127
|
clearTimeout(this.heartbeatTimeout);
|
|
124
128
|
}
|
|
125
|
-
this.heartbeatTimeout = setTimeout(()
|
|
129
|
+
this.heartbeatTimeout = setTimeout(()=>{
|
|
126
130
|
const warnMsg = "Connection timed out. Reconnecting...";
|
|
127
131
|
this.logger.warn(warnMsg);
|
|
128
132
|
if (this.wsClient) {
|
|
129
133
|
if (typeof this.wsClient.terminate === "function") {
|
|
130
134
|
this.wsClient.terminate();
|
|
131
|
-
}
|
|
132
|
-
else {
|
|
135
|
+
} else {
|
|
133
136
|
// terminate is an implementation detail of the node-friendly
|
|
134
137
|
// https://www.npmjs.com/package/ws package, but is not a native WebSocket API,
|
|
135
138
|
// so we have to use the close method
|
|
136
|
-
this.wsClient.close(
|
|
139
|
+
this.wsClient.close(_protocol.CustomSocketClosureCodes.CLIENT_TIMEOUT_BUT_RECONNECTING, warnMsg);
|
|
137
140
|
}
|
|
138
141
|
}
|
|
139
142
|
this.handleReconnect();
|
|
@@ -154,11 +157,9 @@ class ResilientWebSocket {
|
|
|
154
157
|
this.wsClient = undefined;
|
|
155
158
|
this._isReconnecting = true;
|
|
156
159
|
if (this.shouldLogRetry()) {
|
|
157
|
-
this.logger.error("Connection closed unexpectedly or because of timeout. Reconnecting after " +
|
|
158
|
-
String(this.retryDelayMs()) +
|
|
159
|
-
"ms.");
|
|
160
|
+
this.logger.error("Connection closed unexpectedly or because of timeout. Reconnecting after " + String(this.retryDelayMs()) + "ms.");
|
|
160
161
|
}
|
|
161
|
-
this.retryTimeout = setTimeout(()
|
|
162
|
+
this.retryTimeout = setTimeout(()=>{
|
|
162
163
|
this.startWebSocket();
|
|
163
164
|
}, this.retryDelayMs());
|
|
164
165
|
}
|
|
@@ -170,18 +171,16 @@ class ResilientWebSocket {
|
|
|
170
171
|
this.wsUserClosed = true;
|
|
171
172
|
}
|
|
172
173
|
/**
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
retryDelayMs() {
|
|
174
|
+
* Calculates the delay in milliseconds for exponential backoff based on the number of failed attempts.
|
|
175
|
+
*
|
|
176
|
+
* The delay increases exponentially with each attempt, starting at 20ms for the first attempt,
|
|
177
|
+
* and is capped at maxRetryDelayMs for attempts greater than or equal to 10.
|
|
178
|
+
*
|
|
179
|
+
* @returns The calculated delay in milliseconds before the next retry.
|
|
180
|
+
*/ retryDelayMs() {
|
|
181
181
|
if (this.wsFailedAttempts >= 10) {
|
|
182
182
|
return this.maxRetryDelayMs;
|
|
183
183
|
}
|
|
184
184
|
return Math.min(2 ** this.wsFailedAttempts * 10, this.maxRetryDelayMs);
|
|
185
185
|
}
|
|
186
186
|
}
|
|
187
|
-
exports.ResilientWebSocket = ResilientWebSocket;
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
Object.defineProperty(exports, "WebSocketPool", {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
get: function() {
|
|
8
|
+
return WebSocketPool;
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
const _ttlcache = /*#__PURE__*/ _interop_require_default(require("@isaacs/ttlcache"));
|
|
12
|
+
const _tslog = require("ts-log");
|
|
13
|
+
const _resilientwebsocket = require("./resilient-websocket.cjs");
|
|
14
|
+
const _constants = require("../constants.cjs");
|
|
15
|
+
const _index = require("../emitter/index.cjs");
|
|
16
|
+
const _index1 = require("../util/index.cjs");
|
|
17
|
+
function _interop_require_default(obj) {
|
|
18
|
+
return obj && obj.__esModule ? obj : {
|
|
19
|
+
default: obj
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const DEFAULT_NUM_CONNECTIONS = 4;
|
|
23
|
+
class WebSocketPool extends _index.IsomorphicEventEmitter {
|
|
24
|
+
logger;
|
|
25
|
+
rwsPool;
|
|
26
|
+
cache;
|
|
27
|
+
subscriptions;
|
|
28
|
+
messageListeners;
|
|
29
|
+
allConnectionsDownListeners;
|
|
30
|
+
wasAllDown = true;
|
|
31
|
+
checkConnectionStatesInterval;
|
|
32
|
+
constructor(logger){
|
|
33
|
+
super(), this.logger = logger;
|
|
34
|
+
this.rwsPool = [];
|
|
35
|
+
this.cache = new _ttlcache.default({
|
|
36
|
+
ttl: 1000 * 10
|
|
37
|
+
}); // TTL of 10 seconds
|
|
38
|
+
this.subscriptions = new Map();
|
|
39
|
+
this.messageListeners = [];
|
|
40
|
+
this.allConnectionsDownListeners = [];
|
|
41
|
+
// Start monitoring connection states
|
|
42
|
+
this.checkConnectionStatesInterval = setInterval(()=>{
|
|
43
|
+
this.checkConnectionStates();
|
|
44
|
+
}, 100);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Creates a new WebSocketPool instance that uses multiple redundant WebSocket connections for reliability.
|
|
48
|
+
* Usage semantics are similar to using a regular WebSocket client.
|
|
49
|
+
* @param urls - List of WebSocket URLs to connect to
|
|
50
|
+
* @param token - Authentication token to use for the connections
|
|
51
|
+
* @param numConnections - Number of parallel WebSocket connections to maintain (default: 3)
|
|
52
|
+
* @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
|
|
53
|
+
*/ static async create(config, token, logger) {
|
|
54
|
+
const urls = config.urls ?? [
|
|
55
|
+
_constants.DEFAULT_STREAM_SERVICE_0_URL,
|
|
56
|
+
_constants.DEFAULT_STREAM_SERVICE_1_URL
|
|
57
|
+
];
|
|
58
|
+
const log = logger ?? _tslog.dummyLogger;
|
|
59
|
+
const pool = new WebSocketPool(log);
|
|
60
|
+
const numConnections = config.numConnections ?? DEFAULT_NUM_CONNECTIONS;
|
|
61
|
+
// bind a handler to capture any emitted errors and send them to the user-provided
|
|
62
|
+
// onWebSocketPoolError callback (if it is present)
|
|
63
|
+
if (typeof config.onWebSocketPoolError === "function") {
|
|
64
|
+
pool.on("error", config.onWebSocketPoolError);
|
|
65
|
+
pool.once("shutdown", ()=>{
|
|
66
|
+
// unbind all error handlers so we don't leak memory
|
|
67
|
+
pool.off("error");
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
for(let i = 0; i < numConnections; i++){
|
|
71
|
+
const baseUrl = urls[i % urls.length];
|
|
72
|
+
const isBrowser = (0, _index1.envIsBrowserOrWorker)();
|
|
73
|
+
const url = isBrowser ? (0, _index1.addAuthTokenToWebSocketUrl)(baseUrl, token) : baseUrl;
|
|
74
|
+
if (!url) {
|
|
75
|
+
throw new Error(`URLs must not be null or empty`);
|
|
76
|
+
}
|
|
77
|
+
const wsOptions = {
|
|
78
|
+
...config.rwsConfig?.wsOptions,
|
|
79
|
+
headers: isBrowser ? undefined : {
|
|
80
|
+
Authorization: `Bearer ${token}`
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
const rws = new _resilientwebsocket.ResilientWebSocket({
|
|
84
|
+
...config.rwsConfig,
|
|
85
|
+
endpoint: url,
|
|
86
|
+
wsOptions,
|
|
87
|
+
logger: log
|
|
88
|
+
});
|
|
89
|
+
// If a websocket client unexpectedly disconnects, ResilientWebSocket will reestablish
|
|
90
|
+
// the connection and call the onReconnect callback.
|
|
91
|
+
rws.onReconnect = ()=>{
|
|
92
|
+
if (rws.wsUserClosed) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
for (const [, request] of pool.subscriptions){
|
|
96
|
+
try {
|
|
97
|
+
rws.send(JSON.stringify(request));
|
|
98
|
+
} catch (error) {
|
|
99
|
+
pool.logger.error("Failed to resend subscription on reconnect:", error);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
104
|
+
const onErrorHandler = config.onWebSocketError ?? config.onError;
|
|
105
|
+
if (typeof onErrorHandler === "function") {
|
|
106
|
+
rws.onError = onErrorHandler;
|
|
107
|
+
}
|
|
108
|
+
// Handle all client messages ourselves. Dedupe before sending to registered message handlers.
|
|
109
|
+
rws.onMessage = (data)=>{
|
|
110
|
+
pool.dedupeHandler(data).catch((error)=>{
|
|
111
|
+
pool.emitPoolError(error, "Error in WebSocketPool dedupeHandler");
|
|
112
|
+
});
|
|
113
|
+
};
|
|
114
|
+
pool.rwsPool.push(rws);
|
|
115
|
+
rws.startWebSocket();
|
|
116
|
+
}
|
|
117
|
+
pool.logger.info(`Started WebSocketPool with ${numConnections.toString()} connections. Waiting for at least one to connect...`);
|
|
118
|
+
while(!pool.isAnyConnectionEstablished()){
|
|
119
|
+
await new Promise((resolve)=>setTimeout(resolve, 100));
|
|
120
|
+
}
|
|
121
|
+
pool.logger.info(`At least one WebSocket connection is established. WebSocketPool is ready.`);
|
|
122
|
+
return pool;
|
|
123
|
+
}
|
|
124
|
+
emitPoolError(error, context) {
|
|
125
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
126
|
+
if (context) {
|
|
127
|
+
this.logger.error(context, err);
|
|
128
|
+
} else {
|
|
129
|
+
this.logger.error("WebSocketPool error", err);
|
|
130
|
+
}
|
|
131
|
+
for (const errHandler of this.getListeners("error")){
|
|
132
|
+
try {
|
|
133
|
+
errHandler(err);
|
|
134
|
+
} catch (handlerError) {
|
|
135
|
+
this.logger.error("WebSocketPool error handler threw", handlerError);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Checks for error responses in JSON messages and throws appropriate errors
|
|
141
|
+
*/ handleErrorMessages(data) {
|
|
142
|
+
const message = JSON.parse(data);
|
|
143
|
+
if (message.type === "subscriptionError") {
|
|
144
|
+
throw new Error(`Error occurred for subscription ID ${String(message.subscriptionId)}: ${message.error}`);
|
|
145
|
+
} else if (message.type === "error") {
|
|
146
|
+
throw new Error(`Error: ${message.error}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
async constructCacheKeyFromWebsocketData(data) {
|
|
150
|
+
if (typeof data === "string") return data;
|
|
151
|
+
const buff = await (0, _index1.bufferFromWebsocketData)(data);
|
|
152
|
+
return buff.toString("hex");
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Handles incoming websocket messages by deduplicating identical messages received across
|
|
156
|
+
* multiple connections before forwarding to registered handlers
|
|
157
|
+
*/ dedupeHandler = async (data)=>{
|
|
158
|
+
const cacheKey = await this.constructCacheKeyFromWebsocketData(data);
|
|
159
|
+
if (this.cache.has(cacheKey)) {
|
|
160
|
+
this.logger.debug("Dropping duplicate message");
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
this.cache.set(cacheKey, true);
|
|
164
|
+
if (typeof data === "string") {
|
|
165
|
+
this.handleErrorMessages(data);
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
await Promise.all(this.messageListeners.map((handler)=>handler(data)));
|
|
169
|
+
} catch (error) {
|
|
170
|
+
this.emitPoolError(error, "Error in WebSocketPool message handler");
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
sendRequest(request) {
|
|
174
|
+
for (const rws of this.rwsPool){
|
|
175
|
+
try {
|
|
176
|
+
rws.send(JSON.stringify(request));
|
|
177
|
+
} catch (error) {
|
|
178
|
+
this.emitPoolError(error, "Failed to send WebSocket request");
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
addSubscription(request) {
|
|
183
|
+
if (request.type !== "subscribe") {
|
|
184
|
+
this.emitPoolError(new Error("Request must be a subscribe request"));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
this.subscriptions.set(request.subscriptionId, request);
|
|
188
|
+
this.sendRequest(request);
|
|
189
|
+
}
|
|
190
|
+
removeSubscription(subscriptionId) {
|
|
191
|
+
this.subscriptions.delete(subscriptionId);
|
|
192
|
+
const request = {
|
|
193
|
+
type: "unsubscribe",
|
|
194
|
+
subscriptionId
|
|
195
|
+
};
|
|
196
|
+
this.sendRequest(request);
|
|
197
|
+
}
|
|
198
|
+
addMessageListener(handler) {
|
|
199
|
+
this.messageListeners.push(handler);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Calls the handler if all websocket connections are currently down or in reconnecting state.
|
|
203
|
+
* The connections may still try to reconnect in the background.
|
|
204
|
+
*/ addAllConnectionsDownListener(handler) {
|
|
205
|
+
this.allConnectionsDownListeners.push(handler);
|
|
206
|
+
}
|
|
207
|
+
areAllConnectionsDown() {
|
|
208
|
+
return this.rwsPool.every((ws)=>!ws.isConnected() || ws.isReconnecting());
|
|
209
|
+
}
|
|
210
|
+
isAnyConnectionEstablished() {
|
|
211
|
+
return this.rwsPool.some((ws)=>ws.isConnected());
|
|
212
|
+
}
|
|
213
|
+
checkConnectionStates() {
|
|
214
|
+
const allDown = this.areAllConnectionsDown();
|
|
215
|
+
// If all connections just went down
|
|
216
|
+
if (allDown && !this.wasAllDown) {
|
|
217
|
+
this.wasAllDown = true;
|
|
218
|
+
this.logger.error("All WebSocket connections are down or reconnecting");
|
|
219
|
+
// Notify all listeners
|
|
220
|
+
for (const listener of this.allConnectionsDownListeners){
|
|
221
|
+
try {
|
|
222
|
+
listener();
|
|
223
|
+
} catch (error) {
|
|
224
|
+
this.emitPoolError(error, "All-connections-down listener threw");
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// If at least one connection was restored
|
|
229
|
+
if (!allDown && this.wasAllDown) {
|
|
230
|
+
this.wasAllDown = false;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
shutdown() {
|
|
234
|
+
for (const rws of this.rwsPool){
|
|
235
|
+
rws.closeWebSocket();
|
|
236
|
+
}
|
|
237
|
+
this.rwsPool = [];
|
|
238
|
+
this.subscriptions.clear();
|
|
239
|
+
this.messageListeners = [];
|
|
240
|
+
this.allConnectionsDownListeners = [];
|
|
241
|
+
clearInterval(this.checkConnectionStatesInterval);
|
|
242
|
+
// execute all bound shutdown handlers
|
|
243
|
+
for (const shutdownHandler of this.getListeners("shutdown")){
|
|
244
|
+
try {
|
|
245
|
+
shutdownHandler();
|
|
246
|
+
} catch (error) {
|
|
247
|
+
this.emitPoolError(error, "Shutdown handler threw");
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
// ensure any error handlers are removed to avoid leaks
|
|
251
|
+
this.off("error");
|
|
252
|
+
}
|
|
253
|
+
}
|
|
@@ -4,14 +4,47 @@ import type { Logger } from "ts-log";
|
|
|
4
4
|
import type { Request } from "../protocol.js";
|
|
5
5
|
import type { ResilientWebSocketConfig } from "./resilient-websocket.js";
|
|
6
6
|
import { ResilientWebSocket } from "./resilient-websocket.js";
|
|
7
|
+
import { IsomorphicEventEmitter } from "../emitter/index.js";
|
|
7
8
|
type WebSocketOnMessageCallback = (data: WebSocket.Data) => void | Promise<void>;
|
|
8
9
|
export type WebSocketPoolConfig = {
|
|
9
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Maximum number of open, parallel websocket connections
|
|
12
|
+
* @defaultValue 3
|
|
13
|
+
*/
|
|
10
14
|
numConnections?: number;
|
|
11
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Callback that will be executed whenever an error
|
|
17
|
+
* message or an error event occurs on an individual WebSocket connection
|
|
18
|
+
*
|
|
19
|
+
* @deprecated use onWebSocketError() instead
|
|
20
|
+
*/
|
|
12
21
|
onError?: (error: ErrorEvent) => void;
|
|
22
|
+
/**
|
|
23
|
+
* Callback that will be executed whenever an error
|
|
24
|
+
* message or an error event occurs on an individual WebSocket connection
|
|
25
|
+
*/
|
|
26
|
+
onWebSocketError?: (error: ErrorEvent) => void;
|
|
27
|
+
/**
|
|
28
|
+
* Callback that will be executed whenever an error occurs
|
|
29
|
+
* directly within the WebSocket pool. These can typically
|
|
30
|
+
* be errors that would normally manifest as "unhandledRejection" or "uncaughtException"
|
|
31
|
+
* errors.
|
|
32
|
+
*/
|
|
33
|
+
onWebSocketPoolError?: (error: Error) => void;
|
|
34
|
+
/**
|
|
35
|
+
* Additional websocket configuration
|
|
36
|
+
*/
|
|
37
|
+
rwsConfig?: Omit<ResilientWebSocketConfig, "logger" | "endpoint">;
|
|
38
|
+
/**
|
|
39
|
+
* Pyth URLs to use when creating a connection
|
|
40
|
+
*/
|
|
41
|
+
urls?: string[];
|
|
42
|
+
};
|
|
43
|
+
export type WebSocketPoolEvents = {
|
|
44
|
+
error: (error: Error) => void;
|
|
45
|
+
shutdown: () => void;
|
|
13
46
|
};
|
|
14
|
-
export declare class WebSocketPool {
|
|
47
|
+
export declare class WebSocketPool extends IsomorphicEventEmitter<WebSocketPoolEvents> {
|
|
15
48
|
private readonly logger;
|
|
16
49
|
rwsPool: ResilientWebSocket[];
|
|
17
50
|
private cache;
|
|
@@ -30,6 +63,7 @@ export declare class WebSocketPool {
|
|
|
30
63
|
* @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
|
|
31
64
|
*/
|
|
32
65
|
static create(config: WebSocketPoolConfig, token: string, logger?: Logger): Promise<WebSocketPool>;
|
|
66
|
+
private emitPoolError;
|
|
33
67
|
/**
|
|
34
68
|
* Checks for error responses in JSON messages and throws appropriate errors
|
|
35
69
|
*/
|
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.bufferFromWebsocketData = bufferFromWebsocketData;
|
|
4
1
|
// the linting rules don't allow importing anything that might clash with
|
|
5
2
|
// a global, top-level import. we disable this rule because we need this
|
|
6
3
|
// imported from our installed dependency
|
|
7
4
|
// eslint-disable-next-line unicorn/prefer-node-protocol
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
5
|
+
"use strict";
|
|
6
|
+
Object.defineProperty(exports, "__esModule", {
|
|
7
|
+
value: true
|
|
8
|
+
});
|
|
9
|
+
Object.defineProperty(exports, "bufferFromWebsocketData", {
|
|
10
|
+
enumerable: true,
|
|
11
|
+
get: function() {
|
|
12
|
+
return bufferFromWebsocketData;
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
const _buffer = require("buffer");
|
|
16
|
+
const BufferClassToUse = "Buffer" in globalThis ? globalThis.Buffer : _buffer.Buffer;
|
|
15
17
|
async function bufferFromWebsocketData(data) {
|
|
16
18
|
if (typeof data === "string") {
|
|
17
19
|
return BufferClassToUse.from(new TextEncoder().encode(data).buffer);
|
|
18
20
|
}
|
|
19
|
-
if (data instanceof BufferClassToUse)
|
|
20
|
-
return data;
|
|
21
|
+
if (data instanceof BufferClassToUse) return data;
|
|
21
22
|
if (data instanceof Blob) {
|
|
22
23
|
// let the uncaught promise exception bubble up if there's an issue
|
|
23
24
|
return BufferClassToUse.from(await data.arrayBuffer());
|
|
24
25
|
}
|
|
25
|
-
if (data instanceof ArrayBuffer)
|
|
26
|
-
return BufferClassToUse.from(data);
|
|
26
|
+
if (data instanceof ArrayBuffer) return BufferClassToUse.from(data);
|
|
27
27
|
if (Array.isArray(data)) {
|
|
28
28
|
// an array of buffers is highly unlikely, but it is a possibility
|
|
29
29
|
// indicated by the WebSocket Data interface
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
function _export(target, all) {
|
|
6
|
+
for(var name in all)Object.defineProperty(target, name, {
|
|
7
|
+
enumerable: true,
|
|
8
|
+
get: Object.getOwnPropertyDescriptor(all, name).get
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
_export(exports, {
|
|
12
|
+
get envIsBrowser () {
|
|
13
|
+
return envIsBrowser;
|
|
14
|
+
},
|
|
15
|
+
get envIsBrowserOrWorker () {
|
|
16
|
+
return envIsBrowserOrWorker;
|
|
17
|
+
},
|
|
18
|
+
get envIsServiceOrWebWorker () {
|
|
19
|
+
return envIsServiceOrWebWorker;
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
// we create this local-only type, which has assertions made to indicate
|
|
23
|
+
// that we do not know and cannot guarantee which JS environment we are in
|
|
24
|
+
const g = globalThis;
|
|
25
|
+
function envIsServiceOrWebWorker() {
|
|
26
|
+
return typeof WorkerGlobalScope !== "undefined" && g.self instanceof WorkerGlobalScope;
|
|
27
|
+
}
|
|
28
|
+
function envIsBrowser() {
|
|
29
|
+
return g.window !== undefined;
|
|
30
|
+
}
|
|
31
|
+
function envIsBrowserOrWorker() {
|
|
32
|
+
return envIsServiceOrWebWorker() || envIsBrowser();
|
|
33
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
_export_star(require("./buffer-util.cjs"), exports);
|
|
6
|
+
_export_star(require("./env-util.cjs"), exports);
|
|
7
|
+
_export_star(require("./url-util.cjs"), exports);
|
|
8
|
+
function _export_star(from, to) {
|
|
9
|
+
Object.keys(from).forEach(function(k) {
|
|
10
|
+
if (k !== "default" && !Object.prototype.hasOwnProperty.call(to, k)) {
|
|
11
|
+
Object.defineProperty(to, k, {
|
|
12
|
+
enumerable: true,
|
|
13
|
+
get: function() {
|
|
14
|
+
return from[k];
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
return from;
|
|
20
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
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
|
+
}
|