summaryrefslogtreecommitdiffstats
path: root/devtools/shared/protocol/Pool.js
blob: b5cb4c3eb11dad548f3ace069e772343a66bf2eb (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
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
/* 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";

var EventEmitter = require("resource://devtools/shared/event-emitter.js");

/**
 * Actor and Front implementations
 */

/**
 * A protocol object that can manage the lifetime of other protocol
 * objects. Pools are used on both sides of the connection to help coordinate lifetimes.
 *
 * @param {DevToolsServerConnection|DevToolsClient} [conn]
 *   Either a DevToolsServerConnection or a DevToolsClient. Must have
 *   addActorPool, removeActorPool, and poolFor.
 *   conn can be null if the subclass provides a conn property.
 * @param {String} [label]
 *   An optional label for the Pool.
 * @constructor
 */
class Pool extends EventEmitter {
  constructor(conn, label) {
    super();

    if (conn) {
      this.conn = conn;
    }
    this.label = label;

    // Will be individually flipped to true by Actor/Front classes.
    // Will also only be exposed via Actor/Front::isDestroyed().
    this._isDestroyed = false;
  }

  __poolMap = null;
  parentPool = null;

  /**
   * Return the parent pool for this client.
   */
  getParent() {
    return this.parentPool;
  }

  /**
   * A pool is at the top of its pool hierarchy if it has:
   * - no parent
   * - or it is its own parent
   */
  isTopPool() {
    const parent = this.getParent();
    return !parent || parent === this;
  }

  poolFor(actorID) {
    return this.conn.poolFor(actorID);
  }

  /**
   * Override this if you want actors returned by this actor
   * to belong to a different actor by default.
   */
  marshallPool() {
    return this;
  }

  /**
   * Pool is the base class for all actors, even leaf nodes.
   * If the child map is actually referenced, go ahead and create
   * the stuff needed by the pool.
   */
  get _poolMap() {
    if (this.__poolMap) {
      return this.__poolMap;
    }
    this.__poolMap = new Map();
    this.conn.addActorPool(this);
    return this.__poolMap;
  }

  /**
   * Add an actor as a child of this pool.
   */
  manage(actor) {
    if (!actor.actorID) {
      actor.actorID = this.conn.allocID(actor.typeName);
    } else {
      // If the actor is already registered in a pool, remove it without destroying it.
      // This happens for example when an addon is reloaded. To see this behavior, take a
      // look at devtools/server/tests/xpcshell/test_addon_reload.js

      const parent = actor.getParent();
      if (parent && parent !== this) {
        parent.unmanage(actor);
      }
    }

    this._poolMap.set(actor.actorID, actor);
    actor.parentPool = this;
  }

  unmanageChildren(FrontType) {
    for (const front of this.poolChildren()) {
      if (!FrontType || front instanceof FrontType) {
        this.unmanage(front);
      }
    }
  }

  /**
   * Remove an actor as a child of this pool.
   */
  unmanage(actor) {
    if (this.__poolMap) {
      this.__poolMap.delete(actor.actorID);
    }
    actor.parentPool = null;
  }

  // true if the given actor ID exists in the pool.
  has(actorID) {
    return this.__poolMap && this._poolMap.has(actorID);
  }

  /**
   * Search for an actor in this pool, given an actorID
   * @param {String} actorID
   * @returns {Actor/null} Returns null if the actor wasn't found
   */
  getActorByID(actorID) {
    if (this.__poolMap) {
      return this._poolMap.get(actorID);
    }
    return null;
  }

  // Generator that yields each non-self child of the pool.
  *poolChildren() {
    if (!this.__poolMap) {
      return;
    }
    for (const actor of this.__poolMap.values()) {
      // Self-owned actors are ok, but don't need visiting twice.
      if (actor === this) {
        continue;
      }
      yield actor;
    }
  }

  isDestroyed() {
    // Note: _isDestroyed is only flipped from Actor and Front subclasses for
    // now, so this method should not be called on pure Pool instances.
    // See Bug 1717811.
    return this._isDestroyed;
  }

  /**
   * Pools can override this method in order to opt-out of a destroy sequence.
   *
   * For instance, Fronts are destroyed during the toolbox destroy. However when
   * the toolbox is destroyed, the document holding the toolbox is also
   * destroyed. So it should not be necessary to cleanup Fronts during toolbox
   * destroy.
   *
   * For the time being, Fronts (or Pools in general) which want to opt-out of
   * toolbox destroy can override this method and check the value of
   * `this.conn.isToolboxDestroy`.
   */
  skipDestroy() {
    return false;
  }

  /**
   * Destroy this item, removing it from a parent if it has one,
   * and destroying all children if necessary.
   */
  destroy() {
    const parent = this.getParent();
    if (parent) {
      parent.unmanage(this);
    }
    if (!this.__poolMap) {
      return;
    }
    // Immediately clear the poolmap so that we bail out early if the code is reentrant.
    const poolMap = this.__poolMap;
    this.__poolMap = null;

    for (const actor of poolMap.values()) {
      // Self-owned actors are ok, but don't need destroying twice.
      if (actor === this) {
        continue;
      }

      // Some pool-managed values don't extend Pool and won't have skipDestroy
      // defined. For instance, the root actor and the lazy actors.
      if (typeof actor.skipDestroy === "function" && actor.skipDestroy()) {
        continue;
      }

      const destroy = actor.destroy;
      if (destroy) {
        // Disconnect destroy while we're destroying in case of (misbehaving)
        // circular ownership.
        actor.destroy = null;
        destroy.call(actor);
        actor.destroy = destroy;
      }
    }
    this.conn.removeActorPool(this);
    this.conn = null;
  }
}

exports.Pool = Pool;