Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ Features an API using ES6 promises.
* [CrossStorageClient.prototype.getKeys()](#crossstorageclientprototypegetkeys)
* [CrossStorageClient.prototype.clear()](#crossstorageclientprototypeclear)
* [CrossStorageClient.prototype.close()](#crossstorageclientprototypeclose)
* [CrossStorageClient.prototype.listen(callback)](#crossstorageclientprototypelisten)
* [CrossStorageClient.prototype.unlisten(key)](#crossstorageclientprototypeunlisten)
* [Compatibility](#compatibility)
* [Compression](#compression)
* [Building](#building)
Expand Down Expand Up @@ -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']}
]);
```

Expand Down Expand Up @@ -246,6 +248,34 @@ storage.onConnect().then(function() {
});
```

#### CrossStorageClient.prototype.listen(callback)

Adds an event listener to the `storage` event in the hub. All `storage` events
will be sent to the client and used to call the given callback.

The callback will be called on each `storage` event, with an object with the
keys `key`, `newValue`, `oldValue` and `url` taken from the original event.

``` javascript
var storageEventListenerKey;
storage.onConnect().then(function() {
return storage.listen(console.log);
}).then(function(key) {
storageEventListenerKey = key
});
```

#### CrossStorageClient.prototype.unlisten(eventKey)

Removes the storage event listener.

The client will ignore any events as soon as this is called. Returns a promise
that is settled on successful event listener removal from the hub.

``` javascript
storage.unlisten(storageEventListenerKey);
```

## Compatibility

For compatibility with older browsers, simply load a Promise polyfill such as
Expand Down
47 changes: 47 additions & 0 deletions lib/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@
this._count = 0;
this._timeout = opts.timeout || 5000;
this._listener = null;
this._storageEventListeners = {};
this._storageEventListenerCount = 0;

this._installListener();

Expand Down Expand Up @@ -193,6 +195,44 @@
return this._request('get', {keys: args});
};

/**
* Accepts a callback which will be called on `storage` events from the hub.
*
* The callback will be called on changes to the hub's storage (trigger from
* other documents than the hub). It will be called with an object with
* the keys `key`, `newValue`, `oldValue` and `url`, as defined by the `storage`
* event in the hub.
*
* Returns a promise that is settled on success (in adding the event listener),
* in which case it is fullfilled with a key that can be used to remove the
* listener. On failure, it is rejected with the corresponding error message.
*
* @param {function} callback Function to be called on storage changes
* @returns {Promise} A promise that is settled on hub response or timeout
*/
CrossStorageClient.prototype.listen = function(callback) {
this._storageEventListenerCount++;
var eventKey = this._id + ":" + this._storageEventListenerCount;
this._storageEventListeners[eventKey] = callback;
return this._request('listen', {eventKey: eventKey}).then(function () {
return eventKey
});
};

/**
* Removes the storage event listener.
*
* The client will ignore any events as soon as this is called. Returns a promise
* that is settled on successful event listener removal from the hub.
*
* @param {string} eventKey The key returned initiating the listener with `listen`
* @returns {Promise} A promise that is settled on hub response or timeout
*/
CrossStorageClient.prototype.unlisten = function(eventKey) {
delete this._storageEventListeners[eventKey];
return this._request('unlisten', {eventKey: eventKey});
};

/**
* Accepts one or more keys for deletion. Returns a promise that is settled on
* hub response or timeout.
Expand Down Expand Up @@ -307,6 +347,13 @@
return;
}

if(response.type === 'event') {
if (response.eventKey in client._storageEventListeners) {
client._storageEventListeners[response.eventKey](response.eventData);
}
return;
}

if (!response.id) return;

if (client._requests[response.id]) {
Expand Down
58 changes: 55 additions & 3 deletions lib/hub.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
}

CrossStorageHub._permissions = permissions || [];
CrossStorageHub._eventListeners = {};
CrossStorageHub._installListener();
window.parent.postMessage('cross-storage:ready', '*');
};
Expand Down Expand Up @@ -120,16 +121,16 @@
/**
* 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', 'listen' or 'getKeys'.
*
* @param {string} origin The origin for which to determine permissions
* @param {string} method Requested action
* @returns {bool} Whether or not the request is permitted
*/
CrossStorageHub._permitted = function(origin, method) {
var available, i, entry, match;

available = ['get', 'set', 'del', 'clear', 'getKeys'];
if (method==='unlisten') method = 'listen';
available = ['get', 'set', 'listen', 'del', 'clear', 'getKeys'];
if (!CrossStorageHub._inArray(method, available)) {
return false;
}
Expand Down Expand Up @@ -185,6 +186,57 @@
return (result.length > 1) ? result : result[0];
};

/**
* Adds an event listener to `storage` events which sends all events to the client with the given eventKey
*
* @param {object} params An object with an eventKey
*/
CrossStorageHub._listen = function(params) {
if (params.eventKey in CrossStorageHub._eventListeners) {
throw new Error("Can't reuse eventKeys")
}
var handler = function(event) {
if (event.storageArea != window.localStorage) return;
var data = {
type: 'event',
eventKey: params.eventKey,
eventData: {
key: event.key,
newValue: event.newValue,
oldValue: event.oldValue,
url: event.url
// storageArea, ignored because we only use localStorage
}
};
window.parent.postMessage(JSON.stringify(data), '*');
};

// Support IE8 with attachEvent
if (window.addEventListener) {
window.addEventListener('storage', handler, false);
} else {
window.attachEvent('onstorage', handler);
}
CrossStorageHub._eventListeners[params.eventKey] = handler
};

/**
* Removes an event listener with the given eventKey
*
* @param {object} params An object with an eventKey
*/
CrossStorageHub._unlisten = function(params) {
var handler = CrossStorageHub._eventListeners[params.eventKey];

// Support IE8 with attachEvent
if (window.removeEventListener) {
window.removeEventListener('storage', handler, false);
} else {
window.detachEvent('onstorage', handler);
}
CrossStorageHub._eventListeners[params.eventKey] = null
};

/**
* Deletes all keys specified in the array found at params.keys.
*
Expand Down
2 changes: 1 addition & 1 deletion test/hub.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<script type="text/javascript" src="../lib/hub.js"></script>
<script>
CrossStorageHub.init([
{origin: /.*/, allow: ['get', 'set', 'del', 'clear', 'getKeys']}
{origin: /.*/, allow: ['get', 'set', 'del', 'clear', 'getKeys', 'listen']}
]);
</script>
</html>
117 changes: 117 additions & 0 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,18 @@ describe('CrossStorageClient', function() {
};
};

var timeoutPromise = function(timeout) {
return function() {
return new Promise(function (resolve) {
window.setTimeout(function () {
resolve()
}, timeout
);
});
};
};


// Used to delete keys before each test
var cleanup = function(fn) {
storage.onConnect().then(function() {
Expand Down Expand Up @@ -333,5 +345,110 @@ describe('CrossStorageClient', function() {
done();
})['catch'](done);
});

it('can listen to updates', function(done) {
var keys = ['key1', 'key2'];
var values = ['foo', 'bar'];
var storageEvents1 = [];
var storageEvents2 = [];
var otherStorage = new CrossStorageClient(url, {timeout: 10000});

storage.onConnect()
.then(function(){return otherStorage.onConnect()})
.then(function(){
storage.listen(function(evt){storageEvents1.push(evt)});
otherStorage.listen(function(evt){storageEvents2.push(evt)});
})
.then(setGet(keys[0], values[0]))
.then(timeoutPromise(100))
.then(function(){
expect(storageEvents1).to.have.length(0);
expect(storageEvents2).to.eql([{
key: keys[0],
newValue: 'foo',
oldValue: null,
url: url
}]);
storageEvents2.pop();
})
.then(setGet(keys[0], values[1]))
.then(timeoutPromise(100))
.then(function(){
expect(storageEvents1).to.have.length(0);
expect(storageEvents2).to.eql([{
key: keys[0],
newValue: 'bar',
oldValue: 'foo',
url: url
}]);
storageEvents2.pop();
})
.then(function() {
otherStorage.del(keys[0]);
})
.then(timeoutPromise(100))
.then(function(){
expect(storageEvents2).to.have.length(0);
expect(storageEvents1).to.eql([{
key: keys[0],
newValue: null,
oldValue: "bar",
url: url
}]);
done()
})['catch'](done);
});

it('can unlisten to updates', function(done) {
var keys = ['key1', 'key2'];
var values = ['foo', 'bar'];
var storageEvents1 = [];
var storageEvents2 = [];
var otherStorage = new CrossStorageClient(url, {timeout: 10000});
var eventListenerKey;

storage.onConnect()
.then(function(){return otherStorage.onConnect()})
.then(function(){
return Promise.all([
storage.listen(function(evt){storageEvents1.push(evt)}),
otherStorage.listen(function(evt){storageEvents2.push(evt)}).then(function(key){eventListenerKey = key})
]);
})
.then(setGet(keys[0], values[0]))
.then(timeoutPromise(100))
.then(function(){
expect(storageEvents1).to.have.length(0);
expect(storageEvents2).to.eql([{
key: keys[0],
newValue: 'foo',
oldValue: null,
url: url
}]);
storageEvents2.pop();
return otherStorage.unlisten(eventListenerKey)
})
.then(setGet(keys[0], values[1]))
.then(timeoutPromise(100))
.then(function(){
expect(storageEvents1).to.have.length(0);
expect(storageEvents2).to.have.length(0);
storageEvents2.pop();
})
.then(function() {
otherStorage.del(keys[0]);
})
.then(timeoutPromise(100))
.then(function(){
expect(storageEvents2).to.have.length(0);
expect(storageEvents1).to.eql([{
key: keys[0],
newValue: null,
oldValue: "bar",
url: url
}]);
done()
})['catch'](done);
});
});
});