signalk-server
Version:
An implementation of a [Signal K](http://signalk.org) server for boats.
242 lines (240 loc) • 9.36 kB
JavaScript
"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;
}