diff --git a/README.md b/README.md
index 6d16d7d..dc4c09f 100644
--- a/README.md
+++ b/README.md
@@ -17,6 +17,8 @@ Features an API using ES6 promises.
* [CrossStorageClient.prototype.del(key1, \[key2\], \[...\])](#crossstorageclientprototypedelkey1-key2-)
* [CrossStorageClient.prototype.getKeys()](#crossstorageclientprototypegetkeys)
* [CrossStorageClient.prototype.clear()](#crossstorageclientprototypeclear)
+ * [CrossStorageClient.prototype.listen(callback)](#crossstorageclientprototypelisten)
+ * [CrossStorageClient.prototype.unlisten(key)](#crossstorageclientprototypeunlisten)
* [CrossStorageClient.prototype.close()](#crossstorageclientprototypeclose)
* [Compatibility](#compatibility)
* [Compression](#compression)
@@ -125,12 +127,12 @@ Accepts an array of objects with two keys: origin and allow. The value
of origin is expected to be a RegExp, and allow, an array of strings.
The cross storage hub is then initialized to accept requests from any of
the matching origins, allowing access to the associated lists of methods.
-Methods may include any of: get, set, del, getKeys and clear. A 'ready'
+Methods may include any of: get, set, del, getKeys, clear and listen. A 'ready'
message is sent to the parent window once complete.
``` javascript
CrossStorageHub.init([
- {origin: /localhost:3000$/, allow: ['get', 'set', 'del', 'getKeys', 'clear']}
+ {origin: /localhost:3000$/, allow: ['get', 'set', 'del', 'getKeys', 'clear', 'listen']}
]);
```
@@ -231,6 +233,34 @@ storage.onConnect().then(function() {
});
```
+#### CrossStorageClient.prototype.listen(fn)
+
+Adds an event listener to the storage event in the hub. The callback will
+be invoked on any storage event not originating from that client. The
+callback will be invoked with an object containing the following keys taken
+from the original event: `key`, `newValue`, `oldValue` and `url`. Returns a
+promise that resolves to a listener id that can be used to unregister the
+listener.
+
+``` javascript
+storage.onConnect().then(function() {
+ return storage.listen(function(event) {
+ console.log(event);
+ });
+}).then(function(id) {
+ // id can be passed to storage.unlisten
+});
+```
+
+#### CrossStorageClient.prototype.unlisten(id)
+
+Removes the registered listener with the supplied id. Returns a promise
+that resolves on completion.
+
+``` javascript
+storage.unlisten(id);
+```
+
#### CrossStorageClient.prototype.close()
Deletes the iframe and sets the connected state to false. The client can
diff --git a/lib/client.js b/lib/client.js
index 2a78d0f..eaa7bf2 100644
--- a/lib/client.js
+++ b/lib/client.js
@@ -49,6 +49,9 @@
this._timeout = opts.timeout || 5000;
this._listener = null;
+ this._storageListeners = {};
+ this._storageListenerCount = 0;
+
this._installListener();
var frame;
@@ -226,6 +229,39 @@
return this._request('getKeys');
};
+ /**
+ * Adds an event listener to the storage event in the hub. The callback will
+ * be invoked on any storage event not originating from that client. The
+ * callback will be invoked with an object containing the following keys taken
+ * from the original event: `key`, `newValue`, `oldValue` and `url`. Returns a
+ * promise that resolves to a listener id that can be used to unregister the
+ * listener.
+ *
+ * @param {function} fn Callback to invoke on storage event
+ * @returns {Promise} A promise that is settled on hub response or timeout
+ */
+ CrossStorageClient.prototype.listen = function(fn) {
+ this._storageListenerCount++;
+ var id = this._id + ":" + this._storageListenerCount;
+ this._storageListeners[id] = fn;
+ return this._request('listen', {listenerId: id}).then(function() {
+ return id;
+ });
+ };
+
+ /**
+ * Removes the registered listener with the supplied id. Returns a promise
+ * that resolves on completion.
+ *
+ * @param {string} id The id of the listener to unregister
+ * @returns {Promise} A promise that is settled on hub response or timeout
+ */
+ CrossStorageClient.prototype.unlisten = function(id) {
+ delete this._storageListeners[id];
+ return this._request('unlisten', {listenerId: id});
+ };
+
+
/**
* Deletes the iframe and sets the connected state to false. The client can
* no longer be used after being invoked.
@@ -307,6 +343,13 @@
return;
}
+ if (response.event) {
+ if (client._storageListeners[response.listenerId]) {
+ client._storageListeners[response.listenerId](response.event);
+ }
+ return;
+ }
+
if (!response.id) return;
if (client._requests[response.id]) {
diff --git a/lib/hub.js b/lib/hub.js
index 85f395a..bf06963 100644
--- a/lib/hub.js
+++ b/lib/hub.js
@@ -38,6 +38,7 @@
}
CrossStorageHub._permissions = permissions || [];
+ CrossStorageHub._storageListeners = {};
CrossStorageHub._installListener();
window.parent.postMessage('cross-storage:ready', '*');
};
@@ -120,7 +121,8 @@
/**
* Returns a boolean indicating whether or not the requested method is
* permitted for the given origin. The argument passed to method is expected
- * to be one of 'get', 'set', 'del' or 'getKeys'.
+ * to be one of 'get', 'set', 'del', 'clear', 'getKeys', 'listen', or
+ * 'unlisten'.
*
* @param {string} origin The origin for which to determine permissions
* @param {string} method Requested action
@@ -128,8 +130,12 @@
*/
CrossStorageHub._permitted = function(origin, method) {
var available, i, entry, match;
+ available = ['get', 'set', 'listen', 'del', 'clear', 'getKeys', 'listen'];
+
+ if (method === 'unlisten') {
+ method = 'listen';
+ }
- available = ['get', 'set', 'del', 'clear', 'getKeys'];
if (!CrossStorageHub._inArray(method, available)) {
return false;
}
@@ -185,6 +191,57 @@
return (result.length > 1) ? result : result[0];
};
+ /**
+ * Listens to storage events, sending them to the client.
+ *
+ * @param {object} params An object with a listener id
+ */
+ CrossStorageHub._listen = function(params) {
+ if (params.listenerId in CrossStorageHub._storageListeners) {
+ return;
+ }
+
+ var handler = function(event) {
+ if (event.storageArea !== window.localStorage) return;
+
+ var data = {
+ listenerId: params.listenerId,
+ event: {
+ key: event.key,
+ newValue: event.newValue,
+ oldValue: event.oldValue,
+ url: event.url
+ }
+ };
+
+ window.parent.postMessage(JSON.stringify(data), '*');
+ };
+
+ CrossStorageHub._storageListeners[params.listenerId] = handler;
+
+ if (window.addEventListener) {
+ window.addEventListener('storage', handler, false);
+ } else {
+ window.attachEvent('onstorage', handler);
+ }
+ };
+
+ /**
+ * Removes an event listener with the given id
+ *
+ * @param {object} params An object with an id
+ */
+ CrossStorageHub._unlisten = function(params) {
+ var handler = CrossStorageHub._storageListeners[params.listenerId];
+ CrossStorageHub._storageListeners[params.listenerId] = null;
+
+ if (window.removeEventListener) {
+ window.removeEventListener('storage', handler, false);
+ } else {
+ window.detachEvent('onstorage', handler);
+ }
+ };
+
/**
* Deletes all keys specified in the array found at params.keys.
*
diff --git a/test/hub.html b/test/hub.html
index 0895165..bbfc8d6 100644
--- a/test/hub.html
+++ b/test/hub.html
@@ -6,7 +6,7 @@