import { LogContext } from '@rocicorp/logger';
import UrlPattern from 'url-pattern';
import { assert } from "../../../shared/src/asserts.js";
import { h32 } from "../../../shared/src/hash.js";
import { getOrCreateGauge } from "../observability/metrics.js";
import { RunningState } from "../services/running-state.js";
import { installWebSocketHandoff } from "../types/websocket-handoff.js";
import { getConnectParams } from "../workers/connect-params.js";
export class WorkerDispatcher {
    id = 'worker-dispatcher';
    #lc;
    #state = new RunningState(this.id);
    constructor(lc, taskID, parent, syncers, mutator, changeStreamer) {
        this.#lc = lc;
        function connectParams(req) {
            const { headers, url: u } = req;
            const url = new URL(u ?? '', 'http://unused/');
            const path = parsePath(url);
            if (!path) {
                throw new Error(`Invalid URL: ${u}`);
            }
            const version = Number(path.version);
            if (Number.isNaN(version)) {
                throw new Error(`Invalid version: ${u}`);
            }
            const { params, error } = getConnectParams(version, url, headers);
            if (error !== null) {
                throw new Error(error);
            }
            return params;
        }
        const handlePush = (req) => {
            assert(mutator !== undefined, 'Received a push for a custom mutation but no `push.url` was configured.');
            return { payload: connectParams(req), sender: mutator };
        };
        let maxProtocolVersion = 0;
        getOrCreateGauge('sync', 'max-protocol-version', 'Latest sync protocol version from a connecting client').addCallback(result => {
            if (maxProtocolVersion) {
                result.observe(maxProtocolVersion);
            }
        });
        const handleSync = (req) => {
            assert(syncers.length, 'Received a sync request with no sync workers.');
            const params = connectParams(req);
            const { clientGroupID, protocolVersion } = params;
            maxProtocolVersion = Math.max(maxProtocolVersion, protocolVersion);
            // Include the TaskID when hash-bucketting the client group to the sync
            // worker. This diversifies the distribution of client groups (across
            // workers) for different tasks, so that if one task sheds connections
            // from its most heavily loaded sync worker(s), those client groups will
            // be distributed uniformly across workers on the receiving task(s).
            const syncer = h32(taskID + '/' + clientGroupID) % syncers.length;
            lc.debug?.(`connecting ${clientGroupID} to syncer ${syncer}`);
            return { payload: params, sender: syncers[syncer] };
        };
        const handleChangeStream = (req) => {
            // Note: The change-streamer is generally not dispatched via the main
            //       port, and in particular, should *not* be accessible via that
            //       port in single-node mode. However, this plumbing is maintained
            //       for the purpose of allowing --lazy-startup of the
            //       replication-manager as a possible future feature.
            assert(syncers.length === 0 && mutator === undefined, 'Dispatch to the change-streamer via the main port ' +
                'is only allowed in multi-node mode');
            assert(changeStreamer, 'Received a change-streamer request without a change-streamer worker');
            const url = new URL(req.url ?? '', 'http://unused/');
            const path = parsePath(url);
            if (!path) {
                throw new Error(`Invalid URL: ${req.url}`);
            }
            return {
                payload: path.action,
                sender: changeStreamer,
            };
        };
        // handoff messages from this ZeroDispatcher to the appropriate worker (pool).
        installWebSocketHandoff(lc, request => {
            const { url: u } = request;
            const url = new URL(u ?? '', 'http://unused/');
            const path = parsePath(url);
            if (!path) {
                throw new Error(`Invalid URL: ${u}`);
            }
            switch (path.worker) {
                case 'sync':
                    return handleSync(request);
                case 'replication':
                    return handleChangeStream(request);
                case 'mutate':
                    return handlePush(request);
                default:
                    throw new Error(`Invalid URL: ${u}`);
            }
        }, parent);
    }
    run() {
        const readyStart = Date.now();
        getOrCreateGauge('server', 'uptime', {
            description: 'Cumulative uptime, starting from when requests are served',
            unit: 's',
        }).addCallback(result => result.observe((Date.now() - readyStart) / 1000));
        return this.#state.stopped();
    }
    stop() {
        this.#state.stop(this.#lc);
        return this.#state.stopped();
    }
}
const URL_PATTERN = new UrlPattern('(/:base)/:worker/v:version/:action');
export function parsePath(url) {
    // The match() returns both null and undefined.
    return URL_PATTERN.match(url.pathname) || undefined;
} // The server allows the client to use any /:base/ path to facilitate
// servicing requests on the same domain as the application.
//# sourceMappingURL=worker-dispatcher.js.map