import { SpanStatusCode } from '@opentelemetry/api';
import { EventType } from '@rrweb/types';
export class CrashBufferService {
    constructor(db, tabId, windowMs, ready = Promise.resolve()) {
        this.db = db;
        this.tabId = tabId;
        this.windowMs = windowMs;
        this.ready = ready;
        this.lastPruneAt = 0;
        this.pruneInFlight = null;
        this.isActive = true;
        this.frozenAtTs = null;
        this.lastSeenEventTs = 0;
        this.requiresFullSnapshot = true;
        this.lastTouchAt = 0;
        this.listeners = new Map();
    }
    async _safe(fn, fallback) {
        try {
            return await fn();
        }
        catch (_e) {
            return fallback;
        }
    }
    async appendEvent(payload, _windowMs) {
        this.lastSeenEventTs = Math.max(this.lastSeenEventTs, payload.ts || 0);
        if (!this.isActive)
            return;
        const isFullSnapshot = Boolean(payload.isFullSnapshot);
        await this.ready;
        await this._safe(async () => {
            await this.db.appendEvent({
                tabId: this.tabId,
                ts: payload.ts,
                isFullSnapshot: payload.isFullSnapshot,
                event: payload.event,
            });
        }, undefined);
        if (isFullSnapshot) {
            this.requiresFullSnapshot = false;
        }
        this.pruneSoon();
    }
    async appendSpans(payload, _windowMs) {
        for (const p of payload) {
            this.lastSeenEventTs = Math.max(this.lastSeenEventTs, p.ts || 0);
        }
        if (!this.isActive)
            return;
        let errorEvent = null;
        await this.ready;
        await this._safe(async () => {
            const records = payload.map((p) => {
                var _a, _b;
                if (!errorEvent && ((_b = (_a = p === null || p === void 0 ? void 0 : p.span) === null || _a === void 0 ? void 0 : _a.status) === null || _b === void 0 ? void 0 : _b.code) === SpanStatusCode.ERROR) {
                    errorEvent = { ts: p.ts, span: p.span };
                }
                return {
                    tabId: this.tabId,
                    ts: p.ts,
                    span: p.span,
                };
            });
            await this.db.appendSpans(records);
        }, undefined);
        this.pruneSoon();
        if (errorEvent) {
            this._emit('error-span-appended', errorEvent);
        }
    }
    on(event, listener) {
        const set = this.listeners.get(event) || new Set();
        set.add(listener);
        this.listeners.set(event, set);
        return () => this.off(event, listener);
    }
    off(event, listener) {
        const set = this.listeners.get(event);
        if (!set)
            return;
        set.delete(listener);
        if (set.size === 0)
            this.listeners.delete(event);
    }
    _emit(event, payload) {
        const set = this.listeners.get(event);
        if (!set || set.size === 0)
            return;
        for (const fn of Array.from(set)) {
            try {
                fn(payload);
            }
            catch (_e) {
                // never throw into app code
            }
        }
    }
    async snapshot(_windowMs, now = Date.now()) {
        var _a, _b;
        await this.ready;
        const stoppedAt = now;
        const startedAt = Math.max(0, stoppedAt - this.windowMs);
        // Always include a full snapshot "anchor" if one exists at/before the window start.
        const firstSnapshotAt = await this._safe(async () => {
            const anchor = await this.db.getLastRrwebFullSnapshotBefore(this.tabId, startedAt);
            return typeof (anchor === null || anchor === void 0 ? void 0 : anchor.ts) === 'number' ? anchor.ts : startedAt;
        }, startedAt);
        const [allEvents, allSpans] = await Promise.all([
            this._safe(() => this.db.getRrwebEventsWindow(this.tabId, firstSnapshotAt, stoppedAt), []),
            this._safe(() => this.db.getOtelSpansWindow(this.tabId, startedAt, stoppedAt), []),
        ]);
        const eventsSorted = allEvents
            .sort((a, b) => a.ts - b.ts)
            .map((r) => ({ ts: r.ts, isFullSnapshot: r.isFullSnapshot, event: r.event }));
        const payload = {
            startedAt,
            stoppedAt,
            spans: [],
            events: [],
        };
        // Hard guarantee: snapshot payload starts with Meta -> FullSnapshot (or is empty).
        const firstSnapshotIdx = eventsSorted.findIndex((e) => Boolean(e.isFullSnapshot));
        if (firstSnapshotIdx < 0) {
            return payload;
        }
        // Prefer including the Meta event immediately preceding the first FullSnapshot.
        let startIdx = firstSnapshotIdx;
        for (let i = firstSnapshotIdx - 1; i >= 0; i--) {
            const t = (_b = (_a = eventsSorted[i]) === null || _a === void 0 ? void 0 : _a.event) === null || _b === void 0 ? void 0 : _b.eventType;
            if (t === EventType.Meta) {
                startIdx = i;
                break;
            }
        }
        const events = eventsSorted.slice(startIdx);
        const replayStartedAt = events.length > 0 ? events[0].ts : startedAt;
        const spans = allSpans
            .filter((s) => typeof (s === null || s === void 0 ? void 0 : s.ts) === 'number' && s.ts >= replayStartedAt)
            .sort((a, b) => a.ts - b.ts)
            .map((r) => ({ ts: r.ts, span: r.span }));
        payload.events = events;
        payload.spans = spans;
        payload.startedAt = replayStartedAt;
        return payload;
    }
    async clear() {
        await this._safe(() => this.db.clearTab(this.tabId), undefined);
        this.requiresFullSnapshot = true;
    }
    needsFullSnapshot() {
        return this.requiresFullSnapshot;
    }
    /**
     * Mark the tab as active/inactive.
     *
     * - When inactive, we freeze the buffer and keep the last active `windowMs` indefinitely.
     * - When re-activated, we resume buffering but require a new FullSnapshot to start a replayable segment.
     */
    setActive(active) {
        if (this.isActive === active)
            return;
        this.isActive = active;
        if (!active) {
            const freezeAt = this.lastSeenEventTs || Date.now();
            this.frozenAtTs = freezeAt;
            // Keep the last active window, but preserve rrweb replayability via snapshot anchor.
            const cutoff = Math.max(0, freezeAt - this.windowMs);
            void this._safe(() => this.db.pruneOlderThanWithRrwebSnapshotAnchor(this.tabId, cutoff), undefined);
            return;
        }
        const wasFrozen = this.frozenAtTs !== null;
        this.frozenAtTs = null;
        // New active segment must start with a full snapshot (don’t stitch segments together).
        if (wasFrozen)
            this.requiresFullSnapshot = true;
        this.pruneSoon();
    }
    pruneSoon() {
        if (!this.isActive)
            return;
        const now = Date.now();
        // Throttle pruning to avoid hammering IDB on high event rates
        if (now - this.lastPruneAt < 2000)
            return;
        if (this.pruneInFlight)
            return;
        this.lastPruneAt = now;
        const cutoff = Math.max(0, now - this.windowMs);
        // Heartbeat so the stale-tab sweeper never deletes active tabs.
        if (now - this.lastTouchAt > 30000) {
            this.lastTouchAt = now;
            void this._safe(() => this.db.touchTab(this.tabId, now), undefined);
        }
        this.pruneInFlight = this._safe(() => this.db.pruneOlderThanWithRrwebSnapshotAnchor(this.tabId, cutoff), undefined).finally(() => {
            this.pruneInFlight = null;
        });
    }
}
//# sourceMappingURL=crashBuffer.service.js.map