/* 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, * You can obtain one at http://mozilla.org/MPL/2.0/. */ /** * 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();