@ton-api/streaming
Version:
Realtime streaming SDK for tonapi.io
373 lines (364 loc) • 11.6 kB
JavaScript
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var src_exports = {};
__export(src_exports, {
AccountsObserver: () => AccountsObserver,
BlocksObserver: () => BlocksObserver,
MempoolObserver: () => MempoolObserver,
TraceObserver: () => TraceObserver,
WebsocketStreamProvider: () => WebsocketStreamProvider
});
module.exports = __toCommonJS(src_exports);
// src/observers/observer.ts
var Observer = class {
constructor(stream) {
this.stream = stream;
}
subscribers = [];
socketSubscription;
subscribe(triggers, callback) {
if (!Array.isArray(triggers)) {
triggers = [triggers];
}
const subscriber = { callback, triggers };
this.subscribers = this.subscribers.concat(subscriber);
if (!this.socketSubscription) {
this.socketSubscription = this.stream.subscribe(this.onEvent);
}
this.afterSubscribed(subscriber);
return () => {
this.subscribers = this.subscribers.filter((s) => s !== subscriber);
if (!this.subscribers.length) {
this.socketSubscription?.();
}
this.afterUnsubscribed(subscriber);
};
}
send(...args) {
return this.stream.send(...args);
}
onEvent = (rpcEvent) => {
const event = this.extractEvent(rpcEvent);
if (!event) {
return;
}
this.subscribers.forEach((s) => {
if (s.triggers.some((t) => this.tryTrigger(t, event))) {
s.callback(event);
}
});
};
};
// src/observers/blocks-observer.ts
var BlocksObserver = class extends Observer {
subscribe(workchain, callback) {
return super.subscribe(workchain, callback);
}
afterSubscribed(subscriber) {
if (subscriber.triggers.length !== 1) {
throw new Error("BlocksObserver supports only one trigger-workchain");
}
const wc = subscriber.triggers[0];
const needAddWc = this.subscribers.filter((s) => s !== subscriber).every((s) => !s.triggers.includes(wc));
if (needAddWc) {
this.send("subscribe_block", [`workchain=${wc}`]);
}
}
afterUnsubscribed({ triggers }) {
if (triggers.length !== 1) {
throw new Error("BlocksObserver supports only one trigger-workchain");
}
const wc = triggers[0];
const needRemoveWc = this.subscribers.every((s) => !s.triggers.includes(wc));
if (needRemoveWc) {
this.send("unsubscribe_block", [`workchain=${wc}`]);
}
}
extractEvent(rpcEvent) {
if (rpcEvent.method !== "block") {
return;
}
return rpcEvent.params;
}
tryTrigger(trigger, event) {
return event.workchain === trigger;
}
};
// src/observers/accounts-observer.ts
var AccountsObserver = class extends Observer {
afterSubscribed(subscriber) {
const triggersToAdd = subscriber.triggers.filter(
(trigger) => this.subscribers.filter((s) => s !== subscriber).every((sub) => {
sub.triggers.every((subTrigger) => {
if (trigger.account === subTrigger.account) {
if (!subTrigger.operations?.length) {
return false;
}
const isForAllOps = !trigger.operations?.length;
const isNotIncluded = trigger.operations.some(
(op) => !subTrigger.operations.includes(op)
);
return isForAllOps || isNotIncluded;
}
return true;
});
})
);
if (triggersToAdd.length) {
const params = subscriber.triggers.map((t) => {
if (!t.operations?.length) {
return t.account;
}
return `${t.account};operations=${t.operations.join(",")}`;
});
this.send("subscribe_account", params);
}
}
afterUnsubscribed({ triggers }) {
const accountsToRemove = triggers.filter(
(t) => this.subscribers.every((s) => s.triggers.every((st) => st.account !== t.account))
).map((t) => t.account);
if (accountsToRemove.length) {
this.send("unsubscribe_account", accountsToRemove);
}
}
extractEvent(rpcEvent) {
if (rpcEvent.method !== "account_transaction") {
return;
}
return rpcEvent.params;
}
tryTrigger(trigger, event) {
if (event.account_id === trigger.account) {
if (event.operation === void 0) {
return true;
}
return !!trigger.operations?.includes(event.operation);
}
return false;
}
};
// src/observers/mempool-observer.ts
var MempoolObserver = class extends Observer {
needToRemoveAllSubscription = false;
/**
*
* @param accounts list of accounts to watch (in raw-form) or a single account (in raw-form) or 'all' to watch all the accounts
* @param callback
*/
subscribe(accounts, callback) {
return super.subscribe(accounts, callback);
}
afterSubscribed(subscriber) {
if (subscriber.triggers.includes("all")) {
this.send("subscribe_mempool");
return;
}
const accountsToAdd = subscriber.triggers.filter(
(t) => this.subscribers.filter((s) => s !== subscriber).every((s) => !s.triggers.includes(t))
);
if (accountsToAdd.length) {
this.send("subscribe_mempool", accountsToAdd.join(","));
}
}
afterUnsubscribed({ triggers }) {
let accountsToRemove = triggers.filter(
(t) => this.subscribers.every((s) => !s.triggers.includes(t))
);
if (accountsToRemove.includes("all")) {
if (!this.subscribers.length) {
this.send("unsubscribe_mempool");
return;
} else {
this.needToRemoveAllSubscription = true;
}
}
if (!this.subscribers.length && this.needToRemoveAllSubscription) {
this.send("unsubscribe_mempool");
return;
}
accountsToRemove = accountsToRemove.filter((t) => t !== "all");
if (accountsToRemove.length) {
this.send("unsubscribe_mempool", accountsToRemove.join(","));
}
}
extractEvent(rpcEvent) {
if (rpcEvent.method !== "mempool_message") {
return;
}
return rpcEvent.params;
}
tryTrigger(trigger, event) {
if (trigger === "all") {
return true;
}
if (!event.involved_accounts) {
return false;
}
return event.involved_accounts.some((acc) => acc.includes(trigger));
}
};
// src/observers/trace-observer.ts
var TraceObserver = class extends Observer {
afterSubscribed(subscriber) {
const accountsToAdd = subscriber.triggers.filter(
(t) => this.subscribers.filter((s) => s !== subscriber).every((s) => !s.triggers.includes(t))
);
if (accountsToAdd.length) {
this.send("subscribe_trace", accountsToAdd);
}
}
afterUnsubscribed({ triggers }) {
const accountsToRemove = triggers.filter(
(t) => this.subscribers.every((s) => !s.triggers.includes(t))
);
if (accountsToRemove.length) {
this.send("unsubscribe_trace", accountsToRemove);
}
}
extractEvent(rpcEvent) {
if (rpcEvent.method !== "trace") {
return;
}
return rpcEvent.params;
}
tryTrigger(trigger, event) {
return event.accounts.some((acc) => acc.includes(trigger));
}
};
// src/models/rpc.ts
function isJsonRpcResponse(message) {
return "id" in message;
}
function isJsonRpcResponseSuccess(message) {
return "result" in message;
}
// src/stream-providers/websocket-stream-provider.ts
var import_isomorphic_ws = require("@ton-api/isomorphic-ws");
var WebsocketStreamProvider = class {
constructor(url) {
this.url = url;
}
socket;
isClosed = false;
pendingRequests = /* @__PURE__ */ new Map();
lastId = 0;
subscriptions = [];
async open(options) {
if (this.isClosed) {
return;
}
this.socket = new WebSocket(this.url);
return new Promise((resolve, reject) => {
const timeout = options?.openingDeadlineMS ? setTimeout(() => {
if (this.socket?.readyState !== WebSocket.OPEN) {
reject(new Error("TonApi stream connection timeout"));
this.close();
}
}, options.openingDeadlineMS) : void 0;
this.socket.onerror = () => reject;
this.socket.onopen = () => {
clearTimeout(timeout);
this.isClosed = false;
this.socket.onerror = this.errorsHandler.bind(this);
this.socket.onmessage = this.messagesHandler.bind(this);
resolve();
};
});
}
async close() {
this.isClosed = true;
this.socket?.close();
}
async send(method, params) {
if (this.isClosed || !this.socket) {
throw new Error("Can't subscribe to closed stream");
}
this.lastId++;
this.socket.send(JSON.stringify({ id: this.lastId, jsonrpc: "2.0", method, params }));
return new Promise((res, rej) => {
this.pendingRequests.set(this.lastId, { res, rej });
});
}
subscribe(subscription) {
this.subscriptions.push(subscription);
return () => {
this.subscriptions = this.subscriptions.filter((s) => s !== subscription);
};
}
errorsHandler(e) {
if (!this.isClosed) {
if (this.socket?.readyState === EventSource.CLOSED) {
this.socket.close();
this.open();
return;
}
if (this.socket?.readyState === EventSource.CONNECTING) {
console.debug("[TON_API_SDK_ERROR]: Stream error", JSON.stringify(e));
return;
}
}
}
async messagesHandler(e) {
if (!this.isClosed) {
let incomingEvent;
try {
incomingEvent = JSON.parse(e.data);
if (!incomingEvent || typeof incomingEvent !== "object") {
throw new Error("Rpc message must be an object");
}
const isResponse = "id" in incomingEvent && incomingEvent.id !== void 0 && "method" in incomingEvent && ("result" in incomingEvent || "error" in incomingEvent && incomingEvent.error && typeof incomingEvent.error === "object" && "message" in incomingEvent.error);
const isEvent = "params" in incomingEvent && incomingEvent.params && "method" in incomingEvent;
if (!isResponse && !isEvent) {
throw new Error("Wrong rpc message formt " + incomingEvent.toString());
}
} catch (e2) {
throw new Error(`TonApi stream message parse failed, message ${e2}`);
}
if (isJsonRpcResponse(incomingEvent)) {
const id = Number(incomingEvent.id);
if (!this.pendingRequests.has(id)) {
console.debug(
"[TON_API_SDK_ERROR]: Unknown rpc response",
JSON.stringify(incomingEvent)
);
return;
}
const { res, rej } = this.pendingRequests.get(id);
if (isJsonRpcResponseSuccess(incomingEvent)) {
res();
} else {
rej(new Error(incomingEvent.error.message));
}
this.pendingRequests.delete(id);
return;
}
this.subscriptions.forEach((sub) => sub(incomingEvent));
}
}
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
AccountsObserver,
BlocksObserver,
MempoolObserver,
TraceObserver,
WebsocketStreamProvider
});