@pythnetwork/pyth-lazer-sdk 0.1.1 → 0.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/dist/cjs/client.d.ts +32 -0
- package/dist/cjs/client.js +84 -0
- package/dist/cjs/ed25519.d.ts +2 -0
- package/dist/cjs/ed25519.js +69 -0
- package/dist/cjs/index.d.ts +3 -21
- package/dist/cjs/index.js +16 -70
- package/dist/cjs/socket/resilient-web-socket.d.ts +38 -0
- package/dist/cjs/socket/resilient-web-socket.js +161 -0
- package/dist/cjs/socket/web-socket-pool.d.ts +55 -0
- package/dist/cjs/socket/web-socket-pool.js +174 -0
- package/dist/esm/client.d.ts +32 -0
- package/dist/esm/client.js +81 -0
- package/dist/esm/ed25519.d.ts +2 -0
- package/dist/esm/ed25519.js +42 -0
- package/dist/esm/index.d.ts +3 -21
- package/dist/esm/index.js +3 -66
- package/dist/esm/socket/resilient-web-socket.d.ts +38 -0
- package/dist/esm/socket/resilient-web-socket.js +154 -0
- package/dist/esm/socket/web-socket-pool.d.ts +55 -0
- package/dist/esm/socket/web-socket-pool.js +168 -0
- package/package.json +6 -2
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import TTLCache from "@isaacs/ttlcache";
|
|
2
|
+
import WebSocket from "isomorphic-ws";
|
|
3
|
+
import { dummyLogger } from "ts-log";
|
|
4
|
+
import { ResilientWebSocket } from "./resilient-web-socket.js";
|
|
5
|
+
// Number of redundant parallel WebSocket connections
|
|
6
|
+
const DEFAULT_NUM_CONNECTIONS = 3;
|
|
7
|
+
export class WebSocketPool {
|
|
8
|
+
logger;
|
|
9
|
+
rwsPool;
|
|
10
|
+
cache;
|
|
11
|
+
subscriptions; // id -> subscription Request
|
|
12
|
+
messageListeners;
|
|
13
|
+
/**
|
|
14
|
+
* Creates a new WebSocketPool instance that uses multiple redundant WebSocket connections for reliability.
|
|
15
|
+
* Usage semantics are similar to using a regular WebSocket client.
|
|
16
|
+
* @param urls - List of WebSocket URLs to connect to
|
|
17
|
+
* @param token - Authentication token to use for the connections
|
|
18
|
+
* @param numConnections - Number of parallel WebSocket connections to maintain (default: 3)
|
|
19
|
+
* @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
|
|
20
|
+
*/
|
|
21
|
+
constructor(urls, token, numConnections = DEFAULT_NUM_CONNECTIONS, logger = dummyLogger) {
|
|
22
|
+
this.logger = logger;
|
|
23
|
+
if (urls.length === 0) {
|
|
24
|
+
throw new Error("No URLs provided");
|
|
25
|
+
}
|
|
26
|
+
// This cache is used to deduplicate messages received across different websocket clients in the pool.
|
|
27
|
+
// A TTL cache is used to prevent unbounded memory usage. A very short TTL of 10 seconds is chosen since
|
|
28
|
+
// deduplication only needs to happen between messages received very close together in time.
|
|
29
|
+
this.cache = new TTLCache({ ttl: 1000 * 10 }); // TTL of 10 seconds
|
|
30
|
+
this.rwsPool = [];
|
|
31
|
+
this.subscriptions = new Map();
|
|
32
|
+
this.messageListeners = [];
|
|
33
|
+
for (let i = 0; i < numConnections; i++) {
|
|
34
|
+
const url = urls[i % urls.length];
|
|
35
|
+
if (!url) {
|
|
36
|
+
throw new Error(`URLs must not be null or empty`);
|
|
37
|
+
}
|
|
38
|
+
const wsOptions = {
|
|
39
|
+
headers: {
|
|
40
|
+
Authorization: `Bearer ${token}`,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
const rws = new ResilientWebSocket(url, wsOptions, logger);
|
|
44
|
+
// If a websocket client unexpectedly disconnects, ResilientWebSocket will reestablish
|
|
45
|
+
// the connection and call the onReconnect callback.
|
|
46
|
+
// When we reconnect, replay all subscription messages to resume the data stream.
|
|
47
|
+
rws.onReconnect = () => {
|
|
48
|
+
if (rws.wsUserClosed) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
for (const [, request] of this.subscriptions) {
|
|
52
|
+
try {
|
|
53
|
+
void rws.send(JSON.stringify(request));
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
this.logger.error("Failed to resend subscription on reconnect:", error);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
// Handle all client messages ourselves. Dedupe before sending to registered message handlers.
|
|
61
|
+
rws.onMessage = this.dedupeHandler;
|
|
62
|
+
this.rwsPool.push(rws);
|
|
63
|
+
}
|
|
64
|
+
// Let it rip
|
|
65
|
+
// TODO: wait for sockets to receive `open` msg before subscribing?
|
|
66
|
+
for (const rws of this.rwsPool) {
|
|
67
|
+
rws.startWebSocket();
|
|
68
|
+
}
|
|
69
|
+
this.logger.info(`Using ${numConnections.toString()} redundant WebSocket connections`);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Checks for error responses in JSON messages and throws appropriate errors
|
|
73
|
+
*/
|
|
74
|
+
handleErrorMessages(data) {
|
|
75
|
+
const message = JSON.parse(data);
|
|
76
|
+
if (message.type === "subscriptionError") {
|
|
77
|
+
throw new Error(`Error occurred for subscription ID ${String(message.subscriptionId)}: ${message.error}`);
|
|
78
|
+
}
|
|
79
|
+
else if (message.type === "error") {
|
|
80
|
+
throw new Error(`Error: ${message.error}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Handles incoming websocket messages by deduplicating identical messages received across
|
|
85
|
+
* multiple connections before forwarding to registered handlers
|
|
86
|
+
*/
|
|
87
|
+
dedupeHandler = (data) => {
|
|
88
|
+
// For string data, use the whole string as the cache key. This avoids expensive JSON parsing during deduping.
|
|
89
|
+
// For binary data, use the hex string representation as the cache key
|
|
90
|
+
const cacheKey = typeof data === "string"
|
|
91
|
+
? data
|
|
92
|
+
: Buffer.from(data).toString("hex");
|
|
93
|
+
// If we've seen this exact message recently, drop it
|
|
94
|
+
if (this.cache.has(cacheKey)) {
|
|
95
|
+
this.logger.debug("Dropping duplicate message");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
// Haven't seen this message, cache it and forward to handlers
|
|
99
|
+
this.cache.set(cacheKey, true);
|
|
100
|
+
// Check for errors in JSON responses
|
|
101
|
+
if (typeof data === "string") {
|
|
102
|
+
this.handleErrorMessages(data);
|
|
103
|
+
}
|
|
104
|
+
for (const handler of this.messageListeners) {
|
|
105
|
+
handler(data);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* Sends a message to all websockets in the pool
|
|
110
|
+
* @param request - The request to send
|
|
111
|
+
*/
|
|
112
|
+
async sendRequest(request) {
|
|
113
|
+
// Send to all websockets in the pool
|
|
114
|
+
const sendPromises = this.rwsPool.map(async (rws) => {
|
|
115
|
+
try {
|
|
116
|
+
await rws.send(JSON.stringify(request));
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
this.logger.error("Failed to send request:", error);
|
|
120
|
+
throw error; // Re-throw the error
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
await Promise.all(sendPromises);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Adds a subscription by sending a subscribe request to all websockets in the pool
|
|
127
|
+
* and storing it for replay on reconnection
|
|
128
|
+
* @param request - The subscription request to send
|
|
129
|
+
*/
|
|
130
|
+
async addSubscription(request) {
|
|
131
|
+
if (request.type !== "subscribe") {
|
|
132
|
+
throw new Error("Request must be a subscribe request");
|
|
133
|
+
}
|
|
134
|
+
this.subscriptions.set(request.subscriptionId, request);
|
|
135
|
+
await this.sendRequest(request);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Removes a subscription by sending an unsubscribe request to all websockets in the pool
|
|
139
|
+
* and removing it from stored subscriptions
|
|
140
|
+
* @param subscriptionId - The ID of the subscription to remove
|
|
141
|
+
*/
|
|
142
|
+
async removeSubscription(subscriptionId) {
|
|
143
|
+
this.subscriptions.delete(subscriptionId);
|
|
144
|
+
const request = {
|
|
145
|
+
type: "unsubscribe",
|
|
146
|
+
subscriptionId,
|
|
147
|
+
};
|
|
148
|
+
await this.sendRequest(request);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Adds a message handler function to receive websocket messages
|
|
152
|
+
* @param handler - Function that will be called with each received message
|
|
153
|
+
*/
|
|
154
|
+
addMessageListener(handler) {
|
|
155
|
+
this.messageListeners.push(handler);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Elegantly closes all websocket connections in the pool
|
|
159
|
+
*/
|
|
160
|
+
shutdown() {
|
|
161
|
+
for (const rws of this.rwsPool) {
|
|
162
|
+
rws.closeWebSocket();
|
|
163
|
+
}
|
|
164
|
+
this.rwsPool = [];
|
|
165
|
+
this.subscriptions.clear();
|
|
166
|
+
this.messageListeners = [];
|
|
167
|
+
}
|
|
168
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pythnetwork/pyth-lazer-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Pyth Lazer SDK",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -60,8 +60,12 @@
|
|
|
60
60
|
],
|
|
61
61
|
"license": "Apache-2.0",
|
|
62
62
|
"dependencies": {
|
|
63
|
+
"@isaacs/ttlcache": "^1.4.1",
|
|
64
|
+
"@solana/buffer-layout": "^4.0.1",
|
|
65
|
+
"@solana/web3.js": "^1.98.0",
|
|
63
66
|
"isomorphic-ws": "^5.0.0",
|
|
67
|
+
"ts-log": "^2.2.7",
|
|
64
68
|
"ws": "^8.18.0"
|
|
65
69
|
},
|
|
66
|
-
"gitHead": "
|
|
70
|
+
"gitHead": "7cb1725be86f01810025b6ceac1a8d6df2c37285"
|
|
67
71
|
}
|