diff --git a/lib/eventemitter.js b/lib/eventemitter.js new file mode 100644 index 0000000..4bd9dc9 --- /dev/null +++ b/lib/eventemitter.js @@ -0,0 +1,66 @@ +define(function(require) { + + // Based on https://github.com/diy/intercom.js/blob/master/lib/events.js + // Copyright 2012 DIY Co Apache License, Version 2.0 + // http://www.apache.org/licenses/LICENSE-2.0 + + function removeItem(item, array) { + for (var i = array.length - 1; i >= 0; i--) { + if (array[i] === item) { + array.splice(i, 1); + } + } + return array; + } + + function EventEmitter() {} + + EventEmitter.createInterface = function(space) { + var methods = {}; + + methods.on = function(name, fn) { + if (typeof this[space] === 'undefined') { + this[space] = {}; + } + if (!this[space].hasOwnProperty(name)) { + this[space][name] = []; + } + this[space][name].push(fn); + }; + + methods.off = function(name, fn) { + if (typeof this[space] === 'undefined') return; + if (this[space].hasOwnProperty(name)) { + removeItem(fn, this[space][name]); + } + }; + + methods.trigger = function(name) { + if (typeof this[space] !== 'undefined' && this[space].hasOwnProperty(name)) { + var args = Array.prototype.slice.call(arguments, 1); + for (var i = 0; i < this[space][name].length; i++) { + this[space][name][i].apply(this[space][name][i], args); + } + } + }; + + return methods; + }; + + var pvt = EventEmitter.createInterface('_handlers'); + EventEmitter.prototype._on = pvt.on; + EventEmitter.prototype._off = pvt.off; + EventEmitter.prototype._trigger = pvt.trigger; + + var pub = EventEmitter.createInterface('handlers'); + EventEmitter.prototype.on = function() { + pub.on.apply(this, arguments); + Array.prototype.unshift.call(arguments, 'on'); + this._trigger.apply(this, arguments); + }; + EventEmitter.prototype.off = pub.off; + EventEmitter.prototype.trigger = pub.trigger; + + return EventEmitter; + +}); diff --git a/lib/intercom.js b/lib/intercom.js new file mode 100644 index 0000000..6c3bae8 --- /dev/null +++ b/lib/intercom.js @@ -0,0 +1,314 @@ +define(function(require) { + + // 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'); + var guid = require('src/shared').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.localStorage === 'undefined') { + return { + getItem : function() {}, + setItem : function() {}, + removeItem : function() {} + }; + } + return window.localStorage; + }(this)); + + 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 (document.attachEvent) { + document.attachEvent('onstorage', storageHandler); + } else { + window.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 = window.setTimeout(lock, WAIT); + return; + } + executed = true; + localStorage.setItem(INDEX_LOCK, now); + + fn(); + unlock(); + } + + function unlock() { + if (listening) { + self._off('storage', lock); + } + if (waitTimer) { + window.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 || window.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); + + window.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(); + + window.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; + }; + })(); + + return Intercom; +}); diff --git a/src/fs.js b/src/fs.js index b5662eb..414e716 100644 --- a/src/fs.js +++ b/src/fs.js @@ -58,6 +58,7 @@ define(function(require) { var providers = require('src/providers/providers'); var adapters = require('src/adapters/adapters'); var Shell = require('src/shell'); + var Intercom = require('intercom'); /* * DirectoryEntry @@ -164,10 +165,16 @@ define(function(require) { update = true; } + function complete(error) { + // Broadcast this change to all fs instances, in all windows on this origin + context.intercom.emit('change', path); + callback(error); + } + if(update) { - context.put(node.id, node, callback); + context.put(node.id, node, complete); } else { - callback(); + complete(); } } @@ -1624,16 +1631,21 @@ define(function(require) { // Open file system storage provider provider.open(function(err, needsFormatting) { function complete(error) { - // Wrap the provider so we can extend the context with fs flags. + // FileSystem watch events are broadcast between windows via intercom + var intercom = Intercom.getInstance(); + + // Wrap the provider so we can extend the context with fs flags, intercom. // From this point forward we won't call open again, so drop it. fs.provider = { getReadWriteContext: function() { var context = provider.getReadWriteContext(); context.flags = flags; + context.intercom = intercom; return context; }, getReadOnlyContext: function() { var context = provider.getReadOnlyContext(); + context.intercom = intercom; context.flags = flags; return context; }