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 __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 __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 _MessageBroker_gameId, _MessageBroker_userId, _MessageBroker_source, _MessageBroker_connectUrl, _MessageBroker_getMessagesUrl, _MessageBroker_evt, _MessageBroker_queue, _MessageBroker_ws, _MessageBroker_getShortTermToken, _MessageBroker_isAwaitingStt, _MessageBroker_isAwaitingMessages, _MessageBroker_stt, _MessageBroker_pingId, _MessageBroker_oldest, _MessageBroker_newest, _MessageBroker_posted, _MessageBroker_hasMoreMessages;
import { Evt } from 'evt';
import { v4 as uuid } from 'uuid';
import { byDateTime, href, isClosed, isClosing, isConnecting, isOpen, isValidGameId, toKey, ws, } from './helpers';
const MAX_HISTORY_CATCHUP_PAGES = 10;
const FIVE_MINUTES = 1000 * // milliseconds
    60 * // seconds
    5; // minutes
export var MessageBrokerStatus;
(function (MessageBrokerStatus) {
    MessageBrokerStatus["Disconnected"] = "disconnected";
    MessageBrokerStatus["Connecting"] = "connecting";
    MessageBrokerStatus["Open"] = "open";
    MessageBrokerStatus["Closing"] = "closing";
    MessageBrokerStatus["Closed"] = "closed";
})(MessageBrokerStatus || (MessageBrokerStatus = {}));
const MESSAGE_BROKER_RESET_REASON = '@dndbeyond/message-broker-lib#reset';
/**
 * this class makes use of the (at time of writing) stage 3 class fields
 * proposal, specifically private fields to make internal state truly invisible
 * to consumers
 *
 * @see https://github.com/tc39/proposal-class-fields#private-fields
 */
export class MessageBroker {
    get gameId() {
        return __classPrivateFieldGet(this, _MessageBroker_gameId, "f");
    }
    set gameId(nextGameId) {
        if (__classPrivateFieldGet(this, _MessageBroker_gameId, "f") === nextGameId) {
            return;
        }
        __classPrivateFieldSet(this, _MessageBroker_gameId, nextGameId, "f");
        this.reset();
        __classPrivateFieldGet(this, _MessageBroker_evt, "f").post(this.composeMessage({
            eventType: 'replace/messageBroker/gameId',
        }));
        if (isValidGameId(__classPrivateFieldGet(this, _MessageBroker_gameId, "f")) && __classPrivateFieldGet(this, _MessageBroker_evt, "f").getHandlers().length) {
            // if the new game id is valid and we've already got some subscribers
            // listening, we should attempt to reconnect immediately
            this.connect();
        }
    }
    get userId() {
        return __classPrivateFieldGet(this, _MessageBroker_userId, "f");
    }
    set userId(nextUserId) {
        if (__classPrivateFieldGet(this, _MessageBroker_userId, "f") === nextUserId) {
            return;
        }
        __classPrivateFieldSet(this, _MessageBroker_userId, nextUserId, "f");
        this.reset();
    }
    get source() {
        return __classPrivateFieldGet(this, _MessageBroker_source, "f");
    }
    get connectUrl() {
        return __classPrivateFieldGet(this, _MessageBroker_connectUrl, "f");
    }
    get getMessagesUrl() {
        return __classPrivateFieldGet(this, _MessageBroker_getMessagesUrl, "f");
    }
    get status() {
        if (__classPrivateFieldGet(this, _MessageBroker_ws, "f") === undefined) {
            return MessageBrokerStatus.Disconnected;
        }
        if (isConnecting(__classPrivateFieldGet(this, _MessageBroker_ws, "f"))) {
            return MessageBrokerStatus.Connecting;
        }
        if (isOpen(__classPrivateFieldGet(this, _MessageBroker_ws, "f"))) {
            return MessageBrokerStatus.Open;
        }
        if (isClosing(__classPrivateFieldGet(this, _MessageBroker_ws, "f"))) {
            return MessageBrokerStatus.Closing;
        }
        if (isClosed(__classPrivateFieldGet(this, _MessageBroker_ws, "f"))) {
            return MessageBrokerStatus.Closed;
        }
        /**
         * @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState
         */
        throw new Error(`Unexpected web socket \`readyState\` (${__classPrivateFieldGet(this, _MessageBroker_ws, "f")}) exists but is not 'connecting', 'open', 'closing', or 'closed'`);
    }
    constructor({ gameId, userId, source, connectUrl, getMessagesUrl, getShortTermToken, }) {
        _MessageBroker_gameId.set(this, void 0);
        _MessageBroker_userId.set(this, void 0);
        _MessageBroker_source.set(this, void 0);
        _MessageBroker_connectUrl.set(this, void 0);
        _MessageBroker_getMessagesUrl.set(this, void 0);
        /**
         * we use an Evt ("EventEmitter's typesafe replacement") instance to stream
         * messages to subscribers
         *
         * @see https://docs.evt.land/
         */
        _MessageBroker_evt.set(this, Evt.create());
        _MessageBroker_queue.set(this, []);
        _MessageBroker_ws.set(this, void 0);
        _MessageBroker_getShortTermToken.set(this, void 0);
        _MessageBroker_isAwaitingStt.set(this, false);
        _MessageBroker_isAwaitingMessages.set(this, false);
        _MessageBroker_stt.set(this, '');
        _MessageBroker_pingId.set(this, -1);
        /**
         * `#oldest` and `#newest` are strings in the key format used by the lambdas,
         * that is `"<message.dateTime>-<message.eventType>-<message.userId>"`
         *
         * @see @dndbeyond/game-log-lambdas/src/interfaces/MessageLastEvaluatedKey.ts
         * @see ./helpers.ts#toKey
         */
        _MessageBroker_oldest.set(this, void 0);
        _MessageBroker_newest.set(this, void 0);
        _MessageBroker_posted.set(this, new Set());
        _MessageBroker_hasMoreMessages.set(this, true);
        __classPrivateFieldSet(this, _MessageBroker_gameId, gameId, "f");
        __classPrivateFieldSet(this, _MessageBroker_userId, userId, "f");
        __classPrivateFieldSet(this, _MessageBroker_source, source, "f");
        __classPrivateFieldSet(this, _MessageBroker_connectUrl, connectUrl, "f");
        __classPrivateFieldSet(this, _MessageBroker_getMessagesUrl, getMessagesUrl, "f");
        __classPrivateFieldSet(this, _MessageBroker_getShortTermToken, getShortTermToken, "f");
        Evt.setDefaultMaxHandlers(50);
        Evt.from(window, 'online').attach(() => this.connect());
        Evt.from(window, 'offline').attach(() => this.reset());
        Evt.from(window, 'beforeunload').attach(() => this.reset());
        Evt.from(window, 'focus').attach(() => this.handleFocusReconnect());
        Evt.from(document, 'visibilitychange').attach(() => this.handleVisibilityReconnect());
        if (process.env.NODE_ENV === 'test') {
            Object.defineProperties(this, {
                ws: { get: () => __classPrivateFieldGet(this, _MessageBroker_ws, "f") },
                queue: { get: () => __classPrivateFieldGet(this, _MessageBroker_queue, "f") },
            });
        }
    }
    subscribe(callback) {
        const ctx = Evt.newCtx();
        __classPrivateFieldGet(this, _MessageBroker_evt, "f").attach(ctx, callback);
        this.connect();
        /**
         * Detach, from the `Evt` instances they are attached to, all Handlers bound
         * to the context.
         * @see https://docs.evt.land/api/ctx#ctx-done-result
         */
        return () => ctx.done();
    }
    dispatch(partial) {
        const serializedMessage = JSON.stringify(this.composeMessage(partial));
        // post immediately to the Evt instance (message stream) for "optimistic
        // updates" ... alerts local subscribers right away
        this.post(JSON.parse(serializedMessage));
        if (__classPrivateFieldGet(this, _MessageBroker_gameId, "f")) {
            // queue the message for sending over the socket and try to connect, the
            // queue is flushed when the socket opens, or in the last condition of the
            // connect method
            __classPrivateFieldGet(this, _MessageBroker_queue, "f").push(serializedMessage);
            this.connect();
        }
    }
    fetchHistory() {
        return __awaiter(this, void 0, void 0, function* () {
            if (!__classPrivateFieldGet(this, _MessageBroker_gameId, "f") || __classPrivateFieldGet(this, _MessageBroker_isAwaitingStt, "f") || __classPrivateFieldGet(this, _MessageBroker_isAwaitingMessages, "f")) {
                // if we're already fetching an `stt` successive calls to `fetchHistory`
                // should just bail early
                return;
            }
            yield this.validateStt();
            this.getMessages(__classPrivateFieldGet(this, _MessageBroker_oldest, "f")).then((messages) => messages.forEach((message) => this.post(message)));
        });
    }
    composeMessage(partial) {
        return Object.assign({ id: uuid(), 
            /**
             * The static Date.now() method returns the number of milliseconds elapsed
             * since January 1, 1970 00:00:00 UTC.
             *
             * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now
             */
            dateTime: `${Date.now()}`, gameId: this.gameId, userId: this.userId, source: this.source }, partial);
    }
    post(message) {
        if (__classPrivateFieldGet(this, _MessageBroker_posted, "f").has(message.id)) {
            // if we've already posted this message, don't repost it ... I guess I
            // just realized there's kind of a weird edge case where a user looses
            // network connectivity for more than a "pages' worth" of messages and
            // thereby winds up with a gap in their message history
            return;
        }
        __classPrivateFieldGet(this, _MessageBroker_posted, "f").add(message.id);
        /**
         * this works because `message.dateTime` is a stringified number of
         * milliseconds since Unix epoch, and messages are keyed off this string
         * that has that same `dateTime` as its prefix
         *
         * @see @dndbeyond/game-log-lambdas/src/interfaces/MessageLastEvaluatedKey.ts
         */
        if (!__classPrivateFieldGet(this, _MessageBroker_oldest, "f") || __classPrivateFieldGet(this, _MessageBroker_oldest, "f") > message.dateTime) {
            __classPrivateFieldSet(this, _MessageBroker_oldest, toKey(message), "f");
        }
        if (!__classPrivateFieldGet(this, _MessageBroker_newest, "f") || __classPrivateFieldGet(this, _MessageBroker_newest, "f") < message.dateTime) {
            __classPrivateFieldSet(this, _MessageBroker_newest, toKey(message), "f");
        }
        __classPrivateFieldGet(this, _MessageBroker_evt, "f").post(message);
    }
    reset() {
        if (__classPrivateFieldGet(this, _MessageBroker_ws, "f") && !isClosed(__classPrivateFieldGet(this, _MessageBroker_ws, "f"))) {
            __classPrivateFieldGet(this, _MessageBroker_ws, "f").close(1000, MESSAGE_BROKER_RESET_REASON);
        }
        // drop any queued messages, this is a "full" reset
        __classPrivateFieldGet(this, _MessageBroker_queue, "f").length = 0;
        __classPrivateFieldSet(this, _MessageBroker_ws, undefined, "f");
        // reset any notion of message history
        __classPrivateFieldSet(this, _MessageBroker_oldest, undefined, "f");
        __classPrivateFieldSet(this, _MessageBroker_newest, undefined, "f");
        __classPrivateFieldGet(this, _MessageBroker_posted, "f").clear();
        __classPrivateFieldSet(this, _MessageBroker_hasMoreMessages, true, "f");
    }
    connect() {
        return __awaiter(this, void 0, void 0, function* () {
            if (!isValidGameId(__classPrivateFieldGet(this, _MessageBroker_gameId, "f"))) {
                return;
            }
            if (__classPrivateFieldGet(this, _MessageBroker_isAwaitingStt, "f")) {
                // if we're already fetching an `stt` successive calls to `connect` should
                // just bail early
                return;
            }
            if (!__classPrivateFieldGet(this, _MessageBroker_ws, "f")) {
                yield this.validateStt();
                if (!__classPrivateFieldGet(this, _MessageBroker_stt, "f")) {
                    // stt validation failed, bail early / try again on next connect
                    return;
                }
                if (__classPrivateFieldGet(this, _MessageBroker_gameId, "f")) {
                    this.getMessages().then((messages) => messages.forEach((message) => this.post(message)));
                }
                // if we don't already have a socket instance create one now
                __classPrivateFieldSet(this, _MessageBroker_ws, ws(__classPrivateFieldGet(this, _MessageBroker_connectUrl, "f"), Object.assign(Object.assign({}, (__classPrivateFieldGet(this, _MessageBroker_gameId, "f") !== undefined && { gameId: __classPrivateFieldGet(this, _MessageBroker_gameId, "f") })), { userId: __classPrivateFieldGet(this, _MessageBroker_userId, "f"), stt: __classPrivateFieldGet(this, _MessageBroker_stt, "f") })), "f");
                // wire up "socket lifecycle" handlers, flush anything in the queue when
                // the instance opens, reset if we loose the connection
                Evt.from(__classPrivateFieldGet(this, _MessageBroker_ws, "f"), 'open').attachOnce(() => this.flushQueue());
                Evt.merge([
                    Evt.from(__classPrivateFieldGet(this, _MessageBroker_ws, "f"), 'close'),
                    Evt.from(__classPrivateFieldGet(this, _MessageBroker_ws, "f"), 'error'),
                ]).attachOnce((event) => !(event instanceof CloseEvent &&
                    event.reason === MESSAGE_BROKER_RESET_REASON) && event.target === __classPrivateFieldGet(this, _MessageBroker_ws, "f"), () => {
                    this.reset();
                    this.connect();
                });
                // stream messages from the socket to the evt instance
                Evt.from(__classPrivateFieldGet(this, _MessageBroker_ws, "f"), 'message').attach((event) => event.data !== 'pong', (event) => {
                    this.post(JSON.parse(event.data));
                });
                return;
            }
            // if we have a socket instance, but it's already closing or closed, just
            // reset and try again (should hop back up to that first condition above)
            if (isClosing(__classPrivateFieldGet(this, _MessageBroker_ws, "f")) || isClosed(__classPrivateFieldGet(this, _MessageBroker_ws, "f"))) {
                this.reset();
                this.connect();
                return;
            }
            // it's already connecting just do nothing/wait
            if (isConnecting(__classPrivateFieldGet(this, _MessageBroker_ws, "f"))) {
                return;
            }
            // if we tried to connect, but we're already connected/open, flush the queue
            if (isOpen(__classPrivateFieldGet(this, _MessageBroker_ws, "f")) && __classPrivateFieldGet(this, _MessageBroker_queue, "f").length) {
                this.flushQueue();
            }
        });
    }
    validateStt() {
        return __awaiter(this, void 0, void 0, function* () {
            try {
                __classPrivateFieldSet(this, _MessageBroker_isAwaitingStt, true, "f");
                __classPrivateFieldSet(this, _MessageBroker_stt, yield __classPrivateFieldGet(this, _MessageBroker_getShortTermToken, "f").call(this), "f");
            }
            catch (error) {
                if (process.env.NODE_ENV !== 'test') {
                    console.error('Failed to get short term token', error);
                }
            }
            finally {
                __classPrivateFieldSet(this, _MessageBroker_isAwaitingStt, false, "f");
            }
        });
    }
    getMessages(lastEvaluatedKey) {
        var _a, _b;
        return __awaiter(this, void 0, void 0, function* () {
            if (!(isValidGameId(__classPrivateFieldGet(this, _MessageBroker_gameId, "f")) && __classPrivateFieldGet(this, _MessageBroker_hasMoreMessages, "f"))) {
                return [];
            }
            let response;
            try {
                __classPrivateFieldSet(this, _MessageBroker_isAwaitingMessages, true, "f");
                response = yield fetch(href(__classPrivateFieldGet(this, _MessageBroker_getMessagesUrl, "f"), Object.assign(Object.assign(Object.assign({}, (lastEvaluatedKey && { lastEvaluatedKey })), (__classPrivateFieldGet(this, _MessageBroker_gameId, "f") !== undefined && { gameId: __classPrivateFieldGet(this, _MessageBroker_gameId, "f") })), { userId: __classPrivateFieldGet(this, _MessageBroker_userId, "f") })), {
                    headers: {
                        Authorization: `Bearer ${__classPrivateFieldGet(this, _MessageBroker_stt, "f")}`,
                        'X-Source-Url': (_b = (_a = globalThis === null || globalThis === void 0 ? void 0 : globalThis.location) === null || _a === void 0 ? void 0 : _a.href) !== null && _b !== void 0 ? _b : 'unknown',
                    },
                });
            }
            catch (error) {
                if (process.env.NODE_ENV !== 'test') {
                    // this is a network error, not a server error
                    // warn instead of error, so it doesn't fill up RUM logs
                    console.warn(`Failed to get messages`, error);
                }
                // `fetch` will only throw if there's a network error, so I think the
                // right thing to do is reset and try again on next dispatch
                this.reset();
                return [];
            }
            if (!response.ok) {
                if (process.env.NODE_ENV !== 'test') {
                    console.error(`Failed to get messages, server responded with:`, {
                        status: response.status,
                        statusText: response.statusText,
                    });
                }
                return [];
            }
            const { data, lastKey, } = yield response.json();
            __classPrivateFieldSet(this, _MessageBroker_hasMoreMessages, lastKey !== null, "f");
            let nextData = data;
            /**
             * if we're not fetching history (e.g. `lastEvaluatedKey` is `undefined`)
             * and we've got (or had) an active socket (e.g. we've seen some message(s)
             * come through and we have a `#newest` key) then we're in a 'reconnecting'
             * mode and want to serially fetch pages of messages until we hit a page
             * that contains our `#newest` key, or `MAX_HISTORY_CATCHUP_PAGES` (10)
             * pages, or a page comes back empty (e.g. `keys[0]` is undefined) at which
             * point we just quit
             */
            if (!lastEvaluatedKey && __classPrivateFieldGet(this, _MessageBroker_newest, "f")) {
                let pagesFetched = 1; // the fetch above counts as the first page
                let keys = nextData.map(toKey).sort();
                while (!keys.includes(__classPrivateFieldGet(this, _MessageBroker_newest, "f")) &&
                    pagesFetched++ < MAX_HISTORY_CATCHUP_PAGES &&
                    keys[0] !== undefined) {
                    const messages = yield this.getMessages(keys[0]);
                    nextData = nextData.concat(messages);
                    keys = nextData.map(toKey).sort();
                }
                // TODO: if we didn't catch up our previous `#newest` message after
                // fetching 10 pages we should signal `game-log-client` (or other
                // consumers) that they should drop messages older than `keys[0]` and
                // re-fetch history if necessary (or *something*)
            }
            __classPrivateFieldSet(this, _MessageBroker_isAwaitingMessages, false, "f");
            return nextData.sort(byDateTime);
        });
    }
    flushQueue() {
        if (!__classPrivateFieldGet(this, _MessageBroker_ws, "f")) {
            this.connect();
            return;
        }
        while (__classPrivateFieldGet(this, _MessageBroker_queue, "f").length) {
            const serializedMessage = __classPrivateFieldGet(this, _MessageBroker_queue, "f").shift();
            serializedMessage && __classPrivateFieldGet(this, _MessageBroker_ws, "f").send(serializedMessage);
        }
        clearTimeout(__classPrivateFieldGet(this, _MessageBroker_pingId, "f"));
        __classPrivateFieldSet(this, _MessageBroker_pingId, setTimeout(() => this.ping(), FIVE_MINUTES), "f");
    }
    ping() {
        if (!__classPrivateFieldGet(this, _MessageBroker_ws, "f")) {
            this.connect();
            return;
        }
        /**
         * just trying to keep this as light as possible, should be equivalent to:
         * `JSON.stringify({ data: 'ping' })`
         *
         * @see packages/game-log-lambdas/src/gamelog-sendmessage/index.ts
         */
        __classPrivateFieldGet(this, _MessageBroker_ws, "f").send('{"data":"ping"}');
        clearTimeout(__classPrivateFieldGet(this, _MessageBroker_pingId, "f"));
        __classPrivateFieldSet(this, _MessageBroker_pingId, setTimeout(() => this.ping(), FIVE_MINUTES), "f");
    }
    /**
     * Handle window focus events to reconnect if disconnected
     * This helps recover from browser resource optimization that may have closed the connection
     */
    handleFocusReconnect() {
        if (this.status === MessageBrokerStatus.Disconnected) {
            this.connect();
        }
    }
    /**
     * Handle document visibility change events to reconnect if disconnected when tab becomes visible
     * This helps recover when browser throttling/suspension closes connections in background tabs
     */
    handleVisibilityReconnect() {
        if (!document.hidden && this.status === MessageBrokerStatus.Disconnected) {
            this.connect();
        }
    }
}
_MessageBroker_gameId = new WeakMap(), _MessageBroker_userId = new WeakMap(), _MessageBroker_source = new WeakMap(), _MessageBroker_connectUrl = new WeakMap(), _MessageBroker_getMessagesUrl = new WeakMap(), _MessageBroker_evt = new WeakMap(), _MessageBroker_queue = new WeakMap(), _MessageBroker_ws = new WeakMap(), _MessageBroker_getShortTermToken = new WeakMap(), _MessageBroker_isAwaitingStt = new WeakMap(), _MessageBroker_isAwaitingMessages = new WeakMap(), _MessageBroker_stt = new WeakMap(), _MessageBroker_pingId = new WeakMap(), _MessageBroker_oldest = new WeakMap(), _MessageBroker_newest = new WeakMap(), _MessageBroker_posted = new WeakMap(), _MessageBroker_hasMoreMessages = new WeakMap();
//# sourceMappingURL=MessageBroker.js.map