Files
genieacs/lib/device.js
T
Zaid Abdulla 1a56915d4b Defer tail declarations
declare() calls that aren't followed by commit() are applied at the end
of the provision script. This change defers those until all provisions
have been executed. This prevents certain cases of preset loop faults.
2018-09-15 00:09:58 -07:00

403 lines
11 KiB
JavaScript

/**
* Copyright 2013-2018 Zaid Abdulla
*
* This file is part of GenieACS.
*
* GenieACS is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* GenieACS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/
"use strict";
const CHANGE_FLAGS = {
object: 2,
writable: 4,
value: 8
};
function parseBool(v) {
v = "" + v;
if (v === "true" || v === "TRUE" || v === "True" || v === "1") return true;
else if (v === "false" || v === "FALSE" || v === "False" || v === "0")
return false;
else return null;
}
function sanitizeParameterValue(parameterValue) {
if (parameterValue[0] != null) {
switch (parameterValue[1]) {
case "xsd:boolean":
if (typeof parameterValue[0] !== "boolean") {
let v = parseBool(parameterValue[0]);
if (v == null) v = "" + parameterValue[0];
parameterValue[0] = v;
}
break;
case "xsd:string":
case "xsd:base64":
case "xsd:hexBinary":
if (typeof parameterValue[0] !== "string")
parameterValue[0] = "" + parameterValue[0];
break;
case "xsd:int":
case "xsd:unsignedInt":
if (typeof parameterValue[0] !== "number") {
let v = parseInt(parameterValue[0]);
if (isNaN(v)) v = "" + parameterValue[0];
parameterValue[0] = v;
}
break;
case "xsd:dateTime":
if (typeof parameterValue[0] !== "number") {
// Don't use parseInt because it reads date string as a number
let v = +parameterValue[0];
if (isNaN(v)) {
v = Date.parse(parameterValue[0]);
if (isNaN(v)) v = "" + parameterValue[0];
}
parameterValue[0] = v;
}
break;
default:
if (parameterValue[1] != null) {
throw new Error(
`Parameter value type "${parameterValue[1]}" not recognized.`
);
}
parameterValue[0] = JSON.parse(JSON.stringify(parameterValue[0]));
break;
}
}
return parameterValue;
}
function getAliasDeclarations(path, timestamp) {
var decs = [];
function recursive(p, prefix) {
const pattern = prefix.concat(p);
for (let i = prefix.length; i < pattern.length; ++i) {
if (Array.isArray(pattern[i])) {
const pat = pattern[i];
pattern[i] = "*";
for (let j = 0; j < pat.length; j += 2)
recursive(pat[j], pattern.slice(0, i + 1));
}
}
decs.push({
path: pattern,
pathGet: timestamp,
pathSet: null,
attrGet: prefix.length ? { value: timestamp } : null,
attrSet: null,
defer: true
});
}
recursive(path, []);
return decs;
}
function unpack(deviceData, path, revision) {
let allMatches = [];
if (!path.alias) {
for (const p of deviceData.paths.find(path, false, true))
if (deviceData.attributes.has(p, revision)) allMatches.push(p);
} else {
const wildcardPath = path.slice();
wildcardPath.wildcard = path.wildcard;
for (let i = 0; i < wildcardPath.length; ++i) {
if (Array.isArray(wildcardPath[i])) {
wildcardPath[i] = "*";
wildcardPath.wildcard |= 1 << i;
}
}
for (const p of deviceData.paths.find(wildcardPath, false, true))
if (deviceData.attributes.has(p, revision)) allMatches.push(p);
for (let i = path.length - 1; i >= 0; --i) {
if (Array.isArray(path[i])) {
for (let j = 0; j < path[i].length; j += 2) {
const p = wildcardPath.slice(0, i + 1).concat(path[i][j]);
p.alias =
((wildcardPath.alias & ((1 << (i + 1)) - 1)) + path[i][j].alias) <<
(i + 1);
p.wildcard =
((wildcardPath.wildcard & ((1 << (i + 1)) - 1)) +
path[i][j].wildcard) <<
(i + 1);
const unpacked = unpack(deviceData, p, revision);
const filtered = [];
for (const up of unpacked) {
const attributes = deviceData.attributes.get(up, revision);
if (
attributes &&
attributes.value &&
attributes.value[1] &&
sanitizeParameterValue([
path[i][j + 1],
attributes.value[1][1]
])[0] === attributes.value[1][0]
) {
for (let m = 0; m < allMatches.length; ++m) {
let k;
for (k = i; k >= 0; --k) if (allMatches[m][k] != up[k]) break;
if (k < 0) {
filtered.push(allMatches[m]);
allMatches[m] = [];
}
}
}
}
allMatches = filtered;
}
}
}
}
allMatches.sort((a, b) => {
for (let i = 0; i < a.length; ++i) {
if (a === b) continue;
// Use numeric sorting for numbers
const ia = parseInt(a);
const ib = parseInt(b);
if (ia.toString() === a && ib.toString() === b) {
a = ia;
b = ib;
}
if (a < b) return -1;
else return 1;
}
return 0;
});
return allMatches;
}
function clear(deviceData, path, timestamp, attributes, changeFlags = 0) {
const changeTrackers = {};
timestamp = timestamp || 0;
let descendantsTimestamp = timestamp;
if (attributes && attributes.object) {
if (attributes.object > descendantsTimestamp)
descendantsTimestamp = attributes.object;
if (!(attributes.object <= attributes.value))
attributes.value = attributes.object;
}
for (const p of deviceData.paths.find(
path,
true,
true,
descendantsTimestamp ? 99 : path.length
)) {
const tracker = deviceData.trackers.get(p);
for (const k in tracker) changeTrackers[k] |= tracker[k];
const currentTimestamp = deviceData.timestamps.get(p);
if (currentTimestamp === undefined) continue;
if (
timestamp > currentTimestamp ||
(descendantsTimestamp > currentTimestamp && p.length > path.length)
) {
deviceData.timestamps.delete(p);
deviceData.attributes.delete(p);
changeFlags |= 1;
} else if (attributes && p.length == path.length) {
const currentAttributes = deviceData.attributes.get(p);
if (currentAttributes) {
let newAttrs;
for (const attrName in attributes) {
if (
attrName in currentAttributes &&
attributes[attrName] > currentAttributes[attrName][0]
) {
changeFlags |= CHANGE_FLAGS[attrName];
if (!newAttrs) {
newAttrs = Object.assign({}, currentAttributes);
deviceData.attributes.set(p, newAttrs);
}
delete newAttrs[attrName];
}
}
}
}
}
// Note: For performance, we're merging all changes together rather than
// mark changes based the exact parameters affected.
for (const k in changeTrackers)
if (changeTrackers[k] & changeFlags) deviceData.changes.add(k);
}
function compareEquality(a, b) {
var t = typeof a;
if (
a === null ||
a === undefined ||
t === "number" ||
t === "boolean" ||
t === "string" ||
t === "symbol"
)
return a === b;
return JSON.stringify(a) === JSON.stringify(b);
}
function set(deviceData, path, timestamp, attributes, toClear) {
path = deviceData.paths.add(path);
const currentTimestamp = deviceData.timestamps.get(path);
let currentAttributes;
if (path.wildcard) attributes = undefined;
else if (currentTimestamp)
currentAttributes = deviceData.attributes.get(path);
let changeFlags = 0;
if (attributes) {
if (
attributes.value &&
attributes.value[1] &&
attributes.value[0] >= (attributes.object ? attributes.object[0] : 0)
)
attributes.object = [attributes.value[0], 0];
if (
attributes.object &&
attributes.object[1] &&
attributes.object[0] >= (attributes.value ? attributes.value[0] : 0)
)
attributes.value = [attributes.object[0]];
const newAttributes = Object.assign({}, currentAttributes, attributes);
if (currentAttributes) {
for (const attrName in attributes) {
timestamp = Math.max(timestamp, attributes[attrName][0]);
if (!(attrName in currentAttributes))
changeFlags |= CHANGE_FLAGS[attrName];
else if (attributes[attrName][0] <= currentAttributes[attrName][0])
newAttributes[attrName] = currentAttributes[attrName];
else if (
!compareEquality(
attributes[attrName][1],
currentAttributes[attrName][1]
)
)
changeFlags |= CHANGE_FLAGS[attrName];
}
} else {
changeFlags |= 1;
}
deviceData.attributes.set(path, newAttributes);
if (!(timestamp <= currentTimestamp)) {
deviceData.timestamps.set(path, timestamp);
if (path.length > 1) {
toClear = set(
deviceData,
path.slice(0, path.length - 1),
timestamp,
{ object: [timestamp, 1] },
toClear
);
}
}
} else if (!(timestamp <= currentTimestamp)) {
deviceData.timestamps.set(path, timestamp);
if (currentAttributes) {
deviceData.attributes.delete(path);
changeFlags |= 1;
} else if (path.wildcard) {
for (const p of deviceData.paths.find(path, false, true, path.length)) {
if (timestamp > deviceData.timestamps.get(p)) {
toClear = toClear || [];
toClear.push([p, timestamp]);
}
}
}
}
if (changeFlags) {
if (changeFlags & 1) {
toClear = toClear || [];
toClear.push([path, timestamp, null, changeFlags]);
} else if (changeFlags & CHANGE_FLAGS.object) {
toClear = toClear || [];
toClear.push([path, 0, { object: attributes.object[0] }, changeFlags]);
} else {
for (const p of deviceData.paths.find(path, true, false, path.length)) {
const tracker = deviceData.trackers.get(p);
for (const k in tracker)
if (tracker[k] & changeFlags) deviceData.changes.add(k);
}
}
}
return toClear;
}
function track(deviceData, path, marker, attributes) {
path = deviceData.paths.add(path);
let f = 1;
if (attributes)
for (const attrName of attributes) f |= CHANGE_FLAGS[attrName];
let cur = deviceData.trackers.get(path);
if (!cur) {
cur = {};
deviceData.trackers.set(path, cur);
}
cur[marker] |= f;
}
function clearTrackers(deviceData, tracker) {
if (Array.isArray(tracker)) {
for (const v of deviceData.trackers.values())
for (const t of tracker) delete v[t];
for (const t of tracker) deviceData.changes.delete(t);
} else {
for (const v of deviceData.trackers.values()) delete v[tracker];
deviceData.changes.delete(tracker);
}
}
exports.sanitizeParameterValue = sanitizeParameterValue;
exports.getAliasDeclarations = getAliasDeclarations;
exports.unpack = unpack;
exports.set = set;
exports.clear = clear;
exports.track = track;
exports.clearTrackers = clearTrackers;