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
* 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.
*
* @returns a type object that can be used in protocol definitions.
*/
types.addType = function (name, typeObject = {}) {
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;