import { LogContext } from '@rocicorp/logger';
import { resolver } from '@rocicorp/resolver';
import {} from 'jose';
import { pid } from 'node:process';
import { MessagePort } from 'node:worker_threads';
import { WebSocketServer } from 'ws';
import { promiseVoid } from "../../../shared/src/resolved-promises.js";
import { ErrorKind } from "../../../zero-protocol/src/error-kind.js";
import { tokenConfigOptions, verifyToken } from "../auth/jwt.js";
import {} from "../config/zero-config.js";
import { ServiceRunner } from "../services/runner.js";
import { DrainCoordinator } from "../services/view-syncer/drain-coordinator.js";
import { Subscription } from "../types/subscription.js";
import { installWebSocketReceiver } from "../types/websocket-handoff.js";
import { Connection, sendError } from "./connection.js";
import { createNotifierFrom, subscribeTo } from "./replicator.js";
import { SyncerWsMessageHandler } from "./syncer-ws-message-handler.js";
import { recordConnectionSuccess, recordConnectionAttempted, setActiveClientGroupsGetter, } from "../server/anonymous-otel-start.js";
function getWebSocketServerOptions(config) {
    const options = {
        noServer: true,
    };
    if (config.websocketCompression) {
        options.perMessageDeflate = true;
        if (config.websocketCompressionOptions) {
            try {
                const compressionOptions = JSON.parse(config.websocketCompressionOptions);
                options.perMessageDeflate = compressionOptions;
            }
            catch (e) {
                throw new Error(`Failed to parse ZERO_WEBSOCKET_COMPRESSION_OPTIONS: ${String(e)}. Expected valid JSON.`);
            }
        }
    }
    return options;
}
/**
 * The Syncer worker receives websocket handoffs for "/sync" connections
 * from the Dispatcher in the main thread, and creates websocket
 * {@link Connection}s with a corresponding {@link ViewSyncer}, {@link Mutagen},
 * and {@link Subscription} to version notifications from the Replicator
 * worker.
 */
export class Syncer {
    id = `syncer-${pid}`;
    #lc;
    #viewSyncers;
    #mutagens;
    #pushers;
    #connections = new Map();
    #drainCoordinator = new DrainCoordinator();
    #parent;
    #wss;
    #stopped = resolver();
    #config;
    constructor(lc, config, viewSyncerFactory, mutagenFactory, pusherFactory, parent) {
        this.#config = config;
        // Relays notifications from the parent thread subscription
        // to ViewSyncers within this thread.
        const notifier = createNotifierFrom(lc, parent);
        subscribeTo(lc, parent);
        this.#lc = lc;
        this.#viewSyncers = new ServiceRunner(lc, id => viewSyncerFactory(id, notifier.subscribe(), this.#drainCoordinator), v => v.keepalive());
        this.#mutagens = new ServiceRunner(lc, mutagenFactory, m => m.hasRefs());
        if (pusherFactory) {
            this.#pushers = new ServiceRunner(lc, pusherFactory, p => p.hasRefs());
        }
        this.#parent = parent;
        this.#wss = new WebSocketServer(getWebSocketServerOptions(config));
        installWebSocketReceiver(lc, this.#wss, this.#createConnection, this.#parent);
        setActiveClientGroupsGetter(() => this.#viewSyncers.size);
    }
    #createConnection = async (ws, params) => {
        this.#lc.debug?.('creating connection', params.clientGroupID, params.clientID);
        recordConnectionAttempted();
        const { clientID, clientGroupID, auth, userID } = params;
        const existing = this.#connections.get(clientID);
        if (existing) {
            this.#lc.debug?.(`client ${clientID} already connected, closing existing connection`);
            existing.close(`replaced by ${params.wsID}`);
        }
        let decodedToken;
        if (auth) {
            const tokenOptions = tokenConfigOptions(this.#config.auth);
            const hasPushOrMutate = this.#config?.push?.url !== undefined ||
                this.#config?.mutate?.url !== undefined;
            const hasGetQueries = this.#config?.getQueries?.url !== undefined;
            // must either have one of the token options set or have custom mutations & queries enabled
            const hasExactlyOneTokenOption = tokenOptions.length === 1;
            const hasCustomEndpoints = hasPushOrMutate && hasGetQueries;
            if (!hasExactlyOneTokenOption && !hasCustomEndpoints) {
                throw new Error('Exactly one of jwk, secret, or jwksUrl must be set in order to verify tokens but actually the following were set: ' +
                    JSON.stringify(tokenOptions) +
                    '. You may also set both ZERO_MUTATE_URL and ZERO_GET_QUERIES_URL to enable custom mutations and queries without passing token verification options.');
            }
            if (tokenOptions.length > 0) {
                try {
                    decodedToken = await verifyToken(this.#config.auth, auth, {
                        subject: userID,
                    });
                    this.#lc.debug?.(`Received auth token ${auth} for clientID ${clientID}, decoded: ${JSON.stringify(decodedToken)}`);
                }
                catch (e) {
                    sendError(this.#lc, ws, {
                        kind: ErrorKind.AuthInvalidated,
                        message: `Failed to decode auth token: ${String(e)}`,
                    }, e);
                    ws.close(3000, 'Failed to decode JWT');
                    return;
                }
            }
            else {
                this.#lc.warn?.(`One of jwk, secret, or jwksUrl is not configured - the \`authorization\` header must be manually verified by the user`);
            }
        }
        else {
            this.#lc.debug?.(`No auth token received for clientID ${clientID}`);
        }
        const mutagen = this.#mutagens.getService(clientGroupID);
        const pusher = this.#pushers?.getService(clientGroupID);
        // a new connection is using the mutagen and pusher. Bump their ref counts.
        mutagen.ref();
        pusher?.ref();
        let connection;
        try {
            connection = new Connection(this.#lc, params, ws, new SyncerWsMessageHandler(this.#lc, params, 
            // auth is an empty string if the user is not authenticated
            auth
                ? {
                    raw: auth,
                    decoded: decodedToken ?? {},
                }
                : undefined, this.#viewSyncers.getService(clientGroupID), mutagen, pusher), () => {
                if (this.#connections.get(clientID) === connection) {
                    this.#connections.delete(clientID);
                }
                // Connection is closed. We can unref the mutagen and pusher.
                // If their ref counts are zero, they will stop themselves and set themselves invalid.
                mutagen.unref();
                pusher?.unref();
            });
        }
        catch (e) {
            mutagen.unref();
            pusher?.unref();
            throw e;
        }
        this.#connections.set(clientID, connection);
        connection.init() && recordConnectionSuccess();
        if (params.initConnectionMsg) {
            this.#lc.debug?.('handling init connection message from sec header', params.clientGroupID, params.clientID);
            await connection.handleInitConnection(JSON.stringify(params.initConnectionMsg));
        }
    };
    run() {
        return this.#stopped.promise;
    }
    /**
     * Graceful shutdown involves shutting down view syncers one at a time, pausing
     * for the duration of view syncer's hydration between each one. This paces the
     * disconnects to avoid creating a backlog of hydrations in the receiving server
     * when the clients reconnect.
     */
    async drain() {
        const start = Date.now();
        this.#lc.info?.(`draining ${this.#viewSyncers.size} view-syncers`);
        this.#drainCoordinator.drainNextIn(0);
        while (this.#viewSyncers.size) {
            await this.#drainCoordinator.forceDrainTimeout;
            // Pick an arbitrary view syncer to force drain.
            for (const vs of this.#viewSyncers.getServices()) {
                this.#lc.debug?.(`draining view-syncer ${vs.id} (forced)`);
                // When this drain or an elective drain completes, the forceDrainTimeout will
                // resolve after the next drain interval.
                void vs.stop();
                break;
            }
        }
        this.#lc.info?.(`finished draining (${Date.now() - start} ms)`);
    }
    stop() {
        this.#wss.close();
        this.#stopped.resolve();
        return promiseVoid;
    }
}
//# sourceMappingURL=syncer.js.map