diff --git a/lib/common.coffee b/lib/common.coffee index 358a9b4..5bc21f7 100644 --- a/lib/common.coffee +++ b/lib/common.coffee @@ -247,7 +247,7 @@ parsePath = (pattern, start, res) -> while i < pattern.length and pattern[i] != ':' and pattern[i] != '.' ++ i n = pattern.slice(j, i) - path.push(if n == '*' then null else n) + path.push(n) if i >= pattern.length or pattern[i] == ':' break @@ -263,6 +263,15 @@ parsePath = (pattern, start, res) -> return i +hammingWeight = (flags) -> + flags -= ((flags >> 1) & 0x55555555) + flags = (((flags >> 2) & 0x33333333) + (flags & 0x33333333)) + flags = (((flags >> 4) + flags) & 0x0f0f0f0f) + flags += (flags >> 8) + flags += (flags >> 16) + return flags & 0x0000003f + + exports.UNDEFINED_TYPE = UNDEFINED_TYPE exports.NULL_TYPE = NULL_TYPE exports.NUMBER_TYPE = NUMBER_TYPE @@ -274,3 +283,4 @@ exports.REGEXP_TYPE = REGEXP_TYPE exports.typeOf = typeOf exports.pathOverlap = pathOverlap exports.parsePath = parsePath +exports.hammingWeight = hammingWeight diff --git a/lib/cwmp.coffee b/lib/cwmp.coffee index 3c99f93..13aa1cf 100644 --- a/lib/cwmp.coffee +++ b/lib/cwmp.coffee @@ -118,7 +118,7 @@ applyPresets = (currentRequest) -> declarations = [] for k, v of parameters - declarations.push([v, 1, null, null, null, null, null, 1]) + declarations.push([v, {exist: 1, value: 1}]) session.rpcRequest(currentRequest.sessionData, declarations, (err, id, rpcRequest) -> throw err if err diff --git a/lib/db.coffee b/lib/db.coffee index 9d0f887..1d72cec 100644 --- a/lib/db.coffee +++ b/lib/db.coffee @@ -248,24 +248,28 @@ optimizeProjection = (obj) -> fetchDevice = (id, timestamp, patterns, callback) -> res = [] + loaded = [] # Build projection projection = {_id: 1} for pattern in patterns + loaded.push([pattern, 1]) if pattern.length == 0 projection['_timestamp'] = 1 else if not pattern[0]? projection[''] = 1 else if pattern[0] == 'Events' if pattern.length == 1 - res.push([['Events'], timestamp, 1, timestamp, 1, timestamp, 0]) + res.push([['Events'], + {exist: timestamp, object: timestamp, writable: timestamp}, + {exist: 1, object: 1, writable: 0}]) else if pattern.length == 2 if not pattern[1]? projection['_registered'] = 1 projection['_lastInform'] = 1 projection['_lastBootstrap'] = 1 projection['_lastBoot'] = 1 - res.push([['Events', null], timestamp]) + res.push([['Events', '*'], {exist: timestamp}]) else if pattern[1] == 'Registered' projection['_registered'] = 1 else if pattern[1] == 'Inform' @@ -276,7 +280,10 @@ fetchDevice = (id, timestamp, patterns, callback) -> projection['_lastBoot'] = 1 else if pattern[0] == 'DeviceID' if pattern.length == 1 - res.push([['DeviceID'], timestamp, 1, timestamp, 1, timestamp, 0]) + res.push([['DeviceID'], + {exist: timestamp, object: timestamp, writable: timestamp}, + {exist: 1, object: 1, writable: 0}]) + else if pattern.length == 2 if not pattern[1]? projection['_id'] = 1 @@ -284,7 +291,8 @@ fetchDevice = (id, timestamp, patterns, callback) -> projection['_deviceId._ProductClass'] = 1 projection['_deviceId._SerialNumber'] = 1 projection['_deviceId._OUI'] = 1 - res.push([['DeviceID', null], timestamp]) + res.push([['DeviceID', '*'], {exist: timestamp}]) + else if pattern[1] == 'ID' projection['_id'] = 1 else if pattern[1] == 'Manufacturer' @@ -297,15 +305,17 @@ fetchDevice = (id, timestamp, patterns, callback) -> projection['_deviceId._SerialNumber'] = 1 else if pattern[0] == 'Tags' if pattern.length == 1 - res.push([['Tags'], timestamp, 1, timestamp, 1, timestamp, 0]) + res.push([['Tags'], + {exist: timestamp, object: timestamp, writable: timestamp}, + {exist: 1, object: 1, writable: 0}]) + else if pattern.length == 2 - res.push([['Tags', null], timestamp]) + res.push([['Tags', '*'], {exist: timestamp}]) projection['_tags'] = 1 else - for i in [0...pattern.length] by 1 - break if not pattern[i] + wildcardIndex = pattern.indexOf('*') - if i == pattern.length + if wildcardIndex == -1 s = pattern.join('.') projection["#{s}._value"] = 1 projection["#{s}._timestamp"] = 1 @@ -320,8 +330,11 @@ fetchDevice = (id, timestamp, patterns, callback) -> else projection["#{pattern.slice(0, -1).join('.')}._timestamp"] = 1 else - s = pattern.slice(0, i).join('.') + p = pattern.slice(0, wildcardIndex) + loaded.push([p, 99]) + s = p.join('.') projection[s] = 1 + projection[pattern.slice(0, wildcardIndex - 1).concat('_timestamp').join('.')] = 1 if projection[''] projection = {} @@ -339,25 +352,32 @@ fetchDevice = (id, timestamp, patterns, callback) -> if obj['_timestamp']? obj['_timestamp'] = +obj['_timestamp'] - v = [timestamp, 1] + t = {} + v = {} if obj['_value']? - v[6] = obj['_timestamp'] ? timestamp - v[7] = [obj['_value'], obj['_type']] + t.value = obj['_timestamp'] ? timestamp + v.value = [obj['_value'], obj['_type']] obj['_object'] = false + t.exist = Math.max(t.exist || 0, t.value) + v.exist = 1 if obj['_writable']? - v[4] = timestamp - v[5] = if obj['_writable'] then 1 else 0 + t.writable = timestamp + v.writable = if obj['_writable'] then 1 else 0 + t.exist = Math.max(t.exist || 0, t.writable) + v.exist = 1 if obj['_object']? - v[2] = timestamp - v[3] = if obj['_object'] then 1 else 0 + t.object = obj['_timestamp'] ? timestamp + v.object = if obj['_object'] then 1 else 0 + t.exist = Math.max(t.exist || 0, t.object) + v.exist = 1 - if v.length - v[0] ?= timestamp ? 0 - v[1] ?= 1 - res.push([path].concat(v)) + if not obj['_object'] + loaded.push([path, 99]) + + res.push([path, t, v]) for k, v of obj if k[0] != '_' @@ -365,112 +385,146 @@ fetchDevice = (id, timestamp, patterns, callback) -> storeParams(v, path.concat(k), obj['_timestamp'] ? 0, descendantsFetched) if obj['_object'] and descendantsFetched - res.push([path.concat(null), obj['_timestamp'] ? 0]) + res.push([path.concat('*'), {exist: obj['_timestamp'] ? 0}]) for k, v of device if k == '_timestamp' - res.push([[null], v]) if not Object.keys(projection).length + res.push([['*'], {exist: v}]) if not Object.keys(projection).length else if k == '_lastInform' - res.push([['Events', 'Inform'], timestamp, 1, timestamp, 0, timestamp, 0, timestamp, [v, 'xsd:dateTime']]) + res.push([['Events', 'Inform'], + {exist: timestamp, object: timestamp, writable: timestamp, value: timestamp}, + {exist: 1, object: 0, writable: 0, value: [+v, 'xsd:dateTime']}]) else if k == '_lastBoot' - res.push([['Events', '1_BOOT'], timestamp, 1, timestamp, 0, timestamp, 0, timestamp, [v, 'xsd:dateTime']]) + res.push([['Events', '1_BOOT'], + {exist: timestamp, object: timestamp, writable: timestamp, value: timestamp}, + {exist: 1, object: 0, writable: 0, value: [+v, 'xsd:dateTime']}]) else if k == '_lastBootstrap' - res.push([['Events', '0_BOOTSTRAP'], timestamp, 1, timestamp, 0, timestamp, 0, timestamp, [v, 'xsd:dateTime']]) + res.push([['Events', '0_BOOTSTRAP'], + {exist: timestamp, object: timestamp, writable: timestamp, value: timestamp}, + {exist: 1, object: 0, writable: 0, value: [+v, 'xsd:dateTime']}]) else if k == '_registered' - res.push([['Events', 'Registered'], timestamp, 1, timestamp, 0, timestamp, 0, timestamp, [v, 'xsd:dateTime']]) + res.push([['Events', 'Registered'], + {exist: timestamp, object: timestamp, writable: timestamp, value: timestamp}, + {exist: 1, object: 0, writable: 0, value: [+v, 'xsd:dateTime']}]) else if k == '_id' - res.push([['DeviceID', 'ID'], timestamp, 1, timestamp, 0, timestamp, 0, timestamp, [v, 'xsd:string']]) + res.push([['DeviceID', 'ID'], + {exist: timestamp, object: timestamp, writable: timestamp, value: timestamp}, + {exist: 1, object: 0, writable: 0, value: [v, 'xsd:string']}]) else if k == '_tags' for t in v - res.push([['Tags', t], timestamp, 1, timestamp, 0, timestamp, 1, timestamp, [true, 'xsd:boolean']]) + res.push([['Tags', t], + {exist: timestamp, object: timestamp, writable: timestamp, value: timestamp}, + {exist: 1, object: 0, writable: 1, value: [true, 'xsd:boolean']}]) else if k == '_deviceId' for kk, vv of v if kk == '_Manufacturer' - res.push([['DeviceID', 'Manufacturer'], timestamp, 1, timestamp, 0, timestamp, 0, timestamp, [vv, 'xsd:string']]) + res.push([['DeviceID', 'Manufacturer'], + {exist: timestamp, object: timestamp, writable: timestamp, value: timestamp}, + {exist: 1, object: 0, writable: 0, value: [vv, 'xsd:boolean']}]) else if kk == '_OUI' - res.push([['DeviceID', 'OUI'], timestamp, 1, timestamp, 0, timestamp, 0, timestamp, [vv, 'xsd:string']]) + res.push([['DeviceID', 'OUI'], + {exist: timestamp, object: timestamp, writable: timestamp, value: timestamp}, + {exist: 1, object: 0, writable: 0, value: [vv, 'xsd:boolean']}]) if kk == '_ProductClass' - res.push([['DeviceID', 'ProductClass'], timestamp, 1, timestamp, 0, timestamp, 0, timestamp, [vv, 'xsd:string']]) + res.push([['DeviceID', 'ProductClass'], + {exist: timestamp, object: timestamp, writable: timestamp, value: timestamp}, + {exist: 1, object: 0, writable: 0, value: [vv, 'xsd:boolean']}]) if kk == '_SerialNumber' - res.push([['DeviceID', 'SerialNumber'], timestamp, 1, timestamp, 0, timestamp, 0, timestamp, [vv, 'xsd:string']]) + res.push([['DeviceID', 'SerialNumber'], + {exist: timestamp, object: timestamp, writable: timestamp, value: timestamp}, + {exist: 1, object: 0, writable: 0, value: [vv, 'xsd:boolean']}]) else if common.typeOf(v) is common.OBJECT_TYPE - storeParams(v, [k], device['_timestamp'] ? 0, projection == null) + storeParams(v, [k], +(device['_timestamp'] ? 0), projection == null) - return callback(null, res) + return callback(null, res, loaded) ) -saveDevice = (deviceId, diff, isNew, callback) -> +saveDevice = (deviceId, deviceData, isNew, callback) -> update = {'$set' : {}, '$unset' : {}, '$addToSet' : {}, '$pull' : {}} - for p in diff - base = p[2] - current = p[3] - if p[0][0] == 'Events' - if p[0].length == 2 - if p[0][1] == 'Inform' - update['$set']['_lastInform'] = new Date(current[7][0]) - else if p[0][1] == '0_BOOTSTRAP' - update['$set']['_lastBootstrap'] = new Date(current[7][0]) - else if p[0][1] == '1_BOOT' - update['$set']['_lastBoot'] = new Date(current[7][0]) - else if p[0][1] == 'Registered' - update['$set']['_registered'] = new Date(current[7][0]) - else if p[0][0] == 'Tags' - if p[0].length == 2 and p[0][1]? - if current[7][0] and not base[7]?[0] - update['$addToSet']['_tags'] ?= {'$each' : []} - update['$addToSet']['_tags']['$each'].push(p[0][1]) - else if not current[7]?[0] and base[7]?[0] - update['$pull']['_tags'] ?= {'$in' : []} - update['$pull']['_tags']['$in'].push(p[0][1]) - else if p[0][0] == 'DeviceID' - if p[0].length == 2 and current[7]? - if p[0][1] == 'ID' - update['$set']['_id'] = current[7][0] - else if p[0][1] == 'Manufacturer' - update['$set']['_deviceId._Manufacturer'] = current[7][0] - else if p[0][1] == 'OUI' - update['$set']['_deviceId._OUI'] = current[7][0] - else if p[0][1] == 'ProductClass' - update['$set']['_deviceId._ProductClass'] = current[7][0] - else if p[0][1] == 'SerialNumber' - update['$set']['_deviceId._SerialNumber'] = current[7][0] - else if p[1] == p[0].length - param = p[0].join('.') - if current[2]? - if current[3] != base[3] - if current[3] - update['$set']["#{param}._object"] = true - else if base[3]? - update['$unset']["#{param}._object"] = 1 - else if base[2]? - update['$unset']["#{param}._object"] = 1 - if current[4]? - if current[5] != base[5] - update['$set']["#{param}._writable"] = !!current[5] - else if base[4]? - update['$unset']["#{param}._writable"] = 1 + # Delete parameters that no longer exist + iter = deviceData.values.exist.diff() + while not (diff = iter.next()).done + if diff.value[1] and not diff.value[2] + update['$unset'][diff.value[0].join('.')] = 1 - if current[6]? and current[7]? - if current[6] != base[6] - update['$set']["#{param}._timestamp"] = new Date(current[6]) + # Set object's children timestamps + iter = deviceData.timestamps.exist.diff() + while not (diff = iter.next()).done + continue if diff.value[0].wildcards != (1 << (diff.value[0].length - 1)) + continue if diff.value[0][0] in ['Events', 'DeviceID', 'Tags'] - if current[7][0] != base[7]?[0] or current[7][1] != base[7]?[1] - if current[7][1] == 'xsd:dateTime' - update['$set']["#{param}._value"] = new Date(current[7][0]) - else - update['$set']["#{param}._value"] = current[7][0] - update['$set']["#{param}._type"] = current[7][1] - else if base[7]? - update['$unset']["#{param}._value"] = 1 - update['$unset']["#{param}._type"] = 1 - else if p[1] == p[0].length - 1 and not p[0][p[1]]? - param = p[0].slice(0, p[1]).concat('_timestamp').join('.') - if current? - update['$set'][param] = new Date(current) - else if base? - update['$unset'][param] = 1 + if not diff.value[2]? and diff.value[1]? + update['$unset'][diff.value[0].slice(0, -1).concat('_timestamp').join('.')] = 1 + else if diff.value[2] != diff.value[1] + update['$set'][diff.value[0].slice(0, -1).concat('_timestamp').join('.')] = new Date(diff.value[2]) + + # Set writable + iter = deviceData.values.writable.diff() + while not (diff = iter.next()).done + continue if diff.value[0][0] in ['Events', 'DeviceID', 'Tags'] + if not diff.value[2]? and diff.value[1]? + update['$unset'][diff.value[0].concat('_writable').join('.')] = 1 + else if diff.value[2] != diff.value[1] + update['$set'][diff.value[0].concat('_writable').join('.')] = Boolean(diff.value[2]) + + # Set object + iter = deviceData.values.object.diff() + while not (diff = iter.next()).done + continue if diff.value[0][0] in ['Events', 'DeviceID', 'Tags'] + if not diff.value[2] and diff.value[1] + update['$unset'][diff.value[0].concat('_object').join('.')] = 1 + else if diff.value[2] and diff.value[2] != diff.value[1] + update['$set'][diff.value[0].concat('_object').join('.')] = true + + # Set value + iter = deviceData.values.value.diff() + while not (diff = iter.next()).done + if diff.value[0][0] == 'Events' + continue if diff.value[1]?[0] == diff.value[2][0] + if diff.value[0][1] == 'Inform' + update['$set']['_lastInform'] = new Date(diff.value[2][0]) + else if diff.value[0][1] == '1_BOOT' + update['$set']['_lastBoot'] = new Date(diff.value[2][0]) + else if diff.value[0][1] == '0_BOOTSTRAP' + update['$set']['_lastBootstrap'] = new Date(diff.value[2][0]) + else if diff.value[0][1] == 'Registered' + update['$set']['_registered'] = new Date(diff.value[2][0]) + else if diff.value[0][0] == 'Tags' + continue if diff.value[1]?[0] == diff.value[2]?[0] + if diff.value[2][0] + update['$addToSet']['_tags'] ?= {'$each' : []} + update['$addToSet']['_tags']['$each'].push(p[0][1]) + else + update['$pull']['_tags'] ?= {'$in' : []} + update['$pull']['_tags']['$in'].push(p[0][1]) + else if diff.value[0][0] == 'DeviceID' + continue if diff.value[1]?[0] == diff.value[2][0] + if diff.value[0][1] == 'ID' + update['$set']['_id'] = diff.value[2][0] + else if diff.value[0][1] == 'Manufacturer' + update['$set']['_deviceId._Manufacturer'] = diff.value[2][0] + else if diff.value[0][1] == 'OUI' + update['$set']['_deviceId._OUI'] = diff.value[2][0] + else if diff.value[0][1] == 'ProductClass' + update['$set']['_deviceId._ProductClass'] = diff.value[2][0] + else if diff.value[0][1] == 'SerialNumber' + update['$set']['_deviceId._SerialNumber'] = diff.value[2][0] + else + tdiff = deviceData.timestamps.value.getDiff(diff.value[0]) + if diff.value[2]? + if tdiff[2] != tdiff[1] + update['$set'][diff.value[0].concat('_timestamp').join('.')] = new Date(tdiff[2]) + delete update['$unset'][diff.value[0].concat('_timestamp').join('.')] + if diff.value[2][0] != diff.value[1]?[0] + update['$set'][diff.value[0].concat('_value').join('.')] = diff.value[2][0] + if diff.value[2][1] != diff.value[1]?[1] + update['$set'][diff.value[0].concat('_type').join('.')] = diff.value[2][1] + + else if diff.value[1]? + update['$unset'][diff.value[0].concat('_value').join('.')] = 1 + update['$unset'][diff.value[0].concat('_value').join('.')] = 1 # Remove empty keys for k of update diff --git a/lib/device.coffee b/lib/device.coffee deleted file mode 100644 index b832321..0000000 --- a/lib/device.coffee +++ /dev/null @@ -1,412 +0,0 @@ -### -# Copyright 2013-2016 Zaid Abdulla -# -# 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 . -### - -common = require './common' - - -sanitizeDeclaration = (declaration, wildcard, now) -> - if typeof declaration is 'number' - declaration = [declaration] - - if declaration[7]? - declaration[7] = sanitizeParameterValue(declaration[7]) - - for i in [0...declaration.length] by 2 when declaration[i]? - if declaration[i] <= 0 - declaration[i] = now + declaration[i] - else - declaration[i] = Math.min(now, declaration[i]) - - return declaration - - -sanitizeProperties = (properties, wildcard) -> - if not wildcard - if typeof properties is 'number' - properties = [properties] - else if properties[1] - properties[1] = 1 - - for i in [2...properties.length] by 2 \ - when properties[i]? and properties[i + 1]? - properties[0] = Math.max(properties[0] ? 0, properties[i]) - - if properties[6]? and properties[7]? and not (properties[6] < properties[2]) - properties[2] = properties[6] - properties[3] = 0 - else if properties[2]? and properties[3] and not (properties[2] < properties[6]) - properties[6] = properties[2] - properties[7] = null - - else if typeof properties isnt 'number' - properties = properties[0] - - return properties - - -sanitizeParameterValue = (parameterValue) -> - if parameterValue[0]? - switch parameterValue[1] - when 'xsd:boolean' - if typeof parameterValue[0] isnt 'boolean' - parameterValue = parameterValue.slice() - parameterValue[0] = !!JSON.parse(parameterValue[0]) - when 'xsd:string' - if typeof parameterValue[0] isnt 'string' - parameterValue = parameterValue.slice() - parameterValue[0] = '' + parameterValue[0] - when 'xsd:int', 'xsd:unsignedInt' - if typeof parameterValue[0] isnt 'number' - parameterValue = parameterValue.slice() - parameterValue[0] = +parameterValue[0] - when 'xsd:dateTime' - if typeof parameterValue[0] isnt 'number' - parameterValue = parameterValue.slice() - if parameterValue[0].getTime? - parameterValue[0] = parameterValue[0].getTime() - else if isNaN(parameterValue[0]) - parameterValue[0] = Date.parse(parameterValue[0]) - else - parameterValue[0] = +parameterValue[0] - else - if parameterValue[1]? - throw new Error("Parameter value type \"#{parameterValue[1]}\" not recognized.") - - # Ensure consistency after serialization - parameterValue[0] = JSON.parse(JSON.stringify(parameterValue[0])) - - return parameterValue - - -init = () -> - return [[[null, [0, 1]]]] - - -declare = (deviceData, path, declaration, now) -> - wildcard = false - for i in [0...path.length] by 1 when not path[i]? - wildcard = true - break - - declaration = sanitizeDeclaration(declaration, wildcard, now) - - ref = deviceData - depth = 0 - for i in [0...path.length] by 1 - if not (p = path[i])? - ++ depth - continue - - ref[1 + depth] ?= {} - ref[1 + depth][p] ?= [] - ref = ref[1 + depth][p] - depth = 0 - - ref[0] ?= [] - ref[0][depth] ?= [] - - if wildcard - ref[0][depth][0] = Math.max(ref[0][depth][0] ? 0, declaration[0]) - else if not ref[0][depth][0]? - ref[0][depth][0] = declaration - else - for i in [0...declaration.length] by 2 - if declaration[i]? - ref[0][depth][0][i] = Math.max(ref[0][depth][0][i] ? 0, declaration[i]) - - if declaration[i + 1]? - ref[0][depth][0][i + 1] = declaration[i + 1] - - return - - -set = (deviceData, path, revision, properties) -> - ++ revision # revisions start at layer 1 - wildcard = false - for i in [0...path.length] by 1 when not path[i]? - wildcard = true - break - - properties = sanitizeProperties(properties, wildcard) - - ref = deviceData - depth = 0 - for i in [0...path.length] by 1 - if not (p = path[i])? - ++ depth - continue - - if not wildcard and properties[1] - # Update existing timestamp for ancestors - ref[0] ?= [] - ref[0][0] ?= [] - ref[0][0][revision] ?= [] - if not (ref[0][0][revision][0] >= properties[0]) - ref[0][0][revision][0] = properties[0] - ref[0][0][revision][1] = 1 - - if not (ref[0][0][revision][2] >= properties[0]) - ref[0][0][revision][2] = properties[0] - ref[0][0][revision][3] = 1 - - ref[1 + depth] ?= {} - ref[1 + depth][p] ?= [] - ref = ref[1 + depth][p] - depth = 0 - - ref[0] ?= [] - ref[0][depth] ?= [] - - if wildcard - ref[0][depth][revision] = Math.max(ref[0][depth][revision] ? 0, properties) - else if not ref[0][0][revision]? - ref[0][0][revision] = properties - else - for i in [0...properties.length] by 2 \ - when properties[i]?# and properties[i + 1]? - if not (properties[i] < ref[0][0][revision][i]) - ref[0][0][revision][i] = properties[i] - ref[0][0][revision][i + 1] = properties[i + 1] - - if properties[7]? - ref[0][0][revision][7] = sanitizeParameterValue(ref[0][0][revision][7]) - - return - - -traverse = (deviceData, pattern, revision, callback) -> - if not pattern? - pattern = [] - pattern.length = 99 - - if not revision? - revision = 99 - - ++ revision # First layer is for declarations - - collectTimestamps = (ref, path, root, timestamps, wildcard) -> - # Skip properties of parameter we're currently at - skip = if root == path.length then 1 else 0 - - if ref[0]? - for i in [skip...ref[0].length] by 1 - if ref[0][i]? and i <= pattern.length - path.length - p = path.slice() - p.length += i - if wildcard or i > 0 - revisions = ref[0][i] - else - # Extract only timestamp from properties array - revisions = (j?[0] for j in ref[0][i]) - - current = -1 - for j in [Math.min(revision, revisions.length - 1)..1] by -1 when revisions[j]? - current = revisions[j]; break - - timestamps.push([p, root, revisions, current, current]) - - for i in [skip...ref.length - 1] by 1 - if ref[1 + i]? and i < pattern.length - path.length - p = path.slice() - p.length += i - for k, v of ref[1+i] - if not pattern[path.length + i]? or pattern[path.length + i] == k - collectTimestamps(v, p.concat(k), root, timestamps, wildcard or i > 0) - - return - - processOverlaps = (timestamps, root, skip) -> - skip = 1 if not skip - for i in [skip...timestamps.length] by 1 - for j in [0...i] by 1 - if timestamps[i][0].length > timestamps[j][0].length - t = timestamps[j] - tt = timestamps[i] - else - t = timestamps[i] - tt = timestamps[j] - - overlap = common.pathOverlap(t[0], tt[0], root) - - if overlap & 1 - if t[0].length == tt[0].length - tt[3 + (root - tt[1]) * 2] = Math.max(tt[3 + (root - tt[1]) * 2], t[3 + (root - t[1]) * 2]) - else - tt[4 + (root - tt[1]) * 2] = Math.max(tt[4 + (root - tt[1]) * 2], t[4 + (root - t[1]) * 2]) - - if overlap & 2 - t[3 + (root - t[1]) * 2] = Math.max(t[3 + (root - t[1]) * 2], tt[3 + (root - t[1]) * 2]) - - return - - recursive = (ref, path, timestamps, timestampThreshold, revisionThreshold) -> - declaration = ref[0]?[0]?[0]?.slice() or [] - base = ref[0]?[0]?[1]?.slice() or [] - current = [] - - # Find timestamp and revision in which this parameter was most recently - # created in order to ignore older properties - for t in timestamps - for j in [Math.min(revision, t[2].length - 1)..1] by -1 when t[2][j]? - if not (t[2][j] <= ref[0]?[0]?[j]?[0]) and t[2][j] >= timestampThreshold - timestampThreshold = Math.max(timestampThreshold, t[2][j]) - revisionThreshold = Math.max(revisionThreshold, j) - current[0] = timestampThreshold - break - - # Collect current properties - if ref[0]?[0]? - for i in [revisionThreshold...Math.min(revision + 1, ref[0][0].length)] by 1 when (rev = ref[0][0][i])? - for j in [0...rev.length] by 2 when rev[j]? - if rev[j] >= timestampThreshold - current[j] = rev[j] - current[j + 1] = rev[j + 1] - else - current[j] ?= 0 - - # Return if parameter no longer exists - if not (declaration[1] or base[1] or current[1]) - l = timestamps.length - collectTimestamps(ref, path, path.length - 1, timestamps) - processOverlaps(timestamps, path.length - 1, l) - return - - tentativeTimestamp = -1 - if current[2]? and not current[3] - tentativeTimestamp = current[2] - - filteredTimestamps = timestamps.filter((t) -> - if t[0][path.length - 1]? - if t[0][path.length - 1]? and t[0][path.length - 1] == path[path.length - 1] - t[2 + (path.length - t[1]) * 2] = Math.max(t[2 + (path.length - t[1]) * 2], current[0]) - else - return false - - if t[0].length > path.length - t[3 + (path.length - t[1]) * 2] = t[1 + (path.length - t[1]) * 2] - t[4 + (path.length - t[1]) * 2] = tentativeTimestamp - return true - - if not declaration[0] or t[2][0] > declaration[0] - declaration[0] = t[2][0] - - return false - ) - - collectTimestamps(ref, path, path.length, filteredTimestamps) - - processOverlaps(filteredTimestamps, path.length, 0) - - if path.length < pattern.length - children = {} - for k, v of ref[1] - if not pattern[path.length]? or k == pattern[path.length] - r = recursive(v, path.concat(k), filteredTimestamps, timestampThreshold, revisionThreshold) - children[k] = r if r? - - descendantTimestamps = filteredTimestamps.map((t) -> - res = [t[0], t[1]] - res[2] = t[2][0] if t[2][0]? - res[3] = t[2][1] if t[2][1]? - - cur = Math.max(t[3 + (path.length - t[1]) * 2], t[4 + (path.length - t[1]) * 2]) - - if path.length > t[1] - t[2 + (path.length - t[1]) * 2] = Math.min(t[2 + (path.length - t[1]) * 2], cur) - - if cur >= 0 - res[4] = cur - - return res - ).filter((t) -> t.length > 1) - - # Match value type of current if no type is specified - if declaration[7]? and not declaration[7][1]? - declaration[7] = sanitizeParameterValue([declaration[7][0], current[7]?[1]]) - - return callback(path, declaration, base, current, descendantTimestamps, children) - - return recursive(deviceData, [], [], 0, 1) - - -collapse = (deviceData, revision, wildcard) -> - ref = deviceData - - if ref[0]? - for depth, i in ref[0] when depth?.length > revision + 2 - if i == 0 and not wildcard - depth[revision + 1] ?= [] - for j in [revision + 2...depth.length] by 1 when depth[j]? - for k in [0...depth[j].length] by 2 when depth[j][k]? - depth[revision + 1][k] = depth[j][k] - depth[revision + 1][k + 1] = depth[j][k + 1] - else - for j in [revision + 2...depth.length] by 1 when depth[j]? - depth[revision + 1] = depth[j] - - depth.length = revision + 2 - - for i in [1...ref.length] by 1 when ref[i]? - for k, v of ref[i] - collapse(v, revision, wildcard or i != 1) - - -clearDeclarations = (deviceData, prefix) -> - ref = deviceData - - if prefix? - for p in prefix - ref = ref[1]?[p] - return if not ref? - - if ref[0]? - for depth in ref[0] when depth? - depth[0] = null - - for i in [1...ref.length] by 1 when ref[i]? - for k, v of ref[i] - clearDeclarations(v) - - -getPrerequisiteDeclarations = (declarations) -> - dec = [declarations[0], 1] - for i in [1...declarations.length] by 2 - if declarations[i]? - dec[i] = declarations[i] - else if declarations[i + 1]? - dec[i] = 1 - return [dec] - - -getAll = (deviceData, pattern, revision) -> - allParameters = [] - traverse(deviceData, pattern, revision, (path, declaration, base, current, descendantTimestamps, children) -> - if path.length == pattern.length and current[1] - allParameters.push([path].concat(current)) - ) - return allParameters - - -exports.getPrerequisiteDeclarations = getPrerequisiteDeclarations -exports.init = init -exports.getAll = getAll -exports.declare = declare -exports.set = set -exports.traverse = traverse -exports.collapse = collapse -exports.clearDeclarations = clearDeclarations -exports.sanitizeParameterValue = sanitizeParameterValue diff --git a/lib/device.js b/lib/device.js new file mode 100644 index 0000000..7a9508e --- /dev/null +++ b/lib/device.js @@ -0,0 +1,277 @@ +/** + * Copyright 2013-2016 Zaid Abdulla + * + * 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 . + */ +"use strict"; + + +function sanitizeParameterValue(parameterValue) { + if (parameterValue[0] != null) { + switch (parameterValue[1]) { + case 'xsd:boolean': + if (typeof parameterValue[0] !== 'boolean') { + parameterValue = parameterValue.slice(); + parameterValue[0] = !!JSON.parse(parameterValue[0]); + } + break; + case 'xsd:string': + if (typeof parameterValue[0] !== 'string') { + parameterValue = parameterValue.slice(); + parameterValue[0] = '' + parameterValue[0]; + } + break; + case 'xsd:int': + case 'xsd:unsignedInt': + if (typeof parameterValue[0] !== 'number') { + parameterValue = parameterValue.slice(); + parameterValue[0] = +parameterValue[0]; + } + break; + case 'xsd:dateTime': + if (typeof parameterValue[0] !== 'number') { + parameterValue = parameterValue.slice(); + if (parameterValue[0].getTime() != null) + parameterValue[0] = parameterValue[0].getTime(); + else if (isNaN(parameterValue[0])) + parameterValue[0] = Date.parse(parameterValue[0]); + else + parameterValue[0] = +parameterValue[0]; + } + break; + default: + if (parameterValue[1] != null) + throw new Error(`Parameter value type "${parameterValue[1]}" not recognized.`); + + parameterValue[0] = JSON.parse(JSON.stringify(parameterValue[0])); + } + } + return parameterValue; +} + + +function getAliasDeclarations(path) { + // TODO Return declarations necessary for unpacking aliases + return []; +} + + +function inferTimestamps(deviceData, path) { + var res = {}; + + for (let sup of deviceData.paths.superset(path)) { + for (let attrName in deviceData.timestamps) { + let t = deviceData.timestamps[attrName].get(sup); + if (t && !(t < res[sup])) + res[attrName] = t; + } + } + + for (let i = path.length - 1; i > 0; -- i) { + if (path.wildcards & ((2 << i) - 1) === 0) + break; + + let param = null; + let ancestor = path.slice(0, i); + let ancestorTimestamp = 0; + for (let sup of deviceData.paths.superset(ancestor)) { + if (!sup.wildcards) + param = sup; + + let t = deviceData.timestamps.exist.get(sup); + if (t > ancestorTimestamp) + ancestorTimestamp = t; + } + + if (param != null && deviceData.values.exist.get(param)) { + if (deviceData.values.object.get(param) == 0) { + let t = deviceData.timestamps.object.get(param); + for (let attrName in deviceData.timestamps) + if (!(t <= res[attrName])) + res[attrName] = t; + } + } + else if (ancestorTimestamp) { + let newt = {}; + for (let attrName in deviceData.timestamps) + newt[attrName] = ancestorTimestamp; + + for (let sub of deviceData.paths.subset(ancestor)) { + if (sub.wildcards) + continue; + let subt = inferTimestamps(deviceData, sub.concat(path.slice(sub.length, path.length))); + for (let attrName in newt) { + if (!(attrName in subt)) + delete newt[attrName]; + else if (subt[attrName] < newt[attrName]) + newt[attrName] = subt[attrName]; + } + } + for (let attrName in newt) + if (!(newt[attrName] <= res[attrName])) + res[attrName] = newt[attrName]; + } + } + + return res; +} + + +function unpack(deviceData, path, revision) { + // TODO implement alias unpacking + return [path]; +} + + +function invalidateSuperset(deviceData, path, attributes) { + throw new Error('Not implemented'); +} + + +function normalizeAttributes(timestamps, values) { + // TODO Consider moving implemention within set function + if (!values) + return; + + if ('value' in values && timestamps.value >= (timestamps.object || 0)) { + values.object = 0; + timestamps.object = timestamps.value; + } + + if (values.object && timestamps.object > (timestamps.value || 0)) { + delete values.value; + timestamps.value = timestamps.object; + } + + if ('value' in values && timestamps.value >= (timestamps.exist || 0)) { + values.exist = 1; + timestamps.exist = timestamps.value; + } + + if ('object' in values && timestamps.object >= (timestamps.exist || 0)) { + values.exist = 1; + timestamps.exist = timestamps.object; + } + + if ('writable' in values && timestamps.writable >= (timestamps.exist || 0)) { + values.exist = 1; + timestamps.exist = timestamps.writable; + } +} + + +function set(deviceData, path, timestamps, values) { + // TODO Optimize + // TODO Allow overwriting attribute value with same timestamp + path = deviceData.paths.add(path); + + if (!path.wildcards) { + normalizeAttributes(timestamps, values); + if (values.exist && path.length > 1) + set(deviceData, path.slice(0, path.length - 1), + {exist: timestamps.exist, object: timestamps.exist}, + {exist: 1, object: 1}); + } + + var current = inferTimestamps(deviceData, path); + + for (let c in current) + if (current[c] > timestamps[c] || (current[c] == timestamps[c] && path.wildcards > 0))//!deviceData.values[c].has(path))) + delete timestamps[c]; + + if (timestamps.exist != null) { + deviceData.timestamps.exist.set(path, timestamps.exist); + if (values != null && values.exist != null) + deviceData.values.exist.set(path, values.exist); + } + + current = inferTimestamps(deviceData, path); + + for (let c in current) + if (current[c] > timestamps[c] || (current[c] == timestamps[c] && path.wildcards > 0))//!deviceData.values[c].has(path))) + delete timestamps[c]; + + let toInvalidate = new Map(); + + for (let sub of deviceData.paths.subset(path, 0)) { + if (path.wildcards == sub.wildcards) + continue; + + for (let attrName in timestamps) { + if (deviceData.timestamps[attrName].get(sub) <= timestamps[attrName]) { + + if (sub.wildcards || !deviceData.values[attrName].has(sub)) + deviceData.timestamps[attrName].delete(sub); + else if (deviceData.timestamps[attrName].get(sub) < timestamps[attrName]) { + deviceData.timestamps[attrName].delete(sub); + deviceData.values[attrName].delete(sub); + let inv = toInvalidate.get(sub) || {}; + if (!timestamps[attrName] <= inv[attrName]) { + inv[attrName] = timestamps[attrName]; + toInvalidate.set(sub, inv); + } + } + } + } + } + + for (let attrName in timestamps) { + let existing = deviceData.timestamps[attrName].get(path); + + if (existing == null || timestamps[attrName] >= existing) { + deviceData.timestamps[attrName].set(path, timestamps[attrName]); + let invalidate = false; + if (values[attrName] != null) { + if (!deviceData.values[attrName].has(path)) + invalidate = true; + deviceData.values[attrName].set(path, values[attrName]); + } + else { + if (deviceData.values[attrName].has(path)) + invalidate = true; + deviceData.values[attrName].delete(path) + } + + if (invalidate) { + let inv = toInvalidate.get(path) || {}; + if (!timestamps[attrName] <= inv[attrName]) { + inv[attrName] = timestamps[attrName]; + toInvalidate.set(sub, inv); + } + } + } + } + + toInvalidate.forEach(function(p, a) { + invalidateSuperset(deviceData, p, a); + }); + + // TODO recalculate descendant superset + // var depth = 0; + // if (timestamps.exist != null) + // depth = 99; + // + // for (let sup of deviceData.paths.superset(path, depth)) { + // if ((sup.wildcards && (3 << path.length) - 1) != path.wildcards) { + // throw new Error('Not implemented'); + // } + // } +} + + +exports.sanitizeParameterValue = sanitizeParameterValue; +exports.getAliasDeclarations = getAliasDeclarations; +exports.inferTimestamps = inferTimestamps; +exports.unpack = unpack; +exports.set = set; diff --git a/lib/sandbox.coffee b/lib/sandbox.coffee index a7122be..ec61e12 100644 --- a/lib/sandbox.coffee +++ b/lib/sandbox.coffee @@ -26,6 +26,7 @@ class Exit; sandbox = { + timestamp: null deviceData: null args: null revision: null @@ -36,6 +37,16 @@ sandbox = { context: vm.createContext() } +sandbox.context.Date = class + constructor: (arg) -> + if arguments.length + return new (Function.prototype.bind.apply(Date, arguments)) + + return new Date(sandbox.timestamp) + + now: () -> + return sandbox.timestamp + sandbox.context.ext = () -> if sandbox.extensions?.length @@ -56,66 +67,47 @@ sandbox.context.declare = (decs) -> if sandbox.revision == sandbox.maxRevision + 1 for k, v of decs - sandbox.declarations.push([common.parsePath(k)].concat(toIndexDeclaration(v))) + decT = {} + decV = {} + for attrName, dec of v + if Array.isArray(dec) + decT[attrName] = dec[0] + decV[attrName] = dec[1] if 1 in dec + else + decT[attrName] = dec + + sandbox.declarations.push([common.parsePath(k), decT, decV]) throw new Exit() res = {} for k, v of decs path = common.parsePath(k) - all = device.getAll(sandbox.deviceData, path, sandbox.revision) - res[k] = {} - for a in all - r = {} - r.exist = a[1] if v.exist? - r.type = a.slice(3, 5) if v.type? - r.writable = a.slice(5, 7) if v.writable? - r.value = a.slice(7, 9) if v.value? and a[8]? - p = a[0].join('.') - if p == k - res[k] = r - else - res[k][p] = r + res[k] = {} + unpacked = device.unpack(sandbox.deviceData, path, sandbox.revision) + for path in unpacked + iter = sandbox.deviceData.paths.subset(path) + while (param = iter.next().value)? + if param.wildcards == 0 and sandbox.deviceData.values.exist.get(param, sandbox.revision) + r = {} + for attrName, dec of v + if (attrValue = sandbox.deviceData.values[attrName].get(param, sandbox.revision))? + r[attrName] = [sandbox.deviceData.timestamps[attrName].get(param, sandbox.revision), attrValue] + + p = param.join('.') + if p == k + res[k] = r + else + res[k][p] = r return res Object.freeze(sandbox.context) -# Convert from friendly declaration format -toIndexDeclaration = (keyDeclaraiton) -> - indexDeclaraiton = [] - - if keyDeclaraiton['exist']? - if Array.isArray(keyDeclaraiton['exist']) - indexDeclaraiton[0] = keyDeclaraiton['exist'][0] - else - indexDeclaraiton[0] = keyDeclaraiton['exist'] - - if keyDeclaraiton['type']? - if Array.isArray(keyDeclaraiton['type']) - indexDeclaraiton[2] = keyDeclaraiton['type'][0] - else - indexDeclaraiton[2] = keyDeclaraiton['type'] - - if keyDeclaraiton['writable']? - if Array.isArray(keyDeclaraiton['writable']) - indexDeclaraiton[4] = keyDeclaraiton['writable'][0] - else - indexDeclaraiton[4] = keyDeclaraiton['writable'] - - if keyDeclaraiton['value']? - if Array.isArray(keyDeclaraiton['value']) - indexDeclaraiton[6] = keyDeclaraiton['value'][0] - indexDeclaraiton[7] = keyDeclaraiton['value'][1] - else - indexDeclaraiton[6] = keyDeclaraiton['value'] - - return indexDeclaraiton - - -run = (script, args, deviceData, extensionsCache, startRevision, maxRevision) -> +run = (script, args, timestamp, deviceData, extensionsCache, startRevision, maxRevision) -> + sandbox.timestamp = timestamp sandbox.deviceData = deviceData sandbox.extensionsCache = extensionsCache sandbox.args = args @@ -125,7 +117,7 @@ run = (script, args, deviceData, extensionsCache, startRevision, maxRevision) -> sandbox.extensions = null try - ret = script.runInNewContext(sandbox.context) + ret = script.runInNewContext(sandbox.context, {displayErrors: false}) return {done: true, declarations: sandbox.declarations, returnValue: ret} catch err throw err if err not instanceof Exit diff --git a/lib/session.coffee b/lib/session.coffee index 96cba26..b751876 100644 --- a/lib/session.coffee +++ b/lib/session.coffee @@ -24,17 +24,30 @@ device = require './device' sandbox = require './sandbox' cache = require './cache' extensions = require './extensions' +PathSet = require './path-set' +VersionedMap = require './versioned-map.js' + + +initDeviceData = () -> + return { + paths : new PathSet() + timestamps : {exist: new VersionedMap(), object: new VersionedMap(), writable: new VersionedMap(), value: new VersionedMap()} + values : {exist: new VersionedMap(), object: new VersionedMap(), writable: new VersionedMap(), value: new VersionedMap()} + declarationTimestamps : new Map() + declarationValues : new Map() + virtualParameterTimestamps : new Map() + virtualParameterValues : new Map() + loaded : new Map() + } init = (deviceId, cwmpVersion, timeout) -> timestamp = Date.now() - deviceData = device.init() - device.set(deviceData, [], 0, [timestamp, 1, timestamp, 1, timestamp, 0, timestamp, null]) sessionData = { timestamp : timestamp deviceId : deviceId - deviceData : deviceData + deviceData : initDeviceData() cwmpVersion : cwmpVersion timeout : timeout provisions: [] @@ -47,24 +60,62 @@ init = (deviceId, cwmpVersion, timeout) -> return sessionData -loadParameters = (sessionData, toLoad, callback) -> - # TODO optimize by removing overlaps - for p, i in toLoad - device.set(sessionData.deviceData, p, 0, [0, null, 0, null, 0, null, 0, null]) - - if sessionData.new +loadParameters = (sessionData, toLoad, virtualParameters, callback) -> + if not toLoad.length return callback() - db.fetchDevice(sessionData.deviceId, sessionData.timestamp, toLoad, (err, parameters) -> + db.fetchDevice(sessionData.deviceId, sessionData.timestamp, toLoad, (err, parameters, loaded) -> return callback(err) if err if not parameters? # Device not available in database, mark as new sessionData.new = true + path = sessionData.deviceData.paths.add([]) + sessionData.deviceData.loaded.set(path, 99) return callback() + for p in loaded + path = sessionData.deviceData.paths.add(p[0]) + + if p[1] + l = sessionData.deviceData.loaded.get(path) || 0 + sessionData.deviceData.loaded.set(path, Math.max(l, p[1])) + for p in parameters - device.set(sessionData.deviceData, p[0], 0, p.slice(1)) + path = sessionData.deviceData.paths.add(p[0]) + + for k, v of p[1] + sessionData.deviceData.timestamps[k].set(path, v, 0) + + for k, v of p[2] + sessionData.deviceData.values[k].set(path, v, 0) + + # Virtual parameters + for p in toLoad + p = sessionData.deviceData.paths.add(p) + if p[0] == '*' or p[0] == 'VirtualParameters' + if p.length == 1 + sessionData.deviceData.timestamps.exist.set(p, sessionData.timestamp, 0) + sessionData.deviceData.timestamps.object.set(p, sessionData.timestamp, 0) + sessionData.deviceData.timestamps.writable.set(p, sessionData.timestamp, 0) + sessionData.deviceData.values.exist.set(p, 1, 0) + sessionData.deviceData.values.object.set(p, 1, 0) + sessionData.deviceData.values.writable.set(p, 0, 0) + else if p.length == 2 + if not p[1]? + sessionData.deviceData.timestamps.exist.set(p, sessionData.timestamp, 0) + + for k, v of virtualParameters + p = sessionData.deviceData.paths.add(p) + sessionData.deviceData.timestamps.exist.set(p, sessionData.timestamp, 0) + sessionData.deviceData.timestamps.object.set(p, sessionData.timestamp, 0) + sessionData.deviceData.values.exist.set(p, 1, 0) + sessionData.deviceData.values.object.set(p, 0, 0) + else if virtualParameters[p[1]]? + sessionData.deviceData.timestamps.exist.set(p, sessionData.timestamp, 0) + sessionData.deviceData.timestamps.object.set(p, sessionData.timestamp, 0) + sessionData.deviceData.values.exist.set(p, 1, 0) + sessionData.deviceData.values.object.set(p, 0, 0) return callback() ) @@ -79,263 +130,478 @@ generateRpcId = (sessionData) -> inform = (sessionData, rpcReq, callback) -> timestamp = sessionData.timestamp - device.set(sessionData.deviceData, ['DeviceID', 'Manufacturer'], 1, [timestamp, 1, timestamp, 0, timestamp, 0, timestamp, [rpcReq.deviceId.Manufacturer, 'xsd:string']]) - device.set(sessionData.deviceData, ['DeviceID', 'OUI'], 1, [timestamp, 1, timestamp, 0, timestamp, 0, timestamp, [rpcReq.deviceId.OUI, 'xsd:string']]) - device.set(sessionData.deviceData, ['DeviceID', 'ProductClass'], 1, [timestamp, 1, timestamp, 0, timestamp, 0, timestamp, [rpcReq.deviceId.ProductClass, 'xsd:string']]) - device.set(sessionData.deviceData, ['DeviceID', 'SerialNumber'], 1, [timestamp, 1, timestamp, 0, timestamp, 0, timestamp, [rpcReq.deviceId.SerialNumber, 'xsd:string']]) + + params = [] + params.push([['DeviceID', 'Manufacturer'], + {exist: timestamp, object: timestamp, writable: timestamp, value: timestamp}, + {exist: 1, object: 0, writable: 0, value: [rpcReq.deviceId.Manufacturer, 'xsd:string']}]) + + params.push([['DeviceID', 'OUI'], + {exist: timestamp, object: timestamp, writable: timestamp, value: timestamp}, + {exist: 1, object: 0, writable: 0, value: [rpcReq.deviceId.OUI, 'xsd:string']}]) + + params.push([['DeviceID', 'ProductClass'], + {exist: timestamp, object: timestamp, writable: timestamp, value: timestamp}, + {exist: 1, object: 0, writable: 0, value: [rpcReq.deviceId.ProductClass, 'xsd:string']}]) + + params.push([['DeviceID', 'SerialNumber'], + {exist: timestamp, object: timestamp, writable: timestamp, value: timestamp}, + {exist: 1, object: 0, writable: 0, value: [rpcReq.deviceId.SerialNumber, 'xsd:string']}]) for p in rpcReq.parameterList - device.set(sessionData.deviceData, common.parsePath(p[0]), 1, [timestamp, 1, timestamp, 0, null, null, timestamp, p.slice(1)]) + path = common.parsePath(p[0]) + params.push([path, + {exist: timestamp, object: timestamp, value: timestamp}, + {exist: 1, object: 0, value: p.slice(1)}]) - device.set(sessionData.deviceData, ['Events', 'Inform'], 1, [timestamp, 1, timestamp, 0, timestamp, 0, timestamp, [timestamp, 'xsd:dateTime']]) + params.push([['Events', 'Inform'], + {exist: timestamp, object: timestamp, writable: timestamp, value: timestamp}, + {exist: 1, object: 0, writable: 0, value: [timestamp, 'xsd:dateTime']}]) for e in rpcReq.event - device.set(sessionData.deviceData, ['Events', e.replace(' ', '_')], 1, [timestamp, 1, timestamp, 0, timestamp, 0, timestamp, [timestamp, 'xsd:dateTime']]) + params.push([['Events', e.replace(' ', '_')], + {exist: timestamp, object: timestamp, writable: timestamp, value: timestamp}, + {exist: 1, object: 0, writable: 0, value: [timestamp, 'xsd:dateTime']}]) - return callback(null, {type : 'InformResponse'}) + toLoad = [] + for p in params + tl = [] + for i in [p[0].length...0] by -1 + path = p[0].slice(0, i) + loaded = 0 + iter = sessionData.deviceData.paths.superset(path) + while sup = iter.next().value + if (l = sessionData.deviceData.loaded.get(sup)) > loaded + loaded = l + + if loaded + tl = tl.slice(0, tl.length - (loaded - 1)) + break + + tl.push(path) + + toLoad = toLoad.concat(tl) + + loadParameters(sessionData, toLoad, null, (err) -> + return callback(err) if err + + v.revision = 1 for k, v of sessionData.deviceData.timestamps + v.revision = 1 for k, v of sessionData.deviceData.values + + for p in params + device.set(sessionData.deviceData, p[0], p[1], p[2]) + + return callback(null, {type : 'InformResponse'}) + ) addProvisions = (sessionData, provisions) -> sessionData.provisions = sessionData.provisions.concat(provisions) if sessionData.revisions.length > 1 or sessionData.revisions[0] > 0 + v.collapse(1) for k, v of sessionData.timestamps + v.collapse(1) for k, v of sessionData.values sessionData.cycle += 1 sessionData.rpcCount = 0 sessionData.revisions = [0] - device.collapse(sessionData.deviceData, 1) sessionData.extensionsCache.length = 0 clearProvisions = (sessionData) -> sessionData.provisions = [] if sessionData.revisions.length > 1 or sessionData.revisions[0] > 0 + v.collapse(1) for k, v of sessionData.deviceData.timestamps + v.collapse(1) for k, v of sessionData.deviceData.values sessionData.cycle += 1 sessionData.rpcCount = 0 sessionData.revisions = [0] - device.collapse(sessionData.deviceData, 1) sessionData.extensionsCache.length = 0 + generateRpcRequest = (sessionData) -> - res = device.traverse(sessionData.deviceData, null, null, (path, declaration, base, current, descendantTimestamps, children) -> - if path[0] == 'Tags' - if path.length == 2 and path[1]? and declaration[7]? - if declaration[7][0] != current[7][0] - device.set(sessionData.deviceData, path, 1, [sessionData.timestamp, 1, null, null, null, null, sessionData.timestamp, declaration[7]]) + if sessionData.deviceData.declarationTimestamps.size == 0 and + sessionData.deviceData.declarationValues.size == 0 + return null + + refreshPaths = new PathSet() + refreshAttributes = { + exist: new Set() + object: new Set() + writable: new Set() + value: new Set() + } + setValues = new Map() + refreshGpn = new Map() + + revision = sessionData.revisions.reduce(((a, b) -> a + (b >> 1)), 0) + 1 + + test = (deviceData, path, timestamps, values) -> + iter = deviceData.paths.superset(path) + param = null + existTimestamp = 0 + while sup = iter.next().value + param = sup if not sup.wildcards + for k, v of timestamps + continue if not (t = deviceData.timestamps[k].get(sup, revision))? + existTimestamp = Math.max(existTimestamp, t) if k is 'exist' + delete timestamps[k] if t >= v + + if not Object.keys(timestamps).length and + not (values? and Object.keys(values).length) return - r = {} - gpn = false + if param? + if deviceData.values.exist.get(param, revision) + param = refreshPaths.add(param) + isObject = deviceData.values.object.get(param, revision) + isWritable = deviceData.values.writable.get(param, revision) + for k of timestamps + if k isnt 'value' or isObject == 0 + refreshAttributes[k].add(param) + else if not isObject? + refreshAttributes.object.add(param) - descendantRefresh = [] - for i in [0...descendantTimestamps.length] by 1 when descendantTimestamps[i][4]? or descendantTimestamps[i][2]? - if descendantTimestamps[i][2] > descendantTimestamps[i][4] - if current[3] == 1 - descendantRefresh[i] = true - else - r.object = true if current[2] < descendantTimestamps[i][2] + if values?.value? + if isObject == 0 and isWritable == 1 + curVal = deviceData.values.value.get(param, revision) + cur = setValues.get(param) + if not cur? + cur = [deviceData.values.value.get(param, revision), values.value] + setValues.set(param, cur) + else + cur[1] = values.value + else + if not isObject? + refreshAttributes.object.add(param) + if not isWritable? + refreshAttributes.writable.add(param) + return + else if existTimestamp >= (timestamps.exist or 1) + return + else if path.indexOf('*') >= 0 + subIter = deviceData.paths.subset(path) + while sub = subIter.next().value + continue if not deviceData.values.exist.get(sub, revision) - for j in [0...i] by 1 when descendantTimestamps[j][4]? or descendantRefresh[j] - overlap = common.pathOverlap(descendantTimestamps[j][0], descendantTimestamps[i][0], path.length) + isObject = deviceData.values.object.get(sub, revision) + isWritable = deviceData.values.writable.get(sub, revision) - if overlap & 1 and descendantTimestamps[j][4]? - if descendantRefresh[i] and descendantTimestamps[j][4] >= descendantTimestamps[i][2] - descendantRefresh[i] = false + for k, v of timestamps + if not (v <= deviceData.timestamps[k].get(sub, revision)) + if k isnt 'value' or isObject == 0 + sub = refreshPaths.add(sub) + refreshAttributes[k].add(sub) + else if not isObject? + sub = refreshPaths.add(sub) + refreshAttributes.object.add(sub) - if overlap & 2 and descendantTimestamps[i][4]? - if descendantRefresh[j] and descendantTimestamps[i][4] >= descendantTimestamps[j][2] - descendantRefresh[j] = false + if values?.value? + if isObject == 0 and isWritable == 1 + curVal = deviceData.values.value.get(sub, revision) + cur = setValues.get(sub) + if not cur? + cur = [deviceData.values.value.get(sub, revision), values.value] + setValues.set(sub, cur) + else + cur[1] = values.value + else + if not isObject? + refreshAttributes.object.add(sub) + if not isWritable? + refreshAttributes.writable.add(sub) - for d in descendantRefresh when d - gpn = true - break + if existTimestamp >= (timestamps.exist or 1) + return - if declaration.length - if declaration[0] > current[0] - r.exist = true + childrenTimestamp = existTimestamp - if declaration[2] > current[2] - r.object = true - r.exist = false + for i in [path.length - 1...0] by -1 + existTimestamp = 0 + p = path.slice(0, i) + iter = deviceData.paths.superset(p) + param = null + while sup = iter.next().value + param = sup if not sup.wildcards + if (t = deviceData.timestamps.exist.get(sup, revision))? + existTimestamp = Math.max(existTimestamp, t) - if declaration[4] > current[4] - r.writable = true - r.exist = false + if param? + if deviceData.values.exist.get(param, revision) + isObject = deviceData.values.object.get(param, revision) + objectTimestamp = deviceData.timestamps.object.get(param, revision) + if isObject + if childrenTimestamp == 0 + param = refreshPaths.add(param) + refreshGpn.set(param, refreshGpn.get(param) | ((1 << path.length - i) - 1)) + else if childrenTimestamp < (timestamps.exist or 1) + param = refreshPaths.add(param) + refreshGpn.set(param, refreshGpn.get(param) or 1) + if not (objectTimestamp >= (timestamps.exist or 1)) + param = refreshPaths.add(param) + refreshAttributes.object.add(param) + return + else if existTimestamp >= (timestamps.exist or 1) + return + else if p.indexOf('*') >= 0 + subIter = deviceData.paths.subset(p) + ret = true + while sub = subIter.next().value + continue if not deviceData.values.exist.get(sub, revision) + # TODO check if not object + pp = sub.concat(path.slice(sub.length)) + ts = {} + ts[k] = v for k, v of timestamps + ret = ret and test(deviceData, pp, ts) - if declaration[6] > current[6] - if current[7]? or current[3] == 0 - r.gpv ?= [] - r.gpv.push(path) - r.object = false - r.exist = false - gpn = false - else if not current[2] > 0 - r.object = true + if existTimestamp >= (timestamps.exist or 1) + return - if declaration[7]? and current[7]? - if current[4] == 0 - r.writable = true - else if current[5] - if declaration[7][0] != current[7][0] or declaration[7][1] != current[7][1] - r.spv = [[path, declaration[7][0], declaration[7][1]]] - r.exist = false - gpn = false + childrenTimestamp = existTimestamp - for k, v of children - if v.writable or v.object or v.exist - gpn = true + p = refreshPaths.add([]) + refreshGpn.set(p, refreshGpn.get(p) or 1) + return false - if v.gpn - r.gpn = (r.gpn ? []).concat(v.gpn) - r.exist = false + iter = sessionData.deviceData.declarationTimestamps.entries() + while (pair = iter.next().value) + timestamps = {} + timestamps[k] = v for k, v of pair[1] + test(sessionData.deviceData, pair[0], timestamps, sessionData.deviceData.declarationValues.get(pair[0])) - if v.gpv - r.gpv = (r.gpv ? []).concat(v.gpv) - r.exist = false - if v.spv - r.spv = (r.spv ? []).concat(v.spv) - r.exist = false + iter = refreshAttributes.exist.values() + while (path = iter.next().value) + descendantIter = refreshPaths.subset(path, 99) + found = false + while (p = descendantIter.next().value)? + if refreshAttributes.value.has(p) or + refreshAttributes.object.has(p) or + refreshAttributes.writable.has(p) or + refreshGpn.has(p) + found = true + break + if not found + path = refreshPaths.add(path.slice(0, -1)) + refreshGpn.set(path, refreshGpn.get(path) or 1) - if gpn - r.exist = false - if r.gpn? - r.gpn = [path].concat(r.gpn) - else - r.gpn = [path] - return r - ) + iter = refreshAttributes.object.values() + while (path = iter.next().value) + descendantIter = refreshPaths.subset(path, 99) + found = false + while (p = descendantIter.next().value)? + if refreshAttributes.value.has(p) or + (p.length > path.length and + (refreshAttributes.object.has(p) or + refreshAttributes.writable.has(p))) + found = true + break + if not found + path = refreshPaths.add(path.slice(0, -1)) + refreshGpn.set(path, refreshGpn.get(path) or 1) + + iter = refreshAttributes.writable.values() + while (path = iter.next().value) + path = refreshPaths.add(path.slice(0, -1)) + refreshGpn.set(path, refreshGpn.get(path) or 1) + + iter = refreshGpn.keys() + while (path = iter.next().value) + descendantIter = refreshPaths.subset(path, 99) + found = false + while (p = descendantIter.next().value)? + continue if p == path + if (flags = refreshGpn.get(p)) + flags <<= p.length - path.length + refreshGpn.set(path, refreshGpn.get(path) | flags) + refreshGpn.delete(p) + + rpcReq = null + completed = true + + if refreshGpn.size + GET_PARAMETER_NAMES_DEPTH_THRESHOLD = + config.get('GET_PARAMETER_NAMES_DEPTH_THRESHOLD', sessionData.deviceId) + + pair = refreshGpn.entries().next().value + + nextLevel = true + if pair[0].length >= GET_PARAMETER_NAMES_DEPTH_THRESHOLD + nextLevel = false + else if common.hammingWeight(pair[1] >> 3) >= 3 + nextLevel = false + + if refreshGpn.size > 1 or (pair[1] > 1 and nextLevel) + completed = false - if res.gpn? rpcReq = { type: 'GetParameterNames' - parameterPath: res.gpn[0].join('.') - nextLevel: true + parameterPath: pair[0].join('.') + nextLevel: nextLevel } - return rpcReq - if res.gpv? - rpcReq = { - type: 'GetParameterValues' - parameterNames: (p.join('.') for p in res.gpv.slice(0, config.get('TASK_PARAMETERS_BATCH_SIZE', sessionData.deviceId))) - } - return rpcReq + if refreshAttributes.value.size + if rpcReq? + completed = false + else + TASK_PARAMETERS_BATCH_SIZE = + config.get('TASK_PARAMETERS_BATCH_SIZE', sessionData.deviceId) - if res.spv? - rpcReq = { - type: 'SetParameterValues' - parameterList: ([p[0].join('.'), p[1], p[2]] for p in res.spv.slice(0, config.get('TASK_PARAMETERS_BATCH_SIZE', sessionData.deviceId))) - } - return rpcReq + parameterNames = [] + iter = refreshAttributes.value.values() + while (path = iter.next().value) and + parameterNames.length < TASK_PARAMETERS_BATCH_SIZE + parameterNames.push(path) - return null + if refreshAttributes.value.size > parameterNames.length + completed = false + rpcReq = { + type: 'GetParameterValues' + parameterNames: (p.join('.') for p in parameterNames) + } -loadDeclarations = (sessionData, virtualParameters, callback) -> - toLoad = [] - device.traverse(sessionData.deviceData, null, null, (path, declaration, base, current, descendantTimestamps, children) -> - descendantLoad = [] - for i in [0...descendantTimestamps.length] by 1 - if descendantTimestamps[i][2]? - if not descendantTimestamps[i][4]? - toLoad.push(descendantTimestamps[i][0]) - continue - else if descendantTimestamps[i][2] > descendantTimestamps[i][4] - descendantLoad[i] = true if descendantTimestamps[i][0].length > path.length + 1 - else if not descendantTimestamps[i][4]? - continue + TASK_PARAMETERS_BATCH_SIZE = + config.get('TASK_PARAMETERS_BATCH_SIZE', sessionData.deviceId) - for j in [0...i] by 1 when descendantTimestamps[j][4]? or descendantFlags[j] - overlap = common.pathOverlap(descendantTimestamps[j][0], descendantTimestamps[i][0], path.length) + parameterValues = [] + setValues.forEach((v, k) -> + return if completed == false - if overlap & 1 and descendantTimestamps[j][4]? - if descendantLoad[i] and descendantTimestamps[j][0].length == path.length + 1 - descendantLoad[i] = false - - if overlap & 2 and descendantTimestamps[i][4]? - if descendantLoad[j] and descendantTimestamps[i][0].length == path.length + 1 - descendantLoad[j] = false - - for d, j in descendantLoad when d - toLoad.push(path.concat(descendantTimestamps[j][0][path.length] ? null)) - - if declaration[7]? and not current[4]? - # Need writable attribute when declaring a value - toLoad.push(path) + if rpcReq? + completed = false return - for i in [0...declaration.length] by 2 - if declaration[i]? and not current[i]? - toLoad.push(path) + val = v[1].slice() + if not val[1]? + val[1] = v[0][1] + device.sanitizeParameterValue(val) + if val[0] != v[0][0] or val[1] != v[0][1] + if parameterValues.length >= TASK_PARAMETERS_BATCH_SIZE + completed = false + return + parameterValues.push([k, val[0], val[1]]) + ) + + if parameterValues.length and not rpcReq? + rpcReq = { + type: 'SetParameterValues' + parameterList: ([p[0].join('.'), p[1], p[2]] for p in parameterValues) + } + + if completed + sessionData.deviceData.declarationTimestamps.clear() + sessionData.deviceData.declarationValues.clear() + + return rpcReq + + +getParametersToLoad = (sessionData) -> + toLoad = [] + + test = (timestamps, path) -> + tl = [] + for i in [path.length...0] by -1 + p = path.slice(0, i) + loaded = 0 + iter = sessionData.deviceData.paths.superset(p, 1) + + while sup = iter.next().value + if (l = sessionData.deviceData.loaded.get(sup)) > loaded + loaded = l + + if loaded + tl = tl.slice(0, tl.length - (loaded - 1)) break - ) - # Virtual parameters - for p in toLoad - if not p[0]? or p[0] == 'VirtualParameters' - if p.length == 1 - device.set(sessionData.deviceData, ['VirtualParameters'], 1, [sessionData.timestamp, 1, sessionData.timestamp, 1, sessionData.timestamp, 0]) - else if p.length == 2 - if not p[1]? - for k, v of virtualParameters - device.set(sessionData.deviceData, ['VirtualParameters', k], 1, [sessionData.timestamp, 1, sessionData.timestamp, 0]) - else if virtualParameters[p[1]]? - device.set(sessionData.deviceData, ['VirtualParameters', p[1]], 1, [sessionData.timestamp, 1, sessionData.timestamp, 0]) + tl.push(p) - return loadParameters(sessionData, toLoad, callback) + toLoad = toLoad.concat(tl) + + sessionData.deviceData.declarationTimestamps.forEach(test) + sessionData.deviceData.virtualParameterTimestamps.forEach(test) + + return toLoad -extractVirtualParameterDeclarations = (sessionData) -> - virtualParameterDeclarations = [] - res = device.traverse(sessionData.deviceData, ['VirtualParameters', null], null, (path, declaration, base, current, descendantTimestamps, children) -> - if path.length == 2 and declaration?.length - d = [path] - for i in [0...declaration.length] by 2 - if declaration[i]? and not (declaration[i] <= current[i]) - d[i + 1] = declaration[i] - if declaration[i + 1]? - if i != 6 or not current[i + 1]? - d[i + 2] = declaration[i + 1] - else if declaration[i + 1][0] != current[i + 1][0] and - (declaration[i + 1][0] != current[i + 1][0] or not declaration[i + 1][1]?) - d[i + 2] = declaration[i + 1] - if d.length > 1 - virtualParameterDeclarations.push(d) - ) +extractVirtualParameterDeclarations = (deviceData, revision, virtualParameters) -> + if deviceData.virtualParameterTimestamps.size == 0 and + deviceData.virtualParameterValues.size == 0 + return {} + + iter = deviceData.paths.subset(['VirtualParameters', '*']) + virtualParameterDeclarations = {} + while path = iter.next().value + continue if path.wildcards or not virtualParameters[path[1]]? + + if declarationTimestamps = deviceData.virtualParameterTimestamps.get(path) + for k, v of declarationTimestamps + ct = deviceData.timestamps[k].get(path, revision) + if not (v <= ct) + virtualParameterDeclarations[path[1]] ?= {timestamps: {}, values: {}} + virtualParameterDeclarations[path[1]].timestamps[k] = + Math.max(v, virtualParameterDeclarations[path[1]].timestamps[k] ? 0) + + if declarationValues = deviceData.virtualParameterValues.get(path) + for k, v of declarationValues + virtualParameterDeclarations[path[1]] ?= {timestamps: {}, values: {}} + cv = deviceData.values[k].get(path, revision) + if not virtualParameterDeclarations[path[1]].values[k]? + virtualParameterDeclarations[path[1]].values[k] = [cv, v] + else + virtualParameterDeclarations[path[1]].values[k][1] = v + + for k, v of virtualParameterDeclarations + for attrName, attrValue of v.values + val = attrValue[1].slice() + val[1] = attrValue[0][1] if not val[1]? + device.sanitizeParameterValue(val) + if val[0] != attrValue[0][0] or val[1] != attrValue[0][1] + v.values[attrName] = attrValue[1] + else + delete v.values[attrName] + if Object.keys(v.values).length == 1 + if Object.keys(v.timestamps).length == 0 + delete virtualParameterDeclarations[k] + else + delete v.values[attrName] + + deviceData.virtualParameterTimestamps.clear() + deviceData.virtualParameterValues.clear() - device.clearDeclarations(sessionData.deviceData, ['VirtualParameters']) return virtualParameterDeclarations -commitVirtualParameter = (sessionData, parameterDeclaration, revision, update) -> - v = [] +commitVirtualParameter = (sessionData, name, declaration, update) -> + t = {} + v = {} if update.writable? if update.writable[0] <= 0 - v[4] = now + update.writable[0] + t.writable = sessionData.timestamp + update.writable[0] else - v[4] = Math.min(sessionData.timestamp, update.writable[0]) + t.writable = Math.min(sessionData.timestamp, update.writable[0]) - if parameterDeclaration[5]? - v[4] = Math.max(parameterDeclaration[5], v[4]) + if declaration.timestamps.writable? + t.writable = Math.max(declaration.timestamps.writable, t.writable) - v[5] = Boolean(JSON.parse(update.writable[1])) - else if parameterDeclaration[5]? or parameterDeclaration[6]? + v.writable = +update.writable[1] + else if declaration.timestamps.writable? throw new Error('Virtual parameter must provide declared attributes') if update.value? if update.value[0] <= 0 - v[6] = sessionData.timestamp + update.value[0] + t.value = sessionData.timestamp + update.value[0] else - v[6] = Math.min(now, update.value[0]) + t.value = Math.min(sessionData.timestamp, update.value[0]) - if parameterDeclaration[7]? - v[6] = Math.max(parameterDeclaration[7], v[6]) + if declaration.timestamps.value? + t.value = Math.max(declaration.timestamps.value, t.value) - v[7] = device.sanitizeParameterValue(update.value[1]) - else if parameterDeclaration[7]? or parameterDeclaration[8]? + v.value = device.sanitizeParameterValue(update.value[1]) + else if declaration.timestmaps.value? throw new Error('Virtual parameter must provide declared attributes') - device.set(sessionData.deviceData, parameterDeclaration[0], revision, v) + device.set(sessionData.deviceData, ['VirtualParameters', name], t, v) runExtensions = (sessionData, revision, _extensions, callback) -> @@ -361,10 +627,15 @@ runExtensions = (sessionData, revision, _extensions, callback) -> rpcRequest = (sessionData, declarations, callback) -> + if not declarations?.length + if (rpcReq = generateRpcRequest(sessionData))? + sessionData.rpcRequest = rpcReq + return callback(null, generateRpcId(sessionData), rpcReq) + allDeclarations = declarations?.slice() ? [] cache.getProvisionsAndVirtualParameters((err, presetsHash, provisions, virtualParameters) -> - return callback(err) if err or (sessionData.presetsHash? and sessionData.presetsHash != presetsHash) + return callback(err) if err or (sessionData.presetsHash? and sessionData.presetsHashpresetsHash) done = true _extensions = [] @@ -373,31 +644,38 @@ rpcRequest = (sessionData, declarations, callback) -> switch provision[0] when 'refresh' path = common.parsePath(provision[1]) - for i in [path.length...16] by 1 - path.length = i - allDeclarations.push([path.slice(), provision[2], null, 1, null, 1, null, provision[2]]) + l = path.length + path.length = 16 + path.fill('*', l) + t = provision[2] + t += sessionData.timestamp if t <= 0 + + for i in [l...path.length] by 1 + allDeclarations.push([path.slice(0, i), {exist: t, object: 1, writable: 1, value: t}]) when 'value' - allDeclarations.push([common.parsePath(provision[1]), 1, null, null, null, null, null, 1, [provision[2]]]) + allDeclarations.push([common.parsePath(provision[1]), {exist: 1, value: 1}, {value: [provision[2]]}]) when 'tag' - allDeclarations.push([[['Tags', provision[1]], null, null, null, null, null, null, null, [provision[2], 'xsd:boolean']]]) + allDeclarations.push([['Tags', provision[1]], {exist: 1, value: 1}, {exist: 1, value: [provision[2], 'xsd:boolean']}]) when '_task' # A special provision for tasks compatibility switch provision[2] when 'getParameterValues' for i in [3...provision.length] by 1 - allDeclarations.push([common.parsePath(provision[i]), 1, null, null, null, null, null, sessionData.timestamp]) + allDeclarations.push([common.parsePath(provision[i]), {exist: 1, value: sessionData.timestamp}]) when 'setParameterValues' for i in [3...provision.length] by 3 v = if provision[i + 2] then [provision[i + 1], provision[i + 2]] else [provision[i + 1]] - allDeclarations.push([common.parsePath(provision[i]), 1, null, null, null, null, null, 1, v]) + allDeclarations.push([common.parsePath(provision[i]), {exist: 1, value: 1}, {value: v}]) when 'refreshObject' path = common.parsePath(provision[3]) - for i in [path.length...16] by 1 - path.length = i - allDeclarations.push([path.slice(), sessionData.timestamp, null, 1, null, 1, null, sessionData.timestamp]) + l = path.length + path.length = 16 + path.fill('*', l) + for i in [l...16] by 1 + allDeclarations.push([path.slice(0, i), {exist: sessionData.timestamp, object: 1, writable: 1, value: sessionData.timestamp}]) continue - ret = sandbox.run(provisions[provision[0]].script, provision.slice(1), sessionData.deviceData, sessionData.extensionsCache, 0, sessionData.revisions[0] >> 1) + ret = sandbox.run(provisions[provision[0]].script, provision.slice(1), sessionData.timestamp, sessionData.deviceData, sessionData.extensionsCache, 0, sessionData.revisions[0] >> 1) if ret.extensions?.length _extensions = _extensions.concat(ret.extensions) done &&= ret.done @@ -413,91 +691,129 @@ rpcRequest = (sessionData, declarations, callback) -> ) return - doVirtualParameters = (iter, virtualParameterDeclarations, cb) -> + doVirtualParameters = (inception, virtualParameterDeclarations, cb) -> + revision = sessionData.revisions.reduce(((a, b) -> a + (b >> 1)), 0) + 1 if not virtualParameterDeclarations? - virtualParameterDeclarations = extractVirtualParameterDeclarations(sessionData) - if not virtualParameterDeclarations?.length + virtualParameterDeclarations = + extractVirtualParameterDeclarations(sessionData.deviceData, revision, virtualParameters) + if not Object.keys(virtualParameterDeclarations).length return cb(false) - sessionData.revisions[iter] ?= 0 + sessionData.revisions[inception] ?= 0 decs = [] - virtualParameterUpdates = [] + virtualParameterUpdates = {} _extensions = [] - lastRevision = sessionData.revisions.reduce((a, b) -> (a >> 1) + (b >> 1)) - firstRevision = lastRevision - (sessionData.revisions[sessionData.revisions.length - 1] >> 1) - for vpd, i in virtualParameterDeclarations - vpName = vpd[0][1] - continue if not virtualParameters[vpName]? - ret = sandbox.run(virtualParameters[vpName].script, [vpd.slice(1)], sessionData.deviceData, sessionData.extensionsCache, firstRevision, lastRevision) + + firstRevision = revision - (sessionData.revisions[sessionData.revisions.length - 1] >> 1) + for vpName, vpDeclarations of virtualParameterDeclarations + ret = sandbox.run(virtualParameters[vpName].script, [vpDeclarations.timestamps, vpDeclarations.values], sessionData.timestamp, sessionData.deviceData, sessionData.extensionsCache, firstRevision, revision) + if ret.extensions?.length _extensions = _extensions.concat(ret.extensions) decs = decs.concat(ret.declarations) if ret.done - virtualParameterUpdates.push(ret.returnValue) + virtualParameterUpdates[vpName] = ret.returnValue if _extensions.length - runExtensions(sessionData, lastRevision, _extensions, (err) -> + runExtensions(sessionData, revision, _extensions, (err) -> return callback(err) if err - return doVirtualParameters(iter, virtualParameterDeclarations, cb) + return doVirtualParameters(inception, virtualParameterDeclarations, cb) ) return - if virtualParameterUpdates.length == virtualParameterDeclarations.length - rev = sessionData.revisions.reduce((a, b) -> (a >> 1) + (b >> 1)) - for vpd, i in virtualParameterDeclarations - commitVirtualParameter(sessionData, vpd, rev + 1, virtualParameterUpdates[i]) + if Object.keys(virtualParameterUpdates).length == Object.keys(virtualParameterDeclarations).length + rev = sessionData.revisions.reduce(((a, b) -> a + (b >> 1)), 0) + 1 + v.revision = rev for k, v of sessionData.deviceData.timestamps + v.revision = rev for k, v of sessionData.deviceData.values + for vpName, vpDeclarations of virtualParameterDeclarations + commitVirtualParameter(sessionData, vpName, vpDeclarations, virtualParameterUpdates[vpName]) - if sessionData.revisions[iter] == 0 + if sessionData.revisions[inception] == 0 return cb(false) - sessionData.revisions.length = iter - rev = sessionData.revisions.reduce((a, b) -> (a >> 1) + (b >> 1)) - device.collapse(sessionData.deviceData, rev + 1) - if sessionData.extensionsCache.length > rev + 1 - sessionData.extensionsCache.length = rev + 1 + sessionData.revisions.length = inception + rev = sessionData.revisions.reduce(((a, b) -> a + (b >> 1)), 0) + 1 + m.collapse(rev) for k, m of sessionData.deviceData.timestamps + m.collapse(rev) for k, m of sessionData.deviceData.values + if sessionData.extensionsCache.length > rev + sessionData.extensionsCache.length = rev return cb(true) - applyDeclarations(iter, decs, () -> - return doVirtualParameters(iter, virtualParameterDeclarations, cb) + applyDeclarations(inception, decs, () -> + return doVirtualParameters(inception, virtualParameterDeclarations, cb) ) - applyDeclarations = (iter, decs, cb) -> - if sessionData.revisions[iter] % 2 == 0 + applyDeclarations = (inception, decs, cb) -> + declare = (path, timestamps, values, allTimestamps, allValues) -> + dt = null + for k, v of timestamps + if not dt? + dt = allTimestamps.get(path) + if not dt? + dt = {} + allTimestamps.set(path, dt) + v += sessionData.timestamp if v <= 0 + dt[k] = v if v > (dt[k] || 0) + + dv = null + for k, v of values + if not dv? + dv = allValues.get(path) + if not dv? + dv = {} + allValues.set(path, dv) + dv[k] = v + + if sessionData.revisions[inception] % 2 == 0 for declaration in decs - for d in device.getPrerequisiteDeclarations(declaration) - device.declare(sessionData.deviceData, d[0], d.slice(1), sessionData.timestamp) + aliasDeclarations = device.getAliasDeclarations(declaration[0], declaration[1][0]) + if aliasDeclarations? + for d in aliasDeclarations + path = sessionData.deviceData.paths.add(d[0]) + if path[0] == 'VirtualParameters' or path[0] == '*' + declare(path, d[1], d[2], + sessionData.deviceData.virtualParameterTimestamps, + sessionData.deviceData.virtualParameterValues) + continue if path[0] != '*' + + declare(path, d[1], d[2], + sessionData.deviceData.declarationTimestmaps, + sessionData.deviceData.declarationValues) else for declaration in decs - params = device.getAll(sessionData.deviceData, declaration[0], (sessionData.revisions[iter] >> 1) + 1) - # TODO move setting tags elsewhere - if declaration[0][0] == 'Tags' and declaration[0].length == 2 - if not declaration[0][1]? - continue if declaration[8][0] - else if params.length == 0 - device.set(sessionData.deviceData, declaration[0], 1, [sessionData.timestamp, 1, null, null, null, null, sessionData.timestamp, [false, 'xsd:boolean']]) - device.declare(sessionData.deviceData, declaration[0], declaration.slice(1), sessionData.timestamp) - continue - for p in params - device.declare(sessionData.deviceData, p[0], declaration.slice(1), sessionData.timestamp) + unpacked = device.unpack(sessionData.deviceData, declaration[0]) + + for path in unpacked + path = sessionData.deviceData.paths.add(path) + if path[0] == 'VirtualParameters' or path[0] == '*' + declare(path, declaration[1], declaration[2], + sessionData.deviceData.virtualParameterTimestamps, + sessionData.deviceData.virtualParameterValues) + continue if path[0] != '*' + + declare(path, declaration[1], declaration[2], + sessionData.deviceData.declarationTimestamps, + sessionData.deviceData.declarationValues) nextRpc = (applied) -> + ++ sessionData.revisions[inception] if not applied if (rpcReq = generateRpcRequest(sessionData))? sessionData.rpcRequest = rpcReq return callback(null, generateRpcId(sessionData), rpcReq) - if ++ sessionData.revisions[iter] % 2 == 0 + if sessionData.revisions[inception] % 2 == 0 return cb() else - return applyDeclarations(iter, decs, cb) + return applyDeclarations(inception, decs, cb) - if sessionData.revisions.length > iter + 1 - doVirtualParameters(iter + 1, null, nextRpc) + if sessionData.revisions.length > inception + 1 + doVirtualParameters(inception + 1, null, nextRpc) else - loadDeclarations(sessionData, virtualParameters, (err) -> + loadParameters(sessionData, getParametersToLoad(sessionData), virtualParameters, (err) -> return callback(err) if err - doVirtualParameters(iter + 1, null, nextRpc) + doVirtualParameters(inception + 1, null, nextRpc) ) applyDeclarations(0, allDeclarations, () -> @@ -516,35 +832,48 @@ rpcResponse = (sessionData, id, rpcRes, callback) -> sessionData.rpcRequest = null timestamp = sessionData.timestamp - revision = sessionData.revisions.reduce((a, b) -> (a >> 1) + (b >> 1)) + 1 + revision = sessionData.revisions.reduce(((a, b) -> a + (b >> 1)), 0) + 1 + + v.revision = revision for k, v of sessionData.deviceData.timestamps + v.revision = revision for k, v of sessionData.deviceData.values switch rpcRes.type when 'GetParameterValuesResponse' return callback(new Error('Response type does not match request type')) if rpcReq.type isnt 'GetParameterValues' for p in rpcRes.parameterList - device.set(sessionData.deviceData, common.parsePath(p[0]), revision, [timestamp, 1, timestamp, 0, null, null, timestamp, p.slice(1)]) + device.set(sessionData.deviceData, common.parsePath(p[0]), + {exist: timestamp, object: timestamp, value: timestamp}, + {exist: 1, object: 0, value: p.slice(1)}) when 'GetParameterNamesResponse' return callback(new Error('Response type does not match request type')) if rpcReq.type isnt 'GetParameterNames' - device.set(sessionData.deviceData, common.parsePath(rpcReq.parameterPath).concat(null), revision, timestamp) + device.set(sessionData.deviceData, common.parsePath(rpcReq.parameterPath).concat('*'), + {exist: timestamp}) for p in rpcRes.parameterList if common.endsWith(p[0], '.') path = common.parsePath(p[0][0...-1]) if not rpcReq.nextLevel - device.set(sessionData.deviceData, path.conact(null), revision, timestamp) + device.set(sessionData.deviceData, path.concat('*'), + {exist: timestamp}) - device.set(sessionData.deviceData, path, revision, [timestamp, 1, timestamp, 1, timestamp, if p[1] then 1 else 0]) + device.set(sessionData.deviceData, path, + {exist: timestamp, object: timestamp, writable: timestamp}, + {exist: 1, object: 1, writable: if p[1] then 1 else 0}) else - device.set(sessionData.deviceData, common.parsePath(p[0]), revision, [timestamp, 1, timestamp, 0, timestamp, if p[1] then 1 else 0]) + device.set(sessionData.deviceData, common.parsePath(p[0]), + {exist: timestamp, object: timestamp, writable: timestamp}, + {exist: 1, object: 0, writable: if p[1] then 1 else 0}) when 'SetParameterValuesResponse' return callback(new Error('Response type does not match request type')) if rpcReq.type isnt 'SetParameterValues' for p in rpcReq.parameterList - device.set(sessionData.deviceData, common.parsePath(p[0]), revision, [timestamp, 1, timestamp, 0, timestamp, 1, timestamp, p.slice(1)]) + device.set(sessionData.deviceData, common.parsePath(p[0]), + {exist: timestamp, object: timestamp, writable: timestamp, value: timestamp}, + {exist: 1, object: 0, writable: 1, value: p.slice(1)}) else return callback(new Error('Response type not recognized')) @@ -557,56 +886,76 @@ rpcFault = (sessionData, id, faultResponse, callback) -> load = (id, callback) -> - db.redisClient.get("session_#{id}", (err, sessionData) -> - return callback(err) if err - return callback(null, JSON.parse(sessionData)) + db.redisClient.get("session_#{id}", (err, sessionDataString) -> + return callback(err) if err or not sessionDataString? + sessionData = JSON.parse(sessionDataString) + deviceData = initDeviceData() + keys = ['exist', 'object', 'writable', 'value'] + for r in sessionData.deviceData + path = deviceData.paths.add(r[0]) + + if r[1] + deviceData.loaded.set(path, r[1]) + + for k, i in keys + if r[2 + i]? + deviceData.timestamps[k].setRevisions(path, r[2 + i]) + + if r[2 + keys.length + i]? + deviceData.values[k].setRevisions(path, r[2 + keys.length + i]) + + if r[2 + (keys.length * 2) + i]? + deviceData.declarationTimestamps.set(path, r[2 + (keys.length * 2) + i]) + + if r[2 + (keys.length * 3) + i]? + deviceData.declarationValues.set(path, r[2 + (keys.length * 3) + i]) + + sessionData.deviceData = deviceData + + return callback(null, sessionData) ) save = (sessionData, callback) -> sessionData.id ?= crypto.randomBytes(8).toString('hex') + deviceData = [] + keys = ['exist', 'object', 'writable', 'value'] + iter = sessionData.deviceData.paths.find([], 99) + while not (p = iter.next()).done + path = p.value + e = [path] + e[1] = sessionData.deviceData.loaded.get(path) || 0 + for k, i in keys + if r = sessionData.deviceData.timestamps[k].getRevisions(path) + e[2 + i] = r - db.redisClient.setex("session_#{sessionData.id}", sessionData.timeout, JSON.stringify(sessionData), (err) -> + if r = sessionData.deviceData.values[k].getRevisions(path) + e[2 + keys.length + i] = r + + if r = sessionData.deviceData.declarationTimestamps.get(path) + e[2 + (keys.length * 2) + i] = r + + if r = sessionData.deviceData.declarationValues.get(path) + e[2 + (keys.length * 3) + i] = r + + deviceData.push(e) + + oldDeviceData = sessionData.deviceData.deviceData + sessionData.deviceData = deviceData + sessionDataString = JSON.stringify(sessionData) + sessionData.deviceData = oldDeviceData + + db.redisClient.setex("session_#{sessionData.id}", sessionData.timeout, sessionDataString, (err) -> return callback(err, sessionData.id) ) return end = (sessionData, callback) -> - getDiff = (cb) -> - diff = [] - toLoad = [] - res = device.traverse(sessionData.deviceData, null, null, (path, declaration, base, current, descendantTimestamps, children) -> - for dt in descendantTimestamps - if dt[1] >= path.length and dt[3] != dt[4] - toLoad.push(dt[0]) if not dt[3]? - diff.push([dt[0], dt[1], dt[3], dt[4]]) - - for i in [0...current.length] by 2 - if current[i] != base[i] or current[i + 1] != base[i + 1] - if not base[i]? - toLoad.push(path) - break - else - diff.push([path, path.length, base, current]) - break - ) - - if toLoad.length - loadParameters(sessionData, toLoad, (err) -> - return callback(err) if err - getDiff(cb) - ) - return - - return cb(diff) - - getDiff((diff) -> - db.saveDevice(sessionData.deviceId, diff, sessionData.new, (err) -> - return callback(err) if err - db.redisClient.del("session_#{sessionData.id}", (err) -> - callback(err, sessionData.new) - ) + db.saveDevice(sessionData.deviceId, sessionData.deviceData, sessionData.new, (err) -> + return callback(err) if err + db.redisClient.del("session_#{sessionData.id}", (err) -> + callback(err, sessionData.new) ) )