@pythnetwork/pyth-lazer-sdk 6.0.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 +89 -52
- package/dist/cjs/client.d.ts +28 -13
- package/dist/cjs/index.cjs +1 -1
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/protocol.cjs +3 -3
- package/dist/cjs/protocol.d.ts +3 -3
- package/dist/cjs/socket/resilient-websocket.cjs +5 -0
- package/dist/cjs/socket/resilient-websocket.d.ts +1 -0
- package/dist/cjs/socket/websocket-pool.cjs +145 -62
- package/dist/cjs/socket/websocket-pool.d.ts +21 -3
- package/dist/esm/client.d.ts +28 -13
- package/dist/esm/client.mjs +89 -52
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.mjs +1 -1
- package/dist/esm/protocol.d.ts +3 -3
- package/dist/esm/protocol.mjs +3 -3
- package/dist/esm/socket/resilient-websocket.d.ts +1 -0
- package/dist/esm/socket/resilient-websocket.mjs +5 -0
- package/dist/esm/socket/websocket-pool.d.ts +21 -3
- package/dist/esm/socket/websocket-pool.mjs +145 -62
- package/package.json +113 -123
|
@@ -10,10 +10,10 @@ Object.defineProperty(exports, "WebSocketPool", {
|
|
|
10
10
|
});
|
|
11
11
|
const _ttlcache = /*#__PURE__*/ _interop_require_default(require("@isaacs/ttlcache"));
|
|
12
12
|
const _tslog = require("ts-log");
|
|
13
|
-
const _resilientwebsocket = require("./resilient-websocket.cjs");
|
|
14
13
|
const _constants = require("../constants.cjs");
|
|
15
14
|
const _index = require("../emitter/index.cjs");
|
|
16
15
|
const _index1 = require("../util/index.cjs");
|
|
16
|
+
const _resilientwebsocket = require("./resilient-websocket.cjs");
|
|
17
17
|
function _interop_require_default(obj) {
|
|
18
18
|
return obj && obj.__esModule ? obj : {
|
|
19
19
|
default: obj
|
|
@@ -27,8 +27,12 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
|
|
|
27
27
|
subscriptions;
|
|
28
28
|
messageListeners;
|
|
29
29
|
allConnectionsDownListeners;
|
|
30
|
+
connectionRestoredListeners;
|
|
31
|
+
connectionTimeoutListeners;
|
|
32
|
+
connectionReconnectListeners;
|
|
30
33
|
wasAllDown = true;
|
|
31
34
|
checkConnectionStatesInterval;
|
|
35
|
+
isShutdown = false;
|
|
32
36
|
constructor(logger){
|
|
33
37
|
super(), this.logger = logger;
|
|
34
38
|
this.rwsPool = [];
|
|
@@ -38,6 +42,9 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
|
|
|
38
42
|
this.subscriptions = new Map();
|
|
39
43
|
this.messageListeners = [];
|
|
40
44
|
this.allConnectionsDownListeners = [];
|
|
45
|
+
this.connectionRestoredListeners = [];
|
|
46
|
+
this.connectionTimeoutListeners = [];
|
|
47
|
+
this.connectionReconnectListeners = [];
|
|
41
48
|
// Start monitoring connection states
|
|
42
49
|
this.checkConnectionStatesInterval = setInterval(()=>{
|
|
43
50
|
this.checkConnectionStates();
|
|
@@ -50,76 +57,103 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
|
|
|
50
57
|
* @param token - Authentication token to use for the connections
|
|
51
58
|
* @param numConnections - Number of parallel WebSocket connections to maintain (default: 3)
|
|
52
59
|
* @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) {
|
|
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();
|
|
54
69
|
const urls = config.urls ?? [
|
|
55
70
|
_constants.DEFAULT_STREAM_SERVICE_0_URL,
|
|
56
71
|
_constants.DEFAULT_STREAM_SERVICE_1_URL
|
|
57
72
|
];
|
|
58
73
|
const log = logger ?? _tslog.dummyLogger;
|
|
59
74
|
const pool = new WebSocketPool(log);
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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`);
|
|
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
|
+
});
|
|
76
85
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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;
|
|
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`);
|
|
94
92
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
93
|
+
const wsOptions = {
|
|
94
|
+
...config.rwsConfig?.wsOptions,
|
|
95
|
+
headers: isBrowser ? undefined : {
|
|
96
|
+
Authorization: `Bearer ${token}`
|
|
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);
|
|
100
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;
|
|
101
133
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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();
|
|
107
142
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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));
|
|
147
|
+
}
|
|
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;
|
|
120
156
|
}
|
|
121
|
-
pool.logger.info(`At least one WebSocket connection is established. WebSocketPool is ready.`);
|
|
122
|
-
return pool;
|
|
123
157
|
}
|
|
124
158
|
emitPoolError(error, context) {
|
|
125
159
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
@@ -171,6 +205,10 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
|
|
|
171
205
|
}
|
|
172
206
|
};
|
|
173
207
|
sendRequest(request) {
|
|
208
|
+
if (this.isShutdown) {
|
|
209
|
+
this.logger.warn("Cannot send request: WebSocketPool is shutdown");
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
174
212
|
for (const rws of this.rwsPool){
|
|
175
213
|
try {
|
|
176
214
|
rws.send(JSON.stringify(request));
|
|
@@ -180,6 +218,10 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
|
|
|
180
218
|
}
|
|
181
219
|
}
|
|
182
220
|
addSubscription(request) {
|
|
221
|
+
if (this.isShutdown) {
|
|
222
|
+
this.logger.warn("Cannot add subscription: WebSocketPool is shutdown");
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
183
225
|
if (request.type !== "subscribe") {
|
|
184
226
|
this.emitPoolError(new Error("Request must be a subscribe request"));
|
|
185
227
|
return;
|
|
@@ -188,10 +230,14 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
|
|
|
188
230
|
this.sendRequest(request);
|
|
189
231
|
}
|
|
190
232
|
removeSubscription(subscriptionId) {
|
|
233
|
+
if (this.isShutdown) {
|
|
234
|
+
this.logger.warn("Cannot remove subscription: WebSocketPool is shutdown");
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
191
237
|
this.subscriptions.delete(subscriptionId);
|
|
192
238
|
const request = {
|
|
193
|
-
|
|
194
|
-
|
|
239
|
+
subscriptionId,
|
|
240
|
+
type: "unsubscribe"
|
|
195
241
|
};
|
|
196
242
|
this.sendRequest(request);
|
|
197
243
|
}
|
|
@@ -204,6 +250,23 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
|
|
|
204
250
|
*/ addAllConnectionsDownListener(handler) {
|
|
205
251
|
this.allConnectionsDownListeners.push(handler);
|
|
206
252
|
}
|
|
253
|
+
/**
|
|
254
|
+
* Calls the handler when at least one connection is restored after all connections were down.
|
|
255
|
+
*/ addConnectionRestoredListener(handler) {
|
|
256
|
+
this.connectionRestoredListeners.push(handler);
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Calls the handler when an individual connection times out (heartbeat timeout).
|
|
260
|
+
* @param handler - Callback with connection index and endpoint URL
|
|
261
|
+
*/ addConnectionTimeoutListener(handler) {
|
|
262
|
+
this.connectionTimeoutListeners.push(handler);
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Calls the handler when an individual connection reconnects after being down.
|
|
266
|
+
* @param handler - Callback with connection index and endpoint URL
|
|
267
|
+
*/ addConnectionReconnectListener(handler) {
|
|
268
|
+
this.connectionReconnectListeners.push(handler);
|
|
269
|
+
}
|
|
207
270
|
areAllConnectionsDown() {
|
|
208
271
|
return this.rwsPool.every((ws)=>!ws.isConnected() || ws.isReconnecting());
|
|
209
272
|
}
|
|
@@ -211,6 +274,10 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
|
|
|
211
274
|
return this.rwsPool.some((ws)=>ws.isConnected());
|
|
212
275
|
}
|
|
213
276
|
checkConnectionStates() {
|
|
277
|
+
// Stop monitoring if shutdown
|
|
278
|
+
if (this.isShutdown) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
214
281
|
const allDown = this.areAllConnectionsDown();
|
|
215
282
|
// If all connections just went down
|
|
216
283
|
if (allDown && !this.wasAllDown) {
|
|
@@ -225,12 +292,25 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
|
|
|
225
292
|
}
|
|
226
293
|
}
|
|
227
294
|
}
|
|
228
|
-
// If at least one connection was restored
|
|
295
|
+
// If at least one connection was restored after all were down
|
|
229
296
|
if (!allDown && this.wasAllDown) {
|
|
230
297
|
this.wasAllDown = false;
|
|
298
|
+
this.logger.info("At least one WebSocket connection restored");
|
|
299
|
+
for (const listener of this.connectionRestoredListeners){
|
|
300
|
+
try {
|
|
301
|
+
listener();
|
|
302
|
+
} catch (error) {
|
|
303
|
+
this.emitPoolError(error, "Connection-restored listener threw");
|
|
304
|
+
}
|
|
305
|
+
}
|
|
231
306
|
}
|
|
232
307
|
}
|
|
233
308
|
shutdown() {
|
|
309
|
+
// Prevent multiple shutdown calls
|
|
310
|
+
if (this.isShutdown) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
this.isShutdown = true;
|
|
234
314
|
for (const rws of this.rwsPool){
|
|
235
315
|
rws.closeWebSocket();
|
|
236
316
|
}
|
|
@@ -238,6 +318,9 @@ class WebSocketPool extends _index.IsomorphicEventEmitter {
|
|
|
238
318
|
this.subscriptions.clear();
|
|
239
319
|
this.messageListeners = [];
|
|
240
320
|
this.allConnectionsDownListeners = [];
|
|
321
|
+
this.connectionRestoredListeners = [];
|
|
322
|
+
this.connectionTimeoutListeners = [];
|
|
323
|
+
this.connectionReconnectListeners = [];
|
|
241
324
|
clearInterval(this.checkConnectionStatesInterval);
|
|
242
325
|
// execute all bound shutdown handlers
|
|
243
326
|
for (const shutdownHandler of this.getListeners("shutdown")){
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
+
import type WebSocket from "isomorphic-ws";
|
|
1
2
|
import type { ErrorEvent } from "isomorphic-ws";
|
|
2
|
-
import WebSocket from "isomorphic-ws";
|
|
3
3
|
import type { Logger } from "ts-log";
|
|
4
|
+
import { IsomorphicEventEmitter } from "../emitter/index.js";
|
|
4
5
|
import type { Request } from "../protocol.js";
|
|
5
6
|
import type { ResilientWebSocketConfig } from "./resilient-websocket.js";
|
|
6
7
|
import { ResilientWebSocket } from "./resilient-websocket.js";
|
|
7
|
-
import { IsomorphicEventEmitter } from "../emitter/index.js";
|
|
8
8
|
type WebSocketOnMessageCallback = (data: WebSocket.Data) => Promise<void>;
|
|
9
9
|
export type WebSocketPoolConfig = {
|
|
10
10
|
/**
|
|
@@ -51,8 +51,12 @@ export declare class WebSocketPool extends IsomorphicEventEmitter<WebSocketPoolE
|
|
|
51
51
|
private subscriptions;
|
|
52
52
|
private messageListeners;
|
|
53
53
|
private allConnectionsDownListeners;
|
|
54
|
+
private connectionRestoredListeners;
|
|
55
|
+
private connectionTimeoutListeners;
|
|
56
|
+
private connectionReconnectListeners;
|
|
54
57
|
private wasAllDown;
|
|
55
58
|
private checkConnectionStatesInterval;
|
|
59
|
+
private isShutdown;
|
|
56
60
|
private constructor();
|
|
57
61
|
/**
|
|
58
62
|
* Creates a new WebSocketPool instance that uses multiple redundant WebSocket connections for reliability.
|
|
@@ -62,7 +66,7 @@ export declare class WebSocketPool extends IsomorphicEventEmitter<WebSocketPoolE
|
|
|
62
66
|
* @param numConnections - Number of parallel WebSocket connections to maintain (default: 3)
|
|
63
67
|
* @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
|
|
64
68
|
*/
|
|
65
|
-
static create(config: WebSocketPoolConfig, token: string, logger?: Logger): Promise<WebSocketPool>;
|
|
69
|
+
static create(config: WebSocketPoolConfig, token: string, abortSignal?: AbortSignal | null | undefined, logger?: Logger): Promise<WebSocketPool>;
|
|
66
70
|
private emitPoolError;
|
|
67
71
|
/**
|
|
68
72
|
* Checks for error responses in JSON messages and throws appropriate errors
|
|
@@ -83,6 +87,20 @@ export declare class WebSocketPool extends IsomorphicEventEmitter<WebSocketPoolE
|
|
|
83
87
|
* The connections may still try to reconnect in the background.
|
|
84
88
|
*/
|
|
85
89
|
addAllConnectionsDownListener(handler: () => void): void;
|
|
90
|
+
/**
|
|
91
|
+
* Calls the handler when at least one connection is restored after all connections were down.
|
|
92
|
+
*/
|
|
93
|
+
addConnectionRestoredListener(handler: () => void): void;
|
|
94
|
+
/**
|
|
95
|
+
* Calls the handler when an individual connection times out (heartbeat timeout).
|
|
96
|
+
* @param handler - Callback with connection index and endpoint URL
|
|
97
|
+
*/
|
|
98
|
+
addConnectionTimeoutListener(handler: (connectionIndex: number, endpoint: string) => void): void;
|
|
99
|
+
/**
|
|
100
|
+
* Calls the handler when an individual connection reconnects after being down.
|
|
101
|
+
* @param handler - Callback with connection index and endpoint URL
|
|
102
|
+
*/
|
|
103
|
+
addConnectionReconnectListener(handler: (connectionIndex: number, endpoint: string) => void): void;
|
|
86
104
|
private areAllConnectionsDown;
|
|
87
105
|
private isAnyConnectionEstablished;
|
|
88
106
|
private checkConnectionStates;
|
package/dist/esm/client.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Logger } from "ts-log";
|
|
2
|
-
import type {
|
|
2
|
+
import type { JsonUpdate, LatestPriceRequest, ParsedPayload, PriceRequest, Request, Response, SymbolResponse, SymbolsQueryParams } from "./protocol.js";
|
|
3
3
|
import type { WebSocketPoolConfig } from "./socket/websocket-pool.js";
|
|
4
4
|
export type BinaryResponse = {
|
|
5
5
|
subscriptionId: number;
|
|
@@ -17,25 +17,25 @@ export type JsonOrBinaryResponse = {
|
|
|
17
17
|
value: BinaryResponse;
|
|
18
18
|
};
|
|
19
19
|
export type LazerClientConfig = {
|
|
20
|
+
/**
|
|
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'.
|
|
24
|
+
*/
|
|
25
|
+
abortSignal?: AbortSignal;
|
|
20
26
|
token: string;
|
|
21
27
|
metadataServiceUrl?: string;
|
|
22
28
|
priceServiceUrl?: string;
|
|
23
29
|
logger?: Logger;
|
|
24
|
-
webSocketPoolConfig
|
|
30
|
+
webSocketPoolConfig: WebSocketPoolConfig;
|
|
25
31
|
};
|
|
26
32
|
export declare class PythLazerClient {
|
|
27
|
-
private
|
|
28
|
-
private
|
|
29
|
-
private
|
|
30
|
-
private
|
|
31
|
-
private
|
|
33
|
+
private logger;
|
|
34
|
+
private metadataServiceUrl;
|
|
35
|
+
private priceServiceUrl;
|
|
36
|
+
private token;
|
|
37
|
+
private wsp;
|
|
32
38
|
private constructor();
|
|
33
|
-
/**
|
|
34
|
-
* Gets the WebSocket pool. If the WebSocket pool is not configured, an error is thrown.
|
|
35
|
-
* @throws Error if WebSocket pool is not configured
|
|
36
|
-
* @returns The WebSocket pool
|
|
37
|
-
*/
|
|
38
|
-
private getWebSocketPool;
|
|
39
39
|
/**
|
|
40
40
|
* Creates a new PythLazerClient instance.
|
|
41
41
|
* @param config - Configuration including token, metadata service URL, and price service URL, and WebSocket pool configuration
|
|
@@ -57,6 +57,21 @@ export declare class PythLazerClient {
|
|
|
57
57
|
* @param handler - Function to be called when all connections are down
|
|
58
58
|
*/
|
|
59
59
|
addAllConnectionsDownListener(handler: () => void): void;
|
|
60
|
+
/**
|
|
61
|
+
* Registers a handler function that will be called when at least one connection is restored after all were down.
|
|
62
|
+
* @param handler - Function to be called when connection is restored
|
|
63
|
+
*/
|
|
64
|
+
addConnectionRestoredListener(handler: () => void): void;
|
|
65
|
+
/**
|
|
66
|
+
* Registers a handler function that will be called when an individual connection times out (heartbeat timeout).
|
|
67
|
+
* @param handler - Function to be called with connection index and endpoint URL
|
|
68
|
+
*/
|
|
69
|
+
addConnectionTimeoutListener(handler: (connectionIndex: number, endpoint: string) => void): void;
|
|
70
|
+
/**
|
|
71
|
+
* Registers a handler function that will be called when an individual connection reconnects.
|
|
72
|
+
* @param handler - Function to be called with connection index and endpoint URL
|
|
73
|
+
*/
|
|
74
|
+
addConnectionReconnectListener(handler: (connectionIndex: number, endpoint: string) => void): void;
|
|
60
75
|
shutdown(): void;
|
|
61
76
|
/**
|
|
62
77
|
* Private helper method to make authenticated HTTP requests with Bearer token
|
package/dist/esm/client.mjs
CHANGED
|
@@ -7,43 +7,69 @@ const UINT16_NUM_BYTES = 2;
|
|
|
7
7
|
const UINT32_NUM_BYTES = 4;
|
|
8
8
|
const UINT64_NUM_BYTES = 8;
|
|
9
9
|
export class PythLazerClient {
|
|
10
|
-
|
|
10
|
+
logger;
|
|
11
11
|
metadataServiceUrl;
|
|
12
12
|
priceServiceUrl;
|
|
13
|
-
|
|
13
|
+
token;
|
|
14
14
|
wsp;
|
|
15
|
-
constructor(
|
|
16
|
-
this.
|
|
15
|
+
constructor({ logger, metadataServiceUrl, priceServiceUrl, token, wsp }){
|
|
16
|
+
this.logger = logger;
|
|
17
17
|
this.metadataServiceUrl = metadataServiceUrl;
|
|
18
18
|
this.priceServiceUrl = priceServiceUrl;
|
|
19
|
-
this.
|
|
19
|
+
this.token = token;
|
|
20
20
|
this.wsp = wsp;
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
|
-
* Gets the WebSocket pool. If the WebSocket pool is not configured, an error is thrown.
|
|
24
|
-
* @throws Error if WebSocket pool is not configured
|
|
25
|
-
* @returns The WebSocket pool
|
|
26
|
-
*/ getWebSocketPool() {
|
|
27
|
-
if (!this.wsp) {
|
|
28
|
-
throw new Error("WebSocket pool is not available. Make sure to provide webSocketPoolConfig when creating the client.");
|
|
29
|
-
}
|
|
30
|
-
return this.wsp;
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
23
|
* Creates a new PythLazerClient instance.
|
|
34
24
|
* @param config - Configuration including token, metadata service URL, and price service URL, and WebSocket pool configuration
|
|
35
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);
|
|
36
43
|
const token = config.token;
|
|
37
44
|
// Collect and remove trailing slash from URLs
|
|
38
45
|
const metadataServiceUrl = (config.metadataServiceUrl ?? DEFAULT_METADATA_SERVICE_URL).replace(/\/+$/, "");
|
|
39
46
|
const priceServiceUrl = (config.priceServiceUrl ?? DEFAULT_PRICE_SERVICE_URL).replace(/\/+$/, "");
|
|
40
47
|
const logger = config.logger ?? dummyLogger;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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;
|
|
45
72
|
}
|
|
46
|
-
return new PythLazerClient(token, metadataServiceUrl, priceServiceUrl, logger, wsp);
|
|
47
73
|
}
|
|
48
74
|
/**
|
|
49
75
|
* Adds a message listener that receives either JSON or binary responses from the WebSocket connections.
|
|
@@ -51,9 +77,8 @@ export class PythLazerClient {
|
|
|
51
77
|
* @param handler - Callback function that receives the parsed message. The message can be either a JSON response
|
|
52
78
|
* or a binary response containing EVM, Solana, or parsed payload data.
|
|
53
79
|
*/ addMessageListener(handler) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (typeof data == "string") {
|
|
80
|
+
this.wsp.addMessageListener(async (data)=>{
|
|
81
|
+
if (typeof data === "string") {
|
|
57
82
|
handler({
|
|
58
83
|
type: "json",
|
|
59
84
|
value: JSON.parse(data)
|
|
@@ -64,7 +89,7 @@ export class PythLazerClient {
|
|
|
64
89
|
let pos = 0;
|
|
65
90
|
const magic = buffData.subarray(pos, pos + UINT32_NUM_BYTES).readUint32LE();
|
|
66
91
|
pos += UINT32_NUM_BYTES;
|
|
67
|
-
if (magic
|
|
92
|
+
if (magic !== BINARY_UPDATE_FORMAT_MAGIC_LE) {
|
|
68
93
|
throw new Error("binary update format magic mismatch");
|
|
69
94
|
}
|
|
70
95
|
// TODO: some uint64 values may not be representable as Number.
|
|
@@ -77,15 +102,15 @@ export class PythLazerClient {
|
|
|
77
102
|
const len = buffData.subarray(pos, pos + UINT16_NUM_BYTES).readUint16BE();
|
|
78
103
|
pos += UINT16_NUM_BYTES;
|
|
79
104
|
const magic = buffData.subarray(pos, pos + UINT32_NUM_BYTES).readUint32LE();
|
|
80
|
-
if (magic
|
|
105
|
+
if (magic === FORMAT_MAGICS_LE.EVM) {
|
|
81
106
|
value.evm = buffData.subarray(pos, pos + len);
|
|
82
|
-
} else if (magic
|
|
107
|
+
} else if (magic === FORMAT_MAGICS_LE.SOLANA) {
|
|
83
108
|
value.solana = buffData.subarray(pos, pos + len);
|
|
84
|
-
} else if (magic
|
|
109
|
+
} else if (magic === FORMAT_MAGICS_LE.LE_ECDSA) {
|
|
85
110
|
value.leEcdsa = buffData.subarray(pos, pos + len);
|
|
86
|
-
} else if (magic
|
|
111
|
+
} else if (magic === FORMAT_MAGICS_LE.LE_UNSIGNED) {
|
|
87
112
|
value.leUnsigned = buffData.subarray(pos, pos + len);
|
|
88
|
-
} else if (magic
|
|
113
|
+
} else if (magic === FORMAT_MAGICS_LE.JSON) {
|
|
89
114
|
value.parsed = JSON.parse(buffData.subarray(pos + UINT32_NUM_BYTES, pos + len).toString());
|
|
90
115
|
} else {
|
|
91
116
|
throw new Error(`unknown magic: ${magic.toString()}`);
|
|
@@ -99,42 +124,54 @@ export class PythLazerClient {
|
|
|
99
124
|
});
|
|
100
125
|
}
|
|
101
126
|
subscribe(request) {
|
|
102
|
-
const wsp = this.getWebSocketPool();
|
|
103
127
|
if (request.type !== "subscribe") {
|
|
104
128
|
throw new Error("Request must be a subscribe request");
|
|
105
129
|
}
|
|
106
|
-
wsp.addSubscription(request);
|
|
130
|
+
this.wsp.addSubscription(request);
|
|
107
131
|
}
|
|
108
132
|
unsubscribe(subscriptionId) {
|
|
109
|
-
|
|
110
|
-
wsp.removeSubscription(subscriptionId);
|
|
133
|
+
this.wsp.removeSubscription(subscriptionId);
|
|
111
134
|
}
|
|
112
135
|
send(request) {
|
|
113
|
-
|
|
114
|
-
wsp.sendRequest(request);
|
|
136
|
+
this.wsp.sendRequest(request);
|
|
115
137
|
}
|
|
116
138
|
/**
|
|
117
139
|
* Registers a handler function that will be called whenever all WebSocket connections are down or attempting to reconnect.
|
|
118
140
|
* The connections may still try to reconnect in the background. To shut down the pool, call `shutdown()`.
|
|
119
141
|
* @param handler - Function to be called when all connections are down
|
|
120
142
|
*/ addAllConnectionsDownListener(handler) {
|
|
121
|
-
|
|
122
|
-
|
|
143
|
+
this.wsp.addAllConnectionsDownListener(handler);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Registers a handler function that will be called when at least one connection is restored after all were down.
|
|
147
|
+
* @param handler - Function to be called when connection is restored
|
|
148
|
+
*/ addConnectionRestoredListener(handler) {
|
|
149
|
+
this.wsp.addConnectionRestoredListener(handler);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Registers a handler function that will be called when an individual connection times out (heartbeat timeout).
|
|
153
|
+
* @param handler - Function to be called with connection index and endpoint URL
|
|
154
|
+
*/ addConnectionTimeoutListener(handler) {
|
|
155
|
+
this.wsp.addConnectionTimeoutListener(handler);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Registers a handler function that will be called when an individual connection reconnects.
|
|
159
|
+
* @param handler - Function to be called with connection index and endpoint URL
|
|
160
|
+
*/ addConnectionReconnectListener(handler) {
|
|
161
|
+
this.wsp.addConnectionReconnectListener(handler);
|
|
123
162
|
}
|
|
124
163
|
shutdown() {
|
|
125
|
-
|
|
126
|
-
wsp.shutdown();
|
|
164
|
+
this.wsp.shutdown();
|
|
127
165
|
}
|
|
128
166
|
/**
|
|
129
167
|
* Private helper method to make authenticated HTTP requests with Bearer token
|
|
130
168
|
* @param url - The URL to fetch
|
|
131
169
|
* @param options - Additional fetch options
|
|
132
170
|
* @returns Promise resolving to the fetch Response
|
|
133
|
-
*/
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
};
|
|
171
|
+
*/ authenticatedFetch(url, options = {}) {
|
|
172
|
+
// Handle all possible types of headers (Headers object, array, or plain object)
|
|
173
|
+
const headers = new Headers(options.headers);
|
|
174
|
+
headers.set("Authorization", headers.get("authorization") ?? `Bearer ${this.token}`);
|
|
138
175
|
return fetch(url, {
|
|
139
176
|
...options,
|
|
140
177
|
headers
|
|
@@ -171,15 +208,15 @@ export class PythLazerClient {
|
|
|
171
208
|
try {
|
|
172
209
|
const body = JSON.stringify(params);
|
|
173
210
|
this.logger.debug("getLatestPrice", {
|
|
174
|
-
|
|
175
|
-
|
|
211
|
+
body,
|
|
212
|
+
url
|
|
176
213
|
});
|
|
177
214
|
const response = await this.authenticatedFetch(url, {
|
|
178
|
-
|
|
215
|
+
body: body,
|
|
179
216
|
headers: {
|
|
180
217
|
"Content-Type": "application/json"
|
|
181
218
|
},
|
|
182
|
-
|
|
219
|
+
method: "POST"
|
|
183
220
|
});
|
|
184
221
|
if (!response.ok) {
|
|
185
222
|
throw new Error(`HTTP error! status: ${String(response.status)} - ${await response.text()}`);
|
|
@@ -198,15 +235,15 @@ export class PythLazerClient {
|
|
|
198
235
|
try {
|
|
199
236
|
const body = JSON.stringify(params);
|
|
200
237
|
this.logger.debug("getPrice", {
|
|
201
|
-
|
|
202
|
-
|
|
238
|
+
body,
|
|
239
|
+
url
|
|
203
240
|
});
|
|
204
241
|
const response = await this.authenticatedFetch(url, {
|
|
205
|
-
|
|
242
|
+
body: body,
|
|
206
243
|
headers: {
|
|
207
244
|
"Content-Type": "application/json"
|
|
208
245
|
},
|
|
209
|
-
|
|
246
|
+
method: "POST"
|
|
210
247
|
});
|
|
211
248
|
if (!response.ok) {
|
|
212
249
|
throw new Error(`HTTP error! status: ${String(response.status)} - ${await response.text()}`);
|