diff --git a/History.md b/History.md new file mode 100644 index 0000000..d310d68 --- /dev/null +++ b/History.md @@ -0,0 +1,41 @@ +### 0.2.0 + +#### Breaking Changes + +- Request handlers are no longer passed a resolver. Instead they may return + values directly or return promises if the values need to be retrieved + asynchronously. Returned promises may reject, which will cause promise + rejection to the corresponding service or consumer. + +#### Experimental APIs + +- Request handlers may throw errors to indicate values could not be returned. + The message of any error thrown will be passed to the rejection handler of the + corresponding promise. The stack trace is not passed along. + + Example: + ```js + // environment + port.request('data').then( + function(data) { /* ... */ }, + function(errorMessage) { + console.error(errorMessage); + }); + + // sandbox + requests: { + data: { + if (throwString) { + throw "error message"; + } else if (throwError) { + // this case is most useful to simply propagate errors thrown by + // third-party code. + throw new Error("error message"); + } + } + } + ``` + +### 0.1.0 / 9 July 2013 + +Initial version. diff --git a/dist/oasis.amd.js b/dist/oasis.amd.js index 8335d74..e2cdda9 100644 --- a/dist/oasis.amd.js +++ b/dist/oasis.amd.js @@ -12,7 +12,7 @@ define("oasis", var Oasis = {}; - //Logger.enable(); + Logger.enable(); //verifySandbox(); @@ -84,16 +84,15 @@ define("oasis", return Oasis; });define("oasis/base_adapter", - ["oasis/util", "oasis/shims", "oasis/ports", "oasis/message_channel", "oasis/logger", "oasis/config"], - function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, Logger, configuration) { + ["oasis/util", "oasis/shims", "oasis/globals", "oasis/connect", "oasis/message_channel", "oasis/logger", "oasis/config"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, Logger, configuration) { "use strict"; var mustImplement = __dependency1__.mustImplement; - var a_forEach = __dependency2__.a_forEach; var addEventListener = __dependency2__.addEventListener; var removeEventListener = __dependency2__.removeEventListener; var handlers = __dependency3__.handlers; - var PostMessagePort = __dependency4__.PostMessagePort; - var PostMessageMessageChannel = __dependency4__.PostMessageMessageChannel; + var connectCapabilities = __dependency4__.connectCapabilities; + var PostMessageMessageChannel = __dependency5__.PostMessageMessageChannel; function getBase () { @@ -150,21 +149,8 @@ define("oasis", removeEventListener(receiver, 'message', initializeOasisSandbox); adapter.loadScripts(event.data.base, event.data.scriptURLs); - var capabilities = event.data.capabilities, eventPorts = event.ports; - - a_forEach.call(capabilities, function(capability, i) { - var handler = handlers[capability], - port = new PostMessagePort(eventPorts[i]); - - if (handler) { - Logger.log("Invoking handler for '" + capability + "'"); - - handler.setupCapability(port); - port.start(); - } + connectCapabilities(event.data.capabilities, event.ports); - ports[capability] = port; - }); adapter.didConnect(); } addEventListener(receiver, 'message', initializeOasisSandbox); @@ -201,14 +187,18 @@ define("oasis", return configuration; });define("oasis/connect", - ["oasis/util", "oasis/ports", "rsvp", "oasis/logger", "oasis/state", "exports"], - function(__dependency1__, __dependency2__, RSVP, Logger, State, __exports__) { + ["oasis/util", "oasis/shims", "oasis/globals", "oasis/message_channel", "rsvp", "oasis/logger", "oasis/state", "exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, RSVP, Logger, State, __exports__) { "use strict"; var assert = __dependency1__.assert; var rsvpErrorHandler = __dependency1__.rsvpErrorHandler; - var handlers = __dependency2__.handlers; - var ports = __dependency2__.ports; + var a_forEach = __dependency2__.a_forEach; + var handlers = __dependency3__.handlers; + var ports = __dependency3__.ports; + var PostMessagePort = __dependency4__.PostMessagePort; + + var receivedPorts = false; function registerHandler(capability, options) { var port = ports[capability]; @@ -224,9 +214,12 @@ define("oasis", } else { port.start(); } - } else { + } else if (!receivedPorts) { Logger.log("No port found, saving handler for '" + capability + "'"); handlers[capability] = options; + } else { + Logger.log("No port was sent for capability '" + capability + "'"); + options.rejectCapability(); } }; @@ -234,17 +227,65 @@ define("oasis", This is the main entry point that allows sandboxes to connect back to their containing environment. - It should be called once for each service provided by the containing - environment that it wants to connect to. + It can be called either with a set of named consumers, with callbacks, or using promises. + + Example + + // Using promises + Oasis.connect('foo').then( function (port) { + port.send('hello'); + }, function () { + // error + }); + + + // using callbacks + Oasis.connect('foo', function (port) { + port.send('hello'); + }, errorHandler); + + + // connecting several consumers at once. + var ConsumerA = Oasis.Consumer.extend({ + initialize: function (port) { this.port = port; }, + + error: function () { } + }); + + var ConsumerB = Oasis.Consumer.extend({ + initialize: function (port) { this.port = port; }, - @param {String} serviceName the name of the service to connect to + error: function () { } + }); + + Oasis.connect({ + consumers: { + capabilityA: ConsumerA, + capabilityB: ConsumerB + } + }); + + @param {String} capability the name of the service to connect to, or an object + containing named consumers to connect. @param {Function?} callback the callback to trigger once the other - side of the connection is available + side of the connection is available. + @param {Function?} errorCallback the callback to trigger if the capability is + not provided by the environment. @return {Promise} a promise that will be resolved once the other side of the connection is available. You can use this instead - of the callback. + of the callbacks. */ - function connect(capability, callback) { + function connect(capability, callback, errorCallback) { + if (typeof capability === 'object') { + return connectConsumers(capability.consumers); + } else if (callback) { + return connectCallbacks(capability, callback, errorCallback); + } else { + return connectPromise(capability); + } + }; + + function connectConsumers(consumers) { function setupCapability(Consumer, name) { return function(port) { var consumer = new Consumer(port); @@ -254,35 +295,66 @@ define("oasis", }; } - if (typeof capability === 'object') { - var consumers = capability.consumers; - - for (var prop in consumers) { - registerHandler(prop, { - setupCapability: setupCapability(consumers[prop], prop) - }); - } - } else if (callback) { - Logger.log("Connecting to '" + capability + "' with callback."); - - registerHandler(capability, { - setupCapability: function(port) { - callback(port); + for (var prop in consumers) { + registerHandler(prop, { + setupCapability: setupCapability(consumers[prop], prop), + rejectCapability: function () { + (new consumers[prop]).error(); } }); - } else { - Logger.log("Connecting to '" + capability + "' with promise."); + } + } - var defered = RSVP.defer(); - registerHandler(capability, { - promise: defered.promise, - setupCapability: function(port) { - defered.resolve(port); + function connectCallbacks(capability, callback, errorCallback) { + Logger.log("Connecting to '" + capability + "' with callback."); + + registerHandler(capability, { + setupCapability: function(port) { + callback(port); + }, + rejectCapability: function () { + if (errorCallback) { + errorCallback(); } - }); - return defered.promise; - } - }; + } + }); + } + + function connectPromise(capability) { + Logger.log("Connecting to '" + capability + "' with promise."); + + var defered = RSVP.defer(); + registerHandler(capability, { + promise: defered.promise, + setupCapability: function(port) { + defered.resolve(port); + }, + rejectCapability: function () { + defered.reject(); + } + }); + return defered.promise; + } + + function connectCapabilities(capabilities, eventPorts) { + a_forEach.call(capabilities, function(capability, i) { + var handler = handlers[capability], + port = new PostMessagePort(eventPorts[i]); + + if (handler) { + Logger.log("Invoking handler for '" + capability + "'"); + + handler.setupCapability(port); + port.start(); + } + + ports[capability] = port; + }); + + // TODO: for each handler w/o capability, reject + + receivedPorts = true; + } function portFor(capability) { var port = ports[capability]; @@ -292,7 +364,17 @@ define("oasis", __exports__.registerHandler = registerHandler; __exports__.connect = connect; + __exports__.connectCapabilities = connectCapabilities; __exports__.portFor = portFor; + });define("oasis/globals", + ["exports"], + function(__exports__) { + "use strict"; + var ports = {}; + var handlers = {}; + + __exports__.handlers = handlers; + __exports__.ports = ports; });define("oasis/iframe_adapter", ["oasis/util", "oasis/shims", "rsvp", "oasis/logger", "oasis/base_adapter"], function(__dependency1__, __dependency2__, RSVP, Logger, BaseAdapter) { @@ -557,14 +639,27 @@ define("oasis", return new RSVP.Promise(function (resolve, reject) { var requestId = getRequestId(); + var clearObservers = function () { + port.off('@response:' + eventName, observer); + port.off('@errorResponse:' + eventName, errorObserver); + } + var observer = function(event) { if (event.requestId === requestId) { - port.off('@response:' + eventName, observer); + clearObservers(); resolve(event.data); } }; + var errorObserver = function (event) { + if (event.requestId === requestId) { + clearObservers(); + reject(event.data); + } + } + port.on('@response:' + eventName, observer, port); + port.on('@errorResponse:' + eventName, errorObserver, port); port.send('@request:' + eventName, { requestId: requestId, args: args }); }); }, @@ -592,17 +687,29 @@ define("oasis", this.on('@request:' + eventName, function(data) { var requestId = data.requestId, args = data.args, - defered = RSVP.defer(); + getResponse = new RSVP.Promise(function (resolve, reject) { + try { + resolve(callback.apply(binding, data.args)); + } catch (error) { + reject(error); + } + }); - defered.promise.then(function (data) { + RSVP.resolve(getResponse).then(function (value) { self.send('@response:' + eventName, { requestId: requestId, - data: data + data: value }); - }).then(null, rsvpErrorHandler); - - args.unshift(defered); - callback.apply(binding, args); + }, function (error) { + // In the case of `Error`s, it may be useful to transfer the stack as + // well. The easiest way to do this would be to just send a simple + // object with `message` and `stack` properties. + var value = error instanceof Error ? error.message : error; + self.send('@errorResponse:' + eventName, { + requestId: requestId, + data: value + }); + }); }); } }; @@ -697,15 +804,6 @@ define("oasis", __exports__.OasisPort = OasisPort; __exports__.PostMessageMessageChannel = PostMessageMessageChannel; __exports__.PostMessagePort = PostMessagePort; - });define("oasis/ports", - ["exports"], - function(__exports__) { - "use strict"; - var ports = {}; - var handlers = {}; - - __exports__.handlers = handlers; - __exports__.ports = ports; });define("oasis/sandbox", ["oasis/util", "oasis/shims", "oasis/message_channel", "rsvp", "oasis/logger", "oasis/state", "oasis/config", "oasis/iframe_adapter"], function(__dependency1__, __dependency2__, __dependency3__, RSVP, Logger, State, configuration, iframeAdapter) { @@ -883,7 +981,7 @@ define("oasis", return OasisSandbox; });define("oasis/sandbox_init", - ["oasis/ports", "oasis/iframe_adapter", "oasis/webworker_adapter"], + ["oasis/globals", "oasis/iframe_adapter", "oasis/webworker_adapter"], function(__dependency1__, iframeAdapter, webworkerAdapter) { "use strict"; var ports = __dependency1__.ports; @@ -980,7 +1078,7 @@ define("oasis", function xform(callback) { return function() { - callback.apply(service, arguments); + return callback.apply(service, arguments); }; } diff --git a/dist/oasis.js.html b/dist/oasis.js.html index 06070cd..7dc2f38 100644 --- a/dist/oasis.js.html +++ b/dist/oasis.js.html @@ -608,7 +608,7 @@ var Oasis = {}; - //Logger.enable(); + Logger.enable(); //verifySandbox(); @@ -680,16 +680,15 @@ return Oasis; });define("oasis/base_adapter", - ["oasis/util", "oasis/shims", "oasis/ports", "oasis/message_channel", "oasis/logger", "oasis/config"], - function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, Logger, configuration) { + ["oasis/util", "oasis/shims", "oasis/globals", "oasis/connect", "oasis/message_channel", "oasis/logger", "oasis/config"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, Logger, configuration) { "use strict"; var mustImplement = __dependency1__.mustImplement; - var a_forEach = __dependency2__.a_forEach; var addEventListener = __dependency2__.addEventListener; var removeEventListener = __dependency2__.removeEventListener; var handlers = __dependency3__.handlers; - var PostMessagePort = __dependency4__.PostMessagePort; - var PostMessageMessageChannel = __dependency4__.PostMessageMessageChannel; + var connectCapabilities = __dependency4__.connectCapabilities; + var PostMessageMessageChannel = __dependency5__.PostMessageMessageChannel; function getBase () { @@ -746,21 +745,8 @@ removeEventListener(receiver, 'message', initializeOasisSandbox); adapter.loadScripts(event.data.base, event.data.scriptURLs); - var capabilities = event.data.capabilities, eventPorts = event.ports; - - a_forEach.call(capabilities, function(capability, i) { - var handler = handlers[capability], - port = new PostMessagePort(eventPorts[i]); - - if (handler) { - Logger.log("Invoking handler for '" + capability + "'"); - - handler.setupCapability(port); - port.start(); - } + connectCapabilities(event.data.capabilities, event.ports); - ports[capability] = port; - }); adapter.didConnect(); } addEventListener(receiver, 'message', initializeOasisSandbox); @@ -797,14 +783,18 @@ return configuration; });define("oasis/connect", - ["oasis/util", "oasis/ports", "rsvp", "oasis/logger", "oasis/state", "exports"], - function(__dependency1__, __dependency2__, RSVP, Logger, State, __exports__) { + ["oasis/util", "oasis/shims", "oasis/globals", "oasis/message_channel", "rsvp", "oasis/logger", "oasis/state", "exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, RSVP, Logger, State, __exports__) { "use strict"; var assert = __dependency1__.assert; var rsvpErrorHandler = __dependency1__.rsvpErrorHandler; - var handlers = __dependency2__.handlers; - var ports = __dependency2__.ports; + var a_forEach = __dependency2__.a_forEach; + var handlers = __dependency3__.handlers; + var ports = __dependency3__.ports; + var PostMessagePort = __dependency4__.PostMessagePort; + + var receivedPorts = false; function registerHandler(capability, options) { var port = ports[capability]; @@ -820,9 +810,12 @@ } else { port.start(); } - } else { + } else if (!receivedPorts) { Logger.log("No port found, saving handler for '" + capability + "'"); handlers[capability] = options; + } else { + Logger.log("No port was sent for capability '" + capability + "'"); + options.rejectCapability(); } }; @@ -830,17 +823,65 @@ This is the main entry point that allows sandboxes to connect back to their containing environment. - It should be called once for each service provided by the containing - environment that it wants to connect to. + It can be called either with a set of named consumers, with callbacks, or using promises. + + Example + + // Using promises + Oasis.connect('foo').then( function (port) { + port.send('hello'); + }, function () { + // error + }); + + + // using callbacks + Oasis.connect('foo', function (port) { + port.send('hello'); + }, errorHandler); + + + // connecting several consumers at once. + var ConsumerA = Oasis.Consumer.extend({ + initialize: function (port) { this.port = port; }, + + error: function () { } + }); + + var ConsumerB = Oasis.Consumer.extend({ + initialize: function (port) { this.port = port; }, + + error: function () { } + }); + + Oasis.connect({ + consumers: { + capabilityA: ConsumerA, + capabilityB: ConsumerB + } + }); - @param {String} serviceName the name of the service to connect to + @param {String} capability the name of the service to connect to, or an object + containing named consumers to connect. @param {Function?} callback the callback to trigger once the other - side of the connection is available + side of the connection is available. + @param {Function?} errorCallback the callback to trigger if the capability is + not provided by the environment. @return {Promise} a promise that will be resolved once the other side of the connection is available. You can use this instead - of the callback. + of the callbacks. */ - function connect(capability, callback) { + function connect(capability, callback, errorCallback) { + if (typeof capability === 'object') { + return connectConsumers(capability.consumers); + } else if (callback) { + return connectCallbacks(capability, callback, errorCallback); + } else { + return connectPromise(capability); + } + }; + + function connectConsumers(consumers) { function setupCapability(Consumer, name) { return function(port) { var consumer = new Consumer(port); @@ -850,35 +891,66 @@ }; } - if (typeof capability === 'object') { - var consumers = capability.consumers; - - for (var prop in consumers) { - registerHandler(prop, { - setupCapability: setupCapability(consumers[prop], prop) - }); - } - } else if (callback) { - Logger.log("Connecting to '" + capability + "' with callback."); - - registerHandler(capability, { - setupCapability: function(port) { - callback(port); + for (var prop in consumers) { + registerHandler(prop, { + setupCapability: setupCapability(consumers[prop], prop), + rejectCapability: function () { + (new consumers[prop]).error(); } }); - } else { - Logger.log("Connecting to '" + capability + "' with promise."); + } + } + + function connectCallbacks(capability, callback, errorCallback) { + Logger.log("Connecting to '" + capability + "' with callback."); - var defered = RSVP.defer(); - registerHandler(capability, { - promise: defered.promise, - setupCapability: function(port) { - defered.resolve(port); + registerHandler(capability, { + setupCapability: function(port) { + callback(port); + }, + rejectCapability: function () { + if (errorCallback) { + errorCallback(); } - }); - return defered.promise; - } - }; + } + }); + } + + function connectPromise(capability) { + Logger.log("Connecting to '" + capability + "' with promise."); + + var defered = RSVP.defer(); + registerHandler(capability, { + promise: defered.promise, + setupCapability: function(port) { + defered.resolve(port); + }, + rejectCapability: function () { + defered.reject(); + } + }); + return defered.promise; + } + + function connectCapabilities(capabilities, eventPorts) { + a_forEach.call(capabilities, function(capability, i) { + var handler = handlers[capability], + port = new PostMessagePort(eventPorts[i]); + + if (handler) { + Logger.log("Invoking handler for '" + capability + "'"); + + handler.setupCapability(port); + port.start(); + } + + ports[capability] = port; + }); + + // TODO: for each handler w/o capability, reject + + receivedPorts = true; + } function portFor(capability) { var port = ports[capability]; @@ -888,7 +960,17 @@ __exports__.registerHandler = registerHandler; __exports__.connect = connect; + __exports__.connectCapabilities = connectCapabilities; __exports__.portFor = portFor; + });define("oasis/globals", + ["exports"], + function(__exports__) { + "use strict"; + var ports = {}; + var handlers = {}; + + __exports__.handlers = handlers; + __exports__.ports = ports; });define("oasis/iframe_adapter", ["oasis/util", "oasis/shims", "rsvp", "oasis/logger", "oasis/base_adapter"], function(__dependency1__, __dependency2__, RSVP, Logger, BaseAdapter) { @@ -1153,14 +1235,27 @@ return new RSVP.Promise(function (resolve, reject) { var requestId = getRequestId(); + var clearObservers = function () { + port.off('@response:' + eventName, observer); + port.off('@errorResponse:' + eventName, errorObserver); + } + var observer = function(event) { if (event.requestId === requestId) { - port.off('@response:' + eventName, observer); + clearObservers(); resolve(event.data); } }; + var errorObserver = function (event) { + if (event.requestId === requestId) { + clearObservers(); + reject(event.data); + } + } + port.on('@response:' + eventName, observer, port); + port.on('@errorResponse:' + eventName, errorObserver, port); port.send('@request:' + eventName, { requestId: requestId, args: args }); }); }, @@ -1188,17 +1283,29 @@ this.on('@request:' + eventName, function(data) { var requestId = data.requestId, args = data.args, - defered = RSVP.defer(); + getResponse = new RSVP.Promise(function (resolve, reject) { + try { + resolve(callback.apply(binding, data.args)); + } catch (error) { + reject(error); + } + }); - defered.promise.then(function (data) { + RSVP.resolve(getResponse).then(function (value) { self.send('@response:' + eventName, { requestId: requestId, - data: data + data: value }); - }).then(null, rsvpErrorHandler); - - args.unshift(defered); - callback.apply(binding, args); + }, function (error) { + // In the case of `Error`s, it may be useful to transfer the stack as + // well. The easiest way to do this would be to just send a simple + // object with `message` and `stack` properties. + var value = error instanceof Error ? error.message : error; + self.send('@errorResponse:' + eventName, { + requestId: requestId, + data: value + }); + }); }); } }; @@ -1293,15 +1400,6 @@ __exports__.OasisPort = OasisPort; __exports__.PostMessageMessageChannel = PostMessageMessageChannel; __exports__.PostMessagePort = PostMessagePort; - });define("oasis/ports", - ["exports"], - function(__exports__) { - "use strict"; - var ports = {}; - var handlers = {}; - - __exports__.handlers = handlers; - __exports__.ports = ports; });define("oasis/sandbox", ["oasis/util", "oasis/shims", "oasis/message_channel", "rsvp", "oasis/logger", "oasis/state", "oasis/config", "oasis/iframe_adapter"], function(__dependency1__, __dependency2__, __dependency3__, RSVP, Logger, State, configuration, iframeAdapter) { @@ -1479,7 +1577,7 @@ return OasisSandbox; });define("oasis/sandbox_init", - ["oasis/ports", "oasis/iframe_adapter", "oasis/webworker_adapter"], + ["oasis/globals", "oasis/iframe_adapter", "oasis/webworker_adapter"], function(__dependency1__, iframeAdapter, webworkerAdapter) { "use strict"; var ports = __dependency1__.ports; @@ -1576,7 +1674,7 @@ function xform(callback) { return function() { - callback.apply(service, arguments); + return callback.apply(service, arguments); }; } diff --git a/lib/oasis.js b/lib/oasis.js index 24f2fcd..0838212 100644 --- a/lib/oasis.js +++ b/lib/oasis.js @@ -14,7 +14,7 @@ import "oasis/webworker_adapter" as webworkerAdapter; var Oasis = {}; -//Logger.enable(); +Logger.enable(); //verifySandbox(); diff --git a/lib/oasis/base_adapter.js b/lib/oasis/base_adapter.js index d802a53..ffd1b9c 100644 --- a/lib/oasis/base_adapter.js +++ b/lib/oasis/base_adapter.js @@ -1,10 +1,11 @@ import "oasis/logger" as Logger; import { mustImplement } from "oasis/util"; -import { a_forEach, addEventListener, removeEventListener } from "oasis/shims"; +import { addEventListener, removeEventListener } from "oasis/shims"; import "oasis/config" as configuration; -import { handlers } from "oasis/ports"; -import { PostMessagePort, PostMessageMessageChannel } from "oasis/message_channel"; +import { handlers } from "oasis/globals"; +import { connectCapabilities } from "oasis/connect"; +import { PostMessageMessageChannel } from "oasis/message_channel"; function getBase () { var link = document.createElement("a"); @@ -60,21 +61,8 @@ BaseAdapter.prototype = { removeEventListener(receiver, 'message', initializeOasisSandbox); adapter.loadScripts(event.data.base, event.data.scriptURLs); - var capabilities = event.data.capabilities, eventPorts = event.ports; + connectCapabilities(event.data.capabilities, event.ports); - a_forEach.call(capabilities, function(capability, i) { - var handler = handlers[capability], - port = new PostMessagePort(eventPorts[i]); - - if (handler) { - Logger.log("Invoking handler for '" + capability + "'"); - - handler.setupCapability(port); - port.start(); - } - - ports[capability] = port; - }); adapter.didConnect(); } addEventListener(receiver, 'message', initializeOasisSandbox); diff --git a/lib/oasis/connect.js b/lib/oasis/connect.js index 0fbdb34..c4938a5 100644 --- a/lib/oasis/connect.js +++ b/lib/oasis/connect.js @@ -1,9 +1,13 @@ import "rsvp" as RSVP; import "oasis/logger" as Logger; import { assert, rsvpErrorHandler } from "oasis/util"; +import { a_forEach } from "oasis/shims"; import "oasis/state" as State; -import { handlers, ports } from "oasis/ports"; +import { handlers, ports } from "oasis/globals"; +import { PostMessagePort } from "oasis/message_channel"; + +var receivedPorts = false; function registerHandler(capability, options) { var port = ports[capability]; @@ -19,9 +23,12 @@ function registerHandler(capability, options) { } else { port.start(); } - } else { + } else if (!receivedPorts) { Logger.log("No port found, saving handler for '" + capability + "'"); handlers[capability] = options; + } else { + Logger.log("No port was sent for capability '" + capability + "'"); + options.rejectCapability(); } }; @@ -29,17 +36,65 @@ function registerHandler(capability, options) { This is the main entry point that allows sandboxes to connect back to their containing environment. - It should be called once for each service provided by the containing - environment that it wants to connect to. + It can be called either with a set of named consumers, with callbacks, or using promises. + + Example + + // Using promises + Oasis.connect('foo').then( function (port) { + port.send('hello'); + }, function () { + // error + }); + + + // using callbacks + Oasis.connect('foo', function (port) { + port.send('hello'); + }, errorHandler); + + + // connecting several consumers at once. + var ConsumerA = Oasis.Consumer.extend({ + initialize: function (port) { this.port = port; }, + + error: function () { } + }); + + var ConsumerB = Oasis.Consumer.extend({ + initialize: function (port) { this.port = port; }, + + error: function () { } + }); + + Oasis.connect({ + consumers: { + capabilityA: ConsumerA, + capabilityB: ConsumerB + } + }); - @param {String} serviceName the name of the service to connect to + @param {String} capability the name of the service to connect to, or an object + containing named consumers to connect. @param {Function?} callback the callback to trigger once the other - side of the connection is available + side of the connection is available. + @param {Function?} errorCallback the callback to trigger if the capability is + not provided by the environment. @return {Promise} a promise that will be resolved once the other side of the connection is available. You can use this instead - of the callback. + of the callbacks. */ -function connect(capability, callback) { +function connect(capability, callback, errorCallback) { + if (typeof capability === 'object') { + return connectConsumers(capability.consumers); + } else if (callback) { + return connectCallbacks(capability, callback, errorCallback); + } else { + return connectPromise(capability); + } +}; + +function connectConsumers(consumers) { function setupCapability(Consumer, name) { return function(port) { var consumer = new Consumer(port); @@ -49,35 +104,66 @@ function connect(capability, callback) { }; } - if (typeof capability === 'object') { - var consumers = capability.consumers; - - for (var prop in consumers) { - registerHandler(prop, { - setupCapability: setupCapability(consumers[prop], prop) - }); - } - } else if (callback) { - Logger.log("Connecting to '" + capability + "' with callback."); - - registerHandler(capability, { - setupCapability: function(port) { - callback(port); + for (var prop in consumers) { + registerHandler(prop, { + setupCapability: setupCapability(consumers[prop], prop), + rejectCapability: function () { + (new consumers[prop]).error(); } }); - } else { - Logger.log("Connecting to '" + capability + "' with promise."); + } +} + +function connectCallbacks(capability, callback, errorCallback) { + Logger.log("Connecting to '" + capability + "' with callback."); - var defered = RSVP.defer(); - registerHandler(capability, { - promise: defered.promise, - setupCapability: function(port) { - defered.resolve(port); + registerHandler(capability, { + setupCapability: function(port) { + callback(port); + }, + rejectCapability: function () { + if (errorCallback) { + errorCallback(); } - }); - return defered.promise; - } -}; + } + }); +} + +function connectPromise(capability) { + Logger.log("Connecting to '" + capability + "' with promise."); + + var defered = RSVP.defer(); + registerHandler(capability, { + promise: defered.promise, + setupCapability: function(port) { + defered.resolve(port); + }, + rejectCapability: function () { + defered.reject(); + } + }); + return defered.promise; +} + +function connectCapabilities(capabilities, eventPorts) { + a_forEach.call(capabilities, function(capability, i) { + var handler = handlers[capability], + port = new PostMessagePort(eventPorts[i]); + + if (handler) { + Logger.log("Invoking handler for '" + capability + "'"); + + handler.setupCapability(port); + port.start(); + } + + ports[capability] = port; + }); + + // TODO: for each handler w/o capability, reject + + receivedPorts = true; +} function portFor(capability) { var port = ports[capability]; @@ -85,4 +171,4 @@ function portFor(capability) { return port; }; -export { registerHandler, connect, portFor }; +export { registerHandler, connect, connectCapabilities, portFor }; diff --git a/lib/oasis/ports.js b/lib/oasis/globals.js similarity index 100% rename from lib/oasis/ports.js rename to lib/oasis/globals.js diff --git a/lib/oasis/message_channel.js b/lib/oasis/message_channel.js index a15d097..5961cd8 100644 --- a/lib/oasis/message_channel.js +++ b/lib/oasis/message_channel.js @@ -91,14 +91,27 @@ OasisPort.prototype = { return new RSVP.Promise(function (resolve, reject) { var requestId = getRequestId(); + var clearObservers = function () { + port.off('@response:' + eventName, observer); + port.off('@errorResponse:' + eventName, errorObserver); + } + var observer = function(event) { if (event.requestId === requestId) { - port.off('@response:' + eventName, observer); + clearObservers(); resolve(event.data); } }; + var errorObserver = function (event) { + if (event.requestId === requestId) { + clearObservers(); + reject(event.data); + } + } + port.on('@response:' + eventName, observer, port); + port.on('@errorResponse:' + eventName, errorObserver, port); port.send('@request:' + eventName, { requestId: requestId, args: args }); }); }, @@ -126,17 +139,29 @@ OasisPort.prototype = { this.on('@request:' + eventName, function(data) { var requestId = data.requestId, args = data.args, - defered = RSVP.defer(); - - defered.promise.then(function (data) { + getResponse = new RSVP.Promise(function (resolve, reject) { + try { + resolve(callback.apply(binding, data.args)); + } catch (error) { + reject(error); + } + }); + + RSVP.resolve(getResponse).then(function (value) { self.send('@response:' + eventName, { requestId: requestId, - data: data + data: value }); - }).then(null, rsvpErrorHandler); - - args.unshift(defered); - callback.apply(binding, args); + }, function (error) { + // In the case of `Error`s, it may be useful to transfer the stack as + // well. The easiest way to do this would be to just send a simple + // object with `message` and `stack` properties. + var value = error instanceof Error ? error.message : error; + self.send('@errorResponse:' + eventName, { + requestId: requestId, + data: value + }); + }); }); } }; diff --git a/lib/oasis/sandbox_init.js b/lib/oasis/sandbox_init.js index 2ed0bd3..88c136d 100644 --- a/lib/oasis/sandbox_init.js +++ b/lib/oasis/sandbox_init.js @@ -1,7 +1,7 @@ import "oasis/iframe_adapter" as iframeAdapter; import "oasis/webworker_adapter" as webworkerAdapter; -import { ports } from "oasis/ports"; +import { ports } from "oasis/globals"; function initializeSandbox () { if (typeof window !== 'undefined') { diff --git a/lib/oasis/service.js b/lib/oasis/service.js index 41790dc..7b8e14d 100644 --- a/lib/oasis/service.js +++ b/lib/oasis/service.js @@ -76,7 +76,7 @@ function Service (port, sandbox) { function xform(callback) { return function() { - callback.apply(service, arguments); + return callback.apply(service, arguments); }; } diff --git a/package.json b/package.json index 69edbeb..afdcd10 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "oasis.js", - "version": "0.1.0", + "version": "0.2.0", "description": "Oasis.js is a pleasant API for safe communication with untrusted code in sandboxed iframes.", "main": "index.js", "scripts": { diff --git a/test/fixtures/assertions_shorthand.js b/test/fixtures/assertions_shorthand.js index e8a8ff4..eed269c 100644 --- a/test/fixtures/assertions_shorthand.js +++ b/test/fixtures/assertions_shorthand.js @@ -4,8 +4,8 @@ var AssertionsConsumer = Oasis.Consumer.extend({ }, requests: { - ping: function(promise) { - promise.resolve("pong"); + ping: function() { + return 'pong'; } }, diff --git a/test/fixtures/connect_failed.js b/test/fixtures/connect_failed.js new file mode 100644 index 0000000..496715f --- /dev/null +++ b/test/fixtures/connect_failed.js @@ -0,0 +1,5 @@ +Oasis.connect('assertions').then(function (port) { + Oasis.connect('unprovidedCapability').then(null, function () { + port.send('promiseRejected'); + }); +}); diff --git a/test/fixtures/connect_failed_callback.js b/test/fixtures/connect_failed_callback.js new file mode 100644 index 0000000..cfdaceb --- /dev/null +++ b/test/fixtures/connect_failed_callback.js @@ -0,0 +1,5 @@ +Oasis.connect('assertions').then(function (port) { + Oasis.connect('unprovidedCapability', function(){}, function() { + port.send('errorCallbackInvoked'); + }); +}); diff --git a/test/fixtures/connect_failed_consumer.js b/test/fixtures/connect_failed_consumer.js new file mode 100644 index 0000000..ef332a4 --- /dev/null +++ b/test/fixtures/connect_failed_consumer.js @@ -0,0 +1,11 @@ +Oasis.connect('assertions').then(function (port) { + Oasis.connect({ + consumers: { + unprovidedCapability: Oasis.Consumer.extend({ + error: function () { + port.send('consumerErrorInvoked'); + } + }) + } + }); +}); diff --git a/test/fixtures/peter_pong_request.js b/test/fixtures/peter_pong_request.js index cb135a3..d24652e 100644 --- a/test/fixtures/peter_pong_request.js +++ b/test/fixtures/peter_pong_request.js @@ -1,7 +1,7 @@ Oasis.connect('peterpong', function(port) { port.send('peter'); - port.onRequest('ping', function(promise) { - promise.resolve('pong'); + port.onRequest('ping', function() { + return 'pong'; }); }); diff --git a/test/fixtures/promise.js b/test/fixtures/promise.js index a47e179..63836f2 100644 --- a/test/fixtures/promise.js +++ b/test/fixtures/promise.js @@ -1,5 +1,9 @@ Oasis.connect('promisepong', function(port) { - port.onRequest('ping', function(promise) { - promise.resolve('pong'); + port.onRequest('ping', function() { + return new Oasis.RSVP.Promise(function (resolve, reject) { + setTimeout( function () { + resolve('pong'); + }, 1); + }); }); }); diff --git a/test/fixtures/promise_request_from_environment.js b/test/fixtures/promise_request_from_environment.js deleted file mode 100644 index 5792144..0000000 --- a/test/fixtures/promise_request_from_environment.js +++ /dev/null @@ -1,7 +0,0 @@ -Oasis.connect('promisepong', function(port) { - port.request('ping').then(function(data) { - if (data === 'pong') { - port.send('testResolvedToSatisfaction'); - } - }); -}); diff --git a/test/fixtures/promise_with_args.js b/test/fixtures/promise_with_args.js deleted file mode 100644 index 2e1635d..0000000 --- a/test/fixtures/promise_with_args.js +++ /dev/null @@ -1,9 +0,0 @@ -Oasis.connect('promisepong', function(port) { - port.onRequest('ping', function(promise, firstArg, secondArg) { - if (firstArg === 'first' && secondArg === 'second') { - promise.resolve('pong'); - } else { - promise.reject("Arguments were not passed to the request."); - } - }); -}); diff --git a/test/fixtures/rejected_request_from_environment.js b/test/fixtures/rejected_request_from_environment.js new file mode 100644 index 0000000..2bd2de7 --- /dev/null +++ b/test/fixtures/rejected_request_from_environment.js @@ -0,0 +1,9 @@ +Oasis.connect('pong', function(port) { + port.onRequest('ping', function () { + return new Oasis.RSVP.Promise(function (resolve, reject) { + setTimeout( function () { + reject('badpong'); + }, 1); + }); + }); +}); diff --git a/test/fixtures/request_from_environment.js b/test/fixtures/request_from_environment.js new file mode 100644 index 0000000..2f0a410 --- /dev/null +++ b/test/fixtures/request_from_environment.js @@ -0,0 +1,11 @@ +Oasis.connect('pong', function(port) { + port.request('ping').then(function(data) { + if (data === 'pong') { + port.send('testResolvedToSatisfaction'); + } + }, function (error) { + if (error === 'badpong') { + port.send('testResolvedToSatisfaction'); + } + }); +}); diff --git a/test/fixtures/request_from_sandbox.js b/test/fixtures/request_from_sandbox.js new file mode 100644 index 0000000..2f0a410 --- /dev/null +++ b/test/fixtures/request_from_sandbox.js @@ -0,0 +1,11 @@ +Oasis.connect('pong', function(port) { + port.request('ping').then(function(data) { + if (data === 'pong') { + port.send('testResolvedToSatisfaction'); + } + }, function (error) { + if (error === 'badpong') { + port.send('testResolvedToSatisfaction'); + } + }); +}); diff --git a/test/fixtures/promise_request_from_environment_with_args.js b/test/fixtures/request_from_sandbox_with_args.js similarity index 77% rename from test/fixtures/promise_request_from_environment_with_args.js rename to test/fixtures/request_from_sandbox_with_args.js index a881286..0cee31f 100644 --- a/test/fixtures/promise_request_from_environment_with_args.js +++ b/test/fixtures/request_from_sandbox_with_args.js @@ -1,4 +1,4 @@ -Oasis.connect('promisepong', function(port) { +Oasis.connect('pong', function(port) { port.request('ping', 'first', 'second').then(function(data) { if (data === 'pong') { port.send('testResolvedToSatisfaction'); diff --git a/test/fixtures/simple_error.js b/test/fixtures/simple_error.js new file mode 100644 index 0000000..653dca5 --- /dev/null +++ b/test/fixtures/simple_error.js @@ -0,0 +1,5 @@ +Oasis.connect('pong', function(port) { + port.onRequest('ping', function() { + throw new Error('badpong'); + }); +}); diff --git a/test/fixtures/simple_value.js b/test/fixtures/simple_value.js new file mode 100644 index 0000000..7967e21 --- /dev/null +++ b/test/fixtures/simple_value.js @@ -0,0 +1,5 @@ +Oasis.connect('pong', function(port) { + port.onRequest('ping', function() { + return 'pong'; + }); +}); diff --git a/test/fixtures/simple_value_with_args.js b/test/fixtures/simple_value_with_args.js new file mode 100644 index 0000000..c6e1a01 --- /dev/null +++ b/test/fixtures/simple_value_with_args.js @@ -0,0 +1,7 @@ +Oasis.connect('pong', function(port) { + port.onRequest('ping', function(firstArg, secondArg) { + if (firstArg === 'first' && secondArg === 'second') { + return 'pong'; + } + }); +}); diff --git a/test/tests.js b/test/tests.js index afaee32..1c2aa23 100644 --- a/test/tests.js +++ b/test/tests.js @@ -249,6 +249,78 @@ function suite(adapter, extras) { sandbox.start(); }); + test("Oasis.connect's promise rejects when connecting to a service not provided in the initiliazation message", function() { + expect(1); + stop(); + + var AssertionsService = Oasis.Service.extend({ + events: { + promiseRejected: function () { + start(); + ok(true, "Oasis.connect to unprovided capability resulted in a promise rejection."); + } + } + }); + + createSandbox({ + url: "fixtures/connect_failed.js", + capabilities: ['assertions'], + services: { + assertions: AssertionsService + } + }); + + sandbox.start(); + }); + + test("Oasis.connect invokes error callback for services not provided in the initialization message", function() { + expect(1); + stop(); + + var AssertionsService = Oasis.Service.extend({ + events: { + errorCallbackInvoked: function () { + start(); + ok(true, "Oasis.connect to unprovided capability resulted in the error callback being invoked."); + } + } + }); + + createSandbox({ + url: "fixtures/connect_failed_callback.js", + capabilities: ['assertions'], + services: { + assertions: AssertionsService + } + }); + + sandbox.start(); + }); + + test("Oasis.connect invokes `error` on consumers that could not connect", function() { + expect(1); + stop(); + + var AssertionsService = Oasis.Service.extend({ + events: { + consumerErrorInvoked: function () { + start(); + ok(true, "Oasis.connect to unprovided capability resulted in consumer.error being invoked."); + } + } + }); + + createSandbox({ + url: "fixtures/connect_failed_consumer.js", + capabilities: ['assertions'], + services: { + assertions: AssertionsService + } + }); + + sandbox.start(); + }); + test("shorthand - card can communicate with the environment through a port", function() { expect(1); stop(); @@ -337,13 +409,13 @@ function suite(adapter, extras) { test("environment can request a value from a sandbox", function() { expect(1); Oasis.register({ - url: "fixtures/promise.js", - capabilities: ['promisepong'] + url: "fixtures/simple_value.js", + capabilities: ['pong'] }); stop(); - var PingPongPromiseService = Oasis.Service.extend({ + var PingPongService = Oasis.Service.extend({ initialize: function(port, capability) { port.request('ping').then(function(data) { start(); @@ -354,9 +426,9 @@ function suite(adapter, extras) { }); createSandbox({ - url: 'fixtures/promise.js', + url: 'fixtures/simple_value.js', services: { - promisepong: PingPongPromiseService + pong: PingPongService } }); @@ -366,14 +438,14 @@ function suite(adapter, extras) { test("environment can request a value from a sandbox with arguments", function() { expect(1); Oasis.register({ - url: "fixtures/promise_with_args.js", - capabilities: ['promisepong'] + url: "fixtures/simple_value_with_args.js", + capabilities: ['pong'] }); stop(); - var PingPongPromiseService = Oasis.Service.extend({ - initialize: function(port, capability) { + var PingPongService = Oasis.Service.extend({ + initialize: function(port) { port.request('ping', "first", "second").then(function(data) { start(); @@ -383,9 +455,9 @@ function suite(adapter, extras) { }); createSandbox({ - url: 'fixtures/promise_with_args.js', + url: 'fixtures/simple_value_with_args.js', services: { - promisepong: PingPongPromiseService + pong: PingPongService } }); @@ -395,16 +467,16 @@ function suite(adapter, extras) { test("sandbox can request a value from the environment", function() { expect(1); Oasis.register({ - url: "fixtures/promise_request_from_environment.js", - capabilities: ['promisepong'] + url: "fixtures/request_from_sandbox.js", + capabilities: ['pong'] }); stop(); - var PingPongPromiseService = Oasis.Service.extend({ + var PingPongService = Oasis.Service.extend({ requests: { - ping: function(promise) { - promise.resolve('pong'); + ping: function() { + return 'pong'; } }, @@ -417,9 +489,9 @@ function suite(adapter, extras) { }); createSandbox({ - url: 'fixtures/promise_request_from_environment.js', + url: 'fixtures/request_from_sandbox.js', services: { - promisepong: PingPongPromiseService + pong: PingPongService } }); @@ -430,19 +502,17 @@ function suite(adapter, extras) { expect(1); Oasis.register({ - url: "fixtures/promise_request_from_environment_with_args.js", - capabilities: ['promisepong'] + url: "fixtures/request_from_sandbox_with_args.js", + capabilities: ['pong'] }); stop(); - var PingPongPromiseService = Oasis.Service.extend({ + var PingPongService = Oasis.Service.extend({ requests: { - ping: function(promise, firstArg, secondArg) { + ping: function(firstArg, secondArg) { if (firstArg === 'first' && secondArg === 'second') { - promise.resolve('pong'); - } else { - promise.reject("Did not receive expected arguments."); + return 'pong'; } } }, @@ -456,7 +526,139 @@ function suite(adapter, extras) { }); createSandbox({ - url: 'fixtures/promise_request_from_environment_with_args.js', + url: 'fixtures/request_from_sandbox_with_args.js', + services: { + pong: PingPongService + } + }); + + sandbox.start(); + }); + + // Note: experimental API + test("environment can fail a request with an exception", function() { + expect(1); + Oasis.register({ + url: "fixtures/request_from_sandbox.js", + capabilities: ['pong'] + }); + + stop(); + + var PingPongService = Oasis.Service.extend({ + requests: { + ping: function() { + throw new Error('badpong'); + } + }, + + events: { + testResolvedToSatisfaction: function() { + start(); + ok(true, "test was resolved to sandbox's satisfaction"); + } + } + }); + + createSandbox({ + url: 'fixtures/request_from_sandbox.js', + services: { + pong: PingPongService + } + }); + + sandbox.start(); + }); + + // Note: experimental API + test("sandbox can fail a requst with an exception", function() { + expect(1); + Oasis.register({ + url: "fixtures/simple_error.js", + capabilities: ['pong'] + }); + + stop(); + + var PingPongService = Oasis.Service.extend({ + initialize: function(port, capability) { + port.request('ping').then(null, function(error) { + start(); + + equal(error, 'badpong', "promise was rejected with expected error"); + }); + } + }); + + createSandbox({ + url: 'fixtures/simple_error.js', + services: { + pong: PingPongService + } + }); + + sandbox.start(); + }); + + test("environment can respond to a sandbox request with a promise that resolves", function() { + expect(1); + Oasis.register({ + url: "fixtures/request_from_sandbox.js", + capabilities: ['pong'] + }); + + stop(); + + var PingPongPromiseService = Oasis.Service.extend({ + requests: { + ping: function() { + return new Oasis.RSVP.Promise(function (resolve, reject) { + setTimeout( function () { + resolve('pong'); + }, 1); + }); + } + }, + + events: { + testResolvedToSatisfaction: function() { + start(); + ok(true, "test was resolved to sandbox's satisfaction"); + } + } + }); + + createSandbox({ + url: 'fixtures/request_from_sandbox.js', + services: { + pong: PingPongPromiseService + } + }); + + sandbox.start(); + }); + + test("sandbox can respond to an environment request with a promise that resolves", function() { + expect(1); + Oasis.register({ + url: "fixtures/promise.js", + capabilities: ['promisepong'] + }); + + stop(); + + var PingPongPromiseService = Oasis.Service.extend({ + initialize: function(port, capability) { + port.request('ping').then(function(data) { + start(); + + equal(data, 'pong', "promise was resolved with expected value"); + }); + } + }); + + createSandbox({ + url: 'fixtures/promise.js', services: { promisepong: PingPongPromiseService } @@ -465,6 +667,73 @@ function suite(adapter, extras) { sandbox.start(); }); + test("environment can respond to a sandbox request with a promise that rejects", function() { + expect(1); + Oasis.register({ + url: "fixtures/request_from_sandbox.js", + capabilities: ['pong'] + }); + + stop(); + + var PingPongPromiseService = Oasis.Service.extend({ + requests: { + ping: function() { + return new Oasis.RSVP.Promise(function (resolve, reject) { + setTimeout( function () { + reject('badpong'); + }, 1); + }); + } + }, + + events: { + testResolvedToSatisfaction: function() { + start(); + ok(true, "test was resolved to sandbox's satisfaction"); + } + } + }); + + createSandbox({ + url: 'fixtures/request_from_sandbox.js', + services: { + pong: PingPongPromiseService + } + }); + + sandbox.start(); + }); + + test("the sandbox can respond to an environment request with a promise that rejects", function() { + expect(1); + Oasis.register({ + url: "fixtures/rejected_request_from_environment.js", + capabilities: ['pong'] + }); + + stop(); + + var PingPongPromiseService = Oasis.Service.extend({ + initialize: function(port, capability) { + port.request('ping').then(null, function(error) { + start(); + + equal(error, 'badpong', "promise was rejected with expected error"); + }); + } + }); + + createSandbox({ + url: 'fixtures/rejected_request_from_environment.js', + services: { + pong: PingPongPromiseService + } + }); + + sandbox.start(); + }); + // TODO: Get inception adapters working in web workers if (adapter === 'iframe') { test("ports sent to a sandbox can be passed to its child sandboxes", function() { @@ -479,8 +748,8 @@ function suite(adapter, extras) { var InceptionService = Oasis.Service.extend({ initialize: function(port) { - port.onRequest('kick', function(promise) { - promise.resolve('kick'); + port.onRequest('kick', function() { + return 'kick'; }); port.on('workPlacement', function() { @@ -512,9 +781,9 @@ function suite(adapter, extras) { var InceptionService = Oasis.Service.extend({ requests: { - kick: function(promise) { - promise.resolve('kick'); + kick: function() { ok(this instanceof InceptionService, "The callback gets the service instance as `this`"); + return 'kick'; } }, @@ -971,9 +1240,9 @@ suite('iframe', function() { var OriginService = Oasis.Service.extend({ requests: { - origin: function (promise) { + origin: function () { ok(true, "Sandbox requested origin."); - promise.resolve(location.protocol + '//' + location.host); + return location.protocol + '//' + location.host; } } });