import { isSyncularOfflineError } from './errors.js';
import { browserSyncularNetworkStatusSource } from './network.js';
export function getSyncularClientStatus(client) {
    const lifecycle = client.lifecycleState();
    const connection = client.connectionState();
    const outbox = lifecycle.outbox ?? null;
    const conflicts = lifecycle.conflicts ?? null;
    return {
        lifecycle,
        connection,
        outbox,
        conflicts,
        isConnected: connection.realtime === 'connected' && !connection.closed,
        isSyncing: lifecycle.phase === 'syncing' || lifecycle.phase === 'recovering',
        hasPendingMutations: (outbox?.pending ?? 0) + (outbox?.sending ?? 0) > 0,
        hasConflicts: (conflicts?.unresolved ?? 0) > 0,
        requiresAction: lifecycle.requiresAction,
    };
}
export class SyncularClientLifecycle {
    client;
    options;
    #started = false;
    #pollTimer;
    #unsubscribeDiagnostics;
    #unsubscribeNetwork;
    #syncInFlight;
    #queuedSyncWaiters = [];
    #hasConnectedRealtime = false;
    #realtimeStarted = false;
    #network;
    constructor(client, options = {}) {
        this.client = client;
        this.options = options;
        this.#network =
            options.network === false
                ? undefined
                : (options.network ?? browserSyncularNetworkStatusSource());
    }
    async start() {
        if (this.#started)
            return;
        this.#started = true;
        this.#hasConnectedRealtime =
            this.client.connectionState().realtime === 'connected';
        this.#realtimeStarted = this.#hasConnectedRealtime;
        this.#unsubscribeDiagnostics = this.client.addDiagnosticListener((event) => this.#handleDiagnostic(event));
        this.#unsubscribeNetwork = this.#subscribeNetworkEvents();
        try {
            if (this.options.subscriptions) {
                await this.client.setSubscriptions(this.options.subscriptions);
            }
            if (this.options.initialSync !== false && this.#isOnline()) {
                await this.#syncForLifecycle();
            }
            if (this.options.realtime !== false && this.#isOnline()) {
                await this.#startRealtimeForLifecycle();
            }
            this.#startPolling();
        }
        catch (error) {
            await this.stop().catch(() => undefined);
            throw error;
        }
    }
    async stop() {
        if (!this.#started)
            return;
        this.#started = false;
        this.#stopPolling();
        this.#unsubscribeDiagnostics?.();
        this.#unsubscribeDiagnostics = undefined;
        this.#unsubscribeNetwork?.();
        this.#unsubscribeNetwork = undefined;
        this.#rejectQueuedSyncWaiters(new Error('Syncular lifecycle stopped before queued sync could run'));
        if (this.options.realtime !== false) {
            await this.client.stopRealtime();
        }
        this.#realtimeStarted = false;
    }
    async sync() {
        if (this.#syncInFlight) {
            return new Promise((resolve, reject) => {
                this.#queuedSyncWaiters.push({ resolve, reject });
            });
        }
        this.#syncInFlight = this.#startSyncCycle();
        return this.#syncInFlight;
    }
    #startSyncCycle() {
        let sync;
        try {
            sync = Promise.resolve(this.client.syncOnce());
        }
        catch (error) {
            sync = Promise.reject(error);
        }
        const finish = () => {
            if (this.#syncInFlight === sync) {
                this.#syncInFlight = undefined;
            }
            const waiters = this.#queuedSyncWaiters.splice(0);
            if (waiters.length === 0)
                return;
            if (!this.#started) {
                const error = new Error('Syncular lifecycle stopped before queued sync could run');
                for (const waiter of waiters)
                    waiter.reject(error);
                return;
            }
            const queued = this.#startSyncCycle();
            this.#syncInFlight = queued;
            queued.then((result) => {
                for (const waiter of waiters)
                    waiter.resolve(result);
            }, (error) => {
                for (const waiter of waiters)
                    waiter.reject(error);
            });
        };
        void sync.then(finish, finish).catch((error) => {
            this.#rejectQueuedSyncWaiters(error);
        });
        return sync;
    }
    #rejectQueuedSyncWaiters(error) {
        const waiters = this.#queuedSyncWaiters.splice(0);
        for (const waiter of waiters)
            waiter.reject(error);
    }
    #handleDiagnostic(event) {
        if (event.source === 'sync' && event.details?.resyncRequired === true) {
            void this.client
                .forceSubscriptionsBootstrap()
                .then(() => this.sync())
                .catch(() => undefined);
            return;
        }
        if (event.source !== 'realtime' ||
            event.code !== 'realtime.state' ||
            event.details?.state == null) {
            return;
        }
        const state = event.details.state;
        if (state !== 'connected')
            return;
        const wasReconnect = this.#hasConnectedRealtime;
        this.#hasConnectedRealtime = true;
        const shouldSync = this.options.syncOnRealtimeConnect !== false &&
            (wasReconnect || this.options.initialSync === false);
        if (!shouldSync)
            return;
        void this.sync().catch(() => undefined);
    }
    #subscribeNetworkEvents() {
        const network = this.#network;
        if (!network?.addEventListener || !network.removeEventListener)
            return;
        const handleOnline = () => {
            if (!this.#started)
                return;
            void this.#resumeOnline().catch(() => undefined);
        };
        network.addEventListener('online', handleOnline);
        return () => {
            network.removeEventListener?.('online', handleOnline);
        };
    }
    async #resumeOnline() {
        if (!this.#started || !this.#isOnline())
            return;
        if (this.options.initialSync !== false) {
            await this.#syncForLifecycle();
        }
        if (this.options.realtime !== false && !this.#realtimeStarted) {
            await this.#startRealtimeForLifecycle();
        }
    }
    async #syncForLifecycle() {
        try {
            await this.sync();
        }
        catch (error) {
            if (!isSyncularOfflineError(error))
                throw error;
        }
    }
    async #startRealtimeForLifecycle() {
        try {
            await this.client.startRealtime(this.options.realtime);
            this.#realtimeStarted = true;
        }
        catch (error) {
            this.#realtimeStarted = false;
            if (!isSyncularOfflineError(error))
                throw error;
        }
    }
    #isOnline() {
        return this.#network?.isOnline() !== false;
    }
    #startPolling() {
        const interval = this.options.pollIntervalMs;
        if (interval === false || interval === undefined || interval <= 0)
            return;
        this.#pollTimer = setInterval(() => {
            if (!this.#isOnline())
                return;
            void this.#syncForLifecycle();
        }, interval);
    }
    #stopPolling() {
        if (!this.#pollTimer)
            return;
        clearInterval(this.#pollTimer);
        this.#pollTimer = undefined;
    }
}
//# sourceMappingURL=client.js.map