Backed out 3 changesets (bug 1898476) for causing build bustages @ MozContainerSurfac...
[gecko.git] / devtools / shared / protocol / types.js
blob31c71276ea8a3489afc92062004484653596af16
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/. */
5 "use strict";
7 var { Actor } = require("resource://devtools/shared/protocol/Actor.js");
8 var {
9   lazyLoadSpec,
10   lazyLoadFront,
11 } = require("resource://devtools/shared/specs/index.js");
13 /**
14  * Types: named marshallers/demarshallers.
15  *
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.
19  *
20  * The read and write methods are also passed a context object that
21  * represent the actor or front requesting the translation.
22  *
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.
26  */
28 var types = Object.create(null);
29 exports.types = types;
31 var registeredTypes = (types.registeredTypes = new Map());
33 exports.registeredTypes = registeredTypes;
35 /**
36  * Return the type object associated with a given typestring.
37  * If passed a type object, it will be returned unchanged.
38  *
39  * Types can be registered with addType, or can be created on
40  * the fly with typestrings.  Examples:
41  *
42  *   boolean
43  *   threadActor
44  *   threadActor#detail
45  *   array:threadActor
46  *   array:array:threadActor#detail
47  *
48  * @param [typestring|type] type
49  *    Either a typestring naming a type or a type object.
50  *
51  * @returns a type object.
52  */
53 types.getType = function (type) {
54   if (!type) {
55     return types.Primitive;
56   }
58   if (typeof type !== "string") {
59     return type;
60   }
62   // If already registered, we're done here.
63   let reg = registeredTypes.get(type);
64   if (reg) {
65     return reg;
66   }
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);
73     if (reg) {
74       return reg;
75     }
76   }
78   // New type, see if it's a collection type:
79   const sep = type.indexOf(":");
80   if (sep >= 0) {
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);
88     }
90     throw Error("Unknown collection type: " + collection);
91   }
93   // Not a collection, might be actor detail
94   const pieces = type.split("#", 2);
95   if (pieces.length > 1) {
96     if (pieces[1] != "actorid") {
97       throw new Error(
98         "Unsupported detail, only support 'actorid', got: " + pieces[1]
99       );
100     }
101     return types.addActorDetail(type, pieces[0], pieces[1]);
102   }
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.
110  */
111 function identityWrite(v) {
112   if (v === undefined) {
113     throw Error("undefined passed where a value is required");
114   }
115   // This has to handle iterator->array conversion because arrays of
116   // primitive types pass through here.
117   if (v && typeof v.next === "function") {
118     return [...v];
119   }
120   return v;
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
137  *    Name to register
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.
143  */
144 types.addType = function (name, typeObject = {}) {
145   if (registeredTypes.has(name)) {
146     throw Error("Type '" + name + "' already exists.");
147   }
149   const type = Object.assign(
150     {
151       toString() {
152         return "[protocol type:" + name + "]";
153       },
154       name,
155       primitive: !(typeObject.read || typeObject.write),
156       read: identityWrite,
157       write: identityWrite,
158     },
159     typeObject
160   );
162   registeredTypes.set(name, type);
164   return type;
168  * Remove a type previously registered with the system.
169  * Primarily useful for types registered by addons.
170  */
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);
181   };
183   registeredTypes.delete(name);
187  * Add an array type to the type system.
189  * getType() will call this function if provided an "array:<type>"
190  * typestring.
192  * @param type subtype
193  *    The subtype to be held by the array.
194  */
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);
203   }
204   return types.addType(name, {
205     category: "array",
206     read: (v, ctx) => {
207       if (v && typeof v.next === "function") {
208         v = [...v];
209       }
210       return v.map(i => subtype.read(i, ctx));
211     },
212     write: (v, ctx) => {
213       if (v && typeof v.next === "function") {
214         v = [...v];
215       }
216       return v.map(i => subtype.write(i, ctx));
217     },
218   });
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
230  */
231 types.addDictType = function (name, specializations) {
232   const specTypes = {};
233   for (const prop in specializations) {
234     try {
235       specTypes[prop] = types.getType(specializations[prop]);
236     } catch (e) {
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]);
242       });
243     }
244   }
245   return types.addType(name, {
246     category: "dict",
247     specializations,
248     read: (v, ctx) => {
249       const ret = {};
250       for (const prop in v) {
251         if (prop in specTypes) {
252           ret[prop] = specTypes[prop].read(v[prop], ctx);
253         } else {
254           ret[prop] = v[prop];
255         }
256       }
257       return ret;
258     },
260     write: (v, ctx) => {
261       const ret = {};
262       for (const prop in v) {
263         if (prop in specTypes) {
264           ret[prop] = specTypes[prop].write(v[prop], ctx);
265         } else {
266           ret[prop] = v[prop];
267         }
268       }
269       return ret;
270     },
271   });
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.
288  * @param string name
289  *    The typestring to register.
290  */
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);
298   }
299   const type = types.addType(name, {
300     _actor: true,
301     category: "actor",
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);
307       }
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.
319       let form = null;
320       if (detail != "actorid") {
321         form = identityWrite(v);
322       }
324       if (!front) {
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) {
330           lazyLoadFront(name);
331         }
333         const parentFront = ctx.marshallPool();
334         const targetFront = parentFront.isTargetFront
335           ? parentFront
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);
345       } else if (form) {
346         front.form(form, ctx);
347       }
349       return front;
350     },
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()) {
356           throw new Error(
357             `Attempted to write a response containing a destroyed actor`
358           );
359         }
360         if (!v.actorID) {
361           ctx.marshallPool().manage(v);
362         }
363         if (detail == "actorid") {
364           return v.actorID;
365         }
366         return identityWrite(v.form(detail));
367       }
369       // Writing a request from the client side, just send the actor id.
370       return v.actorID;
371     },
372   });
373   return type;
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") {
381       throw new Error(
382         `In polymorphic type '${subtypes.join(
383           ","
384         )}', the type '${subTypeName}' isn't an actor`
385       );
386     }
387   }
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;
395       if (!actorID) {
396         throw new Error(
397           `Was expecting one of these actors '${subtypes}' but instead got value: '${value}'`
398         );
399       }
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)) {
405         throw new Error(
406           `Was expecting one of these actors '${subtypes}' but instead got an actor of type: '${typeName}'`
407         );
408       }
410       const subtype = types.getType(typeName);
411       return subtype.read(value, ctx);
412     },
413     write: (value, ctx) => {
414       if (!value) {
415         throw new Error(
416           `Was expecting one of these actors '${subtypes}' but instead got an empty value.`
417         );
418       }
419       // value is either an `Actor` or a `Front` and both classes exposes a `typeName`
420       const typeName = value.typeName;
421       if (!typeName) {
422         throw new Error(
423           `Was expecting one of these actors '${subtypes}' but instead got value: '${value}'. Did you pass a form instead of an Actor?`
424         );
425       }
427       if (!subtypes.includes(typeName)) {
428         throw new Error(
429           `Was expecting one of these actors '${subtypes}' but instead got an actor of type: '${typeName}'`
430         );
431       }
433       const subtype = types.getType(typeName);
434       return subtype.write(value, ctx);
435     },
436   });
438 types.addNullableType = function (subtype) {
439   subtype = types.getType(subtype);
440   return types.addType("nullable:" + subtype.name, {
441     category: "nullable",
442     read: (value, ctx) => {
443       if (value == null) {
444         return value;
445       }
446       return subtype.read(value, ctx);
447     },
448     write: (value, ctx) => {
449       if (value == null) {
450         return value;
451       }
452       return subtype.write(value, ctx);
453     },
454   });
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/
460  * deserialization.
462  * This is called by getType() when passed an 'actorType#detail' string.
464  * @param string name
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.
470  */
471 types.addActorDetail = function (name, actorType, detail) {
472   actorType = types.getType(actorType);
473   if (!actorType._actor) {
474     throw Error(
475       `Details only apply to actor types, tried to add detail '${detail}' ` +
476         `to ${actorType.name}`
477     );
478   }
479   return types.addType(name, {
480     _actor: true,
481     category: "detail",
482     read: (v, ctx) => actorType.read(v, ctx, detail),
483     write: (v, ctx) => actorType.write(v, ctx, detail),
484   });
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);
498   }
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.
509  * @returns Front
510  *    The created front.
511  */
512 function createFront(client, typeName, target = null) {
513   const type = types.getType(typeName);
514   if (!type) {
515     throw new Error(`No spec for front type '${typeName}'.`);
516   } else if (!type.frontClass) {
517     lazyLoadFront(typeName);
518   }
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
529  * front's form.
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.
535  * @param json form
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.
541  */
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}`);
547   }
548   // Retrieve the actor ID from root or target actor's form
549   front.actorID = form[formAttributeName];
550   if (!front.actorID) {
551     throw new Error(
552       `Can't find the actor ID for ${typeName} from root or target` +
553         ` actor's form.`
554     );
555   }
557   if (!target) {
558     await front.manage(front);
559   } else {
560     await target.manage(front);
561   }
563   return front;
565 exports.getFront = getFront;
568  * Create a RootFront.
570  * @param DevToolsClient client
571  *    The DevToolsClient instance to use.
572  * @param Object packet
573  * @returns RootFront
574  */
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);
583   return rootFront;
585 exports.createRootFront = createRootFront;