signalk-server
Version:
An implementation of a [Signal K](http://signalk.org) server for boats.
868 lines (867 loc) • 37.6 kB
JavaScript
;
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 path_metadata_1 = require("@signalk/path-metadata");
const security_1 = require("../security");
const login_rate_limiter_1 = require("../login-rate-limiter");
const server_api_1 = require("@signalk/server-api");
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_2 = 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 incrementIpCount(ipConnectionCounts, ip) {
ipConnectionCounts.set(ip, (ipConnectionCounts.get(ip) ?? 0) + 1);
}
function decrementIpCount(ipConnectionCounts, ip) {
const count = ipConnectionCounts.get(ip);
if (count !== undefined && count > 1) {
ipConnectionCounts.set(ip, count - 1);
}
else {
ipConnectionCounts.delete(ip);
}
}
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;
const sparkIp = spark.request._resolvedIp;
incrementIpCount(ipConnectionCounts, sparkIp);
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.sourcePolicy = spark.query.sourcePolicy || 'preferred';
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];
}
});
});
});
spark.onDisconnects = [
() => spark.backpressureManager?.clear(),
() => decrementIpCount(ipConnectionCounts, 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;
const isWebSocketUpgrade = String(req.headers.upgrade || '').toLowerCase() === 'websocket';
if (isWebSocketUpgrade &&
(ipConnectionCounts.get(ip) ?? 0) >= maxConnectionsPerIp) {
debug('IP %s exceeded max connections (%d)', ip, maxConnectionsPerIp);
const err = Object.assign(new Error(JSON.stringify({
error: 'Too many concurrent websocket connections from same IP address'
})), { statusCode: 429 });
authorized(err);
return;
}
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_2.hasValues)(update)) {
let source = update.$source;
if (!source && update.source) {
source = (0, server_api_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, path_metadata_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_2.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 {
// Per-message sourcePolicy is honoured: subscriptionmanager.subscribe
// attaches its own listener on the chosen bus (filtered or unfiltered)
// for each subscribe call, independent of the connection-wide delta
// listener attached in handleRealtimeConnection. Falling back to the
// spark-level policy keeps the URL-query default working for clients
// that never send a per-message override.
//
// excludeSelf is only meaningful for plugin subscriptions (where
// the server can resolve it to the plugin's id); WebSocket clients
// have no identity to resolve against, so it's logged and ignored.
// Explicit excludeSources is forwarded as-is.
if (msg.excludeSelf) {
debug('WebSocket subscribe ignores excludeSelf: there is no plugin identity to resolve; use excludeSources instead');
}
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, msg.sourcePolicy ?? spark.sourcePolicy, Array.isArray(msg.excludeSources) && msg.excludeSources.length > 0
? msg.excludeSources
: undefined);
}
}
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);
const deltaEvent = spark.sourcePolicy === 'all' ? 'unfilteredDelta' : 'delta';
app.signalk.removeListener(deltaEvent, 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);
const deltaEvent = spark.sourcePolicy === 'all' ? 'unfilteredDelta' : 'delta';
app.signalk.on(deltaEvent, onChange);
spark.onDisconnects.push(() => {
app.signalk.removeListener(deltaEvent, 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, path_metadata_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, undefined, spark.sourcePolicy)
.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