import { logging } from "@cityofzion/neon-core";
import WebSocket from "isomorphic-ws";
import Subscription from "./subscription";
const log = logging.default("api");
export class Notifications {
    /**
     * Create a new notification service that handles contract subscriptions
     * Source code and instructions on how to run this service are available on https://github.com/corollari/neo-PubSub
     * @param url - URL of a notifications service.
     */
    constructor(url) {
        this.uniqueIdentifier = 0;
        this.url = url;
        this.subscriptions = new Map();
        log.info(`Created Notifications Provider: ${this.url}`);
    }
    get name() {
        return `Notifications[${this.url}]`;
    }
    /**
     * Subscribe to event notifications of a specific contract
     * @param contract - Hash of the contract (null for all contracts) to subscribe to
     * @param callback - Function to call when a notification is received.
     * @returns Subscription object that can be used to cancel the subscription
     */
    subscribe(contract, callback) {
        var _a;
        contract = this.normalizeContract(contract);
        const subscriptionIdentifier = this.uniqueIdentifier++;
        const contractSubscriptions = (_a = this.subscriptions.get(contract)) !== null && _a !== void 0 ? _a : this.createWebsocketForContract(contract);
        contractSubscriptions.callbacks.set(subscriptionIdentifier, callback);
        const unsubscribe = () => {
            var _a;
            const callbacksMaps = (_a = this.subscriptions.get(contract)) === null || _a === void 0 ? void 0 : _a.callbacks;
            if (!(callbacksMaps === null || callbacksMaps === void 0 ? void 0 : callbacksMaps.get(subscriptionIdentifier))) {
                // Check if the subscription hasn't been killed before
                return;
            }
            if (callbacksMaps.size > 1) {
                callbacksMaps.delete(subscriptionIdentifier);
            }
            else {
                this.unsubscribeContract(contract);
            }
        };
        return new Subscription(contract, unsubscribe);
    }
    /**
     * Unsubscribe all subscriptions associated with a specific contract
     * @param contract - Hash of the contract (or null for subscriptions to all contracts) to unsubscribe callbacks from
     */
    unsubscribeContract(contract) {
        contract = this.normalizeContract(contract);
        const subscriptionObj = this.subscriptions.get(contract);
        if (!subscriptionObj ||
            subscriptionObj.websocket.readyState >= WebSocket.CLOSING) {
            return Promise.resolve(); // Not throwing an exception in order to allow unlimited calling of this function
        }
        const closingPromise = new Promise((resolve) => {
            subscriptionObj.websocket.onclose = () => {
                this.subscriptions.delete(contract);
                resolve();
            };
            subscriptionObj.websocket.close();
        });
        const terminatePromise = new Promise((resolve) => setTimeout(resolve, 5000)).then(() => {
            if (subscriptionObj.websocket.readyState != WebSocket.CLOSED) {
                log.warn(`Websocket for Subscription[${contract}] did not close in time (was in state ${subscriptionObj.websocket.readyState}). Force terminating.`);
                subscriptionObj.websocket.terminate();
            }
        });
        return Promise.race([closingPromise, terminatePromise]);
    }
    /**
     * Unsubscribe all subscriptions (equivalent to calling unsubscribeContract() once for every contract)
     */
    unsubscribeAll() {
        const closingPromises = [];
        for (const contract of this.subscriptions.keys()) {
            closingPromises.push(this.unsubscribeContract(contract));
        }
        return Promise.all(closingPromises).then();
    }
    createWebsocketForContract(contract) {
        const ws = new WebSocket(this.url + (contract !== null ? "?contract=" + contract : ""));
        ws.onmessage = (event) => {
            var _a, _b;
            const callbackArray = (_b = (_a = this.subscriptions.get(contract)) === null || _a === void 0 ? void 0 : _a.callbacks) !== null && _b !== void 0 ? _b : [];
            for (const cb of callbackArray.values()) {
                cb(JSON.parse(event.data));
            }
        };
        const contractSubscriptions = {
            websocket: ws,
            callbacks: new Map(),
        };
        this.subscriptions.set(contract, contractSubscriptions);
        return contractSubscriptions;
    }
    normalizeContract(contract) {
        if (contract === null) {
            return null;
        }
        else if (contract.slice(0, 2) === "0x") {
            return contract;
        }
        else {
            return "0x" + contract;
        }
    }
}
export default Notifications;
//# sourceMappingURL=class.js.map