UNPKG

js-moi-providers

Version:

Module to connect and interact with MOI network

315 lines 12.2 kB
import { ErrorCode, ErrorUtils } from "js-moi-utils"; import { w3cwebsocket as Websocket } from "websocket"; import { BaseProvider } from "./base-provider"; import { WebSocketEvent } from "./websocket-events"; import { randomUUID } from "crypto"; const WEBSOCKET_HOST_REGEX = /^wss?:\/\/([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+(:[0-9]+)?(\/.*)?$/; export class WebsocketProvider extends BaseProvider { ws; reconnects = 0; reconnectInterval; host; options; subscriptions = new Map(); constructor(host, options) { if (!WEBSOCKET_HOST_REGEX.test(host)) { ErrorUtils.throwArgumentError("Invalid host", "host", host); } super(); this.host = host; this.options = options; this.ws = this.createNewWebsocket(host, options); } createNewWebsocket(host, options) { const ws = new Websocket(host, options?.protocols, undefined, options?.headers ?? {}, options?.requestOptions, options?.clientConfig); ws.onopen = () => this.handleOnConnect(); ws.onerror = (error) => this.handleOnError(error); ws.onclose = (event) => this.handleOnClose(event); ws.onmessage = (message) => this.emit('message', message); if (options?.timeout) { setTimeout(() => { if (ws.readyState === ws.OPEN) { return; } ; ws.close(3008, "Connection timeout"); }, options.timeout); } return ws; } reconnect() { this.reconnects++; this.ws = this.createNewWebsocket(this.host, this.options); this.emit('reconnect', this.reconnects); if (this.options.reconnect) { const interval = setInterval(() => { if (this.ws.readyState === this.ws.OPEN) { clearInterval(interval); return; } if (this.reconnects >= this.options.reconnect.maxAttempts) { this.emit('error', new Error('Max reconnect attempts reached')); clearInterval(interval); return; } this.reconnects++; this.ws = this.createNewWebsocket(this.host, this.options); this.emit('reconnect', this.reconnects); }, this.options.reconnect.delay); } } async disconnect() { if (this.ws.readyState === this.ws.OPEN) { this.ws.close(); } if (this.ws.readyState === this.ws.CLOSED) { ErrorUtils.throwError("Closing on a closed connection", ErrorCode.ACTION_REJECTED); } if (this.ws.readyState === this.ws.CLOSING) { return new Promise((resolve) => { this.once(WebSocketEvent.Close, () => { resolve(); }); }); } if (this.ws.readyState === this.ws.CONNECTING) { return new Promise((resolve) => { this.once(WebSocketEvent.Connect, () => { this.ws.close(1000); resolve(); }); }); } } handleOnConnect() { this.reconnects = 0; this.emit('connect'); } handleOnError(error) { this.emit('error', error); } handleOnClose(event) { const isError = event.code !== 1000; if (isError) { if (this.options?.reconnect && this.reconnects < this.options.reconnect.maxAttempts) { if (this.reconnectInterval) { clearInterval(this.reconnectInterval); } this.reconnect(); return; } } this.emit('close'); } execute(method, params) { if (this.ws.readyState !== this.ws.OPEN) { return new Promise((resolve) => { this.once(WebSocketEvent.Connect, async () => { resolve(await this.handleRpcRequest(method, params)); }); }); } return this.handleRpcRequest(method, params); } handleRpcRequest(method, params) { const inputParams = Array.isArray(params) ? params : [params]; const payload = { method: method, params: inputParams, jsonrpc: "2.0", id: randomUUID(), }; return new Promise((resolve) => { const handler = (message) => { const response = JSON.parse(message.data); if (response.id !== payload.id) { return; } // @ts-ignore - don't want to expose the message event this.removeListener('message', handler); resolve({ ...response, id: 1 }); }; // @ts-ignore - don't want to expose the message event this.on('message', handler); this.ws.send(JSON.stringify(payload)); }); } isSubscriptionEvent(eventName) { const events = ['newTesseracts', 'newTesseractsByAccount', 'newLogs', 'newPendingInteractions']; const name = typeof eventName === "string" ? eventName : eventName.event; return events.includes(name); } async getSubscription(eventName) { const sub = this.subscriptions.get(eventName); if (sub?.subID != null) { return await this.subscriptions.get(eventName).subID; } if (sub == null) { const promise = super.getSubscription(eventName); this.subscriptions.set(eventName, { subID: promise }); return await promise; } sub.subID = super.getSubscription(eventName); return await sub.subID; } /** * This method listens to events emitted by the provider for the given event * * @param eventName - The event to listen to this can be a string or an object * @param listener - The callback function to be called when the event is emitted * @returns - The provider instance */ on(eventName, listener) { if (typeof eventName === "string") { super.on(eventName, listener); } if (typeof eventName === "object") { if (this.subscriptions.has(eventName)) { const _sub = this.subscriptions.get(eventName); if (_sub?.uuid == null) { _sub.uuid = `${eventName.event}:${randomUUID()}`; } super.on(_sub.uuid, listener); } else { const uuid = `${eventName.event}:${randomUUID()}`; this.subscriptions.set(eventName, { uuid }); super.on(uuid, listener); } } if (this.isSubscriptionEvent(eventName)) { const _sub = this.subscriptions.get(eventName); if (_sub?.subID != null) { return this; } this.getSubscription(eventName).then((subscription) => { // @ts-ignore - don't want to expose the message event this.on("message", (message) => { const data = JSON.parse(message.data); if (!("method" in data) || data.method !== "moi.subscription" || data.params.subscription !== subscription) { return; } if (typeof eventName === "string") { this.emit(eventName, this.processWsResult(eventName, data.params.result)); return; } if (typeof eventName === "object" && _sub.uuid != null) { this.emit(_sub.uuid, this.processWsResult(eventName, data.params.result)); return; } }); }); } return this; } /** * Adds a one-time listener function for the specified event. * * @param eventName - The name of the event to listen for. * @param listener - A function to be called when the event is triggered. * @returns The WebSocketProvider instance. */ once(eventName, listener) { if (typeof eventName === "string") { super.once(eventName, listener); } if (typeof eventName === "object") { if (this.subscriptions.has(eventName)) { const _sub = this.subscriptions.get(eventName); if (_sub?.uuid == null) { _sub.uuid = `${eventName.event}:${randomUUID()}`; } super.once(_sub.uuid, listener); } else { const uuid = `${eventName.event}:${randomUUID()}`; this.subscriptions.set(eventName, { uuid }); super.once(uuid, listener); } } if (this.isSubscriptionEvent(eventName)) { const _sub = this.subscriptions.get(eventName); if (_sub?.subID != null) { return this; } this.getSubscription(eventName).then((subscription) => { // @ts-ignore - don't want to expose the message event this.on("message", (message) => { const data = JSON.parse(message.data); if (!("method" in data) || data.method !== "moi.subscription" || data.params.subscription !== subscription) { return; } if (typeof eventName === "string") { this.emit(eventName, this.processWsResult(eventName, data.params.result)); return; } if (typeof eventName === "object" && _sub.uuid != null) { this.emit(_sub.uuid, this.processWsResult(eventName, data.params.result)); return; } }); }); } return this; } /** * Removes a listener from the WebSocket provider. * * @param eventName - The name of the event or an object representing a subscription. * @param listener - The listener function to be removed. * @returns The WebSocket provider instance. */ removeListener(eventName, listener) { if (typeof eventName === "string") { super.removeListener(eventName, listener); } if (typeof eventName === "object") { const _sub = this.subscriptions.get(eventName); if (_sub?.uuid == null) { return this; } super.removeListener(_sub.uuid, listener); this.subscriptions.delete(eventName); } return this; } /** * This method removes a listener from the provider * * @param eventName - The event to remove the listener from * @param listener - The listener to remove * @returns - The provider instance */ off(eventName, listener) { return super.off(eventName, listener); } /** * This methods returns all the listeners for a given event * * @param eventName - The event to get the listeners for * @returns - An array of listeners */ listeners(eventName) { return super.listeners(eventName); } /** * Returns the number of listeners for the specified event name. * * @param eventName - The name of the event. * @param listener - (Optional) The listener function. * @returns The number of listeners for the specified event name. */ listenerCount(eventName, listener) { return super.listenerCount(eventName, listener); } /** * Removes all event listeners for the specified event or all events. * * @param event - The event to remove listeners for. If not specified, all listeners for all events will be removed. * @returns The instance of the class with all listeners removed. */ removeAllListeners(event) { return super.removeAllListeners(event); } } //# sourceMappingURL=websocket-provider.js.map