summaryrefslogtreecommitdiffstats
path: root/toolkit/modules/ContentDOMReference.sys.mjs
blob: ef4896ff6747c1865bd484fe384ef61ee9be11ca (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
/* 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();