Skip to content

Commit 360b896

Browse files
Alvin Lindstamdanielstjules
authored andcommitted
Add listen and unlisten for storage events
1 parent 9db1cef commit 360b896

File tree

5 files changed

+252
-6
lines changed

5 files changed

+252
-6
lines changed

README.md

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ Features an API using ES6 promises.
1818
* [CrossStorageClient.prototype.getKeys()](#crossstorageclientprototypegetkeys)
1919
* [CrossStorageClient.prototype.clear()](#crossstorageclientprototypeclear)
2020
* [CrossStorageClient.prototype.close()](#crossstorageclientprototypeclose)
21+
* [CrossStorageClient.prototype.listen(callback)](#crossstorageclientprototypelisten)
22+
* [CrossStorageClient.prototype.unlisten(key)](#crossstorageclientprototypeunlisten)
2123
* [Compatibility](#compatibility)
2224
* [Compression](#compression)
2325
* [Building](#building)
@@ -125,12 +127,12 @@ Accepts an array of objects with two keys: origin and allow. The value
125127
of origin is expected to be a RegExp, and allow, an array of strings.
126128
The cross storage hub is then initialized to accept requests from any of
127129
the matching origins, allowing access to the associated lists of methods.
128-
Methods may include any of: get, set, del, getKeys and clear. A 'ready'
130+
Methods may include any of: get, set, del, getKeys, clear and listen. A 'ready'
129131
message is sent to the parent window once complete.
130132

131133
``` javascript
132134
CrossStorageHub.init([
133-
{origin: /localhost:3000$/, allow: ['get', 'set', 'del', 'getKeys', 'clear']}
135+
{origin: /localhost:3000$/, allow: ['get', 'set', 'del', 'getKeys', 'clear', 'listen']}
134136
]);
135137
```
136138

@@ -246,6 +248,34 @@ storage.onConnect().then(function() {
246248
});
247249
```
248250

251+
#### CrossStorageClient.prototype.listen(callback)
252+
253+
Adds an event listener to the `storage` event in the hub. All `storage` events
254+
will be sent to the client and used to call the given callback.
255+
256+
The callback will be called on each `storage` event, with an object with the
257+
keys `key`, `newValue`, `oldValue` and `url` taken from the original event.
258+
259+
``` javascript
260+
var storageEventListenerKey;
261+
storage.onConnect().then(function() {
262+
return storage.listen(console.log);
263+
}).then(function(key) {
264+
storageEventListenerKey = key
265+
});
266+
```
267+
268+
#### CrossStorageClient.prototype.unlisten(eventKey)
269+
270+
Removes the storage event listener.
271+
272+
The client will ignore any events as soon as this is called. Returns a promise
273+
that is settled on successful event listener removal from the hub.
274+
275+
``` javascript
276+
storage.unlisten(storageEventListenerKey);
277+
```
278+
249279
## Compatibility
250280

251281
For compatibility with older browsers, simply load a Promise polyfill such as

lib/client.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@
4848
this._count = 0;
4949
this._timeout = opts.timeout || 5000;
5050
this._listener = null;
51+
this._storageEventListeners = {};
52+
this._storageEventListenerCount = 0;
5153

5254
this._installListener();
5355

@@ -193,6 +195,44 @@
193195
return this._request('get', {keys: args});
194196
};
195197

198+
/**
199+
* Accepts a callback which will be called on `storage` events from the hub.
200+
*
201+
* The callback will be called on changes to the hub's storage (trigger from
202+
* other documents than the hub). It will be called with an object with
203+
* the keys `key`, `newValue`, `oldValue` and `url`, as defined by the `storage`
204+
* event in the hub.
205+
*
206+
* Returns a promise that is settled on success (in adding the event listener),
207+
* in which case it is fullfilled with a key that can be used to remove the
208+
* listener. On failure, it is rejected with the corresponding error message.
209+
*
210+
* @param {function} callback Function to be called on storage changes
211+
* @returns {Promise} A promise that is settled on hub response or timeout
212+
*/
213+
CrossStorageClient.prototype.listen = function(callback) {
214+
this._storageEventListenerCount++;
215+
var eventKey = this._id + ":" + this._storageEventListenerCount;
216+
this._storageEventListeners[eventKey] = callback;
217+
return this._request('listen', {eventKey: eventKey}).then(function () {
218+
return eventKey
219+
});
220+
};
221+
222+
/**
223+
* Removes the storage event listener.
224+
*
225+
* The client will ignore any events as soon as this is called. Returns a promise
226+
* that is settled on successful event listener removal from the hub.
227+
*
228+
* @param {string} eventKey The key returned initiating the listener with `listen`
229+
* @returns {Promise} A promise that is settled on hub response or timeout
230+
*/
231+
CrossStorageClient.prototype.unlisten = function(eventKey) {
232+
delete this._storageEventListeners[eventKey];
233+
return this._request('unlisten', {eventKey: eventKey});
234+
};
235+
196236
/**
197237
* Accepts one or more keys for deletion. Returns a promise that is settled on
198238
* hub response or timeout.
@@ -307,6 +347,13 @@
307347
return;
308348
}
309349

350+
if(response.type === 'event') {
351+
if (response.eventKey in client._storageEventListeners) {
352+
client._storageEventListeners[response.eventKey](response.eventData);
353+
}
354+
return;
355+
}
356+
310357
if (!response.id) return;
311358

312359
if (client._requests[response.id]) {

lib/hub.js

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
}
3939

4040
CrossStorageHub._permissions = permissions || [];
41+
CrossStorageHub._eventListeners = {};
4142
CrossStorageHub._installListener();
4243
window.parent.postMessage('cross-storage:ready', '*');
4344
};
@@ -120,16 +121,16 @@
120121
/**
121122
* Returns a boolean indicating whether or not the requested method is
122123
* permitted for the given origin. The argument passed to method is expected
123-
* to be one of 'get', 'set', 'del' or 'getKeys'.
124+
* to be one of 'get', 'set', 'del', 'clear', 'listen' or 'getKeys'.
124125
*
125126
* @param {string} origin The origin for which to determine permissions
126127
* @param {string} method Requested action
127128
* @returns {bool} Whether or not the request is permitted
128129
*/
129130
CrossStorageHub._permitted = function(origin, method) {
130131
var available, i, entry, match;
131-
132-
available = ['get', 'set', 'del', 'clear', 'getKeys'];
132+
if (method==='unlisten') method = 'listen';
133+
available = ['get', 'set', 'listen', 'del', 'clear', 'getKeys'];
133134
if (!CrossStorageHub._inArray(method, available)) {
134135
return false;
135136
}
@@ -185,6 +186,57 @@
185186
return (result.length > 1) ? result : result[0];
186187
};
187188

189+
/**
190+
* Adds an event listener to `storage` events which sends all events to the client with the given eventKey
191+
*
192+
* @param {object} params An object with an eventKey
193+
*/
194+
CrossStorageHub._listen = function(params) {
195+
if (params.eventKey in CrossStorageHub._eventListeners) {
196+
throw new Error("Can't reuse eventKeys")
197+
}
198+
var handler = function(event) {
199+
if (event.storageArea != window.localStorage) return;
200+
var data = {
201+
type: 'event',
202+
eventKey: params.eventKey,
203+
eventData: {
204+
key: event.key,
205+
newValue: event.newValue,
206+
oldValue: event.oldValue,
207+
url: event.url
208+
// storageArea, ignored because we only use localStorage
209+
}
210+
};
211+
window.parent.postMessage(JSON.stringify(data), '*');
212+
};
213+
214+
// Support IE8 with attachEvent
215+
if (window.addEventListener) {
216+
window.addEventListener('storage', handler, false);
217+
} else {
218+
window.attachEvent('onstorage', handler);
219+
}
220+
CrossStorageHub._eventListeners[params.eventKey] = handler
221+
};
222+
223+
/**
224+
* Removes an event listener with the given eventKey
225+
*
226+
* @param {object} params An object with an eventKey
227+
*/
228+
CrossStorageHub._unlisten = function(params) {
229+
var handler = CrossStorageHub._eventListeners[params.eventKey];
230+
231+
// Support IE8 with attachEvent
232+
if (window.removeEventListener) {
233+
window.removeEventListener('storage', handler, false);
234+
} else {
235+
window.detachEvent('onstorage', handler);
236+
}
237+
CrossStorageHub._eventListeners[params.eventKey] = null
238+
};
239+
188240
/**
189241
* Deletes all keys specified in the array found at params.keys.
190242
*

test/hub.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<script type="text/javascript" src="../lib/hub.js"></script>
77
<script>
88
CrossStorageHub.init([
9-
{origin: /.*/, allow: ['get', 'set', 'del', 'clear', 'getKeys']}
9+
{origin: /.*/, allow: ['get', 'set', 'del', 'clear', 'getKeys', 'listen']}
1010
]);
1111
</script>
1212
</html>

test/test.js

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,18 @@ describe('CrossStorageClient', function() {
5353
};
5454
};
5555

56+
var timeoutPromise = function(timeout) {
57+
return function() {
58+
return new Promise(function (resolve) {
59+
window.setTimeout(function () {
60+
resolve()
61+
}, timeout
62+
);
63+
});
64+
};
65+
};
66+
67+
5668
// Used to delete keys before each test
5769
var cleanup = function(fn) {
5870
storage.onConnect().then(function() {
@@ -333,5 +345,110 @@ describe('CrossStorageClient', function() {
333345
done();
334346
})['catch'](done);
335347
});
348+
349+
it('can listen to updates', function(done) {
350+
var keys = ['key1', 'key2'];
351+
var values = ['foo', 'bar'];
352+
var storageEvents1 = [];
353+
var storageEvents2 = [];
354+
var otherStorage = new CrossStorageClient(url, {timeout: 10000});
355+
356+
storage.onConnect()
357+
.then(function(){return otherStorage.onConnect()})
358+
.then(function(){
359+
storage.listen(function(evt){storageEvents1.push(evt)});
360+
otherStorage.listen(function(evt){storageEvents2.push(evt)});
361+
})
362+
.then(setGet(keys[0], values[0]))
363+
.then(timeoutPromise(100))
364+
.then(function(){
365+
expect(storageEvents1).to.have.length(0);
366+
expect(storageEvents2).to.eql([{
367+
key: keys[0],
368+
newValue: 'foo',
369+
oldValue: null,
370+
url: url
371+
}]);
372+
storageEvents2.pop();
373+
})
374+
.then(setGet(keys[0], values[1]))
375+
.then(timeoutPromise(100))
376+
.then(function(){
377+
expect(storageEvents1).to.have.length(0);
378+
expect(storageEvents2).to.eql([{
379+
key: keys[0],
380+
newValue: 'bar',
381+
oldValue: 'foo',
382+
url: url
383+
}]);
384+
storageEvents2.pop();
385+
})
386+
.then(function() {
387+
otherStorage.del(keys[0]);
388+
})
389+
.then(timeoutPromise(100))
390+
.then(function(){
391+
expect(storageEvents2).to.have.length(0);
392+
expect(storageEvents1).to.eql([{
393+
key: keys[0],
394+
newValue: null,
395+
oldValue: "bar",
396+
url: url
397+
}]);
398+
done()
399+
})['catch'](done);
400+
});
401+
402+
it('can unlisten to updates', function(done) {
403+
var keys = ['key1', 'key2'];
404+
var values = ['foo', 'bar'];
405+
var storageEvents1 = [];
406+
var storageEvents2 = [];
407+
var otherStorage = new CrossStorageClient(url, {timeout: 10000});
408+
var eventListenerKey;
409+
410+
storage.onConnect()
411+
.then(function(){return otherStorage.onConnect()})
412+
.then(function(){
413+
return Promise.all([
414+
storage.listen(function(evt){storageEvents1.push(evt)}),
415+
otherStorage.listen(function(evt){storageEvents2.push(evt)}).then(function(key){eventListenerKey = key})
416+
]);
417+
})
418+
.then(setGet(keys[0], values[0]))
419+
.then(timeoutPromise(100))
420+
.then(function(){
421+
expect(storageEvents1).to.have.length(0);
422+
expect(storageEvents2).to.eql([{
423+
key: keys[0],
424+
newValue: 'foo',
425+
oldValue: null,
426+
url: url
427+
}]);
428+
storageEvents2.pop();
429+
return otherStorage.unlisten(eventListenerKey)
430+
})
431+
.then(setGet(keys[0], values[1]))
432+
.then(timeoutPromise(100))
433+
.then(function(){
434+
expect(storageEvents1).to.have.length(0);
435+
expect(storageEvents2).to.have.length(0);
436+
storageEvents2.pop();
437+
})
438+
.then(function() {
439+
otherStorage.del(keys[0]);
440+
})
441+
.then(timeoutPromise(100))
442+
.then(function(){
443+
expect(storageEvents2).to.have.length(0);
444+
expect(storageEvents1).to.eql([{
445+
key: keys[0],
446+
newValue: null,
447+
oldValue: "bar",
448+
url: url
449+
}]);
450+
done()
451+
})['catch'](done);
452+
});
336453
});
337454
});

0 commit comments

Comments
 (0)