"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SyncEngine = void 0;
const events_1 = require("events");
const syncMessages_1 = require("./syncMessages");
const Validate_1 = __importDefault(require("../Validate/Validate"));
/**
 * The `SyncEngine` enum defines the available synchronization engines that can be used to synchronize devices.
 * Each engine has its own method of connecting devices and synchronizing data.
 */
var SyncEngine;
(function (SyncEngine) {
    /** Use external sync server. Device will connect to the server via websocket. */
    SyncEngine["SyncServer"] = "sync-server";
    /** Synchronize directly with other devices in the local network via UDP and TCP. */
    SyncEngine["P2PLocal"] = "p2p-local";
    /**
     * Synchronize directly with other devices in the local network via UDP.
     * @deprecated use {@link P2PLocal}
     */
    SyncEngine["Udp"] = "udp";
})(SyncEngine || (exports.SyncEngine = SyncEngine = {}));
var SyncEvent;
(function (SyncEvent) {
    SyncEvent["Status"] = "status";
    SyncEvent["BroadcastedValue"] = "broadcasted_value";
    SyncEvent["Closed"] = "closed";
})(SyncEvent || (SyncEvent = {}));
/**
 * The `sos.sync` API groups together methods for synchronization of multiple devices. Devices are synchronized either through an external
 * server or one of the devices becomes a master device.
 */
class Sync {
    messagePrefix;
    postMessage;
    static MESSAGE_PREFIX = 'sync';
    static DEFAULT_GROUP_NAME = '____default____';
    eventEmitter;
    /** @internal */
    constructor(messagePrefix, postMessage) {
        this.messagePrefix = messagePrefix;
        this.postMessage = postMessage;
        this.eventEmitter = new events_1.EventEmitter();
    }
    /** @internal */
    async connect(options) {
        // TODO switch default to UDP once it's mature enough
        const defaultOptions = { engine: SyncEngine.SyncServer };
        let sanitizedOptions;
        if (typeof options === 'string') {
            // support legacy connect(uri) method, which uses the sync-server engine
            sanitizedOptions = {
                engine: SyncEngine.SyncServer,
                uri: options,
            };
        }
        else if (options === undefined) {
            sanitizedOptions = defaultOptions;
        }
        else {
            sanitizedOptions = options;
        }
        if (sanitizedOptions.engine === SyncEngine.SyncServer) {
            (0, Validate_1.default)({ uri: sanitizedOptions.uri }).uri();
        }
        await this.postMessage({
            type: this.getMessage('connect'),
            engine: sanitizedOptions.engine,
            serverUri: sanitizedOptions.uri,
        });
    }
    /**
     * The `close()` method disconnects the device from synchronization server and other devices. Recommended to call this method after the
     * synchronization is not required any longer.
     *
     * @returns {Promise<void>} A promise that resolves when the connection is closed.
     * @throws Error If unable to close the connection.
     * @since 1.0.32
     */
    async close() {
        await this.postMessage({
            type: this.getMessage('close'),
        });
    }
    /**
     * @deprecated use `sos.sync.joinGroup()` instead.
     * @since 1.0.32
     */
    async init(groupName = Sync.DEFAULT_GROUP_NAME, deviceIdentification) {
        await this.joinGroup({ groupName, deviceIdentification });
    }
    /**
     * The `joinGroup()` method joins a group of other devices. This method has to be called after `connect()` call. Before any communication
     * takes place, all participating devices have to be connected and recognize one another. Recommended to call this method early.
     *
     * @param options Options for joining a group.
     * @returns {Promise<void>} A promise that resolves when the device joins the group.
     * @throws Error If the group name is not a string.
     * @throws Error If the device identification is not a string.
     * @throws Error If unable to join the group.
     * @throws Error If any other error occurs during joining the group.
     * @since 5.7.0
     *
     * @example // {@link https://github.com/signageos/applet-examples/tree/master/examples/content-js-api/sync-video | How to use synchronizer in applet}
     * @example
     * await sos.sync.connect();
     * await sos.sync.joinGroup({ groupName: 'my-group', deviceIdentification: 'my-device-id' });
     */
    async joinGroup(options) {
        const { groupName = Sync.DEFAULT_GROUP_NAME, deviceIdentification } = options;
        (0, Validate_1.default)({ groupName }).required().string();
        (0, Validate_1.default)({ deviceIdentification }).string();
        await this.postMessage({
            type: this.getMessage('init'),
            groupName,
            deviceIdentification,
        });
    }
    /**
     * The `leaveMethod` method leaves a group of devices.
     *
     * @param groupName The name of the group to leave. Defaults to the default group.
     * @returns {Promise<void>} A promise that resolves when the device leaves the group.
     * @throws Error If the group name is not a string.
     * @throws Error If unable to leave the group.
     * @throws Error If any other error occurs during leaving the group.
     * @since 5.7.0
     *
     * @example
     * await sos.sync.leaveGroup('my-group');
     */
    async leaveGroup(groupName = Sync.DEFAULT_GROUP_NAME) {
        (0, Validate_1.default)({ groupName }).required().string();
        await this.postMessage({
            type: this.getMessage('leave_group'),
            groupName,
        });
    }
    /**
     * The `wait()` method synchronizes with other devices by waiting for other devices before proceeding.
     *
     * One way to synchronize devices is to make them wait for each other at a certain moment. This would be most commonly used before the
     * device hits “play” on a video, to make it wait for other devices so they all start playing the video at the same time.
     *
     * This method returns a promise that resolves once all the devices meet and are ready to continue together. Any action that results in
     * visible synchronized behavior should be triggered immediately after and any related background preparations should be called before to
     * prevent delays.
     *
     * Sometimes devices might go out of sync due to unpredictable conditions like loss of internet connection. To ensure re-sync of an out of
     * sync device, you can pass some data as the first argument. This can be any data that informs the whole group about what content is
     * about to play next. Once all devices are ready, data from the master device is passed to everyone and the rest of the data is ignored.
     * Therefore, when implementing your applet you should rely on the result data and not the data that is passed to the wait method as an
     * argument.
     *
     * @param data Optional data to pass to the group.
     * @param groupName The name of the group to wait for. Defaults to the default group.
     * @param timeout Timeout in milliseconds. If not specified, the default timeout is 30 seconds.
     * @returns {Promise<any>} A promise that resolves with the data from the master device when all devices are ready to continue.
     * @throws Error If the group name is not a string.
     * @throws Error If the timeout is not a number.
     * @throws Error If unable to wait for the group.
     * @throws Error If any other error occurs during waiting for the group.
     * @since 1.0.32
     *
     * @example
     * // wait for all devices in the group to be ready
     * await sos.sync.wait('someData', 'someRandomNameGroup').then((data) => {
     *     // this will be called once all devices are ready
     *     console.log('All devices are ready with data:', data);
     * });
     */
    async wait(data, groupName = Sync.DEFAULT_GROUP_NAME, timeout) {
        (0, Validate_1.default)({ groupName }).required().string();
        (0, Validate_1.default)({ timeout }).number();
        const response = await this.postMessage({
            type: this.getMessage('wait'),
            groupName,
            data,
            timeout,
        });
        return response.responseData;
    }
    /**
     * The `cancelWait()` method aborts a wait on all devices in a group. Sometimes it's necessary to cancel a pending wait. One such
     * situation would be when the group has to make a sudden change in content or another behavior but there's a risk that part of the group
     * already called wait() and is waiting for the rest but the rest will never call it at this point. In order to gracefully clean up any
     * pending activity, use this method.
     *
     * :::warning
     * Any pending wait will be canceled and the promise will be rejected with an error.
     * :::
     *
     * @param groupName The name of the group to cancel the wait for. Defaults to the default group.
     * @returns {Promise<void>} A promise that resolves when the wait is canceled.
     * @throws Error If the group name is not a string.
     * @throws Error If unable to cancel the wait.
     * @throws Error If any other error occurs during canceling the wait.
     * @since 5.12.0
     *
     * @example
     * sos.sync.wait('someData', 'someRandomNameGroup').catch((err) => {
     *     // this will happen once cancelWait is called
     *     console.error('wait failed', err);
     * });
     *
     * // this will cause above wait promise to reject
     * await sos.sync.cancelWait('someRandomNameGroup');
     */
    async cancelWait(groupName = Sync.DEFAULT_GROUP_NAME) {
        (0, Validate_1.default)({ groupName }).required().string();
        await this.postMessage({
            type: this.getMessage('cancel_wait'),
            groupName,
        });
    }
    /**
     * @deprecated use `sos.sync.broadcastValue()` instead.
     * @since 2.0.0
     */
    async setValue(key, value, groupName = Sync.DEFAULT_GROUP_NAME) {
        await this.broadcastValue({ key, value, groupName });
    }
    /**
     * The `broadcastValue()` method sends a key-value pair to all other devices in a specified group.
     *
     * @param groupName {String} The name of the group to broadcast the value to. Defaults to the default group.
     * @param key {String} The key to broadcast the value under.
     * @param value {any} The value to broadcast. It can be any valid type
     * @returns {Promise<void>} A promise that resolves when the value is broadcasted.
     * @throws Error If the group name is not a string.
     * @throws Error If the key is not a string.
     * @throws Error If the value is not a valid type (e.g. object, array, etc.).
     * @throws Error If unable to broadcast the value.
     * @throws Error If the value can't be sent
     * @since 5.7.0
     *
     * @example
     * // broadcast a value to all devices in the group
     * await sos.sync.broadcastValue({
     * 	groupName: 'my-group',
     * 	key: 'my-key',
     * 	value: 'my-value',
     * });
     *
     * // Received on the other devices:
     * sos.sync.onValue((key, value, groupName) => {
     * 	console.log(`Received value for key ${key} in group ${groupName}:`, value);
     * });
     *
     */
    async broadcastValue({ groupName, key, value }) {
        groupName = groupName ?? Sync.DEFAULT_GROUP_NAME;
        (0, Validate_1.default)({ key }).required().string();
        (0, Validate_1.default)({ groupName }).required().string();
        await this.postMessage({
            type: this.getMessage('request_set_value'),
            groupName,
            key,
            value,
        });
    }
    /**
     * Returns true if the device is currently the master of the group.
     *
     * @param groupName The group name to check for master status. Defaults to the default group.
     * @return {Promise<boolean>} A promise that resolves with true if the device is the master, false otherwise.
     * @since 6.7.0
     */
    async isMaster(groupName) {
        const { isMaster } = await this.postMessage({
            type: this.getMessage('is_master'),
            groupName: groupName ?? Sync.DEFAULT_GROUP_NAME,
        });
        return isMaster;
    }
    /**
     * The `onValue()` method sets up a listener, which is called whenever the device receives a broadcasted message.
     *
     * @param listener The listener function to call when a value is broadcasted.
     * @returns {void} Returns when listener is set up.
     * @throws Error If the listener is not a function.
     * @since 2.0.0
     */
    onValue(listener) {
        (0, Validate_1.default)({ listener }).required().function();
        this.eventEmitter.addListener(SyncEvent.BroadcastedValue, (event) => {
            listener(event.key, event.value, event.groupName === Sync.DEFAULT_GROUP_NAME ? undefined : event.groupName);
        });
    }
    /**
     * The `onStatus()` method sets up a listener, which is called periodically or whenever there is a change (i.e. new device
     * connects/disconnects to/from the group).
     *
     * @param listener The listener function to call when the status changes.
     * @returns {void} Returns when listener is set up.
     * @throws Error If the listener is not a function.
     * @since 2.1.0
     */
    onStatus(listener) {
        (0, Validate_1.default)({ listener }).required().function();
        this.eventEmitter.addListener(SyncEvent.Status, (event) => {
            listener({
                connectedPeers: event.connectedPeers,
                groupName: event.groupName === Sync.DEFAULT_GROUP_NAME ? undefined : event.groupName,
                isMaster: event.isMaster,
            });
        });
    }
    /**
     * The `onClosed()` method sets up a listener, which is called whenever the device is disconnected from the sync. If it closed because
     * `close()` was called, it will emit without any arguments. if it closed because of an error, it will emit with an error object as the
     * first argument.
     *
     * @param listener The listener function to call when the sync is closed.
     * @returns {void} Returns when listener is set up.
     * @throws Error If the listener is not a function.
     * @since 5.12.0
     */
    onClosed(listener) {
        (0, Validate_1.default)({ listener }).required().function();
        this.eventEmitter.addListener(SyncEvent.Closed, listener);
    }
    /**
     * The `removeEventListeners()` method removes all listeners set up on `sos.sync` object.
     *
     * @since 2.0.0
     */
    removeEventListeners() {
        this.eventEmitter.removeAllListeners();
    }
    /** @internal */
    handleMessageData(data) {
        switch (data.type) {
            case this.getMessage(syncMessages_1.SyncMessageType.BroadcastedValue): {
                const broadcastedValueMessage = data;
                const valueEvent = {
                    groupName: broadcastedValueMessage.groupName,
                    key: broadcastedValueMessage.key,
                    value: broadcastedValueMessage.value,
                };
                this.eventEmitter.emit(SyncEvent.BroadcastedValue, valueEvent);
                break;
            }
            case this.getMessage(syncMessages_1.SyncMessageType.Status): {
                const statusMessage = data;
                const statusEvent = {
                    connectedPeers: statusMessage.connectedPeers,
                    groupName: statusMessage.groupName,
                    isMaster: statusMessage.isMaster ?? false, // backwards compatibility with older core apps that don't send isMaster
                };
                this.eventEmitter.emit(SyncEvent.Status, statusEvent);
                break;
            }
            case this.getMessage(syncMessages_1.SyncMessageType.Closed): {
                const closedMessage = data;
                const closedEvent = closedMessage.error ? new Error(closedMessage.error) : undefined;
                this.eventEmitter.emit(SyncEvent.Closed, closedEvent);
                break;
            }
            default:
        }
    }
    getMessage(name) {
        return this.messagePrefix + '.' + Sync.MESSAGE_PREFIX + '.' + name;
    }
}
exports.default = Sync;
//# sourceMappingURL=Sync.js.map