nimiq-rpc-client-ts
Version:
A Nimiq RPC client for TypeScript
177 lines (171 loc) • 6.22 kB
JavaScript
;
const WebSocket = require('isomorphic-ws');
const client = require('./client.cjs');
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; }
const WebSocket__default = /*#__PURE__*/_interopDefaultCompat(WebSocket);
const DEFAULT_TIMEOUT = 3e4;
const DEFAULT_RECONNECT = { retries: 3, delay: 1e3, onFailed: () => {
} };
async function rpcSubscribe(method, params = [], options = {}) {
const baseUrl = await client.__getBaseUrl();
const wsUrl = new URL(baseUrl.toString());
wsUrl.protocol = wsUrl.protocol.replace(/^http/, "ws");
wsUrl.pathname = "/ws";
const { once, filter, timeout = DEFAULT_TIMEOUT, autoReconnect = DEFAULT_RECONNECT, onError } = options;
let ws = null;
let open = false;
let closed = false;
let idCounter = 1;
let reconnects = 0;
let subscriptionId = 0;
const pending = /* @__PURE__ */ new Map();
const emitter = new EventTarget();
emitter.getId = () => subscriptionId;
emitter.isOpen = () => open;
emitter.context = { method, params, url: wsUrl.href.split("?")[0], timestamp: Date.now() };
function scheduleReconnect() {
const settings = typeof autoReconnect === "boolean" ? DEFAULT_RECONNECT : { ...DEFAULT_RECONNECT, ...autoReconnect };
const { retries, delay, onFailed: failCb } = settings;
const canRetry = typeof retries === "number" ? retries < 0 || reconnects < retries : retries();
if (canRetry) {
reconnects++;
ws = null;
setTimeout(() => {
rpcSubscribe(method, params, options).then((newSub) => {
newSub.addEventListener("open", (e) => emitter.dispatchEvent(e));
newSub.addEventListener("data", (e) => emitter.dispatchEvent(e));
newSub.addEventListener("error", (e) => emitter.dispatchEvent(e));
newSub.addEventListener("close", (e) => emitter.dispatchEvent(e));
}).catch((err) => onError?.(err));
}, delay);
} else {
failCb?.();
}
}
function handleError(err) {
onError?.(err);
emitter.dispatchEvent(new CustomEvent("error", { detail: err }));
}
async function handleMessage(raw) {
try {
const Buffer = typeof window === "undefined" ? (await import('node:buffer')).Buffer : globalThis.Buffer;
if (raw instanceof Blob)
raw = await raw.arrayBuffer().then((buffer) => new TextDecoder().decode(buffer));
else if (raw instanceof ArrayBuffer)
raw = new TextDecoder().decode(raw);
else if (raw instanceof Buffer)
raw = raw.toString();
if (raw instanceof ArrayBuffer) {
if (typeof TextDecoder !== "undefined") {
raw = new TextDecoder().decode(raw);
} else {
const { Buffer: Buffer2 } = await import('node:buffer');
raw = Buffer2.from(raw).toString();
}
}
const msg = JSON.parse(raw);
if (msg.method && msg.params && msg.params.subscription && msg.params.result) {
const { data: payload, metadata: meta } = msg.params.result;
let formattedPayload = payload;
if (msg.method === "subscribeForHeadBlockHash" && typeof payload === "string") {
formattedPayload = [payload];
}
if (!filter || filter(formattedPayload)) {
emitter.dispatchEvent(new CustomEvent("data", { detail: { data: formattedPayload, metadata: meta } }));
if (once)
close();
}
return;
}
if ("id" in msg && pending.has(msg.id)) {
const { resolve, reject } = pending.get(msg.id);
if (msg.error) {
reject(msg.error);
} else {
resolve(msg.result);
}
pending.delete(msg.id);
}
} catch (e) {
handleError(e);
}
}
function connect() {
return new Promise((resolve) => {
if (!ws) {
ws = new WebSocket__default(wsUrl.href);
ws.onopen = () => {
open = true;
reconnects = 0;
emitter.dispatchEvent(new Event("open"));
resolve();
};
ws.onmessage = ({ data }) => handleMessage(data);
ws.onerror = (e) => handleError(new Error(String(e)));
ws.onclose = (ev) => {
open = false;
emitter.dispatchEvent(new CustomEvent("close", { detail: { code: ev.code, reason: ev.reason } }));
if (!closed && autoReconnect)
scheduleReconnect();
};
} else if (open) {
resolve();
}
});
}
async function sendRequest() {
await connect();
return new Promise((res, rej) => {
const id = idCounter++;
const payload = { jsonrpc: "2.0", id, method, params };
let timer;
if (timeout !== false) {
timer = setTimeout(() => {
pending.delete(id);
rej(new Error(`Timeout after ${timeout}ms`));
}, timeout);
}
pending.set(id, {
resolve: (val) => {
clearTimeout(timer);
res(val);
},
reject: (err) => {
clearTimeout(timer);
rej(err);
}
});
ws?.send(JSON.stringify(payload));
});
}
function close() {
closed = true;
ws?.close();
pending.clear();
emitter.dispatchEvent(new Event("close"));
}
await connect();
subscriptionId = await sendRequest();
return emitter;
}
function subscribeForHeadBlock(fullBlock = false, opts) {
return rpcSubscribe("subscribeForHeadBlock", [fullBlock], opts);
}
function subscribeForHeadBlockHash(opts) {
return rpcSubscribe("subscribeForHeadBlockHash", [], opts);
}
function subscribeForLogsByAddressesAndTypes(addresses = [], logTypes = [], opts) {
return rpcSubscribe(
"subscribeForLogsByAddressesAndTypes",
[addresses, logTypes],
opts
);
}
function subscribeForValidatorElectionByAddress(address, opts) {
return rpcSubscribe("subscribeForValidatorElectionByAddress", [address], opts);
}
exports.rpcSubscribe = rpcSubscribe;
exports.subscribeForHeadBlock = subscribeForHeadBlock;
exports.subscribeForHeadBlockHash = subscribeForHeadBlockHash;
exports.subscribeForLogsByAddressesAndTypes = subscribeForLogsByAddressesAndTypes;
exports.subscribeForValidatorElectionByAddress = subscribeForValidatorElectionByAddress;