Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
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 | } |