@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
|
@@ -1,79 +1,91 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
Object.defineProperty(exports, "WebSocketPool", {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
get: function() {
|
|
8
|
+
return WebSocketPool;
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
const _ttlcache = /*#__PURE__*/ _interop_require_default(require("@isaacs/ttlcache"));
|
|
12
|
+
const _tslog = require("ts-log");
|
|
13
|
+
const _resilientwebsocket = require("./resilient-websocket.cjs");
|
|
14
|
+
const _constants = require("../constants.cjs");
|
|
15
|
+
const _index = require("../util/index.cjs");
|
|
16
|
+
function _interop_require_default(obj) {
|
|
17
|
+
return obj && obj.__esModule ? obj : {
|
|
18
|
+
default: obj
|
|
19
|
+
};
|
|
20
|
+
}
|
|
11
21
|
const DEFAULT_NUM_CONNECTIONS = 4;
|
|
12
22
|
class WebSocketPool {
|
|
13
23
|
logger;
|
|
14
24
|
rwsPool;
|
|
15
25
|
cache;
|
|
16
|
-
subscriptions;
|
|
26
|
+
subscriptions;
|
|
17
27
|
messageListeners;
|
|
18
28
|
allConnectionsDownListeners;
|
|
19
29
|
wasAllDown = true;
|
|
20
30
|
checkConnectionStatesInterval;
|
|
21
|
-
constructor(logger)
|
|
31
|
+
constructor(logger){
|
|
22
32
|
this.logger = logger;
|
|
23
33
|
this.rwsPool = [];
|
|
24
|
-
this.cache = new
|
|
34
|
+
this.cache = new _ttlcache.default({
|
|
35
|
+
ttl: 1000 * 10
|
|
36
|
+
}); // TTL of 10 seconds
|
|
25
37
|
this.subscriptions = new Map();
|
|
26
38
|
this.messageListeners = [];
|
|
27
39
|
this.allConnectionsDownListeners = [];
|
|
28
40
|
// Start monitoring connection states
|
|
29
|
-
this.checkConnectionStatesInterval = setInterval(()
|
|
41
|
+
this.checkConnectionStatesInterval = setInterval(()=>{
|
|
30
42
|
this.checkConnectionStates();
|
|
31
43
|
}, 100);
|
|
32
44
|
}
|
|
33
45
|
/**
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
static async create(config, token, logger) {
|
|
46
|
+
* Creates a new WebSocketPool instance that uses multiple redundant WebSocket connections for reliability.
|
|
47
|
+
* Usage semantics are similar to using a regular WebSocket client.
|
|
48
|
+
* @param urls - List of WebSocket URLs to connect to
|
|
49
|
+
* @param token - Authentication token to use for the connections
|
|
50
|
+
* @param numConnections - Number of parallel WebSocket connections to maintain (default: 3)
|
|
51
|
+
* @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
|
|
52
|
+
*/ static async create(config, token, logger) {
|
|
42
53
|
const urls = config.urls ?? [
|
|
43
|
-
|
|
44
|
-
|
|
54
|
+
_constants.DEFAULT_STREAM_SERVICE_0_URL,
|
|
55
|
+
_constants.DEFAULT_STREAM_SERVICE_1_URL
|
|
45
56
|
];
|
|
46
|
-
const log = logger ??
|
|
57
|
+
const log = logger ?? _tslog.dummyLogger;
|
|
47
58
|
const pool = new WebSocketPool(log);
|
|
48
59
|
const numConnections = config.numConnections ?? DEFAULT_NUM_CONNECTIONS;
|
|
49
|
-
for
|
|
50
|
-
const
|
|
60
|
+
for(let i = 0; i < numConnections; i++){
|
|
61
|
+
const baseUrl = urls[i % urls.length];
|
|
62
|
+
const isBrowser = (0, _index.envIsBrowserOrWorker)();
|
|
63
|
+
const url = isBrowser ? (0, _index.addAuthTokenToWebSocketUrl)(baseUrl, token) : baseUrl;
|
|
51
64
|
if (!url) {
|
|
52
65
|
throw new Error(`URLs must not be null or empty`);
|
|
53
66
|
}
|
|
54
67
|
const wsOptions = {
|
|
55
68
|
...config.rwsConfig?.wsOptions,
|
|
56
|
-
headers: {
|
|
57
|
-
Authorization: `Bearer ${token}
|
|
58
|
-
}
|
|
69
|
+
headers: isBrowser ? undefined : {
|
|
70
|
+
Authorization: `Bearer ${token}`
|
|
71
|
+
}
|
|
59
72
|
};
|
|
60
|
-
const rws = new
|
|
73
|
+
const rws = new _resilientwebsocket.ResilientWebSocket({
|
|
61
74
|
...config.rwsConfig,
|
|
62
75
|
endpoint: url,
|
|
63
76
|
wsOptions,
|
|
64
|
-
logger: log
|
|
77
|
+
logger: log
|
|
65
78
|
});
|
|
66
79
|
// If a websocket client unexpectedly disconnects, ResilientWebSocket will reestablish
|
|
67
80
|
// the connection and call the onReconnect callback.
|
|
68
|
-
rws.onReconnect = ()
|
|
81
|
+
rws.onReconnect = ()=>{
|
|
69
82
|
if (rws.wsUserClosed) {
|
|
70
83
|
return;
|
|
71
84
|
}
|
|
72
|
-
for (const [, request] of pool.subscriptions)
|
|
85
|
+
for (const [, request] of pool.subscriptions){
|
|
73
86
|
try {
|
|
74
87
|
rws.send(JSON.stringify(request));
|
|
75
|
-
}
|
|
76
|
-
catch (error) {
|
|
88
|
+
} catch (error) {
|
|
77
89
|
pool.logger.error("Failed to resend subscription on reconnect:", error);
|
|
78
90
|
}
|
|
79
91
|
}
|
|
@@ -82,37 +94,42 @@ class WebSocketPool {
|
|
|
82
94
|
rws.onError = config.onError;
|
|
83
95
|
}
|
|
84
96
|
// Handle all client messages ourselves. Dedupe before sending to registered message handlers.
|
|
85
|
-
rws.onMessage =
|
|
97
|
+
rws.onMessage = (data)=>{
|
|
98
|
+
pool.dedupeHandler(data).catch((error)=>{
|
|
99
|
+
const errMsg = `An error occurred in the WebSocket pool's dedupeHandler: ${error instanceof Error ? error.message : String(error)}`;
|
|
100
|
+
throw new Error(errMsg);
|
|
101
|
+
});
|
|
102
|
+
};
|
|
86
103
|
pool.rwsPool.push(rws);
|
|
87
104
|
rws.startWebSocket();
|
|
88
105
|
}
|
|
89
106
|
pool.logger.info(`Started WebSocketPool with ${numConnections.toString()} connections. Waiting for at least one to connect...`);
|
|
90
|
-
while
|
|
91
|
-
await new Promise((resolve)
|
|
107
|
+
while(!pool.isAnyConnectionEstablished()){
|
|
108
|
+
await new Promise((resolve)=>setTimeout(resolve, 100));
|
|
92
109
|
}
|
|
93
110
|
pool.logger.info(`At least one WebSocket connection is established. WebSocketPool is ready.`);
|
|
94
111
|
return pool;
|
|
95
112
|
}
|
|
96
113
|
/**
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
handleErrorMessages(data) {
|
|
114
|
+
* Checks for error responses in JSON messages and throws appropriate errors
|
|
115
|
+
*/ handleErrorMessages(data) {
|
|
100
116
|
const message = JSON.parse(data);
|
|
101
117
|
if (message.type === "subscriptionError") {
|
|
102
118
|
throw new Error(`Error occurred for subscription ID ${String(message.subscriptionId)}: ${message.error}`);
|
|
103
|
-
}
|
|
104
|
-
else if (message.type === "error") {
|
|
119
|
+
} else if (message.type === "error") {
|
|
105
120
|
throw new Error(`Error: ${message.error}`);
|
|
106
121
|
}
|
|
107
122
|
}
|
|
123
|
+
async constructCacheKeyFromWebsocketData(data) {
|
|
124
|
+
if (typeof data === "string") return data;
|
|
125
|
+
const buff = await (0, _index.bufferFromWebsocketData)(data);
|
|
126
|
+
return buff.toString("hex");
|
|
127
|
+
}
|
|
108
128
|
/**
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const cacheKey = typeof data === "string"
|
|
114
|
-
? data
|
|
115
|
-
: Buffer.from(data).toString("hex");
|
|
129
|
+
* Handles incoming websocket messages by deduplicating identical messages received across
|
|
130
|
+
* multiple connections before forwarding to registered handlers
|
|
131
|
+
*/ dedupeHandler = async (data)=>{
|
|
132
|
+
const cacheKey = await this.constructCacheKeyFromWebsocketData(data);
|
|
116
133
|
if (this.cache.has(cacheKey)) {
|
|
117
134
|
this.logger.debug("Dropping duplicate message");
|
|
118
135
|
return;
|
|
@@ -121,12 +138,10 @@ class WebSocketPool {
|
|
|
121
138
|
if (typeof data === "string") {
|
|
122
139
|
this.handleErrorMessages(data);
|
|
123
140
|
}
|
|
124
|
-
|
|
125
|
-
handler(data);
|
|
126
|
-
}
|
|
141
|
+
await Promise.all(this.messageListeners.map((handler)=>handler(data)));
|
|
127
142
|
};
|
|
128
143
|
sendRequest(request) {
|
|
129
|
-
for (const rws of this.rwsPool)
|
|
144
|
+
for (const rws of this.rwsPool){
|
|
130
145
|
rws.send(JSON.stringify(request));
|
|
131
146
|
}
|
|
132
147
|
}
|
|
@@ -141,7 +156,7 @@ class WebSocketPool {
|
|
|
141
156
|
this.subscriptions.delete(subscriptionId);
|
|
142
157
|
const request = {
|
|
143
158
|
type: "unsubscribe",
|
|
144
|
-
subscriptionId
|
|
159
|
+
subscriptionId
|
|
145
160
|
};
|
|
146
161
|
this.sendRequest(request);
|
|
147
162
|
}
|
|
@@ -149,17 +164,16 @@ class WebSocketPool {
|
|
|
149
164
|
this.messageListeners.push(handler);
|
|
150
165
|
}
|
|
151
166
|
/**
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
addAllConnectionsDownListener(handler) {
|
|
167
|
+
* Calls the handler if all websocket connections are currently down or in reconnecting state.
|
|
168
|
+
* The connections may still try to reconnect in the background.
|
|
169
|
+
*/ addAllConnectionsDownListener(handler) {
|
|
156
170
|
this.allConnectionsDownListeners.push(handler);
|
|
157
171
|
}
|
|
158
172
|
areAllConnectionsDown() {
|
|
159
|
-
return this.rwsPool.every((ws)
|
|
173
|
+
return this.rwsPool.every((ws)=>!ws.isConnected() || ws.isReconnecting());
|
|
160
174
|
}
|
|
161
175
|
isAnyConnectionEstablished() {
|
|
162
|
-
return this.rwsPool.some((ws)
|
|
176
|
+
return this.rwsPool.some((ws)=>ws.isConnected());
|
|
163
177
|
}
|
|
164
178
|
checkConnectionStates() {
|
|
165
179
|
const allDown = this.areAllConnectionsDown();
|
|
@@ -168,7 +182,7 @@ class WebSocketPool {
|
|
|
168
182
|
this.wasAllDown = true;
|
|
169
183
|
this.logger.error("All WebSocket connections are down or reconnecting");
|
|
170
184
|
// Notify all listeners
|
|
171
|
-
for (const listener of this.allConnectionsDownListeners)
|
|
185
|
+
for (const listener of this.allConnectionsDownListeners){
|
|
172
186
|
listener();
|
|
173
187
|
}
|
|
174
188
|
}
|
|
@@ -178,7 +192,7 @@ class WebSocketPool {
|
|
|
178
192
|
}
|
|
179
193
|
}
|
|
180
194
|
shutdown() {
|
|
181
|
-
for (const rws of this.rwsPool)
|
|
195
|
+
for (const rws of this.rwsPool){
|
|
182
196
|
rws.closeWebSocket();
|
|
183
197
|
}
|
|
184
198
|
this.rwsPool = [];
|
|
@@ -188,4 +202,3 @@ class WebSocketPool {
|
|
|
188
202
|
clearInterval(this.checkConnectionStatesInterval);
|
|
189
203
|
}
|
|
190
204
|
}
|
|
191
|
-
exports.WebSocketPool = WebSocketPool;
|
|
@@ -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 {};
|
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
"use strict";
|
|
6
|
+
Object.defineProperty(exports, "__esModule", {
|
|
7
|
+
value: true
|
|
8
|
+
});
|
|
9
|
+
Object.defineProperty(exports, "bufferFromWebsocketData", {
|
|
10
|
+
enumerable: true,
|
|
11
|
+
get: function() {
|
|
12
|
+
return bufferFromWebsocketData;
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
const _buffer = require("buffer");
|
|
16
|
+
const BufferClassToUse = "Buffer" in globalThis ? globalThis.Buffer : _buffer.Buffer;
|
|
17
|
+
async function bufferFromWebsocketData(data) {
|
|
18
|
+
if (typeof data === "string") {
|
|
19
|
+
return BufferClassToUse.from(new TextEncoder().encode(data).buffer);
|
|
20
|
+
}
|
|
21
|
+
if (data instanceof BufferClassToUse) return data;
|
|
22
|
+
if (data instanceof Blob) {
|
|
23
|
+
// let the uncaught promise exception bubble up if there's an issue
|
|
24
|
+
return BufferClassToUse.from(await data.arrayBuffer());
|
|
25
|
+
}
|
|
26
|
+
if (data instanceof ArrayBuffer) return BufferClassToUse.from(data);
|
|
27
|
+
if (Array.isArray(data)) {
|
|
28
|
+
// an array of buffers is highly unlikely, but it is a possibility
|
|
29
|
+
// indicated by the WebSocket Data interface
|
|
30
|
+
return BufferClassToUse.concat(data);
|
|
31
|
+
}
|
|
32
|
+
return data;
|
|
33
|
+
}
|
|
@@ -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,33 @@
|
|
|
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
|
+
"use strict";
|
|
4
|
+
Object.defineProperty(exports, "__esModule", {
|
|
5
|
+
value: true
|
|
6
|
+
});
|
|
7
|
+
function _export(target, all) {
|
|
8
|
+
for(var name in all)Object.defineProperty(target, name, {
|
|
9
|
+
enumerable: true,
|
|
10
|
+
get: Object.getOwnPropertyDescriptor(all, name).get
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
_export(exports, {
|
|
14
|
+
get envIsBrowser () {
|
|
15
|
+
return envIsBrowser;
|
|
16
|
+
},
|
|
17
|
+
get envIsBrowserOrWorker () {
|
|
18
|
+
return envIsBrowserOrWorker;
|
|
19
|
+
},
|
|
20
|
+
get envIsServiceOrWebWorker () {
|
|
21
|
+
return envIsServiceOrWebWorker;
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
const g = globalThis;
|
|
25
|
+
function envIsServiceOrWebWorker() {
|
|
26
|
+
return typeof WorkerGlobalScope !== "undefined" && g.self instanceof WorkerGlobalScope;
|
|
27
|
+
}
|
|
28
|
+
function envIsBrowser() {
|
|
29
|
+
return g.window !== undefined;
|
|
30
|
+
}
|
|
31
|
+
function envIsBrowserOrWorker() {
|
|
32
|
+
return envIsServiceOrWebWorker() || envIsBrowser();
|
|
33
|
+
}
|
|
@@ -0,0 +1,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,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
_export_star(require("./buffer-util.cjs"), exports);
|
|
6
|
+
_export_star(require("./env-util.cjs"), exports);
|
|
7
|
+
_export_star(require("./url-util.cjs"), exports);
|
|
8
|
+
function _export_star(from, to) {
|
|
9
|
+
Object.keys(from).forEach(function(k) {
|
|
10
|
+
if (k !== "default" && !Object.prototype.hasOwnProperty.call(to, k)) {
|
|
11
|
+
Object.defineProperty(to, k, {
|
|
12
|
+
enumerable: true,
|
|
13
|
+
get: function() {
|
|
14
|
+
return from[k];
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
return from;
|
|
20
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
Object.defineProperty(exports, "addAuthTokenToWebSocketUrl", {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
get: function() {
|
|
8
|
+
return addAuthTokenToWebSocketUrl;
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
const ACCESS_TOKEN_QUERY_PARAM_KEY = "ACCESS_TOKEN";
|
|
12
|
+
function addAuthTokenToWebSocketUrl(baseUrl, authToken) {
|
|
13
|
+
if (!baseUrl || !authToken) return baseUrl;
|
|
14
|
+
const parsedUrl = new URL(baseUrl);
|
|
15
|
+
parsedUrl.searchParams.set(ACCESS_TOKEN_QUERY_PARAM_KEY, authToken);
|
|
16
|
+
return parsedUrl.toString();
|
|
17
|
+
}
|
|
@@ -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,219 @@
|
|
|
1
|
+
import { dummyLogger } from "ts-log";
|
|
2
|
+
import { DEFAULT_METADATA_SERVICE_URL, DEFAULT_PRICE_SERVICE_URL } from "./constants.mjs";
|
|
3
|
+
import { BINARY_UPDATE_FORMAT_MAGIC_LE, FORMAT_MAGICS_LE } from "./protocol.mjs";
|
|
4
|
+
import { WebSocketPool } from "./socket/websocket-pool.mjs";
|
|
5
|
+
import { bufferFromWebsocketData } from "./util/buffer-util.mjs";
|
|
6
|
+
const UINT16_NUM_BYTES = 2;
|
|
7
|
+
const UINT32_NUM_BYTES = 4;
|
|
8
|
+
const UINT64_NUM_BYTES = 8;
|
|
9
|
+
export class PythLazerClient {
|
|
10
|
+
token;
|
|
11
|
+
metadataServiceUrl;
|
|
12
|
+
priceServiceUrl;
|
|
13
|
+
logger;
|
|
14
|
+
wsp;
|
|
15
|
+
constructor(token, metadataServiceUrl, priceServiceUrl, logger, wsp){
|
|
16
|
+
this.token = token;
|
|
17
|
+
this.metadataServiceUrl = metadataServiceUrl;
|
|
18
|
+
this.priceServiceUrl = priceServiceUrl;
|
|
19
|
+
this.logger = logger;
|
|
20
|
+
this.wsp = wsp;
|
|
21
|
+
}
|
|
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
|
+
* Creates a new PythLazerClient instance.
|
|
34
|
+
* @param config - Configuration including token, metadata service URL, and price service URL, and WebSocket pool configuration
|
|
35
|
+
*/ static async create(config) {
|
|
36
|
+
const token = config.token;
|
|
37
|
+
// Collect and remove trailing slash from URLs
|
|
38
|
+
const metadataServiceUrl = (config.metadataServiceUrl ?? DEFAULT_METADATA_SERVICE_URL).replace(/\/+$/, "");
|
|
39
|
+
const priceServiceUrl = (config.priceServiceUrl ?? DEFAULT_PRICE_SERVICE_URL).replace(/\/+$/, "");
|
|
40
|
+
const logger = config.logger ?? dummyLogger;
|
|
41
|
+
// If webSocketPoolConfig is provided, create a WebSocket pool and block until at least one connection is established.
|
|
42
|
+
let wsp;
|
|
43
|
+
if (config.webSocketPoolConfig) {
|
|
44
|
+
wsp = await WebSocketPool.create(config.webSocketPoolConfig, token, logger);
|
|
45
|
+
}
|
|
46
|
+
return new PythLazerClient(token, metadataServiceUrl, priceServiceUrl, logger, wsp);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Adds a message listener that receives either JSON or binary responses from the WebSocket connections.
|
|
50
|
+
* The listener will be called for each message received, with deduplication across redundant connections.
|
|
51
|
+
* @param handler - Callback function that receives the parsed message. The message can be either a JSON response
|
|
52
|
+
* or a binary response containing EVM, Solana, or parsed payload data.
|
|
53
|
+
*/ addMessageListener(handler) {
|
|
54
|
+
const wsp = this.getWebSocketPool();
|
|
55
|
+
wsp.addMessageListener(async (data)=>{
|
|
56
|
+
if (typeof data == "string") {
|
|
57
|
+
handler({
|
|
58
|
+
type: "json",
|
|
59
|
+
value: JSON.parse(data)
|
|
60
|
+
});
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const buffData = await bufferFromWebsocketData(data);
|
|
64
|
+
let pos = 0;
|
|
65
|
+
const magic = buffData.subarray(pos, pos + UINT32_NUM_BYTES).readUint32LE();
|
|
66
|
+
pos += UINT32_NUM_BYTES;
|
|
67
|
+
if (magic != BINARY_UPDATE_FORMAT_MAGIC_LE) {
|
|
68
|
+
throw new Error("binary update format magic mismatch");
|
|
69
|
+
}
|
|
70
|
+
// TODO: some uint64 values may not be representable as Number.
|
|
71
|
+
const subscriptionId = Number(buffData.subarray(pos, pos + UINT64_NUM_BYTES).readBigInt64BE());
|
|
72
|
+
pos += UINT64_NUM_BYTES;
|
|
73
|
+
const value = {
|
|
74
|
+
subscriptionId
|
|
75
|
+
};
|
|
76
|
+
while(pos < buffData.length){
|
|
77
|
+
const len = buffData.subarray(pos, pos + UINT16_NUM_BYTES).readUint16BE();
|
|
78
|
+
pos += UINT16_NUM_BYTES;
|
|
79
|
+
const magic = buffData.subarray(pos, pos + UINT32_NUM_BYTES).readUint32LE();
|
|
80
|
+
if (magic == FORMAT_MAGICS_LE.EVM) {
|
|
81
|
+
value.evm = buffData.subarray(pos, pos + len);
|
|
82
|
+
} else if (magic == FORMAT_MAGICS_LE.SOLANA) {
|
|
83
|
+
value.solana = buffData.subarray(pos, pos + len);
|
|
84
|
+
} else if (magic == FORMAT_MAGICS_LE.LE_ECDSA) {
|
|
85
|
+
value.leEcdsa = buffData.subarray(pos, pos + len);
|
|
86
|
+
} else if (magic == FORMAT_MAGICS_LE.LE_UNSIGNED) {
|
|
87
|
+
value.leUnsigned = buffData.subarray(pos, pos + len);
|
|
88
|
+
} else if (magic == FORMAT_MAGICS_LE.JSON) {
|
|
89
|
+
value.parsed = JSON.parse(buffData.subarray(pos + UINT32_NUM_BYTES, pos + len).toString());
|
|
90
|
+
} else {
|
|
91
|
+
throw new Error(`unknown magic: ${magic.toString()}`);
|
|
92
|
+
}
|
|
93
|
+
pos += len;
|
|
94
|
+
}
|
|
95
|
+
handler({
|
|
96
|
+
type: "binary",
|
|
97
|
+
value
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
subscribe(request) {
|
|
102
|
+
const wsp = this.getWebSocketPool();
|
|
103
|
+
if (request.type !== "subscribe") {
|
|
104
|
+
throw new Error("Request must be a subscribe request");
|
|
105
|
+
}
|
|
106
|
+
wsp.addSubscription(request);
|
|
107
|
+
}
|
|
108
|
+
unsubscribe(subscriptionId) {
|
|
109
|
+
const wsp = this.getWebSocketPool();
|
|
110
|
+
wsp.removeSubscription(subscriptionId);
|
|
111
|
+
}
|
|
112
|
+
send(request) {
|
|
113
|
+
const wsp = this.getWebSocketPool();
|
|
114
|
+
wsp.sendRequest(request);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Registers a handler function that will be called whenever all WebSocket connections are down or attempting to reconnect.
|
|
118
|
+
* The connections may still try to reconnect in the background. To shut down the pool, call `shutdown()`.
|
|
119
|
+
* @param handler - Function to be called when all connections are down
|
|
120
|
+
*/ addAllConnectionsDownListener(handler) {
|
|
121
|
+
const wsp = this.getWebSocketPool();
|
|
122
|
+
wsp.addAllConnectionsDownListener(handler);
|
|
123
|
+
}
|
|
124
|
+
shutdown() {
|
|
125
|
+
const wsp = this.getWebSocketPool();
|
|
126
|
+
wsp.shutdown();
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Private helper method to make authenticated HTTP requests with Bearer token
|
|
130
|
+
* @param url - The URL to fetch
|
|
131
|
+
* @param options - Additional fetch options
|
|
132
|
+
* @returns Promise resolving to the fetch Response
|
|
133
|
+
*/ async authenticatedFetch(url, options = {}) {
|
|
134
|
+
const headers = {
|
|
135
|
+
Authorization: `Bearer ${this.token}`,
|
|
136
|
+
...options.headers
|
|
137
|
+
};
|
|
138
|
+
return fetch(url, {
|
|
139
|
+
...options,
|
|
140
|
+
headers
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Queries the symbols endpoint to get available price feed symbols.
|
|
145
|
+
* @param params - Optional query parameters to filter symbols
|
|
146
|
+
* @returns Promise resolving to array of symbol information
|
|
147
|
+
*/ async getSymbols(params) {
|
|
148
|
+
const url = new URL(`${this.metadataServiceUrl}/v1/symbols`);
|
|
149
|
+
if (params?.query) {
|
|
150
|
+
url.searchParams.set("query", params.query);
|
|
151
|
+
}
|
|
152
|
+
if (params?.asset_type) {
|
|
153
|
+
url.searchParams.set("asset_type", params.asset_type);
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
const response = await this.authenticatedFetch(url.toString());
|
|
157
|
+
if (!response.ok) {
|
|
158
|
+
throw new Error(`HTTP error! status: ${String(response.status)} - ${await response.text()}`);
|
|
159
|
+
}
|
|
160
|
+
return await response.json();
|
|
161
|
+
} catch (error) {
|
|
162
|
+
throw new Error(`Failed to fetch symbols: ${error instanceof Error ? error.message : String(error)}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Queries the latest price endpoint to get current price data.
|
|
167
|
+
* @param params - Parameters for the latest price request
|
|
168
|
+
* @returns Promise resolving to JsonUpdate with current price data
|
|
169
|
+
*/ async getLatestPrice(params) {
|
|
170
|
+
const url = `${this.priceServiceUrl}/v1/latest_price`;
|
|
171
|
+
try {
|
|
172
|
+
const body = JSON.stringify(params);
|
|
173
|
+
this.logger.debug("getLatestPrice", {
|
|
174
|
+
url,
|
|
175
|
+
body
|
|
176
|
+
});
|
|
177
|
+
const response = await this.authenticatedFetch(url, {
|
|
178
|
+
method: "POST",
|
|
179
|
+
headers: {
|
|
180
|
+
"Content-Type": "application/json"
|
|
181
|
+
},
|
|
182
|
+
body: body
|
|
183
|
+
});
|
|
184
|
+
if (!response.ok) {
|
|
185
|
+
throw new Error(`HTTP error! status: ${String(response.status)} - ${await response.text()}`);
|
|
186
|
+
}
|
|
187
|
+
return await response.json();
|
|
188
|
+
} catch (error) {
|
|
189
|
+
throw new Error(`Failed to fetch latest price: ${error instanceof Error ? error.message : String(error)}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Queries the price endpoint to get historical price data at a specific timestamp.
|
|
194
|
+
* @param params - Parameters for the price request including timestamp
|
|
195
|
+
* @returns Promise resolving to JsonUpdate with price data at the specified time
|
|
196
|
+
*/ async getPrice(params) {
|
|
197
|
+
const url = `${this.priceServiceUrl}/v1/price`;
|
|
198
|
+
try {
|
|
199
|
+
const body = JSON.stringify(params);
|
|
200
|
+
this.logger.debug("getPrice", {
|
|
201
|
+
url,
|
|
202
|
+
body
|
|
203
|
+
});
|
|
204
|
+
const response = await this.authenticatedFetch(url, {
|
|
205
|
+
method: "POST",
|
|
206
|
+
headers: {
|
|
207
|
+
"Content-Type": "application/json"
|
|
208
|
+
},
|
|
209
|
+
body: body
|
|
210
|
+
});
|
|
211
|
+
if (!response.ok) {
|
|
212
|
+
throw new Error(`HTTP error! status: ${String(response.status)} - ${await response.text()}`);
|
|
213
|
+
}
|
|
214
|
+
return await response.json();
|
|
215
|
+
} catch (error) {
|
|
216
|
+
throw new Error(`Failed to fetch price: ${error instanceof Error ? error.message : String(error)}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
package/dist/esm/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"type":"module"}
|
|
1
|
+
{ "type": "module" }
|