Skip to content

Bug 685564 - Make page-mod work on Firefox mobile #310

Merged
merged 13 commits into from Dec 30, 2011

3 participants

@Gozala Gozala commented on an outdated diff Dec 22, 2011
packages/api-utils/lib/message-manager.js
+ * ***** END LICENSE BLOCK ***** */
+
+"use strict";
+
+const BAD_LISTENER = "The event listener must be a function.";
+
+const { Cc, Ci, Cu, CC } = require("chrome");
+const { setTimeout } = require("./timer");
+
+// `Namespace` is a e4x function in the scope, so we import the function as `NS`
+// to avoid clashing, until we don't have a better candidate to replace
+// `Namespace` in namespace module.
+const {Namespace: NS} = require("./namespace");
+
+const scriptLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"].
+ getService(Ci.mozIJSSubScriptLoader);
@Gozala
Mozilla member
Gozala added a note Dec 22, 2011

@ZER0 sandbox manipulation low level API has landed to master recently, could you please use that instead of direct use of mozIJSSubScriptLoader and Cu.Sandbox ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@Gozala Gozala and 1 other commented on an outdated diff Dec 22, 2011
packages/api-utils/lib/message-manager.js
+ loadFrameScript: function loadFrameScript(uri, async) {
+ if (arguments.length < loadFrameScript.length)
+ throw new Error("Not enough arguments");
+
+ let sandbox = frame(this).receiver;
+
+ if (uri.indexOf("data:") === 0) {
+ let source = uri.replace(/^data:[^,]*,/, "");
+
+ try {
+ Cu.evalInSandbox(source, sandbox, "1.8", uri, 0);
+ } catch (e) {
+ console.exception(e);
+ }
+ } else {
+ scriptLoader.loadSubScript(uri, sandbox);
@Gozala
Mozilla member
Gozala added a note Dec 22, 2011

Just wanted to mention that sandbox API uses loadSubScript with third argument 'UTF-8', cause otherwise complex unicode chars are not handled properly.

@ZER0
ZER0 added a note Dec 28, 2011

As we discussed in IRC, I merged this code with load function of sandbox module.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@Gozala Gozala commented on an outdated diff Dec 22, 2011
packages/api-utils/lib/message-manager.js
+/**
+ * Curries a function with the arguments given.
+ *
+ * @param {Function} fn
+ * The function to curry
+ */
+const curry = function curry(fn) {
+ if (arguments.length < 2)
+ return fn;
+
+ let args = Array.slice(arguments, 1);
+
+ return function() {
+ return fn.apply(this, args.concat(Array.slice(arguments)));
+ }
+}
@Gozala
Mozilla member
Gozala added a note Dec 22, 2011

Could you please factor this function out to the utils/function module ? In fact I have a pending pull request which defines this function there as I needed in more then one place, so with this it's one more use case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@Gozala Gozala commented on an outdated diff Dec 22, 2011
packages/api-utils/lib/message-manager.js
+
+const systemPrincipal = CC("@mozilla.org/systemprincipal;1", "nsIPrincipal")();
+
+// JSON.stringify is buggy with cross-sandbox values,
+// it may return "{}" on functions. Use a replacer to match them correctly.
+const jsonFixer = function (k, v) typeof v === "function" ? undefined : v;
+
+/**
+ * Curries a function with the arguments given.
+ *
+ * @param {Function} fn
+ * The function to curry
+ */
+const curry = function curry(fn) {
+ if (arguments.length < 2)
+ return fn;
@Gozala
Mozilla member
Gozala added a note Dec 22, 2011

Unless, you have strong feeling about this, I'd remove this check. I had a case once or twice where I used curry just to wrap original function to hide it's properties.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@Gozala Gozala commented on the diff Dec 22, 2011
packages/api-utils/lib/message-manager.js
+ if (uri.indexOf("data:") === 0) {
+ let source = uri.replace(/^data:[^,]*,/, "");
+
+ try {
+ Cu.evalInSandbox(source, sandbox, "1.8", uri, 0);
+ } catch (e) {
+ console.exception(e);
+ }
+ } else {
+ scriptLoader.loadSubScript(uri, sandbox);
+ }
+ }
+}
+
+Object.freeze(MessageManager);
+Object.freeze(MessageManager.prototype);
@Gozala
Mozilla member
Gozala added a note Dec 22, 2011

I'd encourage you to use Base.extend for defining a MessageManager as it will reduce noise and will make sure that everything that requires freezing etc will be frozen without manually steps.

@ZER0
ZER0 added a note Dec 28, 2011

I'm not comfortable yet with Base.extend approach, I personally think that in case of regular constructors it adds more additional steps (even if they are hidden) and became a sort of overkill. If it's okay to you, as you suggested also in one of your comment, I will leave as is this part until we are able to have have a talk about it, and I will modify later in case of needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@Gozala
Mozilla member
Gozala commented Dec 22, 2011

It feels to me that MessageManager has a big overlap with EventEmitter in terms of listeners registration, dispatching events etc... It would be nice to reuse the same code. As a matter of fact I have a branch that implements event emitters using namespaces and Base that eventually should replace an old API. I think this change would have benefit from it.

@Gozala
Mozilla member
Gozala commented Dec 22, 2011

I don't consider any of my comments to be critical or blocking this from landing, as they can be addressed in a follow up patch. In such case could you please fill a bug report for that and CC me there.

@Gozala Gozala commented on an outdated diff Dec 22, 2011
packages/api-utils/lib/message-manager.js
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+"use strict";
+
+const BAD_LISTENER = "The event listener must be a function.";
+
+const { Cc, Ci, Cu, CC } = require("chrome");
+const { setTimeout } = require("./timer");
+
+// `Namespace` is a e4x function in the scope, so we import the function as `NS`
+// to avoid clashing, until we don't have a better candidate to replace
+// `Namespace` in namespace module.
+const {Namespace: NS} = require("./namespace");
@Gozala
Mozilla member
Gozala added a note Dec 22, 2011

Nit: according to the convention it shoud be (spaces between curlyies) :

const { Namespace: NS } = require("./namespace");

@Gozala
Mozilla member
Gozala added a note Dec 22, 2011

I have used ns as an alias instead, and I think I saw Alex used something else. I'd suggest export ns from namespace module as an alias to Namespace and put the comment regarding e4x conflict there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@Gozala Gozala commented on the diff Dec 22, 2011
packages/api-utils/lib/message-manager.js
+ let json = JSON.parse(JSON.stringify(data, jsonFixer));
+
+ for each(let listener in listeners[name]) {
+ try {
+ let response = listener.call(null, {
+ sync : sync,
+ name : name,
+ json : json,
+ target : null
+ });
+
+ if (sync) {
+ if (typeof response === "undefined")
+ responses.push(response);
+ else
+ responses.push(JSON.parse(JSON.stringify(response, jsonFixer)));
@Gozala
Mozilla member
Gozala added a note Dec 22, 2011

Alternatively you could just JSON.parse(JSON.stringify(responses, jsonFixer)) all the responses.

@ZER0
ZER0 added a note Dec 29, 2011

In that case JSON.stringify will censored the undefined values to null, and that is not the expected behavior.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@Gozala Gozala commented on an outdated diff Dec 22, 2011
packages/api-utils/lib/message-manager.js
+ * Loads a script into the remote frame.
+ *
+ * @param {String} uri
+ * The URL of the script to load into the frame; this must be an absolute
+ * URL, but data: URLs are supported.
+ * @param {Boolean} allowDelayedLoad
+ * Not used.
+ */
+ loadFrameScript: function loadFrameScript(uri, async) {
+ if (arguments.length < loadFrameScript.length)
+ throw new Error("Not enough arguments");
+
+ let sandbox = frame(this).receiver;
+
+ if (uri.indexOf("data:") === 0) {
+ let source = uri.replace(/^data:[^,]*,/, "");
@Gozala
Mozilla member
Gozala added a note Dec 22, 2011

Nit: I think let source = uri.substr(uri.indexOf(',') + 1) is faster and easier to read.

@Gozala
Mozilla member
Gozala added a note Dec 23, 2011

I think you should decodeURIComponent otherwise it may misbehave when data: uri's are encoded as they should.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@mykmelez mykmelez commented on the diff Dec 23, 2011
packages/api-utils/tests/test-message-manager.js
+ let listenerA = function () {};
+ let listenerB = function () {};
+ let listenerC = function () {};
+
+ mm.removeMessageListener(topic, listenerA);
+
+ assert.deepEqual(listeners, {},
+ "No listeners yet");
+
+ mm.addMessageListener(topic, listenerA);
+ mm.addMessageListener(topic, listenerB);
+ mm.addMessageListener(topic, listenerC);
+
+ mm.removeMessageListener(topic, listenerB);
+
+ assert.deepEqual(listeners[topic], [listenerA, listenerC],
@mykmelez
Mozilla member
mykmelez added a note Dec 23, 2011

Are listeners supposed to be called in the order they were registered, or is the order in which they are called undetermined? If the latter, it might be worth sorting these arrays, to guard against failure if the underlying implementation fails. Otherwise, we shouldn't sort them, since then the test should be checking that the listeners are in the expected order.

@ZER0
ZER0 added a note Dec 29, 2011

I didn't find any explicit information about the order in which the listeners are called in the Message Manager APIs, so for this implementation I just followed the easy way and they are called in the order they were registered.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@mykmelez
Mozilla member

@ZER0 asked me to look at the way tests are written, so I read through test-message-manager.js, and it looks great; I have no problem at all with the way the tests are written. r=@mykmelez on it!

@Gozala Gozala and 1 other commented on an outdated diff Dec 23, 2011
packages/api-utils/lib/message-manager.js
+};
+
+let frame = NS({receiver: null, listeners: null});
+
+/**
+ * The MessageManager object emulates the Message Manager API, without creating
+ * new processes. It useful in mono process context, like Fennec.
+ *
+ * @see
+ * https://developer.mozilla.org/en/The_message_manager
+ */
+function MessageManager() {
+
+ let sandbox = Cu.Sandbox(systemPrincipal, { wantXrays : false });
+
+ Object.defineProperties(sandbox, {
@Gozala
Mozilla member
Gozala added a note Dec 23, 2011

You should probably use merge:

merge(sandbox, {
  addMessageListener: addMessageListener.bind(sandbox),
  ....
})

It will methods configurable & writable, but they are in normal massegaManager anyway.

@ZER0
ZER0 added a note Dec 30, 2011

If you don't have hard feeling about it, I'd like to keep the descriptors.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@Gozala Gozala and 1 other commented on an outdated diff Dec 23, 2011
packages/api-utils/lib/message-manager.js
+ * Sends a message to the listeners.
+ *
+ * @param {Boolean} sync
+ * Indicates if the call is synchronous or asynchronous
+ * @param {String} name
+ * The name of the message to send to the listeners.
+ * @param {Object} [data=null]
+ * A JSON object containing data to be delivered to the listeners.
+ * @param {Object} [sender=undefined]
+ * The object that sent the message. If is not specified is the context
+ * object `this`.
+ * @returns {Array|undefined}
+ * An array with the return values of the listeners if `sync` is `true`,
+ * otherwise `undefined`.
+ */
+function sendMessage(sync, name, data, sender) {
@Gozala
Mozilla member
Gozala added a note Dec 23, 2011

After discussing it on IRC I understood that this is a magic ingredient of this function :) More seriously, I think it's hard to follow and would be much cleaner if this sendMessage was always synchronous. Then you could wrap it into Enqueued for sendMessageAsync.

PS: I was also thinking about renaming Enqueued to delay to match _.delay

@ZER0
ZER0 added a note Dec 29, 2011

To me, Enqueued is more similar to _.defer than _.delay

@ZER0
ZER0 added a note Dec 30, 2011

I used invoke to create my own defer function in message-manager. Unfortunately, the Enqueued function returns a value, the timeout's id, and in message-manager I need undefined. I didn't modify Enqueued directly because I don't know if this value is returned on purpose.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@Gozala Gozala commented on an outdated diff Dec 23, 2011
packages/api-utils/lib/message-manager.js
+
+ frame(this).receiver = sandbox;
+ frame(sandbox).receiver = this;
+
+ frame(this).listeners = {};
+ frame(sandbox).listeners = {};
+}
+
+MessageManager.prototype = {
+ constructor: MessageManager,
+
+ addMessageListener : addMessageListener,
+
+ removeMessageListener : removeMessageListener,
+
+ sendAsyncMessage : curry(sendMessage, false),
@Gozala
Mozilla member
Gozala added a note Dec 23, 2011

with a proposal above it will become:

sendAsyncMessage : curry(delay(sendMessage), false),
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@Gozala Gozala commented on the diff Dec 23, 2011
packages/api-utils/tests/test-message-manager.js
@@ -0,0 +1,596 @@
+"use strict";
+
+const { Loader } = require('./helpers');
+
+function createMessageManager() {
+ let loader = Loader(module);
+ let { MessageManager } = loader.require("api-utils/message-manager");
+ let frame = loader.sandbox("api-utils/message-manager").frame;
+
+ return [new MessageManager, frame];
+}
+
+
+exports["test MessageManager addMessageListener"] =
+ function(assert) {
@Gozala
Mozilla member
Gozala added a note Dec 23, 2011

I would suggest to create message manager and load data URI into it, which registers listener. Dispatch to the listener and see if it was registered by sending message back.

@ZER0
ZER0 added a note Dec 29, 2011

I did it later in the test. This is one of the unit testing possible approaches, where we test every single method before use it, and at the end we test the flows using the methods already tested. Never use in a method's test or flow's test a method is not already tested, basically that is the principle.
As we discussed in the IRC, we will have a meeting with @mykmelez in order to figure out the pros and the cons about different unit testing approaches. I have no particular hard feelings about it, I'd like have a good an consistent way to test our code that we can use for all tests in jetpack as guidelines.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@Gozala Gozala commented on the diff Dec 23, 2011
packages/api-utils/tests/test-message-manager.js
+ assert.equal(topic in listeners, false,
+ "No listeners for MessageManager");
+ assert.equal(topic in remoteListeners, false,
+ "No listeners for Remote Frame");
+
+ mm.addMessageListener(topic, listener);
+
+ assert.deepEqual(listeners[topic], [listener],
+ "Listener is added properly");
+ assert.equal(topic in remoteListeners, false,
+ "No listeners for Remote Frame");
+ }
+
+
+exports["test MessageManager addMessageListener with duplicates"] =
+ function(assert) {
@Gozala
Mozilla member
Gozala added a note Dec 23, 2011

same here, just see how many times listeners were called.

@ZER0
ZER0 added a note Dec 30, 2011

see my previous reply.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@ZER0
ZER0 commented Dec 30, 2011

@Gozala about the overlap with EventEmitter, as we discussed on IRC there are some slightly difference that makes not possible reuse the same code (e.g. I need to collect the responses for sync calls).

@mykmelez

Note that you could also have consumers work around the namespace collision at require time via:

let { Namespace: anything } = require("api-utils/namespace");

(where anything can be any non-conflicting name)

Nevertheless, exporting ns directly gives consumers the simplest possible workaround, so it seems worthwhile.

@mykmelez

გამარჯობა, welcome, nice!

I wonder if we should factor out this string at some point, now that it's being used in three places. Hmm... it probably isn't useful enough to do so yet.

@mykmelez
Mozilla member

@Gozala reviewed this pull request and gave it an r+, but @ZER0 made substantive enough changes that he wanted a followup review, so I did that today, as @Gozala is away; and from what I can tell, this patch is ready to land, so r=@mykmelez.

@mykmelez mykmelez merged commit 6eccac8 into mozilla:master Dec 30, 2011
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.