@pythnetwork/pyth-lazer-sdk 4.0.0 → 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -0
- package/dist/cjs/client.cjs +229 -0
- package/dist/cjs/constants.cjs +36 -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 +4 -1
- package/dist/cjs/socket/{resilient-websocket.js → resilient-websocket.cjs} +60 -45
- package/dist/cjs/socket/resilient-websocket.d.ts +15 -1
- package/dist/cjs/socket/{websocket-pool.js → websocket-pool.cjs} +77 -64
- package/dist/cjs/socket/websocket-pool.d.ts +5 -2
- package/dist/cjs/util/buffer-util.cjs +33 -0
- package/dist/cjs/util/buffer-util.d.ts +7 -0
- package/dist/cjs/util/env-util.cjs +33 -0
- package/dist/cjs/util/env-util.d.ts +17 -0
- package/dist/cjs/util/index.cjs +20 -0
- package/dist/cjs/util/index.d.ts +3 -0
- package/dist/cjs/util/url-util.cjs +17 -0
- package/dist/cjs/util/url-util.d.ts +8 -0
- package/dist/esm/client.mjs +219 -0
- package/dist/esm/index.mjs +3 -0
- package/dist/esm/package.json +1 -1
- package/dist/esm/protocol.d.ts +4 -1
- package/dist/esm/protocol.mjs +12 -0
- package/dist/esm/socket/resilient-websocket.d.ts +15 -1
- package/dist/esm/socket/{resilient-websocket.js → resilient-websocket.mjs} +42 -35
- package/dist/esm/socket/websocket-pool.d.ts +5 -2
- package/dist/esm/socket/{websocket-pool.js → websocket-pool.mjs} +58 -54
- package/dist/esm/util/buffer-util.d.ts +7 -0
- package/dist/esm/util/buffer-util.mjs +27 -0
- package/dist/esm/util/env-util.d.ts +17 -0
- package/dist/esm/util/env-util.mjs +23 -0
- package/dist/esm/util/index.d.ts +3 -0
- package/dist/esm/util/index.mjs +3 -0
- package/dist/esm/util/url-util.d.ts +8 -0
- package/dist/esm/util/url-util.mjs +13 -0
- package/package.json +111 -15
- package/dist/cjs/client.js +0 -236
- package/dist/cjs/constants.js +0 -9
- package/dist/cjs/index.js +0 -19
- package/dist/cjs/protocol.js +0 -11
- package/dist/esm/client.js +0 -230
- package/dist/esm/index.js +0 -3
- package/dist/esm/protocol.js +0 -8
- /package/dist/esm/{constants.js → constants.mjs} +0 -0
package/dist/esm/protocol.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export type Format = "evm" | "solana" | "leEcdsa" | "leUnsigned";
|
|
2
2
|
export type DeliveryFormat = "json" | "binary";
|
|
3
3
|
export type JsonBinaryEncoding = "base64" | "hex";
|
|
4
|
-
export type PriceFeedProperty = "price" | "bestBidPrice" | "bestAskPrice" | "exponent" | "publisherCount" | "confidence";
|
|
4
|
+
export type PriceFeedProperty = "price" | "bestBidPrice" | "bestAskPrice" | "exponent" | "publisherCount" | "confidence" | "fundingRate" | "fundingTimestamp" | "fundingRateInterval";
|
|
5
5
|
export type Channel = "real_time" | "fixed_rate@50ms" | "fixed_rate@200ms";
|
|
6
6
|
export type Request = {
|
|
7
7
|
type: "subscribe";
|
|
@@ -122,3 +122,6 @@ export type JsonUpdate = {
|
|
|
122
122
|
leEcdsa?: JsonBinaryData;
|
|
123
123
|
leUnsigned?: JsonBinaryData;
|
|
124
124
|
};
|
|
125
|
+
export declare enum CustomSocketClosureCodes {
|
|
126
|
+
CLIENT_TIMEOUT_BUT_RECONNECTING = 4000
|
|
127
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const BINARY_UPDATE_FORMAT_MAGIC_LE = 461_928_307;
|
|
2
|
+
export const FORMAT_MAGICS_LE = {
|
|
3
|
+
JSON: 3_302_625_434,
|
|
4
|
+
EVM: 2_593_727_018,
|
|
5
|
+
SOLANA: 2_182_742_457,
|
|
6
|
+
LE_ECDSA: 1_296_547_300,
|
|
7
|
+
LE_UNSIGNED: 1_499_680_012
|
|
8
|
+
};
|
|
9
|
+
export var CustomSocketClosureCodes = /*#__PURE__*/ function(CustomSocketClosureCodes) {
|
|
10
|
+
CustomSocketClosureCodes[CustomSocketClosureCodes["CLIENT_TIMEOUT_BUT_RECONNECTING"] = 4000] = "CLIENT_TIMEOUT_BUT_RECONNECTING";
|
|
11
|
+
return CustomSocketClosureCodes;
|
|
12
|
+
}({});
|
|
@@ -10,6 +10,19 @@ export type ResilientWebSocketConfig = {
|
|
|
10
10
|
maxRetryDelayMs?: number;
|
|
11
11
|
logAfterRetryCount?: number;
|
|
12
12
|
};
|
|
13
|
+
/**
|
|
14
|
+
* the isomorphic-ws package ships with some slightly-erroneous typings.
|
|
15
|
+
* namely, it returns a WebSocket with typings that indicate the "terminate()" function
|
|
16
|
+
* is available on all platforms.
|
|
17
|
+
* Given that, under the hood, it is using the globalThis.WebSocket class, if it's available,
|
|
18
|
+
* and falling back to using the https://www.npmjs.com/package/ws package, this
|
|
19
|
+
* means there are API differences between the native WebSocket (the one in a web browser)
|
|
20
|
+
* and the server-side version from the "ws" package.
|
|
21
|
+
*
|
|
22
|
+
* This type creates a WebSocket type reference we use to indicate the unknown
|
|
23
|
+
* nature of the env in which is code is run.
|
|
24
|
+
*/
|
|
25
|
+
type UnsafeWebSocket = Omit<WebSocket, "terminate"> & Partial<Pick<WebSocket, "terminate">>;
|
|
13
26
|
export declare class ResilientWebSocket {
|
|
14
27
|
private endpoint;
|
|
15
28
|
private wsOptions?;
|
|
@@ -17,7 +30,7 @@ export declare class ResilientWebSocket {
|
|
|
17
30
|
private heartbeatTimeoutDurationMs;
|
|
18
31
|
private maxRetryDelayMs;
|
|
19
32
|
private logAfterRetryCount;
|
|
20
|
-
wsClient:
|
|
33
|
+
wsClient: UnsafeWebSocket | undefined;
|
|
21
34
|
wsUserClosed: boolean;
|
|
22
35
|
private wsFailedAttempts;
|
|
23
36
|
private heartbeatTimeout?;
|
|
@@ -47,3 +60,4 @@ export declare class ResilientWebSocket {
|
|
|
47
60
|
*/
|
|
48
61
|
private retryDelayMs;
|
|
49
62
|
}
|
|
63
|
+
export {};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import WebSocket from "isomorphic-ws";
|
|
2
2
|
import { dummyLogger } from "ts-log";
|
|
3
|
+
import { CustomSocketClosureCodes } from "../protocol.mjs";
|
|
4
|
+
import { envIsBrowserOrWorker } from "../util/env-util.mjs";
|
|
3
5
|
const DEFAULT_HEARTBEAT_TIMEOUT_DURATION_MS = 5000; // 5 seconds
|
|
4
6
|
const DEFAULT_MAX_RETRY_DELAY_MS = 1000; // 1 second'
|
|
5
7
|
const DEFAULT_LOG_AFTER_RETRY_COUNT = 10;
|
|
@@ -28,33 +30,29 @@ export class ResilientWebSocket {
|
|
|
28
30
|
onError;
|
|
29
31
|
onMessage;
|
|
30
32
|
onReconnect;
|
|
31
|
-
constructor(config)
|
|
33
|
+
constructor(config){
|
|
32
34
|
this.endpoint = config.endpoint;
|
|
33
35
|
this.wsOptions = config.wsOptions;
|
|
34
36
|
this.logger = config.logger ?? dummyLogger;
|
|
35
|
-
this.heartbeatTimeoutDurationMs =
|
|
36
|
-
config.heartbeatTimeoutDurationMs ??
|
|
37
|
-
DEFAULT_HEARTBEAT_TIMEOUT_DURATION_MS;
|
|
37
|
+
this.heartbeatTimeoutDurationMs = config.heartbeatTimeoutDurationMs ?? DEFAULT_HEARTBEAT_TIMEOUT_DURATION_MS;
|
|
38
38
|
this.maxRetryDelayMs = config.maxRetryDelayMs ?? DEFAULT_MAX_RETRY_DELAY_MS;
|
|
39
|
-
this.logAfterRetryCount =
|
|
40
|
-
config.logAfterRetryCount ?? DEFAULT_LOG_AFTER_RETRY_COUNT;
|
|
39
|
+
this.logAfterRetryCount = config.logAfterRetryCount ?? DEFAULT_LOG_AFTER_RETRY_COUNT;
|
|
41
40
|
this.wsFailedAttempts = 0;
|
|
42
|
-
this.onError = (error)
|
|
41
|
+
this.onError = (error)=>{
|
|
43
42
|
void error;
|
|
44
43
|
};
|
|
45
|
-
this.onMessage = (data)
|
|
44
|
+
this.onMessage = (data)=>{
|
|
46
45
|
void data;
|
|
47
46
|
};
|
|
48
|
-
this.onReconnect = ()
|
|
49
|
-
|
|
47
|
+
this.onReconnect = ()=>{
|
|
48
|
+
// Empty function, can be set by the user.
|
|
50
49
|
};
|
|
51
50
|
}
|
|
52
51
|
send(data) {
|
|
53
52
|
this.logger.debug(`Sending message`);
|
|
54
53
|
if (this.isConnected()) {
|
|
55
54
|
this.wsClient.send(data);
|
|
56
|
-
}
|
|
57
|
-
else {
|
|
55
|
+
} else {
|
|
58
56
|
this.logger.warn(`WebSocket to ${this.endpoint} is not connected. Cannot send message.`);
|
|
59
57
|
}
|
|
60
58
|
}
|
|
@@ -74,34 +72,36 @@ export class ResilientWebSocket {
|
|
|
74
72
|
clearTimeout(this.retryTimeout);
|
|
75
73
|
this.retryTimeout = undefined;
|
|
76
74
|
}
|
|
77
|
-
|
|
78
|
-
|
|
75
|
+
// browser constructor supports a different 2nd argument for the constructor,
|
|
76
|
+
// so we need to ensure it's not included if we're running in that environment:
|
|
77
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket#protocols
|
|
78
|
+
this.wsClient = new WebSocket(this.endpoint, envIsBrowserOrWorker() ? undefined : this.wsOptions);
|
|
79
|
+
this.wsClient.addEventListener("open", ()=>{
|
|
79
80
|
this.logger.info("WebSocket connection established");
|
|
80
81
|
this.wsFailedAttempts = 0;
|
|
81
82
|
this._isReconnecting = false;
|
|
82
83
|
this.resetHeartbeat();
|
|
83
84
|
this.onReconnect();
|
|
84
85
|
});
|
|
85
|
-
this.wsClient.addEventListener("close", (e)
|
|
86
|
+
this.wsClient.addEventListener("close", (e)=>{
|
|
86
87
|
if (this.wsUserClosed) {
|
|
87
88
|
this.logger.info(`WebSocket connection to ${this.endpoint} closed by user`);
|
|
88
|
-
}
|
|
89
|
-
else {
|
|
89
|
+
} else {
|
|
90
90
|
if (this.shouldLogRetry()) {
|
|
91
91
|
this.logger.warn(`WebSocket connection to ${this.endpoint} closed unexpectedly: Code: ${e.code.toString()}`);
|
|
92
92
|
}
|
|
93
93
|
this.handleReconnect();
|
|
94
94
|
}
|
|
95
95
|
});
|
|
96
|
-
this.wsClient.addEventListener("error", (event)
|
|
96
|
+
this.wsClient.addEventListener("error", (event)=>{
|
|
97
97
|
this.onError(event);
|
|
98
98
|
});
|
|
99
|
-
this.wsClient.addEventListener("message", (event)
|
|
99
|
+
this.wsClient.addEventListener("message", (event)=>{
|
|
100
100
|
this.resetHeartbeat();
|
|
101
101
|
this.onMessage(event.data);
|
|
102
102
|
});
|
|
103
103
|
if ("on" in this.wsClient) {
|
|
104
|
-
this.wsClient.on("ping", ()
|
|
104
|
+
this.wsClient.on("ping", ()=>{
|
|
105
105
|
this.logger.info("Ping received");
|
|
106
106
|
this.resetHeartbeat();
|
|
107
107
|
});
|
|
@@ -111,9 +111,19 @@ export class ResilientWebSocket {
|
|
|
111
111
|
if (this.heartbeatTimeout !== undefined) {
|
|
112
112
|
clearTimeout(this.heartbeatTimeout);
|
|
113
113
|
}
|
|
114
|
-
this.heartbeatTimeout = setTimeout(()
|
|
115
|
-
|
|
116
|
-
this.
|
|
114
|
+
this.heartbeatTimeout = setTimeout(()=>{
|
|
115
|
+
const warnMsg = "Connection timed out. Reconnecting...";
|
|
116
|
+
this.logger.warn(warnMsg);
|
|
117
|
+
if (this.wsClient) {
|
|
118
|
+
if (typeof this.wsClient.terminate === "function") {
|
|
119
|
+
this.wsClient.terminate();
|
|
120
|
+
} else {
|
|
121
|
+
// terminate is an implementation detail of the node-friendly
|
|
122
|
+
// https://www.npmjs.com/package/ws package, but is not a native WebSocket API,
|
|
123
|
+
// so we have to use the close method
|
|
124
|
+
this.wsClient.close(CustomSocketClosureCodes.CLIENT_TIMEOUT_BUT_RECONNECTING, warnMsg);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
117
127
|
this.handleReconnect();
|
|
118
128
|
}, this.heartbeatTimeoutDurationMs);
|
|
119
129
|
}
|
|
@@ -132,11 +142,9 @@ export class ResilientWebSocket {
|
|
|
132
142
|
this.wsClient = undefined;
|
|
133
143
|
this._isReconnecting = true;
|
|
134
144
|
if (this.shouldLogRetry()) {
|
|
135
|
-
this.logger.error("Connection closed unexpectedly or because of timeout. Reconnecting after " +
|
|
136
|
-
String(this.retryDelayMs()) +
|
|
137
|
-
"ms.");
|
|
145
|
+
this.logger.error("Connection closed unexpectedly or because of timeout. Reconnecting after " + String(this.retryDelayMs()) + "ms.");
|
|
138
146
|
}
|
|
139
|
-
this.retryTimeout = setTimeout(()
|
|
147
|
+
this.retryTimeout = setTimeout(()=>{
|
|
140
148
|
this.startWebSocket();
|
|
141
149
|
}, this.retryDelayMs());
|
|
142
150
|
}
|
|
@@ -148,14 +156,13 @@ export class ResilientWebSocket {
|
|
|
148
156
|
this.wsUserClosed = true;
|
|
149
157
|
}
|
|
150
158
|
/**
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
retryDelayMs() {
|
|
159
|
+
* Calculates the delay in milliseconds for exponential backoff based on the number of failed attempts.
|
|
160
|
+
*
|
|
161
|
+
* The delay increases exponentially with each attempt, starting at 20ms for the first attempt,
|
|
162
|
+
* and is capped at maxRetryDelayMs for attempts greater than or equal to 10.
|
|
163
|
+
*
|
|
164
|
+
* @returns The calculated delay in milliseconds before the next retry.
|
|
165
|
+
*/ retryDelayMs() {
|
|
159
166
|
if (this.wsFailedAttempts >= 10) {
|
|
160
167
|
return this.maxRetryDelayMs;
|
|
161
168
|
}
|
|
@@ -4,6 +4,7 @@ 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
|
+
type WebSocketOnMessageCallback = (data: WebSocket.Data) => void | Promise<void>;
|
|
7
8
|
export type WebSocketPoolConfig = {
|
|
8
9
|
urls?: string[];
|
|
9
10
|
numConnections?: number;
|
|
@@ -33,15 +34,16 @@ export declare class WebSocketPool {
|
|
|
33
34
|
* Checks for error responses in JSON messages and throws appropriate errors
|
|
34
35
|
*/
|
|
35
36
|
private handleErrorMessages;
|
|
37
|
+
private constructCacheKeyFromWebsocketData;
|
|
36
38
|
/**
|
|
37
39
|
* Handles incoming websocket messages by deduplicating identical messages received across
|
|
38
40
|
* multiple connections before forwarding to registered handlers
|
|
39
41
|
*/
|
|
40
|
-
dedupeHandler: (data: WebSocket.Data) => void
|
|
42
|
+
dedupeHandler: (data: WebSocket.Data) => Promise<void>;
|
|
41
43
|
sendRequest(request: Request): void;
|
|
42
44
|
addSubscription(request: Request): void;
|
|
43
45
|
removeSubscription(subscriptionId: number): void;
|
|
44
|
-
addMessageListener(handler:
|
|
46
|
+
addMessageListener(handler: WebSocketOnMessageCallback): void;
|
|
45
47
|
/**
|
|
46
48
|
* Calls the handler if all websocket connections are currently down or in reconnecting state.
|
|
47
49
|
* The connections may still try to reconnect in the background.
|
|
@@ -52,3 +54,4 @@ export declare class WebSocketPool {
|
|
|
52
54
|
private checkConnectionStates;
|
|
53
55
|
shutdown(): void;
|
|
54
56
|
}
|
|
57
|
+
export {};
|
|
@@ -1,74 +1,76 @@
|
|
|
1
1
|
import TTLCache from "@isaacs/ttlcache";
|
|
2
|
-
import WebSocket from "isomorphic-ws";
|
|
3
2
|
import { dummyLogger } from "ts-log";
|
|
4
|
-
import { ResilientWebSocket } from "./resilient-websocket.
|
|
5
|
-
import { DEFAULT_STREAM_SERVICE_0_URL, DEFAULT_STREAM_SERVICE_1_URL
|
|
3
|
+
import { ResilientWebSocket } from "./resilient-websocket.mjs";
|
|
4
|
+
import { DEFAULT_STREAM_SERVICE_0_URL, DEFAULT_STREAM_SERVICE_1_URL } from "../constants.mjs";
|
|
5
|
+
import { addAuthTokenToWebSocketUrl, bufferFromWebsocketData, envIsBrowserOrWorker } from "../util/index.mjs";
|
|
6
6
|
const DEFAULT_NUM_CONNECTIONS = 4;
|
|
7
7
|
export class WebSocketPool {
|
|
8
8
|
logger;
|
|
9
9
|
rwsPool;
|
|
10
10
|
cache;
|
|
11
|
-
subscriptions;
|
|
11
|
+
subscriptions;
|
|
12
12
|
messageListeners;
|
|
13
13
|
allConnectionsDownListeners;
|
|
14
14
|
wasAllDown = true;
|
|
15
15
|
checkConnectionStatesInterval;
|
|
16
|
-
constructor(logger)
|
|
16
|
+
constructor(logger){
|
|
17
17
|
this.logger = logger;
|
|
18
18
|
this.rwsPool = [];
|
|
19
|
-
this.cache = new TTLCache({
|
|
19
|
+
this.cache = new TTLCache({
|
|
20
|
+
ttl: 1000 * 10
|
|
21
|
+
}); // TTL of 10 seconds
|
|
20
22
|
this.subscriptions = new Map();
|
|
21
23
|
this.messageListeners = [];
|
|
22
24
|
this.allConnectionsDownListeners = [];
|
|
23
25
|
// Start monitoring connection states
|
|
24
|
-
this.checkConnectionStatesInterval = setInterval(()
|
|
26
|
+
this.checkConnectionStatesInterval = setInterval(()=>{
|
|
25
27
|
this.checkConnectionStates();
|
|
26
28
|
}, 100);
|
|
27
29
|
}
|
|
28
30
|
/**
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
static async create(config, token, logger) {
|
|
31
|
+
* Creates a new WebSocketPool instance that uses multiple redundant WebSocket connections for reliability.
|
|
32
|
+
* Usage semantics are similar to using a regular WebSocket client.
|
|
33
|
+
* @param urls - List of WebSocket URLs to connect to
|
|
34
|
+
* @param token - Authentication token to use for the connections
|
|
35
|
+
* @param numConnections - Number of parallel WebSocket connections to maintain (default: 3)
|
|
36
|
+
* @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
|
|
37
|
+
*/ static async create(config, token, logger) {
|
|
37
38
|
const urls = config.urls ?? [
|
|
38
39
|
DEFAULT_STREAM_SERVICE_0_URL,
|
|
39
|
-
DEFAULT_STREAM_SERVICE_1_URL
|
|
40
|
+
DEFAULT_STREAM_SERVICE_1_URL
|
|
40
41
|
];
|
|
41
42
|
const log = logger ?? dummyLogger;
|
|
42
43
|
const pool = new WebSocketPool(log);
|
|
43
44
|
const numConnections = config.numConnections ?? DEFAULT_NUM_CONNECTIONS;
|
|
44
|
-
for
|
|
45
|
-
const
|
|
45
|
+
for(let i = 0; i < numConnections; i++){
|
|
46
|
+
const baseUrl = urls[i % urls.length];
|
|
47
|
+
const isBrowser = envIsBrowserOrWorker();
|
|
48
|
+
const url = isBrowser ? addAuthTokenToWebSocketUrl(baseUrl, token) : baseUrl;
|
|
46
49
|
if (!url) {
|
|
47
50
|
throw new Error(`URLs must not be null or empty`);
|
|
48
51
|
}
|
|
49
52
|
const wsOptions = {
|
|
50
53
|
...config.rwsConfig?.wsOptions,
|
|
51
|
-
headers: {
|
|
52
|
-
Authorization: `Bearer ${token}
|
|
53
|
-
}
|
|
54
|
+
headers: isBrowser ? undefined : {
|
|
55
|
+
Authorization: `Bearer ${token}`
|
|
56
|
+
}
|
|
54
57
|
};
|
|
55
58
|
const rws = new ResilientWebSocket({
|
|
56
59
|
...config.rwsConfig,
|
|
57
60
|
endpoint: url,
|
|
58
61
|
wsOptions,
|
|
59
|
-
logger: log
|
|
62
|
+
logger: log
|
|
60
63
|
});
|
|
61
64
|
// If a websocket client unexpectedly disconnects, ResilientWebSocket will reestablish
|
|
62
65
|
// the connection and call the onReconnect callback.
|
|
63
|
-
rws.onReconnect = ()
|
|
66
|
+
rws.onReconnect = ()=>{
|
|
64
67
|
if (rws.wsUserClosed) {
|
|
65
68
|
return;
|
|
66
69
|
}
|
|
67
|
-
for (const [, request] of pool.subscriptions)
|
|
70
|
+
for (const [, request] of pool.subscriptions){
|
|
68
71
|
try {
|
|
69
72
|
rws.send(JSON.stringify(request));
|
|
70
|
-
}
|
|
71
|
-
catch (error) {
|
|
73
|
+
} catch (error) {
|
|
72
74
|
pool.logger.error("Failed to resend subscription on reconnect:", error);
|
|
73
75
|
}
|
|
74
76
|
}
|
|
@@ -77,37 +79,42 @@ export class WebSocketPool {
|
|
|
77
79
|
rws.onError = config.onError;
|
|
78
80
|
}
|
|
79
81
|
// Handle all client messages ourselves. Dedupe before sending to registered message handlers.
|
|
80
|
-
rws.onMessage =
|
|
82
|
+
rws.onMessage = (data)=>{
|
|
83
|
+
pool.dedupeHandler(data).catch((error)=>{
|
|
84
|
+
const errMsg = `An error occurred in the WebSocket pool's dedupeHandler: ${error instanceof Error ? error.message : String(error)}`;
|
|
85
|
+
throw new Error(errMsg);
|
|
86
|
+
});
|
|
87
|
+
};
|
|
81
88
|
pool.rwsPool.push(rws);
|
|
82
89
|
rws.startWebSocket();
|
|
83
90
|
}
|
|
84
91
|
pool.logger.info(`Started WebSocketPool with ${numConnections.toString()} connections. Waiting for at least one to connect...`);
|
|
85
|
-
while
|
|
86
|
-
await new Promise((resolve)
|
|
92
|
+
while(!pool.isAnyConnectionEstablished()){
|
|
93
|
+
await new Promise((resolve)=>setTimeout(resolve, 100));
|
|
87
94
|
}
|
|
88
95
|
pool.logger.info(`At least one WebSocket connection is established. WebSocketPool is ready.`);
|
|
89
96
|
return pool;
|
|
90
97
|
}
|
|
91
98
|
/**
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
handleErrorMessages(data) {
|
|
99
|
+
* Checks for error responses in JSON messages and throws appropriate errors
|
|
100
|
+
*/ handleErrorMessages(data) {
|
|
95
101
|
const message = JSON.parse(data);
|
|
96
102
|
if (message.type === "subscriptionError") {
|
|
97
103
|
throw new Error(`Error occurred for subscription ID ${String(message.subscriptionId)}: ${message.error}`);
|
|
98
|
-
}
|
|
99
|
-
else if (message.type === "error") {
|
|
104
|
+
} else if (message.type === "error") {
|
|
100
105
|
throw new Error(`Error: ${message.error}`);
|
|
101
106
|
}
|
|
102
107
|
}
|
|
108
|
+
async constructCacheKeyFromWebsocketData(data) {
|
|
109
|
+
if (typeof data === "string") return data;
|
|
110
|
+
const buff = await bufferFromWebsocketData(data);
|
|
111
|
+
return buff.toString("hex");
|
|
112
|
+
}
|
|
103
113
|
/**
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const cacheKey = typeof data === "string"
|
|
109
|
-
? data
|
|
110
|
-
: Buffer.from(data).toString("hex");
|
|
114
|
+
* Handles incoming websocket messages by deduplicating identical messages received across
|
|
115
|
+
* multiple connections before forwarding to registered handlers
|
|
116
|
+
*/ dedupeHandler = async (data)=>{
|
|
117
|
+
const cacheKey = await this.constructCacheKeyFromWebsocketData(data);
|
|
111
118
|
if (this.cache.has(cacheKey)) {
|
|
112
119
|
this.logger.debug("Dropping duplicate message");
|
|
113
120
|
return;
|
|
@@ -116,12 +123,10 @@ export class WebSocketPool {
|
|
|
116
123
|
if (typeof data === "string") {
|
|
117
124
|
this.handleErrorMessages(data);
|
|
118
125
|
}
|
|
119
|
-
|
|
120
|
-
handler(data);
|
|
121
|
-
}
|
|
126
|
+
await Promise.all(this.messageListeners.map((handler)=>handler(data)));
|
|
122
127
|
};
|
|
123
128
|
sendRequest(request) {
|
|
124
|
-
for (const rws of this.rwsPool)
|
|
129
|
+
for (const rws of this.rwsPool){
|
|
125
130
|
rws.send(JSON.stringify(request));
|
|
126
131
|
}
|
|
127
132
|
}
|
|
@@ -136,7 +141,7 @@ export class WebSocketPool {
|
|
|
136
141
|
this.subscriptions.delete(subscriptionId);
|
|
137
142
|
const request = {
|
|
138
143
|
type: "unsubscribe",
|
|
139
|
-
subscriptionId
|
|
144
|
+
subscriptionId
|
|
140
145
|
};
|
|
141
146
|
this.sendRequest(request);
|
|
142
147
|
}
|
|
@@ -144,17 +149,16 @@ export class WebSocketPool {
|
|
|
144
149
|
this.messageListeners.push(handler);
|
|
145
150
|
}
|
|
146
151
|
/**
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
addAllConnectionsDownListener(handler) {
|
|
152
|
+
* Calls the handler if all websocket connections are currently down or in reconnecting state.
|
|
153
|
+
* The connections may still try to reconnect in the background.
|
|
154
|
+
*/ addAllConnectionsDownListener(handler) {
|
|
151
155
|
this.allConnectionsDownListeners.push(handler);
|
|
152
156
|
}
|
|
153
157
|
areAllConnectionsDown() {
|
|
154
|
-
return this.rwsPool.every((ws)
|
|
158
|
+
return this.rwsPool.every((ws)=>!ws.isConnected() || ws.isReconnecting());
|
|
155
159
|
}
|
|
156
160
|
isAnyConnectionEstablished() {
|
|
157
|
-
return this.rwsPool.some((ws)
|
|
161
|
+
return this.rwsPool.some((ws)=>ws.isConnected());
|
|
158
162
|
}
|
|
159
163
|
checkConnectionStates() {
|
|
160
164
|
const allDown = this.areAllConnectionsDown();
|
|
@@ -163,7 +167,7 @@ export class WebSocketPool {
|
|
|
163
167
|
this.wasAllDown = true;
|
|
164
168
|
this.logger.error("All WebSocket connections are down or reconnecting");
|
|
165
169
|
// Notify all listeners
|
|
166
|
-
for (const listener of this.allConnectionsDownListeners)
|
|
170
|
+
for (const listener of this.allConnectionsDownListeners){
|
|
167
171
|
listener();
|
|
168
172
|
}
|
|
169
173
|
}
|
|
@@ -173,7 +177,7 @@ export class WebSocketPool {
|
|
|
173
177
|
}
|
|
174
178
|
}
|
|
175
179
|
shutdown() {
|
|
176
|
-
for (const rws of this.rwsPool)
|
|
180
|
+
for (const rws of this.rwsPool){
|
|
177
181
|
rws.closeWebSocket();
|
|
178
182
|
}
|
|
179
183
|
this.rwsPool = [];
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Data } from "isomorphic-ws";
|
|
2
|
+
/**
|
|
3
|
+
* given a relatively unknown websocket frame data object,
|
|
4
|
+
* returns a valid Buffer instance that is safe to use
|
|
5
|
+
* isomorphically in any JS runtime environment
|
|
6
|
+
*/
|
|
7
|
+
export declare function bufferFromWebsocketData(data: Data): Promise<Buffer>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// the linting rules don't allow importing anything that might clash with
|
|
2
|
+
// a global, top-level import. we disable this rule because we need this
|
|
3
|
+
// imported from our installed dependency
|
|
4
|
+
// eslint-disable-next-line unicorn/prefer-node-protocol
|
|
5
|
+
import { Buffer as BrowserBuffer } from "buffer";
|
|
6
|
+
const BufferClassToUse = "Buffer" in globalThis ? globalThis.Buffer : BrowserBuffer;
|
|
7
|
+
/**
|
|
8
|
+
* given a relatively unknown websocket frame data object,
|
|
9
|
+
* returns a valid Buffer instance that is safe to use
|
|
10
|
+
* isomorphically in any JS runtime environment
|
|
11
|
+
*/ export async function bufferFromWebsocketData(data) {
|
|
12
|
+
if (typeof data === "string") {
|
|
13
|
+
return BufferClassToUse.from(new TextEncoder().encode(data).buffer);
|
|
14
|
+
}
|
|
15
|
+
if (data instanceof BufferClassToUse) return data;
|
|
16
|
+
if (data instanceof Blob) {
|
|
17
|
+
// let the uncaught promise exception bubble up if there's an issue
|
|
18
|
+
return BufferClassToUse.from(await data.arrayBuffer());
|
|
19
|
+
}
|
|
20
|
+
if (data instanceof ArrayBuffer) return BufferClassToUse.from(data);
|
|
21
|
+
if (Array.isArray(data)) {
|
|
22
|
+
// an array of buffers is highly unlikely, but it is a possibility
|
|
23
|
+
// indicated by the WebSocket Data interface
|
|
24
|
+
return BufferClassToUse.concat(data);
|
|
25
|
+
}
|
|
26
|
+
return data;
|
|
27
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects if this code is running within any Service or WebWorker context.
|
|
3
|
+
* @returns true if in a worker of some kind, false if otherwise
|
|
4
|
+
*/
|
|
5
|
+
export declare function envIsServiceOrWebWorker(): boolean;
|
|
6
|
+
/**
|
|
7
|
+
* Detects if the code is running in a regular DOM or Web Worker context.
|
|
8
|
+
* @returns true if running in a DOM or Web Worker context, false if running in Node.js
|
|
9
|
+
*/
|
|
10
|
+
export declare function envIsBrowser(): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* a convenience method that returns whether or not
|
|
13
|
+
* this code is executing in some type of browser-centric environment
|
|
14
|
+
*
|
|
15
|
+
* @returns true if in the browser's main UI thread or in a worker, false if otherwise
|
|
16
|
+
*/
|
|
17
|
+
export declare function envIsBrowserOrWorker(): boolean;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// we create this local-only type, which has assertions made to indicate
|
|
2
|
+
// that we do not know and cannot guarantee which JS environment we are in
|
|
3
|
+
const g = globalThis;
|
|
4
|
+
/**
|
|
5
|
+
* Detects if this code is running within any Service or WebWorker context.
|
|
6
|
+
* @returns true if in a worker of some kind, false if otherwise
|
|
7
|
+
*/ export function envIsServiceOrWebWorker() {
|
|
8
|
+
return typeof WorkerGlobalScope !== "undefined" && g.self instanceof WorkerGlobalScope;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Detects if the code is running in a regular DOM or Web Worker context.
|
|
12
|
+
* @returns true if running in a DOM or Web Worker context, false if running in Node.js
|
|
13
|
+
*/ export function envIsBrowser() {
|
|
14
|
+
return g.window !== undefined;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* a convenience method that returns whether or not
|
|
18
|
+
* this code is executing in some type of browser-centric environment
|
|
19
|
+
*
|
|
20
|
+
* @returns true if in the browser's main UI thread or in a worker, false if otherwise
|
|
21
|
+
*/ export function envIsBrowserOrWorker() {
|
|
22
|
+
return envIsServiceOrWebWorker() || envIsBrowser();
|
|
23
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Given a URL to a hosted lazer stream service and a possible auth token,
|
|
3
|
+
* appends the auth token as a query parameter and returns the URL with the token
|
|
4
|
+
* contained within.
|
|
5
|
+
* If the URL provided is nullish, it is returned as-is (in the same nullish format).
|
|
6
|
+
* If the token is nullish, the baseUrl given is returned, instead.
|
|
7
|
+
*/
|
|
8
|
+
export declare function addAuthTokenToWebSocketUrl(baseUrl: string | null | undefined, authToken: string | null | undefined): string | null | undefined;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const ACCESS_TOKEN_QUERY_PARAM_KEY = "ACCESS_TOKEN";
|
|
2
|
+
/**
|
|
3
|
+
* Given a URL to a hosted lazer stream service and a possible auth token,
|
|
4
|
+
* appends the auth token as a query parameter and returns the URL with the token
|
|
5
|
+
* contained within.
|
|
6
|
+
* If the URL provided is nullish, it is returned as-is (in the same nullish format).
|
|
7
|
+
* If the token is nullish, the baseUrl given is returned, instead.
|
|
8
|
+
*/ export function addAuthTokenToWebSocketUrl(baseUrl, authToken) {
|
|
9
|
+
if (!baseUrl || !authToken) return baseUrl;
|
|
10
|
+
const parsedUrl = new URL(baseUrl);
|
|
11
|
+
parsedUrl.searchParams.set(ACCESS_TOKEN_QUERY_PARAM_KEY, authToken);
|
|
12
|
+
return parsedUrl.toString();
|
|
13
|
+
}
|