// Based on https://github.com/diy/intercom.js/blob/master/lib/intercom.js // Copyright 2012 DIY Co Apache License, Version 2.0 // http://www.apache.org/licenses/LICENSE-2.0 var EventEmitter = require('./eventemitter.js'); var guid = require('../src/shared.js').guid; function throttle(delay, fn) { var last = 0; return function() { var now = Date.now(); if (now - last > delay) { last = now; fn.apply(this, arguments); } }; } function extend(a, b) { if (typeof a === 'undefined' || !a) { a = {}; } if (typeof b === 'object') { for (var key in b) { if (b.hasOwnProperty(key)) { a[key] = b[key]; } } } return a; } var localStorage = (function(window) { if (typeof window === 'undefined' || typeof window.localStorage === 'undefined') { return { getItem : function() {}, setItem : function() {}, removeItem : function() {} }; } return window.localStorage; }(global)); function Intercom() { var self = this; var now = Date.now(); this.origin = guid(); this.lastMessage = now; this.receivedIDs = {}; this.previousValues = {}; var storageHandler = function() { self._onStorageEvent.apply(self, arguments); }; // If we're in node.js, skip event registration if (typeof document === 'undefined') { return; } if (document.attachEvent) { document.attachEvent('onstorage', storageHandler); } else { global.addEventListener('storage', storageHandler, false); } } Intercom.prototype._transaction = function(fn) { var TIMEOUT = 1000; var WAIT = 20; var self = this; var executed = false; var listening = false; var waitTimer = null; function lock() { if (executed) { return; } var now = Date.now(); var activeLock = localStorage.getItem(INDEX_LOCK)|0; if (activeLock && now - activeLock < TIMEOUT) { if (!listening) { self._on('storage', lock); listening = true; } waitTimer = setTimeout(lock, WAIT); return; } executed = true; localStorage.setItem(INDEX_LOCK, now); fn(); unlock(); } function unlock() { if (listening) { self._off('storage', lock); } if (waitTimer) { clearTimeout(waitTimer); } localStorage.removeItem(INDEX_LOCK); } lock(); }; Intercom.prototype._cleanup_emit = throttle(100, function() { var self = this; self._transaction(function() { var now = Date.now(); var threshold = now - THRESHOLD_TTL_EMIT; var changed = 0; var messages; try { messages = JSON.parse(localStorage.getItem(INDEX_EMIT) || '[]'); } catch(e) { messages = []; } for (var i = messages.length - 1; i >= 0; i--) { if (messages[i].timestamp < threshold) { messages.splice(i, 1); changed++; } } if (changed > 0) { localStorage.setItem(INDEX_EMIT, JSON.stringify(messages)); } }); }); Intercom.prototype._cleanup_once = throttle(100, function() { var self = this; self._transaction(function() { var timestamp, ttl, key; var table; var now = Date.now(); var changed = 0; try { table = JSON.parse(localStorage.getItem(INDEX_ONCE) || '{}'); } catch(e) { table = {}; } for (key in table) { if (self._once_expired(key, table)) { delete table[key]; changed++; } } if (changed > 0) { localStorage.setItem(INDEX_ONCE, JSON.stringify(table)); } }); }); Intercom.prototype._once_expired = function(key, table) { if (!table) { return true; } if (!table.hasOwnProperty(key)) { return true; } if (typeof table[key] !== 'object') { return true; } var ttl = table[key].ttl || THRESHOLD_TTL_ONCE; var now = Date.now(); var timestamp = table[key].timestamp; return timestamp < now - ttl; }; Intercom.prototype._localStorageChanged = function(event, field) { if (event && event.key) { return event.key === field; } var currentValue = localStorage.getItem(field); if (currentValue === this.previousValues[field]) { return false; } this.previousValues[field] = currentValue; return true; }; Intercom.prototype._onStorageEvent = function(event) { event = event || global.event; var self = this; if (this._localStorageChanged(event, INDEX_EMIT)) { this._transaction(function() { var now = Date.now(); var data = localStorage.getItem(INDEX_EMIT); var messages; try { messages = JSON.parse(data || '[]'); } catch(e) { messages = []; } for (var i = 0; i < messages.length; i++) { if (messages[i].origin === self.origin) continue; if (messages[i].timestamp < self.lastMessage) continue; if (messages[i].id) { if (self.receivedIDs.hasOwnProperty(messages[i].id)) continue; self.receivedIDs[messages[i].id] = true; } self.trigger(messages[i].name, messages[i].payload); } self.lastMessage = now; }); } this._trigger('storage', event); }; Intercom.prototype._emit = function(name, message, id) { id = (typeof id === 'string' || typeof id === 'number') ? String(id) : null; if (id && id.length) { if (this.receivedIDs.hasOwnProperty(id)) return; this.receivedIDs[id] = true; } var packet = { id : id, name : name, origin : this.origin, timestamp : Date.now(), payload : message }; var self = this; this._transaction(function() { var data = localStorage.getItem(INDEX_EMIT) || '[]'; var delimiter = (data === '[]') ? '' : ','; data = [data.substring(0, data.length - 1), delimiter, JSON.stringify(packet), ']'].join(''); localStorage.setItem(INDEX_EMIT, data); self.trigger(name, message); setTimeout(function() { self._cleanup_emit(); }, 50); }); }; Intercom.prototype.emit = function(name, message) { this._emit.apply(this, arguments); this._trigger('emit', name, message); }; Intercom.prototype.once = function(key, fn, ttl) { if (!Intercom.supported) { return; } var self = this; this._transaction(function() { var data; try { data = JSON.parse(localStorage.getItem(INDEX_ONCE) || '{}'); } catch(e) { data = {}; } if (!self._once_expired(key, data)) { return; } data[key] = {}; data[key].timestamp = Date.now(); if (typeof ttl === 'number') { data[key].ttl = ttl * 1000; } localStorage.setItem(INDEX_ONCE, JSON.stringify(data)); fn(); setTimeout(function() { self._cleanup_once(); }, 50); }); }; extend(Intercom.prototype, EventEmitter.prototype); Intercom.supported = (typeof localStorage !== 'undefined'); var INDEX_EMIT = 'intercom'; var INDEX_ONCE = 'intercom_once'; var INDEX_LOCK = 'intercom_lock'; var THRESHOLD_TTL_EMIT = 50000; var THRESHOLD_TTL_ONCE = 1000 * 3600; Intercom.destroy = function() { localStorage.removeItem(INDEX_LOCK); localStorage.removeItem(INDEX_EMIT); localStorage.removeItem(INDEX_ONCE); }; Intercom.getInstance = (function() { var intercom; return function() { if (!intercom) { intercom = new Intercom(); } return intercom; }; })(); module.exports = Intercom;