Bug 1539764 - Add a targetFront attribute to the Front class to retrieve the target...
[gecko.git] / devtools / shared / protocol / types.js
blob9334e69f4047b3caa85ae938ad83c09f18c5ef3c
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("./Actor");
8 var { lazyLoadSpec, lazyLoadFront } = require("devtools/shared/specs/index");
10 /**
11  * Types: named marshallers/demarshallers.
12  *
13  * Types provide a 'write' function that takes a js representation and
14  * returns a protocol representation, and a "read" function that
15  * takes a protocol representation and returns a js representation.
16  *
17  * The read and write methods are also passed a context object that
18  * represent the actor or front requesting the translation.
19  *
20  * Types are referred to with a typestring.  Basic types are
21  * registered by name using addType, and more complex types can
22  * be generated by adding detail to the type name.
23  */
25 var types = Object.create(null);
26 exports.types = types;
28 var registeredTypes = (types.registeredTypes = new Map());
29 var registeredLifetimes = (types.registeredLifetimes = new Map());
31 exports.registeredTypes = registeredTypes;
33 /**
34  * Return the type object associated with a given typestring.
35  * If passed a type object, it will be returned unchanged.
36  *
37  * Types can be registered with addType, or can be created on
38  * the fly with typestrings.  Examples:
39  *
40  *   boolean
41  *   threadActor
42  *   threadActor#detail
43  *   array:threadActor
44  *   array:array:threadActor#detail
45  *
46  * @param [typestring|type] type
47  *    Either a typestring naming a type or a type object.
48  *
49  * @returns a type object.
50  */
51 types.getType = function(type) {
52   if (!type) {
53     return types.Primitive;
54   }
56   if (typeof type !== "string") {
57     return type;
58   }
60   // If already registered, we're done here.
61   let reg = registeredTypes.get(type);
62   if (reg) {
63     return reg;
64   }
66   // Try to lazy load the spec, if not already loaded.
67   if (lazyLoadSpec(type)) {
68     // If a spec module was lazy loaded, it will synchronously call
69     // generateActorSpec, and set the type in `registeredTypes`.
70     reg = registeredTypes.get(type);
71     if (reg) {
72       return reg;
73     }
74   }
76   // New type, see if it's a collection/lifetime type:
77   const sep = type.indexOf(":");
78   if (sep >= 0) {
79     const collection = type.substring(0, sep);
80     const subtype = types.getType(type.substring(sep + 1));
82     if (collection === "array") {
83       return types.addArrayType(subtype);
84     } else if (collection === "nullable") {
85       return types.addNullableType(subtype);
86     }
88     if (registeredLifetimes.has(collection)) {
89       return types.addLifetimeType(collection, subtype);
90     }
92     throw Error("Unknown collection type: " + collection);
93   }
95   // Not a collection, might be actor detail
96   const pieces = type.split("#", 2);
97   if (pieces.length > 1) {
98     if (pieces[1] != "actorid") {
99       throw new Error(
100         "Unsupported detail, only support 'actorid', got: " + pieces[1]
101       );
102     }
103     return types.addActorDetail(type, pieces[0], pieces[1]);
104   }
106   throw Error("Unknown type: " + type);
110  * Don't allow undefined when writing primitive types to packets.  If
111  * you want to allow undefined, use a nullable type.
112  */
113 function identityWrite(v) {
114   if (v === undefined) {
115     throw Error("undefined passed where a value is required");
116   }
117   // This has to handle iterator->array conversion because arrays of
118   // primitive types pass through here.
119   if (v && typeof v.next === "function") {
120     return [...v];
121   }
122   return v;
126  * Add a type to the type system.
128  * When registering a type, you can provide `read` and `write` methods.
130  * The `read` method will be passed a JS object value from the JSON
131  * packet and must return a native representation.  The `write` method will
132  * be passed a native representation and should provide a JSONable value.
134  * These methods will both be passed a context.  The context is the object
135  * performing or servicing the request - on the server side it will be
136  * an Actor, on the client side it will be a Front.
138  * @param typestring name
139  *    Name to register
140  * @param object typeObject
141  *    An object whose properties will be stored in the type, including
142  *    the `read` and `write` methods.
143  * @param object options
144  *    Can specify `thawed` to prevent the type from being frozen.
146  * @returns a type object that can be used in protocol definitions.
147  */
148 types.addType = function(name, typeObject = {}, options = {}) {
149   if (registeredTypes.has(name)) {
150     throw Error("Type '" + name + "' already exists.");
151   }
153   const type = Object.assign(
154     {
155       toString() {
156         return "[protocol type:" + name + "]";
157       },
158       name: name,
159       primitive: !(typeObject.read || typeObject.write),
160       read: identityWrite,
161       write: identityWrite,
162     },
163     typeObject
164   );
166   registeredTypes.set(name, type);
168   return type;
172  * Remove a type previously registered with the system.
173  * Primarily useful for types registered by addons.
174  */
175 types.removeType = function(name) {
176   // This type may still be referenced by other types, make sure
177   // those references don't work.
178   const type = registeredTypes.get(name);
180   type.name = "DEFUNCT:" + name;
181   type.category = "defunct";
182   type.primitive = false;
183   type.read = type.write = function() {
184     throw new Error("Using defunct type: " + name);
185   };
187   registeredTypes.delete(name);
191  * Add an array type to the type system.
193  * getType() will call this function if provided an "array:<type>"
194  * typestring.
196  * @param type subtype
197  *    The subtype to be held by the array.
198  */
199 types.addArrayType = function(subtype) {
200   subtype = types.getType(subtype);
202   const name = "array:" + subtype.name;
204   // Arrays of primitive types are primitive types themselves.
205   if (subtype.primitive) {
206     return types.addType(name);
207   }
208   return types.addType(name, {
209     category: "array",
210     read: (v, ctx) => {
211       if (v && typeof v.next === "function") {
212         v = [...v];
213       }
214       return v.map(i => subtype.read(i, ctx));
215     },
216     write: (v, ctx) => {
217       if (v && typeof v.next === "function") {
218         v = [...v];
219       }
220       return v.map(i => subtype.write(i, ctx));
221     },
222   });
226  * Add a dict type to the type system.  This allows you to serialize
227  * a JS object that contains non-primitive subtypes.
229  * Properties of the value that aren't included in the specializations
230  * will be serialized as primitive values.
232  * @param object specializations
233  *    A dict of property names => type
234  */
235 types.addDictType = function(name, specializations) {
236   const specTypes = {};
237   for (const prop in specializations) {
238     try {
239       specTypes[prop] = types.getType(specializations[prop]);
240     } catch (e) {
241       // Types may not be defined yet. Sometimes, we define the type *after* using it, but
242       // also, we have cyclic definitions on types. So lazily load them when they are not
243       // immediately available.
244       loader.lazyGetter(specTypes, prop, () => {
245         return types.getType(specializations[prop]);
246       });
247     }
248   }
249   return types.addType(name, {
250     category: "dict",
251     specializations,
252     read: (v, ctx) => {
253       const ret = {};
254       for (const prop in v) {
255         if (prop in specTypes) {
256           ret[prop] = specTypes[prop].read(v[prop], ctx);
257         } else {
258           ret[prop] = v[prop];
259         }
260       }
261       return ret;
262     },
264     write: (v, ctx) => {
265       const ret = {};
266       for (const prop in v) {
267         if (prop in specTypes) {
268           ret[prop] = specTypes[prop].write(v[prop], ctx);
269         } else {
270           ret[prop] = v[prop];
271         }
272       }
273       return ret;
274     },
275   });
279  * Register an actor type with the type system.
281  * Types are marshalled differently when communicating server->client
282  * than they are when communicating client->server.  The server needs
283  * to provide useful information to the client, so uses the actor's
284  * `form` method to get a json representation of the actor.  When
285  * making a request from the client we only need the actor ID string.
287  * This function can be called before the associated actor has been
288  * constructed, but the read and write methods won't work until
289  * the associated addActorImpl or addActorFront methods have been
290  * called during actor/front construction.
292  * @param string name
293  *    The typestring to register.
294  */
295 types.addActorType = function(name) {
296   // We call addActorType from:
297   //   FrontClassWithSpec when registering front synchronously,
298   //   generateActorSpec when defining specs,
299   //   specs modules to register actor type early to use them in other types
300   if (registeredTypes.has(name)) {
301     return registeredTypes.get(name);
302   }
303   const type = types.addType(name, {
304     _actor: true,
305     category: "actor",
306     read: (v, ctx, detail) => {
307       // If we're reading a request on the server side, just
308       // find the actor registered with this actorID.
309       if (ctx instanceof Actor) {
310         return ctx.conn.getActor(v);
311       }
313       // Reading a response on the client side, check for an
314       // existing front on the connection, and create the front
315       // if it isn't found.
316       const actorID = typeof v === "string" ? v : v.actor;
317       let front = ctx.conn.getActor(actorID);
318       if (!front) {
319         // If front isn't instantiated yet, create one.
320         // Try lazy loading front if not already loaded.
321         // The front module will synchronously call `FrontClassWithSpec` and
322         // augment `type` with the `frontClass` attribute.
323         if (!type.frontClass) {
324           lazyLoadFront(name);
325         }
327         // Use intermediate Class variable to please eslint requiring
328         // a capital letter for all constructors.
329         const Class = type.frontClass;
330         front = new Class(ctx.conn);
331         front.actorID = actorID;
332         const parentFront = ctx.marshallPool();
333         // If this is a child of a target-scoped front, propagate the target front to the
334         // child front that it manages.
335         front.targetFront = parentFront.targetFront;
336         parentFront.manage(front);
337       }
339       // When the type `${name}#actorid` is used, `v` is a string refering to the
340       // actor ID. We only set the actorID just before and so do not need anything else.
341       if (detail != "actorid") {
342         v = identityWrite(v);
343         front.form(v, ctx);
344       }
346       return front;
347     },
348     write: (v, ctx, detail) => {
349       // If returning a response from the server side, make sure
350       // the actor is added to a parent object and return its form.
351       if (v instanceof Actor) {
352         if (!v.actorID) {
353           ctx.marshallPool().manage(v);
354         }
355         if (detail == "actorid") {
356           return v.actorID;
357         }
358         return identityWrite(v.form(detail));
359       }
361       // Writing a request from the client side, just send the actor id.
362       return v.actorID;
363     },
364   });
365   return type;
368 types.addNullableType = function(subtype) {
369   subtype = types.getType(subtype);
370   return types.addType("nullable:" + subtype.name, {
371     category: "nullable",
372     read: (value, ctx) => {
373       if (value == null) {
374         return value;
375       }
376       return subtype.read(value, ctx);
377     },
378     write: (value, ctx) => {
379       if (value == null) {
380         return value;
381       }
382       return subtype.write(value, ctx);
383     },
384   });
388  * Register an actor detail type.  This is just like an actor type, but
389  * will pass a detail hint to the actor's form method during serialization/
390  * deserialization.
392  * This is called by getType() when passed an 'actorType#detail' string.
394  * @param string name
395  *   The typestring to register this type as.
396  * @param type actorType
397  *   The actor type you'll be detailing.
398  * @param string detail
399  *   The detail to pass.
400  */
401 types.addActorDetail = function(name, actorType, detail) {
402   actorType = types.getType(actorType);
403   if (!actorType._actor) {
404     throw Error(
405       `Details only apply to actor types, tried to add detail '${detail}' ` +
406         `to ${actorType.name}`
407     );
408   }
409   return types.addType(name, {
410     _actor: true,
411     category: "detail",
412     read: (v, ctx) => actorType.read(v, ctx, detail),
413     write: (v, ctx) => actorType.write(v, ctx, detail),
414   });
418  * Register an actor lifetime.  This lets the type system find a parent
419  * actor that differs from the actor fulfilling the request.
421  * @param string name
422  *    The lifetime name to use in typestrings.
423  * @param string prop
424  *    The property of the actor that holds the parent that should be used.
425  */
426 types.addLifetime = function(name, prop) {
427   if (registeredLifetimes.has(name)) {
428     throw Error("Lifetime '" + name + "' already registered.");
429   }
430   registeredLifetimes.set(name, prop);
434  * Remove a previously-registered lifetime.  Useful for lifetimes registered
435  * in addons.
436  */
437 types.removeLifetime = function(name) {
438   registeredLifetimes.delete(name);
442  * Register a lifetime type.  This creates an actor type tied to the given
443  * lifetime.
445  * This is called by getType() when passed a '<lifetimeType>:<actorType>'
446  * typestring.
448  * @param string lifetime
449  *    A lifetime string previously regisered with addLifetime()
450  * @param type subtype
451  *    An actor type
452  */
453 types.addLifetimeType = function(lifetime, subtype) {
454   subtype = types.getType(subtype);
455   if (!subtype._actor) {
456     throw Error(
457       `Lifetimes only apply to actor types, tried to apply ` +
458         `lifetime '${lifetime}' to ${subtype.name}`
459     );
460   }
461   const prop = registeredLifetimes.get(lifetime);
462   return types.addType(lifetime + ":" + subtype.name, {
463     category: "lifetime",
464     read: (value, ctx) => subtype.read(value, ctx[prop]),
465     write: (value, ctx) => subtype.write(value, ctx[prop]),
466   });
469 // Add a few named primitive types.
470 types.Primitive = types.addType("primitive");
471 types.String = types.addType("string");
472 types.Number = types.addType("number");
473 types.Boolean = types.addType("boolean");
474 types.JSON = types.addType("json");
476 exports.registerFront = function(cls) {
477   const { typeName } = cls.prototype;
478   if (!registeredTypes.has(typeName)) {
479     types.addActorType(typeName);
480   }
481   registeredTypes.get(typeName).frontClass = cls;
485  * Instantiate a global (preference, device) or target-scoped (webconsole, inspector)
486  * front of the given type by picking its actor ID out of either the target or root
487  * front's form.
489  * @param DebuggerClient client
490  *    The DebuggerClient instance to use.
491  * @param string typeName
492  *    The type name of the front to instantiate. This is defined in its specifiation.
493  * @param json form
494  *    If we want to instantiate a global actor's front, this is the root front's form,
495  *    otherwise we are instantiating a target-scoped front from the target front's form.
496  * @param [Target|null] target
497  *    If we are instantiating a target-scoped front, this is a reference to the front's
498  *    Target instance, otherwise this is null.
499  */
500 function getFront(client, typeName, form, target = null) {
501   const type = types.getType(typeName);
502   if (!type) {
503     throw new Error(`No spec for front type '${typeName}'.`);
504   }
505   if (!type.frontClass) {
506     lazyLoadFront(typeName);
507   }
508   // Use intermediate Class variable to please eslint requiring
509   // a capital letter for all constructors.
510   const Class = type.frontClass;
511   const instance = new Class(client);
512   // Set the targetFront for target-scoped fronts.
513   instance.targetFront = target;
514   const { formAttributeName } = instance;
515   if (!formAttributeName) {
516     throw new Error(`Can't find the form attribute name for ${typeName}`);
517   }
518   // Retrive the actor ID from root or target actor's form
519   instance.actorID = form[formAttributeName];
520   if (!instance.actorID) {
521     throw new Error(
522       `Can't find the actor ID for ${typeName} from root or target` +
523         ` actor's form.`
524     );
525   }
526   // Historically, all global and target scoped front were the first protocol.js in the
527   // hierarchy of fronts. So that they have to self-own themself. But now, Root and Target
528   // are fronts and should own them. The only issue here is that we should manage the
529   // front *before* calling initialize which is going to try managing child fronts.
530   instance.manage(instance);
532   if (typeof instance.initialize == "function") {
533     return instance.initialize().then(() => instance);
534   }
535   return instance;
537 exports.getFront = getFront;