Source code
Revision control
Copy as Markdown
Other Tools
/* 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
"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);
}
}
// Duplicate the ID into another field as `actorID` will be cleared on destruction
actor.persistedActorID = actor.actorID;
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.
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;
}
}
// `conn` might be null for LazyPool in an unexplained way.
if (this.conn) {
this.conn.removeActorPool(this);
this.conn = null;
}
}
}
exports.Pool = Pool;