Bug 1450944 - Convert ObjectActor to protocol.js; r=ochameau. draft
authorNicolas Chevobbe <nchevobbe@mozilla.com>
Fri, 20 Apr 2018 07:56:26 +0200
changeset 811787 3c2400ba0bdc0d12daf339667fe8c50891c194a0
parent 811678 f8acdf0185d786809bfbe8cabab081400dc47c68
child 811788 dcc87d659281b909fe973da33b13b9a655d9d300
push id114426
push userbmo:nchevobbe@mozilla.com
push dateThu, 28 Jun 2018 10:24:35 +0000
reviewersochameau
bugs1450944
milestone63.0a1
Bug 1450944 - Convert ObjectActor to protocol.js; r=ochameau. MozReview-Commit-ID: KFsBT3aywBz
devtools/server/actors/object.js
devtools/server/actors/object/property-iterator.js
devtools/server/actors/object/symbol-iterator.js
devtools/server/actors/pause-scoped.js
devtools/server/actors/promises.js
devtools/server/actors/thread.js
devtools/server/actors/webconsole.js
devtools/shared/specs/index.js
devtools/shared/specs/moz.build
devtools/shared/specs/object.js
devtools/shared/specs/promises.js
devtools/shared/specs/symbol-iterator.js
--- a/devtools/server/actors/object.js
+++ b/devtools/server/actors/object.js
@@ -6,88 +6,92 @@
 
 "use strict";
 
 const { Cu } = require("chrome");
 const { GeneratedLocation } = require("devtools/server/actors/common");
 const DevToolsUtils = require("devtools/shared/DevToolsUtils");
 const { assert } = DevToolsUtils;
 
+const protocol = require("devtools/shared/protocol");
+const { objectSpec } = require("devtools/shared/specs/object");
+
 loader.lazyRequireGetter(this, "PropertyIteratorActor", "devtools/server/actors/object/property-iterator", true);
 loader.lazyRequireGetter(this, "SymbolIteratorActor", "devtools/server/actors/object/symbol-iterator", true);
 loader.lazyRequireGetter(this, "previewers", "devtools/server/actors/object/previewers");
 loader.lazyRequireGetter(this, "stringify", "devtools/server/actors/object/stringifiers");
 
 const {
   getArrayLength,
   getPromiseState,
   getStorageLength,
   isArray,
   isStorage,
   isTypedArray,
 } = require("devtools/server/actors/object/utils");
-/**
- * Creates an actor for the specified object.
- *
- * @param obj Debugger.Object
- *        The debuggee object.
- * @param hooks Object
- *        A collection of abstract methods that are implemented by the caller.
- *        ObjectActor requires the following functions to be implemented by
- *        the caller:
- *          - createValueGrip
- *              Creates a value grip for the given object
- *          - sources
- *              TabSources getter that manages the sources of a thread
- *          - createEnvironmentActor
- *              Creates and return an environment actor
- *          - getGripDepth
- *              An actor's grip depth getter
- *          - incrementGripDepth
- *              Increment the actor's grip depth
- *          - decrementGripDepth
- *              Decrement the actor's grip depth
- *          - globalDebugObject
- *              The Debuggee Global Object as given by the ThreadActor
- */
-function ObjectActor(obj, {
-  createValueGrip: createValueGripHook,
-  sources,
-  createEnvironmentActor,
-  getGripDepth,
-  incrementGripDepth,
-  decrementGripDepth,
-  getGlobalDebugObject
-}) {
-  assert(!obj.optimizedOut,
-         "Should not create object actors for optimized out values!");
-  this.obj = obj;
-  this.hooks = {
+
+const proto = {
+  /**
+   * Creates an actor for the specified object.
+   *
+   * @param obj Debugger.Object
+   *        The debuggee object.
+   * @param Object
+   *        A collection of abstract methods that are implemented by the caller.
+   *        ObjectActor requires the following functions to be implemented by
+   *        the caller:
+   *          - createValueGrip
+   *              Creates a value grip for the given object
+   *          - sources
+   *              TabSources getter that manages the sources of a thread
+   *          - createEnvironmentActor
+   *              Creates and return an environment actor
+   *          - getGripDepth
+   *              An actor's grip depth getter
+   *          - incrementGripDepth
+   *              Increment the actor's grip depth
+   *          - decrementGripDepth
+   *              Decrement the actor's grip depth
+   *          - globalDebugObject
+   *              The Debuggee Global Object as given by the ThreadActor
+   */
+  initialize(obj, {
     createValueGrip: createValueGripHook,
     sources,
     createEnvironmentActor,
     getGripDepth,
     incrementGripDepth,
     decrementGripDepth,
     getGlobalDebugObject
-  };
-  this.iterators = new Set();
-}
+  }, conn) {
+    assert(!obj.optimizedOut,
+          "Should not create object actors for optimized out values!");
+    protocol.Actor.prototype.initialize.call(this, conn);
 
-ObjectActor.prototype = {
-  actorPrefix: "obj",
+    this.conn = conn;
+    this.obj = obj;
+    this.hooks = {
+      createValueGrip: createValueGripHook,
+      sources,
+      createEnvironmentActor,
+      getGripDepth,
+      incrementGripDepth,
+      decrementGripDepth,
+      getGlobalDebugObject
+    };
+  },
 
   rawValue: function() {
     return this.obj.unsafeDereference();
   },
 
   /**
    * Returns a grip for this actor for returning in a protocol message.
    */
-  grip: function() {
+  form: function() {
     const g = {
       "type": "object",
       "actor": this.actorID,
       "class": this.obj.class,
     };
 
     const unwrapped = DevToolsUtils.unwrap(this.obj);
 
@@ -190,136 +194,105 @@ ObjectActor.prototype = {
     if (state !== "pending") {
       promiseState.timeToSettle = this.obj.promiseTimeToResolution;
     }
 
     return promiseState;
   },
 
   /**
-   * Releases this actor from the pool.
-   */
-  release: function() {
-    if (this.registeredPool.objectActors) {
-      this.registeredPool.objectActors.delete(this.obj);
-    }
-    this.iterators.forEach(actor => this.registeredPool.removeActor(actor));
-    this.iterators.clear();
-    this.registeredPool.removeActor(this);
-  },
-
-  /**
    * Handle a protocol request to provide the definition site of this function
    * object.
    */
-  onDefinitionSite: function() {
+  definitionSite: function() {
     if (this.obj.class != "Function") {
-      return {
-        from: this.actorID,
-        error: "objectNotFunction",
-        message: this.actorID + " is not a function."
-      };
+      return this.throwError("objectNotFunction", this.actorID + " is not a function.");
     }
 
     if (!this.obj.script) {
-      return {
-        from: this.actorID,
-        error: "noScript",
-        message: this.actorID + " has no Debugger.Script"
-      };
+      return this.throwError("noScript", this.actorID + " has no Debugger.Script");
     }
 
     return this.hooks.sources().getOriginalLocation(new GeneratedLocation(
       this.hooks.sources().createNonSourceMappedActor(this.obj.script.source),
       this.obj.script.startLine,
       0 // TODO bug 901138: use Debugger.Script.prototype.startColumn
     )).then((originalLocation) => {
       return {
-        source: originalLocation.originalSourceActor.form(),
+        source: originalLocation.originalSourceActor,
         line: originalLocation.originalLine,
         column: originalLocation.originalColumn
       };
     });
   },
 
   /**
    * Handle a protocol request to provide the names of the properties defined on
    * the object and not its prototype.
    */
-  onOwnPropertyNames: function() {
+  ownPropertyNames: function() {
     let props = [];
     if (DevToolsUtils.isSafeDebuggerObject(this.obj)) {
       try {
         props = this.obj.getOwnPropertyNames();
       } catch (err) {
         // The above can throw when the debuggee does not subsume the object's
         // compartment, or for some WrappedNatives like Cu.Sandbox.
       }
     }
-    return { from: this.actorID, ownPropertyNames: props };
+    return { ownPropertyNames: props };
   },
 
   /**
    * Creates an actor to iterate over an object property names and values.
    * See PropertyIteratorActor constructor for more info about options param.
    *
-   * @param request object
-   *        The protocol request object.
+   * @param options object
    */
-  onEnumProperties: function(request) {
-    const actor = new PropertyIteratorActor(this, request.options);
-    this.registeredPool.addActor(actor);
-    this.iterators.add(actor);
-    return { iterator: actor.form() };
+  enumProperties: function(options) {
+    return PropertyIteratorActor(this, options, this.conn);
   },
 
   /**
    * Creates an actor to iterate over entries of a Map/Set-like object.
    */
-  onEnumEntries: function() {
-    const actor = new PropertyIteratorActor(this, { enumEntries: true });
-    this.registeredPool.addActor(actor);
-    this.iterators.add(actor);
-    return { iterator: actor.form() };
+  enumEntries: function() {
+    return PropertyIteratorActor(this, { enumEntries: true }, this.conn);
   },
 
   /**
    * Creates an actor to iterate over an object symbols properties.
    */
-  onEnumSymbols: function() {
-    const actor = new SymbolIteratorActor(this);
-    this.registeredPool.addActor(actor);
-    this.iterators.add(actor);
-    return { iterator: actor.form() };
+  enumSymbols: function() {
+    return SymbolIteratorActor(this, this.conn);
   },
 
   /**
    * Handle a protocol request to provide the prototype and own properties of
    * the object.
    *
    * @returns {Object} An object containing the data of this.obj, of the following form:
-   *          - {string} from: this.obj's actorID.
    *          - {Object} prototype: The descriptor of this.obj's prototype.
    *          - {Object} ownProperties: an object where the keys are the names of the
    *                     this.obj's ownProperties, and the values the descriptors of
    *                     the properties.
    *          - {Array} ownSymbols: An array containing all descriptors of this.obj's
    *                    ownSymbols. Here we have an array, and not an object like for
    *                    ownProperties, because we can have multiple symbols with the same
    *                    name in this.obj, e.g. `{[Symbol()]: "a", [Symbol()]: "b"}`.
    *          - {Object} safeGetterValues: an object that maps this.obj's property names
    *                     with safe getters descriptors.
    */
-  onPrototypeAndProperties: function() {
-    let proto = null;
+  prototypeAndProperties: function() {
+    let objProto = null;
     let names = [];
     let symbols = [];
     if (DevToolsUtils.isSafeDebuggerObject(this.obj)) {
       try {
-        proto = this.obj.proto;
+        objProto = this.obj.proto;
         names = this.obj.getOwnPropertyNames();
         symbols = this.obj.getOwnPropertySymbols();
       } catch (err) {
         // The above can throw when the debuggee does not subsume the object's
         // compartment, or for some WrappedNatives like Cu.Sandbox.
       }
     }
 
@@ -332,21 +305,22 @@ ObjectActor.prototype = {
 
     for (const sym of symbols) {
       ownSymbols.push({
         name: sym.toString(),
         descriptor: this._propertyDescriptor(sym)
       });
     }
 
-    return { from: this.actorID,
-             prototype: this.hooks.createValueGrip(proto),
-             ownProperties,
-             ownSymbols,
-             safeGetterValues: this._findSafeGetterValues(names) };
+    return {
+      prototype: this.hooks.createValueGrip(objProto),
+      ownProperties,
+      ownSymbols,
+      safeGetterValues: this._findSafeGetterValues(names)
+    };
   },
 
   /**
    * Find the safe getter values for the current Debugger.Object, |this.obj|.
    *
    * @private
    * @param array ownProperties
    *        The array that holds the list of known ownProperties names for
@@ -488,49 +462,46 @@ ObjectActor.prototype = {
 
     object._safeGetters = getters;
     return getters;
   },
 
   /**
    * Handle a protocol request to provide the prototype of the object.
    */
-  onPrototype: function() {
-    let proto = null;
+  prototype: function() {
+    let objProto = null;
     if (DevToolsUtils.isSafeDebuggerObject(this.obj)) {
-      proto = this.obj.proto;
+      objProto = this.obj.proto;
     }
-    return { from: this.actorID,
-             prototype: this.hooks.createValueGrip(proto) };
+    return { prototype: this.hooks.createValueGrip(objProto) };
   },
 
   /**
    * Handle a protocol request to provide the property descriptor of the
    * object's specified property.
    *
-   * @param request object
-   *        The protocol request object.
+   * @param name string
+   *        The property we want the description of.
    */
-  onProperty: function(request) {
-    if (!request.name) {
+  property: function(name) {
+    if (!name) {
       return { error: "missingParameter",
                message: "no property name was specified" };
     }
 
-    return { from: this.actorID,
-             descriptor: this._propertyDescriptor(request.name) };
+    return { descriptor: this._propertyDescriptor(name) };
   },
 
   /**
    * Handle a protocol request to provide the display string for the object.
    */
-  onDisplayString: function() {
+  displayString: function() {
     const string = stringify(this.obj);
-    return { from: this.actorID,
-             displayString: this.hooks.createValueGrip(string) };
+    return { displayString: this.hooks.createValueGrip(string) };
   },
 
   /**
    * A helper method that creates a property descriptor for the provided object,
    * properly formatted for sending in a protocol response.
    *
    * @private
    * @param string name
@@ -595,96 +566,92 @@ ObjectActor.prototype = {
       }
     }
     return retval;
   },
 
   /**
    * Handle a protocol request to provide the source code of a function.
    *
-   * @param request object
-   *        The protocol request object.
+   * @param pretty boolean
    */
-  onDecompile: function(request) {
+  decompile: function(pretty) {
     if (this.obj.class !== "Function") {
       return { error: "objectNotFunction",
                message: "decompile request is only valid for object grips " +
                         "with a 'Function' class." };
     }
 
-    return { from: this.actorID,
-             decompiledCode: this.obj.decompile(!!request.pretty) };
+    return { decompiledCode: this.obj.decompile(!!pretty) };
   },
 
   /**
    * Handle a protocol request to provide the parameters of a function.
    */
-  onParameterNames: function() {
+  parameterNames: function() {
     if (this.obj.class !== "Function") {
       return { error: "objectNotFunction",
                message: "'parameterNames' request is only valid for object " +
                         "grips with a 'Function' class." };
     }
 
     return { parameterNames: this.obj.parameterNames };
   },
 
   /**
-   * Handle a protocol request to release a thread-lifetime grip.
-   */
-  onRelease: function() {
-    this.release();
-    return {};
-  },
-
-  /**
    * Handle a protocol request to provide the lexical scope of a function.
    */
-  onScope: function() {
+  scope: function() {
     if (this.obj.class !== "Function") {
-      return { error: "objectNotFunction",
-               message: "scope request is only valid for object grips with a" +
-                        " 'Function' class." };
+      return {
+        error: "objectNotFunction",
+        message: "scope request is only valid for object grips with a 'Function' class."
+      };
     }
 
-    const envActor = this.hooks.createEnvironmentActor(this.obj.environment,
-                                                     this.registeredPool);
+    const { createEnvironmentActor } = this.hooks;
+    const envActor = createEnvironmentActor(this.obj.environment, this.registeredPool);
+
     if (!envActor) {
-      return { error: "notDebuggee",
-               message: "cannot access the environment of this function." };
+      return {
+        error: "notDebuggee",
+        message: "cannot access the environment of this function."
+      };
     }
 
-    return { from: this.actorID, scope: envActor.form() };
+    return {
+      scope: envActor
+    };
   },
 
   /**
    * Handle a protocol request to get the list of dependent promises of a
    * promise.
    *
    * @return object
    *         Returns an object containing an array of object grips of the
    *         dependent promises
    */
-  onDependentPromises: function() {
+  dependentPromises: function() {
     if (this.obj.class != "Promise") {
       return { error: "objectNotPromise",
                message: "'dependentPromises' request is only valid for " +
                         "object grips with a 'Promise' class." };
     }
 
     const promises = this.obj.promiseDependentPromises
                            .map(p => this.hooks.createValueGrip(p));
 
     return { promises };
   },
 
   /**
    * Handle a protocol request to get the allocation stack of a promise.
    */
-  onAllocationStack: function() {
+  allocationStack: function() {
     if (this.obj.class != "Promise") {
       return { error: "objectNotPromise",
                message: "'allocationStack' request is only valid for " +
                         "object grips with a 'Promise' class." };
     }
 
     let stack = this.obj.promiseAllocationSite;
     const allocationStacks = [];
@@ -695,25 +662,23 @@ ObjectActor.prototype = {
 
         if (source) {
           allocationStacks.push(source);
         }
       }
       stack = stack.parent;
     }
 
-    return Promise.all(allocationStacks).then(stacks => {
-      return { allocationStack: stacks };
-    });
+    return Promise.all(allocationStacks);
   },
 
   /**
    * Handle a protocol request to get the fulfillment stack of a promise.
    */
-  onFulfillmentStack: function() {
+  fulfillmentStack: function() {
     if (this.obj.class != "Promise") {
       return { error: "objectNotPromise",
                message: "'fulfillmentStack' request is only valid for " +
                         "object grips with a 'Promise' class." };
     }
 
     let stack = this.obj.promiseResolutionSite;
     const fulfillmentStacks = [];
@@ -724,25 +689,23 @@ ObjectActor.prototype = {
 
         if (source) {
           fulfillmentStacks.push(source);
         }
       }
       stack = stack.parent;
     }
 
-    return Promise.all(fulfillmentStacks).then(stacks => {
-      return { fulfillmentStack: stacks };
-    });
+    return Promise.all(fulfillmentStacks);
   },
 
   /**
    * Handle a protocol request to get the rejection stack of a promise.
    */
-  onRejectionStack: function() {
+  rejectionStack: function() {
     if (this.obj.class != "Promise") {
       return { error: "objectNotPromise",
                message: "'rejectionStack' request is only valid for " +
                         "object grips with a 'Promise' class." };
     }
 
     let stack = this.obj.promiseResolutionSite;
     const rejectionStacks = [];
@@ -753,19 +716,17 @@ ObjectActor.prototype = {
 
         if (source) {
           rejectionStacks.push(source);
         }
       }
       stack = stack.parent;
     }
 
-    return Promise.all(rejectionStacks).then(stacks => {
-      return { rejectionStack: stacks };
-    });
+    return Promise.all(rejectionStacks);
   },
 
   /**
    * Helper function for fetching the source location of a SavedFrame stack.
    *
    * @param SavedFrame stack
    *        The promise allocation stack frame
    * @return object
@@ -787,38 +748,25 @@ ObjectActor.prototype = {
     }
 
     return this.hooks.sources().getOriginalLocation(new GeneratedLocation(
       source,
       stack.line,
       stack.column
     )).then((originalLocation) => {
       return {
-        source: originalLocation.originalSourceActor.form(),
+        source: originalLocation.originalSourceActor,
         line: originalLocation.originalLine,
         column: originalLocation.originalColumn,
         functionDisplayName: stack.functionDisplayName
       };
     });
-  }
+  },
+
+  /**
+   * Release the actor, when it isn't needed anymore.
+   * Protocol.js uses this release method to call the destroy method.
+   */
+  release: function() {}
 };
 
-ObjectActor.prototype.requestTypes = {
-  "definitionSite": ObjectActor.prototype.onDefinitionSite,
-  "parameterNames": ObjectActor.prototype.onParameterNames,
-  "prototypeAndProperties": ObjectActor.prototype.onPrototypeAndProperties,
-  "enumProperties": ObjectActor.prototype.onEnumProperties,
-  "prototype": ObjectActor.prototype.onPrototype,
-  "property": ObjectActor.prototype.onProperty,
-  "displayString": ObjectActor.prototype.onDisplayString,
-  "ownPropertyNames": ObjectActor.prototype.onOwnPropertyNames,
-  "decompile": ObjectActor.prototype.onDecompile,
-  "release": ObjectActor.prototype.onRelease,
-  "scope": ObjectActor.prototype.onScope,
-  "dependentPromises": ObjectActor.prototype.onDependentPromises,
-  "allocationStack": ObjectActor.prototype.onAllocationStack,
-  "fulfillmentStack": ObjectActor.prototype.onFulfillmentStack,
-  "rejectionStack": ObjectActor.prototype.onRejectionStack,
-  "enumEntries": ObjectActor.prototype.onEnumEntries,
-  "enumSymbols": ObjectActor.prototype.onEnumSymbols,
-};
-
-exports.ObjectActor = ObjectActor;
+exports.ObjectActor = protocol.ActorClassWithSpec(objectSpec, proto);
+exports.ObjectActorProto = proto;
--- a/devtools/server/actors/object/property-iterator.js
+++ b/devtools/server/actors/object/property-iterator.js
@@ -35,18 +35,18 @@ loader.lazyRequireGetter(this, "ObjectUt
  *          before dispatching them.
  *        - query String
  *          If non-empty, will filter the properties by names and values
  *          containing this query string. The match is not case-sensitive.
  *          Regarding value filtering it just compare to the stringification
  *          of the property value.
  */
 const PropertyIteratorActor  = protocol.ActorClassWithSpec(propertyIteratorSpec, {
-  initialize(objectActor, options) {
-    protocol.Actor.prototype.initialize.call(this);
+  initialize(objectActor, options, conn) {
+    protocol.Actor.prototype.initialize.call(this, conn);
     if (!DevToolsUtils.isSafeDebuggerObject(objectActor.obj)) {
       this.iterator = {
         size: 0,
         propertyName: index => undefined,
         propertyDescription: index => undefined,
       };
     } else if (options.enumEntries) {
       const cls = objectActor.obj.class;
--- a/devtools/server/actors/object/symbol-iterator.js
+++ b/devtools/server/actors/object/symbol-iterator.js
@@ -12,18 +12,18 @@ const DevToolsUtils = require("devtools/
 
 /**
  * Creates an actor to iterate over an object's symbols.
  *
  * @param objectActor ObjectActor
  *        The object actor.
  */
 const SymbolIteratorActor  = protocol.ActorClassWithSpec(symbolIteratorSpec, {
-  initialize(objectActor) {
-    protocol.Actor.prototype.initialize.call(this);
+  initialize(objectActor, conn) {
+    protocol.Actor.prototype.initialize.call(this, conn);
 
     let symbols = [];
     if (DevToolsUtils.isSafeDebuggerObject(objectActor.obj)) {
       try {
         symbols = objectActor.obj.getOwnPropertySymbols();
       } catch (err) {
         // The above can throw when the debuggee does not subsume the object's
         // compartment, or for some WrappedNatives like Cu.Sandbox.
--- a/devtools/server/actors/pause-scoped.js
+++ b/devtools/server/actors/pause-scoped.js
@@ -1,128 +1,101 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 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/. */
 
 "use strict";
 
-const { ObjectActor } = require("devtools/server/actors/object");
-
-/**
- * A base actor for any actors that should only respond receive messages in the
- * paused state. Subclasses may expose a `threadActor` which is used to help
- * determine when we are in a paused state. Subclasses should set their own
- * "constructor" property if they want better error messages. You should never
- * instantiate a PauseScopedActor directly, only through subclasses.
- */
-function PauseScopedActor() {
-}
+const { extend } = require("devtools/shared/extend");
+const { ObjectActorProto } = require("devtools/server/actors/object");
+const protocol = require("devtools/shared/protocol");
+const { ActorClassWithSpec } = protocol;
+const { objectSpec } = require("devtools/shared/specs/object");
 
 /**
- * A function decorator for creating methods to handle protocol messages that
- * should only be received while in the paused state.
- *
- * @param method Function
- *        The function we are decorating.
- */
-PauseScopedActor.withPaused = function(method) {
-  return function() {
-    if (this.isPaused()) {
-      return method.apply(this, arguments);
-    }
-    return this._wrongState();
-  };
-};
+ * Protocol.js expects only the prototype object, and does not maintain the prototype
+ * chain when it constructs the ActorClass. For this reason we are using extend to
+ * maintain the properties of ObjectActorProto.
+ **/
+const proto = extend({}, ObjectActorProto);
 
-PauseScopedActor.prototype = {
-
+Object.assign(proto, {
+  typeName: "pausedobj",
   /**
-   * Returns true if we are in the paused state.
+   * Creates a pause-scoped actor for the specified object.
+   * @see ObjectActor
    */
+  initialize: function(obj, hooks, conn) {
+    ObjectActorProto.initialize.call(this, obj, hooks, conn);
+    this.hooks.promote = hooks.promote;
+    this.hooks.isThreadLifetimePool = hooks.isThreadLifetimePool;
+  },
+
   isPaused: function() {
-    // When there is not a ThreadActor available (like in the webconsole) we
-    // have to be optimistic and assume that we are paused so that we can
-    // respond to requests.
-    return this.threadActor ? this.threadActor.state === "paused" : true;
+    return this.threadActor
+      ? this.threadActor.state === "paused"
+      : true;
   },
 
-  /**
-   * Returns the wrongState response packet for this actor.
-   */
-  _wrongState: function() {
-    return {
-      error: "wrongState",
-      message: this.constructor.name +
-        " actors can only be accessed while the thread is paused."
+  withPaused: function(method) {
+    return function() {
+      if (this.isPaused()) {
+        return method.apply(this, arguments);
+      }
+
+      return {
+        error: "wrongState",
+        message: this.constructor.name +
+          " actors can only be accessed while the thread is paused."
+      };
     };
   }
-};
-
-/**
- * Creates a pause-scoped actor for the specified object.
- * @see ObjectActor
- */
-function PauseScopedObjectActor(obj, hooks) {
-  ObjectActor.call(this, obj, hooks);
-  this.hooks.promote = hooks.promote;
-  this.hooks.isThreadLifetimePool = hooks.isThreadLifetimePool;
-}
-
-PauseScopedObjectActor.prototype = Object.create(PauseScopedActor.prototype);
-
-Object.assign(PauseScopedObjectActor.prototype, ObjectActor.prototype);
+});
 
-Object.assign(PauseScopedObjectActor.prototype, {
-  constructor: PauseScopedObjectActor,
-  actorPrefix: "pausedobj",
-
-  onOwnPropertyNames:
-    PauseScopedActor.withPaused(ObjectActor.prototype.onOwnPropertyNames),
-
-  onPrototypeAndProperties:
-    PauseScopedActor.withPaused(ObjectActor.prototype.onPrototypeAndProperties),
+const guardWithPaused = [
+  "decompile",
+  "displayString",
+  "ownPropertyNames",
+  "parameterNames",
+  "property",
+  "prototype",
+  "prototypeAndProperties",
+  "scope",
+];
 
-  onPrototype: PauseScopedActor.withPaused(ObjectActor.prototype.onPrototype),
-  onProperty: PauseScopedActor.withPaused(ObjectActor.prototype.onProperty),
-  onDecompile: PauseScopedActor.withPaused(ObjectActor.prototype.onDecompile),
+guardWithPaused.forEach(f => {
+  proto[f] = proto.withPaused(ObjectActorProto[f]);
+});
 
-  onDisplayString:
-    PauseScopedActor.withPaused(ObjectActor.prototype.onDisplayString),
-
-  onParameterNames:
-    PauseScopedActor.withPaused(ObjectActor.prototype.onParameterNames),
-
+Object.assign(proto, {
   /**
    * Handle a protocol request to promote a pause-lifetime grip to a
    * thread-lifetime grip.
    *
    * @param request object
    *        The protocol request object.
    */
-  onThreadGrip: PauseScopedActor.withPaused(function(request) {
+  threadGrip: proto.withPaused(function(request) {
     this.hooks.promote();
     return {};
   }),
 
   /**
    * Handle a protocol request to release a thread-lifetime grip.
    *
    * @param request object
    *        The protocol request object.
    */
-  onRelease: PauseScopedActor.withPaused(function(request) {
+  destroy: proto.withPaused(function(request) {
     if (this.hooks.isThreadLifetimePool()) {
       return { error: "notReleasable",
                message: "Only thread-lifetime actors can be released." };
     }
 
-    this.release();
-    return {};
+    return protocol.Actor.prototype.destroy.call(this);
   }),
 });
 
-Object.assign(PauseScopedObjectActor.prototype.requestTypes, {
-  "threadGrip": PauseScopedObjectActor.prototype.onThreadGrip,
-});
+exports.PauseScopedObjectActor = ActorClassWithSpec(objectSpec, proto);
+  // ActorClassWithSpec(objectSpec, {...ObjectActorProto, ...proto});
 
-exports.PauseScopedObjectActor = PauseScopedObjectActor;
--- a/devtools/server/actors/promises.js
+++ b/devtools/server/actors/promises.js
@@ -122,34 +122,34 @@ var PromisesActor = protocol.ActorClassW
       incrementGripDepth: () => this._gripDepth++,
       decrementGripDepth: () => this._gripDepth--,
       createValueGrip: v =>
         createValueGrip(v, this._navigationLifetimePool, this.objectGrip),
       sources: () => this.parentActor.sources,
       createEnvironmentActor: () => DevToolsUtils.reportException(
         "PromisesActor", Error("createEnvironmentActor not yet implemented")),
       getGlobalDebugObject: () => null,
-    });
+    }, this.conn);
 
     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();
+    return this._createObjectActorForPromise(value).form();
   },
 
   /**
    * Get a list of ObjectActors for all live Promise Objects.
    */
   listPromises: function() {
     const promises = this.dbg.findObjects({ class: "Promise" });
 
--- a/devtools/server/actors/thread.js
+++ b/devtools/server/actors/thread.js
@@ -987,17 +987,24 @@ const ThreadActor = ActorClassWithSpec(t
       const actor = this.threadLifetimePool.get(actorID);
       if (!actor) {
         if (!res) {
           res = { error: "notReleasable",
                   message: "Only thread-lifetime actors can be released." };
         }
         continue;
       }
-      actor.onRelease();
+
+      // We can still have old-style actors (e.g. object/long-string) in the pool, so we
+      // need to check onRelease existence.
+      if (actor.onRelease) {
+        actor.onRelease();
+      } else if (actor.destroy) {
+        actor.destroy();
+      }
     }
     return res ? res : {};
   },
 
   /**
    * Get the script and source lists from the debugger.
    */
   _discoverSources: function() {
@@ -1379,38 +1386,37 @@ const ThreadActor = ActorClassWithSpec(t
    *        The actor pool where the new object actor will be added.
    */
   objectGrip: function(value, pool) {
     if (!pool.objectActors) {
       pool.objectActors = new WeakMap();
     }
 
     if (pool.objectActors.has(value)) {
-      return pool.objectActors.get(value).grip();
-    } else if (this.threadLifetimePool.objectActors.has(value)) {
-      return this.threadLifetimePool.objectActors.get(value).grip();
+      return pool.objectActors.get(value).form();
+    }
+
+    if (this.threadLifetimePool.objectActors.has(value)) {
+      return this.threadLifetimePool.objectActors.get(value).form();
     }
 
     const actor = new PauseScopedObjectActor(value, {
       getGripDepth: () => this._gripDepth,
       incrementGripDepth: () => this._gripDepth++,
       decrementGripDepth: () => this._gripDepth--,
-      createValueGrip: v => createValueGrip(v, this._pausePool,
-        this.pauseObjectGrip),
+      createValueGrip: v => createValueGrip(v, this._pausePool, this.pauseObjectGrip),
       sources: () => this.sources,
-      createEnvironmentActor: (e, p) =>
-        this.createEnvironmentActor(e, p),
+      createEnvironmentActor: (e, p) => this.createEnvironmentActor(e, p),
       promote: () => this.threadObjectGrip(actor),
-      isThreadLifetimePool: () =>
-        actor.registeredPool !== this.threadLifetimePool,
+      isThreadLifetimePool: () => actor.registeredPool !== this.threadLifetimePool,
       getGlobalDebugObject: () => this.globalDebugObject
-    });
+    }, this.conn);
     pool.addActor(actor);
     pool.objectActors.set(value, actor);
-    return actor.grip();
+    return actor.form();
   },
 
   /**
    * Create a grip for the given debuggee object with a pause lifetime.
    *
    * @param value Debugger.Object
    *        The debuggee object value.
    */
@@ -1730,17 +1736,17 @@ const ThreadActor = ActorClassWithSpec(t
     for (const actorID of request.actors) {
       // This code assumes that there are no lazily loaded actors returned
       // by this call.
       const actor = this.conn.getActor(actorID);
       if (!actor) {
         return { from: this.actorID,
                  error: "noSuchActor" };
       }
-      const handler = actor.onPrototypeAndProperties;
+      const handler = actor.prototypeAndProperties;
       if (!handler) {
         return { from: this.actorID,
                  error: "unrecognizedPacketType",
                  message: ('Actor "' + actorID +
                            '" does not recognize the packet type ' +
                            '"prototypeAndProperties"') };
       }
       result[actorID] = handler.call(actor, {});
--- a/devtools/server/actors/webconsole.js
+++ b/devtools/server/actors/webconsole.js
@@ -476,19 +476,19 @@ WebConsoleActor.prototype =
       getGripDepth: () => this._gripDepth,
       incrementGripDepth: () => this._gripDepth++,
       decrementGripDepth: () => this._gripDepth--,
       createValueGrip: v => this.createValueGrip(v),
       sources: () => DevToolsUtils.reportException("WebConsoleActor",
         Error("sources not yet implemented")),
       createEnvironmentActor: (env) => this.createEnvironmentActor(env),
       getGlobalDebugObject: () => this.globalDebugObject
-    });
+    }, this.conn);
     pool.addActor(actor);
-    return actor.grip();
+    return actor.form();
   },
 
   /**
    * Create a grip for the given string.
    *
    * @param string string
    *        The string you want to create the grip for.
    * @param object pool
--- a/devtools/shared/specs/index.js
+++ b/devtools/shared/specs/index.js
@@ -143,16 +143,21 @@ const Types = exports.__TypesForTests = 
     front: null,
   },
   {
     types: ["domnode", "domnodelist"],
     spec: "devtools/shared/specs/node",
     front: "devtools/shared/fronts/node",
   },
   {
+    types: ["obj", "object.descriptor"],
+    spec: "devtools/shared/specs/object",
+    front: null,
+  },
+  {
     types: ["perf"],
     spec: "devtools/shared/specs/perf",
     front: "devtools/shared/fronts/perf",
   },
   {
     types: ["performance"],
     spec: "devtools/shared/specs/performance",
     front: "devtools/shared/fronts/performance",
--- a/devtools/shared/specs/moz.build
+++ b/devtools/shared/specs/moz.build
@@ -28,16 +28,17 @@ DevToolsModules(
     'heap-snapshot-file.js',
     'highlighters.js',
     'index.js',
     'inspector.js',
     'layout.js',
     'memory.js',
     'network-event.js',
     'node.js',
+    'object.js',
     'perf.js',
     'performance-recording.js',
     'performance.js',
     'preference.js',
     'promises.js',
     'property-iterator.js',
     'reflow.js',
     'script.js',
new file mode 100644
--- /dev/null
+++ b/devtools/shared/specs/object.js
@@ -0,0 +1,187 @@
+/* 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 {
+  generateActorSpec,
+  Arg,
+  RetVal,
+  types,
+} = require("devtools/shared/protocol");
+
+types.addDictType("object.descriptor", {
+  configurable: "boolean",
+  enumerable: "boolean",
+  // Can be null if there is a getter for the property.
+  value: "nullable:json",
+  // Only set `value` exists.
+  writable: "nullable:boolean",
+  // Only set when `value` does not exist and there is a getter for the property.
+  get: "nullable:json",
+  // Only set when `value` does not exist and there is a setter for the property.
+  set: "nullable:json",
+});
+
+types.addDictType("object.definitionSite", {
+  source: "source",
+  line: "number",
+  column: "number",
+});
+
+types.addDictType("object.prototypeproperties", {
+  prototype: "object.descriptor",
+  ownProperties: "nullable:json",
+  ownSymbols: "nullable:array:object.descriptor",
+  safeGetterValues: "nullable:json",
+});
+
+types.addDictType("object.prototype", {
+  prototype: "object.descriptor",
+});
+
+types.addDictType("object.property", {
+  descriptor: "nullable:object.descriptor"
+});
+
+types.addDictType("object.bindings", {
+  arguments: "array:json",
+  variables: "json",
+});
+
+types.addDictType("object.scope", {
+  scope: "environment"
+});
+
+types.addDictType("object.enumProperties.Options", {
+  enumEntries: "nullable:boolean",
+  ignoreNonIndexedProperties: "nullable:boolean",
+  ignoreIndexedProperties: "nullable:boolean",
+  query: "nullable:string",
+  sort: "nullable:boolean",
+});
+
+types.addDictType("object.ownPropertyNames", {
+  ownPropertyNames: "array:string"
+});
+
+types.addDictType("object.displayString", {
+  displayString: "string"
+});
+
+types.addDictType("object.decompile", {
+  decompiledCode: "string"
+});
+
+types.addDictType("object.parameterNames", {
+  parameterNames: "nullable:array:string"
+});
+
+types.addDictType("object.dependentPromises", {
+  promises: "array:object.descriptor"
+});
+
+types.addDictType("object.originalSourceLocation", {
+  source: "source",
+  line: "number",
+  column: "number",
+  functionDisplayName: "string"
+});
+
+const objectSpec = generateActorSpec({
+  typeName: "obj",
+
+  methods: {
+    allocationStack: {
+      request: {},
+      response: {
+        allocationStack: RetVal("array:object.originalSourceLocation")
+      },
+    },
+    decompile: {
+      request: {
+        pretty: Arg(0, "boolean")
+      },
+      response: RetVal("object.decompile"),
+    },
+    definitionSite: {
+      request: {},
+      response: RetVal("object.definitionSite"),
+    },
+    dependentPromises: {
+      request: {},
+      response: RetVal("object.dependentPromises")
+    },
+    displayString: {
+      request: {},
+      response: RetVal("object.displayString")
+    },
+    enumEntries: {
+      request: {},
+      response: {
+        iterator: RetVal("propertyIterator")
+      }
+    },
+    enumProperties: {
+      request: {
+        options: Arg(0, "nullable:object.enumProperties.Options"),
+      },
+      response: {
+        iterator: RetVal("propertyIterator")
+      }
+    },
+    enumSymbols: {
+      request: {},
+      response: {
+        iterator: RetVal("symbolIterator")
+      }
+    },
+    fulfillmentStack: {
+      request: {},
+      response: {
+        fulfillmentStack: RetVal("array:object.originalSourceLocation")
+      },
+    },
+    ownPropertyNames: {
+      request: {},
+      response: RetVal("object.ownPropertyNames")
+    },
+    parameterNames: {
+      request: {},
+      response: RetVal("object.parameterNames")
+    },
+    prototypeAndProperties: {
+      request: {},
+      response: RetVal("object.prototypeproperties")
+    },
+    prototype: {
+      request: {},
+      response: RetVal("object.prototype")
+    },
+    property: {
+      request: {
+        name: Arg(0, "string")
+      },
+      response: RetVal("object.property")
+    },
+    rejectionStack: {
+      request: {},
+      response: {
+        rejectionStack: RetVal("array:object.originalSourceLocation")
+      },
+    },
+    release: { release: true },
+    scope: {
+      request: {},
+      response: RetVal("object.scope"),
+    },
+    // Needed for the PauseScopedObjectActor which extends the ObjectActor.
+    threadGrip: {
+      request: {},
+      response: {},
+    }
+  }
+});
+
+exports.objectSpec = objectSpec;
--- a/devtools/shared/specs/promises.js
+++ b/devtools/shared/specs/promises.js
@@ -7,17 +7,17 @@ const {
   Arg,
   RetVal,
   generateActorSpec,
   types
 } = require("devtools/shared/protocol");
 
 // Teach protocol.js how to deal with legacy actor types
 types.addType("ObjectActor", {
-  write: actor => actor.grip(),
+  write: actor => actor.form(),
   read: grip => grip
 });
 
 const promisesSpec = generateActorSpec({
   typeName: "promises",
 
   events: {
     // Event emitted for new promises allocated in debuggee and bufferred by
--- a/devtools/shared/specs/symbol-iterator.js
+++ b/devtools/shared/specs/symbol-iterator.js
@@ -15,30 +15,16 @@ types.addDictType("symboliterator.data",
   ownSymbols: "array:symboliterator.ownsymbols",
 });
 
 types.addDictType("symboliterator.ownsymbols", {
   name: "string",
   descriptor: "nullable:object.descriptor",
 });
 
-// XXX Move to the object spec in Bug 1450944.
-types.addDictType("object.descriptor", {
-  configurable: "boolean",
-  enumerable: "boolean",
-  // Can be null if there is a getter for the property.
-  value: "nullable:json",
-  // Only set `value` exists.
-  writable: "nullable:boolean",
-  // Only set when `value` does not exist and there is a getter for the property.
-  get: "nullable:json",
-  // Only set when `value` does not exist and there is a setter for the property.
-  set: "nullable:json",
-});
-
 const symbolIteratorSpec = generateActorSpec({
   typeName: "symbolIterator",
 
   methods: {
     slice: {
       request: {
         start: Option(0, "number"),
         count: Option(0, "number"),