"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SessionReplayEventsIDBStore = exports.createStore = exports.defineObjectStores = exports.withTimeout = exports.TX_DONE_TIMEOUT_MS = exports.OPEN_DB_TIMEOUT_MS = exports.remoteConfigKey = exports.sequencesToSendKey = exports.currentSequenceKey = exports.generateUUID = void 0;
var tslib_1 = require("tslib");
var idb_1 = require("idb");
var messages_1 = require("../messages");
var base_events_store_1 = require("./base-events-store");
var is_abort_error_1 = require("../utils/is-abort-error");
// crypto.randomUUID() requires a secure context (https). Fall back to a
// Math.random-based UUID for http origins or older browsers — tab IDs don't
// need to be cryptographically secure, just unique within a session.
function generateUUID() {
    try {
        return crypto.randomUUID();
    }
    catch (_a) {
        return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
            var r = (Math.random() * 16) | 0;
            return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);
        });
    }
}
exports.generateUUID = generateUUID;
exports.currentSequenceKey = 'sessionCurrentSequence';
exports.sequencesToSendKey = 'sequencesToSend';
exports.remoteConfigKey = 'remoteConfig';
// Timeout for openDB at init.  IDB openDB can hang forever when another tab
// holds an open connection during a version upgrade, or when the DB is in a
// "closing" state (documented Chrome behaviour).  When that happens we want
// SessionReplayEventsIDBStore.new() to bail out so the caller can fall back
// to the in-memory store.
exports.OPEN_DB_TIMEOUT_MS = 2000;
// Timeout for per-operation tx.done settlement.  Mid-recording a readwrite
// transaction can stall (storage pressure in some browsers stalls instead of
// throwing); without a timeout, recordFailure() is never called and the
// memory fallback never triggers.  The transaction may still settle later;
// the timedOut flag prevents double-counting alongside errorLogged.
exports.TX_DONE_TIMEOUT_MS = 5000;
/**
 * Race a promise against a timeout.  Resolves/rejects with the original
 * promise's value when it settles first; rejects with a timeout error if
 * `ms` elapses first.  Either way the timer is cleared so we don't leak
 * pending setTimeouts.
 */
function withTimeout(promise, ms, message) {
    if (message === void 0) { message = 'IDB operation timed out'; }
    return new Promise(function (resolve, reject) {
        var timer = setTimeout(function () { return reject(new Error("".concat(message, " after ").concat(ms, "ms"))); }, ms);
        promise.then(function (v) {
            clearTimeout(timer);
            resolve(v);
        }, function (e) {
            clearTimeout(timer);
            reject(e);
        });
    });
}
exports.withTimeout = withTimeout;
/**
 * Arms a watchdog that fires `onTimeout` only if `tx.done` hasn't settled
 * within `ms`.  A "soft cancel" returned to the caller suppresses the timeout
 * if the synchronous operation body completes without exception — important
 * because in test environments fake timers can fire ahead of tx.done's commit
 * microtask.  Production behaviour is preserved: a genuinely stalled
 * transaction (no commit, no error, individual op promises never resolved)
 * cannot reach the soft-cancel call and the timeout fires as designed.
 *
 * Soft-cancel is only invoked once the awaits inside the operation's outer
 * try block have all returned — i.e. the IDB driver acknowledged each
 * individual put/get.  If the driver accepts a put but the underlying
 * transaction silently fails to commit (the production stall scenario),
 * tx.done still hasn't settled, and recordFailure must NOT have been
 * suppressed.  The fix for that scenario in production is the tx.done.catch
 * handler attached separately by callers, which catches the eventual abort.
 *
 * The soft-cancel pattern only suppresses the timeout for the "all puts
 * resolved successfully" case — a case where tx.done is overwhelmingly
 * likely to settle imminently in production.  If it doesn't, callers can
 * still detect the failure on the NEXT operation (which will fail to open
 * a transaction or hit the same pressure), because all three methods use
 * readwrite transactions on the same two stores, which IDB serializes:
 * if T1's tx.done never settles, T2 is blocked waiting for T1 to commit or
 * abort — T2's put/get requests never resolve, T2 never reaches its
 * soft-cancel, and T2's watchdog fires, calling recordFailure().
 */
function armTxDoneTimeout(txDone, ms, onTimeout) {
    var timer = setTimeout(onTimeout, ms);
    // Belt-and-braces: clear the timer when tx.done settles, even though the
    // primary cancel path is the caller's success-path cancel().  This covers
    // the case where tx.done settles with no caller cancellation (shouldn't
    // happen in current code paths, but cheap insurance).
    txDone.then(function () { return clearTimeout(timer); }, function () { return clearTimeout(timer); });
    return function () { return clearTimeout(timer); };
}
var defineObjectStores = function (db) {
    var sequencesStore;
    var currentSequenceStore;
    if (!db.objectStoreNames.contains(exports.currentSequenceKey)) {
        currentSequenceStore = db.createObjectStore(exports.currentSequenceKey, {
            keyPath: 'sessionId',
        });
    }
    if (!db.objectStoreNames.contains(exports.sequencesToSendKey)) {
        sequencesStore = db.createObjectStore(exports.sequencesToSendKey, {
            keyPath: 'sequenceId',
            autoIncrement: true,
        });
        sequencesStore.createIndex('sessionId', 'sessionId');
    }
    return {
        sequencesStore: sequencesStore,
        currentSequenceStore: currentSequenceStore,
    };
};
exports.defineObjectStores = defineObjectStores;
var createStore = function (dbName) { return tslib_1.__awaiter(void 0, void 0, void 0, function () {
    return tslib_1.__generator(this, function (_a) {
        switch (_a.label) {
            case 0: return [4 /*yield*/, withTimeout((0, idb_1.openDB)(dbName, 1, {
                    upgrade: exports.defineObjectStores,
                }), exports.OPEN_DB_TIMEOUT_MS, 'IDB openDB timed out')];
            case 1: 
            // Wrap openDB with a timeout so a hung connection (foreign tab holding an
            // open handle during version upgrade, or "closing" DB) doesn't block the
            // SDK from initialising.  On timeout this rejects, which propagates up to
            // SessionReplayEventsIDBStore.new()'s catch block, returning undefined and
            // triggering the memory fallback.
            return [2 /*return*/, _a.sent()];
        }
    });
}); };
exports.createStore = createStore;
var SessionReplayEventsIDBStore = /** @class */ (function (_super) {
    tslib_1.__extends(SessionReplayEventsIDBStore, _super);
    function SessionReplayEventsIDBStore(args) {
        var _this = this;
        var _a;
        _this = _super.call(this, args) || this;
        _this.consecutiveFailures = 0;
        _this.hasTriggeredFallback = false;
        _this.getSequencesToSend = function () { return tslib_1.__awaiter(_this, void 0, void 0, function () {
            var errorLogged, timedOut, sequences, tx, cancelTimeout, cursor, _a, sessionId, events, e_1;
            var _this = this;
            return tslib_1.__generator(this, function (_b) {
                switch (_b.label) {
                    case 0:
                        errorLogged = false;
                        timedOut = false;
                        _b.label = 1;
                    case 1:
                        _b.trys.push([1, 6, , 7]);
                        sequences = [];
                        tx = this.db.transaction('sequencesToSend');
                        // Attach a catch handler immediately so tx.done rejections (e.g. AbortError after
                        // cursor traversal completes) are always handled without blocking the return path.
                        // The errorLogged / timedOut flags prevent double-logging and double-recording
                        // when the outer catch (or the timeout race) already fired for the same abort.
                        tx.done.catch(function (e) {
                            if (!errorLogged && !timedOut) {
                                (0, is_abort_error_1.logIdbError)(_this.loggerProvider, "".concat(messages_1.STORAGE_FAILURE, ": ").concat(e), e);
                                _this.recordFailure();
                            }
                        });
                        cancelTimeout = armTxDoneTimeout(tx.done, exports.TX_DONE_TIMEOUT_MS, function () {
                            if (!errorLogged && !timedOut) {
                                timedOut = true;
                                (0, is_abort_error_1.logIdbError)(_this.loggerProvider, "".concat(messages_1.STORAGE_FAILURE, ": transaction timed out"));
                                _this.recordFailure();
                            }
                        });
                        return [4 /*yield*/, tx.store.openCursor()];
                    case 2:
                        cursor = _b.sent();
                        _b.label = 3;
                    case 3:
                        if (!cursor) return [3 /*break*/, 5];
                        _a = cursor.value, sessionId = _a.sessionId, events = _a.events;
                        // Return all completed sequences regardless of tabId.  Filtering by tab
                        // would cause event loss on page reload: a new store instance gets a
                        // fresh in-memory UUID and would never see sequences written by the
                        // previous instance.  Completed sequences are safe to flush by any
                        // tab/instance; the server deduplicates, and cleanUpSessionEventsStore
                        // on an already-deleted key is a no-op.
                        sequences.push({
                            events: events,
                            sequenceId: cursor.key,
                            sessionId: sessionId,
                        });
                        return [4 /*yield*/, cursor.continue()];
                    case 4:
                        cursor = _b.sent();
                        return [3 /*break*/, 3];
                    case 5:
                        this.recordSuccess();
                        cancelTimeout();
                        return [2 /*return*/, sequences];
                    case 6:
                        e_1 = _b.sent();
                        if (!timedOut) {
                            errorLogged = true;
                            (0, is_abort_error_1.logIdbError)(this.loggerProvider, "".concat(messages_1.STORAGE_FAILURE, ": ").concat(e_1), e_1);
                            this.recordFailure();
                        }
                        return [3 /*break*/, 7];
                    case 7: return [2 /*return*/, undefined];
                }
            });
        }); };
        _this.storeCurrentSequence = function (sessionId) { return tslib_1.__awaiter(_this, void 0, void 0, function () {
            var errorLogged, timedOut, tx, cancelTimeout, currentSequenceData, sequenceId, _tabId, rest, e_2;
            var _this = this;
            return tslib_1.__generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        errorLogged = false;
                        timedOut = false;
                        _a.label = 1;
                    case 1:
                        _a.trys.push([1, 5, , 6]);
                        tx = this.db.transaction([exports.currentSequenceKey, exports.sequencesToSendKey], 'readwrite');
                        tx.done.catch(function (e) {
                            if (!errorLogged && !timedOut) {
                                (0, is_abort_error_1.logIdbError)(_this.loggerProvider, "".concat(messages_1.STORAGE_FAILURE, ": ").concat(e), e);
                                _this.recordFailure();
                            }
                        });
                        cancelTimeout = armTxDoneTimeout(tx.done, exports.TX_DONE_TIMEOUT_MS, function () {
                            if (!errorLogged && !timedOut) {
                                timedOut = true;
                                (0, is_abort_error_1.logIdbError)(_this.loggerProvider, "".concat(messages_1.STORAGE_FAILURE, ": transaction timed out"));
                                _this.recordFailure();
                            }
                        });
                        return [4 /*yield*/, tx.objectStore(exports.currentSequenceKey).get(sessionId)];
                    case 2:
                        currentSequenceData = _a.sent();
                        // Skip promotion if the slot is empty or owned by another tab — let the owning
                        // tab promote its own events on its next addEventToCurrentSequence/storeCurrentSequence
                        // call (via Bug 1's foreign-tab promotion path).
                        // Don't call recordSuccess() here: no write was performed, so this is not
                        // evidence the storage layer is healthy — leave the failure counter unchanged.
                        if (!currentSequenceData || (currentSequenceData.tabId && currentSequenceData.tabId !== this.tabId)) {
                            cancelTimeout();
                            return [2 /*return*/, undefined];
                        }
                        // Skip empty sequences — no point writing a zero-event row to sequencesToSend.
                        if (currentSequenceData.events.length === 0) {
                            cancelTimeout();
                            return [2 /*return*/, undefined];
                        }
                        return [4 /*yield*/, tx.objectStore(exports.sequencesToSendKey).put({
                                sessionId: sessionId,
                                events: currentSequenceData.events,
                                tabId: this.tabId,
                            })];
                    case 3:
                        sequenceId = _a.sent();
                        return [4 /*yield*/, tx.objectStore(exports.currentSequenceKey).put({
                                sessionId: sessionId,
                                events: [],
                                tabId: this.tabId,
                            })];
                    case 4:
                        _a.sent();
                        this.recordSuccess();
                        cancelTimeout();
                        _tabId = currentSequenceData.tabId, rest = tslib_1.__rest(currentSequenceData, ["tabId"]);
                        return [2 /*return*/, tslib_1.__assign(tslib_1.__assign({}, rest), { sessionId: sessionId, sequenceId: sequenceId })];
                    case 5:
                        e_2 = _a.sent();
                        if (!timedOut) {
                            errorLogged = true;
                            (0, is_abort_error_1.logIdbError)(this.loggerProvider, "".concat(messages_1.STORAGE_FAILURE, ": ").concat(e_2), e_2);
                            this.recordFailure();
                        }
                        return [3 /*break*/, 6];
                    case 6: return [2 /*return*/, undefined];
                }
            });
        }); };
        _this.addEventToCurrentSequence = function (sessionId, event) { return tslib_1.__awaiter(_this, void 0, void 0, function () {
            var errorLogged, timedOut, tx, cancelTimeout, sequenceEvents, ownedSequence, eventsToSend, sequenceId, e_3;
            var _this = this;
            return tslib_1.__generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        errorLogged = false;
                        timedOut = false;
                        _a.label = 1;
                    case 1:
                        _a.trys.push([1, 13, , 14]);
                        tx = this.db.transaction([exports.currentSequenceKey, exports.sequencesToSendKey], 'readwrite');
                        // Attach a catch handler immediately so tx.done rejections (e.g. AbortError after
                        // put succeeds but before auto-commit) are always handled without blocking.
                        // The errorLogged / timedOut flags prevent double-logging when the outer catch
                        // (or the timeout) already fired for the same transaction.
                        tx.done.catch(function (e) {
                            if (!errorLogged && !timedOut) {
                                (0, is_abort_error_1.logIdbError)(_this.loggerProvider, "".concat(messages_1.STORAGE_FAILURE, ": ").concat(e), e);
                                _this.recordFailure();
                            }
                        });
                        cancelTimeout = armTxDoneTimeout(tx.done, exports.TX_DONE_TIMEOUT_MS, function () {
                            if (!errorLogged && !timedOut) {
                                timedOut = true;
                                (0, is_abort_error_1.logIdbError)(_this.loggerProvider, "".concat(messages_1.STORAGE_FAILURE, ": transaction timed out"));
                                _this.recordFailure();
                            }
                        });
                        return [4 /*yield*/, tx.objectStore(exports.currentSequenceKey).get(sessionId)];
                    case 2:
                        sequenceEvents = _a.sent();
                        if (!((sequenceEvents === null || sequenceEvents === void 0 ? void 0 : sequenceEvents.tabId) && sequenceEvents.tabId !== this.tabId)) return [3 /*break*/, 6];
                        if (!(sequenceEvents.events.length > 0)) return [3 /*break*/, 4];
                        return [4 /*yield*/, tx.objectStore(exports.sequencesToSendKey).put({
                                sessionId: sessionId,
                                events: sequenceEvents.events,
                                tabId: sequenceEvents.tabId,
                            })];
                    case 3:
                        _a.sent();
                        _a.label = 4;
                    case 4: return [4 /*yield*/, tx.objectStore(exports.currentSequenceKey).put({ sessionId: sessionId, events: [event], tabId: this.tabId })];
                    case 5:
                        _a.sent();
                        this.recordSuccess();
                        cancelTimeout();
                        return [2 /*return*/, undefined];
                    case 6:
                        ownedSequence = sequenceEvents;
                        if (!!ownedSequence) return [3 /*break*/, 8];
                        return [4 /*yield*/, tx.objectStore(exports.currentSequenceKey).put({ sessionId: sessionId, events: [event], tabId: this.tabId })];
                    case 7:
                        _a.sent();
                        this.recordSuccess();
                        cancelTimeout();
                        return [2 /*return*/, undefined];
                    case 8:
                        if (!!this.shouldSplitEventsList(ownedSequence.events, event)) return [3 /*break*/, 10];
                        return [4 /*yield*/, tx
                                .objectStore(exports.currentSequenceKey)
                                .put({ sessionId: sessionId, events: ownedSequence.events.concat(event), tabId: this.tabId })];
                    case 9:
                        _a.sent();
                        this.recordSuccess();
                        cancelTimeout();
                        return [2 /*return*/, undefined];
                    case 10:
                        eventsToSend = ownedSequence.events;
                        return [4 /*yield*/, tx.objectStore(exports.currentSequenceKey).put({ sessionId: sessionId, events: [event], tabId: this.tabId })];
                    case 11:
                        _a.sent();
                        return [4 /*yield*/, tx.objectStore(exports.sequencesToSendKey).put({
                                sessionId: sessionId,
                                events: eventsToSend,
                                tabId: this.tabId,
                            })];
                    case 12:
                        sequenceId = _a.sent();
                        this.recordSuccess();
                        cancelTimeout();
                        return [2 /*return*/, {
                                events: eventsToSend,
                                sessionId: sessionId,
                                sequenceId: sequenceId,
                            }];
                    case 13:
                        e_3 = _a.sent();
                        if (!timedOut) {
                            errorLogged = true;
                            (0, is_abort_error_1.logIdbError)(this.loggerProvider, "".concat(messages_1.STORAGE_FAILURE, ": ").concat(e_3), e_3);
                            this.recordFailure();
                        }
                        return [3 /*break*/, 14];
                    case 14: return [2 /*return*/, undefined];
                }
            });
        }); };
        _this.storeSendingEvents = function (sessionId, events) { return tslib_1.__awaiter(_this, void 0, void 0, function () {
            var sequenceId, e_4;
            return tslib_1.__generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        _a.trys.push([0, 2, , 3]);
                        return [4 /*yield*/, this.db.put(exports.sequencesToSendKey, {
                                sessionId: sessionId,
                                events: events,
                                tabId: this.tabId,
                            })];
                    case 1:
                        sequenceId = _a.sent();
                        this.recordSuccess();
                        return [2 /*return*/, sequenceId];
                    case 2:
                        e_4 = _a.sent();
                        (0, is_abort_error_1.logIdbError)(this.loggerProvider, "".concat(messages_1.STORAGE_FAILURE, ": ").concat(e_4), e_4);
                        this.recordFailure();
                        return [3 /*break*/, 3];
                    case 3: return [2 /*return*/, undefined];
                }
            });
        }); };
        _this.cleanUpSessionEventsStore = function (_sessionId, sequenceId) { return tslib_1.__awaiter(_this, void 0, void 0, function () {
            var e_5;
            return tslib_1.__generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        if (!sequenceId) {
                            return [2 /*return*/];
                        }
                        _a.label = 1;
                    case 1:
                        _a.trys.push([1, 3, , 4]);
                        return [4 /*yield*/, this.db.delete(exports.sequencesToSendKey, sequenceId)];
                    case 2:
                        _a.sent();
                        this.recordSuccess();
                        return [3 /*break*/, 4];
                    case 3:
                        e_5 = _a.sent();
                        (0, is_abort_error_1.logIdbError)(this.loggerProvider, "".concat(messages_1.STORAGE_FAILURE, ": ").concat(e_5), e_5);
                        this.recordFailure();
                        return [3 /*break*/, 4];
                    case 4: return [2 /*return*/];
                }
            });
        }); };
        _this.db = args.db;
        _this.tabId = args.tabId;
        _this.onPersistentFailure = args.onPersistentFailure;
        // Default threshold of 1: fall back to memory immediately on the first IDB failure.
        // Session replay correctness is far more important than persistence, and IDB errors
        // are typically the symptom of a deeper problem (storage pressure, locked DB, broken
        // browser implementation) that won't recover within a single session.  Memory store
        // is always safe — fall back early.
        _this.consecutiveFailureThreshold = (_a = args.consecutiveFailureThreshold) !== null && _a !== void 0 ? _a : 1;
        return _this;
    }
    SessionReplayEventsIDBStore.prototype.recordFailure = function () {
        var _a;
        this.consecutiveFailures++;
        if (!this.hasTriggeredFallback && this.consecutiveFailures >= this.consecutiveFailureThreshold) {
            this.hasTriggeredFallback = true;
            (_a = this.onPersistentFailure) === null || _a === void 0 ? void 0 : _a.call(this);
        }
    };
    SessionReplayEventsIDBStore.prototype.recordSuccess = function () {
        this.consecutiveFailures = 0;
    };
    SessionReplayEventsIDBStore.new = function (type, args) {
        var _a;
        return tslib_1.__awaiter(this, void 0, void 0, function () {
            var dbSuffix, dbName, db, tabId, e_6;
            return tslib_1.__generator(this, function (_b) {
                switch (_b.label) {
                    case 0:
                        _b.trys.push([0, 2, , 3]);
                        dbSuffix = type === 'replay' ? '' : "_".concat(type);
                        dbName = "".concat(args.apiKey.substring(0, 10), "_amp_session_replay_events").concat(dbSuffix);
                        return [4 /*yield*/, (0, exports.createStore)(dbName)];
                    case 1:
                        db = _b.sent();
                        tabId = (_a = args.tabId) !== null && _a !== void 0 ? _a : generateUUID();
                        return [2 /*return*/, new SessionReplayEventsIDBStore(tslib_1.__assign(tslib_1.__assign({}, args), { db: db, tabId: tabId }))];
                    case 2:
                        e_6 = _b.sent();
                        (0, is_abort_error_1.logIdbError)(args.loggerProvider, "".concat(messages_1.STORAGE_FAILURE, ": ").concat(e_6), e_6);
                        return [3 /*break*/, 3];
                    case 3: return [2 /*return*/];
                }
            });
        });
    };
    SessionReplayEventsIDBStore.prototype.getCurrentSequenceEvents = function (sessionId) {
        return tslib_1.__awaiter(this, void 0, void 0, function () {
            var record, _tabId, rest, allEvents, _a, _b, record, _tabId, rest, e_7_1;
            var e_7, _c;
            return tslib_1.__generator(this, function (_d) {
                switch (_d.label) {
                    case 0:
                        if (!sessionId) return [3 /*break*/, 2];
                        return [4 /*yield*/, this.db.get('sessionCurrentSequence', sessionId)];
                    case 1:
                        record = _d.sent();
                        if (!record) {
                            return [2 /*return*/, undefined];
                        }
                        // Only return our own tab's record (or legacy untagged records).
                        if (record.tabId && record.tabId !== this.tabId) {
                            return [2 /*return*/, undefined];
                        }
                        _tabId = record.tabId, rest = tslib_1.__rest(record, ["tabId"]);
                        return [2 /*return*/, [rest]];
                    case 2:
                        allEvents = [];
                        _d.label = 3;
                    case 3:
                        _d.trys.push([3, 8, 9, 10]);
                        return [4 /*yield*/, this.db.getAll('sessionCurrentSequence')];
                    case 4:
                        _a = tslib_1.__values.apply(void 0, [_d.sent()]), _b = _a.next();
                        _d.label = 5;
                    case 5:
                        if (!!_b.done) return [3 /*break*/, 7];
                        record = _b.value;
                        if (record.tabId && record.tabId !== this.tabId) {
                            return [3 /*break*/, 6];
                        }
                        _tabId = record.tabId, rest = tslib_1.__rest(record, ["tabId"]);
                        allEvents.push(rest);
                        _d.label = 6;
                    case 6:
                        _b = _a.next();
                        return [3 /*break*/, 5];
                    case 7: return [3 /*break*/, 10];
                    case 8:
                        e_7_1 = _d.sent();
                        e_7 = { error: e_7_1 };
                        return [3 /*break*/, 10];
                    case 9:
                        try {
                            if (_b && !_b.done && (_c = _a.return)) _c.call(_a);
                        }
                        finally { if (e_7) throw e_7.error; }
                        return [7 /*endfinally*/];
                    case 10: return [2 /*return*/, allEvents];
                }
            });
        });
    };
    return SessionReplayEventsIDBStore;
}(base_events_store_1.BaseEventsStore));
exports.SessionReplayEventsIDBStore = SessionReplayEventsIDBStore;
//# sourceMappingURL=events-idb-store.js.map