@spindox/venice
Version:
An abstraction layer on top of js-channel instance.
303 lines (281 loc) • 9.42 kB
JavaScript
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import Channel from 'js-channel';
/**
* A Venice channel.
*
* An abstraction layer on top of {@link http://mozilla.github.io/jschannel/docs/ js-channel} instance.
*
* @param {string} key - The channel identifier
* @param {ChannelOptions} options - The channel configuration options
*/
class VeniceChannel {
constructor(key, options) {
const computedOptions = Object.assign({}, {
window: window.parent, // eslint-disable-line no-undef
origin: '*',
scope: key,
}, options);
this.inner = Channel.build(computedOptions);
}
/**
* Publishes a message related to the specified topic.
*
* @example
* const payload = { type: 'geolocation' };
* channel.publish('event.hardware', payload, (err, data) => {
* if (err) return err;
* console.log('position', data);
* });
*
* @param {string} topic - The message topic
* @param {*} data - Data to be sent to the other window. The data is serialized using {@link https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm the structured clone algorithm}. This means you can pass a broad variety of data objects safely to the destination window without having to serialize them yourself
* @param {VeniceCallback} callback - The message publication callback
*/
publish(topic, data, callback = () => {}) {
const object = {
method: topic,
params: data,
success: (result) => callback(null, result),
error: (err) => callback(err),
};
this.inner.call(object);
}
/**
* Subscribes a message handler for the specified topic.
*
* @example
* channel.subscribe('event.hardware', (data, tx) => {
* if (data.type === 'geolocation') {
* // async
* getCurrentPosition()
* .then((position) => {
* tx.complete(position);
* })
* .catch((ex) => {
* tx.error('fail', ex);
* });
* tx.delayReturn(true);
* } else {
* // sync
* return 'Sync data';
* }
* });
*
* @param {string} topic - The message topic
* @param {SubscriptionHandler} handler - The message handler
*/
subscribe(topic, handler) {
this.inner.bind(topic, (transaction, data) => handler(data, transaction));
}
/**
* Unsubscribes the handler for the specified topic.
*
* @example
* channel.unsubscribe('event.hardware');
*
* @param {string} topic - The message topic
*/
unsubscribe(topic) {
this.inner.unbind(topic);
}
/**
* Destroys the channel
*
* @example
* channel.disconnect();
*/
disconnect() {
this.inner.destroy();
}
}
/**
* Contains all Venice channels of the application.
*/
class Venice {
constructor() {
this.channels = {};
}
/**
* Creates a communication channel between the parent window and the iframe.
* If it's invoked without options, returns an existing channel.
*
* @example
* // Set Venice channel on parent
* const channel = venice.channel('channel.sample', {
* window: iframe.contentWindow,
* onReady: () => {
* console.log('channel is ready!');
* },
* });
*
* @example
* // Set Venice channel on iframe
* const channel = venice.channel('channel.sample', {});
*
* @example
* // Get Venice channel
* const channel = venice.channel('channel.sample');
*
* @param {string} key - The channel identifier
* @param {ChannelOptions} options - The channel configuration options
* @returns {object} - A VeniceChannel instance
*/
channel(key, options = null) {
if (!options) {
const channel = this.channels[key];
if (!channel) {
console.error(`Channel ${key} not found!`); // eslint-disable-line no-console
return null;
}
return channel;
}
if (this.channels[key]) {
console.error(`Channel ${key} already exists!`); // eslint-disable-line no-console
return null;
}
const channel = new VeniceChannel(key, options);
this.channels[key] = channel;
return channel;
}
/**
* Publishes a message related to the specified topic on the specified channel.
*
* @example
* venice.publish({
* channel: 'channel.sample',
* topic: 'event.hardware',
* data: { type: 'geolocation' },
* callback: (err, data) => {
* if (err) return err;
* console.log('position', data);
* });
* });
*
* @param {object} params
* @param {string} params.channel - The Venice channel identifier
* @param {string} params.topic - The message topic
* @param {*} params.data - Data to be sent to the other window. The data is serialized using {@link https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm the structured clone algorithm}. This means you can pass a broad variety of data objects safely to the destination window without having to serialize them yourself
* @param {VeniceCallback} params.callback - The message publication callback
*/
publish(params) {
const channel = this.channel(params.channel);
if (channel) channel.publish(params.topic, params.data, params.callback);
}
/**
* Subscribes a message handler for the specified topic on the specified channel.
*
* @example
* venice.subscribe({
* channel: 'channel.sample',
* topic: 'event.hardware',
* handler: (data, tx) => {
* if (data.type === 'geolocation') {
* // async
* getCurrentPosition()
* .then((position) => {
* tx.complete(position);
* })
* .catch((ex) => {
* tx.error('fail', ex);
* });
* tx.delayReturn(true);
* } else {
* // sync
* return 'Sync data';
* }
* }
* });
*
* @param {object} params
* @param {string} params.channel - The Venice channel identifier
* @param {string} params.topic - The message topic
* @param {SubscriptionHandler} params.handler - The message handler
*/
subscribe(params) {
const channel = this.channel(params.channel);
if (channel) channel.subscribe(params.topic, params.handler);
}
/**
* Unsubscribes the handler for the specified topic on the specified channel.
*
* @example
* venice.unsubscribe({
* channel: 'channel.sample',
* topic: 'event.hardware',
* });
*
* @param {object} params
* @param {string} params.channel - The Venice channel identifier
* @param {string} params.topic - The message topic
*/
unsubscribe(params) {
const channel = this.channel(params.channel);
if (channel) channel.unsubscribe(params.topic);
}
/**
* Destroys the specified Venice channel.
*
* @example
* venice.disconnect('venice.channel');
*
* @param {string} key - The Venice channel identifier
*/
disconnect(key) {
const channel = this.channel(key);
if (channel) channel.disconnect();
delete this.channels[key];
}
}
/**
* Venice channel options.
*
* By defaul *window* is equal to **window.parent**, *origin* is equal to *****, and *scope* has the same value of *key* parameter.
*
* @typedef {object} ChannelOptions
*
* @property {element} window - A reference to the window to communicate with
* @property {string} origin - Specifies what the origin of other window must be for the event to be dispatched
* @property {string} scope - All messages will be safely tucked under the namespace you provide. If omitted, it's equal to the channel key
* @property {function} onReady - Callback called once actual communication has been established between the parent page and child frame. If the child frame hadn't set up its end of the channel, for instance, onReady would never get called
*/
/**
* This callback is invoked after that the subscribed listener received the message data, handled it, and, eventually, returned a response.
*
* It's a node-style method, whit the error (or *null*) as the first argument and the response data as the second argument.
*
* @callback VeniceCallback
*
* @param {string} error
* @param {*} result
*/
/**
* This method is invoked by the message topic listener. It receives data as the first argument.
*
* If your implementation is asychronous, you'll have to use the transaction object that's automatically passed as the second argument.
*
* @function SubscriptionHandler
*
* @param {*} data - The message data
* @param {Transaction} transaction - The transaction object
*/
/**
* Transaction object for asychronous implementations of message.
*
* @typedef {object} Transaction
*
* @property {function} complete - Resolves transaction with data passed as argument
* @property {function} completed
* @property {function} delayReturn - Indicates that the handler implementation is asynchronous
* @property {function} error - Rejects transaction with error (code and message) passed as argument. ```e.g. tx.error('mega_fail', 'I could not get your stuff.');```
* @property {function} invoke
* @property {string} origin - Contains the origin of the message
*/
export default new Venice();
export {
VeniceChannel,
Venice,
};