UNPKG

signalk-server

Version:

An implementation of a [Signal K](http://signalk.org) server for boats.

198 lines (196 loc) 9.2 kB
"use strict"; /* 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;