"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
/*
 * Copyright 2014-2015 Fabian Tollenaar <fabian@starting-point.nl>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0

 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
const cookie_1 = __importDefault(require("cookie"));
const primus_1 = __importDefault(require("primus"));
const ws_1 = __importDefault(require("ws"));
const signalk_schema_1 = require("@signalk/signalk-schema");
const security_1 = require("../security");
const login_rate_limiter_1 = require("../login-rate-limiter");
const requestResponse_1 = require("../requestResponse");
const put_1 = require("../put");
const debug_1 = require("../debug");
const jsonwebtoken_1 = require("jsonwebtoken");
const events_1 = require("../events");
const BackpressureManager_1 = require("../BackpressureManager");
const ports_1 = require("../ports");
const unitpreferences_1 = require("../unitpreferences");
const server_api_1 = require("@signalk/server-api");
const debug = (0, debug_1.createDebug)('signalk-server:interfaces:ws');
const debugConnection = (0, debug_1.createDebug)('signalk-server:interfaces:ws:connections');
function isWsRequestReply(msg) {
    if (!msg || typeof msg !== 'object') {
        return false;
    }
    const candidate = msg;
    const state = candidate.state;
    return (typeof candidate.requestId === 'string' &&
        (state === 'PENDING' || state === 'COMPLETED' || state === null));
}
function normalizeStatusCode(statusCode) {
    return typeof statusCode === 'number' && Number.isFinite(statusCode)
        ? statusCode
        : null;
}
function normalizeMessage(message) {
    return typeof message === 'string' ? message : null;
}
function normalizePercentComplete(percentComplete) {
    return typeof percentComplete === 'number' &&
        Number.isFinite(percentComplete) &&
        percentComplete >= 0 &&
        percentComplete <= 100
        ? percentComplete
        : null;
}
const DEFAULT_MAX_WS_CONNECTIONS_PER_IP = 30;
function wsInterface(app) {
    const maxWsConnectionsPerIp = parseInt(process.env.MAX_WS_CONNECTIONS_PER_IP ??
        String(DEFAULT_MAX_WS_CONNECTIONS_PER_IP), 10);
    const backpressureThresholds = (0, BackpressureManager_1.parseBackpressureThresholds)({
        maxSendBufferSize: app.config.maxSendBufferSize,
        maxSendBufferCheckTime: app.config.maxSendBufferCheckTime
    });
    debug('Backpressure thresholds: enter=%d, exit=%d, max=%d, maxTime=%d', backpressureThresholds.enterThreshold, backpressureThresholds.exitThreshold, backpressureThresholds.maxBufferSize, backpressureThresholds.maxBufferCheckTime);
    let primuses = [];
    const pathSources = {};
    const ipConnectionCounts = new Map();
    const api = {
        mdns: {
            name: app.config.settings.ssl || app.config.isExternalSsl()
                ? '_signalk-wss'
                : '_signalk-ws',
            type: 'tcp',
            port: (0, ports_1.getExternalPort)(app)
        },
        numClients: function () {
            let count = 0;
            primuses.forEach((primus) => primus.forEach(() => {
                count++;
                return true;
            }));
            return count;
        },
        canHandlePut: function (path, source) {
            const sources = pathSources[path];
            return sources !== undefined && (!source || sources[source] !== undefined);
        },
        handlePut: function (requestId, context, path, source, value) {
            return new Promise((resolve, reject) => {
                const sources = pathSources[path];
                if (sources) {
                    let spark;
                    if (source) {
                        spark = sources[source];
                    }
                    else if (Object.keys(sources).length === 1) {
                        spark = Object.values(sources)[0];
                    }
                    else {
                        (0, requestResponse_1.updateRequest)(requestId, 'COMPLETED', {
                            statusCode: 400,
                            message: 'there are multiple sources for the given path, but no source was specified in the request'
                        })
                            .then(resolve)
                            .catch(reject);
                        return;
                    }
                    if (!spark) {
                        reject(new Error('no spark found'));
                        return;
                    }
                    const listener = (msg) => {
                        let parsedMsg = msg;
                        if (typeof parsedMsg === 'string' || Buffer.isBuffer(parsedMsg)) {
                            try {
                                parsedMsg = JSON.parse(String(parsedMsg));
                            }
                            catch (_err) {
                                return;
                            }
                        }
                        if (isWsRequestReply(parsedMsg) &&
                            parsedMsg.requestId === requestId) {
                            const updateOptions = {
                                statusCode: normalizeStatusCode(parsedMsg.statusCode),
                                data: parsedMsg.data ?? null,
                                message: normalizeMessage(parsedMsg.message),
                                percentComplete: normalizePercentComplete(parsedMsg.percentComplete)
                            };
                            (0, requestResponse_1.updateRequest)(requestId, parsedMsg.state, updateOptions)
                                .then((reply) => {
                                if (reply.state !== 'PENDING') {
                                    spark.removeListener('data', listener);
                                }
                            })
                                .catch(() => {
                                console.error(`could not update requestId ${requestId}`);
                            });
                        }
                    };
                    spark.on('data', listener);
                    setTimeout(() => {
                        const request = (0, requestResponse_1.findRequest)((r) => r.requestId === requestId);
                        if (request && request.state === 'PENDING') {
                            spark.removeListener('data', listener);
                            (0, requestResponse_1.updateRequest)(requestId, 'COMPLETED', { statusCode: 504 });
                        }
                    }, 60 * 1000);
                    spark.write({
                        requestId: requestId,
                        context: context,
                        put: [{ path: path, value: value }]
                    });
                    (0, requestResponse_1.updateRequest)(requestId, 'PENDING', { statusCode: 202 })
                        .then(resolve)
                        .catch(reject);
                }
                else {
                    reject(new Error('no source found'));
                }
            });
        },
        start: function () {
            debug('Starting Primus/WS interface');
            const wsPingInterval = app.config.settings.wsPingInterval ?? 30000;
            let baseOptions = {
                transformer: 'websockets',
                pingInterval: false
            };
            if (app.config.settings.wsCompression) {
                baseOptions = {
                    ...baseOptions,
                    compression: true,
                    transport: {
                        perMessageDeflate: { threshold: 0 }
                    }
                };
            }
            const allWsOptions = [
                {
                    ...baseOptions,
                    pathname: '/signalk/v1/stream',
                    isPlayback: false
                },
                {
                    ...baseOptions,
                    pathname: '/signalk/v1/playback',
                    isPlayback: true
                }
            ];
            primuses = allWsOptions.map((primusOptions) => {
                const primus = new primus_1.default(app.server, primusOptions);
                if (wsPingInterval) {
                    const interval = setInterval(() => {
                        primus.forEach((primusSpark) => {
                            const spark = primusSpark;
                            if (spark.wsAlive === false) {
                                debug('heartbeat timeout for spark %s, closing', spark.id);
                                return spark.end(undefined, { reconnect: true });
                            }
                            spark.wsAlive = false;
                            if (spark.socket && spark.socket.readyState === ws_1.default.OPEN) {
                                spark.socket.ping();
                            }
                        });
                    }, wsPingInterval);
                    primus.once('close', () => clearInterval(interval));
                }
                primus.authorize(createPrimusAuthorize(app.securityStrategy.canAuthorizeWS()
                    ? app.securityStrategy.authorizeWS
                    : undefined, ipConnectionCounts, maxWsConnectionsPerIp, app.config.settings.trustProxy));
                primus.on('connection', function (primusSpark) {
                    const spark = primusSpark;
                    let principalId;
                    if (spark.request.skPrincipal) {
                        principalId = spark.request.skPrincipal.identifier;
                    }
                    debugConnection(`${spark.id} connected ${JSON.stringify(spark.query)} ${spark.request._resolvedIp}:${principalId} (ip connections: ${ipConnectionCounts.get(spark.request._resolvedIp) ?? '?'})`);
                    spark.sendMetaDeltas = spark.query.sendMeta === 'all';
                    spark.sentMetaData = {};
                    spark.backpressureManager = new BackpressureManager_1.BackpressureManager({
                        get id() {
                            return spark.id;
                        },
                        getBufferLength() {
                            return spark.request.socket.writableLength;
                        },
                        write(delta) {
                            spark.write(delta);
                        },
                        destroy() {
                            spark.end({
                                errorMessage: 'Server outgoing buffer overflow, terminating connection'
                            });
                        }
                    }, {
                        ...backpressureThresholds,
                        beforeWrite: (delta) => sendMetaData(app, spark, delta)
                    });
                    spark.request.socket.on('drain', () => {
                        spark.backpressureManager.onDrain();
                    });
                    if (wsPingInterval) {
                        spark.wsAlive = true;
                        spark.socket.on('pong', () => {
                            spark.wsAlive = true;
                        });
                    }
                    const selfOnly = isSelfSubscription(spark.query);
                    const subscribeNone = spark.query.subscribe === 'none';
                    const canVerify = app.securityStrategy.canAuthorizeWS();
                    const onChange = (delta) => {
                        if (canVerify) {
                            try {
                                app.securityStrategy.verifyWS(spark.request);
                            }
                            catch (error) {
                                handleVerifyWSError(spark, error);
                                return;
                            }
                        }
                        if (subscribeNone)
                            return;
                        if (selfOnly &&
                            delta.context &&
                            delta.context !== app.selfContext) {
                            return;
                        }
                        const filtered = app.securityStrategy.filterReadDelta(spark.request.skPrincipal, delta);
                        if (filtered === null)
                            return;
                        spark.backpressureManager.send(filtered);
                    };
                    const unsubscribes = [];
                    if (primusOptions.isPlayback) {
                        spark.on('data', () => {
                            console.error('Playback does not support ws upstream messages');
                            spark.end('Playback does not support ws upstream messages');
                        });
                    }
                    else {
                        spark.on('data', function (msg) {
                            let parsedMsg;
                            try {
                                parsedMsg = JSON.parse(String(msg));
                            }
                            catch (e) {
                                debug('Failed to parse message: ' + e.message);
                                return;
                            }
                            // Guard to avoid JSON.stringify on every inbound message
                            // when the debug namespace is disabled — the argument to
                            // debug() would otherwise be eagerly evaluated for each
                            // message. Pattern mirrors tcp.ts:169/181.
                            debug.enabled && debug('<' + JSON.stringify(parsedMsg));
                            try {
                                if (parsedMsg.updates) {
                                    processUpdates(app, pathSources, spark, parsedMsg);
                                }
                                if (parsedMsg.subscribe) {
                                    processSubscribe(app, unsubscribes, spark, parsedMsg);
                                }
                                if (parsedMsg.unsubscribe) {
                                    processUnsubscribe(app, unsubscribes, parsedMsg, onChange, spark);
                                }
                                if (parsedMsg.accessRequest) {
                                    processAccessRequest(app, spark, parsedMsg);
                                }
                                if (parsedMsg.login && app.securityStrategy.supportsLogin()) {
                                    processLoginRequest(app, spark, parsedMsg);
                                }
                                if (parsedMsg.put) {
                                    processPutRequest(app, spark, parsedMsg);
                                }
                                if (parsedMsg.delete) {
                                    processDeleteRequest(app, spark, parsedMsg);
                                }
                                if (parsedMsg.requestId && parsedMsg.query) {
                                    processRequestQuery(spark, parsedMsg);
                                }
                            }
                            catch (e) {
                                console.error(e);
                            }
                        });
                    }
                    spark.on('end', function () {
                        debugConnection(`${spark.id} end ${JSON.stringify(spark.query)} ${spark.request._resolvedIp}:${principalId}`);
                        unsubscribes.forEach((unsubscribe) => unsubscribe());
                        Object.keys(pathSources).forEach((path) => {
                            Object.keys(pathSources[path]).forEach((source) => {
                                if (pathSources[path][source] === spark) {
                                    debug('removing source for %s', path);
                                    delete pathSources[path][source];
                                }
                            });
                        });
                    });
                    const sparkIp = spark.request._resolvedIp;
                    spark.onDisconnects = [
                        () => spark.backpressureManager?.clear(),
                        () => {
                            const count = ipConnectionCounts.get(sparkIp);
                            if (count !== undefined && count > 1) {
                                ipConnectionCounts.set(sparkIp, count - 1);
                            }
                            else {
                                ipConnectionCounts.delete(sparkIp);
                            }
                        }
                    ];
                    if (primusOptions.isPlayback) {
                        if (!spark.query.startTime) {
                            spark.end('startTime is a required query parameter for playback connections');
                        }
                        else {
                            handlePlaybackConnection(app, spark, onChange);
                        }
                    }
                    else {
                        handleRealtimeConnection(app, spark, onChange);
                    }
                });
                primus.on('disconnection', function (primusSpark) {
                    const spark = primusSpark;
                    spark.onDisconnects.forEach((f) => f());
                    debug(spark.id + ' disconnected');
                });
                return primus;
            });
        },
        stop: function () {
            debug('Destroying primuses...');
            primuses.forEach((primus) => primus.destroy({ close: false, timeout: 500, reconnect: false }, () => {
                /* noop */
            }));
        }
    };
    function processRequestQuery(spark, msg) {
        (0, requestResponse_1.queryRequest)(msg.requestId)
            .then((reply) => {
            spark.write(reply);
        })
            .catch(() => {
            spark.write({
                requestId: msg.requestId,
                statusCode: 404
            });
        });
    }
    function processPutRequest(app, spark, msg) {
        (0, put_1.putPath)(app, msg.context || null, msg.put.path, msg.put, spark.request, msg.requestId || null, (reply) => {
            debug('sending put update %j', reply);
            spark.write(reply);
        }).catch((err) => {
            console.error(err);
            spark.write({
                requestId: msg.requestId,
                state: 'COMPLETED',
                statusCode: 502,
                message: err.message
            });
        });
    }
    function processDeleteRequest(app, spark, msg) {
        (0, put_1.deletePath)(app, msg.context || null, msg.delete.path, spark.request, msg.requestId || null, (reply) => {
            debug('sending delete update %j', reply);
            spark.write(reply);
        }).catch((err) => {
            console.error(err);
            spark.write({
                requestId: msg.requestId,
                state: 'COMPLETED',
                statusCode: 502,
                message: err.message
            });
        });
    }
    function processAccessRequest(app, spark, msg) {
        if (spark.skPendingAccessRequest) {
            spark.write({
                requestId: msg.requestId,
                state: 'COMPLETED',
                statusCode: 400,
                message: 'A request has already been submitted'
            });
        }
        else {
            const clientIp = spark.request._resolvedIp;
            (0, security_1.requestAccess)(app, msg, clientIp, (res) => {
                if (res.state === 'COMPLETED') {
                    spark.skPendingAccessRequest = false;
                    if (res.accessRequest && res.accessRequest.token) {
                        spark.request.token = res.accessRequest.token;
                        app.securityStrategy.authorizeWS(spark.request);
                        spark.request.source =
                            'ws.' +
                                spark.request.skPrincipal.identifier.replace(/\./g, '_');
                    }
                }
                spark.write(res);
            })
                .then((res) => {
                if (res.state === 'PENDING') {
                    spark.skPendingAccessRequest = true;
                }
            })
                .catch((err) => {
                console.log(err.stack);
                spark.write({
                    requestId: msg.requestId,
                    state: 'COMPLETED',
                    statusCode: 502,
                    message: err.message
                });
            });
        }
    }
    function processLoginRequest(app, spark, msg) {
        const rateLimiter = app.securityStrategy.loginRateLimiter;
        if (rateLimiter) {
            const { allowed } = rateLimiter.check(spark.request._resolvedIp);
            if (!allowed) {
                spark.write({
                    requestId: msg.requestId,
                    state: 'COMPLETED',
                    statusCode: 429,
                    message: login_rate_limiter_1.LOGIN_RATE_LIMIT_MESSAGE
                });
                return;
            }
        }
        app.securityStrategy
            .login(msg.login.username, msg.login.password)
            .then((reply) => {
            if (reply.token) {
                spark.request.token = reply.token;
                app.securityStrategy.authorizeWS(spark.request);
            }
            spark.write({
                requestId: msg.requestId,
                state: 'COMPLETED',
                statusCode: reply.statusCode,
                login: {
                    timeToLive: reply.timeToLive,
                    token: reply.token
                }
            });
        })
            .catch((err) => {
            console.error(err);
            spark.write({
                requestId: msg.requestId,
                state: 'COMPLETED',
                statusCode: 502,
                message: err.message
            });
        });
    }
    return api;
}
function createPrimusAuthorize(authorizeWS, ipConnectionCounts, maxConnectionsPerIp, trustProxy) {
    return function (primusReq, authorized) {
        const req = primusReq;
        const forwardedFor = req.headers['x-forwarded-for'];
        const firstForwardedIp = trustProxy && trustProxy !== 'false' && typeof forwardedFor === 'string'
            ? forwardedFor.split(',')[0].trim()
            : undefined;
        req._resolvedIp = firstForwardedIp || req.connection.remoteAddress;
        const ip = req._resolvedIp;
        if ((ipConnectionCounts.get(ip) ?? 0) >= maxConnectionsPerIp) {
            debug(`IP ${ip} exceeded max connections (${maxConnectionsPerIp})`);
            const err = Object.assign(new Error(JSON.stringify({
                error: 'Too many concurrent websocket connections from same IP address'
            })), { statusCode: 429 });
            authorized(err);
            return;
        }
        ipConnectionCounts.set(ip, (ipConnectionCounts.get(ip) ?? 0) + 1);
        if (!authorizeWS) {
            authorized();
            return;
        }
        try {
            const cookieHeader = req.headers.cookie;
            if (typeof cookieHeader === 'string') {
                req.cookies = cookie_1.default.parse(cookieHeader);
            }
            authorizeWS(req);
            authorized();
            const identifier = req.skPrincipal?.identifier;
            if (identifier) {
                debug(`authorized username: ${identifier}`);
                req.source = 'ws.' + identifier.replace(/\./g, '_');
            }
        }
        catch (error) {
            if (error instanceof security_1.InvalidTokenError ||
                error instanceof jsonwebtoken_1.JsonWebTokenError ||
                error instanceof jsonwebtoken_1.TokenExpiredError) {
                authorized(error);
            }
            else {
                authorized();
            }
        }
    };
}
function processUpdates(app, pathSources, spark, msg) {
    if (!app.securityStrategy.shouldAllowWrite(spark.request, msg)) {
        debug('security disallowed update');
        app.setProviderError('ws', spark.request.connection.remoteAddress + ' needs authentication');
        return;
    }
    app.handleMessage(spark.request.source || 'ws', msg);
    msg.updates?.forEach((update) => {
        if ((0, server_api_1.hasValues)(update)) {
            let source = update.$source;
            if (!source && update.source) {
                source = (0, signalk_schema_1.getSourceId)(update.source);
            }
            if (source) {
                update.values.forEach((valuePath) => {
                    if (!pathSources[valuePath.path]) {
                        pathSources[valuePath.path] = {};
                    }
                    if (!pathSources[valuePath.path][source] ||
                        pathSources[valuePath.path][source] !== spark) {
                        if (pathSources[valuePath.path][source]) {
                            console.log(`WARNING: got a new ws client for path ${valuePath.path} source ${source}`);
                        }
                        debug('registered spark for source %s path %s = %s', source, valuePath.path, spark.id);
                        pathSources[valuePath.path][source] = spark;
                    }
                });
            }
        }
    });
}
let canonical_meta_contextpath_values = {};
const getContextPathMetaKey = (context, path) => {
    const contextPaths = canonical_meta_contextpath_values[context] ||
        (canonical_meta_contextpath_values[context] = {});
    const result = contextPaths[path] || (contextPaths[path] = `${context}.${path}`);
    return result;
};
setInterval(() => {
    canonical_meta_contextpath_values = {};
}, 30 * 60 * 1000);
function handleValuesMeta(kp) {
    const fullContextPathKey = getContextPathMetaKey(this.context, kp.path);
    if (kp.path && !this.spark.sentMetaData[fullContextPathKey]) {
        const split = kp.path.split('.');
        for (let i = split.length; i > 1; i--) {
            const path = split.slice(0, i).join('.');
            const partialContextPathKey = getContextPathMetaKey(this.context, path);
            if (this.spark.sentMetaData[partialContextPathKey]) {
                break;
            }
            else {
                this.spark.sentMetaData[partialContextPathKey] = true;
                const meta = (0, signalk_schema_1.getMetadata)(partialContextPathKey);
                if (meta) {
                    // Clone and enhance metadata with displayUnits formulas
                    const metaClone = structuredClone(meta);
                    let storedDisplayUnits = metaClone.displayUnits;
                    const username = this.spark.request.skPrincipal?.identifier;
                    if (!storedDisplayUnits?.category && path) {
                        const defaultCategory = (0, unitpreferences_1.getDefaultCategory)(path, metaClone.units, username);
                        if (defaultCategory) {
                            storedDisplayUnits = { category: defaultCategory };
                        }
                    }
                    if (storedDisplayUnits?.category) {
                        const enhanced = (0, unitpreferences_1.resolveDisplayUnits)(storedDisplayUnits, metaClone.units, username);
                        if (enhanced) {
                            metaClone.displayUnits = enhanced;
                        }
                    }
                    this.spark.write({
                        context: this.context,
                        updates: [
                            {
                                timestamp: this.timestamp,
                                meta: [
                                    {
                                        path: path,
                                        value: metaClone
                                    }
                                ]
                            }
                        ]
                    });
                }
            }
        }
    }
}
function handleUpdatesMeta(update) {
    if ((0, server_api_1.hasValues)(update)) {
        this.timestamp = update.timestamp;
        update.values.forEach(handleValuesMeta, this);
    }
}
function sendMetaData(app, spark, delta) {
    if (spark.sendMetaDeltas && delta.updates) {
        const thisContext = {
            context: delta.context || '',
            spark
        };
        delta.updates.forEach(handleUpdatesMeta, thisContext);
    }
}
function processSubscribe(app, unsubscribes, spark, msg) {
    if (Array.isArray(msg.subscribe) &&
        msg.subscribe.length > 0 &&
        msg.subscribe[0].path === 'log') {
        if (!spark.logUnsubscribe) {
            spark.logUnsubscribe = startServerLog(app, spark);
        }
    }
    else {
        app.subscriptionmanager.subscribe(msg, unsubscribes, spark.write.bind(spark), (message) => {
            const filtered = app.securityStrategy.filterReadDelta(spark.request.skPrincipal, message);
            if (!filtered)
                return;
            spark.backpressureManager.send(filtered);
        }, spark.request.skPrincipal);
    }
}
function processUnsubscribe(app, unsubscribes, msg, onChange, spark) {
    try {
        if (Array.isArray(msg.unsubscribe) &&
            msg.unsubscribe.length > 0 &&
            msg.unsubscribe[0].path === 'log') {
            if (spark.logUnsubscribe) {
                spark.logUnsubscribe();
                spark.logUnsubscribe = undefined;
            }
        }
        else {
            app.subscriptionmanager.unsubscribe(msg, unsubscribes);
            app.signalk.removeListener('delta', onChange);
            spark.sentMetaData = {};
        }
    }
    catch (e) {
        console.log(e);
        spark.write(e.message);
        spark.end();
    }
}
const isSelfSubscription = (query) => !query.subscribe || query.subscribe === 'self';
function handleVerifyWSError(spark, error) {
    if (!spark.skPendingAccessRequest) {
        spark.end('{message: "Connection disconnected by security constraint"}', {
            reconnect: true
        });
    }
    const identifier = spark.request.skPrincipal?.identifier ||
        spark.request.connection.remoteAddress ||
        'unknown';
    console.error(`WebSocket security error for ${identifier}: ${error.message}`);
}
function wrapWithVerifyWS(securityStrategy, spark, theFunction) {
    if (!securityStrategy.canAuthorizeWS()) {
        return theFunction;
    }
    return (msg) => {
        try {
            securityStrategy.verifyWS(spark.request);
            theFunction(msg);
        }
        catch (error) {
            handleVerifyWSError(spark, error);
            return;
        }
    };
}
function sendHello(app, helloProps, spark) {
    spark.write({
        ...app.getHello(),
        ...helloProps
    });
}
function handlePlaybackConnection(app, spark, onChange) {
    if (app.historyProvider === undefined) {
        spark.end('No history provider');
        return;
    }
    const startTimeStr = spark.query.startTime;
    if (!startTimeStr) {
        spark.end('startTime is required');
        return;
    }
    const options = {
        startTime: new Date(startTimeStr),
        playbackRate: spark.query.playbackRate || 1
    };
    sendHello(app, options, spark);
    options.subscribe = spark.query.subscribe;
    app.historyProvider.hasAnyData(options, (hasResults) => {
        if (hasResults) {
            spark.onDisconnects.push(app.historyProvider.streamHistory(spark, options, onChange));
            spark.isHistory = true;
        }
        else {
            spark.end('No data found');
        }
    });
}
function handleRealtimeConnection(app, spark, onChange) {
    sendHello(app, {}, spark);
    app.signalk.on('delta', onChange);
    spark.onDisconnects.push(() => {
        app.signalk.removeListener('delta', onChange);
    });
    if (spark.sendMetaDeltas) {
        const onUnitPrefsChanged = (event) => {
            const username = spark.request.skPrincipal?.identifier;
            const ev = event;
            if (ev?.username && ev.username !== username)
                return;
            const allPaths = app.streambundle.getAvailablePaths();
            const meta = allPaths.reduce((acc, path) => {
                const fullPath = 'vessels.self.' + path;
                const pathMeta = (0, signalk_schema_1.getMetadata)(fullPath) || {};
                const storedDU = pathMeta.displayUnits;
                const category = storedDU?.category ||
                    (0, unitpreferences_1.getDefaultCategory)(path, pathMeta.units, username);
                if (category) {
                    const displayUnits = (0, unitpreferences_1.resolveDisplayUnits)({ ...storedDU, category }, pathMeta.units, username);
                    if (displayUnits) {
                        acc.push({ path, value: { ...pathMeta, displayUnits } });
                    }
                }
                return acc;
            }, []);
            if (meta.length > 0) {
                const timestamp = new Date().toISOString();
                spark.write({
                    context: 'vessels.' + app.selfId,
                    updates: [{ timestamp, meta }]
                });
            }
        };
        app.on('unitpreferencesChanged', onUnitPrefsChanged);
        spark.onDisconnects.push(() => {
            app.removeListener('unitpreferencesChanged', onUnitPrefsChanged);
        });
    }
    if (spark.request.query?.sendCachedValues !== 'false') {
        sendLatestDeltas(app, app.deltaCache, app.selfContext, spark);
    }
    if (spark.query.serverevents === 'all') {
        spark.hasServerEvents = true;
        (0, events_1.startServerEvents)(app, spark, wrapWithVerifyWS(app.securityStrategy, spark, spark.write.bind(spark)));
    }
    if (spark.query.events) {
        (0, events_1.startEvents)(app, spark, wrapWithVerifyWS(app.securityStrategy, spark, spark.write.bind(spark)), spark.query.events);
    }
}
function sendLatestDeltas(app, deltaCache, selfContext, spark) {
    let deltaFilter = () => false;
    if (!spark.query.subscribe || spark.query.subscribe === 'self') {
        deltaFilter = (delta) => delta.context === selfContext;
    }
    else if (spark.query.subscribe === 'all') {
        deltaFilter = () => true;
    }
    deltaCache
        .getCachedDeltas(deltaFilter, spark.request.skPrincipal)
        .forEach((delta) => {
        sendMetaData(app, spark, delta);
        spark.write(delta);
    });
}
function startServerLog(app, spark) {
    const onServerLogEvent = wrapWithVerifyWS(app.securityStrategy, spark, spark.write.bind(spark));
    app.on('serverlog', onServerLogEvent);
    spark.onDisconnects.push(() => {
        app.removeListener('serverlog', onServerLogEvent);
    });
    app.logging.getLog().forEach((log) => {
        spark.write({
            type: 'LOG',
            data: log
        });
    });
    return () => {
        app.removeListener('serverlog', onServerLogEvent);
    };
}
module.exports = wsInterface;
//# sourceMappingURL=ws.js.map