export class EventBatcher {
    outbox = [];
    outboxCursor = 0;
    draining = false;
    txnEventSink;
    debounceState;
    runtime;
    emit;
    constructor(options) {
        this.runtime = options.runtime;
        this.emit = options.emit;
        this.txnEventSink = undefined;
        this.debounceState = undefined;
    }
    handleCommitEvents(source, events, bufferable) {
        if (events.length === 0) {
            return;
        }
        if (bufferable && this.txnEventSink) {
            this.txnEventSink.push(...events);
            return;
        }
        if (bufferable && this.debounceState) {
            this.bufferLocalEvents(events);
            return;
        }
        this.enqueueBatch({ source, events });
    }
    beforeImport() {
        if (this.debounceState !== undefined) {
            this.commit();
        }
    }
    async txn(callback) {
        if (this.txnEventSink !== undefined) {
            throw new Error("Nested transactions are not supported");
        }
        if (this.debounceState !== undefined) {
            throw new Error("Cannot start transaction while autoDebounceCommit is active");
        }
        const eventSink = [];
        this.txnEventSink = eventSink;
        try {
            const result = await callback();
            if (eventSink.length > 0) {
                this.txnEventSink = undefined;
                this.enqueueBatch({ source: "local", events: eventSink });
            }
            return result;
        }
        finally {
            this.txnEventSink = undefined;
        }
    }
    isInTxn() {
        return this.txnEventSink !== undefined;
    }
    autoDebounceCommit(timeout, options) {
        if (this.txnEventSink !== undefined) {
            throw new Error("Cannot enable autoDebounceCommit while transaction is active");
        }
        if (this.debounceState !== undefined) {
            throw new Error("autoDebounceCommit is already active");
        }
        const maxDebounceTime = options?.maxDebounceTime ?? 10000;
        this.debounceState = {
            timeout,
            maxDebounceTime,
            timerId: undefined,
            deadlineAt: undefined,
            pendingEvents: [],
        };
    }
    disableAutoDebounceCommit() {
        if (this.debounceState === undefined) {
            return;
        }
        const eventsToEmit = this.takeDebouncedEvents();
        this.debounceState = undefined;
        if (eventsToEmit.length > 0) {
            this.enqueueBatch({ source: "local", events: eventsToEmit });
        }
    }
    commit() {
        if (this.debounceState === undefined) {
            return;
        }
        this.flushDebouncedEvents();
    }
    isAutoDebounceActive() {
        return this.debounceState !== undefined;
    }
    close() {
        // Commit any pending debounced events
        if (this.debounceState !== undefined) {
            this.disableAutoDebounceCommit();
        }
        // Commit any transaction events (edge case: close during txn)
        if (this.txnEventSink !== undefined) {
            const pending = this.txnEventSink;
            this.txnEventSink = undefined;
            if (pending.length > 0) {
                const eventsToEmit = pending.slice();
                pending.length = 0;
                this.enqueueBatch({ source: "local", events: eventsToEmit });
            }
        }
    }
    enqueueBatch(batch) {
        this.outbox.push(batch);
        this.drainOutbox();
    }
    drainOutbox() {
        if (this.draining) {
            return;
        }
        this.draining = true;
        try {
            while (this.outboxCursor < this.outbox.length) {
                const batch = this.outbox[this.outboxCursor];
                this.outboxCursor += 1;
                this.deliverBatch(batch);
            }
        }
        finally {
            this.outbox = [];
            this.outboxCursor = 0;
            this.draining = false;
        }
    }
    deliverBatch(batch) {
        try {
            this.emit(batch.source, batch.events);
        }
        catch (error) {
            void error;
        }
    }
    bufferLocalEvents(events) {
        if (this.debounceState === undefined) {
            return;
        }
        if (events.length === 0) {
            return;
        }
        const state = this.debounceState;
        const now = this.runtime.now();
        if (state.pendingEvents.length === 0) {
            state.deadlineAt = now + state.maxDebounceTime;
        }
        state.pendingEvents.push(...events);
        this.scheduleDebounceFlush(now);
    }
    scheduleDebounceFlush(now) {
        if (this.debounceState === undefined) {
            return;
        }
        const state = this.debounceState;
        if (state.pendingEvents.length === 0) {
            return;
        }
        if (state.timeout <= 0) {
            this.flushDebouncedEvents();
            return;
        }
        const deadlineAt = state.deadlineAt ?? now + state.maxDebounceTime;
        state.deadlineAt = deadlineAt;
        const dueAt = Math.min(now + state.timeout, deadlineAt);
        if (dueAt <= now) {
            this.flushDebouncedEvents();
            return;
        }
        if (state.timerId !== undefined) {
            this.runtime.clearTimeout(state.timerId);
        }
        state.timerId = this.runtime.setTimeout(() => {
            this.flushDebouncedEvents();
        }, Math.max(0, dueAt - now));
    }
    takeDebouncedEvents() {
        if (this.debounceState === undefined) {
            return [];
        }
        const state = this.debounceState;
        if (state.timerId !== undefined) {
            this.runtime.clearTimeout(state.timerId);
            state.timerId = undefined;
        }
        state.deadlineAt = undefined;
        if (state.pendingEvents.length === 0) {
            return [];
        }
        const eventsToEmit = state.pendingEvents;
        state.pendingEvents = [];
        return eventsToEmit;
    }
    flushDebouncedEvents() {
        const eventsToEmit = this.takeDebouncedEvents();
        if (eventsToEmit.length === 0) {
            return;
        }
        this.enqueueBatch({ source: "local", events: eventsToEmit });
    }
}
