summaryrefslogtreecommitdiffstats
path: root/devtools/shared/protocol/types.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/shared/protocol/types.js')
-rw-r--r--devtools/shared/protocol/types.js587
1 files changed, 587 insertions, 0 deletions
diff --git a/devtools/shared/protocol/types.js b/devtools/shared/protocol/types.js
new file mode 100644
index 0000000000..41764fbd79
--- /dev/null
+++ b/devtools/shared/protocol/types.js
@@ -0,0 +1,587 @@
+/* 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 { Actor } = require("resource://devtools/shared/protocol/Actor.js");
+var {
+ lazyLoadSpec,
+ lazyLoadFront,
+} = require("resource://devtools/shared/specs/index.js");
+
+/**
+ * Types: named marshallers/demarshallers.
+ *
+ * Types provide a 'write' function that takes a js representation and
+ * returns a protocol representation, and a "read" function that
+ * takes a protocol representation and returns a js representation.
+ *
+ * The read and write methods are also passed a context object that
+ * represent the actor or front requesting the translation.
+ *
+ * Types are referred to with a typestring. Basic types are
+ * registered by name using addType, and more complex types can
+ * be generated by adding detail to the type name.
+ */
+
+var types = Object.create(null);
+exports.types = types;
+
+var registeredTypes = (types.registeredTypes = new Map());
+
+exports.registeredTypes = registeredTypes;
+
+/**
+ * Return the type object associated with a given typestring.
+ * If passed a type object, it will be returned unchanged.
+ *
+ * Types can be registered with addType, or can be created on
+ * the fly with typestrings. Examples:
+ *
+ * boolean
+ * threadActor
+ * threadActor#detail
+ * array:threadActor
+ * array:array:threadActor#detail
+ *
+ * @param [typestring|type] type
+ * Either a typestring naming a type or a type object.
+ *
+ * @returns a type object.
+ */
+types.getType = function (type) {
+ if (!type) {
+ return types.Primitive;
+ }
+
+ if (typeof type !== "string") {
+ return type;
+ }
+
+ // If already registered, we're done here.
+ let reg = registeredTypes.get(type);
+ if (reg) {
+ return reg;
+ }
+
+ // Try to lazy load the spec, if not already loaded.
+ if (lazyLoadSpec(type)) {
+ // If a spec module was lazy loaded, it will synchronously call
+ // generateActorSpec, and set the type in `registeredTypes`.
+ reg = registeredTypes.get(type);
+ if (reg) {
+ return reg;
+ }
+ }
+
+ // New type, see if it's a collection type:
+ const sep = type.indexOf(":");
+ if (sep >= 0) {
+ const collection = type.substring(0, sep);
+ const subtype = types.getType(type.substring(sep + 1));
+
+ if (collection === "array") {
+ return types.addArrayType(subtype);
+ } else if (collection === "nullable") {
+ return types.addNullableType(subtype);
+ }
+
+ throw Error("Unknown collection type: " + collection);
+ }
+
+ // Not a collection, might be actor detail
+ const pieces = type.split("#", 2);
+ if (pieces.length > 1) {
+ if (pieces[1] != "actorid") {
+ throw new Error(
+ "Unsupported detail, only support 'actorid', got: " + pieces[1]
+ );
+ }
+ return types.addActorDetail(type, pieces[0], pieces[1]);
+ }
+
+ throw Error("Unknown type: " + type);
+};
+
+/**
+ * Don't allow undefined when writing primitive types to packets. If
+ * you want to allow undefined, use a nullable type.
+ */
+function identityWrite(v) {
+ if (v === undefined) {
+ throw Error("undefined passed where a value is required");
+ }
+ // This has to handle iterator->array conversion because arrays of
+ // primitive types pass through here.
+ if (v && typeof v.next === "function") {
+ return [...v];
+ }
+ return v;
+}
+
+/**
+ * Add a type to the type system.
+ *
+ * When registering a type, you can provide `read` and `write` methods.
+ *
+ * The `read` method will be passed a JS object value from the JSON
+ * packet and must return a native representation. The `write` method will
+ * be passed a native representation and should provide a JSONable value.
+ *
+ * These methods will both be passed a context. The context is the object
+ * performing or servicing the request - on the server side it will be
+ * an Actor, on the client side it will be a Front.
+ *
+ * @param typestring name
+ * Name to register
+ * @param object typeObject
+ * An object whose properties will be stored in the type, including
+ * the `read` and `write` methods.
+ * @param object options
+ * Can specify `thawed` to prevent the type from being frozen.
+ *
+ * @returns a type object that can be used in protocol definitions.
+ */
+types.addType = function (name, typeObject = {}, options = {}) {
+ if (registeredTypes.has(name)) {
+ throw Error("Type '" + name + "' already exists.");
+ }
+
+ const type = Object.assign(
+ {
+ toString() {
+ return "[protocol type:" + name + "]";
+ },
+ name,
+ primitive: !(typeObject.read || typeObject.write),
+ read: identityWrite,
+ write: identityWrite,
+ },
+ typeObject
+ );
+
+ registeredTypes.set(name, type);
+
+ return type;
+};
+
+/**
+ * Remove a type previously registered with the system.
+ * Primarily useful for types registered by addons.
+ */
+types.removeType = function (name) {
+ // This type may still be referenced by other types, make sure
+ // those references don't work.
+ const type = registeredTypes.get(name);
+
+ type.name = "DEFUNCT:" + name;
+ type.category = "defunct";
+ type.primitive = false;
+ type.read = type.write = function () {
+ throw new Error("Using defunct type: " + name);
+ };
+
+ registeredTypes.delete(name);
+};
+
+/**
+ * Add an array type to the type system.
+ *
+ * getType() will call this function if provided an "array:<type>"
+ * typestring.
+ *
+ * @param type subtype
+ * The subtype to be held by the array.
+ */
+types.addArrayType = function (subtype) {
+ subtype = types.getType(subtype);
+
+ const name = "array:" + subtype.name;
+
+ // Arrays of primitive types are primitive types themselves.
+ if (subtype.primitive) {
+ return types.addType(name);
+ }
+ return types.addType(name, {
+ category: "array",
+ read: (v, ctx) => {
+ if (v && typeof v.next === "function") {
+ v = [...v];
+ }
+ return v.map(i => subtype.read(i, ctx));
+ },
+ write: (v, ctx) => {
+ if (v && typeof v.next === "function") {
+ v = [...v];
+ }
+ return v.map(i => subtype.write(i, ctx));
+ },
+ });
+};
+
+/**
+ * Add a dict type to the type system. This allows you to serialize
+ * a JS object that contains non-primitive subtypes.
+ *
+ * Properties of the value that aren't included in the specializations
+ * will be serialized as primitive values.
+ *
+ * @param object specializations
+ * A dict of property names => type
+ */
+types.addDictType = function (name, specializations) {
+ const specTypes = {};
+ for (const prop in specializations) {
+ try {
+ specTypes[prop] = types.getType(specializations[prop]);
+ } catch (e) {
+ // Types may not be defined yet. Sometimes, we define the type *after* using it, but
+ // also, we have cyclic definitions on types. So lazily load them when they are not
+ // immediately available.
+ loader.lazyGetter(specTypes, prop, () => {
+ return types.getType(specializations[prop]);
+ });
+ }
+ }
+ return types.addType(name, {
+ category: "dict",
+ specializations,
+ read: (v, ctx) => {
+ const ret = {};
+ for (const prop in v) {
+ if (prop in specTypes) {
+ ret[prop] = specTypes[prop].read(v[prop], ctx);
+ } else {
+ ret[prop] = v[prop];
+ }
+ }
+ return ret;
+ },
+
+ write: (v, ctx) => {
+ const ret = {};
+ for (const prop in v) {
+ if (prop in specTypes) {
+ ret[prop] = specTypes[prop].write(v[prop], ctx);
+ } else {
+ ret[prop] = v[prop];
+ }
+ }
+ return ret;
+ },
+ });
+};
+
+/**
+ * Register an actor type with the type system.
+ *
+ * Types are marshalled differently when communicating server->client
+ * than they are when communicating client->server. The server needs
+ * to provide useful information to the client, so uses the actor's
+ * `form` method to get a json representation of the actor. When
+ * making a request from the client we only need the actor ID string.
+ *
+ * This function can be called before the associated actor has been
+ * constructed, but the read and write methods won't work until
+ * the associated addActorImpl or addActorFront methods have been
+ * called during actor/front construction.
+ *
+ * @param string name
+ * The typestring to register.
+ */
+types.addActorType = function (name) {
+ // We call addActorType from:
+ // FrontClassWithSpec when registering front synchronously,
+ // generateActorSpec when defining specs,
+ // specs modules to register actor type early to use them in other types
+ if (registeredTypes.has(name)) {
+ return registeredTypes.get(name);
+ }
+ const type = types.addType(name, {
+ _actor: true,
+ category: "actor",
+ read: (v, ctx, detail) => {
+ // If we're reading a request on the server side, just
+ // find the actor registered with this actorID.
+ if (ctx instanceof Actor) {
+ return ctx.conn.getActor(v);
+ }
+
+ // Reading a response on the client side, check for an
+ // existing front on the connection, and create the front
+ // if it isn't found.
+ const actorID = typeof v === "string" ? v : v.actor;
+ // `ctx.conn` is a DevToolsClient
+ let front = ctx.conn.getFrontByID(actorID);
+
+ // When the type `${name}#actorid` is used, `v` is a string refering to the
+ // actor ID. We cannot read form information in this case and the actorID was
+ // already set when creating the front, so no need to do anything.
+ let form = null;
+ if (detail != "actorid") {
+ form = identityWrite(v);
+ }
+
+ if (!front) {
+ // If front isn't instantiated yet, create one.
+ // Try lazy loading front if not already loaded.
+ // The front module will synchronously call `FrontClassWithSpec` and
+ // augment `type` with the `frontClass` attribute.
+ if (!type.frontClass) {
+ lazyLoadFront(name);
+ }
+
+ const parentFront = ctx.marshallPool();
+ const targetFront = parentFront.isTargetFront
+ ? parentFront
+ : parentFront.targetFront;
+
+ // Use intermediate Class variable to please eslint requiring
+ // a capital letter for all constructors.
+ const Class = type.frontClass;
+ front = new Class(ctx.conn, targetFront, parentFront);
+ front.actorID = actorID;
+
+ parentFront.manage(front, form, ctx);
+ } else if (form) {
+ front.form(form, ctx);
+ }
+
+ return front;
+ },
+ write: (v, ctx, detail) => {
+ // If returning a response from the server side, make sure
+ // the actor is added to a parent object and return its form.
+ if (v instanceof Actor) {
+ if (v.isDestroyed()) {
+ throw new Error(
+ `Attempted to write a response containing a destroyed actor`
+ );
+ }
+ if (!v.actorID) {
+ ctx.marshallPool().manage(v);
+ }
+ if (detail == "actorid") {
+ return v.actorID;
+ }
+ return identityWrite(v.form(detail));
+ }
+
+ // Writing a request from the client side, just send the actor id.
+ return v.actorID;
+ },
+ });
+ return type;
+};
+
+types.addPolymorphicType = function (name, subtypes) {
+ // Assert that all subtypes are actors, as the marshalling implementation depends on that.
+ for (const subTypeName of subtypes) {
+ const subtype = types.getType(subTypeName);
+ if (subtype.category != "actor") {
+ throw new Error(
+ `In polymorphic type '${subtypes.join(
+ ","
+ )}', the type '${subTypeName}' isn't an actor`
+ );
+ }
+ }
+
+ return types.addType(name, {
+ category: "polymorphic",
+ read: (value, ctx) => {
+ // `value` is either a string which is an Actor ID or a form object
+ // where `actor` is an actor ID
+ const actorID = typeof value === "string" ? value : value.actor;
+ if (!actorID) {
+ throw new Error(
+ `Was expecting one of these actors '${subtypes}' but instead got value: '${value}'`
+ );
+ }
+
+ // Extract the typeName out of the actor ID, which should be composed like this
+ // ${DevToolsServerConnectionPrefix}.${typeName}${Number}
+ const typeName = actorID.match(/\.([a-zA-Z]+)\d+$/)[1];
+ if (!subtypes.includes(typeName)) {
+ throw new Error(
+ `Was expecting one of these actors '${subtypes}' but instead got an actor of type: '${typeName}'`
+ );
+ }
+
+ const subtype = types.getType(typeName);
+ return subtype.read(value, ctx);
+ },
+ write: (value, ctx) => {
+ if (!value) {
+ throw new Error(
+ `Was expecting one of these actors '${subtypes}' but instead got an empty value.`
+ );
+ }
+ // value is either an `Actor` or a `Front` and both classes exposes a `typeName`
+ const typeName = value.typeName;
+ if (!typeName) {
+ throw new Error(
+ `Was expecting one of these actors '${subtypes}' but instead got value: '${value}'. Did you pass a form instead of an Actor?`
+ );
+ }
+
+ if (!subtypes.includes(typeName)) {
+ throw new Error(
+ `Was expecting one of these actors '${subtypes}' but instead got an actor of type: '${typeName}'`
+ );
+ }
+
+ const subtype = types.getType(typeName);
+ return subtype.write(value, ctx);
+ },
+ });
+};
+types.addNullableType = function (subtype) {
+ subtype = types.getType(subtype);
+ return types.addType("nullable:" + subtype.name, {
+ category: "nullable",
+ read: (value, ctx) => {
+ if (value == null) {
+ return value;
+ }
+ return subtype.read(value, ctx);
+ },
+ write: (value, ctx) => {
+ if (value == null) {
+ return value;
+ }
+ return subtype.write(value, ctx);
+ },
+ });
+};
+
+/**
+ * Register an actor detail type. This is just like an actor type, but
+ * will pass a detail hint to the actor's form method during serialization/
+ * deserialization.
+ *
+ * This is called by getType() when passed an 'actorType#detail' string.
+ *
+ * @param string name
+ * The typestring to register this type as.
+ * @param type actorType
+ * The actor type you'll be detailing.
+ * @param string detail
+ * The detail to pass.
+ */
+types.addActorDetail = function (name, actorType, detail) {
+ actorType = types.getType(actorType);
+ if (!actorType._actor) {
+ throw Error(
+ `Details only apply to actor types, tried to add detail '${detail}' ` +
+ `to ${actorType.name}`
+ );
+ }
+ return types.addType(name, {
+ _actor: true,
+ category: "detail",
+ read: (v, ctx) => actorType.read(v, ctx, detail),
+ write: (v, ctx) => actorType.write(v, ctx, detail),
+ });
+};
+
+// Add a few named primitive types.
+types.Primitive = types.addType("primitive");
+types.String = types.addType("string");
+types.Number = types.addType("number");
+types.Boolean = types.addType("boolean");
+types.JSON = types.addType("json");
+
+exports.registerFront = function (cls) {
+ const { typeName } = cls.prototype;
+ if (!registeredTypes.has(typeName)) {
+ types.addActorType(typeName);
+ }
+ registeredTypes.get(typeName).frontClass = cls;
+};
+
+/**
+ * Instantiate a front of the given type.
+ *
+ * @param DevToolsClient client
+ * The DevToolsClient instance to use.
+ * @param string typeName
+ * The type name of the front to instantiate. This is defined in its specifiation.
+ * @returns Front
+ * The created front.
+ */
+function createFront(client, typeName, target = null) {
+ const type = types.getType(typeName);
+ if (!type) {
+ throw new Error(`No spec for front type '${typeName}'.`);
+ } else if (!type.frontClass) {
+ lazyLoadFront(typeName);
+ }
+
+ // Use intermediate Class variable to please eslint requiring
+ // a capital letter for all constructors.
+ const Class = type.frontClass;
+ return new Class(client, target, target);
+}
+
+/**
+ * Instantiate a global (preference, device) or target-scoped (webconsole, inspector)
+ * front of the given type by picking its actor ID out of either the target or root
+ * front's form.
+ *
+ * @param DevToolsClient client
+ * The DevToolsClient instance to use.
+ * @param string typeName
+ * The type name of the front to instantiate. This is defined in its specifiation.
+ * @param json form
+ * If we want to instantiate a global actor's front, this is the root front's form,
+ * otherwise we are instantiating a target-scoped front from the target front's form.
+ * @param [Target|null] target
+ * If we are instantiating a target-scoped front, this is a reference to the front's
+ * Target instance, otherwise this is null.
+ */
+async function getFront(client, typeName, form, target = null) {
+ const front = createFront(client, typeName, target);
+ const { formAttributeName } = front;
+ if (!formAttributeName) {
+ throw new Error(`Can't find the form attribute name for ${typeName}`);
+ }
+ // Retrieve the actor ID from root or target actor's form
+ front.actorID = form[formAttributeName];
+ if (!front.actorID) {
+ throw new Error(
+ `Can't find the actor ID for ${typeName} from root or target` +
+ ` actor's form.`
+ );
+ }
+
+ if (!target) {
+ await front.manage(front);
+ } else {
+ await target.manage(front);
+ }
+
+ return front;
+}
+exports.getFront = getFront;
+
+/**
+ * Create a RootFront.
+ *
+ * @param DevToolsClient client
+ * The DevToolsClient instance to use.
+ * @param Object packet
+ * @returns RootFront
+ */
+function createRootFront(client, packet) {
+ const rootFront = createFront(client, "root");
+ rootFront.form(packet);
+
+ // Root Front is a special case, managing itself as it doesn't have any parent.
+ // It will register itself to DevToolsClient as a Pool via Front._poolMap.
+ rootFront.manage(rootFront);
+
+ return rootFront;
+}
+exports.createRootFront = createRootFront;