toolkit/devtools/server/protocol.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 "use strict";
michael@0 6
michael@0 7 let {Cu} = require("chrome");
michael@0 8 let Services = require("Services");
michael@0 9 let promise = require("devtools/toolkit/deprecated-sync-thenables");
michael@0 10 let {Class} = require("sdk/core/heritage");
michael@0 11 let {EventTarget} = require("sdk/event/target");
michael@0 12 let events = require("sdk/event/core");
michael@0 13 let object = require("sdk/util/object");
michael@0 14
michael@0 15 // Waiting for promise.done() to be added, see bug 851321
michael@0 16 function promiseDone(err) {
michael@0 17 console.error(err);
michael@0 18 return promise.reject(err);
michael@0 19 }
michael@0 20
michael@0 21 /**
michael@0 22 * Types: named marshallers/demarshallers.
michael@0 23 *
michael@0 24 * Types provide a 'write' function that takes a js representation and
michael@0 25 * returns a protocol representation, and a "read" function that
michael@0 26 * takes a protocol representation and returns a js representation.
michael@0 27 *
michael@0 28 * The read and write methods are also passed a context object that
michael@0 29 * represent the actor or front requesting the translation.
michael@0 30 *
michael@0 31 * Types are referred to with a typestring. Basic types are
michael@0 32 * registered by name using addType, and more complex types can
michael@0 33 * be generated by adding detail to the type name.
michael@0 34 */
michael@0 35
michael@0 36 let types = Object.create(null);
michael@0 37 exports.types = types;
michael@0 38
michael@0 39 let registeredTypes = new Map();
michael@0 40 let registeredLifetimes = new Map();
michael@0 41
michael@0 42 /**
michael@0 43 * Return the type object associated with a given typestring.
michael@0 44 * If passed a type object, it will be returned unchanged.
michael@0 45 *
michael@0 46 * Types can be registered with addType, or can be created on
michael@0 47 * the fly with typestrings. Examples:
michael@0 48 *
michael@0 49 * boolean
michael@0 50 * threadActor
michael@0 51 * threadActor#detail
michael@0 52 * array:threadActor
michael@0 53 * array:array:threadActor#detail
michael@0 54 *
michael@0 55 * @param [typestring|type] type
michael@0 56 * Either a typestring naming a type or a type object.
michael@0 57 *
michael@0 58 * @returns a type object.
michael@0 59 */
michael@0 60 types.getType = function(type) {
michael@0 61 if (!type) {
michael@0 62 return types.Primitive;
michael@0 63 }
michael@0 64
michael@0 65 if (typeof(type) !== "string") {
michael@0 66 return type;
michael@0 67 }
michael@0 68
michael@0 69 // If already registered, we're done here.
michael@0 70 let reg = registeredTypes.get(type);
michael@0 71 if (reg) return reg;
michael@0 72
michael@0 73 // New type, see if it's a collection/lifetime type:
michael@0 74 let sep = type.indexOf(":");
michael@0 75 if (sep >= 0) {
michael@0 76 let collection = type.substring(0, sep);
michael@0 77 let subtype = types.getType(type.substring(sep + 1));
michael@0 78
michael@0 79 if (collection === "array") {
michael@0 80 return types.addArrayType(subtype);
michael@0 81 } else if (collection === "nullable") {
michael@0 82 return types.addNullableType(subtype);
michael@0 83 }
michael@0 84
michael@0 85 if (registeredLifetimes.has(collection)) {
michael@0 86 return types.addLifetimeType(collection, subtype);
michael@0 87 }
michael@0 88
michael@0 89 throw Error("Unknown collection type: " + collection);
michael@0 90 }
michael@0 91
michael@0 92 // Not a collection, might be actor detail
michael@0 93 let pieces = type.split("#", 2);
michael@0 94 if (pieces.length > 1) {
michael@0 95 return types.addActorDetail(type, pieces[0], pieces[1]);
michael@0 96 }
michael@0 97
michael@0 98 // Might be a lazily-loaded type
michael@0 99 if (type === "longstring") {
michael@0 100 require("devtools/server/actors/string");
michael@0 101 return registeredTypes.get("longstring");
michael@0 102 }
michael@0 103
michael@0 104 throw Error("Unknown type: " + type);
michael@0 105 }
michael@0 106
michael@0 107 /**
michael@0 108 * Don't allow undefined when writing primitive types to packets. If
michael@0 109 * you want to allow undefined, use a nullable type.
michael@0 110 */
michael@0 111 function identityWrite(v) {
michael@0 112 if (v === undefined) {
michael@0 113 throw Error("undefined passed where a value is required");
michael@0 114 }
michael@0 115 return v;
michael@0 116 }
michael@0 117
michael@0 118 /**
michael@0 119 * Add a type to the type system.
michael@0 120 *
michael@0 121 * When registering a type, you can provide `read` and `write` methods.
michael@0 122 *
michael@0 123 * The `read` method will be passed a JS object value from the JSON
michael@0 124 * packet and must return a native representation. The `write` method will
michael@0 125 * be passed a native representation and should provide a JSONable value.
michael@0 126 *
michael@0 127 * These methods will both be passed a context. The context is the object
michael@0 128 * performing or servicing the request - on the server side it will be
michael@0 129 * an Actor, on the client side it will be a Front.
michael@0 130 *
michael@0 131 * @param typestring name
michael@0 132 * Name to register
michael@0 133 * @param object typeObject
michael@0 134 * An object whose properties will be stored in the type, including
michael@0 135 * the `read` and `write` methods.
michael@0 136 * @param object options
michael@0 137 * Can specify `thawed` to prevent the type from being frozen.
michael@0 138 *
michael@0 139 * @returns a type object that can be used in protocol definitions.
michael@0 140 */
michael@0 141 types.addType = function(name, typeObject={}, options={}) {
michael@0 142 if (registeredTypes.has(name)) {
michael@0 143 throw Error("Type '" + name + "' already exists.");
michael@0 144 }
michael@0 145
michael@0 146 let type = object.merge({
michael@0 147 name: name,
michael@0 148 primitive: !(typeObject.read || typeObject.write),
michael@0 149 read: identityWrite,
michael@0 150 write: identityWrite
michael@0 151 }, typeObject);
michael@0 152
michael@0 153 registeredTypes.set(name, type);
michael@0 154
michael@0 155 if (!options.thawed) {
michael@0 156 Object.freeze(type);
michael@0 157 }
michael@0 158
michael@0 159 return type;
michael@0 160 };
michael@0 161
michael@0 162 /**
michael@0 163 * Add an array type to the type system.
michael@0 164 *
michael@0 165 * getType() will call this function if provided an "array:<type>"
michael@0 166 * typestring.
michael@0 167 *
michael@0 168 * @param type subtype
michael@0 169 * The subtype to be held by the array.
michael@0 170 */
michael@0 171 types.addArrayType = function(subtype) {
michael@0 172 subtype = types.getType(subtype);
michael@0 173
michael@0 174 let name = "array:" + subtype.name;
michael@0 175
michael@0 176 // Arrays of primitive types are primitive types themselves.
michael@0 177 if (subtype.primitive) {
michael@0 178 return types.addType(name);
michael@0 179 }
michael@0 180 return types.addType(name, {
michael@0 181 category: "array",
michael@0 182 read: (v, ctx) => [subtype.read(i, ctx) for (i of v)],
michael@0 183 write: (v, ctx) => [subtype.write(i, ctx) for (i of v)]
michael@0 184 });
michael@0 185 };
michael@0 186
michael@0 187 /**
michael@0 188 * Add a dict type to the type system. This allows you to serialize
michael@0 189 * a JS object that contains non-primitive subtypes.
michael@0 190 *
michael@0 191 * Properties of the value that aren't included in the specializations
michael@0 192 * will be serialized as primitive values.
michael@0 193 *
michael@0 194 * @param object specializations
michael@0 195 * A dict of property names => type
michael@0 196 */
michael@0 197 types.addDictType = function(name, specializations) {
michael@0 198 return types.addType(name, {
michael@0 199 category: "dict",
michael@0 200 specializations: specializations,
michael@0 201 read: (v, ctx) => {
michael@0 202 let ret = {};
michael@0 203 for (let prop in v) {
michael@0 204 if (prop in specializations) {
michael@0 205 ret[prop] = types.getType(specializations[prop]).read(v[prop], ctx);
michael@0 206 } else {
michael@0 207 ret[prop] = v[prop];
michael@0 208 }
michael@0 209 }
michael@0 210 return ret;
michael@0 211 },
michael@0 212
michael@0 213 write: (v, ctx) => {
michael@0 214 let ret = {};
michael@0 215 for (let prop in v) {
michael@0 216 if (prop in specializations) {
michael@0 217 ret[prop] = types.getType(specializations[prop]).write(v[prop], ctx);
michael@0 218 } else {
michael@0 219 ret[prop] = v[prop];
michael@0 220 }
michael@0 221 }
michael@0 222 return ret;
michael@0 223 }
michael@0 224 })
michael@0 225 }
michael@0 226
michael@0 227 /**
michael@0 228 * Register an actor type with the type system.
michael@0 229 *
michael@0 230 * Types are marshalled differently when communicating server->client
michael@0 231 * than they are when communicating client->server. The server needs
michael@0 232 * to provide useful information to the client, so uses the actor's
michael@0 233 * `form` method to get a json representation of the actor. When
michael@0 234 * making a request from the client we only need the actor ID string.
michael@0 235 *
michael@0 236 * This function can be called before the associated actor has been
michael@0 237 * constructed, but the read and write methods won't work until
michael@0 238 * the associated addActorImpl or addActorFront methods have been
michael@0 239 * called during actor/front construction.
michael@0 240 *
michael@0 241 * @param string name
michael@0 242 * The typestring to register.
michael@0 243 */
michael@0 244 types.addActorType = function(name) {
michael@0 245 let type = types.addType(name, {
michael@0 246 _actor: true,
michael@0 247 category: "actor",
michael@0 248 read: (v, ctx, detail) => {
michael@0 249 // If we're reading a request on the server side, just
michael@0 250 // find the actor registered with this actorID.
michael@0 251 if (ctx instanceof Actor) {
michael@0 252 return ctx.conn.getActor(v);
michael@0 253 }
michael@0 254
michael@0 255 // Reading a response on the client side, check for an
michael@0 256 // existing front on the connection, and create the front
michael@0 257 // if it isn't found.
michael@0 258 let actorID = typeof(v) === "string" ? v : v.actor;
michael@0 259 let front = ctx.conn.getActor(actorID);
michael@0 260 if (front) {
michael@0 261 front.form(v, detail, ctx);
michael@0 262 } else {
michael@0 263 front = new type.frontClass(ctx.conn, v, detail, ctx)
michael@0 264 front.actorID = actorID;
michael@0 265 ctx.marshallPool().manage(front);
michael@0 266 }
michael@0 267 return front;
michael@0 268 },
michael@0 269 write: (v, ctx, detail) => {
michael@0 270 // If returning a response from the server side, make sure
michael@0 271 // the actor is added to a parent object and return its form.
michael@0 272 if (v instanceof Actor) {
michael@0 273 if (!v.actorID) {
michael@0 274 ctx.marshallPool().manage(v);
michael@0 275 }
michael@0 276 return v.form(detail);
michael@0 277 }
michael@0 278
michael@0 279 // Writing a request from the client side, just send the actor id.
michael@0 280 return v.actorID;
michael@0 281 },
michael@0 282 }, {
michael@0 283 // We usually freeze types, but actor types are updated when clients are
michael@0 284 // created, so don't freeze yet.
michael@0 285 thawed: true
michael@0 286 });
michael@0 287 return type;
michael@0 288 }
michael@0 289
michael@0 290 types.addNullableType = function(subtype) {
michael@0 291 subtype = types.getType(subtype);
michael@0 292 return types.addType("nullable:" + subtype.name, {
michael@0 293 category: "nullable",
michael@0 294 read: (value, ctx) => {
michael@0 295 if (value == null) {
michael@0 296 return value;
michael@0 297 }
michael@0 298 return subtype.read(value, ctx);
michael@0 299 },
michael@0 300 write: (value, ctx) => {
michael@0 301 if (value == null) {
michael@0 302 return value;
michael@0 303 }
michael@0 304 return subtype.write(value, ctx);
michael@0 305 }
michael@0 306 });
michael@0 307 }
michael@0 308
michael@0 309 /**
michael@0 310 * Register an actor detail type. This is just like an actor type, but
michael@0 311 * will pass a detail hint to the actor's form method during serialization/
michael@0 312 * deserialization.
michael@0 313 *
michael@0 314 * This is called by getType() when passed an 'actorType#detail' string.
michael@0 315 *
michael@0 316 * @param string name
michael@0 317 * The typestring to register this type as.
michael@0 318 * @param type actorType
michael@0 319 * The actor type you'll be detailing.
michael@0 320 * @param string detail
michael@0 321 * The detail to pass.
michael@0 322 */
michael@0 323 types.addActorDetail = function(name, actorType, detail) {
michael@0 324 actorType = types.getType(actorType);
michael@0 325 if (!actorType._actor) {
michael@0 326 throw Error("Details only apply to actor types, tried to add detail '" + detail + "'' to " + actorType.name + "\n");
michael@0 327 }
michael@0 328 return types.addType(name, {
michael@0 329 _actor: true,
michael@0 330 category: "detail",
michael@0 331 read: (v, ctx) => actorType.read(v, ctx, detail),
michael@0 332 write: (v, ctx) => actorType.write(v, ctx, detail)
michael@0 333 });
michael@0 334 }
michael@0 335
michael@0 336 /**
michael@0 337 * Register an actor lifetime. This lets the type system find a parent
michael@0 338 * actor that differs from the actor fulfilling the request.
michael@0 339 *
michael@0 340 * @param string name
michael@0 341 * The lifetime name to use in typestrings.
michael@0 342 * @param string prop
michael@0 343 * The property of the actor that holds the parent that should be used.
michael@0 344 */
michael@0 345 types.addLifetime = function(name, prop) {
michael@0 346 if (registeredLifetimes.has(name)) {
michael@0 347 throw Error("Lifetime '" + name + "' already registered.");
michael@0 348 }
michael@0 349 registeredLifetimes.set(name, prop);
michael@0 350 }
michael@0 351
michael@0 352 /**
michael@0 353 * Register a lifetime type. This creates an actor type tied to the given
michael@0 354 * lifetime.
michael@0 355 *
michael@0 356 * This is called by getType() when passed a '<lifetimeType>:<actorType>'
michael@0 357 * typestring.
michael@0 358 *
michael@0 359 * @param string lifetime
michael@0 360 * A lifetime string previously regisered with addLifetime()
michael@0 361 * @param type subtype
michael@0 362 * An actor type
michael@0 363 */
michael@0 364 types.addLifetimeType = function(lifetime, subtype) {
michael@0 365 subtype = types.getType(subtype);
michael@0 366 if (!subtype._actor) {
michael@0 367 throw Error("Lifetimes only apply to actor types, tried to apply lifetime '" + lifetime + "'' to " + subtype.name);
michael@0 368 }
michael@0 369 let prop = registeredLifetimes.get(lifetime);
michael@0 370 return types.addType(lifetime + ":" + subtype.name, {
michael@0 371 category: "lifetime",
michael@0 372 read: (value, ctx) => subtype.read(value, ctx[prop]),
michael@0 373 write: (value, ctx) => subtype.write(value, ctx[prop])
michael@0 374 })
michael@0 375 }
michael@0 376
michael@0 377 // Add a few named primitive types.
michael@0 378 types.Primitive = types.addType("primitive");
michael@0 379 types.String = types.addType("string");
michael@0 380 types.Number = types.addType("number");
michael@0 381 types.Boolean = types.addType("boolean");
michael@0 382 types.JSON = types.addType("json");
michael@0 383
michael@0 384 /**
michael@0 385 * Request/Response templates and generation
michael@0 386 *
michael@0 387 * Request packets are specified as json templates with
michael@0 388 * Arg and Option placeholders where arguments should be
michael@0 389 * placed.
michael@0 390 *
michael@0 391 * Reponse packets are also specified as json templates,
michael@0 392 * with a RetVal placeholder where the return value should be
michael@0 393 * placed.
michael@0 394 */
michael@0 395
michael@0 396 /**
michael@0 397 * Placeholder for simple arguments.
michael@0 398 *
michael@0 399 * @param number index
michael@0 400 * The argument index to place at this position.
michael@0 401 * @param type type
michael@0 402 * The argument should be marshalled as this type.
michael@0 403 * @constructor
michael@0 404 */
michael@0 405 let Arg = Class({
michael@0 406 initialize: function(index, type) {
michael@0 407 this.index = index;
michael@0 408 this.type = types.getType(type);
michael@0 409 },
michael@0 410
michael@0 411 write: function(arg, ctx) {
michael@0 412 return this.type.write(arg, ctx);
michael@0 413 },
michael@0 414
michael@0 415 read: function(v, ctx, outArgs) {
michael@0 416 outArgs[this.index] = this.type.read(v, ctx);
michael@0 417 },
michael@0 418
michael@0 419 describe: function() {
michael@0 420 return {
michael@0 421 _arg: this.index,
michael@0 422 type: this.type.name,
michael@0 423 }
michael@0 424 }
michael@0 425 });
michael@0 426 exports.Arg = Arg;
michael@0 427
michael@0 428 /**
michael@0 429 * Placeholder for an options argument value that should be hoisted
michael@0 430 * into the packet.
michael@0 431 *
michael@0 432 * If provided in a method specification:
michael@0 433 *
michael@0 434 * { optionArg: Option(1)}
michael@0 435 *
michael@0 436 * Then arguments[1].optionArg will be placed in the packet in this
michael@0 437 * value's place.
michael@0 438 *
michael@0 439 * @param number index
michael@0 440 * The argument index of the options value.
michael@0 441 * @param type type
michael@0 442 * The argument should be marshalled as this type.
michael@0 443 * @constructor
michael@0 444 */
michael@0 445 let Option = Class({
michael@0 446 extends: Arg,
michael@0 447 initialize: function(index, type) {
michael@0 448 Arg.prototype.initialize.call(this, index, type)
michael@0 449 },
michael@0 450
michael@0 451 write: function(arg, ctx, name) {
michael@0 452 if (!arg) {
michael@0 453 return undefined;
michael@0 454 }
michael@0 455 let v = arg[name] || undefined;
michael@0 456 if (v === undefined) {
michael@0 457 return undefined;
michael@0 458 }
michael@0 459 return this.type.write(v, ctx);
michael@0 460 },
michael@0 461 read: function(v, ctx, outArgs, name) {
michael@0 462 if (outArgs[this.index] === undefined) {
michael@0 463 outArgs[this.index] = {};
michael@0 464 }
michael@0 465 if (v === undefined) {
michael@0 466 return;
michael@0 467 }
michael@0 468 outArgs[this.index][name] = this.type.read(v, ctx);
michael@0 469 },
michael@0 470
michael@0 471 describe: function() {
michael@0 472 return {
michael@0 473 _option: this.index,
michael@0 474 type: this.type.name,
michael@0 475 }
michael@0 476 }
michael@0 477 });
michael@0 478
michael@0 479 exports.Option = Option;
michael@0 480
michael@0 481 /**
michael@0 482 * Placeholder for return values in a response template.
michael@0 483 *
michael@0 484 * @param type type
michael@0 485 * The return value should be marshalled as this type.
michael@0 486 */
michael@0 487 let RetVal = Class({
michael@0 488 initialize: function(type) {
michael@0 489 this.type = types.getType(type);
michael@0 490 },
michael@0 491
michael@0 492 write: function(v, ctx) {
michael@0 493 return this.type.write(v, ctx);
michael@0 494 },
michael@0 495
michael@0 496 read: function(v, ctx) {
michael@0 497 return this.type.read(v, ctx);
michael@0 498 },
michael@0 499
michael@0 500 describe: function() {
michael@0 501 return {
michael@0 502 _retval: this.type.name
michael@0 503 }
michael@0 504 }
michael@0 505 });
michael@0 506
michael@0 507 exports.RetVal = RetVal;
michael@0 508
michael@0 509 /* Template handling functions */
michael@0 510
michael@0 511 /**
michael@0 512 * Get the value at a given path, or undefined if not found.
michael@0 513 */
michael@0 514 function getPath(obj, path) {
michael@0 515 for (let name of path) {
michael@0 516 if (!(name in obj)) {
michael@0 517 return undefined;
michael@0 518 }
michael@0 519 obj = obj[name];
michael@0 520 }
michael@0 521 return obj;
michael@0 522 }
michael@0 523
michael@0 524 /**
michael@0 525 * Find Placeholders in the template and save them along with their
michael@0 526 * paths.
michael@0 527 */
michael@0 528 function findPlaceholders(template, constructor, path=[], placeholders=[]) {
michael@0 529 if (!template || typeof(template) != "object") {
michael@0 530 return placeholders;
michael@0 531 }
michael@0 532
michael@0 533 if (template instanceof constructor) {
michael@0 534 placeholders.push({ placeholder: template, path: [p for (p of path)] });
michael@0 535 return placeholders;
michael@0 536 }
michael@0 537
michael@0 538 for (let name in template) {
michael@0 539 path.push(name);
michael@0 540 findPlaceholders(template[name], constructor, path, placeholders);
michael@0 541 path.pop();
michael@0 542 }
michael@0 543
michael@0 544 return placeholders;
michael@0 545 }
michael@0 546
michael@0 547
michael@0 548 function describeTemplate(template) {
michael@0 549 return JSON.parse(JSON.stringify(template, (key, value) => {
michael@0 550 if (value.describe) {
michael@0 551 return value.describe();
michael@0 552 }
michael@0 553 return value;
michael@0 554 }));
michael@0 555 }
michael@0 556
michael@0 557 /**
michael@0 558 * Manages a request template.
michael@0 559 *
michael@0 560 * @param object template
michael@0 561 * The request template.
michael@0 562 * @construcor
michael@0 563 */
michael@0 564 let Request = Class({
michael@0 565 initialize: function(template={}) {
michael@0 566 this.type = template.type;
michael@0 567 this.template = template;
michael@0 568 this.args = findPlaceholders(template, Arg);
michael@0 569 },
michael@0 570
michael@0 571 /**
michael@0 572 * Write a request.
michael@0 573 *
michael@0 574 * @param array fnArgs
michael@0 575 * The function arguments to place in the request.
michael@0 576 * @param object ctx
michael@0 577 * The object making the request.
michael@0 578 * @returns a request packet.
michael@0 579 */
michael@0 580 write: function(fnArgs, ctx) {
michael@0 581 let str = JSON.stringify(this.template, (key, value) => {
michael@0 582 if (value instanceof Arg) {
michael@0 583 return value.write(fnArgs[value.index], ctx, key);
michael@0 584 }
michael@0 585 return value;
michael@0 586 });
michael@0 587 return JSON.parse(str);
michael@0 588 },
michael@0 589
michael@0 590 /**
michael@0 591 * Read a request.
michael@0 592 *
michael@0 593 * @param object packet
michael@0 594 * The request packet.
michael@0 595 * @param object ctx
michael@0 596 * The object making the request.
michael@0 597 * @returns an arguments array
michael@0 598 */
michael@0 599 read: function(packet, ctx) {
michael@0 600 let fnArgs = [];
michael@0 601 for (let templateArg of this.args) {
michael@0 602 let arg = templateArg.placeholder;
michael@0 603 let path = templateArg.path;
michael@0 604 let name = path[path.length - 1];
michael@0 605 arg.read(getPath(packet, path), ctx, fnArgs, name);
michael@0 606 }
michael@0 607 return fnArgs;
michael@0 608 },
michael@0 609
michael@0 610 describe: function() { return describeTemplate(this.template); }
michael@0 611 });
michael@0 612
michael@0 613 /**
michael@0 614 * Manages a response template.
michael@0 615 *
michael@0 616 * @param object template
michael@0 617 * The response template.
michael@0 618 * @construcor
michael@0 619 */
michael@0 620 let Response = Class({
michael@0 621 initialize: function(template={}) {
michael@0 622 this.template = template;
michael@0 623 let placeholders = findPlaceholders(template, RetVal);
michael@0 624 if (placeholders.length > 1) {
michael@0 625 throw Error("More than one RetVal specified in response");
michael@0 626 }
michael@0 627 let placeholder = placeholders.shift();
michael@0 628 if (placeholder) {
michael@0 629 this.retVal = placeholder.placeholder;
michael@0 630 this.path = placeholder.path;
michael@0 631 }
michael@0 632 },
michael@0 633
michael@0 634 /**
michael@0 635 * Write a response for the given return value.
michael@0 636 *
michael@0 637 * @param val ret
michael@0 638 * The return value.
michael@0 639 * @param object ctx
michael@0 640 * The object writing the response.
michael@0 641 */
michael@0 642 write: function(ret, ctx) {
michael@0 643 return JSON.parse(JSON.stringify(this.template, function(key, value) {
michael@0 644 if (value instanceof RetVal) {
michael@0 645 return value.write(ret, ctx);
michael@0 646 }
michael@0 647 return value;
michael@0 648 }));
michael@0 649 },
michael@0 650
michael@0 651 /**
michael@0 652 * Read a return value from the given response.
michael@0 653 *
michael@0 654 * @param object packet
michael@0 655 * The response packet.
michael@0 656 * @param object ctx
michael@0 657 * The object reading the response.
michael@0 658 */
michael@0 659 read: function(packet, ctx) {
michael@0 660 if (!this.retVal) {
michael@0 661 return undefined;
michael@0 662 }
michael@0 663 let v = getPath(packet, this.path);
michael@0 664 return this.retVal.read(v, ctx);
michael@0 665 },
michael@0 666
michael@0 667 describe: function() { return describeTemplate(this.template); }
michael@0 668 });
michael@0 669
michael@0 670 /**
michael@0 671 * Actor and Front implementations
michael@0 672 */
michael@0 673
michael@0 674 /**
michael@0 675 * A protocol object that can manage the lifetime of other protocol
michael@0 676 * objects.
michael@0 677 */
michael@0 678 let Pool = Class({
michael@0 679 extends: EventTarget,
michael@0 680
michael@0 681 /**
michael@0 682 * Pools are used on both sides of the connection to help coordinate
michael@0 683 * lifetimes.
michael@0 684 *
michael@0 685 * @param optional conn
michael@0 686 * Either a DebuggerServerConnection or a DebuggerClient. Must have
michael@0 687 * addActorPool, removeActorPool, and poolFor.
michael@0 688 * conn can be null if the subclass provides a conn property.
michael@0 689 * @constructor
michael@0 690 */
michael@0 691 initialize: function(conn) {
michael@0 692 if (conn) {
michael@0 693 this.conn = conn;
michael@0 694 }
michael@0 695 },
michael@0 696
michael@0 697 /**
michael@0 698 * Return the parent pool for this client.
michael@0 699 */
michael@0 700 parent: function() { return this.conn.poolFor(this.actorID) },
michael@0 701
michael@0 702 /**
michael@0 703 * Override this if you want actors returned by this actor
michael@0 704 * to belong to a different actor by default.
michael@0 705 */
michael@0 706 marshallPool: function() { return this; },
michael@0 707
michael@0 708 /**
michael@0 709 * Pool is the base class for all actors, even leaf nodes.
michael@0 710 * If the child map is actually referenced, go ahead and create
michael@0 711 * the stuff needed by the pool.
michael@0 712 */
michael@0 713 __poolMap: null,
michael@0 714 get _poolMap() {
michael@0 715 if (this.__poolMap) return this.__poolMap;
michael@0 716 this.__poolMap = new Map();
michael@0 717 this.conn.addActorPool(this);
michael@0 718 return this.__poolMap;
michael@0 719 },
michael@0 720
michael@0 721 /**
michael@0 722 * Add an actor as a child of this pool.
michael@0 723 */
michael@0 724 manage: function(actor) {
michael@0 725 if (!actor.actorID) {
michael@0 726 actor.actorID = this.conn.allocID(actor.actorPrefix || actor.typeName);
michael@0 727 }
michael@0 728
michael@0 729 this._poolMap.set(actor.actorID, actor);
michael@0 730 return actor;
michael@0 731 },
michael@0 732
michael@0 733 /**
michael@0 734 * Remove an actor as a child of this pool.
michael@0 735 */
michael@0 736 unmanage: function(actor) {
michael@0 737 this.__poolMap.delete(actor.actorID);
michael@0 738 },
michael@0 739
michael@0 740 // true if the given actor ID exists in the pool.
michael@0 741 has: function(actorID) this.__poolMap && this._poolMap.has(actorID),
michael@0 742
michael@0 743 // The actor for a given actor id stored in this pool
michael@0 744 actor: function(actorID) this.__poolMap ? this._poolMap.get(actorID) : null,
michael@0 745
michael@0 746 // Same as actor, should update debugger connection to use 'actor'
michael@0 747 // and then remove this.
michael@0 748 get: function(actorID) this.__poolMap ? this._poolMap.get(actorID) : null,
michael@0 749
michael@0 750 // True if this pool has no children.
michael@0 751 isEmpty: function() !this.__poolMap || this._poolMap.size == 0,
michael@0 752
michael@0 753 /**
michael@0 754 * Destroy this item, removing it from a parent if it has one,
michael@0 755 * and destroying all children if necessary.
michael@0 756 */
michael@0 757 destroy: function() {
michael@0 758 let parent = this.parent();
michael@0 759 if (parent) {
michael@0 760 parent.unmanage(this);
michael@0 761 }
michael@0 762 if (!this.__poolMap) {
michael@0 763 return;
michael@0 764 }
michael@0 765 for (let actor of this.__poolMap.values()) {
michael@0 766 // Self-owned actors are ok, but don't need destroying twice.
michael@0 767 if (actor === this) {
michael@0 768 continue;
michael@0 769 }
michael@0 770 let destroy = actor.destroy;
michael@0 771 if (destroy) {
michael@0 772 // Disconnect destroy while we're destroying in case of (misbehaving)
michael@0 773 // circular ownership.
michael@0 774 actor.destroy = null;
michael@0 775 destroy.call(actor);
michael@0 776 actor.destroy = destroy;
michael@0 777 }
michael@0 778 };
michael@0 779 this.conn.removeActorPool(this, true);
michael@0 780 this.__poolMap.clear();
michael@0 781 this.__poolMap = null;
michael@0 782 },
michael@0 783
michael@0 784 /**
michael@0 785 * For getting along with the debugger server pools, should be removable
michael@0 786 * eventually.
michael@0 787 */
michael@0 788 cleanup: function() {
michael@0 789 this.destroy();
michael@0 790 }
michael@0 791 });
michael@0 792 exports.Pool = Pool;
michael@0 793
michael@0 794 /**
michael@0 795 * An actor in the actor tree.
michael@0 796 */
michael@0 797 let Actor = Class({
michael@0 798 extends: Pool,
michael@0 799
michael@0 800 // Will contain the actor's ID
michael@0 801 actorID: null,
michael@0 802
michael@0 803 /**
michael@0 804 * Initialize an actor.
michael@0 805 *
michael@0 806 * @param optional conn
michael@0 807 * Either a DebuggerServerConnection or a DebuggerClient. Must have
michael@0 808 * addActorPool, removeActorPool, and poolFor.
michael@0 809 * conn can be null if the subclass provides a conn property.
michael@0 810 * @constructor
michael@0 811 */
michael@0 812 initialize: function(conn) {
michael@0 813 Pool.prototype.initialize.call(this, conn);
michael@0 814
michael@0 815 // Forward events to the connection.
michael@0 816 if (this._actorSpec && this._actorSpec.events) {
michael@0 817 for (let key of this._actorSpec.events.keys()) {
michael@0 818 let name = key;
michael@0 819 let sendEvent = this._sendEvent.bind(this, name)
michael@0 820 this.on(name, (...args) => {
michael@0 821 sendEvent.apply(null, args);
michael@0 822 });
michael@0 823 }
michael@0 824 }
michael@0 825 },
michael@0 826
michael@0 827 _sendEvent: function(name, ...args) {
michael@0 828 if (!this._actorSpec.events.has(name)) {
michael@0 829 // It's ok to emit events that don't go over the wire.
michael@0 830 return;
michael@0 831 }
michael@0 832 let request = this._actorSpec.events.get(name);
michael@0 833 let packet = request.write(args, this);
michael@0 834 packet.from = packet.from || this.actorID;
michael@0 835 this.conn.send(packet);
michael@0 836 },
michael@0 837
michael@0 838 destroy: function() {
michael@0 839 Pool.prototype.destroy.call(this);
michael@0 840 this.actorID = null;
michael@0 841 },
michael@0 842
michael@0 843 /**
michael@0 844 * Override this method in subclasses to serialize the actor.
michael@0 845 * @param [optional] string hint
michael@0 846 * Optional string to customize the form.
michael@0 847 * @returns A jsonable object.
michael@0 848 */
michael@0 849 form: function(hint) {
michael@0 850 return { actor: this.actorID }
michael@0 851 },
michael@0 852
michael@0 853 writeError: function(err) {
michael@0 854 console.error(err);
michael@0 855 if (err.stack) {
michael@0 856 dump(err.stack);
michael@0 857 }
michael@0 858 this.conn.send({
michael@0 859 from: this.actorID,
michael@0 860 error: "unknownError",
michael@0 861 message: err.toString()
michael@0 862 });
michael@0 863 },
michael@0 864
michael@0 865 _queueResponse: function(create) {
michael@0 866 let pending = this._pendingResponse || promise.resolve(null);
michael@0 867 let response = create(pending);
michael@0 868 this._pendingResponse = response;
michael@0 869 }
michael@0 870 });
michael@0 871 exports.Actor = Actor;
michael@0 872
michael@0 873 /**
michael@0 874 * Tags a prtotype method as an actor method implementation.
michael@0 875 *
michael@0 876 * @param function fn
michael@0 877 * The implementation function, will be returned.
michael@0 878 * @param spec
michael@0 879 * The method specification, with the following (optional) properties:
michael@0 880 * request (object): a request template.
michael@0 881 * response (object): a response template.
michael@0 882 * oneway (bool): 'true' if no response should be sent.
michael@0 883 * telemetry (string): Telemetry probe ID for measuring completion time.
michael@0 884 */
michael@0 885 exports.method = function(fn, spec={}) {
michael@0 886 fn._methodSpec = Object.freeze(spec);
michael@0 887 if (spec.request) Object.freeze(spec.request);
michael@0 888 if (spec.response) Object.freeze(spec.response);
michael@0 889 return fn;
michael@0 890 }
michael@0 891
michael@0 892 /**
michael@0 893 * Process an actor definition from its prototype and generate
michael@0 894 * request handlers.
michael@0 895 */
michael@0 896 let actorProto = function(actorProto) {
michael@0 897 if (actorProto._actorSpec) {
michael@0 898 throw new Error("actorProto called twice on the same actor prototype!");
michael@0 899 }
michael@0 900
michael@0 901 let protoSpec = {
michael@0 902 methods: [],
michael@0 903 };
michael@0 904
michael@0 905 // Find method specifications attached to prototype properties.
michael@0 906 for (let name of Object.getOwnPropertyNames(actorProto)) {
michael@0 907 let desc = Object.getOwnPropertyDescriptor(actorProto, name);
michael@0 908 if (!desc.value) {
michael@0 909 continue;
michael@0 910 }
michael@0 911
michael@0 912 if (desc.value._methodSpec) {
michael@0 913 let frozenSpec = desc.value._methodSpec;
michael@0 914 let spec = {};
michael@0 915 spec.name = frozenSpec.name || name;
michael@0 916 spec.request = Request(object.merge({type: spec.name}, frozenSpec.request || undefined));
michael@0 917 spec.response = Response(frozenSpec.response || undefined);
michael@0 918 spec.telemetry = frozenSpec.telemetry;
michael@0 919 spec.release = frozenSpec.release;
michael@0 920 spec.oneway = frozenSpec.oneway;
michael@0 921
michael@0 922 protoSpec.methods.push(spec);
michael@0 923 }
michael@0 924 }
michael@0 925
michael@0 926 // Find event specifications
michael@0 927 if (actorProto.events) {
michael@0 928 protoSpec.events = new Map();
michael@0 929 for (let name in actorProto.events) {
michael@0 930 let eventRequest = actorProto.events[name];
michael@0 931 Object.freeze(eventRequest);
michael@0 932 protoSpec.events.set(name, Request(object.merge({type: name}, eventRequest)));
michael@0 933 }
michael@0 934 }
michael@0 935
michael@0 936 // Generate request handlers for each method definition
michael@0 937 actorProto.requestTypes = Object.create(null);
michael@0 938 protoSpec.methods.forEach(spec => {
michael@0 939 let handler = function(packet, conn) {
michael@0 940 try {
michael@0 941 let args = spec.request.read(packet, this);
michael@0 942
michael@0 943 let ret = this[spec.name].apply(this, args);
michael@0 944
michael@0 945 if (spec.oneway) {
michael@0 946 // No need to send a response.
michael@0 947 return;
michael@0 948 }
michael@0 949
michael@0 950 let sendReturn = (ret) => {
michael@0 951 let response = spec.response.write(ret, this);
michael@0 952 response.from = this.actorID;
michael@0 953 // If spec.release has been specified, destroy the object.
michael@0 954 if (spec.release) {
michael@0 955 try {
michael@0 956 this.destroy();
michael@0 957 } catch(e) {
michael@0 958 this.writeError(e);
michael@0 959 return;
michael@0 960 }
michael@0 961 }
michael@0 962
michael@0 963 conn.send(response);
michael@0 964 };
michael@0 965
michael@0 966 this._queueResponse(p => {
michael@0 967 return p
michael@0 968 .then(() => ret)
michael@0 969 .then(sendReturn)
michael@0 970 .then(null, this.writeError.bind(this));
michael@0 971 })
michael@0 972 } catch(e) {
michael@0 973 this._queueResponse(p => {
michael@0 974 return p.then(() => this.writeError(e));
michael@0 975 });
michael@0 976 }
michael@0 977 };
michael@0 978
michael@0 979 actorProto.requestTypes[spec.request.type] = handler;
michael@0 980 });
michael@0 981
michael@0 982 actorProto._actorSpec = protoSpec;
michael@0 983 return actorProto;
michael@0 984 }
michael@0 985
michael@0 986 /**
michael@0 987 * Create an actor class for the given actor prototype.
michael@0 988 *
michael@0 989 * @param object proto
michael@0 990 * The object prototype. Must have a 'typeName' property,
michael@0 991 * should have method definitions, can have event definitions.
michael@0 992 */
michael@0 993 exports.ActorClass = function(proto) {
michael@0 994 if (!proto.typeName) {
michael@0 995 throw Error("Actor prototype must have a typeName member.");
michael@0 996 }
michael@0 997 proto.extends = Actor;
michael@0 998 if (!registeredTypes.has(proto.typeName)) {
michael@0 999 types.addActorType(proto.typeName);
michael@0 1000 }
michael@0 1001 let cls = Class(actorProto(proto));
michael@0 1002
michael@0 1003 registeredTypes.get(proto.typeName).actorSpec = proto._actorSpec;
michael@0 1004 return cls;
michael@0 1005 };
michael@0 1006
michael@0 1007 /**
michael@0 1008 * Base class for client-side actor fronts.
michael@0 1009 */
michael@0 1010 let Front = Class({
michael@0 1011 extends: Pool,
michael@0 1012
michael@0 1013 actorID: null,
michael@0 1014
michael@0 1015 /**
michael@0 1016 * The base class for client-side actor fronts.
michael@0 1017 *
michael@0 1018 * @param optional conn
michael@0 1019 * Either a DebuggerServerConnection or a DebuggerClient. Must have
michael@0 1020 * addActorPool, removeActorPool, and poolFor.
michael@0 1021 * conn can be null if the subclass provides a conn property.
michael@0 1022 * @param optional form
michael@0 1023 * The json form provided by the server.
michael@0 1024 * @constructor
michael@0 1025 */
michael@0 1026 initialize: function(conn=null, form=null, detail=null, context=null) {
michael@0 1027 Pool.prototype.initialize.call(this, conn);
michael@0 1028 this._requests = [];
michael@0 1029 if (form) {
michael@0 1030 this.actorID = form.actor;
michael@0 1031 this.form(form, detail, context);
michael@0 1032 }
michael@0 1033 },
michael@0 1034
michael@0 1035 destroy: function() {
michael@0 1036 // Reject all outstanding requests, they won't make sense after
michael@0 1037 // the front is destroyed.
michael@0 1038 while (this._requests && this._requests.length > 0) {
michael@0 1039 let deferred = this._requests.shift();
michael@0 1040 deferred.reject(new Error("Connection closed"));
michael@0 1041 }
michael@0 1042 Pool.prototype.destroy.call(this);
michael@0 1043 this.actorID = null;
michael@0 1044 },
michael@0 1045
michael@0 1046 /**
michael@0 1047 * @returns a promise that will resolve to the actorID this front
michael@0 1048 * represents.
michael@0 1049 */
michael@0 1050 actor: function() { return promise.resolve(this.actorID) },
michael@0 1051
michael@0 1052 toString: function() { return "[Front for " + this.typeName + "/" + this.actorID + "]" },
michael@0 1053
michael@0 1054 /**
michael@0 1055 * Update the actor from its representation.
michael@0 1056 * Subclasses should override this.
michael@0 1057 */
michael@0 1058 form: function(form) {},
michael@0 1059
michael@0 1060 /**
michael@0 1061 * Send a packet on the connection.
michael@0 1062 */
michael@0 1063 send: function(packet) {
michael@0 1064 if (packet.to) {
michael@0 1065 this.conn._transport.send(packet);
michael@0 1066 } else {
michael@0 1067 this.actor().then(actorID => {
michael@0 1068 packet.to = actorID;
michael@0 1069 this.conn._transport.send(packet);
michael@0 1070 });
michael@0 1071 }
michael@0 1072 },
michael@0 1073
michael@0 1074 /**
michael@0 1075 * Send a two-way request on the connection.
michael@0 1076 */
michael@0 1077 request: function(packet) {
michael@0 1078 let deferred = promise.defer();
michael@0 1079 this._requests.push(deferred);
michael@0 1080 this.send(packet);
michael@0 1081 return deferred.promise;
michael@0 1082 },
michael@0 1083
michael@0 1084 /**
michael@0 1085 * Handler for incoming packets from the client's actor.
michael@0 1086 */
michael@0 1087 onPacket: function(packet) {
michael@0 1088 // Pick off event packets
michael@0 1089 if (this._clientSpec.events && this._clientSpec.events.has(packet.type)) {
michael@0 1090 let event = this._clientSpec.events.get(packet.type);
michael@0 1091 let args = event.request.read(packet, this);
michael@0 1092 if (event.pre) {
michael@0 1093 event.pre.forEach((pre) => pre.apply(this, args));
michael@0 1094 }
michael@0 1095 events.emit.apply(null, [this, event.name].concat(args));
michael@0 1096 return;
michael@0 1097 }
michael@0 1098
michael@0 1099 // Remaining packets must be responses.
michael@0 1100 if (this._requests.length === 0) {
michael@0 1101 let msg = "Unexpected packet " + this.actorID + ", " + JSON.stringify(packet);
michael@0 1102 let err = Error(msg);
michael@0 1103 console.error(err);
michael@0 1104 throw err;
michael@0 1105 }
michael@0 1106
michael@0 1107 let deferred = this._requests.shift();
michael@0 1108 if (packet.error) {
michael@0 1109 deferred.reject(packet.error);
michael@0 1110 } else {
michael@0 1111 deferred.resolve(packet);
michael@0 1112 }
michael@0 1113 }
michael@0 1114 });
michael@0 1115 exports.Front = Front;
michael@0 1116
michael@0 1117 /**
michael@0 1118 * A method tagged with preEvent will be called after recieving a packet
michael@0 1119 * for that event, and before the front emits the event.
michael@0 1120 */
michael@0 1121 exports.preEvent = function(eventName, fn) {
michael@0 1122 fn._preEvent = eventName;
michael@0 1123 return fn;
michael@0 1124 }
michael@0 1125
michael@0 1126 /**
michael@0 1127 * Mark a method as a custom front implementation, replacing the generated
michael@0 1128 * front method.
michael@0 1129 *
michael@0 1130 * @param function fn
michael@0 1131 * The front implementation, will be returned.
michael@0 1132 * @param object options
michael@0 1133 * Options object:
michael@0 1134 * impl (string): If provided, the generated front method will be
michael@0 1135 * stored as this property on the prototype.
michael@0 1136 */
michael@0 1137 exports.custom = function(fn, options={}) {
michael@0 1138 fn._customFront = options;
michael@0 1139 return fn;
michael@0 1140 }
michael@0 1141
michael@0 1142 function prototypeOf(obj) {
michael@0 1143 return typeof(obj) === "function" ? obj.prototype : obj;
michael@0 1144 }
michael@0 1145
michael@0 1146 /**
michael@0 1147 * Process a front definition from its prototype and generate
michael@0 1148 * request methods.
michael@0 1149 */
michael@0 1150 let frontProto = function(proto) {
michael@0 1151 let actorType = prototypeOf(proto.actorType);
michael@0 1152 if (proto._actorSpec) {
michael@0 1153 throw new Error("frontProto called twice on the same front prototype!");
michael@0 1154 }
michael@0 1155 proto._actorSpec = actorType._actorSpec;
michael@0 1156 proto.typeName = actorType.typeName;
michael@0 1157
michael@0 1158 // Generate request methods.
michael@0 1159 let methods = proto._actorSpec.methods;
michael@0 1160 methods.forEach(spec => {
michael@0 1161 let name = spec.name;
michael@0 1162
michael@0 1163 // If there's already a property by this name in the front, it must
michael@0 1164 // be a custom front method.
michael@0 1165 if (name in proto) {
michael@0 1166 let custom = proto[spec.name]._customFront;
michael@0 1167 if (custom === undefined) {
michael@0 1168 throw Error("Existing method for " + spec.name + " not marked customFront while processing " + actorType.typeName + ".");
michael@0 1169 }
michael@0 1170 // If the user doesn't need the impl don't generate it.
michael@0 1171 if (!custom.impl) {
michael@0 1172 return;
michael@0 1173 }
michael@0 1174 name = custom.impl;
michael@0 1175 }
michael@0 1176
michael@0 1177 proto[name] = function(...args) {
michael@0 1178 let histogram, startTime;
michael@0 1179 if (spec.telemetry) {
michael@0 1180 if (spec.oneway) {
michael@0 1181 // That just doesn't make sense.
michael@0 1182 throw Error("Telemetry specified for a oneway request");
michael@0 1183 }
michael@0 1184 let transportType = this.conn.localTransport
michael@0 1185 ? "LOCAL_"
michael@0 1186 : "REMOTE_";
michael@0 1187 let histogramId = "DEVTOOLS_DEBUGGER_RDP_"
michael@0 1188 + transportType + spec.telemetry + "_MS";
michael@0 1189 try {
michael@0 1190 histogram = Services.telemetry.getHistogramById(histogramId);
michael@0 1191 startTime = new Date();
michael@0 1192 } catch(ex) {
michael@0 1193 // XXX: Is this expected in xpcshell tests?
michael@0 1194 console.error(ex);
michael@0 1195 spec.telemetry = false;
michael@0 1196 }
michael@0 1197 }
michael@0 1198
michael@0 1199 let packet = spec.request.write(args, this);
michael@0 1200 if (spec.oneway) {
michael@0 1201 // Fire-and-forget oneway packets.
michael@0 1202 this.send(packet);
michael@0 1203 return undefined;
michael@0 1204 }
michael@0 1205
michael@0 1206 return this.request(packet).then(response => {
michael@0 1207 let ret = spec.response.read(response, this);
michael@0 1208
michael@0 1209 if (histogram) {
michael@0 1210 histogram.add(+new Date - startTime);
michael@0 1211 }
michael@0 1212
michael@0 1213 return ret;
michael@0 1214 }).then(null, promiseDone);
michael@0 1215 }
michael@0 1216
michael@0 1217 // Release methods should call the destroy function on return.
michael@0 1218 if (spec.release) {
michael@0 1219 let fn = proto[name];
michael@0 1220 proto[name] = function(...args) {
michael@0 1221 return fn.apply(this, args).then(result => {
michael@0 1222 this.destroy();
michael@0 1223 return result;
michael@0 1224 })
michael@0 1225 }
michael@0 1226 }
michael@0 1227 });
michael@0 1228
michael@0 1229
michael@0 1230 // Process event specifications
michael@0 1231 proto._clientSpec = {};
michael@0 1232
michael@0 1233 let events = proto._actorSpec.events;
michael@0 1234 if (events) {
michael@0 1235 // This actor has events, scan the prototype for preEvent handlers...
michael@0 1236 let preHandlers = new Map();
michael@0 1237 for (let name of Object.getOwnPropertyNames(proto)) {
michael@0 1238 let desc = Object.getOwnPropertyDescriptor(proto, name);
michael@0 1239 if (!desc.value) {
michael@0 1240 continue;
michael@0 1241 }
michael@0 1242 if (desc.value._preEvent) {
michael@0 1243 let preEvent = desc.value._preEvent;
michael@0 1244 if (!events.has(preEvent)) {
michael@0 1245 throw Error("preEvent for event that doesn't exist: " + preEvent);
michael@0 1246 }
michael@0 1247 let handlers = preHandlers.get(preEvent);
michael@0 1248 if (!handlers) {
michael@0 1249 handlers = [];
michael@0 1250 preHandlers.set(preEvent, handlers);
michael@0 1251 }
michael@0 1252 handlers.push(desc.value);
michael@0 1253 }
michael@0 1254 }
michael@0 1255
michael@0 1256 proto._clientSpec.events = new Map();
michael@0 1257
michael@0 1258 for (let [name, request] of events) {
michael@0 1259 proto._clientSpec.events.set(request.type, {
michael@0 1260 name: name,
michael@0 1261 request: request,
michael@0 1262 pre: preHandlers.get(name)
michael@0 1263 });
michael@0 1264 }
michael@0 1265 }
michael@0 1266 return proto;
michael@0 1267 }
michael@0 1268
michael@0 1269 /**
michael@0 1270 * Create a front class for the given actor class, with the given prototype.
michael@0 1271 *
michael@0 1272 * @param ActorClass actorType
michael@0 1273 * The actor class you're creating a front for.
michael@0 1274 * @param object proto
michael@0 1275 * The object prototype. Must have a 'typeName' property,
michael@0 1276 * should have method definitions, can have event definitions.
michael@0 1277 */
michael@0 1278 exports.FrontClass = function(actorType, proto) {
michael@0 1279 proto.actorType = actorType;
michael@0 1280 proto.extends = Front;
michael@0 1281 let cls = Class(frontProto(proto));
michael@0 1282 registeredTypes.get(cls.prototype.typeName).frontClass = cls;
michael@0 1283 return cls;
michael@0 1284 }
michael@0 1285
michael@0 1286
michael@0 1287 exports.dumpActorSpec = function(type) {
michael@0 1288 let actorSpec = type.actorSpec;
michael@0 1289 let ret = {
michael@0 1290 category: "actor",
michael@0 1291 typeName: type.name,
michael@0 1292 methods: [],
michael@0 1293 events: {}
michael@0 1294 };
michael@0 1295
michael@0 1296 for (let method of actorSpec.methods) {
michael@0 1297 ret.methods.push({
michael@0 1298 name: method.name,
michael@0 1299 release: method.release || undefined,
michael@0 1300 oneway: method.oneway || undefined,
michael@0 1301 request: method.request.describe(),
michael@0 1302 response: method.response.describe()
michael@0 1303 });
michael@0 1304 }
michael@0 1305
michael@0 1306 if (actorSpec.events) {
michael@0 1307 for (let [name, request] of actorSpec.events) {
michael@0 1308 ret.events[name] = request.describe();
michael@0 1309 }
michael@0 1310 }
michael@0 1311
michael@0 1312
michael@0 1313 JSON.stringify(ret);
michael@0 1314
michael@0 1315 return ret;
michael@0 1316 }
michael@0 1317
michael@0 1318 exports.dumpProtocolSpec = function() {
michael@0 1319 let ret = {
michael@0 1320 types: {},
michael@0 1321 };
michael@0 1322
michael@0 1323 for (let [name, type] of registeredTypes) {
michael@0 1324 // Force lazy instantiation if needed.
michael@0 1325 type = types.getType(name);
michael@0 1326 if (type.category === "dict") {
michael@0 1327 ret.types[name] = {
michael@0 1328 category: "dict",
michael@0 1329 typeName: name,
michael@0 1330 specializations: type.specializations
michael@0 1331 }
michael@0 1332 } else if (type.category === "actor") {
michael@0 1333 ret.types[name] = exports.dumpActorSpec(type);
michael@0 1334 }
michael@0 1335 }
michael@0 1336
michael@0 1337 return ret;
michael@0 1338 }

mercurial