/* 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"; const protocol = require("devtools/server/protocol"); const { method, RetVal, Arg, types } = protocol; const { expectState, ActorPool } = require("devtools/server/actors/common"); const { ObjectActor, createValueGrip } = require("devtools/server/actors/object"); const DevToolsUtils = require("devtools/shared/DevToolsUtils"); loader.lazyRequireGetter(this, "events", "sdk/event/core"); // Teach protocol.js how to deal with legacy actor types types.addType("ObjectActor", { write: actor => actor.grip(), read: grip => grip }); /** * The Promises Actor provides support for getting the list of live promises and * observing changes to their settlement state. */ var PromisesActor = protocol.ActorClass({ typeName: "promises", events: { // Event emitted for new promises allocated in debuggee and bufferred by // sending the list of promise objects in a batch. "new-promises": { type: "new-promises", data: Arg(0, "array:ObjectActor"), }, // Event emitted for promise settlements. "promises-settled": { type: "promises-settled", data: Arg(0, "array:ObjectActor") } }, /** * @param conn DebuggerServerConnection. * @param parent TabActor|RootActor */ initialize: function(conn, parent) { protocol.Actor.prototype.initialize.call(this, conn); this.conn = conn; this.parent = parent; this.state = "detached"; this._dbg = null; this._gripDepth = 0; this._navigationLifetimePool = null; this._newPromises = null; this._promisesSettled = null; this.objectGrip = this.objectGrip.bind(this); this._makePromiseEventHandler = this._makePromiseEventHandler.bind(this); this._onWindowReady = this._onWindowReady.bind(this); }, destroy: function() { protocol.Actor.prototype.destroy.call(this, this.conn); if (this.state === "attached") { this.detach(); } }, get dbg() { if (!this._dbg) { this._dbg = this.parent.makeDebugger(); } return this._dbg; }, /** * Attach to the PromisesActor. */ attach: method(expectState("detached", function() { this.dbg.addDebuggees(); this._navigationLifetimePool = this._createActorPool(); this.conn.addActorPool(this._navigationLifetimePool); this._newPromises = []; this._promisesSettled = []; this.dbg.findScripts().forEach(s => { this.parent.sources.createSourceActors(s.source); }); this.dbg.onNewScript = s => { this.parent.sources.createSourceActors(s.source); }; events.on(this.parent, "window-ready", this._onWindowReady); this.state = "attached"; }, `attaching to the PromisesActor`), { request: {}, response: {} }), /** * Detach from the PromisesActor upon Debugger closing. */ detach: method(expectState("attached", function() { this.dbg.removeAllDebuggees(); this.dbg.enabled = false; this._dbg = null; this._newPromises = null; this._promisesSettled = null; if (this._navigationLifetimePool) { this.conn.removeActorPool(this._navigationLifetimePool); this._navigationLifetimePool = null; } events.off(this.parent, "window-ready", this._onWindowReady); this.state = "detached"; }, `detaching from the PromisesActor`), { request: {}, response: {} }), _createActorPool: function() { let pool = new ActorPool(this.conn); pool.objectActors = new WeakMap(); return pool; }, /** * Create an ObjectActor for the given Promise object. * * @param object promise * The promise object * @return object * An ObjectActor object that wraps the given Promise object */ _createObjectActorForPromise: function(promise) { if (this._navigationLifetimePool.objectActors.has(promise)) { return this._navigationLifetimePool.objectActors.get(promise); } let actor = new ObjectActor(promise, { getGripDepth: () => this._gripDepth, incrementGripDepth: () => this._gripDepth++, decrementGripDepth: () => this._gripDepth--, createValueGrip: v => createValueGrip(v, this._navigationLifetimePool, this.objectGrip), sources: () => this.parent.sources, createEnvironmentActor: () => DevToolsUtils.reportException( "PromisesActor", Error("createEnvironmentActor not yet implemented")), getGlobalDebugObject: () => DevToolsUtils.reportException( "PromisesActor", Error("getGlobalDebugObject not yet implemented")), }); this._navigationLifetimePool.addActor(actor); this._navigationLifetimePool.objectActors.set(promise, actor); return actor; }, /** * Get a grip for the given Promise object. * * @param object value * The Promise object * @return object * The grip for the given Promise object */ objectGrip: function(value) { return this._createObjectActorForPromise(value).grip(); }, /** * Get a list of ObjectActors for all live Promise Objects. */ listPromises: method(function() { let promises = this.dbg.findObjects({ class: "Promise" }); this.dbg.onNewPromise = this._makePromiseEventHandler(this._newPromises, "new-promises"); this.dbg.onPromiseSettled = this._makePromiseEventHandler( this._promisesSettled, "promises-settled"); return promises.map(p => this._createObjectActorForPromise(p)); }, { request: { }, response: { promises: RetVal("array:ObjectActor") } }), /** * Creates an event handler for onNewPromise that will add the new * Promise ObjectActor to the array and schedule it to be emitted as a * batch for the provided event. * * @param array array * The list of Promise ObjectActors to emit * @param string eventName * The event name */ _makePromiseEventHandler: function(array, eventName) { return promise => { let actor = this._createObjectActorForPromise(promise); let needsScheduling = array.length == 0; array.push(actor); if (needsScheduling) { DevToolsUtils.executeSoon(() => { events.emit(this, eventName, array.splice(0, array.length)); }); } }; }, _onWindowReady: expectState("attached", function({ isTopLevel }) { if (!isTopLevel) { return; } this._navigationLifetimePool.cleanup(); this.dbg.removeAllDebuggees(); this.dbg.addDebuggees(); }) }); exports.PromisesActor = PromisesActor; exports.PromisesFront = protocol.FrontClass(PromisesActor, { initialize: function(client, form) { protocol.Front.prototype.initialize.call(this, client, form); this.actorID = form.promisesActor; this.manage(this); }, destroy: function() { protocol.Front.prototype.destroy.call(this); } });