@pythnetwork/pyth-lazer-sdk 6.2.0 → 6.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.cjs +43 -30
- package/dist/cjs/client.d.ts +3 -11
- package/dist/cjs/socket/websocket-pool.cjs +94 -83
- package/dist/cjs/socket/websocket-pool.d.ts +1 -2
- package/dist/esm/client.d.ts +3 -11
- package/dist/esm/client.mjs +43 -30
- package/dist/esm/socket/websocket-pool.d.ts +1 -2
- package/dist/esm/socket/websocket-pool.mjs +94 -83
- package/package.json +1 -1
package/dist/cjs/client.cjs
CHANGED
|
@@ -22,39 +22,64 @@ class PythLazerClient {
|
|
|
22
22
|
priceServiceUrl;
|
|
23
23
|
token;
|
|
24
24
|
wsp;
|
|
25
|
-
|
|
26
|
-
constructor({ abortSignal, logger, metadataServiceUrl, priceServiceUrl, token, wsp }){
|
|
27
|
-
this.abortSignal = abortSignal;
|
|
25
|
+
constructor({ logger, metadataServiceUrl, priceServiceUrl, token, wsp }){
|
|
28
26
|
this.logger = logger;
|
|
29
27
|
this.metadataServiceUrl = metadataServiceUrl;
|
|
30
28
|
this.priceServiceUrl = priceServiceUrl;
|
|
31
29
|
this.token = token;
|
|
32
30
|
this.wsp = wsp;
|
|
33
|
-
this.bindHandlers();
|
|
34
31
|
}
|
|
35
32
|
/**
|
|
36
33
|
* Creates a new PythLazerClient instance.
|
|
37
34
|
* @param config - Configuration including token, metadata service URL, and price service URL, and WebSocket pool configuration
|
|
38
35
|
*/ static async create(config) {
|
|
36
|
+
let wsp = undefined;
|
|
37
|
+
let client = undefined;
|
|
38
|
+
const { abortSignal } = config;
|
|
39
|
+
const handleAbort = ()=>{
|
|
40
|
+
// we need to EXPLICITLY shutdown both items,
|
|
41
|
+
// because there is a possibility the client was undefined
|
|
42
|
+
// when the abort signal was called
|
|
43
|
+
client?.shutdown();
|
|
44
|
+
wsp?.shutdown();
|
|
45
|
+
abortSignal?.removeEventListener("abort", handleAbort);
|
|
46
|
+
};
|
|
47
|
+
const throwIfAborted = ()=>{
|
|
48
|
+
if (abortSignal?.aborted) {
|
|
49
|
+
throw new DOMException("PythLazerClient.create() was aborted", "AbortError");
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
abortSignal?.addEventListener("abort", handleAbort);
|
|
39
53
|
const token = config.token;
|
|
40
54
|
// Collect and remove trailing slash from URLs
|
|
41
55
|
const metadataServiceUrl = (config.metadataServiceUrl ?? _constants.DEFAULT_METADATA_SERVICE_URL).replace(/\/+$/, "");
|
|
42
56
|
const priceServiceUrl = (config.priceServiceUrl ?? _constants.DEFAULT_PRICE_SERVICE_URL).replace(/\/+$/, "");
|
|
43
57
|
const logger = config.logger ?? _tslog.dummyLogger;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
try {
|
|
59
|
+
throwIfAborted();
|
|
60
|
+
// the prior API was mismatched, in that it marked a websocket pool as optional,
|
|
61
|
+
// yet all internal code on the Pyth Pro client used it and threw if it didn't exist.
|
|
62
|
+
// now, the typings indicate it's no longer optional and we don't sanity check
|
|
63
|
+
// if it's set
|
|
64
|
+
wsp = await _websocketpool.WebSocketPool.create(config.webSocketPoolConfig, token, abortSignal, logger);
|
|
65
|
+
throwIfAborted();
|
|
66
|
+
client = new PythLazerClient({
|
|
67
|
+
logger,
|
|
68
|
+
metadataServiceUrl,
|
|
69
|
+
priceServiceUrl,
|
|
70
|
+
token,
|
|
71
|
+
wsp
|
|
72
|
+
});
|
|
73
|
+
throwIfAborted();
|
|
74
|
+
// detach the abortSignal handler here, because we've made it!
|
|
75
|
+
abortSignal?.removeEventListener("abort", handleAbort);
|
|
76
|
+
return client;
|
|
77
|
+
} catch (error) {
|
|
78
|
+
client?.shutdown();
|
|
79
|
+
wsp?.shutdown();
|
|
80
|
+
abortSignal?.removeEventListener("abort", handleAbort);
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
58
83
|
}
|
|
59
84
|
/**
|
|
60
85
|
* Adds a message listener that receives either JSON or binary responses from the WebSocket connections.
|
|
@@ -108,11 +133,6 @@ class PythLazerClient {
|
|
|
108
133
|
});
|
|
109
134
|
});
|
|
110
135
|
}
|
|
111
|
-
/**
|
|
112
|
-
* binds any internal event handlers
|
|
113
|
-
*/ bindHandlers() {
|
|
114
|
-
this.abortSignal?.addEventListener("abort", this.abortHandler);
|
|
115
|
-
}
|
|
116
136
|
subscribe(request) {
|
|
117
137
|
if (request.type !== "subscribe") {
|
|
118
138
|
throw new Error("Request must be a subscribe request");
|
|
@@ -150,14 +170,7 @@ class PythLazerClient {
|
|
|
150
170
|
*/ addConnectionReconnectListener(handler) {
|
|
151
171
|
this.wsp.addConnectionReconnectListener(handler);
|
|
152
172
|
}
|
|
153
|
-
/**
|
|
154
|
-
* called if and only if a user provided an abort signal and it was aborted
|
|
155
|
-
*/ abortHandler = ()=>{
|
|
156
|
-
this.shutdown();
|
|
157
|
-
};
|
|
158
173
|
shutdown() {
|
|
159
|
-
// Clean up abort signal listener to prevent memory leak
|
|
160
|
-
this.abortSignal?.removeEventListener("abort", this.abortHandler);
|
|
161
174
|
this.wsp.shutdown();
|
|
162
175
|
}
|
|
163
176
|
/**
|
package/dist/cjs/client.d.ts
CHANGED
|
@@ -18,8 +18,9 @@ export type JsonOrBinaryResponse = {
|
|
|
18
18
|
};
|
|
19
19
|
export type LazerClientConfig = {
|
|
20
20
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
21
|
+
* If provided, allows aborting the client creation process.
|
|
22
|
+
* Once the client is successfully created, this signal has no effect on the client's lifecycle.
|
|
23
|
+
* When aborted, the promise will reject with a DOMException with name 'AbortError'.
|
|
23
24
|
*/
|
|
24
25
|
abortSignal?: AbortSignal;
|
|
25
26
|
token: string;
|
|
@@ -34,7 +35,6 @@ export declare class PythLazerClient {
|
|
|
34
35
|
private priceServiceUrl;
|
|
35
36
|
private token;
|
|
36
37
|
private wsp;
|
|
37
|
-
private abortSignal;
|
|
38
38
|
private constructor();
|
|
39
39
|
/**
|
|
40
40
|
* Creates a new PythLazerClient instance.
|
|
@@ -48,10 +48,6 @@ export declare class PythLazerClient {
|
|
|
48
48
|
* or a binary response containing EVM, Solana, or parsed payload data.
|
|
49
49
|
*/
|
|
50
50
|
addMessageListener(handler: (event: JsonOrBinaryResponse) => void): void;
|
|
51
|
-
/**
|
|
52
|
-
* binds any internal event handlers
|
|
53
|
-
*/
|
|
54
|
-
bindHandlers(): void;
|
|
55
51
|
subscribe(request: Request): void;
|
|
56
52
|
unsubscribe(subscriptionId: number): void;
|
|
57
53
|
send(request: Request): void;
|
|
@@ -76,10 +72,6 @@ export declare class PythLazerClient {
|
|
|
76
72
|
* @param handler - Function to be called with connection index and endpoint URL
|
|
77
73
|
*/
|
|
78
74
|
addConnectionReconnectListener(handler: (connectionIndex: number, endpoint: string) => void): void;
|
|
79
|
-
/**
|
|
80
|
-
* called if and only if a user provided an abort signal and it was aborted
|
|
81
|
-
*/
|
|
82
|
-
protected abortHandler: () => void;
|
|
83
75
|
shutdown(): void;
|
|
84
76
|
/**
|
|
85
77
|
* Private helper method to make authenticated HTTP requests with Bearer token
|
|
@@ -22,7 +22,6 @@ function _interop_require_default(obj) {
|
|
|
22
22
|
const DEFAULT_NUM_CONNECTIONS = 4;
|
|
23
23
|
class WebSocketPool extends _index.IsomorphicEventEmitter {
|
|
24
24
|
logger;
|
|
25
|
-
abortSignal;
|
|
26
25
|
rwsPool;
|
|
27
26
|
cache;
|
|
28
27
|
subscriptions;
|
|
@@ -34,8 +33,8 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
|
|
|
34
33
|
wasAllDown = true;
|
|
35
34
|
checkConnectionStatesInterval;
|
|
36
35
|
isShutdown = false;
|
|
37
|
-
constructor(logger
|
|
38
|
-
super(), this.logger = logger
|
|
36
|
+
constructor(logger){
|
|
37
|
+
super(), this.logger = logger;
|
|
39
38
|
this.rwsPool = [];
|
|
40
39
|
this.cache = new _ttlcache.default({
|
|
41
40
|
ttl: 1000 * 10
|
|
@@ -59,90 +58,102 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
|
|
|
59
58
|
* @param numConnections - Number of parallel WebSocket connections to maintain (default: 3)
|
|
60
59
|
* @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
|
|
61
60
|
*/ static async create(config, token, abortSignal, logger) {
|
|
61
|
+
// Helper to check if aborted and throw
|
|
62
|
+
const throwIfAborted = ()=>{
|
|
63
|
+
if (abortSignal?.aborted) {
|
|
64
|
+
throw new DOMException("WebSocketPool.create() was aborted", "AbortError");
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
// Check before starting
|
|
68
|
+
throwIfAborted();
|
|
62
69
|
const urls = config.urls ?? [
|
|
63
70
|
_constants.DEFAULT_STREAM_SERVICE_0_URL,
|
|
64
71
|
_constants.DEFAULT_STREAM_SERVICE_1_URL
|
|
65
72
|
];
|
|
66
73
|
const log = logger ?? _tslog.dummyLogger;
|
|
67
|
-
const pool = new WebSocketPool(log
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
for(let i = 0; i < numConnections; i++){
|
|
79
|
-
const baseUrl = urls[i % urls.length];
|
|
80
|
-
const isBrowser = (0, _index1.envIsBrowserOrWorker)();
|
|
81
|
-
const url = isBrowser ? (0, _index1.addAuthTokenToWebSocketUrl)(baseUrl, token) : baseUrl;
|
|
82
|
-
if (!url) {
|
|
83
|
-
throw new Error(`URLs must not be null or empty`);
|
|
74
|
+
const pool = new WebSocketPool(log);
|
|
75
|
+
try {
|
|
76
|
+
const numConnections = config.numConnections ?? DEFAULT_NUM_CONNECTIONS;
|
|
77
|
+
// bind a handler to capture any emitted errors and send them to the user-provided
|
|
78
|
+
// onWebSocketPoolError callback (if it is present)
|
|
79
|
+
if (typeof config.onWebSocketPoolError === "function") {
|
|
80
|
+
pool.on("error", config.onWebSocketPoolError);
|
|
81
|
+
pool.once("shutdown", ()=>{
|
|
82
|
+
// unbind all error handlers so we don't leak memory
|
|
83
|
+
pool.off("error");
|
|
84
|
+
});
|
|
84
85
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
86
|
+
for(let i = 0; i < numConnections; i++){
|
|
87
|
+
const baseUrl = urls[i % urls.length];
|
|
88
|
+
const isBrowser = (0, _index1.envIsBrowserOrWorker)();
|
|
89
|
+
const url = isBrowser ? (0, _index1.addAuthTokenToWebSocketUrl)(baseUrl, token) : baseUrl;
|
|
90
|
+
if (!url) {
|
|
91
|
+
throw new Error(`URLs must not be null or empty`);
|
|
89
92
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
logger: log,
|
|
95
|
-
wsOptions
|
|
96
|
-
});
|
|
97
|
-
const connectionIndex = i;
|
|
98
|
-
const connectionEndpoint = url;
|
|
99
|
-
// If a websocket client unexpectedly disconnects, ResilientWebSocket will reestablish
|
|
100
|
-
// the connection and call the onReconnect callback.
|
|
101
|
-
rws.onReconnect = ()=>{
|
|
102
|
-
if (rws.wsUserClosed || pool.isShutdown || pool.abortSignal?.aborted) {
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
for (const listener of pool.connectionReconnectListeners){
|
|
106
|
-
listener(connectionIndex, connectionEndpoint);
|
|
107
|
-
}
|
|
108
|
-
for (const [, request] of pool.subscriptions){
|
|
109
|
-
try {
|
|
110
|
-
rws.send(JSON.stringify(request));
|
|
111
|
-
} catch (error) {
|
|
112
|
-
pool.logger.error("Failed to resend subscription on reconnect:", error);
|
|
93
|
+
const wsOptions = {
|
|
94
|
+
...config.rwsConfig?.wsOptions,
|
|
95
|
+
headers: isBrowser ? undefined : {
|
|
96
|
+
Authorization: `Bearer ${token}`
|
|
113
97
|
}
|
|
98
|
+
};
|
|
99
|
+
const rws = new _resilientwebsocket.ResilientWebSocket({
|
|
100
|
+
...config.rwsConfig,
|
|
101
|
+
endpoint: url,
|
|
102
|
+
logger: log,
|
|
103
|
+
wsOptions
|
|
104
|
+
});
|
|
105
|
+
const connectionIndex = i;
|
|
106
|
+
const connectionEndpoint = url;
|
|
107
|
+
// If a websocket client unexpectedly disconnects, ResilientWebSocket will reestablish
|
|
108
|
+
// the connection and call the onReconnect callback.
|
|
109
|
+
rws.onReconnect = ()=>{
|
|
110
|
+
if (rws.wsUserClosed || pool.isShutdown) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
for (const listener of pool.connectionReconnectListeners){
|
|
114
|
+
listener(connectionIndex, connectionEndpoint);
|
|
115
|
+
}
|
|
116
|
+
for (const [, request] of pool.subscriptions){
|
|
117
|
+
try {
|
|
118
|
+
rws.send(JSON.stringify(request));
|
|
119
|
+
} catch (error) {
|
|
120
|
+
pool.logger.error("Failed to resend subscription on reconnect:", error);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
rws.onTimeout = ()=>{
|
|
125
|
+
for (const listener of pool.connectionTimeoutListeners){
|
|
126
|
+
listener(connectionIndex, connectionEndpoint);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
130
|
+
const onErrorHandler = config.onWebSocketError ?? config.onError;
|
|
131
|
+
if (typeof onErrorHandler === "function") {
|
|
132
|
+
rws.onError = onErrorHandler;
|
|
114
133
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (typeof onErrorHandler === "function") {
|
|
124
|
-
rws.onError = onErrorHandler;
|
|
134
|
+
// Handle all client messages ourselves. Dedupe before sending to registered message handlers.
|
|
135
|
+
rws.onMessage = (data)=>{
|
|
136
|
+
pool.dedupeHandler(data).catch((error)=>{
|
|
137
|
+
pool.emitPoolError(error, "Error in WebSocketPool dedupeHandler");
|
|
138
|
+
});
|
|
139
|
+
};
|
|
140
|
+
pool.rwsPool.push(rws);
|
|
141
|
+
rws.startWebSocket();
|
|
125
142
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
});
|
|
131
|
-
};
|
|
132
|
-
pool.rwsPool.push(rws);
|
|
133
|
-
rws.startWebSocket();
|
|
134
|
-
}
|
|
135
|
-
pool.logger.info(`Started WebSocketPool with ${numConnections.toString()} connections. Waiting for at least one to connect...`);
|
|
136
|
-
while(!pool.isAnyConnectionEstablished()){
|
|
137
|
-
if (pool.abortSignal?.aborted) {
|
|
138
|
-
pool.logger.warn("the WebSocket Pool's abort signal was aborted during connection. Shutting down.");
|
|
139
|
-
pool.shutdown();
|
|
140
|
-
return pool;
|
|
143
|
+
pool.logger.info(`Started WebSocketPool with ${numConnections.toString()} connections. Waiting for at least one to connect...`);
|
|
144
|
+
while(!pool.isAnyConnectionEstablished() && !pool.isShutdown){
|
|
145
|
+
throwIfAborted();
|
|
146
|
+
await new Promise((resolve)=>setTimeout(resolve, 100));
|
|
141
147
|
}
|
|
142
|
-
|
|
148
|
+
// Final check after loop exits
|
|
149
|
+
throwIfAborted();
|
|
150
|
+
pool.logger.info(`At least one WebSocket connection is established. WebSocketPool is ready.`);
|
|
151
|
+
return pool;
|
|
152
|
+
} catch (error) {
|
|
153
|
+
// Clean up the pool if aborted during connection wait
|
|
154
|
+
pool.shutdown();
|
|
155
|
+
throw error;
|
|
143
156
|
}
|
|
144
|
-
pool.logger.info(`At least one WebSocket connection is established. WebSocketPool is ready.`);
|
|
145
|
-
return pool;
|
|
146
157
|
}
|
|
147
158
|
emitPoolError(error, context) {
|
|
148
159
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
@@ -194,8 +205,8 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
|
|
|
194
205
|
}
|
|
195
206
|
};
|
|
196
207
|
sendRequest(request) {
|
|
197
|
-
if (this.isShutdown
|
|
198
|
-
this.logger.warn("Cannot send request: WebSocketPool is shutdown
|
|
208
|
+
if (this.isShutdown) {
|
|
209
|
+
this.logger.warn("Cannot send request: WebSocketPool is shutdown");
|
|
199
210
|
return;
|
|
200
211
|
}
|
|
201
212
|
for (const rws of this.rwsPool){
|
|
@@ -207,8 +218,8 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
|
|
|
207
218
|
}
|
|
208
219
|
}
|
|
209
220
|
addSubscription(request) {
|
|
210
|
-
if (this.isShutdown
|
|
211
|
-
this.logger.warn("Cannot add subscription: WebSocketPool is shutdown
|
|
221
|
+
if (this.isShutdown) {
|
|
222
|
+
this.logger.warn("Cannot add subscription: WebSocketPool is shutdown");
|
|
212
223
|
return;
|
|
213
224
|
}
|
|
214
225
|
if (request.type !== "subscribe") {
|
|
@@ -219,8 +230,8 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
|
|
|
219
230
|
this.sendRequest(request);
|
|
220
231
|
}
|
|
221
232
|
removeSubscription(subscriptionId) {
|
|
222
|
-
if (this.isShutdown
|
|
223
|
-
this.logger.warn("Cannot remove subscription: WebSocketPool is shutdown
|
|
233
|
+
if (this.isShutdown) {
|
|
234
|
+
this.logger.warn("Cannot remove subscription: WebSocketPool is shutdown");
|
|
224
235
|
return;
|
|
225
236
|
}
|
|
226
237
|
this.subscriptions.delete(subscriptionId);
|
|
@@ -263,8 +274,8 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
|
|
|
263
274
|
return this.rwsPool.some((ws)=>ws.isConnected());
|
|
264
275
|
}
|
|
265
276
|
checkConnectionStates() {
|
|
266
|
-
// Stop monitoring if shutdown
|
|
267
|
-
if (this.isShutdown
|
|
277
|
+
// Stop monitoring if shutdown
|
|
278
|
+
if (this.isShutdown) {
|
|
268
279
|
return;
|
|
269
280
|
}
|
|
270
281
|
const allDown = this.areAllConnectionsDown();
|
|
@@ -46,7 +46,6 @@ export type WebSocketPoolEvents = {
|
|
|
46
46
|
};
|
|
47
47
|
export declare class WebSocketPool extends IsomorphicEventEmitter<WebSocketPoolEvents> {
|
|
48
48
|
private readonly logger;
|
|
49
|
-
private readonly abortSignal?;
|
|
50
49
|
rwsPool: ResilientWebSocket[];
|
|
51
50
|
private cache;
|
|
52
51
|
private subscriptions;
|
|
@@ -67,7 +66,7 @@ export declare class WebSocketPool extends IsomorphicEventEmitter<WebSocketPoolE
|
|
|
67
66
|
* @param numConnections - Number of parallel WebSocket connections to maintain (default: 3)
|
|
68
67
|
* @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
|
|
69
68
|
*/
|
|
70
|
-
static create(config: WebSocketPoolConfig, token: string, abortSignal
|
|
69
|
+
static create(config: WebSocketPoolConfig, token: string, abortSignal?: AbortSignal | null | undefined, logger?: Logger): Promise<WebSocketPool>;
|
|
71
70
|
private emitPoolError;
|
|
72
71
|
/**
|
|
73
72
|
* Checks for error responses in JSON messages and throws appropriate errors
|
package/dist/esm/client.d.ts
CHANGED
|
@@ -18,8 +18,9 @@ export type JsonOrBinaryResponse = {
|
|
|
18
18
|
};
|
|
19
19
|
export type LazerClientConfig = {
|
|
20
20
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
21
|
+
* If provided, allows aborting the client creation process.
|
|
22
|
+
* Once the client is successfully created, this signal has no effect on the client's lifecycle.
|
|
23
|
+
* When aborted, the promise will reject with a DOMException with name 'AbortError'.
|
|
23
24
|
*/
|
|
24
25
|
abortSignal?: AbortSignal;
|
|
25
26
|
token: string;
|
|
@@ -34,7 +35,6 @@ export declare class PythLazerClient {
|
|
|
34
35
|
private priceServiceUrl;
|
|
35
36
|
private token;
|
|
36
37
|
private wsp;
|
|
37
|
-
private abortSignal;
|
|
38
38
|
private constructor();
|
|
39
39
|
/**
|
|
40
40
|
* Creates a new PythLazerClient instance.
|
|
@@ -48,10 +48,6 @@ export declare class PythLazerClient {
|
|
|
48
48
|
* or a binary response containing EVM, Solana, or parsed payload data.
|
|
49
49
|
*/
|
|
50
50
|
addMessageListener(handler: (event: JsonOrBinaryResponse) => void): void;
|
|
51
|
-
/**
|
|
52
|
-
* binds any internal event handlers
|
|
53
|
-
*/
|
|
54
|
-
bindHandlers(): void;
|
|
55
51
|
subscribe(request: Request): void;
|
|
56
52
|
unsubscribe(subscriptionId: number): void;
|
|
57
53
|
send(request: Request): void;
|
|
@@ -76,10 +72,6 @@ export declare class PythLazerClient {
|
|
|
76
72
|
* @param handler - Function to be called with connection index and endpoint URL
|
|
77
73
|
*/
|
|
78
74
|
addConnectionReconnectListener(handler: (connectionIndex: number, endpoint: string) => void): void;
|
|
79
|
-
/**
|
|
80
|
-
* called if and only if a user provided an abort signal and it was aborted
|
|
81
|
-
*/
|
|
82
|
-
protected abortHandler: () => void;
|
|
83
75
|
shutdown(): void;
|
|
84
76
|
/**
|
|
85
77
|
* Private helper method to make authenticated HTTP requests with Bearer token
|
package/dist/esm/client.mjs
CHANGED
|
@@ -12,39 +12,64 @@ export class PythLazerClient {
|
|
|
12
12
|
priceServiceUrl;
|
|
13
13
|
token;
|
|
14
14
|
wsp;
|
|
15
|
-
|
|
16
|
-
constructor({ abortSignal, logger, metadataServiceUrl, priceServiceUrl, token, wsp }){
|
|
17
|
-
this.abortSignal = abortSignal;
|
|
15
|
+
constructor({ logger, metadataServiceUrl, priceServiceUrl, token, wsp }){
|
|
18
16
|
this.logger = logger;
|
|
19
17
|
this.metadataServiceUrl = metadataServiceUrl;
|
|
20
18
|
this.priceServiceUrl = priceServiceUrl;
|
|
21
19
|
this.token = token;
|
|
22
20
|
this.wsp = wsp;
|
|
23
|
-
this.bindHandlers();
|
|
24
21
|
}
|
|
25
22
|
/**
|
|
26
23
|
* Creates a new PythLazerClient instance.
|
|
27
24
|
* @param config - Configuration including token, metadata service URL, and price service URL, and WebSocket pool configuration
|
|
28
25
|
*/ static async create(config) {
|
|
26
|
+
let wsp = undefined;
|
|
27
|
+
let client = undefined;
|
|
28
|
+
const { abortSignal } = config;
|
|
29
|
+
const handleAbort = ()=>{
|
|
30
|
+
// we need to EXPLICITLY shutdown both items,
|
|
31
|
+
// because there is a possibility the client was undefined
|
|
32
|
+
// when the abort signal was called
|
|
33
|
+
client?.shutdown();
|
|
34
|
+
wsp?.shutdown();
|
|
35
|
+
abortSignal?.removeEventListener("abort", handleAbort);
|
|
36
|
+
};
|
|
37
|
+
const throwIfAborted = ()=>{
|
|
38
|
+
if (abortSignal?.aborted) {
|
|
39
|
+
throw new DOMException("PythLazerClient.create() was aborted", "AbortError");
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
abortSignal?.addEventListener("abort", handleAbort);
|
|
29
43
|
const token = config.token;
|
|
30
44
|
// Collect and remove trailing slash from URLs
|
|
31
45
|
const metadataServiceUrl = (config.metadataServiceUrl ?? DEFAULT_METADATA_SERVICE_URL).replace(/\/+$/, "");
|
|
32
46
|
const priceServiceUrl = (config.priceServiceUrl ?? DEFAULT_PRICE_SERVICE_URL).replace(/\/+$/, "");
|
|
33
47
|
const logger = config.logger ?? dummyLogger;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
+
try {
|
|
49
|
+
throwIfAborted();
|
|
50
|
+
// the prior API was mismatched, in that it marked a websocket pool as optional,
|
|
51
|
+
// yet all internal code on the Pyth Pro client used it and threw if it didn't exist.
|
|
52
|
+
// now, the typings indicate it's no longer optional and we don't sanity check
|
|
53
|
+
// if it's set
|
|
54
|
+
wsp = await WebSocketPool.create(config.webSocketPoolConfig, token, abortSignal, logger);
|
|
55
|
+
throwIfAborted();
|
|
56
|
+
client = new PythLazerClient({
|
|
57
|
+
logger,
|
|
58
|
+
metadataServiceUrl,
|
|
59
|
+
priceServiceUrl,
|
|
60
|
+
token,
|
|
61
|
+
wsp
|
|
62
|
+
});
|
|
63
|
+
throwIfAborted();
|
|
64
|
+
// detach the abortSignal handler here, because we've made it!
|
|
65
|
+
abortSignal?.removeEventListener("abort", handleAbort);
|
|
66
|
+
return client;
|
|
67
|
+
} catch (error) {
|
|
68
|
+
client?.shutdown();
|
|
69
|
+
wsp?.shutdown();
|
|
70
|
+
abortSignal?.removeEventListener("abort", handleAbort);
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
48
73
|
}
|
|
49
74
|
/**
|
|
50
75
|
* Adds a message listener that receives either JSON or binary responses from the WebSocket connections.
|
|
@@ -98,11 +123,6 @@ export class PythLazerClient {
|
|
|
98
123
|
});
|
|
99
124
|
});
|
|
100
125
|
}
|
|
101
|
-
/**
|
|
102
|
-
* binds any internal event handlers
|
|
103
|
-
*/ bindHandlers() {
|
|
104
|
-
this.abortSignal?.addEventListener("abort", this.abortHandler);
|
|
105
|
-
}
|
|
106
126
|
subscribe(request) {
|
|
107
127
|
if (request.type !== "subscribe") {
|
|
108
128
|
throw new Error("Request must be a subscribe request");
|
|
@@ -140,14 +160,7 @@ export class PythLazerClient {
|
|
|
140
160
|
*/ addConnectionReconnectListener(handler) {
|
|
141
161
|
this.wsp.addConnectionReconnectListener(handler);
|
|
142
162
|
}
|
|
143
|
-
/**
|
|
144
|
-
* called if and only if a user provided an abort signal and it was aborted
|
|
145
|
-
*/ abortHandler = ()=>{
|
|
146
|
-
this.shutdown();
|
|
147
|
-
};
|
|
148
163
|
shutdown() {
|
|
149
|
-
// Clean up abort signal listener to prevent memory leak
|
|
150
|
-
this.abortSignal?.removeEventListener("abort", this.abortHandler);
|
|
151
164
|
this.wsp.shutdown();
|
|
152
165
|
}
|
|
153
166
|
/**
|
|
@@ -46,7 +46,6 @@ export type WebSocketPoolEvents = {
|
|
|
46
46
|
};
|
|
47
47
|
export declare class WebSocketPool extends IsomorphicEventEmitter<WebSocketPoolEvents> {
|
|
48
48
|
private readonly logger;
|
|
49
|
-
private readonly abortSignal?;
|
|
50
49
|
rwsPool: ResilientWebSocket[];
|
|
51
50
|
private cache;
|
|
52
51
|
private subscriptions;
|
|
@@ -67,7 +66,7 @@ export declare class WebSocketPool extends IsomorphicEventEmitter<WebSocketPoolE
|
|
|
67
66
|
* @param numConnections - Number of parallel WebSocket connections to maintain (default: 3)
|
|
68
67
|
* @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
|
|
69
68
|
*/
|
|
70
|
-
static create(config: WebSocketPoolConfig, token: string, abortSignal
|
|
69
|
+
static create(config: WebSocketPoolConfig, token: string, abortSignal?: AbortSignal | null | undefined, logger?: Logger): Promise<WebSocketPool>;
|
|
71
70
|
private emitPoolError;
|
|
72
71
|
/**
|
|
73
72
|
* Checks for error responses in JSON messages and throws appropriate errors
|
|
@@ -7,7 +7,6 @@ import { ResilientWebSocket } from "./resilient-websocket.mjs";
|
|
|
7
7
|
const DEFAULT_NUM_CONNECTIONS = 4;
|
|
8
8
|
export class WebSocketPool extends IsomorphicEventEmitter {
|
|
9
9
|
logger;
|
|
10
|
-
abortSignal;
|
|
11
10
|
rwsPool;
|
|
12
11
|
cache;
|
|
13
12
|
subscriptions;
|
|
@@ -19,8 +18,8 @@ export class WebSocketPool extends IsomorphicEventEmitter {
|
|
|
19
18
|
wasAllDown = true;
|
|
20
19
|
checkConnectionStatesInterval;
|
|
21
20
|
isShutdown = false;
|
|
22
|
-
constructor(logger
|
|
23
|
-
super(), this.logger = logger
|
|
21
|
+
constructor(logger){
|
|
22
|
+
super(), this.logger = logger;
|
|
24
23
|
this.rwsPool = [];
|
|
25
24
|
this.cache = new TTLCache({
|
|
26
25
|
ttl: 1000 * 10
|
|
@@ -44,90 +43,102 @@ export class WebSocketPool extends IsomorphicEventEmitter {
|
|
|
44
43
|
* @param numConnections - Number of parallel WebSocket connections to maintain (default: 3)
|
|
45
44
|
* @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
|
|
46
45
|
*/ static async create(config, token, abortSignal, logger) {
|
|
46
|
+
// Helper to check if aborted and throw
|
|
47
|
+
const throwIfAborted = ()=>{
|
|
48
|
+
if (abortSignal?.aborted) {
|
|
49
|
+
throw new DOMException("WebSocketPool.create() was aborted", "AbortError");
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
// Check before starting
|
|
53
|
+
throwIfAborted();
|
|
47
54
|
const urls = config.urls ?? [
|
|
48
55
|
DEFAULT_STREAM_SERVICE_0_URL,
|
|
49
56
|
DEFAULT_STREAM_SERVICE_1_URL
|
|
50
57
|
];
|
|
51
58
|
const log = logger ?? dummyLogger;
|
|
52
|
-
const pool = new WebSocketPool(log
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
for(let i = 0; i < numConnections; i++){
|
|
64
|
-
const baseUrl = urls[i % urls.length];
|
|
65
|
-
const isBrowser = envIsBrowserOrWorker();
|
|
66
|
-
const url = isBrowser ? addAuthTokenToWebSocketUrl(baseUrl, token) : baseUrl;
|
|
67
|
-
if (!url) {
|
|
68
|
-
throw new Error(`URLs must not be null or empty`);
|
|
59
|
+
const pool = new WebSocketPool(log);
|
|
60
|
+
try {
|
|
61
|
+
const numConnections = config.numConnections ?? DEFAULT_NUM_CONNECTIONS;
|
|
62
|
+
// bind a handler to capture any emitted errors and send them to the user-provided
|
|
63
|
+
// onWebSocketPoolError callback (if it is present)
|
|
64
|
+
if (typeof config.onWebSocketPoolError === "function") {
|
|
65
|
+
pool.on("error", config.onWebSocketPoolError);
|
|
66
|
+
pool.once("shutdown", ()=>{
|
|
67
|
+
// unbind all error handlers so we don't leak memory
|
|
68
|
+
pool.off("error");
|
|
69
|
+
});
|
|
69
70
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
for(let i = 0; i < numConnections; i++){
|
|
72
|
+
const baseUrl = urls[i % urls.length];
|
|
73
|
+
const isBrowser = envIsBrowserOrWorker();
|
|
74
|
+
const url = isBrowser ? addAuthTokenToWebSocketUrl(baseUrl, token) : baseUrl;
|
|
75
|
+
if (!url) {
|
|
76
|
+
throw new Error(`URLs must not be null or empty`);
|
|
74
77
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
logger: log,
|
|
80
|
-
wsOptions
|
|
81
|
-
});
|
|
82
|
-
const connectionIndex = i;
|
|
83
|
-
const connectionEndpoint = url;
|
|
84
|
-
// If a websocket client unexpectedly disconnects, ResilientWebSocket will reestablish
|
|
85
|
-
// the connection and call the onReconnect callback.
|
|
86
|
-
rws.onReconnect = ()=>{
|
|
87
|
-
if (rws.wsUserClosed || pool.isShutdown || pool.abortSignal?.aborted) {
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
for (const listener of pool.connectionReconnectListeners){
|
|
91
|
-
listener(connectionIndex, connectionEndpoint);
|
|
92
|
-
}
|
|
93
|
-
for (const [, request] of pool.subscriptions){
|
|
94
|
-
try {
|
|
95
|
-
rws.send(JSON.stringify(request));
|
|
96
|
-
} catch (error) {
|
|
97
|
-
pool.logger.error("Failed to resend subscription on reconnect:", error);
|
|
78
|
+
const wsOptions = {
|
|
79
|
+
...config.rwsConfig?.wsOptions,
|
|
80
|
+
headers: isBrowser ? undefined : {
|
|
81
|
+
Authorization: `Bearer ${token}`
|
|
98
82
|
}
|
|
83
|
+
};
|
|
84
|
+
const rws = new ResilientWebSocket({
|
|
85
|
+
...config.rwsConfig,
|
|
86
|
+
endpoint: url,
|
|
87
|
+
logger: log,
|
|
88
|
+
wsOptions
|
|
89
|
+
});
|
|
90
|
+
const connectionIndex = i;
|
|
91
|
+
const connectionEndpoint = url;
|
|
92
|
+
// If a websocket client unexpectedly disconnects, ResilientWebSocket will reestablish
|
|
93
|
+
// the connection and call the onReconnect callback.
|
|
94
|
+
rws.onReconnect = ()=>{
|
|
95
|
+
if (rws.wsUserClosed || pool.isShutdown) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
for (const listener of pool.connectionReconnectListeners){
|
|
99
|
+
listener(connectionIndex, connectionEndpoint);
|
|
100
|
+
}
|
|
101
|
+
for (const [, request] of pool.subscriptions){
|
|
102
|
+
try {
|
|
103
|
+
rws.send(JSON.stringify(request));
|
|
104
|
+
} catch (error) {
|
|
105
|
+
pool.logger.error("Failed to resend subscription on reconnect:", error);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
rws.onTimeout = ()=>{
|
|
110
|
+
for (const listener of pool.connectionTimeoutListeners){
|
|
111
|
+
listener(connectionIndex, connectionEndpoint);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
115
|
+
const onErrorHandler = config.onWebSocketError ?? config.onError;
|
|
116
|
+
if (typeof onErrorHandler === "function") {
|
|
117
|
+
rws.onError = onErrorHandler;
|
|
99
118
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if (typeof onErrorHandler === "function") {
|
|
109
|
-
rws.onError = onErrorHandler;
|
|
119
|
+
// Handle all client messages ourselves. Dedupe before sending to registered message handlers.
|
|
120
|
+
rws.onMessage = (data)=>{
|
|
121
|
+
pool.dedupeHandler(data).catch((error)=>{
|
|
122
|
+
pool.emitPoolError(error, "Error in WebSocketPool dedupeHandler");
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
pool.rwsPool.push(rws);
|
|
126
|
+
rws.startWebSocket();
|
|
110
127
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
});
|
|
116
|
-
};
|
|
117
|
-
pool.rwsPool.push(rws);
|
|
118
|
-
rws.startWebSocket();
|
|
119
|
-
}
|
|
120
|
-
pool.logger.info(`Started WebSocketPool with ${numConnections.toString()} connections. Waiting for at least one to connect...`);
|
|
121
|
-
while(!pool.isAnyConnectionEstablished()){
|
|
122
|
-
if (pool.abortSignal?.aborted) {
|
|
123
|
-
pool.logger.warn("the WebSocket Pool's abort signal was aborted during connection. Shutting down.");
|
|
124
|
-
pool.shutdown();
|
|
125
|
-
return pool;
|
|
128
|
+
pool.logger.info(`Started WebSocketPool with ${numConnections.toString()} connections. Waiting for at least one to connect...`);
|
|
129
|
+
while(!pool.isAnyConnectionEstablished() && !pool.isShutdown){
|
|
130
|
+
throwIfAborted();
|
|
131
|
+
await new Promise((resolve)=>setTimeout(resolve, 100));
|
|
126
132
|
}
|
|
127
|
-
|
|
133
|
+
// Final check after loop exits
|
|
134
|
+
throwIfAborted();
|
|
135
|
+
pool.logger.info(`At least one WebSocket connection is established. WebSocketPool is ready.`);
|
|
136
|
+
return pool;
|
|
137
|
+
} catch (error) {
|
|
138
|
+
// Clean up the pool if aborted during connection wait
|
|
139
|
+
pool.shutdown();
|
|
140
|
+
throw error;
|
|
128
141
|
}
|
|
129
|
-
pool.logger.info(`At least one WebSocket connection is established. WebSocketPool is ready.`);
|
|
130
|
-
return pool;
|
|
131
142
|
}
|
|
132
143
|
emitPoolError(error, context) {
|
|
133
144
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
@@ -179,8 +190,8 @@ export class WebSocketPool extends IsomorphicEventEmitter {
|
|
|
179
190
|
}
|
|
180
191
|
};
|
|
181
192
|
sendRequest(request) {
|
|
182
|
-
if (this.isShutdown
|
|
183
|
-
this.logger.warn("Cannot send request: WebSocketPool is shutdown
|
|
193
|
+
if (this.isShutdown) {
|
|
194
|
+
this.logger.warn("Cannot send request: WebSocketPool is shutdown");
|
|
184
195
|
return;
|
|
185
196
|
}
|
|
186
197
|
for (const rws of this.rwsPool){
|
|
@@ -192,8 +203,8 @@ export class WebSocketPool extends IsomorphicEventEmitter {
|
|
|
192
203
|
}
|
|
193
204
|
}
|
|
194
205
|
addSubscription(request) {
|
|
195
|
-
if (this.isShutdown
|
|
196
|
-
this.logger.warn("Cannot add subscription: WebSocketPool is shutdown
|
|
206
|
+
if (this.isShutdown) {
|
|
207
|
+
this.logger.warn("Cannot add subscription: WebSocketPool is shutdown");
|
|
197
208
|
return;
|
|
198
209
|
}
|
|
199
210
|
if (request.type !== "subscribe") {
|
|
@@ -204,8 +215,8 @@ export class WebSocketPool extends IsomorphicEventEmitter {
|
|
|
204
215
|
this.sendRequest(request);
|
|
205
216
|
}
|
|
206
217
|
removeSubscription(subscriptionId) {
|
|
207
|
-
if (this.isShutdown
|
|
208
|
-
this.logger.warn("Cannot remove subscription: WebSocketPool is shutdown
|
|
218
|
+
if (this.isShutdown) {
|
|
219
|
+
this.logger.warn("Cannot remove subscription: WebSocketPool is shutdown");
|
|
209
220
|
return;
|
|
210
221
|
}
|
|
211
222
|
this.subscriptions.delete(subscriptionId);
|
|
@@ -248,8 +259,8 @@ export class WebSocketPool extends IsomorphicEventEmitter {
|
|
|
248
259
|
return this.rwsPool.some((ws)=>ws.isConnected());
|
|
249
260
|
}
|
|
250
261
|
checkConnectionStates() {
|
|
251
|
-
// Stop monitoring if shutdown
|
|
252
|
-
if (this.isShutdown
|
|
262
|
+
// Stop monitoring if shutdown
|
|
263
|
+
if (this.isShutdown) {
|
|
253
264
|
return;
|
|
254
265
|
}
|
|
255
266
|
const allDown = this.areAllConnectionsDown();
|
package/package.json
CHANGED