Skip to content
This repository has been archived by the owner on Feb 26, 2022. It is now read-only.

Bug 854980 - Implementing simple utils for observing window / tab events #903

Merged
merged 1 commit into from Apr 1, 2013
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
20 changes: 20 additions & 0 deletions lib/sdk/browser/events.js
@@ -0,0 +1,20 @@
/* 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/. */
"use strict";

module.metadata = {
"stability": "unstable"
};

const { events } = require("../window/events");
const { filter } = require("../event/utils");
const { isBrowser } = require("../window/utils");

// TODO: `isBrowser` detects weather window is a browser by checking
// `windowtype` attribute, which means that all 'open' events will be
// filtered out since document is not loaded yet. Maybe we can find a better
// implementation for `isBrowser`. Either way it's not really needed yet
// neither window tracker provides this event.

exports.events = filter(function({target}) isBrowser(target), events);
26 changes: 26 additions & 0 deletions lib/sdk/event/dom.js
@@ -0,0 +1,26 @@
/* 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/. */

"use strict";

module.metadata = {
"stability": "unstable"
};

let { emit, on, off } = require("./core");

// Simple utility function takes event target, event type and optional
// `options.capture` and returns node style event stream that emits "data"
// events every time event of that type occurs on the given `target`.
function open(target, type, options) {
let output = {};
let capture = options && options.capture ? true : false;

target.addEventListener(type, function(event) {
emit(output, "data", event);
}, capture);

return output;
}
exports.open = open;
101 changes: 101 additions & 0 deletions lib/sdk/event/utils.js
@@ -0,0 +1,101 @@
/* 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/. */
"use strict";

module.metadata = {
"stability": "unstable"
};

let { emit, on, off } = require("./core");

// This module provides set of high order function for working with event
// streams (streams in a NodeJS style that dispatch data, end and error
// events).

// Function takes a `target` object and returns set of implicit references
// (non property references) it keeps. This basically allows defining
// references between objects without storing the explicitly. See transform for
// more details.
let refs = (function() {
let refSets = new WeakMap();
return function refs(target) {
if (!refSets.has(target)) refSets.set(target, new Set());
return refSets.get(target);
}
})();

function transform(f, input) {
let output = {};

// Since event listeners don't prevent `input` to be GC-ed we wanna presrve
// it until `output` can be GC-ed. There for we add implicit reference which
// is removed once `input` ends.
refs(output).add(input);

function next(data) emit(output, "data", data);
on(input, "error", function(error) emit(output, "error", error));
on(input, "end", function() {
refs(output).delete(input);
emit(output, "end");
});
on(input, "data", function(data) f(data, next));
return output;
}

// High order event transformation function that takes `input` event channel
// and returns transformation containing only events on which `p` predicate
// returns `true`.
function filter(predicate, input) {
return transform(function(data, next) {
if (predicate(data)) next(data)
}, input);
}
exports.filter = filter;

// High order function that takes `input` and returns input of it's values
// mapped via given `f` function.
function map(f, input) transform(function(data, next) next(f(data)), input)
exports.map = map;

// High order function that takes `input` stream of streams and merges them
// into single event stream. Like flatten but time based rather than order
// based.
function merge(inputs) {
let output = {};
let open = 1;
let state = [];
output.state = state;
refs(output).add(inputs);

function end(input) {
open = open - 1;
refs(output).delete(input);
if (open === 0) emit(output, "end");
}
function error(e) emit(output, "error", e);
function forward(input) {
state.push(input);
open = open + 1;
on(input, "end", function() end(input));
on(input, "error", error);
on(input, "data", function(data) emit(output, "data", data));
}

// If `inputs` is an array treat it as a stream.
if (Array.isArray(inputs)) {
inputs.forEach(forward)
end(inputs)
}
else {
on(inputs, "end", function() end(inputs));
on(inputs, "error", error);
on(inputs, "data", forward);
}

return output;
}
exports.merge = merge;

function expand(f, inputs) merge(map(f, inputs))
exports.expand = expand;
60 changes: 60 additions & 0 deletions lib/sdk/tab/events.js
@@ -0,0 +1,60 @@
/* 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/. */

"use strict";

// This module provides temporary shim until Bug 843901 is shipped.
// It basically registers tab event listeners on all windows that get
// opened and forwards them through observer notifications.

module.metadata = {
"stability": "experimental"
};

const { Ci } = require("chrome");
const { windows, isInteractive } = require("../window/utils");
const { events } = require("../browser/events");
const { open } = require("../event/dom");
const { filter, map, merge, expand } = require("../event/utils");

// Module provides event stream (in nodejs style) that emits data events
// for all the tab events that happen in running firefox. At the moment
// it does it by registering listeners on all browser windows and then
// forwarding events when they occur to a stream. This will become obsolete
// once Bug 843901 is fixed, and we'll just leverage observer notifications.

// Set of tab events that this module going to aggregate and expose.
const TYPES = ["TabOpen","TabClose","TabSelect","TabMove","TabPinned",
"TabUnpinned"];

// Utility function that given a browser `window` returns stream of above
// defined tab events for all tabs on the given window.
function tabEventsFor(window) {
// Map supported event types to a streams of those events on the given
// `window` and than merge these streams into single form stream off
// all events.
let channels = TYPES.map(function(type) open(window, type));
return merge(channels);
}

// Filter DOMContentLoaded events from all the browser events.
let readyEvents = filter(function(e) e.type === "DOMContentLoaded", events);
// Map DOMContentLoaded events to it's target browser windows.
let futureWindows = map(function(e) e.target, readyEvents);
// Expand all browsers that will become interactive to supported tab events
// on these windows. Result will be a tab events from all tabs of all windows
// that will become interactive.
let eventsFromFuture = expand(tabEventsFor, futureWindows);

// Above covers only windows that will become interactive in a future, but some
// windows may already be interactive so we pick those and expand to supported
// tab events for them too.
let interactiveWindows = windows("navigator:browser", { includePrivate: true }).
filter(isInteractive);
let eventsFromInteractive = merge(interactiveWindows.map(tabEventsFor));


// Finally merge stream of tab events from future windows and current windows
// to cover all tab events on all windows that will open.
exports.events = merge([eventsFromInteractive, eventsFromFuture]);
50 changes: 50 additions & 0 deletions lib/sdk/tabs/utils.js
Expand Up @@ -303,3 +303,53 @@ function getTabForBrowser(browser) {
}
exports.getTabForBrowser = getTabForBrowser;

function pin(tab) {
let gBrowser = getTabBrowserForTab(tab);
// TODO: Implement Fennec support
if (gBrowser) gBrowser.pinTab(tab);
}
exports.pin = pin;

function unpin(tab) {
let gBrowser = getTabBrowserForTab(tab);
// TODO: Implement Fennec support
if (gBrowser) gBrowser.unpinTab(tab);
}
exports.unpin = unpin;

function isPinned(tab) !!tab.pinned
exports.isPinned = isPinned;

function reload(tab) {
let gBrowser = getTabBrowserForTab(tab);
// Firefox
if (gBrowser) gBrowser.unpinTab(tab);
// Fennec
else if (tab.browser) tab.browser.reload();
}
exports.reload = reload

function getIndex(tab) {
let gBrowser = getTabBrowserForTab(tab);
// Firefox
if (gBrowser) {
let document = getBrowserForTab(tab).contentDocument;
return gBrowser.getBrowserIndexForDocument(document);
}
// Fennec
else {
let window = getWindowHoldingTab(tab)
let tabs = window.BrowserApp.tabs;
for (let i = tabs.length; i >= 0; i--)
if (tabs[i] === tab) return i;
}
}
exports.getIndex = getIndex;

function move(tab, index) {
let gBrowser = getTabBrowserForTab(tab);
// Firefox
if (gBrowser) gBrowser.moveTabTo(tab, index);
// TODO: Implement fennec support
}
exports.move = move;
80 changes: 80 additions & 0 deletions lib/sdk/window/events.js
@@ -0,0 +1,80 @@
/* 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/. */
"use strict";

module.metadata = {
"stability": "unstable"
};

const { Ci } = require("chrome");
const events = require("../system/events");
const { on, off, emit } = require("../event/core");
const { windows } = require("../window/utils");

// Object represents event channel on which all top level window events
// will be dispatched, allowing users to react to those evens.
const channel = {};
exports.events = channel;

const types = {
domwindowopened: "open",
domwindowclosed: "close",
}

// Utility function to query observer notification subject to get DOM window.
function nsIDOMWindow($) $.QueryInterface(Ci.nsIDOMWindow);

// Utility function used as system event listener that is invoked every time
// top level window is open. This function does two things:
// 1. Registers event listeners to track when document becomes interactive and
// when it's done loading. This will become obsolete once Bug 843910 is
// fixed.
// 2. Forwards event to an exported event stream.
function onOpen(event) {
observe(nsIDOMWindow(event.subject));
dispatch(event);
}

// Function registers single shot event listeners for relevant window events
// that forward events to exported event stream.
function observe(window) {
function listener(event) {
if (event.target === window.document) {
window.removeEventListener(event.type, listener, true);
emit(channel, "data", { type: event.type, target: window });
}
}

// Note: we do not remove listeners on unload since on add-on unload we
// nuke add-on sandbox that should allow GC-ing listeners. This also has
// positive effects on add-on / firefox unloads.
window.addEventListener("DOMContentLoaded", listener, true);
window.addEventListener("load", listener, true);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is the same window being emitted twice here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also shouldn't these listeners be removed early if unload occurs before the DOMContentLoaded or load events?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is the same window being emitted twice here?

Different events for the same window yes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also shouldn't these listeners be removed early if unload occurs before the DOMContentLoaded or load events?

I don't think it's necessary because if sandbox is nuked it GC-ed regardless of weather it had listeners on other
windows and if that's not the case it's a bug. And we do nuke add-on sandbox allowing GC to claim all of this: https://github.com/mozilla/addon-sdk/blob/master/app-extension/bootstrap.js#L284

Very little window for load / DOMContentLoaded events to go through is if add-on has being unloaded while
window is being loaded and window managed to load before add-on was unloaded completely.

On the other hand not doing all the manual tear down will benefit both add-on and firefox unloads, so I think we
should leverage GC as much as we can instead of doing manual tear-downs.

Finally all the high level APIs that will use this will likely handle unloads in some ways and that's where listeners to exports.events can be removed. This has benefits of having less things to do at unload as well.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

alright I guess this isn't an issue anymore then.

// TODO: Also add focus event listener so that can be forwarded to event
// stream. It can be part of Bug 854982.
}

// Utility function that takes system notification event and forwards it to a
// channel in restructured form.
function dispatch({ type: topic, subject }) {
emit(channel, "data", {
topic: topic,
type: types[topic],
target: nsIDOMWindow(subject)
});
}

// In addition to observing windows that are open we also observe windows
// that are already already opened in case they're in process of loading.
let opened = windows(null, { includePrivate: true });
opened.forEach(observe);

// Register system event listeners to forward messages on exported event
// stream. Note that by default only weak refs are kept by system events
// module so they will be GC-ed once add-on unloads and no manual cleanup
// is required. Also note that listeners are intentionally not inlined since
// to avoid premature GC-ing. Currently refs are kept by module scope and there
// for they remain alive.
events.on("domwindowopened", onOpen);
events.on("domwindowclosed", dispatch);
9 changes: 9 additions & 0 deletions lib/sdk/window/utils.js
Expand Up @@ -275,6 +275,15 @@ function windows(type, options) {
}
exports.windows = windows;

/**
* Check if the given window is interactive.
* i.e. if its "DOMContentLoaded" event has already been fired.
* @params {nsIDOMWindow} window
*/
function isInteractive(window)
window.document.readyState === "interactive" || isDocumentLoaded(window)
exports.isInteractive = isInteractive;

/**
* Check if the given window is completely loaded.
* i.e. if its "load" event has already been fired and all possible DOM content
Expand Down