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("./Actor");
8 var { lazyLoadSpec, lazyLoadFront } = require("devtools/shared/specs/index");
11 * Types: named marshallers/demarshallers.
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.
17 * The read and write methods are also passed a context object that
18 * represent the actor or front requesting the translation.
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.
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;
34 * Return the type object associated with a given typestring.
35 * If passed a type object, it will be returned unchanged.
37 * Types can be registered with addType, or can be created on
38 * the fly with typestrings. Examples:
44 * array:array:threadActor#detail
46 * @param [typestring|type] type
47 * Either a typestring naming a type or a type object.
49 * @returns a type object.
51 types.getType = function(type) {
53 return types.Primitive;
56 if (typeof type !== "string") {
60 // If already registered, we're done here.
61 let reg = registeredTypes.get(type);
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);
76 // New type, see if it's a collection/lifetime type:
77 const sep = type.indexOf(":");
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);
88 if (registeredLifetimes.has(collection)) {
89 return types.addLifetimeType(collection, subtype);
92 throw Error("Unknown collection type: " + collection);
95 // Not a collection, might be actor detail
96 const pieces = type.split("#", 2);
97 if (pieces.length > 1) {
98 if (pieces[1] != "actorid") {
100 "Unsupported detail, only support 'actorid', got: " + pieces[1]
103 return types.addActorDetail(type, pieces[0], pieces[1]);
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.
113 function identityWrite(v) {
114 if (v === undefined) {
115 throw Error("undefined passed where a value is required");
117 // This has to handle iterator->array conversion because arrays of
118 // primitive types pass through here.
119 if (v && typeof v.next === "function") {
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
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.
148 types.addType = function(name, typeObject = {}, options = {}) {
149 if (registeredTypes.has(name)) {
150 throw Error("Type '" + name + "' already exists.");
153 const type = Object.assign(
156 return "[protocol type:" + name + "]";
159 primitive: !(typeObject.read || typeObject.write),
161 write: identityWrite,
166 registeredTypes.set(name, type);
172 * Remove a type previously registered with the system.
173 * Primarily useful for types registered by addons.
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);
187 registeredTypes.delete(name);
191 * Add an array type to the type system.
193 * getType() will call this function if provided an "array:<type>"
196 * @param type subtype
197 * The subtype to be held by the array.
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);
208 return types.addType(name, {
211 if (v && typeof v.next === "function") {
214 return v.map(i => subtype.read(i, ctx));
217 if (v && typeof v.next === "function") {
220 return v.map(i => subtype.write(i, ctx));
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
235 types.addDictType = function(name, specializations) {
236 const specTypes = {};
237 for (const prop in specializations) {
239 specTypes[prop] = types.getType(specializations[prop]);
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]);
249 return types.addType(name, {
254 for (const prop in v) {
255 if (prop in specTypes) {
256 ret[prop] = specTypes[prop].read(v[prop], ctx);
266 for (const prop in v) {
267 if (prop in specTypes) {
268 ret[prop] = specTypes[prop].write(v[prop], ctx);
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.
293 * The typestring to register.
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);
303 const type = types.addType(name, {
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);
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);
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) {
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);
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);
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) {
353 ctx.marshallPool().manage(v);
355 if (detail == "actorid") {
358 return identityWrite(v.form(detail));
361 // Writing a request from the client side, just send the actor id.
368 types.addNullableType = function(subtype) {
369 subtype = types.getType(subtype);
370 return types.addType("nullable:" + subtype.name, {
371 category: "nullable",
372 read: (value, ctx) => {
376 return subtype.read(value, ctx);
378 write: (value, ctx) => {
382 return subtype.write(value, ctx);
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/
392 * This is called by getType() when passed an 'actorType#detail' string.
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.
401 types.addActorDetail = function(name, actorType, detail) {
402 actorType = types.getType(actorType);
403 if (!actorType._actor) {
405 `Details only apply to actor types, tried to add detail '${detail}' ` +
406 `to ${actorType.name}`
409 return types.addType(name, {
412 read: (v, ctx) => actorType.read(v, ctx, detail),
413 write: (v, ctx) => actorType.write(v, ctx, detail),
418 * Register an actor lifetime. This lets the type system find a parent
419 * actor that differs from the actor fulfilling the request.
422 * The lifetime name to use in typestrings.
424 * The property of the actor that holds the parent that should be used.
426 types.addLifetime = function(name, prop) {
427 if (registeredLifetimes.has(name)) {
428 throw Error("Lifetime '" + name + "' already registered.");
430 registeredLifetimes.set(name, prop);
434 * Remove a previously-registered lifetime. Useful for lifetimes registered
437 types.removeLifetime = function(name) {
438 registeredLifetimes.delete(name);
442 * Register a lifetime type. This creates an actor type tied to the given
445 * This is called by getType() when passed a '<lifetimeType>:<actorType>'
448 * @param string lifetime
449 * A lifetime string previously regisered with addLifetime()
450 * @param type subtype
453 types.addLifetimeType = function(lifetime, subtype) {
454 subtype = types.getType(subtype);
455 if (!subtype._actor) {
457 `Lifetimes only apply to actor types, tried to apply ` +
458 `lifetime '${lifetime}' to ${subtype.name}`
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]),
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);
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
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.
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.
500 function getFront(client, typeName, form, target = null) {
501 const type = types.getType(typeName);
503 throw new Error(`No spec for front type '${typeName}'.`);
505 if (!type.frontClass) {
506 lazyLoadFront(typeName);
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}`);
518 // Retrive the actor ID from root or target actor's form
519 instance.actorID = form[formAttributeName];
520 if (!instance.actorID) {
522 `Can't find the actor ID for ${typeName} from root or target` +
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);
537 exports.getFront = getFront;