signalk-server
Version:
An implementation of a [Signal K](http://signalk.org) server for boats.
198 lines (196 loc) • 9.2 kB
JavaScript
;
/* eslint-disable @typescript-eslint/no-explicit-any */
/*
* Copyright 2016 Teppo Kurki <teppo.kurki@iki.fi>
*
* 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.
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
const baconjs_1 = __importDefault(require("baconjs"));
const geolib_1 = require("geolib");
const lodash_1 = __importStar(require("lodash"));
const debug_1 = require("./debug");
const streambundle_1 = require("./streambundle");
const debug = (0, debug_1.createDebug)('signalk-server:subscriptionmanager');
class SubscriptionManager {
streambundle;
selfContext;
app;
constructor(app) {
this.streambundle = app.streambundle;
this.selfContext = app.selfContext;
this.app = app;
}
subscribe(command, unsubscribes, errorCallback, callback, user) {
const contextFilter = contextMatcher(this.selfContext, this.app, command, errorCallback);
if (Array.isArray(command.subscribe)) {
handleSubscribeRows(this.app, command.subscribe, unsubscribes, this.streambundle.buses, contextFilter, callback, errorCallback, user);
// listen to new keys and then use the same logic to check if we
// want to subscribe, passing in a map with just that single bus
unsubscribes.push(this.streambundle.keys.onValue((path) => {
const buses = {};
buses[path] = this.streambundle.getBus(path);
handleSubscribeRows(this.app, command.subscribe, unsubscribes, buses, contextFilter, callback, errorCallback, user);
}));
}
}
unsubscribe(msg, unsubscribes) {
if (msg.unsubscribe &&
msg.context === '*' &&
msg.unsubscribe &&
msg.unsubscribe.length === 1 &&
msg.unsubscribe[0].path === '*') {
debug('Unsubscribe all');
unsubscribes.forEach((unsubscribe) => unsubscribe());
// clear unsubscribes
unsubscribes.length = 0;
}
else {
throw new Error(`Only '{"context":"*","unsubscribe":[{"path":"*"}]}' supported, received ${JSON.stringify(msg)}`);
}
}
}
function handleSubscribeRows(app, rows, unsubscribes, buses, filter, callback, errorCallback, user) {
rows.reduce((acc, subscribeRow) => {
if (subscribeRow.path !== undefined) {
handleSubscribeRow(app, subscribeRow, unsubscribes, buses, filter, callback, errorCallback, user);
}
return acc;
}, unsubscribes);
}
function handleSubscribeRow(app, subscribeRow, unsubscribes, buses, filter, callback, errorCallback, user) {
const matcher = pathMatcher(subscribeRow.path);
// iterate over all the buses, checking if we want to subscribe to its values
(0, lodash_1.forOwn)(buses, (bus, key) => {
if (matcher(key)) {
debug('Subscribing to key ' + key);
let filteredBus = bus.filter(filter);
if (subscribeRow.minPeriod) {
if (subscribeRow.policy && subscribeRow.policy !== 'instant') {
errorCallback(`minPeriod assumes policy 'instant', ignoring policy ${subscribeRow.policy}`);
}
debug('minPeriod:' + subscribeRow.minPeriod);
if (key !== '') {
// we can not apply minPeriod for empty path subscriptions
debug('debouncing');
filteredBus = filteredBus.debounceImmediate(subscribeRow.minPeriod);
}
}
else if (subscribeRow.period ||
(subscribeRow.policy && subscribeRow.policy === 'fixed')) {
if (subscribeRow.policy && subscribeRow.policy !== 'fixed') {
errorCallback(`period assumes policy 'fixed', ignoring policy ${subscribeRow.policy}`);
}
else if (key !== '') {
// we can not apply period for empty path subscriptions
const interval = subscribeRow.period || 1000;
filteredBus = filteredBus
.bufferWithTime(interval)
.flatMapLatest((bufferedValues) => {
const uniqueValues = (0, lodash_1.default)(bufferedValues)
.reverse()
.uniqBy((value) => value.context + ':' + value.$source + ':' + value.path)
.value();
return baconjs_1.default.fromArray(uniqueValues);
});
}
}
if (subscribeRow.format && subscribeRow.format !== 'delta') {
errorCallback('Only delta format supported, using it');
}
if (subscribeRow.policy &&
!['instant', 'fixed'].some((s) => s === subscribeRow.policy)) {
errorCallback(`Only 'instant' and 'fixed' policies supported, ignoring policy ${subscribeRow.policy}`);
}
unsubscribes.push(filteredBus.map(streambundle_1.toDelta).onValue(callback));
const latest = app.deltaCache.getCachedDeltas(filter, user, key);
if (latest) {
latest.forEach(callback);
}
}
});
}
function pathMatcher(path = '*') {
const pattern = path.replace('.', '\\.').replace('*', '.*');
const matcher = new RegExp('^' + pattern + '$');
return (aPath) => matcher.test(aPath);
}
function contextMatcher(selfContext, app, subscribeCommand, errorCallback) {
debug('subscribeCommand:' + JSON.stringify(subscribeCommand));
if (subscribeCommand.context) {
if ((0, lodash_1.isString)(subscribeCommand.context)) {
const pattern = subscribeCommand.context
.replace('.', '\\.')
.replace('*', '.*');
const matcher = new RegExp('^' + pattern + '$');
return (normalizedDeltaData) => matcher.test(normalizedDeltaData.context) ||
((subscribeCommand.context === 'vessels.self' ||
subscribeCommand.context === 'self') &&
normalizedDeltaData.context === selfContext);
}
else if ('radius' in subscribeCommand.context) {
if (!(0, lodash_1.get)(subscribeCommand.context, 'radius') ||
!(0, lodash_1.get)(subscribeCommand.context, 'position.latitude') ||
!(0, lodash_1.get)(subscribeCommand.context, 'position.longitude')) {
errorCallback('Please specify a radius and position for relativePosition');
return () => false;
}
return (normalizedDeltaData) => checkPosition(app, subscribeCommand.context, normalizedDeltaData);
}
}
return () => true;
}
function checkPosition(app, origin, normalizedDelta) {
const vessel = (0, lodash_1.get)(app.signalk.root, normalizedDelta.context);
const position = (0, lodash_1.get)(vessel, 'navigation.position');
return (position &&
position.value &&
position.value.latitude &&
position.value.longitude &&
(0, geolib_1.isPointWithinRadius)(position.value, origin.position, origin.radius));
}
module.exports = SubscriptionManager;