import { resolveSyncularClientConfig } from './client-config.js';
import { syncularDiagnosticAttemptFields } from './diagnostics.js';
import { classifySyncularError, SyncularClientError, syncularErrorDetails, syncularErrorMessage, syncularErrorStatus, } from './errors.js';
import { dispatchGeneratedSyncularWorkerRequest, generatedSyncularWorkerRequestDiagnosticSource, isGeneratedSyncularAbortableWorkerRequestType, isGeneratedSyncularDiagnosedSuccessWorkerRequestType, } from './generated-bridge.js';
import { createSyncularRuntimeInfo } from './runtime-contract.js';
import { openSyncularRustClient } from './rust-client.js';
import { getSyncularRustRuntimeInfo, getSyncularWasmGlueUrl, getSyncularWasmUrl, } from './wasm-runtime.js';
import { createSyncularWorkerErrorPayload, SYNCULAR_WORKER_PROTOCOL_VERSION, } from './worker-protocol.js';
import { SyncularWorkerRealtimeController } from './worker-realtime.js';
let client;
let openedConfig;
let openedRuntime;
let removeRowsChangedListener;
const canceledRequests = new Set();
const abortControllers = new Map();
const realtime = new SyncularWorkerRealtimeController({
    getClient: requireClient,
    getConfig: () => openedConfig,
    getLocationOrigin: () => typeof self.location?.origin === 'string'
        ? self.location.origin
        : 'http://localhost',
    createWebSocket: (url) => new WebSocket(url),
    postEvent: (event) => self.postMessage(event),
    postDiagnostic,
});
self.onmessage = (event) => {
    void handleRequest(event.data);
};
async function handleRequest(request) {
    if (request.protocolVersion !== SYNCULAR_WORKER_PROTOCOL_VERSION) {
        post({
            id: request.id,
            protocolVersion: SYNCULAR_WORKER_PROTOCOL_VERSION,
            ok: false,
            error: createSyncularWorkerErrorPayload('worker.protocol_mismatch', `Unsupported Syncular worker protocol ${request.protocolVersion}`, { details: { supported: SYNCULAR_WORKER_PROTOCOL_VERSION } }),
        });
        return;
    }
    if (request.type === 'cancel') {
        canceledRequests.add(request.requestId);
        const controller = abortControllers.get(request.requestId);
        controller?.abort();
        if (controller) {
            postDiagnostic({
                level: 'warn',
                source: 'worker',
                code: 'worker.request_aborted',
                message: 'Syncular worker request aborted',
                details: { requestId: request.requestId },
            });
        }
        post({
            id: request.id,
            protocolVersion: request.protocolVersion,
            ok: true,
        });
        return;
    }
    const startedAt = Date.now();
    const abortController = createAbortController(request.type);
    if (abortController) {
        abortControllers.set(request.id, abortController);
    }
    try {
        if (abortController)
            client?.setAbortSignal(abortController.signal);
        const value = await dispatch(request);
        if (canceledRequests.delete(request.id))
            return;
        post({
            id: request.id,
            protocolVersion: request.protocolVersion,
            ok: true,
            value,
        });
        postRequestSuccessDiagnostic(request, value, Date.now() - startedAt);
    }
    catch (err) {
        if (canceledRequests.delete(request.id))
            return;
        const source = requestDiagnosticSource(request.type);
        const resyncRequired = errorRequiresFullRefresh(err);
        const encodedError = encodeWorkerError(err);
        const diagnosticCode = resyncRequired
            ? `${source}.resync_required`
            : encodedError.code.startsWith(`${source}.`)
                ? encodedError.code
                : `${source}.${request.type}.failed`;
        postDiagnostic({
            level: requestDiagnosticLevel(request.type, err),
            source,
            code: diagnosticCode,
            message: resyncRequired
                ? `Syncular worker request ${request.type} requires full resync`
                : `Syncular worker request ${request.type} failed`,
            details: {
                requestType: request.type,
                durationMs: Date.now() - startedAt,
                error: errorMessage(err),
                errorCode: encodedError.code,
                ...(encodedError.category ? { category: encodedError.category } : {}),
                ...(encodedError.retryable != null
                    ? { retryable: encodedError.retryable }
                    : {}),
                ...(encodedError.recommendedAction
                    ? { recommendedAction: encodedError.recommendedAction }
                    : {}),
                ...(resyncRequired ? { resyncRequired: true } : {}),
                ...diagnosticStatus(err),
            },
            ...requestSyncAttemptDiagnosticFields(request),
        });
        post({
            id: request.id,
            protocolVersion: request.protocolVersion,
            ok: false,
            error: encodedError,
        });
    }
    finally {
        abortControllers.delete(request.id);
        if (abortController)
            clearClientAbortSignal();
    }
}
async function dispatch(request) {
    return dispatchGeneratedSyncularWorkerRequest({
        requireClient,
        openClient,
        startRealtime: (options) => {
            realtime.start(options);
            return true;
        },
        stopRealtime: () => {
            realtime.stop();
            return true;
        },
        sendPresence: (action, scopeKey, metadata) => {
            realtime.sendPresence(action, scopeKey, metadata);
            return true;
        },
        runtimeInfo,
        closeClient,
    }, request);
}
async function openClient(request) {
    const config = resolveSyncularClientConfig(request.config);
    realtime.stop();
    detachRowsChangedListener();
    client?.close();
    client = undefined;
    openedConfig = undefined;
    openedRuntime = undefined;
    const runtime = request.runtime;
    client = await openSyncularRustClient({
        config,
        module: runtime?.wasmGlueUrl
            ? loadWorkerWasmGlue(runtime.wasmGlueUrl)
            : undefined,
        wasmGlueUrl: runtime?.wasmGlueUrl,
        wasmUrl: runtime?.wasmUrl,
    });
    attachRowsChangedListener(client);
    openedConfig = config;
    openedRuntime = runtime;
    return true;
}
function closeClient() {
    realtime.stop();
    detachRowsChangedListener();
    client?.close();
    client = undefined;
    openedConfig = undefined;
    openedRuntime = undefined;
    return true;
}
async function runtimeInfo() {
    const wasmUrl = getSyncularWasmUrl();
    const selectedWasmUrl = openedRuntime?.wasmUrl ?? wasmUrl;
    const selectedWasmGlueUrl = openedRuntime?.wasmGlueUrl ?? getSyncularWasmGlueUrl();
    return createSyncularRuntimeInfo({
        storage: openedConfig?.storage,
        workerUrl: typeof self.location?.href === 'string' ? self.location.href : '',
        wasmGlueUrl: selectedWasmGlueUrl,
        wasmUrl: selectedWasmUrl,
        rust: await getSyncularRustRuntimeInfo(openedRuntime?.wasmGlueUrl
            ? loadWorkerWasmGlue(openedRuntime.wasmGlueUrl)
            : undefined, selectedWasmUrl),
    });
}
function loadWorkerWasmGlue(wasmGlueUrl) {
    return import(/* @vite-ignore */ wasmGlueUrl);
}
function requireClient() {
    if (!client) {
        throw createSyncularWorkerErrorPayload('worker.not_open', 'Syncular worker client is not open');
    }
    return client;
}
function attachRowsChangedListener(nextClient) {
    removeRowsChangedListener = nextClient.addRowsChangedListener((event) => {
        post({
            protocolVersion: SYNCULAR_WORKER_PROTOCOL_VERSION,
            type: 'rowsChanged',
            source: event.source,
            changedTables: event.changedTables,
            changedRows: event.changedRows,
            changedRowsTruncated: event.changedRowsTruncated,
        });
    });
}
function detachRowsChangedListener() {
    removeRowsChangedListener?.();
    removeRowsChangedListener = undefined;
}
function post(message) {
    self.postMessage(message);
}
function createAbortController(type) {
    if (!isAbortableRequest(type) || typeof AbortController === 'undefined') {
        return undefined;
    }
    return new AbortController();
}
function isAbortableRequest(type) {
    return isGeneratedSyncularAbortableWorkerRequestType(type);
}
function clearClientAbortSignal() {
    try {
        client?.setAbortSignal(null);
    }
    catch {
        // Best effort after a request completed, failed, or closed the client.
    }
}
function postDiagnostic(event) {
    self.postMessage({
        protocolVersion: SYNCULAR_WORKER_PROTOCOL_VERSION,
        type: 'diagnostic',
        event: {
            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 } : {}),
        },
    });
}
function postRequestSuccessDiagnostic(request, value, durationMs) {
    if (!isDiagnosedSuccessRequest(request.type))
        return;
    const syncRevocation = syncScopeRevocationDetails(value);
    postDiagnostic({
        level: 'info',
        source: requestDiagnosticSource(request.type),
        code: `${requestDiagnosticSource(request.type)}.${request.type}.completed`,
        message: `Syncular worker request ${request.type} completed`,
        ...requestSyncAttemptDiagnosticFields(request),
        details: {
            requestType: request.type,
            durationMs,
            ...requestSuccessDetails(request, value),
        },
    });
    if (syncRevocation &&
        (request.type === 'syncPull' ||
            request.type === 'syncPush' ||
            request.type === 'syncOnce')) {
        postDiagnostic({
            level: 'warn',
            source: 'sync',
            code: 'sync.scope_revoked',
            message: 'Syncular subscription scope revoked',
            ...requestSyncAttemptDiagnosticFields(request),
            details: {
                requestType: request.type,
                ...syncRevocation,
            },
        });
    }
}
function requestSyncAttemptDiagnosticFields(request) {
    if (request.type !== 'syncPull' &&
        request.type !== 'syncPush' &&
        request.type !== 'syncOnce') {
        return {};
    }
    return syncularDiagnosticAttemptFields(request.syncAttempt);
}
function isDiagnosedSuccessRequest(type) {
    return isGeneratedSyncularDiagnosedSuccessWorkerRequestType(type);
}
function requestDiagnosticSource(type) {
    return generatedSyncularWorkerRequestDiagnosticSource(type);
}
function requestDiagnosticLevel(type, error) {
    if (type === 'setAuthHeaders' || syncularErrorStatus(error))
        return 'warn';
    return 'error';
}
function requestSuccessDetails(request, value) {
    switch (request.type) {
        case 'open':
            return {
                storage: openedConfig?.storage,
                clientId: openedConfig?.clientId,
            };
        case 'setAuthHeaders':
            return { headerCount: Object.keys(request.headers).length };
        case 'setFieldEncryption':
            return {
                enabled: request.config != null,
                ruleCount: request.config?.rules.length ?? 0,
            };
        case 'setEncryptedCrdt':
            return {
                enabled: request.config != null,
                keyCount: request.config ? Object.keys(request.config.keys).length : 0,
            };
        case 'setBlobEncryption':
            return {
                enabled: request.config != null,
                keyCount: request.config ? Object.keys(request.config.keys).length : 0,
            };
        case 'syncPull':
        case 'syncPush':
        case 'syncOnce':
            return syncResultDetails(value);
        case 'storeBlob':
            return { immediate: request.options?.immediate === true };
        case 'pruneBlobCache':
            return { maxBytes: request.maxBytes ?? null, prunedBytes: value };
        case 'compactStorage':
            return objectRecord(value);
        case 'localHealthCheck': {
            const report = objectRecord(value);
            const findings = Array.isArray(report.findings) ? report.findings : [];
            return {
                ok: report.ok === true,
                findingCount: findings.length,
            };
        }
        case 'exportLocalSupportBundle': {
            const bundle = objectRecord(value);
            const health = objectRecord(bundle.health);
            const findings = Array.isArray(health.findings) ? health.findings : [];
            return {
                redacted: bundle.redacted === true,
                source: bundle.source,
                findingCount: findings.length,
            };
        }
        case 'importLocalSupportBundle':
            return objectRecord(value);
        case 'repairLocalHealth':
            return objectRecord(value);
        case 'resetLocalSyncState':
            return objectRecord(value);
        case 'processBlobUploadQueue':
            return objectRecord(value);
        default:
            return {};
    }
}
function syncResultDetails(value) {
    const result = objectRecord(value);
    const changedTables = Array.isArray(result.changedTables)
        ? result.changedTables
        : [];
    const changedRows = Array.isArray(result.changedRows)
        ? result.changedRows
        : [];
    return {
        changedTables,
        changedRows,
        changedRowsTruncated: result.changedRowsTruncated === true,
        changedTableCount: changedTables.length,
        changedRowCount: changedRows.length,
        bootstrap: objectRecord(result.bootstrap),
        pushedCommits: typeof result.pushedCommits === 'number' ? result.pushedCommits : 0,
    };
}
function syncScopeRevocationDetails(value) {
    const result = objectRecord(value);
    const subscriptions = Array.isArray(result.subscriptions)
        ? result.subscriptions
        : [];
    const revokedSubscriptionIds = subscriptions
        .filter((subscription) => {
        return (subscription !== null &&
            typeof subscription === 'object' &&
            subscription.status === 'revoked' &&
            typeof subscription.id === 'string');
    })
        .map((subscription) => subscription.id);
    return revokedSubscriptionIds.length > 0
        ? {
            revokedSubscriptionIds,
            revokedSubscriptionCount: revokedSubscriptionIds.length,
        }
        : null;
}
function diagnosticStatus(error) {
    const status = syncularErrorStatus(error);
    return status ? { status } : {};
}
function objectRecord(value) {
    return value && typeof value === 'object'
        ? value
        : {};
}
function errorMessage(error) {
    return syncularErrorMessage(error);
}
function errorRequiresFullRefresh(error) {
    return errorMessage(error).includes('full snapshot resync required');
}
function encodeWorkerError(error) {
    if (isWorkerErrorPayload(error) && !(error instanceof Error))
        return error;
    if (error instanceof SyncularClientError) {
        return {
            code: error.code,
            message: error.message,
            category: error.category,
            retryable: error.retryable,
            recommendedAction: error.recommendedAction,
            name: error.name,
            stack: error.stack,
            details: error.details,
        };
    }
    if (error instanceof Error) {
        const details = syncularErrorDetails(error);
        const classification = classifySyncularError(error, error.message, details);
        if (!classification) {
            return createSyncularWorkerErrorPayload('worker.failed', error.message, {
                name: error.name,
                stack: error.stack,
                details,
            });
        }
        return {
            code: classification.code,
            message: error.message,
            category: classification.category,
            retryable: classification.retryable,
            recommendedAction: classification.recommendedAction,
            name: error.name,
            stack: error.stack,
            details,
        };
    }
    const message = String(error);
    const classification = classifySyncularError(error, message);
    if (!classification) {
        return createSyncularWorkerErrorPayload('worker.failed', message);
    }
    return {
        code: classification.code,
        message,
        category: classification.category,
        retryable: classification.retryable,
        recommendedAction: classification.recommendedAction,
    };
}
function isWorkerErrorPayload(value) {
    return Boolean(value && typeof value === 'object' && 'code' in value && 'message' in value);
}
//# sourceMappingURL=worker-entry.js.map