@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,32 @@
|
|
|
1
|
+
import { type Logger } from "ts-log";
|
|
2
|
+
import { type ParsedPayload, type Request, type Response } from "./protocol.js";
|
|
3
|
+
import { WebSocketPool } from "./socket/web-socket-pool.js";
|
|
4
|
+
export type BinaryResponse = {
|
|
5
|
+
subscriptionId: number;
|
|
6
|
+
evm?: Buffer | undefined;
|
|
7
|
+
solana?: Buffer | undefined;
|
|
8
|
+
parsed?: ParsedPayload | undefined;
|
|
9
|
+
};
|
|
10
|
+
export type JsonOrBinaryResponse = {
|
|
11
|
+
type: "json";
|
|
12
|
+
value: Response;
|
|
13
|
+
} | {
|
|
14
|
+
type: "binary";
|
|
15
|
+
value: BinaryResponse;
|
|
16
|
+
};
|
|
17
|
+
export declare class PythLazerClient {
|
|
18
|
+
wsp: WebSocketPool;
|
|
19
|
+
/**
|
|
20
|
+
* Creates a new PythLazerClient instance.
|
|
21
|
+
* @param urls - List of WebSocket URLs of the Pyth Lazer service
|
|
22
|
+
* @param token - The access token for authentication
|
|
23
|
+
* @param numConnections - The number of parallel WebSocket connections to establish (default: 3). A higher number gives a more reliable stream.
|
|
24
|
+
* @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
|
|
25
|
+
*/
|
|
26
|
+
constructor(urls: string[], token: string, numConnections?: number, logger?: Logger);
|
|
27
|
+
addMessageListener(handler: (event: JsonOrBinaryResponse) => void): void;
|
|
28
|
+
subscribe(request: Request): Promise<void>;
|
|
29
|
+
unsubscribe(subscriptionId: number): Promise<void>;
|
|
30
|
+
send(request: Request): Promise<void>;
|
|
31
|
+
shutdown(): void;
|
|
32
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PythLazerClient = void 0;
|
|
4
|
+
const ts_log_1 = require("ts-log");
|
|
5
|
+
const protocol_js_1 = require("./protocol.js");
|
|
6
|
+
const web_socket_pool_js_1 = require("./socket/web-socket-pool.js");
|
|
7
|
+
const UINT16_NUM_BYTES = 2;
|
|
8
|
+
const UINT32_NUM_BYTES = 4;
|
|
9
|
+
const UINT64_NUM_BYTES = 8;
|
|
10
|
+
class PythLazerClient {
|
|
11
|
+
wsp;
|
|
12
|
+
/**
|
|
13
|
+
* Creates a new PythLazerClient instance.
|
|
14
|
+
* @param urls - List of WebSocket URLs of the Pyth Lazer service
|
|
15
|
+
* @param token - The access token for authentication
|
|
16
|
+
* @param numConnections - The number of parallel WebSocket connections to establish (default: 3). A higher number gives a more reliable stream.
|
|
17
|
+
* @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
|
|
18
|
+
*/
|
|
19
|
+
constructor(urls, token, numConnections = 3, logger = ts_log_1.dummyLogger) {
|
|
20
|
+
this.wsp = new web_socket_pool_js_1.WebSocketPool(urls, token, numConnections, logger);
|
|
21
|
+
}
|
|
22
|
+
addMessageListener(handler) {
|
|
23
|
+
this.wsp.addMessageListener((data) => {
|
|
24
|
+
if (typeof data == "string") {
|
|
25
|
+
handler({
|
|
26
|
+
type: "json",
|
|
27
|
+
value: JSON.parse(data),
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
else if (Buffer.isBuffer(data)) {
|
|
31
|
+
let pos = 0;
|
|
32
|
+
const magic = data.subarray(pos, pos + UINT32_NUM_BYTES).readUint32BE();
|
|
33
|
+
pos += UINT32_NUM_BYTES;
|
|
34
|
+
if (magic != protocol_js_1.BINARY_UPDATE_FORMAT_MAGIC) {
|
|
35
|
+
throw new Error("binary update format magic mismatch");
|
|
36
|
+
}
|
|
37
|
+
// TODO: some uint64 values may not be representable as Number.
|
|
38
|
+
const subscriptionId = Number(data.subarray(pos, pos + UINT64_NUM_BYTES).readBigInt64BE());
|
|
39
|
+
pos += UINT64_NUM_BYTES;
|
|
40
|
+
const value = { subscriptionId };
|
|
41
|
+
while (pos < data.length) {
|
|
42
|
+
const len = data.subarray(pos, pos + UINT16_NUM_BYTES).readUint16BE();
|
|
43
|
+
pos += UINT16_NUM_BYTES;
|
|
44
|
+
const magic = data
|
|
45
|
+
.subarray(pos, pos + UINT32_NUM_BYTES)
|
|
46
|
+
.readUint32BE();
|
|
47
|
+
if (magic == protocol_js_1.EVM_FORMAT_MAGIC) {
|
|
48
|
+
value.evm = data.subarray(pos, pos + len);
|
|
49
|
+
}
|
|
50
|
+
else if (magic == protocol_js_1.SOLANA_FORMAT_MAGIC_BE) {
|
|
51
|
+
value.solana = data.subarray(pos, pos + len);
|
|
52
|
+
}
|
|
53
|
+
else if (magic == protocol_js_1.PARSED_FORMAT_MAGIC) {
|
|
54
|
+
value.parsed = JSON.parse(data.subarray(pos + UINT32_NUM_BYTES, pos + len).toString());
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
throw new Error("unknown magic: " + magic.toString());
|
|
58
|
+
}
|
|
59
|
+
pos += len;
|
|
60
|
+
}
|
|
61
|
+
handler({ type: "binary", value });
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
throw new TypeError("unexpected event data type");
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
async subscribe(request) {
|
|
69
|
+
if (request.type !== "subscribe") {
|
|
70
|
+
throw new Error("Request must be a subscribe request");
|
|
71
|
+
}
|
|
72
|
+
await this.wsp.addSubscription(request);
|
|
73
|
+
}
|
|
74
|
+
async unsubscribe(subscriptionId) {
|
|
75
|
+
await this.wsp.removeSubscription(subscriptionId);
|
|
76
|
+
}
|
|
77
|
+
async send(request) {
|
|
78
|
+
await this.wsp.sendRequest(request);
|
|
79
|
+
}
|
|
80
|
+
shutdown() {
|
|
81
|
+
this.wsp.shutdown();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
exports.PythLazerClient = PythLazerClient;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.createEd25519Instruction = void 0;
|
|
27
|
+
const BufferLayout = __importStar(require("@solana/buffer-layout"));
|
|
28
|
+
const web3_js_1 = require("@solana/web3.js");
|
|
29
|
+
const ED25519_INSTRUCTION_LEN = 16;
|
|
30
|
+
const SIGNATURE_LEN = 64;
|
|
31
|
+
const PUBKEY_LEN = 32;
|
|
32
|
+
const MAGIC_LEN = 4;
|
|
33
|
+
const MESSAGE_SIZE_LEN = 2;
|
|
34
|
+
const ED25519_INSTRUCTION_LAYOUT = BufferLayout.struct([
|
|
35
|
+
BufferLayout.u8("numSignatures"),
|
|
36
|
+
BufferLayout.u8("padding"),
|
|
37
|
+
BufferLayout.u16("signatureOffset"),
|
|
38
|
+
BufferLayout.u16("signatureInstructionIndex"),
|
|
39
|
+
BufferLayout.u16("publicKeyOffset"),
|
|
40
|
+
BufferLayout.u16("publicKeyInstructionIndex"),
|
|
41
|
+
BufferLayout.u16("messageDataOffset"),
|
|
42
|
+
BufferLayout.u16("messageDataSize"),
|
|
43
|
+
BufferLayout.u16("messageInstructionIndex"),
|
|
44
|
+
]);
|
|
45
|
+
const createEd25519Instruction = (message, instructionIndex, startingOffset) => {
|
|
46
|
+
const signatureOffset = startingOffset + MAGIC_LEN;
|
|
47
|
+
const publicKeyOffset = signatureOffset + SIGNATURE_LEN;
|
|
48
|
+
const messageDataSizeOffset = publicKeyOffset + PUBKEY_LEN;
|
|
49
|
+
const messageDataOffset = messageDataSizeOffset + MESSAGE_SIZE_LEN;
|
|
50
|
+
const messageDataSize = message.readUInt16LE(messageDataSizeOffset - startingOffset);
|
|
51
|
+
const instructionData = Buffer.alloc(ED25519_INSTRUCTION_LEN);
|
|
52
|
+
ED25519_INSTRUCTION_LAYOUT.encode({
|
|
53
|
+
numSignatures: 1,
|
|
54
|
+
padding: 0,
|
|
55
|
+
signatureOffset,
|
|
56
|
+
signatureInstructionIndex: instructionIndex,
|
|
57
|
+
publicKeyOffset,
|
|
58
|
+
publicKeyInstructionIndex: instructionIndex,
|
|
59
|
+
messageDataOffset,
|
|
60
|
+
messageDataSize: messageDataSize,
|
|
61
|
+
messageInstructionIndex: instructionIndex,
|
|
62
|
+
}, instructionData);
|
|
63
|
+
return new web3_js_1.TransactionInstruction({
|
|
64
|
+
keys: [],
|
|
65
|
+
programId: web3_js_1.Ed25519Program.programId,
|
|
66
|
+
data: instructionData,
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
exports.createEd25519Instruction = createEd25519Instruction;
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -1,21 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export
|
|
4
|
-
subscriptionId: number;
|
|
5
|
-
evm?: Buffer | undefined;
|
|
6
|
-
solana?: Buffer | undefined;
|
|
7
|
-
parsed?: ParsedPayload | undefined;
|
|
8
|
-
};
|
|
9
|
-
export type JsonOrBinaryResponse = {
|
|
10
|
-
type: "json";
|
|
11
|
-
value: Response;
|
|
12
|
-
} | {
|
|
13
|
-
type: "binary";
|
|
14
|
-
value: BinaryResponse;
|
|
15
|
-
};
|
|
16
|
-
export declare class PythLazerClient {
|
|
17
|
-
ws: WebSocket;
|
|
18
|
-
constructor(url: string, token: string);
|
|
19
|
-
addMessageListener(handler: (event: JsonOrBinaryResponse) => void): void;
|
|
20
|
-
send(request: Request): void;
|
|
21
|
-
}
|
|
1
|
+
export * from "./client.js";
|
|
2
|
+
export * from "./protocol.js";
|
|
3
|
+
export * from "./ed25519.js";
|
package/dist/cjs/index.js
CHANGED
|
@@ -1,73 +1,19 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var
|
|
3
|
-
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
4
15
|
};
|
|
5
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const UINT16_NUM_BYTES = 2;
|
|
10
|
-
const UINT32_NUM_BYTES = 4;
|
|
11
|
-
const UINT64_NUM_BYTES = 8;
|
|
12
|
-
class PythLazerClient {
|
|
13
|
-
ws;
|
|
14
|
-
constructor(url, token) {
|
|
15
|
-
const finalUrl = new URL(url);
|
|
16
|
-
finalUrl.searchParams.append("ACCESS_TOKEN", token);
|
|
17
|
-
this.ws = new isomorphic_ws_1.default(finalUrl);
|
|
18
|
-
}
|
|
19
|
-
addMessageListener(handler) {
|
|
20
|
-
this.ws.addEventListener("message", (event) => {
|
|
21
|
-
if (typeof event.data == "string") {
|
|
22
|
-
handler({
|
|
23
|
-
type: "json",
|
|
24
|
-
value: JSON.parse(event.data),
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
else if (Buffer.isBuffer(event.data)) {
|
|
28
|
-
let pos = 0;
|
|
29
|
-
const magic = event.data
|
|
30
|
-
.subarray(pos, pos + UINT32_NUM_BYTES)
|
|
31
|
-
.readUint32BE();
|
|
32
|
-
pos += UINT32_NUM_BYTES;
|
|
33
|
-
if (magic != protocol_js_1.BINARY_UPDATE_FORMAT_MAGIC) {
|
|
34
|
-
throw new Error("binary update format magic mismatch");
|
|
35
|
-
}
|
|
36
|
-
// TODO: some uint64 values may not be representable as Number.
|
|
37
|
-
const subscriptionId = Number(event.data.subarray(pos, pos + UINT64_NUM_BYTES).readBigInt64BE());
|
|
38
|
-
pos += UINT64_NUM_BYTES;
|
|
39
|
-
const value = { subscriptionId };
|
|
40
|
-
while (pos < event.data.length) {
|
|
41
|
-
const len = event.data
|
|
42
|
-
.subarray(pos, pos + UINT16_NUM_BYTES)
|
|
43
|
-
.readUint16BE();
|
|
44
|
-
pos += UINT16_NUM_BYTES;
|
|
45
|
-
const magic = event.data
|
|
46
|
-
.subarray(pos, pos + UINT32_NUM_BYTES)
|
|
47
|
-
.readUint32BE();
|
|
48
|
-
if (magic == protocol_js_1.EVM_FORMAT_MAGIC) {
|
|
49
|
-
value.evm = event.data.subarray(pos, pos + len);
|
|
50
|
-
}
|
|
51
|
-
else if (magic == protocol_js_1.SOLANA_FORMAT_MAGIC_BE) {
|
|
52
|
-
value.solana = event.data.subarray(pos, pos + len);
|
|
53
|
-
}
|
|
54
|
-
else if (magic == protocol_js_1.PARSED_FORMAT_MAGIC) {
|
|
55
|
-
value.parsed = JSON.parse(event.data.subarray(pos + UINT32_NUM_BYTES, pos + len).toString());
|
|
56
|
-
}
|
|
57
|
-
else {
|
|
58
|
-
throw new Error("unknown magic: " + magic.toString());
|
|
59
|
-
}
|
|
60
|
-
pos += len;
|
|
61
|
-
}
|
|
62
|
-
handler({ type: "binary", value });
|
|
63
|
-
}
|
|
64
|
-
else {
|
|
65
|
-
throw new TypeError("unexpected event data type");
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
send(request) {
|
|
70
|
-
this.ws.send(JSON.stringify(request));
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
exports.PythLazerClient = PythLazerClient;
|
|
17
|
+
__exportStar(require("./client.js"), exports);
|
|
18
|
+
__exportStar(require("./protocol.js"), exports);
|
|
19
|
+
__exportStar(require("./ed25519.js"), exports);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ClientRequestArgs } from "node:http";
|
|
2
|
+
import WebSocket, { type ClientOptions, type ErrorEvent } from "isomorphic-ws";
|
|
3
|
+
import type { Logger } from "ts-log";
|
|
4
|
+
/**
|
|
5
|
+
* This class wraps websocket to provide a resilient web socket client.
|
|
6
|
+
*
|
|
7
|
+
* It will reconnect if connection fails with exponential backoff. Also, it will reconnect
|
|
8
|
+
* if it receives no ping request or regular message from server within a while as indication
|
|
9
|
+
* of timeout (assuming the server sends either regularly).
|
|
10
|
+
*
|
|
11
|
+
* This class also logs events if logger is given and by replacing onError method you can handle
|
|
12
|
+
* connection errors yourself (e.g: do not retry and close the connection).
|
|
13
|
+
*/
|
|
14
|
+
export declare class ResilientWebSocket {
|
|
15
|
+
endpoint: string;
|
|
16
|
+
wsClient: undefined | WebSocket;
|
|
17
|
+
wsUserClosed: boolean;
|
|
18
|
+
private wsOptions;
|
|
19
|
+
private wsFailedAttempts;
|
|
20
|
+
private heartbeatTimeout;
|
|
21
|
+
private logger;
|
|
22
|
+
onError: (error: ErrorEvent) => void;
|
|
23
|
+
onMessage: (data: WebSocket.Data) => void;
|
|
24
|
+
onReconnect: () => void;
|
|
25
|
+
constructor(endpoint: string, wsOptions?: ClientOptions | ClientRequestArgs, logger?: Logger);
|
|
26
|
+
send(data: string | Buffer): Promise<void>;
|
|
27
|
+
startWebSocket(): void;
|
|
28
|
+
/**
|
|
29
|
+
* Reset the heartbeat timeout. This is called when we receive any message (ping or regular)
|
|
30
|
+
* from the server. If we don't receive any message within HEARTBEAT_TIMEOUT_DURATION,
|
|
31
|
+
* we assume the connection is dead and reconnect.
|
|
32
|
+
*/
|
|
33
|
+
private resetHeartbeat;
|
|
34
|
+
private waitForMaybeReadyWebSocket;
|
|
35
|
+
private handleClose;
|
|
36
|
+
private restartUnexpectedClosedWebsocket;
|
|
37
|
+
closeWebSocket(): void;
|
|
38
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.ResilientWebSocket = void 0;
|
|
7
|
+
const isomorphic_ws_1 = __importDefault(require("isomorphic-ws"));
|
|
8
|
+
// Reconnect with expo backoff if we don't get a message or ping for 10 seconds
|
|
9
|
+
const HEARTBEAT_TIMEOUT_DURATION = 10_000;
|
|
10
|
+
/**
|
|
11
|
+
* This class wraps websocket to provide a resilient web socket client.
|
|
12
|
+
*
|
|
13
|
+
* It will reconnect if connection fails with exponential backoff. Also, it will reconnect
|
|
14
|
+
* if it receives no ping request or regular message from server within a while as indication
|
|
15
|
+
* of timeout (assuming the server sends either regularly).
|
|
16
|
+
*
|
|
17
|
+
* This class also logs events if logger is given and by replacing onError method you can handle
|
|
18
|
+
* connection errors yourself (e.g: do not retry and close the connection).
|
|
19
|
+
*/
|
|
20
|
+
class ResilientWebSocket {
|
|
21
|
+
endpoint;
|
|
22
|
+
wsClient;
|
|
23
|
+
wsUserClosed;
|
|
24
|
+
wsOptions;
|
|
25
|
+
wsFailedAttempts;
|
|
26
|
+
heartbeatTimeout;
|
|
27
|
+
logger;
|
|
28
|
+
onError;
|
|
29
|
+
onMessage;
|
|
30
|
+
onReconnect;
|
|
31
|
+
constructor(endpoint, wsOptions, logger) {
|
|
32
|
+
this.endpoint = endpoint;
|
|
33
|
+
this.wsOptions = wsOptions;
|
|
34
|
+
this.logger = logger;
|
|
35
|
+
this.wsFailedAttempts = 0;
|
|
36
|
+
this.onError = (error) => {
|
|
37
|
+
this.logger?.error(error.error);
|
|
38
|
+
};
|
|
39
|
+
this.wsUserClosed = true;
|
|
40
|
+
this.onMessage = (data) => {
|
|
41
|
+
void data;
|
|
42
|
+
};
|
|
43
|
+
this.onReconnect = () => {
|
|
44
|
+
// Empty function, can be set by the user.
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
async send(data) {
|
|
48
|
+
this.logger?.info(`Sending message`);
|
|
49
|
+
await this.waitForMaybeReadyWebSocket();
|
|
50
|
+
if (this.wsClient === undefined) {
|
|
51
|
+
this.logger?.error("Couldn't connect to the websocket server. Error callback is called.");
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
this.wsClient.send(data);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
startWebSocket() {
|
|
58
|
+
if (this.wsClient !== undefined) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
this.logger?.info(`Creating Web Socket client`);
|
|
62
|
+
this.wsClient = new isomorphic_ws_1.default(this.endpoint, this.wsOptions);
|
|
63
|
+
this.wsUserClosed = false;
|
|
64
|
+
this.wsClient.addEventListener("open", () => {
|
|
65
|
+
this.wsFailedAttempts = 0;
|
|
66
|
+
this.resetHeartbeat();
|
|
67
|
+
});
|
|
68
|
+
this.wsClient.addEventListener("error", (event) => {
|
|
69
|
+
this.onError(event);
|
|
70
|
+
});
|
|
71
|
+
this.wsClient.addEventListener("message", (event) => {
|
|
72
|
+
this.resetHeartbeat();
|
|
73
|
+
this.onMessage(event.data);
|
|
74
|
+
});
|
|
75
|
+
this.wsClient.addEventListener("close", () => {
|
|
76
|
+
void this.handleClose();
|
|
77
|
+
});
|
|
78
|
+
// Handle ping events if supported (Node.js only)
|
|
79
|
+
if ("on" in this.wsClient) {
|
|
80
|
+
// Ping handler is undefined in browser side
|
|
81
|
+
this.wsClient.on("ping", () => {
|
|
82
|
+
this.logger?.info("Ping received");
|
|
83
|
+
this.resetHeartbeat();
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Reset the heartbeat timeout. This is called when we receive any message (ping or regular)
|
|
89
|
+
* from the server. If we don't receive any message within HEARTBEAT_TIMEOUT_DURATION,
|
|
90
|
+
* we assume the connection is dead and reconnect.
|
|
91
|
+
*/
|
|
92
|
+
resetHeartbeat() {
|
|
93
|
+
if (this.heartbeatTimeout !== undefined) {
|
|
94
|
+
clearTimeout(this.heartbeatTimeout);
|
|
95
|
+
}
|
|
96
|
+
this.heartbeatTimeout = setTimeout(() => {
|
|
97
|
+
this.logger?.warn("Connection timed out. Reconnecting...");
|
|
98
|
+
this.wsClient?.terminate();
|
|
99
|
+
void this.restartUnexpectedClosedWebsocket();
|
|
100
|
+
}, HEARTBEAT_TIMEOUT_DURATION);
|
|
101
|
+
}
|
|
102
|
+
async waitForMaybeReadyWebSocket() {
|
|
103
|
+
let waitedTime = 0;
|
|
104
|
+
while (this.wsClient !== undefined &&
|
|
105
|
+
this.wsClient.readyState !== this.wsClient.OPEN) {
|
|
106
|
+
if (waitedTime > 5000) {
|
|
107
|
+
this.wsClient.close();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
waitedTime += 10;
|
|
112
|
+
await sleep(10);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async handleClose() {
|
|
117
|
+
if (this.heartbeatTimeout !== undefined) {
|
|
118
|
+
clearTimeout(this.heartbeatTimeout);
|
|
119
|
+
}
|
|
120
|
+
if (this.wsUserClosed) {
|
|
121
|
+
this.logger?.info("The connection has been closed successfully.");
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
this.wsFailedAttempts += 1;
|
|
125
|
+
this.wsClient = undefined;
|
|
126
|
+
const waitTime = expoBackoff(this.wsFailedAttempts);
|
|
127
|
+
this.logger?.error("Connection closed unexpectedly or because of timeout. Reconnecting after " +
|
|
128
|
+
String(waitTime) +
|
|
129
|
+
"ms.");
|
|
130
|
+
await sleep(waitTime);
|
|
131
|
+
await this.restartUnexpectedClosedWebsocket();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async restartUnexpectedClosedWebsocket() {
|
|
135
|
+
if (this.wsUserClosed) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
this.startWebSocket();
|
|
139
|
+
await this.waitForMaybeReadyWebSocket();
|
|
140
|
+
if (this.wsClient === undefined) {
|
|
141
|
+
this.logger?.error("Couldn't reconnect to websocket. Error callback is called.");
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
this.onReconnect();
|
|
145
|
+
}
|
|
146
|
+
closeWebSocket() {
|
|
147
|
+
if (this.wsClient !== undefined) {
|
|
148
|
+
const client = this.wsClient;
|
|
149
|
+
this.wsClient = undefined;
|
|
150
|
+
client.close();
|
|
151
|
+
}
|
|
152
|
+
this.wsUserClosed = true;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
exports.ResilientWebSocket = ResilientWebSocket;
|
|
156
|
+
async function sleep(ms) {
|
|
157
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
158
|
+
}
|
|
159
|
+
function expoBackoff(attempts) {
|
|
160
|
+
return 2 ** attempts * 100;
|
|
161
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import WebSocket from "isomorphic-ws";
|
|
2
|
+
import { type Logger } from "ts-log";
|
|
3
|
+
import { ResilientWebSocket } from "./resilient-web-socket.js";
|
|
4
|
+
import type { Request } from "../protocol.js";
|
|
5
|
+
export declare class WebSocketPool {
|
|
6
|
+
private readonly logger;
|
|
7
|
+
rwsPool: ResilientWebSocket[];
|
|
8
|
+
private cache;
|
|
9
|
+
private subscriptions;
|
|
10
|
+
private messageListeners;
|
|
11
|
+
/**
|
|
12
|
+
* Creates a new WebSocketPool instance that uses multiple redundant WebSocket connections for reliability.
|
|
13
|
+
* Usage semantics are similar to using a regular WebSocket client.
|
|
14
|
+
* @param urls - List of WebSocket URLs to connect to
|
|
15
|
+
* @param token - Authentication token to use for the connections
|
|
16
|
+
* @param numConnections - Number of parallel WebSocket connections to maintain (default: 3)
|
|
17
|
+
* @param logger - Optional logger to get socket level logs. Compatible with most loggers such as the built-in console and `bunyan`.
|
|
18
|
+
*/
|
|
19
|
+
constructor(urls: string[], token: string, numConnections?: number, logger?: Logger);
|
|
20
|
+
/**
|
|
21
|
+
* Checks for error responses in JSON messages and throws appropriate errors
|
|
22
|
+
*/
|
|
23
|
+
private handleErrorMessages;
|
|
24
|
+
/**
|
|
25
|
+
* Handles incoming websocket messages by deduplicating identical messages received across
|
|
26
|
+
* multiple connections before forwarding to registered handlers
|
|
27
|
+
*/
|
|
28
|
+
dedupeHandler: (data: WebSocket.Data) => void;
|
|
29
|
+
/**
|
|
30
|
+
* Sends a message to all websockets in the pool
|
|
31
|
+
* @param request - The request to send
|
|
32
|
+
*/
|
|
33
|
+
sendRequest(request: Request): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Adds a subscription by sending a subscribe request to all websockets in the pool
|
|
36
|
+
* and storing it for replay on reconnection
|
|
37
|
+
* @param request - The subscription request to send
|
|
38
|
+
*/
|
|
39
|
+
addSubscription(request: Request): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Removes a subscription by sending an unsubscribe request to all websockets in the pool
|
|
42
|
+
* and removing it from stored subscriptions
|
|
43
|
+
* @param subscriptionId - The ID of the subscription to remove
|
|
44
|
+
*/
|
|
45
|
+
removeSubscription(subscriptionId: number): Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* Adds a message handler function to receive websocket messages
|
|
48
|
+
* @param handler - Function that will be called with each received message
|
|
49
|
+
*/
|
|
50
|
+
addMessageListener(handler: (data: WebSocket.Data) => void): void;
|
|
51
|
+
/**
|
|
52
|
+
* Elegantly closes all websocket connections in the pool
|
|
53
|
+
*/
|
|
54
|
+
shutdown(): void;
|
|
55
|
+
}
|