/* 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:" * 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;