import { issueSyncularAuthLease } from './auth-leases.js';
import { assertSyncularBlobPayloadLimit } from './blob-limits.js';
import { isSyncularRemoteMode, resolveSyncularClientConfig, SYNCULAR_DEFAULT_STORAGE, } from './client-config.js';
import { appendSyncularDiagnosticEvent, appendSyncularSyncTimings, createSyncularSyncAttempt, summarizeSyncularDiagnosticSubscriptions, } from './diagnostics.js';
import { isGeneratedSyncularOperationalStateWorkerRequestType, } from './generated-bridge.js';
import { browserSyncularNetworkStatusSource } from './network.js';
import { assertSyncularReadonlySql } from './sql-safety.js';
import { getSyncularRuntimeArtifact, selectSyncularRuntimeArtifact, } from './wasm-runtime.js';
import { createSyncularWorkerErrorPayload, SYNCULAR_WORKER_PROTOCOL_VERSION, } from './worker-protocol.js';
const DEFAULT_SYNCULAR_WORKER_REQUEST_TIMEOUT_MS = 30_000;
const NO_SYNCULAR_WORKER_REQUEST_TIMEOUT = 0;
const SYNCULAR_SYNC_WORKER_REQUEST_TYPES = new Set([
    'forceSubscriptionsBootstrap',
    'syncPull',
    'syncPush',
    'syncOnce',
]);
const SYNCULAR_BLOB_WORKER_REQUEST_TYPES = new Set([
    'storeBlob',
    'retrieveBlob',
    'processBlobUploadQueue',
]);
const SYNCULAR_STORAGE_MAINTENANCE_WORKER_REQUEST_TYPES = new Set([
    'open',
    'compactStorage',
    'exportLocalSupportBundle',
    'importLocalSupportBundle',
    'repairLocalHealth',
    'resetLocalSyncState',
]);
function syncularWorkerRequestTimeoutMs(config, type) {
    if (typeof config === 'number' || config === false) {
        return normalizeSyncularWorkerTimeoutMs(config);
    }
    const byType = config?.byType?.[type];
    if (byType !== undefined)
        return normalizeSyncularWorkerTimeoutMs(byType);
    if (SYNCULAR_SYNC_WORKER_REQUEST_TYPES.has(type)) {
        return normalizeSyncularWorkerTimeoutMs(config?.syncMs ?? false);
    }
    if (SYNCULAR_BLOB_WORKER_REQUEST_TYPES.has(type)) {
        return normalizeSyncularWorkerTimeoutMs(config?.blobMs ?? false);
    }
    if (SYNCULAR_STORAGE_MAINTENANCE_WORKER_REQUEST_TYPES.has(type)) {
        return normalizeSyncularWorkerTimeoutMs(config?.storageMaintenanceMs ?? false);
    }
    return normalizeSyncularWorkerTimeoutMs(config?.defaultMs ?? DEFAULT_SYNCULAR_WORKER_REQUEST_TIMEOUT_MS);
}
function normalizeSyncularWorkerTimeoutMs(value) {
    if (value === false)
        return NO_SYNCULAR_WORKER_REQUEST_TIMEOUT;
    if (!Number.isFinite(value) || value <= 0) {
        return NO_SYNCULAR_WORKER_REQUEST_TIMEOUT;
    }
    return Math.trunc(value);
}
export async function createSyncularWorkerClient(options) {
    const config = resolveSyncularClientConfig(options.config);
    const runtime = typeof options.runtime === 'string'
        ? getSyncularRuntimeArtifact(options.runtime)
        : (options.runtime ??
            selectSyncularRuntimeArtifact(options.requiredRuntimeFeatures, options.runtimeArtifacts));
    const worker = typeof options.worker === 'function'
        ? options.worker()
        : (options.worker ?? createDefaultSyncularWorker());
    const client = new SyncularWorkerClient(worker, {
        ownsWorker: options.worker == null,
        requestTimeoutMs: options.requestTimeoutMs,
        getHeaders: options.getHeaders,
        authLifecycle: options.authLifecycle,
        diagnostics: options.diagnostics,
        rowsChangedDebounceMs: options.sync?.rowsChangedDebounceMs,
        network: options.sync?.network === false
            ? undefined
            : (options.sync?.network ?? browserSyncularNetworkStatusSource()),
        blobLimits: options.blobLimits,
    });
    try {
        await client.open(config, runtime);
    }
    catch (err) {
        if (options.config.storage == null &&
            config.storage === SYNCULAR_DEFAULT_STORAGE &&
            isOpfsOpenFailure(err)) {
            const fallbackConfig = { ...config, storage: 'indexedDb' };
            client.setStorageFallback({
                from: SYNCULAR_DEFAULT_STORAGE,
                to: fallbackConfig.storage,
                reason: errorMessage(err),
            });
            await client.open(fallbackConfig, runtime);
        }
        else {
            throw err;
        }
    }
    return client;
}
export function getDefaultSyncularWorkerUrl() {
    const runtimeUrl = new URL(import.meta.url);
    const sourceRuntime = runtimeUrl.pathname.endsWith('/src/worker-client.ts');
    return new URL(sourceRuntime ? './worker-entry.ts' : './worker-entry.js', runtimeUrl);
}
export function createDefaultSyncularWorker() {
    return new Worker(getDefaultSyncularWorkerUrl(), {
        type: 'module',
        credentials: 'same-origin',
    });
}
export class SyncularWorkerError extends Error {
    code;
    category;
    retryable;
    recommendedAction;
    details;
    constructor(payload) {
        super(payload.message);
        this.name = payload.name ?? 'SyncularWorkerError';
        this.code = payload.code;
        this.category = payload.category;
        this.retryable = payload.retryable === true;
        this.recommendedAction = payload.recommendedAction;
        this.details = payload.details;
        if (payload.stack)
            this.stack = payload.stack;
    }
}
export class SyncularWorkerClient {
    worker;
    #nextId = 1;
    #pending = new Map();
    #closed = false;
    #requestTimeouts;
    #getHeaders;
    #authHeaders = {};
    #authLifecycle;
    #authRefreshInFlight;
    #config;
    #realtimeOptions;
    #realtimeState = 'disconnected';
    #storageFallback;
    #lastDiagnostic;
    #lastError;
    #lastLifecycleState;
    #recoveryRequired = false;
    #authRequired = false;
    #subscriptions = [];
    #lastBootstrap;
    #recentDiagnostics = [];
    #recentSyncTimings = [];
    #rowsChangedDebounceMs;
    #rowsChangedDebounceTimer;
    #pendingRowsChanged;
    #lastOutboxStats;
    #lastConflictStats;
    #lastBlobUploadStats;
    #blobLimits;
    #network;
    #networkOnline;
    #unsubscribeNetwork;
    #diagnosticListeners = new Set();
    #rowsChangedListeners = new Set();
    #eventListeners = new Map();
    #presenceByScopeKey = new Map();
    #joinedPresence = new Map();
    #liveListeners = new Map();
    constructor(worker, options) {
        this.worker = worker;
        this.#requestTimeouts = options.requestTimeoutMs;
        this.#getHeaders = options.getHeaders;
        this.#authLifecycle = options.authLifecycle;
        this.#blobLimits = options.blobLimits;
        this.#network = options.network;
        this.#networkOnline = this.#network?.isOnline();
        this.#unsubscribeNetwork = this.#subscribeNetworkEvents();
        this.#rowsChangedDebounceMs =
            options.rowsChangedDebounceMs === false
                ? false
                : Math.max(0, options.rowsChangedDebounceMs ?? 0);
        if (options.diagnostics) {
            this.#diagnosticListeners.add(options.diagnostics);
        }
        this.worker.onmessage = (event) => {
            this.#handleWorkerMessage(event.data);
        };
        this.worker.onerror = (event) => {
            const payload = createSyncularWorkerErrorPayload('worker.failed', event.message || 'Syncular worker failed');
            this.#emitDiagnostic({
                level: 'error',
                source: 'worker',
                code: 'worker.failed',
                message: payload.message,
            });
            this.#rejectAll(payload);
        };
        this.worker.onmessageerror = () => {
            const payload = createSyncularWorkerErrorPayload('worker.message_unreadable', 'Syncular worker sent an unreadable message');
            this.#emitDiagnostic({
                level: 'error',
                source: 'worker',
                code: 'worker.message_unreadable',
                message: payload.message,
            });
            this.#rejectAll(payload);
        };
        this.ownsWorker = options.ownsWorker;
    }
    ownsWorker;
    async open(config, runtime) {
        const resolvedConfig = resolveSyncularClientConfig(config);
        const resolvedRuntime = typeof runtime === 'string'
            ? getSyncularRuntimeArtifact(runtime)
            : runtime;
        await this.#request({
            type: 'open',
            config: resolvedConfig,
            runtime: serializeRuntimeArtifact(resolvedRuntime),
        });
        this.#config = resolvedConfig;
        this.#realtimeOptions = undefined;
        this.#subscriptions = [];
        this.#lastBootstrap = undefined;
        await this.#refreshAuthHeaders();
    }
    async setAuthHeaders(headers) {
        await this.#setAuthHeaders(headers, { restartRealtime: true });
    }
    async issueAuthLease(request) {
        await this.#refreshAuthHeaders();
        try {
            return await this.#issueAuthLeaseWithCurrentHeaders(request);
        }
        catch (error) {
            const shouldRetry = await this.#resolveAuthRetry(error, 'authLeaseIssue');
            if (!shouldRetry)
                throw error;
            await this.#refreshAuthHeaders();
            return this.#issueAuthLeaseWithCurrentHeaders(request);
        }
    }
    async upsertAuthLease(lease) {
        await this.#requestAndDrain({
            type: 'upsertAuthLease',
            lease,
        });
    }
    async authLease(leaseId) {
        return this.#request({
            type: 'authLease',
            leaseId,
        });
    }
    async activeAuthLeases(actorId, nowMs = Date.now()) {
        return this.#request({
            type: 'activeAuthLeases',
            actorId: actorId ?? null,
            nowMs: Math.trunc(nowMs),
        });
    }
    async setFieldEncryption(config) {
        await this.#request({
            type: 'setFieldEncryption',
            config: config == null ? null : normalizeFieldEncryptionConfig(config),
        });
    }
    async setEncryptedCrdt(config) {
        await this.#request({
            type: 'setEncryptedCrdt',
            config: config == null ? null : normalizeEncryptedCrdtConfig(config),
        });
    }
    async setBlobEncryption(config) {
        await this.#request({
            type: 'setBlobEncryption',
            config: config == null ? null : normalizeBlobEncryptionConfig(config),
        });
    }
    async #setAuthHeaders(headers, options) {
        this.#authHeaders = cloneAuthHeaders(headers);
        await this.#request({
            type: 'setAuthHeaders',
            headers: cloneAuthHeaders(this.#authHeaders),
        });
        if (options.restartRealtime)
            await this.#restartRealtime();
    }
    async #issueAuthLeaseWithCurrentHeaders(request) {
        const config = this.#config;
        if (!config) {
            throw new Error('Syncular worker client must be opened before auth lease issue');
        }
        assertSyncularRemoteMode(config, 'auth lease issue');
        const lease = await issueSyncularAuthLease({
            baseUrl: config.baseUrl,
            headers: this.#authHeaders,
            request,
            appSchema: config.appSchema,
        });
        await this.upsertAuthLease(lease);
        this.#emitDiagnostic({
            level: 'info',
            source: 'auth',
            code: 'auth_lease.issued',
            message: 'Syncular auth lease issued and stored',
            details: {
                leaseId: lease.leaseId,
                expiresAtMs: lease.expiresAtMs,
                schemaVersion: lease.schemaVersion,
            },
        });
        return lease;
    }
    async startRealtime(options = {}) {
        if (options === false) {
            await this.stopRealtime();
            return;
        }
        const config = this.#config;
        if (!config) {
            throw new Error('Syncular worker client must be opened before realtime');
        }
        assertSyncularRemoteMode(config, 'realtime');
        const realtimeOptions = options === true ? {} : options;
        if (realtimeOptions.enabled === false) {
            await this.stopRealtime();
            return;
        }
        this.#realtimeOptions = realtimeOptions;
        await this.#refreshAuthHeaders({ restartRealtime: false });
        await this.#request({
            type: 'startRealtime',
            options: await resolveRealtimeWorkerOptions(realtimeOptions, config),
        });
    }
    async stopRealtime() {
        this.#realtimeOptions = undefined;
        await this.#request({ type: 'stopRealtime' });
    }
    async setSubscriptions(subscriptions) {
        this.#subscriptions = [...subscriptions];
        this.#lastBootstrap = undefined;
        await this.#request({
            type: 'setSubscriptions',
            subscriptions: this.#subscriptions,
        });
    }
    async forceSubscriptionsBootstrap(subscriptionIds = []) {
        return this.#request({
            type: 'forceSubscriptionsBootstrap',
            subscriptionIds: [...subscriptionIds],
        });
    }
    async executeSql(sql, params = []) {
        assertSyncularReadonlySql(sql);
        return this.#request({
            type: 'executeSql',
            sql,
            params: [...params],
        });
    }
    executeUnsafeSql(sql, params = []) {
        return this.#request({
            type: 'executeUnsafeSql',
            sql,
            params: [...params],
        });
    }
    subscribeQuery(sql, params, tables, hints = []) {
        return this.#request({
            type: 'subscribeQuery',
            sql,
            params: [...params],
            tables: [...tables],
            hints: hints.map((hint) => ({
                table: hint.table,
                rowIds: [...(hint.rowIds ?? [])],
                fields: [...(hint.fields ?? [])],
            })),
        });
    }
    async unsubscribeQuery(id) {
        await this.#request({ type: 'unsubscribeQuery', queryId: id });
    }
    drainLiveQueryEvents() {
        return this.#request({ type: 'drainLiveQueryEvents' });
    }
    liveQueryDiagnostics() {
        return this.#request({ type: 'liveQueryDiagnostics' });
    }
    async applyMutation(operation, localRow) {
        return this.#requestAndDrain({
            type: 'applyMutation',
            operation,
            localRow,
        });
    }
    async applyLeasedMutation(operation, localRow) {
        return this.#requestAndDrain({
            type: 'applyLeasedMutation',
            operation,
            localRow,
        });
    }
    async applyMutationsBatch(operations) {
        return this.#requestAndDrain({
            type: 'applyMutationsBatch',
            operations,
        });
    }
    async applyMutationsCommit(operations) {
        return this.#requestAndDrain({
            type: 'applyMutationsCommit',
            operations,
        });
    }
    async applyLeasedMutationsCommit(operations) {
        return this.#requestAndDrain({
            type: 'applyLeasedMutationsCommit',
            operations,
        });
    }
    async syncPull(options = {}) {
        return this.#syncWithAuthRetry({
            type: 'syncPull',
            syncAttempt: options.syncAttempt ?? createSyncularSyncAttempt(),
        });
    }
    async syncPush(options = {}) {
        return this.#syncWithAuthRetry({
            type: 'syncPush',
            syncAttempt: options.syncAttempt ?? createSyncularSyncAttempt(),
        });
    }
    async syncOnce(options = {}) {
        return this.#syncWithAuthRetry({
            type: 'syncOnce',
            syncAttempt: options.syncAttempt ?? createSyncularSyncAttempt(),
        });
    }
    async resumeFromBackground(options = {}) {
        this.#recoveryRequired = true;
        this.#emitDiagnostic({
            level: 'info',
            source: 'client',
            code: 'lifecycle.resume_from_background',
            message: 'Syncular resumed from background',
        });
        await this.#refreshAuthHeaders({ restartRealtime: false });
        await this.#restartRealtime();
        return this.#syncWithAuthRetry({
            type: 'syncOnce',
            syncAttempt: options.syncAttempt ?? createSyncularSyncAttempt(),
        }, { refreshAuthHeaders: false });
    }
    transportStats() {
        return this.#request({ type: 'transportStats' });
    }
    async resetTransportStats() {
        await this.#request({ type: 'resetTransportStats' });
    }
    conflictSummaries() {
        return this.#request({ type: 'conflictSummaries' });
    }
    retryConflictKeepLocal(id) {
        return this.#requestAndDrain({
            type: 'retryConflictKeepLocal',
            conflictId: id,
        });
    }
    async resolveConflict(id, resolution) {
        await this.#requestAndDrain({
            type: 'resolveConflict',
            conflictId: id,
            resolution,
        });
    }
    listTable(table) {
        return this.#request({ type: 'listTable', table });
    }
    storeBlob(data, options) {
        try {
            assertSyncularBlobPayloadLimit({
                operation: 'store',
                size: data.byteLength,
                limits: this.#blobLimits,
                options,
                diagnostics: (event) => this.#emitDiagnostic(event),
            });
        }
        catch (error) {
            return Promise.reject(error);
        }
        const request = { type: 'storeBlob', data, options };
        const result = options?.immediate
            ? this.#requestWithAuthRetry(request, 'blobInitiateUpload')
            : this.#request(request);
        return result.then((ref) => {
            if (options?.immediate) {
                this.#emitClientEvent('blobUploadCompleted', { ref });
            }
            void this.#emitOperationalState();
            return ref;
        });
    }
    async retrieveBlob(ref) {
        assertSyncularBlobPayloadLimit({
            operation: 'retrieve',
            size: ref.size,
            limits: this.#blobLimits,
            refHash: ref.hash,
            diagnostics: (event) => this.#emitDiagnostic(event),
        });
        const wasLocal = this.#hasDiagnosticListeners()
            ? await this.#request({
                type: 'isBlobLocal',
                hash: ref.hash,
            }).catch(() => undefined)
            : undefined;
        try {
            const bytes = await this.#requestWithAuthRetry({ type: 'retrieveBlob', ref }, 'blobGetDownloadUrl');
            if (wasLocal !== undefined) {
                this.#emitDiagnostic({
                    level: 'info',
                    source: 'blob',
                    code: wasLocal ? 'blob.cache_hit' : 'blob.cache_miss',
                    message: wasLocal
                        ? 'Syncular blob served from local cache'
                        : 'Syncular blob fetched after local cache miss',
                    details: {
                        hash: ref.hash,
                        size: ref.size,
                        mimeType: ref.mimeType,
                        encrypted: ref.encrypted === true,
                        ...(ref.keyId ? { keyId: ref.keyId } : {}),
                    },
                });
            }
            return bytes;
        }
        catch (error) {
            if (this.#hasDiagnosticListeners()) {
                this.#emitDiagnostic({
                    level: 'warn',
                    source: 'blob',
                    code: 'blob.download_failed',
                    message: `Syncular blob download failed: ${errorMessage(error)}`,
                    details: {
                        hash: ref.hash,
                        size: ref.size,
                        mimeType: ref.mimeType,
                        encrypted: ref.encrypted === true,
                        ...(ref.keyId ? { keyId: ref.keyId } : {}),
                    },
                });
            }
            throw error;
        }
    }
    async isBlobLocal(hash) {
        const local = await this.#request({ type: 'isBlobLocal', hash });
        if (this.#hasDiagnosticListeners()) {
            this.#emitDiagnostic({
                level: 'debug',
                source: 'blob',
                code: local ? 'blob.cache_hit' : 'blob.cache_miss',
                message: local
                    ? 'Syncular blob is available in local cache'
                    : 'Syncular blob is not available in local cache',
                details: { hash },
            });
        }
        return local;
    }
    async processBlobUploadQueue(options) {
        const observeBlobEvents = this.#hasClientEventListeners('blobUploadCompleted') ||
            this.#hasClientEventListeners('blobUploadFailed') ||
            this.#hasDiagnosticListeners();
        const before = observeBlobEvents
            ? await this.#readBlobOutboxRows().catch(() => [])
            : [];
        const result = await this.#requestWithAuthRetry({ type: 'processBlobUploadQueue', options }, 'blobInitiateUpload');
        if (observeBlobEvents) {
            const after = await this.#readBlobOutboxRows().catch(() => []);
            this.#emitBlobUploadEvents(before, after);
        }
        if (this.#hasDiagnosticListeners()) {
            this.#emitDiagnostic({
                level: result.failed > 0 ? 'warn' : 'info',
                source: 'blob',
                code: 'blob.upload_queue_processed',
                message: 'Syncular blob upload queue processed',
                details: result,
            });
        }
        void this.#emitOperationalState();
        return result;
    }
    async blobUploadQueueStats() {
        const stats = await this.#readBlobUploadStats();
        this.#lastBlobUploadStats = stats;
        return stats;
    }
    blobCacheStats() {
        return this.#request({ type: 'blobCacheStats' });
    }
    async pruneBlobCache(maxBytes) {
        const prunedBytes = await this.#request({
            type: 'pruneBlobCache',
            maxBytes,
        });
        if (this.#hasDiagnosticListeners()) {
            this.#emitDiagnostic({
                level: 'info',
                source: 'blob',
                code: 'blob.cache_pruned',
                message: 'Syncular blob cache pruned',
                details: {
                    prunedBytes,
                    ...(maxBytes !== undefined ? { maxBytes } : {}),
                },
            });
        }
        return prunedBytes;
    }
    async clearBlobCache() {
        await this.#request({ type: 'clearBlobCache' });
        if (this.#hasDiagnosticListeners()) {
            this.#emitDiagnostic({
                level: 'info',
                source: 'blob',
                code: 'blob.cache_cleared',
                message: 'Syncular blob cache cleared',
            });
        }
    }
    compactStorage(options = {}) {
        return this.#request({ type: 'compactStorage', options });
    }
    generatedSchemaState() {
        return this.#request({ type: 'generatedSchemaState' });
    }
    localHealthCheck() {
        return this.#request({ type: 'localHealthCheck' });
    }
    exportLocalSupportBundle() {
        return this.#request({ type: 'exportLocalSupportBundle' });
    }
    importLocalSupportBundle(bundle) {
        return this.#request({
            type: 'importLocalSupportBundle',
            bundleJson: typeof bundle === 'string' ? bundle : JSON.stringify(bundle),
        });
    }
    async repairLocalHealth(request) {
        const message = {
            type: 'repairLocalHealth',
            request: {
                action: request.action,
                subscriptionIds: [...(request.subscriptionIds ?? [])],
                tables: [...(request.tables ?? [])],
            },
        };
        const result = request.action === 'clearOrphanedSyncedRows'
            ? await this.#requestAndDrain(message)
            : await this.#request(message);
        if (request.action === 'clearOrphanedSyncedRows') {
            this.#emitLifecycleChanged();
        }
        return result;
    }
    async resetLocalSyncState(request = {}) {
        const normalized = {
            subscriptionIds: [...(request.subscriptionIds ?? [])],
            clearSyncedRows: request.clearSyncedRows === true,
        };
        const result = await this.#requestAndDrain({
            type: 'resetLocalSyncState',
            request: normalized,
        });
        this.#lastBootstrap = undefined;
        this.#emitLifecycleChanged();
        return result;
    }
    buildYjsTextUpdate(args) {
        return this.#request({ type: 'buildYjsTextUpdate', args });
    }
    applyYjsTextUpdates(args) {
        return this.#request({ type: 'applyYjsTextUpdates', args });
    }
    applyYjsEnvelopeToPayload(args) {
        return this.#request({ type: 'applyYjsEnvelopeToPayload', args });
    }
    openCrdtField(request) {
        return this.#request({ type: 'openCrdtField', request });
    }
    applyCrdtFieldText(request) {
        return this.#requestAndDrain({ type: 'applyCrdtFieldText', request });
    }
    applyCrdtFieldYjsUpdate(request) {
        return this.#requestAndDrain({
            type: 'applyCrdtFieldYjsUpdate',
            request,
        });
    }
    materializeCrdtField(request) {
        return this.#request({ type: 'materializeCrdtField', request });
    }
    crdtDocumentSnapshot(request) {
        return this.#request({ type: 'crdtDocumentSnapshot', request });
    }
    crdtUpdateLog(request) {
        return this.#request({ type: 'crdtUpdateLog', request });
    }
    snapshotCrdtFieldStateVector(request) {
        return this.#request({ type: 'snapshotCrdtFieldStateVector', request });
    }
    compactCrdtField(request) {
        return this.#requestAndDrain({ type: 'compactCrdtField', request });
    }
    encryptionHelper(method, args = {}) {
        return this.#request({ type: 'encryptionHelper', method, args });
    }
    runtimeInfo() {
        return this.#request({ type: 'runtimeInfo' }).then((info) => ({
            ...info,
            storageFallback: this.#storageFallback,
        }));
    }
    connectionState() {
        return {
            closed: this.#closed,
            pendingRequests: this.#pending.size,
            realtime: this.#realtimeState,
            ...(this.#storageFallback
                ? { storageFallback: this.#storageFallback }
                : {}),
            ...(this.#lastDiagnostic ? { lastDiagnostic: this.#lastDiagnostic } : {}),
            ...(this.#lastError ? { lastError: this.#lastError } : {}),
        };
    }
    lifecycleState() {
        return this.#computeLifecycleState();
    }
    #computeLifecycleState() {
        const outbox = this.#lastOutboxStats;
        const conflicts = this.#lastConflictStats;
        const blobUploads = this.#lastBlobUploadStats;
        const bootstrap = this.#lastBootstrap
            ? {
                complete: this.#lastBootstrap.complete,
                criticalReady: this.#lastBootstrap.criticalReady,
                interactiveReady: this.#lastBootstrap.interactiveReady,
                isBootstrapping: this.#lastBootstrap.isBootstrapping,
                progressPercent: this.#lastBootstrap.progressPercent,
            }
            : undefined;
        const hasFailedOutbox = (outbox?.failed ?? 0) > 0;
        const hasConflicts = (conflicts?.unresolved ?? 0) > 0;
        const hasFailedBlobUploads = (blobUploads?.failed ?? 0) > 0;
        const authRequired = this.#authRequired || this.#lastError?.code === 'sync.auth_required';
        const offline = this.#networkOnline === false;
        const pendingSyncRequests = this.#pendingSyncRequestCount();
        const phase = this.#closed
            ? 'closed'
            : authRequired
                ? 'authRequired'
                : offline
                    ? 'offline'
                    : this.#recoveryRequired || bootstrap?.isBootstrapping === true
                        ? 'recovering'
                        : pendingSyncRequests > 0
                            ? 'syncing'
                            : this.#realtimeState === 'connecting'
                                ? 'connecting'
                                : hasFailedOutbox || hasConflicts || hasFailedBlobUploads
                                    ? 'degraded'
                                    : bootstrap?.complete === true
                                        ? 'complete'
                                        : 'offline';
        return {
            phase,
            realtime: this.#realtimeState,
            online: this.#networkOnline ?? this.#realtimeState === 'connected',
            requiresAction: authRequired || hasFailedOutbox || hasConflicts || hasFailedBlobUploads,
            pendingRequests: this.#pending.size,
            ...(bootstrap ? { bootstrap } : {}),
            ...(outbox ? { outbox } : {}),
            ...(conflicts ? { conflicts } : {}),
            ...(blobUploads ? { blobUploads } : {}),
            ...(this.#lastDiagnostic ? { lastDiagnostic: this.#lastDiagnostic } : {}),
            ...(this.#lastError ? { lastError: this.#lastError } : {}),
        };
    }
    async diagnosticSnapshot() {
        const [runtime, transportStats] = await Promise.all([
            this.runtimeInfo(),
            this.transportStats().catch(() => undefined),
        ]);
        const outboxStats = this.#lastOutboxStats ??
            (await this.#readOutboxStats().catch(() => undefined));
        if (outboxStats)
            this.#lastOutboxStats = outboxStats;
        const conflictStats = this.#lastConflictStats ??
            (await this.#readConflictStats().catch(() => undefined));
        if (conflictStats)
            this.#lastConflictStats = conflictStats;
        const blobUploadStats = this.#lastBlobUploadStats ??
            (await this.blobUploadQueueStats().catch(() => undefined));
        return {
            generatedAt: Date.now(),
            runtime,
            connection: this.connectionState(),
            subscriptions: summarizeSyncularDiagnosticSubscriptions(this.#subscriptions, this.#lastBootstrap),
            recentDiagnostics: [...this.#recentDiagnostics],
            recentSyncTimings: [...this.#recentSyncTimings],
            ...(this.#lastBootstrap ? { bootstrap: this.#lastBootstrap } : {}),
            ...(transportStats ? { transportStats } : {}),
            ...(outboxStats ? { outboxStats } : {}),
            ...(conflictStats ? { conflictStats } : {}),
            ...(blobUploadStats ? { blobUploadStats } : {}),
        };
    }
    setStorageFallback(fallback) {
        this.#storageFallback = fallback;
        this.#emitDiagnostic({
            level: 'warn',
            source: 'storage',
            code: 'storage.fallback',
            message: `Syncular storage fell back from ${fallback.from} to ${fallback.to}`,
            details: {
                from: fallback.from,
                to: fallback.to,
                reason: fallback.reason,
            },
        });
    }
    addDiagnosticListener(listener) {
        this.#diagnosticListeners.add(listener);
        return () => {
            this.#diagnosticListeners.delete(listener);
        };
    }
    addEventListener(event, listener) {
        const listeners = this.#eventListeners.get(event) ?? new Set();
        listeners.add(listener);
        this.#eventListeners.set(event, listeners);
        return () => {
            listeners.delete(listener);
            if (listeners.size === 0)
                this.#eventListeners.delete(event);
        };
    }
    addRowsChangedListener(listener) {
        this.#rowsChangedListeners.add(listener);
        return () => {
            this.#rowsChangedListeners.delete(listener);
        };
    }
    getPresence(scopeKey) {
        return (this.#presenceByScopeKey.get(scopeKey) ??
            []);
    }
    joinPresence(scopeKey, metadata) {
        this.#joinedPresence.set(scopeKey, metadata);
        void this.#request({
            type: 'sendPresence',
            action: 'join',
            scopeKey,
            ...(metadata === undefined ? {} : { metadata }),
        }).catch((error) => {
            this.#emitDiagnostic({
                level: 'warn',
                source: 'realtime',
                code: 'realtime.presence_join_failed',
                message: `Syncular presence join failed: ${errorMessage(error)}`,
                details: { scopeKey },
            });
        });
        this.#applyPresenceEvent({
            action: 'join',
            scopeKey,
            clientId: this.#config?.clientId ?? '',
            actorId: this.#config?.actorId ?? '',
            metadata,
        });
    }
    leavePresence(scopeKey) {
        this.#joinedPresence.delete(scopeKey);
        void this.#request({
            type: 'sendPresence',
            action: 'leave',
            scopeKey,
        }).catch((error) => {
            this.#emitDiagnostic({
                level: 'warn',
                source: 'realtime',
                code: 'realtime.presence_leave_failed',
                message: `Syncular presence leave failed: ${errorMessage(error)}`,
                details: { scopeKey },
            });
        });
        this.#applyPresenceEvent({
            action: 'leave',
            scopeKey,
            clientId: this.#config?.clientId ?? '',
            actorId: this.#config?.actorId ?? '',
        });
    }
    updatePresenceMetadata(scopeKey, metadata) {
        if (this.#joinedPresence.has(scopeKey)) {
            this.#joinedPresence.set(scopeKey, metadata);
        }
        void this.#request({
            type: 'sendPresence',
            action: 'update',
            scopeKey,
            metadata,
        }).catch((error) => {
            this.#emitDiagnostic({
                level: 'warn',
                source: 'realtime',
                code: 'realtime.presence_update_failed',
                message: `Syncular presence update failed: ${errorMessage(error)}`,
                details: { scopeKey },
            });
        });
        this.#applyPresenceEvent({
            action: 'update',
            scopeKey,
            clientId: this.#config?.clientId ?? '',
            actorId: this.#config?.actorId ?? '',
            metadata,
        });
    }
    addPresenceListener(listener) {
        return this.addEventListener('presenceChanged', listener);
    }
    addLiveQueryListener(queryId, listener) {
        this.#liveListeners.set(queryId, listener);
    }
    removeLiveQueryListener(queryId) {
        this.#liveListeners.delete(queryId);
    }
    async close() {
        if (this.#closed)
            return;
        this.#closed = true;
        this.#realtimeState = 'disconnected';
        this.#unsubscribeNetwork?.();
        this.#unsubscribeNetwork = undefined;
        this.#emitLifecycleChanged();
        try {
            await this.#request({ type: 'close' });
        }
        finally {
            this.#flushRowsChanged();
            this.#rejectAll(createSyncularWorkerErrorPayload('worker.closed', 'Syncular worker client closed'));
            if (this.ownsWorker)
                this.worker.terminate();
        }
    }
    async #requestAndDrain(request) {
        const value = await this.#request(request);
        await this.#emitLiveEvents();
        if (shouldEmitOperationalState(request.type)) {
            void this.#emitOperationalState();
        }
        return value;
    }
    async #syncWithAuthRetry(request, options = { refreshAuthHeaders: true }) {
        const config = this.#config;
        if (!config) {
            throw new Error('Syncular worker client must be opened before sync');
        }
        assertSyncularRemoteMode(config, request.type);
        if (options.refreshAuthHeaders) {
            await this.#refreshAuthHeaders({ restartRealtime: false });
        }
        try {
            const result = await this.#requestAndDrain(request);
            this.#emitBootstrapChanged(result);
            return result;
        }
        catch (error) {
            const shouldRetry = await this.#resolveAuthRetry(error, 'sync');
            if (!shouldRetry)
                throw error;
            await this.#refreshAuthHeaders({ restartRealtime: false });
            const result = await this.#requestAndDrain(request);
            this.#emitBootstrapChanged(result);
            return result;
        }
    }
    async #requestWithAuthRetry(request, operation) {
        await this.#refreshAuthHeaders();
        try {
            return await this.#request(request);
        }
        catch (error) {
            const shouldRetry = await this.#resolveAuthRetry(error, operation);
            if (!shouldRetry)
                throw error;
            await this.#refreshAuthHeaders();
            return this.#request(request);
        }
    }
    async #refreshAuthHeaders(options = { restartRealtime: true }) {
        if (!this.#getHeaders)
            return;
        const headers = await this.#getHeaders();
        this.#emitDiagnostic({
            level: 'debug',
            source: 'auth',
            code: 'auth.headers_refreshed',
            message: 'Syncular auth headers refreshed',
            details: { headerCount: Object.keys(headers).length },
        });
        await this.#setAuthHeaders(headers, options);
    }
    async #restartRealtime() {
        const realtimeOptions = this.#realtimeOptions;
        const config = this.#config;
        if (!realtimeOptions || !config || this.#closed)
            return;
        await this.#request({
            type: 'startRealtime',
            options: await resolveRealtimeWorkerOptions(realtimeOptions, config),
        });
    }
    async #resolveAuthRetry(error, operation) {
        const status = authStatusFromError(error);
        const lifecycle = this.#authLifecycle;
        if (!status || !lifecycle)
            return false;
        const context = { operation, status };
        this.#emitDiagnostic({
            level: 'warn',
            source: 'auth',
            code: 'auth.expired',
            message: `Syncular auth expired during ${operation}`,
            details: context,
        });
        await lifecycle.onAuthExpired?.(context);
        const refreshResult = await this.#refreshAuthSingleFlight(context);
        this.#emitDiagnostic({
            level: refreshResult ? 'info' : 'warn',
            source: 'auth',
            code: refreshResult ? 'auth.refresh_succeeded' : 'auth.refresh_failed',
            message: refreshResult
                ? 'Syncular auth refresh succeeded'
                : 'Syncular auth refresh did not produce fresh credentials',
            details: context,
        });
        if (lifecycle.retryWithFreshToken) {
            return Boolean(await lifecycle.retryWithFreshToken({ ...context, refreshResult }));
        }
        return refreshResult;
    }
    #refreshAuthSingleFlight(context) {
        const refreshToken = this.#authLifecycle?.refreshToken;
        if (!refreshToken)
            return Promise.resolve(false);
        this.#authRefreshInFlight ??= Promise.resolve(refreshToken(context))
            .then(Boolean)
            .finally(() => {
            this.#authRefreshInFlight = undefined;
        });
        return this.#authRefreshInFlight;
    }
    async #emitLiveEvents() {
        const events = await this.drainLiveQueryEvents();
        for (const event of events) {
            this.#liveListeners.get(event.queryId)?.(event);
        }
    }
    #request(request) {
        if (this.#closed && request.type !== 'close') {
            return Promise.reject(new SyncularWorkerError(createSyncularWorkerErrorPayload('worker.closed', 'Syncular worker client is closed')));
        }
        const id = this.#nextId++;
        const message = {
            ...request,
            id,
            protocolVersion: SYNCULAR_WORKER_PROTOCOL_VERSION,
        };
        return new Promise((resolve, reject) => {
            const requestTimeoutMs = this.#timeoutMsForRequest(request.type);
            const timeout = requestTimeoutMs > 0
                ? setTimeout(() => {
                    this.#pending.delete(id);
                    this.#sendCancel(id);
                    this.#emitDiagnostic({
                        level: 'warn',
                        source: 'worker',
                        code: 'worker.request_timeout',
                        message: `Syncular worker request ${request.type} timed out`,
                        details: {
                            requestId: id,
                            requestType: request.type,
                            timeoutMs: requestTimeoutMs,
                        },
                    });
                    const error = new SyncularWorkerError(createSyncularWorkerErrorPayload('worker.request_timeout', `Syncular worker request ${request.type} timed out after ${requestTimeoutMs}ms`, { details: { requestId: id, requestType: request.type } }));
                    this.#lastError = {
                        message: error.message,
                        code: error.code,
                    };
                    this.#emitLifecycleChanged();
                    reject(error);
                }, requestTimeoutMs)
                : undefined;
            this.#pending.set(id, {
                type: request.type,
                timeout,
                resolve: (value) => resolve(value),
                reject,
            });
            this.#emitLifecycleChanged();
            this.worker.postMessage(message);
        });
    }
    #timeoutMsForRequest(type) {
        return syncularWorkerRequestTimeoutMs(this.#requestTimeouts, type);
    }
    #handleWorkerMessage(message) {
        if (message.protocolVersion !== SYNCULAR_WORKER_PROTOCOL_VERSION) {
            this.#rejectAll(createSyncularWorkerErrorPayload('worker.protocol_mismatch', `Unsupported Syncular worker protocol ${message.protocolVersion}`, { details: { supported: SYNCULAR_WORKER_PROTOCOL_VERSION } }));
            return;
        }
        if (isWorkerEvent(message)) {
            this.#handleWorkerEvent(message);
            return;
        }
        this.#handleResponse(message);
    }
    #handleResponse(response) {
        const pending = this.#pending.get(response.id);
        if (!pending)
            return;
        this.#pending.delete(response.id);
        if (pending.timeout)
            clearTimeout(pending.timeout);
        if (response.ok) {
            pending.resolve(response.value);
        }
        else {
            const error = new SyncularWorkerError(response.error);
            this.#lastError = {
                message: error.message,
                code: error.code,
            };
            pending.reject(error);
        }
        this.#emitLifecycleChanged();
    }
    #handleWorkerEvent(event) {
        if (event.type === 'liveQueryEvents') {
            for (const liveEvent of event.events) {
                this.#liveListeners.get(liveEvent.queryId)?.(liveEvent);
            }
            return;
        }
        if (event.type === 'rowsChanged') {
            this.#emitRowsChanged({
                source: event.source,
                changedTables: event.changedTables,
                changedRows: event.changedRows,
                changedRowsTruncated: event.changedRowsTruncated,
            });
            return;
        }
        if (event.type === 'bootstrapChanged') {
            this.#lastBootstrap = event.bootstrap;
            if (event.bootstrap.complete) {
                this.#recoveryRequired = false;
                this.#authRequired = false;
                this.#lastError = undefined;
            }
            this.#emitClientEvent('bootstrapChanged', event.bootstrap);
            this.#emitLifecycleChanged();
            return;
        }
        if (event.type === 'realtimeState') {
            this.#realtimeState = event.state;
            this.#emitDiagnostic({
                level: 'info',
                source: 'realtime',
                code: 'realtime.state',
                message: `Syncular realtime is ${event.state}`,
                details: { state: event.state },
            });
            if (event.state === 'connected') {
                this.#rejoinPresence();
            }
            this.#emitLifecycleChanged();
            return;
        }
        if (event.type === 'presenceEvent') {
            this.#applyPresenceEvent(event);
            return;
        }
        this.#emitDiagnostic(event.event);
    }
    #emitClientEvent(event, payload) {
        const listeners = this.#eventListeners.get(event);
        if (!listeners || listeners.size === 0)
            return;
        for (const listener of listeners) {
            try {
                listener(payload);
            }
            catch {
                // Client event listeners must never break sync control flow.
            }
        }
    }
    #emitLifecycleChanged() {
        const state = this.#computeLifecycleState();
        if (this.#lastLifecycleState && sameJson(this.#lastLifecycleState, state)) {
            return;
        }
        this.#lastLifecycleState = state;
        this.#emitClientEvent('lifecycleChanged', state);
    }
    #pendingSyncRequestCount() {
        let count = 0;
        for (const pending of this.#pending.values()) {
            if (pending.type === 'syncOnce' ||
                pending.type === 'syncPull' ||
                pending.type === 'syncPush') {
                count += 1;
            }
        }
        return count;
    }
    #subscribeNetworkEvents() {
        const network = this.#network;
        if (!network?.addEventListener || !network.removeEventListener)
            return;
        const refresh = () => {
            const next = network.isOnline();
            if (next === this.#networkOnline)
                return;
            this.#networkOnline = next;
            this.#emitLifecycleChanged();
        };
        network.addEventListener('online', refresh);
        network.addEventListener('offline', refresh);
        return () => {
            network.removeEventListener?.('online', refresh);
            network.removeEventListener?.('offline', refresh);
        };
    }
    #hasClientEventListeners(event) {
        return (this.#eventListeners.get(event)?.size ?? 0) > 0;
    }
    #hasDiagnosticListeners() {
        return this.#diagnosticListeners.size > 0;
    }
    #emitRowsChanged(event) {
        if (this.#rowsChangedDebounceMs === false ||
            this.#rowsChangedDebounceMs <= 0) {
            this.#deliverRowsChanged(event);
            return;
        }
        if (!this.#pendingRowsChanged) {
            this.#pendingRowsChanged = {
                source: event.source,
                changedTables: new Set(event.changedTables),
                changedRows: [...event.changedRows],
                changedRowsTruncated: event.changedRowsTruncated === true,
            };
        }
        else {
            const pending = this.#pendingRowsChanged;
            pending.source =
                pending.source === event.source ? pending.source : 'mixed';
            for (const table of event.changedTables)
                pending.changedTables.add(table);
            pending.changedRows.push(...event.changedRows);
            pending.changedRowsTruncated ||= event.changedRowsTruncated === true;
        }
        if (this.#rowsChangedDebounceTimer)
            return;
        this.#rowsChangedDebounceTimer = setTimeout(() => {
            this.#rowsChangedDebounceTimer = undefined;
            this.#flushRowsChanged();
        }, this.#rowsChangedDebounceMs);
    }
    #flushRowsChanged() {
        if (this.#rowsChangedDebounceTimer) {
            clearTimeout(this.#rowsChangedDebounceTimer);
            this.#rowsChangedDebounceTimer = undefined;
        }
        const pending = this.#pendingRowsChanged;
        this.#pendingRowsChanged = undefined;
        if (!pending)
            return;
        this.#deliverRowsChanged({
            source: pending.source,
            changedTables: [...pending.changedTables],
            changedRows: pending.changedRows,
            changedRowsTruncated: pending.changedRowsTruncated,
        });
    }
    #deliverRowsChanged(event) {
        for (const listener of this.#rowsChangedListeners) {
            try {
                listener(event);
            }
            catch {
                // Row-change listeners must never break worker event handling.
            }
        }
        this.#emitClientEvent('rowsChanged', event);
    }
    #emitBootstrapChanged(result) {
        this.#lastBootstrap = result.bootstrap;
        this.#recoveryRequired = false;
        this.#authRequired = false;
        this.#lastError = undefined;
        appendSyncularSyncTimings(this.#recentSyncTimings, result.timings);
        this.#emitClientEvent('bootstrapChanged', result.bootstrap);
        this.#emitLifecycleChanged();
    }
    async #emitOperationalState() {
        const observeLifecycle = this.#hasClientEventListeners('lifecycleChanged');
        const observeOutbox = observeLifecycle || this.#hasClientEventListeners('outboxChanged');
        const observeConflicts = observeLifecycle || this.#hasClientEventListeners('conflictsChanged');
        const observeBlobUploads = observeLifecycle || this.#hasClientEventListeners('blobUploadsChanged');
        if (!observeOutbox && !observeConflicts && !observeBlobUploads) {
            return;
        }
        const [outboxStats, conflictStats, blobUploadStats] = await Promise.all([
            observeOutbox
                ? this.#readOutboxStats().catch(() => undefined)
                : Promise.resolve(undefined),
            observeConflicts
                ? this.#readConflictStats().catch(() => undefined)
                : Promise.resolve(undefined),
            observeBlobUploads
                ? this.#readBlobUploadStats().catch(() => undefined)
                : Promise.resolve(undefined),
        ]);
        if (outboxStats && !sameJson(this.#lastOutboxStats, outboxStats)) {
            this.#lastOutboxStats = outboxStats;
            this.#emitClientEvent('outboxChanged', outboxStats);
        }
        if (conflictStats && !sameJson(this.#lastConflictStats, conflictStats)) {
            this.#lastConflictStats = conflictStats;
            this.#emitClientEvent('conflictsChanged', conflictStats);
        }
        if (blobUploadStats &&
            !sameJson(this.#lastBlobUploadStats, blobUploadStats)) {
            this.#lastBlobUploadStats = blobUploadStats;
            this.#emitClientEvent('blobUploadsChanged', blobUploadStats);
        }
        this.#emitLifecycleChanged();
    }
    #readBlobUploadStats() {
        return this.#request({ type: 'blobUploadQueueStats' });
    }
    async #readOutboxStats() {
        const result = await this.#request({
            type: 'executeUnsafeSql',
            sql: 'select status, count(*) as count from sync_outbox_commits group by status',
            params: [],
        });
        const stats = {
            pending: 0,
            sending: 0,
            failed: 0,
            acked: 0,
            total: 0,
        };
        for (const row of result.rows) {
            const count = coerceCount(row.count);
            if (row.status === 'pending')
                stats.pending = count;
            if (row.status === 'sending')
                stats.sending = count;
            if (row.status === 'failed')
                stats.failed = count;
            if (row.status === 'acked')
                stats.acked = count;
            stats.total += count;
        }
        return stats;
    }
    async #readConflictStats() {
        const result = await this.#request({
            type: 'executeUnsafeSql',
            sql: 'select ' +
                'coalesce(sum(case when resolved_at is null then 1 else 0 end), 0) as unresolved, ' +
                'coalesce(sum(case when resolved_at is not null then 1 else 0 end), 0) as resolved, ' +
                'count(*) as total from sync_conflicts',
            params: [],
        });
        const row = result.rows[0];
        return {
            unresolved: coerceCount(row?.unresolved),
            resolved: coerceCount(row?.resolved),
            total: coerceCount(row?.total),
        };
    }
    async #readBlobOutboxRows() {
        const result = await this.#request({
            type: 'executeUnsafeSql',
            sql: 'select hash, size, mime_type, encrypted, key_id, status, error ' +
                'from sync_blob_outbox order by created_at asc',
            params: [],
        });
        return result.rows;
    }
    #emitBlobUploadEvents(beforeRows, afterRows) {
        const after = new Map(afterRows.map((row) => [row.hash, row]));
        for (const before of beforeRows) {
            const next = after.get(before.hash);
            if (!next) {
                const ref = {
                    hash: before.hash,
                    size: coerceCount(before.size),
                    mimeType: before.mime_type,
                    encrypted: before.encrypted === true || before.encrypted === 1,
                    ...(before.key_id ? { keyId: before.key_id } : {}),
                };
                this.#emitClientEvent('blobUploadCompleted', {
                    ref,
                });
                if (this.#hasDiagnosticListeners()) {
                    this.#emitDiagnostic({
                        level: 'info',
                        source: 'blob',
                        code: 'blob.upload_completed',
                        message: 'Syncular blob upload completed',
                        details: ref,
                    });
                }
                continue;
            }
            if (before.status !== 'failed' && next.status === 'failed') {
                const ref = {
                    hash: next.hash,
                    size: coerceCount(next.size),
                    mimeType: next.mime_type,
                    encrypted: next.encrypted === true || next.encrypted === 1,
                    ...(next.key_id ? { keyId: next.key_id } : {}),
                };
                this.#emitClientEvent('blobUploadFailed', {
                    hash: next.hash,
                    error: next.error ?? 'Blob upload failed',
                    ref,
                });
                if (this.#hasDiagnosticListeners()) {
                    this.#emitDiagnostic({
                        level: 'warn',
                        source: 'blob',
                        code: 'blob.upload_failed',
                        message: next.error ?? 'Syncular blob upload failed',
                        details: {
                            ...ref,
                            error: next.error ?? 'Blob upload failed',
                        },
                    });
                }
            }
        }
    }
    #applyPresenceEvent(event) {
        const scopeKey = normalizePresenceScopeKey(event.scopeKey);
        if (!scopeKey)
            return;
        const current = this.#presenceByScopeKey.get(scopeKey) ?? [];
        let next;
        if (event.action === 'snapshot') {
            next = event.entries ?? [];
        }
        else {
            if (!event.clientId || !event.actorId)
                return;
            if (event.action === 'leave') {
                next = current.filter((entry) => entry.clientId !== event.clientId);
            }
            else if (event.action === 'update') {
                const existing = current.find((entry) => entry.clientId === event.clientId);
                if (!existing)
                    return;
                next = current.map((entry) => entry.clientId === event.clientId
                    ? { ...entry, metadata: event.metadata }
                    : entry);
            }
            else {
                const existing = current.find((entry) => entry.clientId === event.clientId);
                next = [
                    ...current.filter((entry) => entry.clientId !== event.clientId),
                    {
                        clientId: event.clientId,
                        actorId: event.actorId,
                        joinedAt: existing?.joinedAt ?? Date.now(),
                        metadata: event.metadata,
                    },
                ];
            }
        }
        if (arePresenceEntriesEqual(current, next))
            return;
        if (next.length === 0) {
            this.#presenceByScopeKey.delete(scopeKey);
        }
        else {
            this.#presenceByScopeKey.set(scopeKey, next);
        }
        this.#emitClientEvent('presenceChanged', {
            scopeKey,
            presence: next,
        });
    }
    #rejoinPresence() {
        if (this.#joinedPresence.size === 0)
            return;
        for (const [scopeKey, metadata] of this.#joinedPresence) {
            void this.#request({
                type: 'sendPresence',
                action: 'join',
                scopeKey,
                ...(metadata === undefined ? {} : { metadata }),
            }).catch((error) => {
                this.#emitDiagnostic({
                    level: 'warn',
                    source: 'realtime',
                    code: 'realtime.presence_rejoin_failed',
                    message: `Syncular presence rejoin failed: ${errorMessage(error)}`,
                    details: { scopeKey },
                });
            });
        }
    }
    #rejectAll(reason) {
        const error = reason instanceof Error ? reason : new SyncularWorkerError(reason);
        this.#lastError = {
            message: error.message,
            ...(error instanceof SyncularWorkerError ? { code: error.code } : {}),
        };
        for (const pending of this.#pending.values()) {
            if (pending.timeout)
                clearTimeout(pending.timeout);
            pending.reject(error);
        }
        this.#pending.clear();
    }
    #sendCancel(requestId) {
        const id = this.#nextId++;
        this.worker.postMessage({
            id,
            protocolVersion: SYNCULAR_WORKER_PROTOCOL_VERSION,
            type: 'cancel',
            requestId,
        });
    }
    #emitDiagnostic(event) {
        const diagnostic = {
            at: event.at ?? Date.now(),
            level: event.level,
            source: event.source,
            code: event.code,
            message: event.message,
            ...(event.syncAttemptId ? { syncAttemptId: event.syncAttemptId } : {}),
            ...(event.traceId ? { traceId: event.traceId } : {}),
            ...(event.spanId ? { spanId: event.spanId } : {}),
            ...(event.clientId ? { clientId: event.clientId } : {}),
            ...(event.subscriptionId ? { subscriptionId: event.subscriptionId } : {}),
            ...(event.table ? { table: event.table } : {}),
            ...(event.rowId ? { rowId: event.rowId } : {}),
            ...(event.cursor !== undefined ? { cursor: event.cursor } : {}),
            ...(event.details ? { details: event.details } : {}),
        };
        if (diagnostic.details?.resyncRequired === true) {
            this.#recoveryRequired = true;
        }
        if (diagnostic.code === 'auth.expired' ||
            diagnostic.code === 'auth.refresh_failed' ||
            diagnostic.code === 'sync.auth_required') {
            this.#authRequired = true;
        }
        if (diagnostic.code === 'auth.refresh_succeeded') {
            this.#authRequired = false;
        }
        this.#lastDiagnostic = diagnostic;
        appendSyncularDiagnosticEvent(this.#recentDiagnostics, diagnostic);
        this.#emitLifecycleChanged();
        if (this.#diagnosticListeners.size === 0)
            return;
        for (const listener of this.#diagnosticListeners) {
            try {
                listener(diagnostic);
            }
            catch {
                // Diagnostics must never break client control flow.
            }
        }
    }
}
function isOpfsOpenFailure(error) {
    if (!(error instanceof SyncularWorkerError))
        return false;
    const message = errorMessage(error).toLowerCase();
    return (message.includes('opfs') ||
        message.includes('sahpool') ||
        message.includes('sync access handle'));
}
function shouldEmitOperationalState(type) {
    return isGeneratedSyncularOperationalStateWorkerRequestType(type);
}
function errorMessage(error) {
    return error instanceof Error ? error.message : String(error);
}
function normalizePresenceScopeKey(scopeKey) {
    const separator = scopeKey.indexOf('::');
    return separator >= 0 ? scopeKey.slice(separator + 2) : scopeKey;
}
function coerceCount(value) {
    const numeric = typeof value === 'number' ? value : Number(value ?? 0);
    return Number.isFinite(numeric) && numeric > 0 ? Math.trunc(numeric) : 0;
}
function sameJson(left, right) {
    return JSON.stringify(left) === JSON.stringify(right);
}
function cloneAuthHeaders(headers) {
    return Object.fromEntries(Object.entries(headers));
}
function serializeRuntimeArtifact(runtime) {
    if (!runtime)
        return undefined;
    return {
        wasmGlueUrl: runtime.wasmGlueUrl == null
            ? undefined
            : runtimeUrlHref(runtime.wasmGlueUrl),
        wasmUrl: runtime.wasmUrl == null ? undefined : runtimeUrlHref(runtime.wasmUrl),
    };
}
function runtimeUrlHref(value) {
    if (value instanceof Request)
        return value.url;
    return value instanceof URL ? value.href : value;
}
function normalizeFieldEncryptionConfig(config) {
    const keys = {};
    for (const [kid, value] of Object.entries(config.keys)) {
        keys[kid] = value instanceof Uint8Array ? bytesToBase64Url(value) : value;
    }
    return { ...config, keys };
}
function normalizeEncryptedCrdtConfig(config) {
    const keys = {};
    for (const [kid, value] of Object.entries(config.keys)) {
        keys[kid] = value instanceof Uint8Array ? bytesToBase64Url(value) : value;
    }
    return { ...config, keys };
}
function normalizeBlobEncryptionConfig(config) {
    const keys = {};
    for (const [kid, value] of Object.entries(config.keys)) {
        keys[kid] = value instanceof Uint8Array ? bytesToBase64Url(value) : value;
    }
    return { ...config, keys };
}
function arePresenceEntriesEqual(left, right) {
    if (left.length !== right.length)
        return false;
    for (let index = 0; index < left.length; index += 1) {
        const a = left[index];
        const b = right[index];
        if (a.clientId !== b.clientId ||
            a.actorId !== b.actorId ||
            a.joinedAt !== b.joinedAt ||
            JSON.stringify(a.metadata ?? {}) !== JSON.stringify(b.metadata ?? {})) {
            return false;
        }
    }
    return true;
}
function bytesToBase64Url(bytes) {
    let binary = '';
    for (const byte of bytes)
        binary += String.fromCharCode(byte);
    return btoa(binary)
        .replace(/\+/g, '-')
        .replace(/\//g, '_')
        .replace(/=+$/g, '');
}
async function resolveRealtimeWorkerOptions(options, config) {
    const params = {
        ...(options.params ?? {}),
        ...((await options.getParams?.({ clientId: config.clientId })) ?? {}),
    };
    return {
        ...(options.wsUrl ? { wsUrl: options.wsUrl } : {}),
        ...(Object.keys(params).length > 0 ? { params } : {}),
        ...(options.initialReconnectDelayMs != null
            ? { initialReconnectDelayMs: options.initialReconnectDelayMs }
            : {}),
        ...(options.maxReconnectDelayMs != null
            ? { maxReconnectDelayMs: options.maxReconnectDelayMs }
            : {}),
        ...(options.reconnectBackoffFactor != null
            ? { reconnectBackoffFactor: options.reconnectBackoffFactor }
            : {}),
        ...(options.heartbeatTimeoutMs != null
            ? { heartbeatTimeoutMs: options.heartbeatTimeoutMs }
            : {}),
    };
}
function assertSyncularRemoteMode(config, operation) {
    if (isSyncularRemoteMode(config))
        return;
    throw new Error(`Syncular ${operation} requires remote mode; current mode is ${config.mode}`);
}
function isWorkerEvent(message) {
    return 'type' in message && !('id' in message);
}
function authStatusFromError(error) {
    const details = error instanceof SyncularWorkerError ? error.details : undefined;
    if (details &&
        typeof details === 'object' &&
        'status' in details &&
        (details.status === 401 || details.status === 403)) {
        return details.status;
    }
    const message = errorMessage(error);
    const match = /\bHTTP (401|403)\b/.exec(message);
    if (!match)
        return undefined;
    return match[1] === '401' ? 401 : 403;
}
//# sourceMappingURL=worker-client.js.map