UNPKG

signalk-server

Version:

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

242 lines (240 loc) 9.36 kB
"use strict"; /* eslint-disable @typescript-eslint/no-explicit-any */ /* * Copyright 2017 Scott Bender <scott@scottbender.net> * * 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; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); const debug_1 = require("./debug"); const debug = (0, debug_1.createDebug)('signalk-server:deltacache'); const signalk_schema_1 = require("@signalk/signalk-schema"); const lodash_1 = __importStar(require("lodash")); const streambundle_1 = require("./streambundle"); class DeltaCache { cache = {}; lastModifieds = {}; app; defaults; sourceDeltas = {}; cachedContextPaths = {}; constructor(app, streambundle) { this.app = app; streambundle.keys.onValue((key) => { streambundle.getBus(key).onValue(this.onValue.bind(this)); }); // String.split() is heavy enough and called frequently enough // to warrant caching the result. Has a noticeable effect // on throughput of a server going full blast with the n2k // sample data and the memory hit is negligible. The cache // must be pruned, or AIS vessel data will stick forever. // No fancy pruning, just clear & let it recreate. setInterval(() => (this.cachedContextPaths = {}), 5 * 60 * 1000); } getContextAndPathParts(msg) { let result; if (this.cachedContextPaths[msg.context] && (result = this.cachedContextPaths[msg.context][msg.path])) { return result; } let contextAndPathParts = msg.context.split('.'); if (msg.path.length !== 0) { contextAndPathParts = contextAndPathParts.concat(msg.path.split('.')); } if (!this.cachedContextPaths[msg.context]) { this.cachedContextPaths[msg.context] = {}; } this.cachedContextPaths[msg.context][msg.path] = contextAndPathParts; return contextAndPathParts; } onValue(msg) { // debug(`onValue ${JSON.stringify(msg)}`) if (msg.isMeta) { // ignore meta data since it's getting managed by FullSignalK return; } const sourceRef = ensureHasDollarSource(msg); const leaf = getLeafObject(this.cache, this.getContextAndPathParts(msg), true); if (msg.path.length !== 0) { leaf[sourceRef] = msg; } else if (msg.value) { lodash_1.default.keys(msg.value).forEach((key) => { if (!leaf[key]) { leaf[key] = {}; } leaf[key][sourceRef] = msg; }); } this.lastModifieds[msg.context] = Date.now(); } setSourceDelta(key, delta) { this.sourceDeltas[key] = delta; this.app.signalk.addDelta(delta); } deleteContext(contextKey) { debug('Deleting context ' + contextKey); const contextParts = contextKey.split('.'); if (contextParts.length === 2) { delete this.cache[contextParts[0]][contextParts[1]]; } } pruneContexts(seconds) { debug('pruning contexts...'); const threshold = Date.now() - seconds * 1000; for (const contextKey in this.lastModifieds) { if (this.lastModifieds[contextKey] < threshold) { this.deleteContext(contextKey); delete this.lastModifieds[contextKey]; } } } buildFull(user, path) { const leaf = getLeafObject(this.cache, pathToProcessForFull(path), false, true); let deltas; if (leaf) { deltas = findDeltas(leaf).map(streambundle_1.toDelta); } return this.buildFullFromDeltas(user, deltas, path.length === 0 || path[0] === 'sources'); } getSources() { const signalk = new signalk_schema_1.FullSignalK(this.app.selfId, this.app.selfType); const addDelta = signalk.addDelta.bind(signalk); lodash_1.default.values(this.sourceDeltas).forEach(addDelta); return signalk.retrieve().sources; } buildFullFromDeltas(user, deltas, includeSources) { const signalk = new signalk_schema_1.FullSignalK(this.app.selfId, this.app.selfType); const addDelta = signalk.addDelta.bind(signalk); if (includeSources) { lodash_1.default.values(this.sourceDeltas).forEach(addDelta); } if (deltas && deltas.length) { const secFilter = this.app.securityStrategy.shouldFilterDeltas() ? (delta) => this.app.securityStrategy.filterReadDelta(user, delta) : () => true; deltas.filter(secFilter).forEach(addDelta); } return signalk.retrieve(); } getCachedDeltas(contextFilter, user, key) { const contexts = []; lodash_1.default.keys(this.cache).forEach((type) => { lodash_1.default.keys(this.cache[type]).forEach((id) => { const context = `${type}.${id}`; if (contextFilter({ context })) { contexts.push(this.cache[type][id]); } }); }); const deltas = contexts.reduce((acc, context) => { let deltasToProcess; if (key) { deltasToProcess = lodash_1.default.get(context, key); } else { deltasToProcess = findDeltas(context); } if (deltasToProcess) { acc = acc.concat(lodash_1.default.values(lodash_1.default.pickBy(deltasToProcess, (val, akey) => { return akey !== 'meta'; }))); } return acc; }, []); deltas.sort((left, right) => { return (new Date(left.timestamp).getTime() - new Date(right.timestamp).getTime()); }); return deltas.map(streambundle_1.toDelta).filter((delta) => { return this.app.securityStrategy.filterReadDelta(user, delta); }); } } exports.default = DeltaCache; function pathToProcessForFull(pathArray) { if (pathArray.length > 0 && pathArray[0] === 'sources') { return []; } return pathArray; } function pickDeltasFromBranch(acc, obj) { if (typeof obj === 'object') { if ((0, lodash_1.isUndefined)(obj.path) || (0, lodash_1.isUndefined)(obj.value)) { // not a delta, so process possible children lodash_1.default.values(obj).reduce(pickDeltasFromBranch, acc); } else { acc.push(obj); } } return acc; } function findDeltas(branchOrLeaf) { return lodash_1.default.values(branchOrLeaf).reduce(pickDeltasFromBranch, []); } function ensureHasDollarSource(normalizedDelta) { let dollarSource = normalizedDelta.$source; if (!dollarSource) { dollarSource = (0, signalk_schema_1.getSourceId)(normalizedDelta.source); normalizedDelta.$source = dollarSource; } return dollarSource; } function getLeafObject(start, contextAndPathParts, createIfMissing = false, returnLast = false) { let current = start; for (let i = 0; i < contextAndPathParts.length; i++) { const p = contextAndPathParts[i]; if ((0, lodash_1.isUndefined)(current[p])) { if (createIfMissing) { current[p] = {}; } else { return returnLast ? current : null; } } current = current[p]; } return current; }