var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
    if (kind === "m") throw new TypeError("Private method is not writable");
    if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
    if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
    return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
    if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
    if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
    return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var _WebSocketClient_pingInterval, _WebSocketClient_socketUrl, _WebSocketClient_reconnectCount, _WebSocketClient_reconnectTimeout, _WebSocketClient_resumeSignalHandler, _WebSocketClient_visibilityResumeHandler, _WebSocketClient_resumeListenersAttached;
/* eslint-disable max-classes-per-file */
import { nanoid } from 'nanoid';
import { Observable, Subject } from 'rxjs';
import { filter, map, take } from 'rxjs/operators';
import { SESSION_REFRESHED_EVENT } from './events';
const interAttemptTimeout = 500;
const maxDisconnectedTime = 10000;
const interPingInterval = 5000;
const maxAttempts = Math.round(maxDisconnectedTime / interAttemptTimeout);
export class TaskCancelledError extends Error {
    constructor(message, task_id) {
        super(message);
        this.task_id = task_id;
    }
}
export class TaskError extends Error {
    constructor(message, task_id) {
        super(message);
        this.task_id = task_id;
    }
}
export var TaskStatus;
(function (TaskStatus) {
    TaskStatus["CANCELED"] = "CANCELED";
    TaskStatus["COMPLETE"] = "COMPLETE";
    TaskStatus["ERROR"] = "ERROR";
    TaskStatus["PROGRESS"] = "PROGRESS";
})(TaskStatus || (TaskStatus = {}));
export function isInitMessage(message) {
    return message.type === 'init';
}
export function isTaskNotification(message) {
    return message.type === 'message' && 'status' in message.message && 'task_id' in message.message;
}
export function isServerVariableMessage(message) {
    return message.type === 'message' && '__type' in message.message && message.message.__type === 'ServerVariable';
}
export function isServerErrorMessage(message) {
    return message.type === 'message' && 'error' in message.message;
}
export function isVariableRequestMessage(message) {
    return message.type === 'message' && 'variable' in message.message;
}
export function isActionMessage(message) {
    return message.type === 'message' && 'action' in message.message;
}
export function isBackendStoreMessage(message) {
    return message.type === 'message' && 'store_uid' in message.message && 'value' in message.message;
}
export function isBackendStorePatchMessage(message) {
    return message.type === 'message' && 'store_uid' in message.message && 'patches' in message.message;
}
export function isCustomMessage(message) {
    return message.type === 'custom';
}
const pingMessage = {
    message: null,
    type: 'ping',
};
/**
 * The WebsocketClient class exposes an interface for easily dealing with the websocket connection to the Dara backend.
 * It provides easy helpers for fetching the current channel and working with tasks.
 */
export class WebSocketClient {
    constructor(_socketUrl, _liveReload = false) {
        _WebSocketClient_pingInterval.set(this, void 0);
        _WebSocketClient_socketUrl.set(this, void 0);
        _WebSocketClient_reconnectCount.set(this, void 0);
        _WebSocketClient_reconnectTimeout.set(this, void 0);
        _WebSocketClient_resumeSignalHandler.set(this, void 0);
        _WebSocketClient_visibilityResumeHandler.set(this, void 0);
        _WebSocketClient_resumeListenersAttached.set(this, void 0);
        this.liveReload = _liveReload;
        this.messages$ = new Subject();
        this.closeHandler = this.onClose.bind(this);
        this.maxAttempts = maxAttempts;
        this.maxAttemptsReached = false;
        __classPrivateFieldSet(this, _WebSocketClient_socketUrl, _socketUrl, "f");
        __classPrivateFieldSet(this, _WebSocketClient_reconnectCount, 0, "f");
        __classPrivateFieldSet(this, _WebSocketClient_pingInterval, null, "f");
        __classPrivateFieldSet(this, _WebSocketClient_reconnectTimeout, null, "f");
        __classPrivateFieldSet(this, _WebSocketClient_resumeSignalHandler, this.onResumeSignal.bind(this), "f");
        __classPrivateFieldSet(this, _WebSocketClient_visibilityResumeHandler, this.onVisibilityResumeSignal.bind(this), "f");
        __classPrivateFieldSet(this, _WebSocketClient_resumeListenersAttached, false, "f");
        // Satisfy TSC, channel is set within initialize again
        this.channel = Promise.resolve('');
        // Lastly call initialize to setup the socket properly
        this.socket = this.initialize();
    }
    initialize(isReconnect = false) {
        // Create the underlying socket instance from the url.
        const socket = new WebSocket(__classPrivateFieldGet(this, _WebSocketClient_socketUrl, "f"));
        // Send heartbeat to ping every few seconds and clear it on error
        __classPrivateFieldSet(this, _WebSocketClient_pingInterval, setInterval(() => {
            if (socket.readyState === WebSocket.OPEN) {
                socket.send(JSON.stringify(pingMessage));
            }
        }, interPingInterval), "f");
        socket.addEventListener('error', () => {
            if (__classPrivateFieldGet(this, _WebSocketClient_pingInterval, "f")) {
                clearInterval(__classPrivateFieldGet(this, _WebSocketClient_pingInterval, "f"));
            }
        });
        // Register the message event listener to start the stream of messages and get the new channel
        socket.addEventListener('message', (ev) => {
            const msg = JSON.parse(ev.data);
            this.messages$.next(msg);
        });
        // Update the channel on the class and broadcast the init message to subscribers
        this.channel = new Promise((resolve) => {
            const handler = (ev) => {
                var _a;
                const msg = JSON.parse(ev.data);
                if (msg.type === 'init') {
                    __classPrivateFieldSet(this, _WebSocketClient_reconnectCount, 0, "f");
                    this.maxAttemptsReached = false;
                    this.removeReconnectResumeListeners();
                    this.messages$.next(msg);
                    // Remove the handler after the channel is received and then resolve the promise
                    socket.removeEventListener('message', handler);
                    resolve((_a = msg.message) === null || _a === void 0 ? void 0 : _a.channel);
                    // If liveReload is true and this is a reconnect attempt then reload the page
                    if (this.liveReload && isReconnect) {
                        window.location.reload();
                    }
                }
            };
            socket.addEventListener('message', handler);
        });
        // Bind the close handler so the re-initialize logic is added every time
        socket.addEventListener('close', this.closeHandler);
        return socket;
    }
    addReconnectResumeListeners() {
        if (__classPrivateFieldGet(this, _WebSocketClient_resumeListenersAttached, "f")) {
            return;
        }
        document.addEventListener('visibilitychange', __classPrivateFieldGet(this, _WebSocketClient_visibilityResumeHandler, "f"));
        window.addEventListener('focus', __classPrivateFieldGet(this, _WebSocketClient_resumeSignalHandler, "f"));
        window.addEventListener('online', __classPrivateFieldGet(this, _WebSocketClient_resumeSignalHandler, "f"));
        window.addEventListener(SESSION_REFRESHED_EVENT, __classPrivateFieldGet(this, _WebSocketClient_resumeSignalHandler, "f"));
        __classPrivateFieldSet(this, _WebSocketClient_resumeListenersAttached, true, "f");
    }
    removeReconnectResumeListeners() {
        if (!__classPrivateFieldGet(this, _WebSocketClient_resumeListenersAttached, "f")) {
            return;
        }
        document.removeEventListener('visibilitychange', __classPrivateFieldGet(this, _WebSocketClient_visibilityResumeHandler, "f"));
        window.removeEventListener('focus', __classPrivateFieldGet(this, _WebSocketClient_resumeSignalHandler, "f"));
        window.removeEventListener('online', __classPrivateFieldGet(this, _WebSocketClient_resumeSignalHandler, "f"));
        window.removeEventListener(SESSION_REFRESHED_EVENT, __classPrivateFieldGet(this, _WebSocketClient_resumeSignalHandler, "f"));
        __classPrivateFieldSet(this, _WebSocketClient_resumeListenersAttached, false, "f");
    }
    resumeReconnectBurst() {
        if (!this.maxAttemptsReached) {
            return;
        }
        __classPrivateFieldSet(this, _WebSocketClient_reconnectCount, 0, "f");
        this.maxAttemptsReached = false;
        this.removeReconnectResumeListeners();
        this.socket = this.initialize(true);
    }
    onResumeSignal() {
        this.resumeReconnectBurst();
    }
    onVisibilityResumeSignal() {
        if (document.visibilityState === 'visible') {
            this.resumeReconnectBurst();
        }
    }
    /**
     * Close handler to attempt to reconnect on WS closed
     */
    onClose() {
        if (__classPrivateFieldGet(this, _WebSocketClient_reconnectCount, "f") >= this.maxAttempts) {
            // eslint-disable-next-line no-console
            console.error('Could not reconnect the websocket to the server');
            this.maxAttemptsReached = true;
            this.addReconnectResumeListeners();
            return;
        }
        __classPrivateFieldSet(this, _WebSocketClient_reconnectTimeout, setTimeout(() => {
            var _a;
            __classPrivateFieldSet(this, _WebSocketClient_reconnectTimeout, null, "f");
            __classPrivateFieldSet(this, _WebSocketClient_reconnectCount, (_a = __classPrivateFieldGet(this, _WebSocketClient_reconnectCount, "f"), _a++, _a), "f");
            this.socket = this.initialize(true);
        }, interAttemptTimeout), "f");
    }
    /**
     * Forcefully close the websocket connection, first clearing the closehandler
     */
    close() {
        if (__classPrivateFieldGet(this, _WebSocketClient_pingInterval, "f")) {
            clearInterval(__classPrivateFieldGet(this, _WebSocketClient_pingInterval, "f"));
        }
        if (__classPrivateFieldGet(this, _WebSocketClient_reconnectTimeout, "f")) {
            clearTimeout(__classPrivateFieldGet(this, _WebSocketClient_reconnectTimeout, "f"));
            __classPrivateFieldSet(this, _WebSocketClient_reconnectTimeout, null, "f");
        }
        this.removeReconnectResumeListeners();
        this.socket.removeEventListener('close', this.closeHandler);
        this.socket.close();
    }
    /**
     * Get the channel setup for this websocket client instance
     */
    getChannel() {
        return this.channel;
    }
    backendStoreMessages$() {
        return this.messages$.pipe(filter(isBackendStoreMessage), map((msg) => msg.message));
    }
    backendStorePatchMessages$() {
        return this.messages$.pipe(filter(isBackendStorePatchMessage), map((msg) => msg.message));
    }
    serverVariableMessages$() {
        return this.messages$.pipe(filter(isServerVariableMessage), map((msg) => msg.message));
    }
    /**
     * Get the observable to receive the new channel when the socket reconnects
     */
    channel$() {
        return this.messages$.pipe(filter((msg) => isInitMessage(msg)), map((msg) => msg.message.channel));
    }
    /**
     * Get the observable to receive status updates for given tasks
     *
     * @param task_ids the ids of tasks to receive updates from
     */
    taskStatusUpdates$(...task_ids) {
        return this.messages$.pipe(filter((msg) => isTaskNotification(msg) && task_ids.includes(msg.message.task_id)), map((msg) => msg.message.status));
    }
    /**
     * Get the observable to receive progress updates for given tasks
     *
     * @param task_ids the ids of the task to receive updates from
     */
    progressUpdates$(...task_ids) {
        return this.messages$.pipe(filter((msg) => isTaskNotification(msg) &&
            msg.message.status === TaskStatus.PROGRESS &&
            task_ids.includes(msg.message.task_id)));
    }
    /**
     * Get the observable to receive server error messages
     */
    serverErrors$() {
        return this.messages$.pipe(filter(isServerErrorMessage));
    }
    /**
     * Get the observable to receive variable request messages
     */
    variableRequests$() {
        return this.messages$.pipe(filter(isVariableRequestMessage));
    }
    /**
     * Get the observable to receive action implementations to execute for a given execution id
     *
     * @param executionId id of the execution to receive action implementations for
     */
    actionMessages$(executionId) {
        return this.messages$.pipe(filter((msg) => isActionMessage(msg) && msg.message.uid === executionId), map((msg) => msg.message.action));
    }
    /**
     * Get the observable to receive custom messages
     */
    customMessages$() {
        return this.messages$.pipe(filter(isCustomMessage));
    }
    /**
     * Returns a promise that will resolve when the task is completed. If the task is cancelled or errored then this will throw an
     * error to signify that.
     *
     * @param task_id the id of the task to wait for
     */
    waitForTask(task_id) {
        return this.messages$
            .pipe(filter((msg) => {
            var _a;
            return (isTaskNotification(msg) &&
                ((_a = msg.message) === null || _a === void 0 ? void 0 : _a.task_id) === task_id &&
                msg.message.status !== TaskStatus.PROGRESS); // don't take progress updates
        }), map((msg) => {
            if (msg.message.status === TaskStatus.CANCELED) {
                throw new TaskCancelledError('Task was cancelled', msg.message.task_id);
            }
            else if (msg.message.status === TaskStatus.ERROR) {
                throw new TaskError(msg.message.error, msg.message.task_id);
            }
            return msg;
        }), take(1))
            .toPromise();
    }
    /**
     * Send variable value to the backend
     *
     * @param value variable value to send
     * @param channel return channel to identify the message
     */
    sendVariable(value, channel) {
        this.sendMessage(value, channel);
    }
    /**
     * Send an internal message to the backend. This is used to respond to sendAndWait calls from the backend.
     *
     * @param value variable value to send
     * @param channel return channel to identify the message
     * @param chunkCount total number of chunks this message has been split into
     */
    sendMessage(value, channel, chunkCount) {
        if (this.socket.readyState === WebSocket.OPEN) {
            this.socket.send(JSON.stringify({
                channel,
                chunk_count: chunkCount !== null && chunkCount !== void 0 ? chunkCount : null,
                message: value,
                type: 'message',
            }));
        }
    }
    /**
     * Send custom message to the backend
     *
     * @param kind kind of custom message
     * @param data custom message data
     * @param awaitResponse whether to await a response for this message
     */
    sendCustomMessage(kind, data, awaitResponse = false) {
        if (this.socket.readyState === WebSocket.OPEN) {
            // if awaiting response, setup a subscription to the response channel
            if (awaitResponse) {
                const rchan = nanoid();
                return new Promise((resolve) => {
                    const subscription = this.customMessages$()
                        .pipe()
                        .subscribe({
                        next: (msg) => {
                            if (msg.message.__response_for === rchan) {
                                resolve(msg);
                                subscription.unsubscribe();
                            }
                        },
                    });
                    this.socket.send(JSON.stringify({
                        message: {
                            data,
                            kind,
                            __rchan: rchan,
                        },
                        type: 'custom',
                    }));
                });
            }
            // otherwise just fire and forget a message
            this.socket.send(JSON.stringify({
                message: {
                    data,
                    kind,
                },
                type: 'custom',
            }));
            return Promise.resolve(null);
        }
        return Promise.resolve(null);
    }
}
_WebSocketClient_pingInterval = new WeakMap(), _WebSocketClient_socketUrl = new WeakMap(), _WebSocketClient_reconnectCount = new WeakMap(), _WebSocketClient_reconnectTimeout = new WeakMap(), _WebSocketClient_resumeSignalHandler = new WeakMap(), _WebSocketClient_visibilityResumeHandler = new WeakMap(), _WebSocketClient_resumeListenersAttached = new WeakMap();
/**
 * Set up websocket connection and handlers.
 *
 * @param liveReload whether to enable live reload
 */
export function setupWebsocket(liveReload) {
    var _a;
    // Setup socket url
    let { host } = window.location;
    if ((_a = window.dara) === null || _a === void 0 ? void 0 : _a.base_url) {
        const baseUrl = new URL(window.dara.base_url, window.origin);
        let { pathname } = baseUrl;
        if (pathname.endsWith('/')) {
            pathname = pathname.slice(0, -1);
        }
        host = baseUrl.host + pathname;
    }
    const socketUrl = `${window.location.protocol === 'https:' ? 'wss://' : 'ws://'}${host}/api/core/ws`;
    return new WebSocketClient(socketUrl, liveReload);
}
//# sourceMappingURL=websocket.js.map