UNPKG

uio-plus

Version:

User Interface Options Plus (UIO+) allows you to customize websites to match your own personal needs and preferences.

395 lines (356 loc) 15.8 kB
/* * Copyright The UIO+ copyright holders * See the AUTHORS.md file at the top-level directory of this distribution and at * https://github.com/fluid-project/uio-plus/blob/master/AUTHORS.md * * Licensed under the BSD 3-Clause License. You may not use this file except in * compliance with this license. * * You may obtain a copy of the license at * https://github.com/fluid-project/uio-plus/blob/master/LICENSE.txt */ /* global fluid, chrome */ "use strict"; (function (fluid) { var uioPlus = fluid.registerNamespace("uioPlus"); /* The `uioPlus.chrome.portBinding` grade provides a system for creating a port connection and sending/receiving messages across that port. To verify that messages sent across a port are received and acted upon correctly, posts provide a promise that is resolved/rejected based on a returned receipt. Connections are also set to return receipts after an incoming message has been handled. Workflow: post - ( sends payload with message id ) - message id gets stored in a map with the promise to resolve/reject - receiver takes action of the message and returns a receipt ( includes the same message id ) receive - (receives a message) - if it is a returned receipt - look into message map and resolve/reject corresponding promise - remove from map - if it is a new message - take action on the message - send a return message (receipt) containing the same message id Post Messages: 1) read/write message - wait for receipt 2) read-receipt/write-receipt - do not wait for receipt Received Messages: 1) read/write message - action, return receipt 2) read-receipt/write-receipt - remove from message map By default the following message types are handled. - "uioPlus.chrome.readRequest" - "uioPlus.chrome.readReceipt" - "uioPlus.chrome.writeRequest" - "uioPlus.chrome.writeReceipt" By default the following message types are sent. - "uioPlus.chrome.readRequest" - "uioPlus.chrome.readReceipt" - "uioPlus.chrome.writeRequest" - "uioPlus.chrome.writeReceipt" */ fluid.defaults("uioPlus.chrome.portBinding", { gradeNames: ["fluid.component"], // Name of the port connection. Will be sent as the `name` when a port connection is created. portName: "", members: { port: { expander: { func: "{that}.setPort" } }, openRequests: {} }, events: { onIncoming: null, onIncomingRead: null, onIncomingReadReceipt: null, onIncomingWrite: null, onIncomingWriteReceipt: null, onDisconnect: null }, // Defines which types of messages may be handled and/or sent messageTypes: { "readRequest": "uioPlus.chrome.readRequest", "readReceipt": "uioPlus.chrome.readReceipt", "writeRequest": "uioPlus.chrome.writeRequest", "writeReceipt": "uioPlus.chrome.writeReceipt" }, // an inverse lookup for the messageTypes messageTypeInverseMap: { expander: { funcName: "uioPlus.chrome.portBinding.invertMap", args: ["{that}.options.messageTypes"] } }, // Defines which message types are handled by which event. // Message types that aren't defined here are ignored. messageHandlingMap: { readRequest: "onIncomingRead", readReceipt: "onIncomingReadReceipt", writeRequest: "onIncomingWrite", writeReceipt: "onIncomingWriteReceipt" }, listeners: { "onCreate.bindPortEvents": "uioPlus.chrome.portBinding.bindPortEvents", "onIncoming.filterMessages": { listener: "uioPlus.chrome.portBinding.handleIncoming", args: ["{that}", "{arguments}.0"] }, "onIncomingRead.handle": { listener: "{that}.handleMessage", args: ["{that}.options.messageTypes.readReceipt", "{arguments}.0", "{that}.handleRead"] }, "onIncomingReadReceipt.handle": "{that}.handleReceipt", "onIncomingWrite.handle": { listener: "{that}.handleMessage", args: ["{that}.options.messageTypes.writeReceipt", "{arguments}.0", "{that}.handleWrite"] }, "onIncomingWriteReceipt.handle": "{that}.handleReceipt" }, invokers: { read: { funcName: "uioPlus.chrome.portBinding.postRequest", args: ["{that}", "{that}.options.messageTypes.readRequest", "{arguments}.0"] }, write: { funcName: "uioPlus.chrome.portBinding.postRequest", args: ["{that}", "{that}.options.messageTypes.writeRequest", "{arguments}.0"] }, postReceipt: { funcName: "uioPlus.chrome.portBinding.postReceipt", args: ["{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2", "{arguments}.3"] }, setPort: { funcName: "uioPlus.chrome.portBinding.setPort", args: [{name: "{that}.options.portName"}] }, rejectMessage: { funcName: "uioPlus.chrome.portBinding.requestNotAccepted", args: ["{that}", "{arguments}.0", "{arguments}.1"] }, handleMessage: { funcName: "uioPlus.chrome.portBinding.handleMessage", args: ["{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2"] }, handleReceipt: { funcName: "uioPlus.chrome.portBinding.handleReceipt", args: ["{that}", "{arguments}.0"] }, // handleRead and handleWrite must be implemented by an integrator. They will be called by the handleMessage // flow for handling read and write requests. They may return a promise to indicate if the task has // completed successfully or not. If a promise isn't returned all responses will be treated as a success. handleRead: "fluid.notImplemented", handleWrite: "fluid.notImplemented" } }); /** * Reverses a mapping object, swapping the values (rhs) and props (lhs). The original is preserved. * {"a": "b"} -> {"b": "a"} * * @param {Object} map - the mapping object to invert * * @return {Object} - a new inverted map will be returned */ uioPlus.chrome.portBinding.invertMap = function (map) { var inverted = {}; fluid.each(map, function (value, prop) { inverted[value] = prop; }); return inverted; }; /** * An object which allows two way communication with other pages. * See: https://developer.chrome.com/apps/runtime#type-Port * * @typedef {Object} Port */ /** * Creates a connection and returns a port. * * @param {Object} options - Options to pass to the port connection. For example a `name` can be passed along to * the connection listener. * see: https://developer.chrome.com/extensions/runtime#method-connect * * @return {Port} - the chrome port connection */ uioPlus.chrome.portBinding.setPort = function (options) { return chrome.runtime.connect(options); }; /** * Binds/relays {Port} events to infusion events that can be used by the component. * * @param {Component} that - an instance of `uioPlus.chrome.portBinding` */ uioPlus.chrome.portBinding.bindPortEvents = function (that) { that.port.onDisconnect.addListener(that.events.onDisconnect.fire); that.port.onMessage.addListener(that.events.onIncoming.fire); }; /** * Posts a request over the {Port} connection. The content of the message must be provided in the `payload` * argument. A promise is returned and will be resolved/rejected upon receiving a receipt from the other end. An * `id` is sent with the message and should be used as the `id` in a receipt to identify which message the receipt * is for. * * @param {Component} that - an instance of `uioPlus.chrome.portBinding` * @param {String} type - identifies the type of message for listeners on the other end. * @param {Object} payload - the content of the message * * @return {Promise} - a promise that is resolved/rejected upon receipt from the other end. */ uioPlus.chrome.portBinding.postRequest = function (that, type, payload) { var promise = fluid.promise(); var id = type + "-" + fluid.allocateGuid(); that.openRequests[id] = promise; try { that.port.postMessage({ id: id, type: type, payload: payload }); } catch (error) { delete that.openRequests[id]; promise.reject(error); } return promise; }; /** * Posts the receipt over the {Port} connection to reply to a previously received message. Content of the receipt * may be provided in the `payload` argument. * * @param {Component} that - an instance of `uioPlus.chrome.portBinding` * @param {String} type - identifies the type of message for listeners on the other end. * @param {String} id - must match a previously received message. * @param {Object} payload - the content to return * @param {Object} error - an error object to return if the previous message handling failed. */ uioPlus.chrome.portBinding.postReceipt = function (that, type, id, payload, error) { var toPost = { id: id, type: type, payload: payload }; if (error) { toPost.error = error; } that.port.postMessage(toPost); }; /** * Can be used to replace the listener for the onIncomingRead and onIncomingWrite events if those operations aren't * supported. * * @param {Component} that - an instance of `uioPlus.chrome.portBinding` * @param {String} type - identifies the type of message for listeners on the other end. * @param {Object} data - the incoming data from the message. */ uioPlus.chrome.portBinding.requestNotAccepted = function (that, type, data) { that.postReceipt(type, data.id, null, {message: "Request of type: " + data.type + " are not accepted."}); }; /** * Directs the incoming message to the appropriate event: * `onIncomingRead`, `onIncomingReadReceipt`, `onIncomingWrite`, `onIncomingWriteReceipt` * * @param {Component} that - an instance of `uioPlus.chrome.portBinding` * @param {Object} data - the data to handle from the incoming port message */ uioPlus.chrome.portBinding.handleIncoming = function (that, data) { var messageType = that.options.messageTypeInverseMap[data.type]; var eventName = that.options.messageHandlingMap[messageType]; if (eventName) { that.events[eventName].fire(data); } }; /** * A function to handle incoming request messages. The function will be passed in the message `data` as its only * argument. The `data` in a well formed request will typically take the form of * { * type: {String} // the message type e.g. "uioPlus.chrome.readRequest" * id: {String} // a unique ID. This will be returned in the receipt. * payload: {Object} // the content of the request. May not be included in all requests * } * * The MessageHandler function should return a promise, which will be used to indicate if the message request was * accepted or rejected. If a promise isn't retuned, all requests will be treated as accepted. * * @typedef {Function} MessageHandler */ /** * Handles an incoming message. It will call the `handleMessageImpl` invoker; which needs to be set by an * integrator. The result or promise returned by `handleMessageImpl` is used to determine if a receipt should be * sent with or without an error and with what payload. * * @param {Component} that - an instance of `uioPlus.chrome.portBinding` * @param {String} type - identifies the type of message for listeners on the other end. * @param {Object} data - the incoming data from the message. * @param {MessageHandler} handleFn - a function to handle the message * * @return {Promise} - a promise that is resolved/rejected based on the result of `handleFn` execution */ uioPlus.chrome.portBinding.handleMessage = function (that, type, data, handleFn) { var promise = fluid.promise(); promise.then(function (value) { that.postReceipt(type, data.id, value); }, function (reason) { // this should be a rejected receipt that.postReceipt(type, data.id, data.payload, reason); }); var handlePromise = handleFn(data); handlePromise = fluid.toPromise(handlePromise); fluid.promise.follow(handlePromise, promise); return promise; }; /** * Handles the receipt of a posted message. Based on the `id` in the receipt, the promise related to the originally * sent message will be removed from the `openRequests` object and resolved/rejected as needed. * * @param {Component} that - an instance of `uioPlus.chrome.portBinding` * @param {Object} receipt - the data from the receipt */ uioPlus.chrome.portBinding.handleReceipt = function (that, receipt) { var promise = that.openRequests[receipt.id]; if (promise) { delete that.openRequests[receipt.id]; if (receipt.error) { promise.reject(receipt.error); } else { promise.resolve(receipt.payload); } } }; /*************************** * port binding data store * ***************************/ fluid.defaults("uioPlus.chrome.portBinding.store", { gradeNames: ["fluid.dataSource", "uioPlus.chrome.portBinding"], components: { encoding: { type: "fluid.dataSource.encoding.model" } }, listeners: { "onRead.impl": "{that}.read", "onIncomingRead.handle": { listener: "{that}.rejectMessage", args: ["{that}.options.messageTypes.readReceipt", "{arguments}.0"] }, "onIncomingWrite.handle": { listener: "{that}.rejectMessage", args: ["{that}.options.messageTypes.writeReceipt", "{arguments}.0"] } }, invokers: { handleRead: "fluid.identity", handleWrite: "fluid.identity" } }); fluid.defaults("uioPlus.chrome.portBinding.store.writable", { gradeNames: ["fluid.prefs.tempStore.writable"], listeners: { "onWrite.impl": { listener: "{that}.write" } } }); fluid.makeGradeLinkage("uioPlus.chrome.portBinding.store.linkage", ["fluid.dataSource.writable", "uioPlus.chrome.portBinding.store"], "uioPlus.chrome.portBinding.store.writable"); })(fluid);