Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Events #41

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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 @@ -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)
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 @@ -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
Expand Down
43 changes: 43 additions & 0 deletions lib/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@
this._timeout = opts.timeout || 5000;
this._listener = null;

this._storageListeners = {};
this._storageListenerCount = 0;

this._installListener();

var frame;
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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]) {
Expand Down
61 changes: 59 additions & 2 deletions lib/hub.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
}

CrossStorageHub._permissions = permissions || [];
CrossStorageHub._storageListeners = {};
CrossStorageHub._installListener();
window.parent.postMessage('cross-storage:ready', '*');
};
Expand Down Expand Up @@ -120,16 +121,21 @@
/**
* 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
* @returns {bool} Whether or not the request is permitted
*/
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;
}
Expand Down Expand Up @@ -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.
*
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>
81 changes: 81 additions & 0 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ describe('CrossStorageClient', function() {
};
};

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

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

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

var listen = function() {
return Promise.all([
storage.listen(function(evt) {
storageEvents1.push(evt)
}),
otherStorage.listen(function(evt) {
storageEvents2.push(evt)
})
]);
};

Promise.all([
storage.onConnect(),
otherStorage.onConnect()
])
.then(listen)
.then(setGet(key, value))
.then(timeoutPromise(100))
.then(function() {
expect(storageEvents1).to.have.length(0);
expect(storageEvents2).to.eql([{
key: key,
newValue: value,
oldValue: null,
url: url
}]);
done();
})['catch'](done);
});

it('can unlisten to updates', function(done) {
var storageEvents1 = [];
var storageEvents2 = [];
var otherStorage = new CrossStorageClient(url, {timeout: 10000});
var listenerId;

var listen = function() {
return Promise.all([
storage.listen(function(evt) {
storageEvents1.push(evt)
}),
otherStorage.listen(function(evt) {
storageEvents2.push(evt)
}).then(function(key){
listenerId = key
})
]);
};

Promise.all([
storage.onConnect(),
otherStorage.onConnect()
])
.then(listen)
.then(function() {
return otherStorage.unlisten(listenerId);
})
.then(setGet('foo', 'bar'))
.then(timeoutPromise(100))
.then(function() {
expect(storageEvents1).to.have.length(0);
expect(storageEvents2).to.have.length(0);
done();
})['catch'](done);
});
});
});