Source code
Revision control
Copy as Markdown
Other Tools
/* vim: set ts=2 sw=2 sts=2 et tw=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,
/**
* This module holds weak references to DOM elements that exist within the
* current content process, and converts them to a unique identifier that can be
* passed between processes. The identifer, if received by the same content process
* that issued it, can then be converted back into the DOM element (presuming the
* element hasn't had all of its other references dropped).
*
* The hope is that this module can eliminate the need for passing CPOW references
* between processes during runtime.
*/
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const lazy = {};
XPCOMUtils.defineLazyServiceGetter(
lazy,
"finalizationService",
"@mozilla.org/toolkit/finalizationwitness;1",
"nsIFinalizationWitnessService"
);
/**
* @typedef {number} ElementID
* @typedef {Object} ElementIdentifier
*/
const FINALIZATION_TOPIC = "content-dom-reference-finalized";
// A WeakMap which ties finalization witness objects to the lifetime of the DOM
// nodes they're meant to witness. When the DOM node in the map key is
// finalized, the WeakMap stops holding the finalization witness in its value
// alive, which alerts our observer that the element has been destroyed.
const finalizerRoots = new WeakMap();
/**
* An identifier generated by ContentDOMReference is a unique pair of BrowsingContext
* ID and a numeric ID. gRegistry maps BrowsingContext's to an object with the following
* properties:
*
* IDToElement:
* A Map of IDs to WeakReference's to the elements they refer to.
*
* elementToID:
* A WeakMap from a DOM element to an ID that refers to it.
*/
var gRegistry = new WeakMap();
export var ContentDOMReference = {
_init() {
Services.obs.addObserver(this, FINALIZATION_TOPIC);
},
observe(subject, topic, data) {
if (topic !== FINALIZATION_TOPIC) {
throw new Error("Unexpected observer topic");
}
let identifier = JSON.parse(data);
this._revoke(identifier);
},
/**
* Generate and return an identifier for a given DOM element.
*
* @param {Element} element The DOM element to generate the identifier for.
* @return {ElementIdentifier} The identifier for the DOM element that can be passed between
* processes as a message.
*/
get(element) {
if (!element) {
throw new Error(
"Can't create a ContentDOMReference identifier for " +
"non-existant nodes."
);
}
let browsingContext = BrowsingContext.getFromWindow(element.ownerGlobal);
let mappings = gRegistry.get(browsingContext);
if (!mappings) {
mappings = {
IDToElement: new Map(),
elementToID: new WeakMap(),
};
gRegistry.set(browsingContext, mappings);
}
let id = mappings.elementToID.get(element);
if (id) {
// We already had this element registered, so return the pre-existing ID.
return { browsingContextId: browsingContext.id, id };
}
// We must be registering a new element at this point.
id = Math.random();
mappings.elementToID.set(element, id);
mappings.IDToElement.set(id, Cu.getWeakReference(element));
let identifier = { browsingContextId: browsingContext.id, id };
finalizerRoots.set(
element,
lazy.finalizationService.make(
FINALIZATION_TOPIC,
JSON.stringify(identifier)
)
);
return identifier;
},
/**
* Resolves an identifier back into the DOM Element that it was generated from.
*
* @param {ElementIdentifier} The identifier generated via ContentDOMReference.get for a
* DOM element.
* @return {Element} The DOM element that the identifier was generated for, or
* null if the element does not still exist.
*/
resolve(identifier) {
let browsingContext = BrowsingContext.get(identifier.browsingContextId);
let { id } = identifier;
return this._resolveIDToElement(browsingContext, id);
},
/**
* Removes an identifier from the registry so that subsequent attempts
* to resolve it will result in null. This is done automatically when the
* target node is GCed.
*
* @param {ElementIdentifier} The identifier to revoke, issued by ContentDOMReference.get for
* a DOM element.
*/
_revoke(identifier) {
let browsingContext = BrowsingContext.get(identifier.browsingContextId);
let { id } = identifier;
let mappings = gRegistry.get(browsingContext);
if (!mappings) {
return;
}
mappings.IDToElement.delete(id);
},
/**
* Private helper function that resolves a BrowsingContext and ID (the
* pair that makes up an identifier) to a DOM element.
*
* @param {BrowsingContext} browsingContext The BrowsingContext that was hosting
* the DOM element at the time that the identifier was generated.
* @param {ElementID} id The ID generated for the DOM element.
*
* @return {Element} The DOM element that the identifier was generated for, or
* null if the element does not still exist.
*/
_resolveIDToElement(browsingContext, id) {
let mappings = gRegistry.get(browsingContext);
if (!mappings) {
return null;
}
let weakReference = mappings.IDToElement.get(id);
if (!weakReference) {
return null;
}
return weakReference.get();
},
};
ContentDOMReference._init();