1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 var { Actor } = require("resource://devtools/shared/protocol/Actor.js");
11 } = require("resource://devtools/shared/specs/index.js");
14 * Types: named marshallers/demarshallers.
16 * Types provide a 'write' function that takes a js representation and
17 * returns a protocol representation, and a "read" function that
18 * takes a protocol representation and returns a js representation.
20 * The read and write methods are also passed a context object that
21 * represent the actor or front requesting the translation.
23 * Types are referred to with a typestring. Basic types are
24 * registered by name using addType, and more complex types can
25 * be generated by adding detail to the type name.
28 var types = Object.create(null);
29 exports.types = types;
31 var registeredTypes = (types.registeredTypes = new Map());
33 exports.registeredTypes = registeredTypes;
36 * Return the type object associated with a given typestring.
37 * If passed a type object, it will be returned unchanged.
39 * Types can be registered with addType, or can be created on
40 * the fly with typestrings. Examples:
46 * array:array:threadActor#detail
48 * @param [typestring|type] type
49 * Either a typestring naming a type or a type object.
51 * @returns a type object.
53 types.getType = function (type) {
55 return types.Primitive;
58 if (typeof type !== "string") {
62 // If already registered, we're done here.
63 let reg = registeredTypes.get(type);
68 // Try to lazy load the spec, if not already loaded.
69 if (lazyLoadSpec(type)) {
70 // If a spec module was lazy loaded, it will synchronously call
71 // generateActorSpec, and set the type in `registeredTypes`.
72 reg = registeredTypes.get(type);
78 // New type, see if it's a collection type:
79 const sep = type.indexOf(":");
81 const collection = type.substring(0, sep);
82 const subtype = types.getType(type.substring(sep + 1));
84 if (collection === "array") {
85 return types.addArrayType(subtype);
86 } else if (collection === "nullable") {
87 return types.addNullableType(subtype);
90 throw Error("Unknown collection type: " + collection);
93 // Not a collection, might be actor detail
94 const pieces = type.split("#", 2);
95 if (pieces.length > 1) {
96 if (pieces[1] != "actorid") {
98 "Unsupported detail, only support 'actorid', got: " + pieces[1]
101 return types.addActorDetail(type, pieces[0], pieces[1]);
104 throw Error("Unknown type: " + type);
108 * Don't allow undefined when writing primitive types to packets. If
109 * you want to allow undefined, use a nullable type.
111 function identityWrite(v) {
112 if (v === undefined) {
113 throw Error("undefined passed where a value is required");
115 // This has to handle iterator->array conversion because arrays of
116 // primitive types pass through here.
117 if (v && typeof v.next === "function") {
124 * Add a type to the type system.
126 * When registering a type, you can provide `read` and `write` methods.
128 * The `read` method will be passed a JS object value from the JSON
129 * packet and must return a native representation. The `write` method will
130 * be passed a native representation and should provide a JSONable value.
132 * These methods will both be passed a context. The context is the object
133 * performing or servicing the request - on the server side it will be
134 * an Actor, on the client side it will be a Front.
136 * @param typestring name
138 * @param object typeObject
139 * An object whose properties will be stored in the type, including
140 * the `read` and `write` methods.
142 * @returns a type object that can be used in protocol definitions.
144 types.addType = function (name, typeObject = {}) {
145 if (registeredTypes.has(name)) {
146 throw Error("Type '" + name + "' already exists.");
149 const type = Object.assign(
152 return "[protocol type:" + name + "]";
155 primitive: !(typeObject.read || typeObject.write),
157 write: identityWrite,
162 registeredTypes.set(name, type);
168 * Remove a type previously registered with the system.
169 * Primarily useful for types registered by addons.
171 types.removeType = function (name) {
172 // This type may still be referenced by other types, make sure
173 // those references don't work.
174 const type = registeredTypes.get(name);
176 type.name = "DEFUNCT:" + name;
177 type.category = "defunct";
178 type.primitive = false;
179 type.read = type.write = function () {
180 throw new Error("Using defunct type: " + name);
183 registeredTypes.delete(name);
187 * Add an array type to the type system.
189 * getType() will call this function if provided an "array:<type>"
192 * @param type subtype
193 * The subtype to be held by the array.
195 types.addArrayType = function (subtype) {
196 subtype = types.getType(subtype);
198 const name = "array:" + subtype.name;
200 // Arrays of primitive types are primitive types themselves.
201 if (subtype.primitive) {
202 return types.addType(name);
204 return types.addType(name, {
207 if (v && typeof v.next === "function") {
210 return v.map(i => subtype.read(i, ctx));
213 if (v && typeof v.next === "function") {
216 return v.map(i => subtype.write(i, ctx));
222 * Add a dict type to the type system. This allows you to serialize
223 * a JS object that contains non-primitive subtypes.
225 * Properties of the value that aren't included in the specializations
226 * will be serialized as primitive values.
228 * @param object specializations
229 * A dict of property names => type
231 types.addDictType = function (name, specializations) {
232 const specTypes = {};
233 for (const prop in specializations) {
235 specTypes[prop] = types.getType(specializations[prop]);
237 // Types may not be defined yet. Sometimes, we define the type *after* using it, but
238 // also, we have cyclic definitions on types. So lazily load them when they are not
239 // immediately available.
240 loader.lazyGetter(specTypes, prop, () => {
241 return types.getType(specializations[prop]);
245 return types.addType(name, {
250 for (const prop in v) {
251 if (prop in specTypes) {
252 ret[prop] = specTypes[prop].read(v[prop], ctx);
262 for (const prop in v) {
263 if (prop in specTypes) {
264 ret[prop] = specTypes[prop].write(v[prop], ctx);
275 * Register an actor type with the type system.
277 * Types are marshalled differently when communicating server->client
278 * than they are when communicating client->server. The server needs
279 * to provide useful information to the client, so uses the actor's
280 * `form` method to get a json representation of the actor. When
281 * making a request from the client we only need the actor ID string.
283 * This function can be called before the associated actor has been
284 * constructed, but the read and write methods won't work until
285 * the associated addActorImpl or addActorFront methods have been
286 * called during actor/front construction.
289 * The typestring to register.
291 types.addActorType = function (name) {
292 // We call addActorType from:
293 // FrontClassWithSpec when registering front synchronously,
294 // generateActorSpec when defining specs,
295 // specs modules to register actor type early to use them in other types
296 if (registeredTypes.has(name)) {
297 return registeredTypes.get(name);
299 const type = types.addType(name, {
302 read: (v, ctx, detail) => {
303 // If we're reading a request on the server side, just
304 // find the actor registered with this actorID.
305 if (ctx instanceof Actor) {
306 return ctx.conn.getActor(v);
309 // Reading a response on the client side, check for an
310 // existing front on the connection, and create the front
311 // if it isn't found.
312 const actorID = typeof v === "string" ? v : v.actor;
313 // `ctx.conn` is a DevToolsClient
314 let front = ctx.conn.getFrontByID(actorID);
316 // When the type `${name}#actorid` is used, `v` is a string refering to the
317 // actor ID. We cannot read form information in this case and the actorID was
318 // already set when creating the front, so no need to do anything.
320 if (detail != "actorid") {
321 form = identityWrite(v);
325 // If front isn't instantiated yet, create one.
326 // Try lazy loading front if not already loaded.
327 // The front module will synchronously call `FrontClassWithSpec` and
328 // augment `type` with the `frontClass` attribute.
329 if (!type.frontClass) {
333 const parentFront = ctx.marshallPool();
334 const targetFront = parentFront.isTargetFront
336 : parentFront.targetFront;
338 // Use intermediate Class variable to please eslint requiring
339 // a capital letter for all constructors.
340 const Class = type.frontClass;
341 front = new Class(ctx.conn, targetFront, parentFront);
342 front.actorID = actorID;
344 parentFront.manage(front, form, ctx);
346 front.form(form, ctx);
351 write: (v, ctx, detail) => {
352 // If returning a response from the server side, make sure
353 // the actor is added to a parent object and return its form.
354 if (v instanceof Actor) {
355 if (v.isDestroyed()) {
357 `Attempted to write a response containing a destroyed actor`
361 ctx.marshallPool().manage(v);
363 if (detail == "actorid") {
366 return identityWrite(v.form(detail));
369 // Writing a request from the client side, just send the actor id.
376 types.addPolymorphicType = function (name, subtypes) {
377 // Assert that all subtypes are actors, as the marshalling implementation depends on that.
378 for (const subTypeName of subtypes) {
379 const subtype = types.getType(subTypeName);
380 if (subtype.category != "actor") {
382 `In polymorphic type '${subtypes.join(
384 )}', the type '${subTypeName}' isn't an actor`
389 return types.addType(name, {
390 category: "polymorphic",
391 read: (value, ctx) => {
392 // `value` is either a string which is an Actor ID or a form object
393 // where `actor` is an actor ID
394 const actorID = typeof value === "string" ? value : value.actor;
397 `Was expecting one of these actors '${subtypes}' but instead got value: '${value}'`
401 // Extract the typeName out of the actor ID, which should be composed like this
402 // ${DevToolsServerConnectionPrefix}.${typeName}${Number}
403 const typeName = actorID.match(/\.([a-zA-Z]+)\d+$/)[1];
404 if (!subtypes.includes(typeName)) {
406 `Was expecting one of these actors '${subtypes}' but instead got an actor of type: '${typeName}'`
410 const subtype = types.getType(typeName);
411 return subtype.read(value, ctx);
413 write: (value, ctx) => {
416 `Was expecting one of these actors '${subtypes}' but instead got an empty value.`
419 // value is either an `Actor` or a `Front` and both classes exposes a `typeName`
420 const typeName = value.typeName;
423 `Was expecting one of these actors '${subtypes}' but instead got value: '${value}'. Did you pass a form instead of an Actor?`
427 if (!subtypes.includes(typeName)) {
429 `Was expecting one of these actors '${subtypes}' but instead got an actor of type: '${typeName}'`
433 const subtype = types.getType(typeName);
434 return subtype.write(value, ctx);
438 types.addNullableType = function (subtype) {
439 subtype = types.getType(subtype);
440 return types.addType("nullable:" + subtype.name, {
441 category: "nullable",
442 read: (value, ctx) => {
446 return subtype.read(value, ctx);
448 write: (value, ctx) => {
452 return subtype.write(value, ctx);
458 * Register an actor detail type. This is just like an actor type, but
459 * will pass a detail hint to the actor's form method during serialization/
462 * This is called by getType() when passed an 'actorType#detail' string.
465 * The typestring to register this type as.
466 * @param type actorType
467 * The actor type you'll be detailing.
468 * @param string detail
469 * The detail to pass.
471 types.addActorDetail = function (name, actorType, detail) {
472 actorType = types.getType(actorType);
473 if (!actorType._actor) {
475 `Details only apply to actor types, tried to add detail '${detail}' ` +
476 `to ${actorType.name}`
479 return types.addType(name, {
482 read: (v, ctx) => actorType.read(v, ctx, detail),
483 write: (v, ctx) => actorType.write(v, ctx, detail),
487 // Add a few named primitive types.
488 types.Primitive = types.addType("primitive");
489 types.String = types.addType("string");
490 types.Number = types.addType("number");
491 types.Boolean = types.addType("boolean");
492 types.JSON = types.addType("json");
494 exports.registerFront = function (cls) {
495 const { typeName } = cls.prototype;
496 if (!registeredTypes.has(typeName)) {
497 types.addActorType(typeName);
499 registeredTypes.get(typeName).frontClass = cls;
503 * Instantiate a front of the given type.
505 * @param DevToolsClient client
506 * The DevToolsClient instance to use.
507 * @param string typeName
508 * The type name of the front to instantiate. This is defined in its specifiation.
512 function createFront(client, typeName, target = null) {
513 const type = types.getType(typeName);
515 throw new Error(`No spec for front type '${typeName}'.`);
516 } else if (!type.frontClass) {
517 lazyLoadFront(typeName);
520 // Use intermediate Class variable to please eslint requiring
521 // a capital letter for all constructors.
522 const Class = type.frontClass;
523 return new Class(client, target, target);
527 * Instantiate a global (preference, device) or target-scoped (webconsole, inspector)
528 * front of the given type by picking its actor ID out of either the target or root
531 * @param DevToolsClient client
532 * The DevToolsClient instance to use.
533 * @param string typeName
534 * The type name of the front to instantiate. This is defined in its specifiation.
536 * If we want to instantiate a global actor's front, this is the root front's form,
537 * otherwise we are instantiating a target-scoped front from the target front's form.
538 * @param [Target|null] target
539 * If we are instantiating a target-scoped front, this is a reference to the front's
540 * Target instance, otherwise this is null.
542 async function getFront(client, typeName, form, target = null) {
543 const front = createFront(client, typeName, target);
544 const { formAttributeName } = front;
545 if (!formAttributeName) {
546 throw new Error(`Can't find the form attribute name for ${typeName}`);
548 // Retrieve the actor ID from root or target actor's form
549 front.actorID = form[formAttributeName];
550 if (!front.actorID) {
552 `Can't find the actor ID for ${typeName} from root or target` +
558 await front.manage(front);
560 await target.manage(front);
565 exports.getFront = getFront;
568 * Create a RootFront.
570 * @param DevToolsClient client
571 * The DevToolsClient instance to use.
572 * @param Object packet
575 function createRootFront(client, packet) {
576 const rootFront = createFront(client, "root");
577 rootFront.form(packet);
579 // Root Front is a special case, managing itself as it doesn't have any parent.
580 // It will register itself to DevToolsClient as a Pool via Front._poolMap.
581 rootFront.manage(rootFront);
585 exports.createRootFront = createRootFront;