"use strict";
/* eslint-disable @typescript-eslint/camelcase */
// Copyright 2017-2020 @polkadot/rpc-provider authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
require("./polyfill");
const eventemitter3_1 = __importDefault(require("eventemitter3"));
const util_1 = require("@polkadot/util");
const coder_1 = __importDefault(require("../coder"));
const defaults_1 = __importDefault(require("../defaults"));
const ALIASSES = {
    chain_finalisedHead: 'chain_finalizedHead',
    chain_subscribeFinalisedHeads: 'chain_subscribeFinalizedHeads',
    chain_unsubscribeFinalisedHeads: 'chain_unsubscribeFinalizedHeads'
};
const l = util_1.logger('api-ws');
/**
 * # @polkadot/rpc-provider/ws
 *
 * @name WsProvider
 *
 * @description The WebSocket Provider allows sending requests using WebSocket to a WebSocket RPC server TCP port. Unlike the [[HttpProvider]], it does support subscriptions and allows listening to events such as new blocks or balance changes.
 *
 * @example
 * <BR>
 *
 * ```javascript
 * import Api from '@polkadot/api/promise';
 * import WsProvider from '@polkadot/rpc-provider/ws';
 *
 * const provider = new WsProvider('ws://127.0.0.1:9944');
 * const api = new Api(provider);
 * ```
 *
 * @see [[HttpProvider]]
 */
class WsProvider {
    /**
     * @param {string}  endpoint    The endpoint url. Usually `ws://ip:9944` or `wss://ip:9944`
     * @param {boolean} autoConnect Whether to connect automatically or not.
     */
    constructor(endpoint = defaults_1.default.WS_URL, autoConnect = true) {
        this._isConnected = false;
        this.onSocketClose = (event) => {
            if (this.autoConnect) {
                l.error(`disconnected from ${this.endpoint} code: '${event.code}' reason: '${event.reason}'`);
            }
            this._isConnected = false;
            this.emit('disconnected');
            if (this.autoConnect) {
                setTimeout(() => {
                    this.connect();
                }, 1000);
            }
        };
        this.onSocketError = (error) => {
            l.debug(() => ['socket error', error]);
            this.emit('error', error);
        };
        this.onSocketMessage = (message) => {
            l.debug(() => ['received', message.data]);
            const response = JSON.parse(message.data);
            return util_1.isUndefined(response.method)
                ? this.onSocketMessageResult(response)
                : this.onSocketMessageSubscribe(response);
        };
        this.onSocketMessageResult = (response) => {
            const handler = this.handlers[response.id];
            if (!handler) {
                l.debug(() => `Unable to find handler for id=${response.id}`);
                return;
            }
            try {
                const { method, params, subscription } = handler;
                const result = this.coder.decodeResponse(response);
                // first send the result - in case of subs, we may have an update
                // immediately if we have some queued results already
                handler.callback(null, result);
                if (subscription) {
                    const subId = `${subscription.type}::${result}`;
                    this.subscriptions[subId] = Object.assign(Object.assign({}, subscription), { method,
                        params });
                    // if we have a result waiting for this subscription already
                    if (this.waitingForId[subId]) {
                        this.onSocketMessageSubscribe(this.waitingForId[subId]);
                    }
                }
            }
            catch (error) {
                handler.callback(error, undefined);
            }
            delete this.handlers[response.id];
        };
        this.onSocketMessageSubscribe = (response) => {
            const method = ALIASSES[response.method] || response.method;
            const subId = `${method}::${response.params.subscription}`;
            const handler = this.subscriptions[subId];
            if (!handler) {
                // store the JSON, we could have out-of-order subid coming in
                this.waitingForId[subId] = response;
                l.debug(() => `Unable to find handler for subscription=${subId}`);
                return;
            }
            // housekeeping
            delete this.waitingForId[subId];
            try {
                const result = this.coder.decodeResponse(response);
                handler.callback(null, result);
            }
            catch (error) {
                handler.callback(error, undefined);
            }
        };
        this.onSocketOpen = () => {
            util_1.assert(!util_1.isNull(this.websocket), 'WebSocket cannot be null in onOpen');
            l.debug(() => ['connected to', this.endpoint]);
            this._isConnected = true;
            this.emit('connected');
            this.sendQueue();
            this.resubscribe();
            return true;
        };
        util_1.assert(/^(wss|ws):\/\//.test(endpoint), `Endpoint should start with 'ws://', received '${endpoint}'`);
        this._eventemitter = new eventemitter3_1.default();
        this.autoConnect = autoConnect;
        this.coder = new coder_1.default();
        this.endpoint = endpoint;
        this.handlers = {};
        this.queued = {};
        this.subscriptions = {};
        this.waitingForId = {};
        this.websocket = null;
        if (autoConnect) {
            this.connect();
        }
    }
    /**
     * @summary `true` when this provider supports subscriptions
     */
    get hasSubscriptions() {
        return true;
    }
    /**
     * @description Returns a clone of the object
     */
    clone() {
        return new WsProvider(this.endpoint);
    }
    /**
     * @summary Manually connect
     * @description The [[WsProvider]] connects automatically by default, however if you decided otherwise, you may
     * connect manually using this method.
     */
    connect() {
        try {
            this.websocket = new WebSocket(this.endpoint);
            this.websocket.onclose = this.onSocketClose;
            this.websocket.onerror = this.onSocketError;
            this.websocket.onmessage = this.onSocketMessage;
            this.websocket.onopen = this.onSocketOpen;
        }
        catch (error) {
            l.error(error);
        }
    }
    /**
     * @description Manually disconnect from the connection, clearing autoconnect logic
     */
    disconnect() {
        if (util_1.isNull(this.websocket)) {
            throw new Error('Cannot disconnect on a non-open websocket');
        }
        // switch off autoConnect, we are in manual mode now
        this.autoConnect = false;
        // 1000 - Normal closure; the connection successfully completed
        this.websocket.close(1000);
        this.websocket = null;
    }
    /**
     * @summary Whether the node is connected or not.
     * @return {boolean} true if connected
     */
    isConnected() {
        return this._isConnected;
    }
    /**
     * @summary Listens on events after having subscribed using the [[subscribe]] function.
     * @param  {ProviderInterfaceEmitted} type Event
     * @param  {ProviderInterfaceEmitCb}  sub  Callback
     * @return unsubscribe function
     */
    on(type, sub) {
        this._eventemitter.on(type, sub);
        return () => {
            this._eventemitter.removeListener(type, sub);
        };
    }
    /**
     * @summary Send JSON data using WebSockets to configured HTTP Endpoint or queue.
     * @param method The RPC methods to execute
     * @param params Encoded paramaters as appliucable for the method
     * @param subscription Subscription details (internally used)
     */
    send(method, params, subscription) {
        return new Promise((resolve, reject) => {
            try {
                const json = this.coder.encodeJson(method, params);
                const id = this.coder.getId();
                const callback = (error, result) => {
                    error
                        ? reject(error)
                        : resolve(result);
                };
                l.debug(() => ['calling', method, json]);
                this.handlers[id] = {
                    callback,
                    method,
                    params,
                    subscription
                };
                if (this.isConnected() && !util_1.isNull(this.websocket)) {
                    this.websocket.send(json);
                }
                else {
                    this.queued[id] = json;
                }
            }
            catch (error) {
                reject(error);
            }
        });
    }
    /**
     * @name subscribe
     * @summary Allows subscribing to a specific event.
     * @param  {string}                     type     Subscription type
     * @param  {string}                     method   Subscription method
     * @param  {any[]}                 params   Parameters
     * @param  {ProviderInterfaceCallback} callback Callback
     * @return {Promise<number>}                     Promise resolving to the dd of the subscription you can use with [[unsubscribe]].
     *
     * @example
     * <BR>
     *
     * ```javascript
     * const provider = new WsProvider('ws://127.0.0.1:9944');
     * const rpc = new Rpc(provider);
     *
     * rpc.state.subscribeStorage([[storage.balances.freeBalance, <Address>]], (_, values) => {
     *   console.log(values)
     * }).then((subscriptionId) => {
     *   console.log('balance changes subscription id: ', subscriptionId)
     * })
     * ```
     */
    subscribe(type, method, params, callback) {
        return __awaiter(this, void 0, void 0, function* () {
            const id = yield this.send(method, params, { callback, type });
            return id;
        });
    }
    /**
     * @summary Allows unsubscribing to subscriptions made with [[subscribe]].
     */
    unsubscribe(type, method, id) {
        return __awaiter(this, void 0, void 0, function* () {
            const subscription = `${type}::${id}`;
            // FIXME This now could happen with re-subscriptions. The issue is that with a re-sub
            // the assigned id now does not match what the API user originally received. It has
            // a slight complication in solving - since we cannot rely on the send id, but rather
            // need to find the actual subscription id to map it
            if (util_1.isUndefined(this.subscriptions[subscription])) {
                l.debug(() => `Unable to find active subscription=${subscription}`);
                return false;
            }
            delete this.subscriptions[subscription];
            const result = yield this.send(method, [id]);
            return result;
        });
    }
    emit(type, ...args) {
        this._eventemitter.emit(type, ...args);
    }
    resubscribe() {
        const subscriptions = this.subscriptions;
        this.subscriptions = {};
        // eslint-disable-next-line @typescript-eslint/no-misused-promises
        Object.keys(subscriptions).forEach((id) => __awaiter(this, void 0, void 0, function* () {
            const { callback, method, params, type } = subscriptions[id];
            // only re-create subscriptions which are not in author (only area where
            // transactions are created, i.e. submissions such as 'author_submitAndWatchExtrinsic'
            // are not included (and will not be re-broadcast)
            if (type.startsWith('author_')) {
                return;
            }
            try {
                yield this.subscribe(type, method, params, callback);
            }
            catch (error) {
                l.error(error);
            }
        }));
    }
    sendQueue() {
        Object.keys(this.queued).forEach((id) => {
            try {
                // we have done the websocket check in onSocketOpen, if an issue, will catch it
                this.websocket.send(this.queued[id]);
                delete this.queued[id];
            }
            catch (error) {
                l.error(error);
            }
        });
    }
}
exports.default = WsProvider;
