UNPKG

pareto-anywhere

Version:

Open source IoT middleware suite that makes the data from just about anything usable. We believe in an open Internet of Things.

408 lines (360 loc) 13.7 kB
/** * Copyright reelyActive 2016-2026 * We believe in an open Internet of Things */ let beaver = (function() { // Internal constants const SIGNATURE_SEPARATOR = '/'; const DEFAULT_STREAM_PATH = '/devices'; const DEFAULT_QUERY_PATH = '/context'; const DEFAULT_UPDATE_MILLISECONDS = 5000; const DEFAULT_STALE_DEVICE_MILLISECONDS = 60000; // Internal variables let devices = new Map(); let sources = new Map(); let eventCallbacks = { connect: [], raddec: [], dynamb: [], spatem: [], poll: [], appearance: [], disappearance: [], stats: [], error: [], disconnect: [] }; let eventCounts = { raddec: 0, dynamb: 0, spatem: 0 }; let staleDeviceMilliseconds = DEFAULT_STALE_DEVICE_MILLISECONDS; let updateMilliseconds = DEFAULT_UPDATE_MILLISECONDS; let updateTimeout = null; let reviseTimestamps = false; let staleMillisecondsSum = 0; let staleMillisecondsCount = 0; let lastUpdateTime; // Determine if the given URL is valid function isValidUrl(url) { try { new URL(url); } catch(error) { return false; } return true; } // Create the array of nearest devices based on the given raddec & dynamb function createNearest(raddec, dynamb) { let nearest = []; if(raddec && Array.isArray(raddec.rssiSignature)) { raddec.rssiSignature.forEach((entry) => { let signature = entry.receiverId + SIGNATURE_SEPARATOR + entry.receiverIdType; nearest.push({ device: signature, rssi: entry.rssi }); }); } if(dynamb && Array.isArray(dynamb.nearest)) { dynamb.nearest.forEach((entry) => { nearest.push(entry); }); } return nearest.sort((a, b) => b.rssi - a.rssi); } // Create a copy of the given dynamb, trimmed of its identifier function createTrimmedDynamb(dynamb) { let trimmedDynamb = Object.assign({}, dynamb); delete trimmedDynamb.deviceId; delete trimmedDynamb.deviceIdType; return trimmedDynamb; } // Create a copy of the given spatem, trimmed of its identifier function createTrimmedSpatem(spatem) { let trimmedSpatem = Object.assign({}, spatem); delete trimmedSpatem.deviceId; delete trimmedSpatem.deviceIdType; return trimmedSpatem; } // Manage the timestamp of the given event function manageTimestamp(event) { if(event && Number.isInteger(event.timestamp)) { let staleMilliseconds = Date.now() - event.timestamp; staleMillisecondsSum += staleMilliseconds; staleMillisecondsCount++; if(reviseTimestamps) { event.timestamp = Date.now(); } } } // Handle the given raddec function handleRaddec(raddec) { let signature = raddec.transmitterId + SIGNATURE_SEPARATOR + raddec.transmitterIdType; let device = devices.get(signature); manageTimestamp(raddec); if(device) { if(!device.hasOwnProperty('raddec') || (raddec.timestamp > device.raddec.timestamp)) { device.raddec = raddec; device.nearest = createNearest(device.raddec, device.dynamb); } } else { device = { raddec: raddec, nearest: createNearest(raddec) }; devices.set(signature, device); eventCallbacks['appearance'].forEach(callback => callback(signature, device)); } eventCallbacks['raddec'].forEach(callback => callback(raddec)); eventCounts.raddec++; } // Handle the given dynamb function handleDynamb(dynamb) { let signature = dynamb.deviceId + SIGNATURE_SEPARATOR + dynamb.deviceIdType; let device = devices.get(signature); manageTimestamp(dynamb); if(device) { if(!device.hasOwnProperty('dynamb') || (dynamb.timestamp > device.dynamb.timestamp)) { device.dynamb = createTrimmedDynamb(dynamb); device.nearest = createNearest(device.raddec, device.dynamb); } } else { device = { dynamb: createTrimmedDynamb(dynamb) }; if(Array.isArray(dynamb.nearest)) { device.nearest = createNearest(device.raddec, device.dynamb); } devices.set(signature, device); eventCallbacks['appearance'].forEach(callback => callback(signature, device)); } eventCallbacks['dynamb'].forEach(callback => callback(dynamb)); eventCounts.dynamb++; } // Handle the given spatem function handleSpatem(spatem) { let signature = spatem.deviceId + SIGNATURE_SEPARATOR + spatem.deviceIdType; let device = devices.get(signature); manageTimestamp(spatem); if(device) { if(!device.hasOwnProperty('spatem') || (spatem.timestamp > device.spatem.timestamp)) { device.spatem = createTrimmedSpatem(spatem); } } else { device = { spatem: createTrimmedSpatem(spatem) }; devices.set(signature, device); eventCallbacks['appearance'].forEach(callback => callback(signature, device)); } eventCallbacks['spatem'].forEach(callback => callback(spatem)); eventCounts.spatem++; } // Handle a context query callback function handleContext(data) { if(data) { for(const signature in data.devices) { let device = data.devices[signature]; manageTimestamp(device.dynamb); manageTimestamp(device.spatem); if(!devices.has(signature)) { devices.set(signature, device); eventCallbacks['appearance'].forEach( callback => callback(signature, device)); } else { devices.set(signature, device); // TODO: merge? } } } } // Remove stale data/devices from the graph function purgeStaleData() { let staleCollection = []; let nearestCollection = []; let staleTimestamp = Date.now() - staleDeviceMilliseconds; devices.forEach((device, signature) => { let isStale = !((device.raddec && (device.raddec.timestamp > staleTimestamp)) || (device.dynamb && (device.dynamb.timestamp > staleTimestamp)) || (device.spatem && (device.spatem.timestamp > staleTimestamp))); if(isStale) { staleCollection.push(signature); } else if(Array.isArray(device.nearest)) { device.nearest.forEach((entry) => { if(entry.device && !nearestCollection.includes(entry.device)) { nearestCollection.push(entry.device); } }); } }); staleCollection.forEach((signature) => { if(!nearestCollection.includes(signature)) { devices.delete(signature); eventCallbacks['disappearance'].forEach( callback => callback(signature)); } }); } // Update the hyperlocal context graph and stats, then set next update function update() { let currentUpdateTime = Date.now(); purgeStaleData(); if(lastUpdateTime) { let updateIntervalSeconds = (currentUpdateTime - lastUpdateTime) / 1000; let stats = { numberOfDevices: devices.size, eventsPerSecond: { raddec: eventCounts.raddec / updateIntervalSeconds, dynamb: eventCounts.dynamb / updateIntervalSeconds, spatem: eventCounts.spatem / updateIntervalSeconds }, averageEventStaleMilliseconds: Math.round(staleMillisecondsSum / staleMillisecondsCount) }; eventCounts.raddec = 0; eventCounts.dynamb = 0; eventCounts.spatem = 0; eventCallbacks['stats'].forEach(callback => callback(stats)); } lastUpdateTime = currentUpdateTime; updateTimeout = setTimeout(update, updateMilliseconds); } // Perform a HTTP GET on the given URL, accepting JSON function retrieveJson(url, callback) { fetch(url, { headers: { "Accept": "application/json" } }) .then((response) => { if(!response.ok) { throw new Error('GET returned ' + response.status); } return response.json(); }) .then((result) => { return callback(result, true); }) .catch((error) => { let helpfulError = new Error('Failed to GET ' + url); eventCallbacks['error'].forEach(callback => callback(helpfulError)); return callback(null, false); }); } // Handle socket.io events function handleSocketEvents(socket, url) { socket.on('connect', () => { console.log('beaver.js connected to Socket.IO on', url); eventCallbacks['connect'].forEach(callback => callback()); }); socket.on('raddec', handleRaddec); socket.on('dynamb', handleDynamb); socket.on('spatem', handleSpatem); socket.on('connect_error', (error) => { let helpfulError = new Error('Socket.IO connect error on ' + url); eventCallbacks['error'].forEach(callback => callback(helpfulError)); }); socket.on('disconnect', (reason) => { console.log('beaver.js disconnected from Socket.IO on', url); eventCallbacks['disconnect'].forEach(callback => callback(reason)); }); } // Handle WebSocket events function handleWebSocketEvents(socket) { socket.addEventListener('open', () => { console.log('beaver.js connected to WebSocket on', socket.url); eventCallbacks['connect'].forEach(callback => callback()); }); socket.addEventListener('message', (event) => { try { let message = JSON.parse(event.data); switch(message.type) { case 'raddec': handleRaddec(message.data); break; case 'dynamb': handleDynamb(message.data); break; case 'spatem': handleSpatem(message.data); break; } } catch(err) {} }); socket.addEventListener('error', (event) => { let helpfulError = new Error('WebSocket error on ' + socket.url); eventCallbacks['error'].forEach(callback => callback(helpfulError)); }); socket.addEventListener('close', (event) => { console.log('beaver.js disconnected from WebSocket on', socket.url); eventCallbacks['disconnect'].forEach(callback => callback(event.reason)); }); } // Stream from the given server let stream = function(serverRootUrl, options) { options = options || {}; options.isDebug = options.isDebug || false; reviseTimestamps = options.reviseTimestamps || false; let streams = {}; if(isValidUrl(serverRootUrl)) { let streamUrl = serverRootUrl + DEFAULT_STREAM_PATH; let queryUrl = serverRootUrl + DEFAULT_QUERY_PATH; if(options.deviceSignature) { streamUrl = serverRootUrl + '/devices/' + options.deviceSignature; queryUrl = serverRootUrl + '/context/device/' + options.deviceSignature; } retrieveJson(queryUrl, handleContext); if(options.io) { streams.socket = options.io.connect(streamUrl); handleSocketEvents(streams.socket, streamUrl); sources.set(streamUrl, { socket: streams.socket }); } } else { // Sources will be incorrect in rare case of identical ioUrl & wsUrl! if(options.io && isValidUrl(options.ioUrl)) { streams.socket = options.io.connect(options.ioUrl); handleSocketEvents(streams.socket, options.ioUrl); sources.set(options.ioUrl, { socket: streams.socket }); } if(isValidUrl(options.wsUrl)) { streams.websocket = new WebSocket(options.wsUrl); handleWebSocketEvents(streams.websocket); sources.set(options.wsUrl, { websocket: streams.websocket }); } } if(!updateTimeout) { update(); // Start periodic updates } return streams; }; // Poll from the given server let poll = function(serverRootUrl, options) { options = options || {}; reviseTimestamps = options.reviseTimestamps || false; let queryUrl = serverRootUrl + '/context/'; if(options.deviceSignature) { queryUrl += 'device/' + options.deviceSignature; } if(options.clearDevices) { devices.clear(); } retrieveJson(queryUrl, (data, isSuccess) => { handleContext(data); eventCallbacks['poll'].forEach(callback => callback(isSuccess)); if(Number.isInteger(options.intervalMilliseconds)) { let timeoutId = setTimeout(poll, options.intervalMilliseconds, serverRootUrl, options); sources.set(queryUrl, { timeoutId: timeoutId }); } }); if(!updateTimeout) { update(); // Start periodic updates } }; // Reset all streams and polls, and optionally clear devices let reset = function(options) { options = options || {}; sources.forEach((source, url) => { if(source.socket) { source.socket.disconnect(); } if(source.websocket) { source.websocket.close(); } if(source.timeoutId) { clearTimeout(source.timeoutId); } }); sources.clear(); if(options.clearDevices) { devices.clear(); } } // Register a callback for the given event let setEventCallback = function(event, callback) { let isValidEvent = event && eventCallbacks.hasOwnProperty(event); let isValidCallback = callback && (typeof callback === 'function'); if(isValidEvent && isValidCallback) { eventCallbacks[event].push(callback); } } // Expose the following functions and variables return { stream: stream, poll: poll, on: setEventCallback, devices: devices, reset: reset } }());