types.js (18148B)
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/. */ 4 5 "use strict"; 6 7 var { Actor } = require("resource://devtools/shared/protocol/Actor.js"); 8 var { 9 lazyLoadSpec, 10 lazyLoadFront, 11 } = require("resource://devtools/shared/specs/index.js"); 12 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 */ 27 28 var types = Object.create(null); 29 exports.types = types; 30 31 var registeredTypes = (types.registeredTypes = new Map()); 32 33 exports.registeredTypes = registeredTypes; 34 35 // Values used in specification's request or response attributes to specify 36 // if a request should only send (request) orreceive (response) raw byte thanks to bulk requests. 37 // In such case, the front/actor can't send/receive any custom json attribute. 38 // The request will result into a one-way JSON packet with "actor", "type" and "length" attributes only. 39 // (i.e. there is no response packet as being one-way) 40 exports.BULK_REQUEST = Symbol("request"); 41 exports.BULK_RESPONSE = Symbol("response"); 42 43 /** 44 * Return the type object associated with a given typestring. 45 * If passed a type object, it will be returned unchanged. 46 * 47 * Types can be registered with addType, or can be created on 48 * the fly with typestrings. Examples: 49 * 50 * boolean 51 * threadActor 52 * threadActor#detail 53 * array:threadActor 54 * array:array:threadActor#detail 55 * 56 * @param [typestring|type] type 57 * Either a typestring naming a type or a type object. 58 * 59 * @returns a type object. 60 */ 61 types.getType = function (type) { 62 if (!type) { 63 return types.Primitive; 64 } 65 66 if (typeof type !== "string") { 67 return type; 68 } 69 70 // If already registered, we're done here. 71 let reg = registeredTypes.get(type); 72 if (reg) { 73 return reg; 74 } 75 76 // Try to lazy load the spec, if not already loaded. 77 if (lazyLoadSpec(type)) { 78 // If a spec module was lazy loaded, it will synchronously call 79 // generateActorSpec, and set the type in `registeredTypes`. 80 reg = registeredTypes.get(type); 81 if (reg) { 82 return reg; 83 } 84 } 85 86 // New type, see if it's a collection type: 87 const sep = type.indexOf(":"); 88 if (sep >= 0) { 89 const collection = type.substring(0, sep); 90 const subtype = types.getType(type.substring(sep + 1)); 91 92 if (collection === "array") { 93 return types.addArrayType(subtype); 94 } else if (collection === "nullable") { 95 return types.addNullableType(subtype); 96 } 97 98 throw Error("Unknown collection type: " + collection); 99 } 100 101 // Not a collection, might be actor detail 102 const pieces = type.split("#", 2); 103 if (pieces.length > 1) { 104 if (pieces[1] != "actorid") { 105 throw new Error( 106 "Unsupported detail, only support 'actorid', got: " + pieces[1] 107 ); 108 } 109 return types.addActorDetail(type, pieces[0], pieces[1]); 110 } 111 112 throw Error("Unknown type: " + type); 113 }; 114 115 /** 116 * Don't allow undefined when writing primitive types to packets. If 117 * you want to allow undefined, use a nullable type. 118 */ 119 function identityWrite(v) { 120 if (v === undefined) { 121 throw Error("undefined passed where a value is required"); 122 } 123 // This has to handle iterator->array conversion because arrays of 124 // primitive types pass through here. 125 if (v && typeof v.next === "function") { 126 return [...v]; 127 } 128 return v; 129 } 130 131 /** 132 * Add a type to the type system. 133 * 134 * When registering a type, you can provide `read` and `write` methods. 135 * 136 * The `read` method will be passed a JS object value from the JSON 137 * packet and must return a native representation. The `write` method will 138 * be passed a native representation and should provide a JSONable value. 139 * 140 * These methods will both be passed a context. The context is the object 141 * performing or servicing the request - on the server side it will be 142 * an Actor, on the client side it will be a Front. 143 * 144 * @param typestring name 145 * Name to register 146 * @param object typeObject 147 * An object whose properties will be stored in the type, including 148 * the `read` and `write` methods. 149 * 150 * @returns a type object that can be used in protocol definitions. 151 */ 152 types.addType = function (name, typeObject = {}) { 153 if (registeredTypes.has(name)) { 154 throw Error("Type '" + name + "' already exists."); 155 } 156 157 const type = Object.assign( 158 { 159 toString() { 160 return "[protocol type:" + name + "]"; 161 }, 162 name, 163 primitive: !(typeObject.read || typeObject.write), 164 read: identityWrite, 165 write: identityWrite, 166 }, 167 typeObject 168 ); 169 170 registeredTypes.set(name, type); 171 172 return type; 173 }; 174 175 /** 176 * Remove a type previously registered with the system. 177 * Primarily useful for types registered by addons. 178 */ 179 types.removeType = function (name) { 180 // This type may still be referenced by other types, make sure 181 // those references don't work. 182 const type = registeredTypes.get(name); 183 184 type.name = "DEFUNCT:" + name; 185 type.category = "defunct"; 186 type.primitive = false; 187 type.read = type.write = function () { 188 throw new Error("Using defunct type: " + name); 189 }; 190 191 registeredTypes.delete(name); 192 }; 193 194 /** 195 * Add an array type to the type system. 196 * 197 * getType() will call this function if provided an "array:<type>" 198 * typestring. 199 * 200 * @param type subtype 201 * The subtype to be held by the array. 202 */ 203 types.addArrayType = function (subtype) { 204 subtype = types.getType(subtype); 205 206 const name = "array:" + subtype.name; 207 208 // Arrays of primitive types are primitive types themselves. 209 if (subtype.primitive) { 210 return types.addType(name); 211 } 212 return types.addType(name, { 213 category: "array", 214 read: (v, ctx) => { 215 if (v && typeof v.next === "function") { 216 v = [...v]; 217 } 218 return v.map(i => subtype.read(i, ctx)); 219 }, 220 write: (v, ctx) => { 221 if (v && typeof v.next === "function") { 222 v = [...v]; 223 } 224 return v.map(i => subtype.write(i, ctx)); 225 }, 226 }); 227 }; 228 229 /** 230 * Add a dict type to the type system. This allows you to serialize 231 * a JS object that contains non-primitive subtypes. 232 * 233 * Properties of the value that aren't included in the specializations 234 * will be serialized as primitive values. 235 * 236 * @param object specializations 237 * A dict of property names => type 238 */ 239 types.addDictType = function (name, specializations) { 240 const specTypes = {}; 241 for (const prop in specializations) { 242 try { 243 specTypes[prop] = types.getType(specializations[prop]); 244 } catch (e) { 245 // Types may not be defined yet. Sometimes, we define the type *after* using it, but 246 // also, we have cyclic definitions on types. So lazily load them when they are not 247 // immediately available. 248 loader.lazyGetter(specTypes, prop, () => { 249 return types.getType(specializations[prop]); 250 }); 251 } 252 } 253 return types.addType(name, { 254 category: "dict", 255 specializations, 256 read: (v, ctx) => { 257 const ret = {}; 258 for (const prop in v) { 259 if (prop in specTypes) { 260 ret[prop] = specTypes[prop].read(v[prop], ctx); 261 } else { 262 ret[prop] = v[prop]; 263 } 264 } 265 return ret; 266 }, 267 268 write: (v, ctx) => { 269 const ret = {}; 270 for (const prop in v) { 271 if (prop in specTypes) { 272 ret[prop] = specTypes[prop].write(v[prop], ctx); 273 } else { 274 ret[prop] = v[prop]; 275 } 276 } 277 return ret; 278 }, 279 }); 280 }; 281 282 /** 283 * Register an actor type with the type system. 284 * 285 * Types are marshalled differently when communicating server->client 286 * than they are when communicating client->server. The server needs 287 * to provide useful information to the client, so uses the actor's 288 * `form` method to get a json representation of the actor. When 289 * making a request from the client we only need the actor ID string. 290 * 291 * This function can be called before the associated actor has been 292 * constructed, but the read and write methods won't work until 293 * the associated addActorImpl or addActorFront methods have been 294 * called during actor/front construction. 295 * 296 * @param string name 297 * The typestring to register. 298 */ 299 types.addActorType = function (name) { 300 // We call addActorType from: 301 // FrontClassWithSpec when registering front synchronously, 302 // generateActorSpec when defining specs, 303 // specs modules to register actor type early to use them in other types 304 if (registeredTypes.has(name)) { 305 return registeredTypes.get(name); 306 } 307 const type = types.addType(name, { 308 _actor: true, 309 category: "actor", 310 read: (v, ctx, detail) => { 311 // If we're reading a request on the server side, just 312 // find the actor registered with this actorID. 313 if (ctx instanceof Actor) { 314 return ctx.conn.getActor(v); 315 } 316 317 // Reading a response on the client side, check for an 318 // existing front on the connection, and create the front 319 // if it isn't found. 320 const actorID = typeof v === "string" ? v : v.actor; 321 // `ctx.conn` is a DevToolsClient 322 let front = ctx.conn.getFrontByID(actorID); 323 324 // When the type `${name}#actorid` is used, `v` is a string refering to the 325 // actor ID. We cannot read form information in this case and the actorID was 326 // already set when creating the front, so no need to do anything. 327 let form = null; 328 if (detail != "actorid") { 329 form = identityWrite(v); 330 } 331 332 if (!front) { 333 // If front isn't instantiated yet, create one. 334 // Try lazy loading front if not already loaded. 335 // The front module will synchronously call `FrontClassWithSpec` and 336 // augment `type` with the `frontClass` attribute. 337 if (!type.frontClass) { 338 lazyLoadFront(name); 339 } 340 341 const parentFront = ctx.marshallPool(); 342 const targetFront = parentFront.isTargetFront 343 ? parentFront 344 : parentFront.targetFront; 345 346 // Use intermediate Class variable to please eslint requiring 347 // a capital letter for all constructors. 348 const Class = type.frontClass; 349 front = new Class(ctx.conn, targetFront, parentFront); 350 front.actorID = actorID; 351 352 parentFront.manage(front, form, ctx); 353 } else if (form) { 354 front.form(form, ctx); 355 } 356 357 return front; 358 }, 359 write: (v, ctx, detail) => { 360 // If returning a response from the server side, make sure 361 // the actor is added to a parent object and return its form. 362 if (v instanceof Actor) { 363 if (v.isDestroyed()) { 364 throw new Error( 365 `Attempted to write a response containing a destroyed actor` 366 ); 367 } 368 if (!v.actorID) { 369 ctx.marshallPool().manage(v); 370 } 371 if (detail == "actorid") { 372 return v.actorID; 373 } 374 return identityWrite(v.form(detail)); 375 } 376 377 // Writing a request from the client side, just send the actor id. 378 return v.actorID; 379 }, 380 }); 381 return type; 382 }; 383 384 types.addPolymorphicType = function (name, subtypes) { 385 // Assert that all subtypes are actors, as the marshalling implementation depends on that. 386 for (const subTypeName of subtypes) { 387 const subtype = types.getType(subTypeName); 388 if (subtype.category != "actor") { 389 throw new Error( 390 `In polymorphic type '${subtypes.join( 391 "," 392 )}', the type '${subTypeName}' isn't an actor` 393 ); 394 } 395 } 396 397 return types.addType(name, { 398 category: "polymorphic", 399 read: (value, ctx) => { 400 // `value` is either a string which is an Actor ID or a form object 401 // where `actor` is an actor ID 402 const actorID = typeof value === "string" ? value : value.actor; 403 if (!actorID) { 404 throw new Error( 405 `Was expecting one of these actors '${subtypes}' but instead got value: '${value}'` 406 ); 407 } 408 409 // Extract the typeName out of the actor ID, which should be composed like this 410 // ${DevToolsServerConnectionPrefix}.${typeName}${Number} 411 const typeName = actorID.match(/\.([a-zA-Z]+)\d+$/)[1]; 412 if (!subtypes.includes(typeName)) { 413 throw new Error( 414 `Was expecting one of these actors '${subtypes}' but instead got an actor of type: '${typeName}'` 415 ); 416 } 417 418 const subtype = types.getType(typeName); 419 return subtype.read(value, ctx); 420 }, 421 write: (value, ctx) => { 422 if (!value) { 423 throw new Error( 424 `Was expecting one of these actors '${subtypes}' but instead got an empty value.` 425 ); 426 } 427 // value is either an `Actor` or a `Front` and both classes exposes a `typeName` 428 const typeName = value.typeName; 429 if (!typeName) { 430 throw new Error( 431 `Was expecting one of these actors '${subtypes}' but instead got value: '${value}'. Did you pass a form instead of an Actor?` 432 ); 433 } 434 435 if (!subtypes.includes(typeName)) { 436 throw new Error( 437 `Was expecting one of these actors '${subtypes}' but instead got an actor of type: '${typeName}'` 438 ); 439 } 440 441 const subtype = types.getType(typeName); 442 return subtype.write(value, ctx); 443 }, 444 }); 445 }; 446 types.addNullableType = function (subtype) { 447 subtype = types.getType(subtype); 448 return types.addType("nullable:" + subtype.name, { 449 category: "nullable", 450 read: (value, ctx) => { 451 if (value == null) { 452 return value; 453 } 454 return subtype.read(value, ctx); 455 }, 456 write: (value, ctx) => { 457 if (value == null) { 458 return value; 459 } 460 return subtype.write(value, ctx); 461 }, 462 }); 463 }; 464 465 /** 466 * Register an actor detail type. This is just like an actor type, but 467 * will pass a detail hint to the actor's form method during serialization/ 468 * deserialization. 469 * 470 * This is called by getType() when passed an 'actorType#detail' string. 471 * 472 * @param string name 473 * The typestring to register this type as. 474 * @param type actorType 475 * The actor type you'll be detailing. 476 * @param string detail 477 * The detail to pass. 478 */ 479 types.addActorDetail = function (name, actorType, detail) { 480 actorType = types.getType(actorType); 481 if (!actorType._actor) { 482 throw Error( 483 `Details only apply to actor types, tried to add detail '${detail}' ` + 484 `to ${actorType.name}` 485 ); 486 } 487 return types.addType(name, { 488 _actor: true, 489 category: "detail", 490 read: (v, ctx) => actorType.read(v, ctx, detail), 491 write: (v, ctx) => actorType.write(v, ctx, detail), 492 }); 493 }; 494 495 // Add a few named primitive types. 496 types.Primitive = types.addType("primitive"); 497 types.String = types.addType("string"); 498 types.Number = types.addType("number"); 499 types.Boolean = types.addType("boolean"); 500 types.JSON = types.addType("json"); 501 502 exports.registerFront = function (cls) { 503 const { typeName } = cls.prototype; 504 if (!registeredTypes.has(typeName)) { 505 types.addActorType(typeName); 506 } 507 registeredTypes.get(typeName).frontClass = cls; 508 }; 509 510 /** 511 * Instantiate a front of the given type. 512 * 513 * @param DevToolsClient client 514 * The DevToolsClient instance to use. 515 * @param string typeName 516 * The type name of the front to instantiate. This is defined in its specifiation. 517 * @returns Front 518 * The created front. 519 */ 520 function createFront(client, typeName, target = null) { 521 const type = types.getType(typeName); 522 if (!type) { 523 throw new Error(`No spec for front type '${typeName}'.`); 524 } else if (!type.frontClass) { 525 lazyLoadFront(typeName); 526 } 527 528 // Use intermediate Class variable to please eslint requiring 529 // a capital letter for all constructors. 530 const Class = type.frontClass; 531 return new Class(client, target, target); 532 } 533 534 /** 535 * Instantiate a global (preference, device) or target-scoped (webconsole, inspector) 536 * front of the given type by picking its actor ID out of either the target or root 537 * front's form. 538 * 539 * @param DevToolsClient client 540 * The DevToolsClient instance to use. 541 * @param string typeName 542 * The type name of the front to instantiate. This is defined in its specifiation. 543 * @param json form 544 * If we want to instantiate a global actor's front, this is the root front's form, 545 * otherwise we are instantiating a target-scoped front from the target front's form. 546 * @param [Target|null] target 547 * If we are instantiating a target-scoped front, this is a reference to the front's 548 * Target instance, otherwise this is null. 549 */ 550 async function getFront(client, typeName, form, target = null) { 551 const front = createFront(client, typeName, target); 552 const { formAttributeName } = front; 553 if (!formAttributeName) { 554 throw new Error(`Can't find the form attribute name for ${typeName}`); 555 } 556 // Retrieve the actor ID from root or target actor's form 557 front.actorID = form[formAttributeName]; 558 if (!front.actorID) { 559 throw new Error( 560 `Can't find the actor ID for ${typeName} from root or target` + 561 ` actor's form.` 562 ); 563 } 564 565 if (!target) { 566 await front.manage(front); 567 } else { 568 await target.manage(front); 569 } 570 571 return front; 572 } 573 exports.getFront = getFront; 574 575 /** 576 * Create a RootFront. 577 * 578 * @param DevToolsClient client 579 * The DevToolsClient instance to use. 580 * @param Object packet 581 * @returns RootFront 582 */ 583 function createRootFront(client, packet) { 584 const rootFront = createFront(client, "root"); 585 rootFront.form(packet); 586 587 // Root Front is a special case, managing itself as it doesn't have any parent. 588 // It will register itself to DevToolsClient as a Pool via Front._poolMap. 589 rootFront.manage(rootFront); 590 591 return rootFront; 592 } 593 exports.createRootFront = createRootFront;