mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-24 21:31:04 +00:00
Bug 1560445
- Extract classes from protocol.js. r=yulia
Differential Revision: https://phabricator.services.mozilla.com/D35502 --HG-- rename : devtools/shared/protocol.js => devtools/shared/protocol/Actor.js rename : devtools/shared/protocol.js => devtools/shared/protocol/Actor/generateActorSpec.js rename : devtools/shared/protocol.js => devtools/shared/protocol/Front.js rename : devtools/shared/protocol.js => devtools/shared/protocol/Front/FrontClassWithSpec.js rename : devtools/shared/protocol.js => devtools/shared/protocol/Pool.js rename : devtools/shared/protocol.js => devtools/shared/protocol/Request.js rename : devtools/shared/protocol.js => devtools/shared/protocol/Response.js rename : devtools/shared/protocol.js => devtools/shared/protocol/types.js rename : devtools/shared/protocol.js => devtools/shared/protocol/utils.js extra : moz-landing-system : lando
This commit is contained in:
parent
9f81e46a34
commit
890892c4e2
File diff suppressed because it is too large
Load Diff
225
devtools/shared/protocol/Actor.js
Normal file
225
devtools/shared/protocol/Actor.js
Normal file
@ -0,0 +1,225 @@
|
||||
"use strict";
|
||||
|
||||
const { extend } = require("devtools/shared/extend");
|
||||
var { Pool } = require("./Pool");
|
||||
|
||||
/**
|
||||
* Keep track of which actorSpecs have been created. If a replica of a spec
|
||||
* is created, it can be caught, and specs which inherit from other specs will
|
||||
* not overwrite eachother.
|
||||
*/
|
||||
var actorSpecs = new WeakMap();
|
||||
|
||||
exports.actorSpecs = actorSpecs;
|
||||
|
||||
/**
|
||||
* An actor in the actor tree.
|
||||
*
|
||||
* @param optional conn
|
||||
* Either a DebuggerServerConnection or a DebuggerClient. Must have
|
||||
* addActorPool, removeActorPool, and poolFor.
|
||||
* conn can be null if the subclass provides a conn property.
|
||||
* @constructor
|
||||
*/
|
||||
class Actor extends Pool {
|
||||
// Existing Actors extending this class expect initialize to contain constructor logic.
|
||||
initialize(conn) {
|
||||
// Repeat Pool.constructor here as we can't call it from initialize
|
||||
// This is to be removed once actors switch to es classes and are able to call
|
||||
// Actor's contructor.
|
||||
if (conn) {
|
||||
this.conn = conn;
|
||||
}
|
||||
|
||||
// Will contain the actor's ID
|
||||
this.actorID = null;
|
||||
|
||||
this._actorSpec = actorSpecs.get(Object.getPrototypeOf(this));
|
||||
// Forward events to the connection.
|
||||
if (this._actorSpec && this._actorSpec.events) {
|
||||
for (const [name, request] of this._actorSpec.events.entries()) {
|
||||
this.on(name, (...args) => {
|
||||
this._sendEvent(name, request, ...args);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toString() {
|
||||
return "[Actor " + this.typeName + "/" + this.actorID + "]";
|
||||
}
|
||||
|
||||
_sendEvent(name, request, ...args) {
|
||||
if (!this.actorID) {
|
||||
console.error(`Tried to send a '${name}' event on an already destroyed actor` +
|
||||
` '${this.typeName}'`);
|
||||
return;
|
||||
}
|
||||
let packet;
|
||||
try {
|
||||
packet = request.write(args, this);
|
||||
} catch (ex) {
|
||||
console.error("Error sending event: " + name);
|
||||
throw ex;
|
||||
}
|
||||
packet.from = packet.from || this.actorID;
|
||||
this.conn.send(packet);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
super.destroy();
|
||||
this.actorID = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override this method in subclasses to serialize the actor.
|
||||
* @param [optional] string hint
|
||||
* Optional string to customize the form.
|
||||
* @returns A jsonable object.
|
||||
*/
|
||||
form(hint) {
|
||||
return { actor: this.actorID };
|
||||
}
|
||||
|
||||
writeError(error, typeName, method) {
|
||||
console.error(`Error while calling actor '${typeName}'s method '${method}'`,
|
||||
error.message);
|
||||
if (error.stack) {
|
||||
console.error(error.stack);
|
||||
}
|
||||
this.conn.send({
|
||||
from: this.actorID,
|
||||
error: error.error || "unknownError",
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
_queueResponse(create) {
|
||||
const pending = this._pendingResponse || Promise.resolve(null);
|
||||
const response = create(pending);
|
||||
this._pendingResponse = response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Throw an error with the passed message and attach an `error` property to the Error
|
||||
* object so it can be consumed by the writeError function.
|
||||
* @param {String} error: A string (usually a single word serving as an id) that will
|
||||
* be assign to error.error.
|
||||
* @param {String} message: The string that will be passed to the Error constructor.
|
||||
* @throws This always throw.
|
||||
*/
|
||||
throwError(error, message) {
|
||||
const err = new Error(message);
|
||||
err.error = error;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
exports.Actor = Actor;
|
||||
|
||||
/**
|
||||
* Generates request handlers as described by the given actor specification on
|
||||
* the given actor prototype. Returns the actor prototype.
|
||||
*/
|
||||
var generateRequestHandlers = function(actorSpec, actorProto) {
|
||||
actorProto.typeName = actorSpec.typeName;
|
||||
|
||||
// Generate request handlers for each method definition
|
||||
actorProto.requestTypes = Object.create(null);
|
||||
actorSpec.methods.forEach(spec => {
|
||||
const handler = function(packet, conn) {
|
||||
try {
|
||||
let args;
|
||||
try {
|
||||
args = spec.request.read(packet, this);
|
||||
} catch (ex) {
|
||||
console.error("Error reading request: " + packet.type);
|
||||
throw ex;
|
||||
}
|
||||
|
||||
if (!this[spec.name]) {
|
||||
throw new Error(
|
||||
`Spec for '${actorProto.typeName}' specifies a '${spec.name}'` +
|
||||
` method that isn't implemented by the actor`
|
||||
);
|
||||
}
|
||||
const ret = this[spec.name].apply(this, args);
|
||||
|
||||
const sendReturn = retToSend => {
|
||||
if (spec.oneway) {
|
||||
// No need to send a response.
|
||||
return;
|
||||
}
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = spec.response.write(retToSend, this);
|
||||
} catch (ex) {
|
||||
console.error("Error writing response to: " + spec.name);
|
||||
throw ex;
|
||||
}
|
||||
response.from = this.actorID;
|
||||
// If spec.release has been specified, destroy the object.
|
||||
if (spec.release) {
|
||||
try {
|
||||
this.destroy();
|
||||
} catch (e) {
|
||||
this.writeError(e, actorProto.typeName, spec.name);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
conn.send(response);
|
||||
};
|
||||
|
||||
this._queueResponse(p => {
|
||||
return p
|
||||
.then(() => ret)
|
||||
.then(sendReturn)
|
||||
.catch(e => this.writeError(e, actorProto.typeName, spec.name));
|
||||
});
|
||||
} catch (e) {
|
||||
this._queueResponse(p => {
|
||||
return p.then(() =>
|
||||
this.writeError(e, actorProto.typeName, spec.name)
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
actorProto.requestTypes[spec.request.type] = handler;
|
||||
});
|
||||
|
||||
return actorProto;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create an actor class for the given actor specification and prototype.
|
||||
*
|
||||
* @param object actorSpec
|
||||
* The actor specification. Must have a 'typeName' property.
|
||||
* @param object actorProto
|
||||
* The actor prototype. Should have method definitions, can have event
|
||||
* definitions.
|
||||
*/
|
||||
var ActorClassWithSpec = function(actorSpec, actorProto) {
|
||||
if (!actorSpec.typeName) {
|
||||
throw Error("Actor specification must have a typeName member.");
|
||||
}
|
||||
|
||||
// Existing Actors are relying on the initialize instead of constructor methods.
|
||||
const cls = function() {
|
||||
const instance = Object.create(cls.prototype);
|
||||
instance.initialize.apply(instance, arguments);
|
||||
return instance;
|
||||
};
|
||||
cls.prototype = extend(
|
||||
Actor.prototype,
|
||||
generateRequestHandlers(actorSpec, actorProto)
|
||||
);
|
||||
|
||||
actorSpecs.set(cls.prototype, actorSpec);
|
||||
|
||||
return cls;
|
||||
};
|
||||
exports.ActorClassWithSpec = ActorClassWithSpec;
|
77
devtools/shared/protocol/Actor/generateActorSpec.js
Normal file
77
devtools/shared/protocol/Actor/generateActorSpec.js
Normal file
@ -0,0 +1,77 @@
|
||||
"use strict";
|
||||
|
||||
var { Request } = require("../Request");
|
||||
const { Response } = require("../Response");
|
||||
var { types, registeredTypes } = require("../types");
|
||||
|
||||
/**
|
||||
* Generates an actor specification from an actor description.
|
||||
*/
|
||||
var generateActorSpec = function(actorDesc) {
|
||||
const actorSpec = {
|
||||
typeName: actorDesc.typeName,
|
||||
methods: [],
|
||||
};
|
||||
|
||||
// Find method and form specifications attached to properties.
|
||||
for (const name of Object.getOwnPropertyNames(actorDesc)) {
|
||||
const desc = Object.getOwnPropertyDescriptor(actorDesc, name);
|
||||
if (!desc.value) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (desc.value._methodSpec) {
|
||||
const methodSpec = desc.value._methodSpec;
|
||||
const spec = {};
|
||||
spec.name = methodSpec.name || name;
|
||||
spec.request = new Request(
|
||||
Object.assign({ type: spec.name }, methodSpec.request || undefined)
|
||||
);
|
||||
spec.response = new Response(methodSpec.response || undefined);
|
||||
spec.release = methodSpec.release;
|
||||
spec.oneway = methodSpec.oneway;
|
||||
|
||||
actorSpec.methods.push(spec);
|
||||
}
|
||||
}
|
||||
|
||||
// Find additional method specifications
|
||||
if (actorDesc.methods) {
|
||||
for (const name in actorDesc.methods) {
|
||||
const methodSpec = actorDesc.methods[name];
|
||||
const spec = {};
|
||||
|
||||
spec.name = methodSpec.name || name;
|
||||
spec.request = new Request(
|
||||
Object.assign({ type: spec.name }, methodSpec.request || undefined)
|
||||
);
|
||||
spec.response = new Response(methodSpec.response || undefined);
|
||||
spec.release = methodSpec.release;
|
||||
spec.oneway = methodSpec.oneway;
|
||||
|
||||
actorSpec.methods.push(spec);
|
||||
}
|
||||
}
|
||||
|
||||
// Find event specifications
|
||||
if (actorDesc.events) {
|
||||
actorSpec.events = new Map();
|
||||
for (const name in actorDesc.events) {
|
||||
const eventRequest = actorDesc.events[name];
|
||||
Object.freeze(eventRequest);
|
||||
actorSpec.events.set(
|
||||
name,
|
||||
new Request(Object.assign({ type: name }, eventRequest))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!registeredTypes.has(actorSpec.typeName)) {
|
||||
types.addActorType(actorSpec.typeName);
|
||||
}
|
||||
registeredTypes.get(actorSpec.typeName).actorSpec = actorSpec;
|
||||
|
||||
return actorSpec;
|
||||
};
|
||||
|
||||
exports.generateActorSpec = generateActorSpec;
|
8
devtools/shared/protocol/Actor/moz.build
Normal file
8
devtools/shared/protocol/Actor/moz.build
Normal file
@ -0,0 +1,8 @@
|
||||
# vim: set filetype=python:
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
DevToolsModules(
|
||||
'generateActorSpec.js',
|
||||
)
|
249
devtools/shared/protocol/Front.js
Normal file
249
devtools/shared/protocol/Front.js
Normal file
@ -0,0 +1,249 @@
|
||||
"use strict";
|
||||
|
||||
var { settleAll } = require("devtools/shared/DevToolsUtils");
|
||||
var EventEmitter = require("devtools/shared/event-emitter");
|
||||
|
||||
var { Pool } = require("./Pool");
|
||||
var {
|
||||
getStack,
|
||||
callFunctionWithAsyncStack,
|
||||
} = require("devtools/shared/platform/stack");
|
||||
// Bug 1454373: devtools/shared/defer still uses Promise.jsm which is slower
|
||||
// than DOM Promises. So implement our own copy of `defer` based on DOM Promises.
|
||||
function defer() {
|
||||
let resolve, reject;
|
||||
const promise = new Promise(function() {
|
||||
resolve = arguments[0];
|
||||
reject = arguments[1];
|
||||
});
|
||||
return {
|
||||
resolve: resolve,
|
||||
reject: reject,
|
||||
promise: promise,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class for client-side actor fronts.
|
||||
*
|
||||
* @param optional conn
|
||||
* Either a DebuggerServerConnection or a DebuggerClient. Must have
|
||||
* addActorPool, removeActorPool, and poolFor.
|
||||
* conn can be null if the subclass provides a conn property.
|
||||
* @constructor
|
||||
*/
|
||||
class Front extends Pool {
|
||||
constructor(conn = null) {
|
||||
super(conn);
|
||||
this.actorID = null;
|
||||
this._requests = [];
|
||||
|
||||
// Front listener functions registered via `onFront` get notified
|
||||
// of new fronts via this dedicated EventEmitter object.
|
||||
this._frontListeners = new EventEmitter();
|
||||
|
||||
// List of optional listener for each event, that is processed immediatly on packet
|
||||
// receival, before emitting event via EventEmitter on the Front.
|
||||
// These listeners are register via Front.before function.
|
||||
// Map(Event Name[string] => Event Listener[function])
|
||||
this._beforeListeners = new Map();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
// Reject all outstanding requests, they won't make sense after
|
||||
// the front is destroyed.
|
||||
while (this._requests && this._requests.length > 0) {
|
||||
const { deferred, to, type, stack } = this._requests.shift();
|
||||
const msg =
|
||||
"Connection closed, pending request to " +
|
||||
to +
|
||||
", type " +
|
||||
type +
|
||||
" failed" +
|
||||
"\n\nRequest stack:\n" +
|
||||
stack.formattedStack;
|
||||
deferred.reject(new Error(msg));
|
||||
}
|
||||
super.destroy();
|
||||
this.clearEvents();
|
||||
this.actorID = null;
|
||||
this._frontListeners = null;
|
||||
this._beforeListeners = null;
|
||||
}
|
||||
|
||||
manage(front) {
|
||||
if (!front.actorID) {
|
||||
throw new Error(
|
||||
"Can't manage front without an actor ID.\n" +
|
||||
"Ensure server supports " +
|
||||
front.typeName +
|
||||
"."
|
||||
);
|
||||
}
|
||||
super.manage(front);
|
||||
|
||||
// Call listeners registered via `onFront` method
|
||||
this._frontListeners.emit(front.typeName, front);
|
||||
}
|
||||
|
||||
// Run callback on every front of this type that currently exists, and on every
|
||||
// instantiation of front type in the future.
|
||||
onFront(typeName, callback) {
|
||||
// First fire the callback on already instantiated fronts
|
||||
for (const front of this.poolChildren()) {
|
||||
if (front.typeName == typeName) {
|
||||
callback(front);
|
||||
}
|
||||
}
|
||||
// Then register the callback for fronts instantiated in the future
|
||||
this._frontListeners.on(typeName, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register an event listener that will be called immediately on packer receival.
|
||||
* The given callback is going to be called before emitting the event via EventEmitter
|
||||
* API on the Front. Event emitting will be delayed if the callback is async.
|
||||
* Only one such listener can be registered per type of event.
|
||||
*
|
||||
* @param String type
|
||||
* Event emitted by the actor to intercept.
|
||||
* @param Function callback
|
||||
* Function that will process the event.
|
||||
*/
|
||||
before(type, callback) {
|
||||
if (this._beforeListeners.has(type)) {
|
||||
throw new Error(
|
||||
`Can't register multiple before listeners for "${type}".`
|
||||
);
|
||||
}
|
||||
this._beforeListeners.set(type, callback);
|
||||
}
|
||||
|
||||
toString() {
|
||||
return "[Front for " + this.typeName + "/" + this.actorID + "]";
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the actor from its representation.
|
||||
* Subclasses should override this.
|
||||
*/
|
||||
form(form) {}
|
||||
|
||||
/**
|
||||
* Send a packet on the connection.
|
||||
*/
|
||||
send(packet) {
|
||||
if (packet.to) {
|
||||
this.conn._transport.send(packet);
|
||||
} else {
|
||||
packet.to = this.actorID;
|
||||
// The connection might be closed during the promise resolution
|
||||
if (this.conn._transport) {
|
||||
this.conn._transport.send(packet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a two-way request on the connection.
|
||||
*/
|
||||
request(packet) {
|
||||
const deferred = defer();
|
||||
// Save packet basics for debugging
|
||||
const { to, type } = packet;
|
||||
this._requests.push({
|
||||
deferred,
|
||||
to: to || this.actorID,
|
||||
type,
|
||||
stack: getStack(),
|
||||
});
|
||||
this.send(packet);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for incoming packets from the client's actor.
|
||||
*/
|
||||
onPacket(packet) {
|
||||
// Pick off event packets
|
||||
const type = packet.type || undefined;
|
||||
if (this._clientSpec.events && this._clientSpec.events.has(type)) {
|
||||
const event = this._clientSpec.events.get(packet.type);
|
||||
let args;
|
||||
try {
|
||||
args = event.request.read(packet, this);
|
||||
} catch (ex) {
|
||||
console.error("Error reading event: " + packet.type);
|
||||
console.exception(ex);
|
||||
throw ex;
|
||||
}
|
||||
// Check for "pre event" callback to be processed before emitting events on fronts
|
||||
// Use event.name instead of packet.type to use specific event name instead of RDP
|
||||
// packet's type.
|
||||
const beforeEvent = this._beforeListeners.get(event.name);
|
||||
if (beforeEvent) {
|
||||
const result = beforeEvent.apply(this, args);
|
||||
// Check to see if the beforeEvent returned a promise -- if so,
|
||||
// wait for their resolution before emitting. Otherwise, emit synchronously.
|
||||
if (result && typeof result.then == "function") {
|
||||
result.then(() => {
|
||||
super.emit(event.name, ...args);
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
super.emit(event.name, ...args);
|
||||
return;
|
||||
}
|
||||
|
||||
// Remaining packets must be responses.
|
||||
if (this._requests.length === 0) {
|
||||
const msg =
|
||||
"Unexpected packet " + this.actorID + ", " + JSON.stringify(packet);
|
||||
const err = Error(msg);
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
|
||||
const { deferred, stack } = this._requests.shift();
|
||||
callFunctionWithAsyncStack(
|
||||
() => {
|
||||
if (packet.error) {
|
||||
// "Protocol error" is here to avoid TBPL heuristics. See also
|
||||
// https://dxr.mozilla.org/webtools-central/source/tbpl/php/inc/GeneralErrorFilter.php
|
||||
let message;
|
||||
if (packet.error && packet.message) {
|
||||
message =
|
||||
"Protocol error (" + packet.error + "): " + packet.message;
|
||||
} else {
|
||||
message = packet.error;
|
||||
}
|
||||
deferred.reject(message);
|
||||
} else {
|
||||
deferred.resolve(packet);
|
||||
}
|
||||
},
|
||||
stack,
|
||||
"DevTools RDP"
|
||||
);
|
||||
}
|
||||
|
||||
hasRequests() {
|
||||
return !!this._requests.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for all current requests from this front to settle. This is especially useful
|
||||
* for tests and other utility environments that may not have events or mechanisms to
|
||||
* await the completion of requests without this utility.
|
||||
*
|
||||
* @return Promise
|
||||
* Resolved when all requests have settled.
|
||||
*/
|
||||
waitForRequestsToSettle() {
|
||||
return settleAll(this._requests.map(({ deferred }) => deferred.promise));
|
||||
}
|
||||
}
|
||||
|
||||
exports.Front = Front;
|
103
devtools/shared/protocol/Front/FrontClassWithSpec.js
Normal file
103
devtools/shared/protocol/Front/FrontClassWithSpec.js
Normal file
@ -0,0 +1,103 @@
|
||||
"use strict";
|
||||
|
||||
var { Front } = require("../Front");
|
||||
|
||||
/**
|
||||
* Generates request methods as described by the given actor specification on
|
||||
* the given front prototype. Returns the front prototype.
|
||||
*/
|
||||
var generateRequestMethods = function(actorSpec, frontProto) {
|
||||
if (frontProto._actorSpec) {
|
||||
throw new Error("frontProto called twice on the same front prototype!");
|
||||
}
|
||||
|
||||
frontProto.typeName = actorSpec.typeName;
|
||||
|
||||
// Generate request methods.
|
||||
const methods = actorSpec.methods;
|
||||
methods.forEach(spec => {
|
||||
const name = spec.name;
|
||||
|
||||
frontProto[name] = function(...args) {
|
||||
// If this.actorID are not available, the request will not be able to complete.
|
||||
// The front was probably destroyed earlier.
|
||||
if (!this.actorID) {
|
||||
throw new Error(
|
||||
`Can not send request because front '${
|
||||
this.typeName
|
||||
}' is already destroyed.`
|
||||
);
|
||||
}
|
||||
|
||||
let packet;
|
||||
try {
|
||||
packet = spec.request.write(args, this);
|
||||
} catch (ex) {
|
||||
console.error("Error writing request: " + name);
|
||||
throw ex;
|
||||
}
|
||||
if (spec.oneway) {
|
||||
// Fire-and-forget oneway packets.
|
||||
this.send(packet);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this.request(packet).then(response => {
|
||||
let ret;
|
||||
try {
|
||||
ret = spec.response.read(response, this);
|
||||
} catch (ex) {
|
||||
console.error("Error reading response to: " + name + "\n" + ex);
|
||||
throw ex;
|
||||
}
|
||||
return ret;
|
||||
});
|
||||
};
|
||||
|
||||
// Release methods should call the destroy function on return.
|
||||
if (spec.release) {
|
||||
const fn = frontProto[name];
|
||||
frontProto[name] = function(...args) {
|
||||
return fn.apply(this, args).then(result => {
|
||||
this.destroy();
|
||||
return result;
|
||||
});
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Process event specifications
|
||||
frontProto._clientSpec = {};
|
||||
|
||||
const actorEvents = actorSpec.events;
|
||||
if (actorEvents) {
|
||||
frontProto._clientSpec.events = new Map();
|
||||
|
||||
for (const [name, request] of actorEvents) {
|
||||
frontProto._clientSpec.events.set(request.type, {
|
||||
name,
|
||||
request,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
frontProto._actorSpec = actorSpec;
|
||||
|
||||
return frontProto;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a front class for the given actor specification and front prototype.
|
||||
*
|
||||
* @param object actorSpec
|
||||
* The actor specification you're creating a front for.
|
||||
* @param object proto
|
||||
* The object prototype. Must have a 'typeName' property,
|
||||
* should have method definitions, can have event definitions.
|
||||
*/
|
||||
var FrontClassWithSpec = function(actorSpec) {
|
||||
class OneFront extends Front {}
|
||||
generateRequestMethods(actorSpec, OneFront.prototype);
|
||||
return OneFront;
|
||||
};
|
||||
exports.FrontClassWithSpec = FrontClassWithSpec;
|
8
devtools/shared/protocol/Front/moz.build
Normal file
8
devtools/shared/protocol/Front/moz.build
Normal file
@ -0,0 +1,8 @@
|
||||
# vim: set filetype=python:
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
DevToolsModules(
|
||||
'FrontClassWithSpec.js',
|
||||
)
|
171
devtools/shared/protocol/Pool.js
Normal file
171
devtools/shared/protocol/Pool.js
Normal file
@ -0,0 +1,171 @@
|
||||
"use strict";
|
||||
|
||||
var EventEmitter = require("devtools/shared/event-emitter");
|
||||
|
||||
/**
|
||||
* Actor and Front implementations
|
||||
*/
|
||||
|
||||
/**
|
||||
* A protocol object that can manage the lifetime of other protocol
|
||||
* objects. Pools are used on both sides of the connection to help coordinate lifetimes.
|
||||
*
|
||||
* @param optional conn
|
||||
* Either a DebuggerServerConnection or a DebuggerClient. Must have
|
||||
* addActorPool, removeActorPool, and poolFor.
|
||||
* conn can be null if the subclass provides a conn property.
|
||||
* @constructor
|
||||
*/
|
||||
class Pool extends EventEmitter {
|
||||
constructor(conn) {
|
||||
super();
|
||||
|
||||
if (conn) {
|
||||
this.conn = conn;
|
||||
}
|
||||
this.__poolMap = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the parent pool for this client.
|
||||
*/
|
||||
parent() {
|
||||
return this.conn.poolFor(this.actorID);
|
||||
}
|
||||
|
||||
poolFor(actorID) {
|
||||
return this.conn.poolFor(actorID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Override this if you want actors returned by this actor
|
||||
* to belong to a different actor by default.
|
||||
*/
|
||||
marshallPool() {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pool is the base class for all actors, even leaf nodes.
|
||||
* If the child map is actually referenced, go ahead and create
|
||||
* the stuff needed by the pool.
|
||||
*/
|
||||
get _poolMap() {
|
||||
if (this.__poolMap) {
|
||||
return this.__poolMap;
|
||||
}
|
||||
this.__poolMap = new Map();
|
||||
this.conn.addActorPool(this);
|
||||
return this.__poolMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an actor as a child of this pool.
|
||||
*/
|
||||
manage(actor) {
|
||||
if (!actor.actorID) {
|
||||
actor.actorID = this.conn.allocID(actor.actorPrefix || actor.typeName);
|
||||
} else {
|
||||
// If the actor is already registerd in a pool, remove it without destroying it.
|
||||
// This happens for example when an addon is reloaded. To see this behavior, take a
|
||||
// look at devtools/server/tests/unit/test_addon_reload.js
|
||||
|
||||
// TODO: not all actors have been moved to protocol.js, so they do not all have
|
||||
// a parent field. Remove the check for the parent once the conversion is finished
|
||||
const parent = this.poolFor(actor.actorID);
|
||||
if (parent) {
|
||||
parent.unmanage(actor);
|
||||
}
|
||||
}
|
||||
this._poolMap.set(actor.actorID, actor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an actor as a child of this pool.
|
||||
*/
|
||||
unmanage(actor) {
|
||||
this.__poolMap && this.__poolMap.delete(actor.actorID);
|
||||
}
|
||||
|
||||
// true if the given actor ID exists in the pool.
|
||||
has(actorID) {
|
||||
return this.__poolMap && this._poolMap.has(actorID);
|
||||
}
|
||||
|
||||
// The actor for a given actor id stored in this pool
|
||||
actor(actorID) {
|
||||
if (this.__poolMap) {
|
||||
return this._poolMap.get(actorID);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Same as actor, should update debugger connection to use 'actor'
|
||||
// and then remove this.
|
||||
get(actorID) {
|
||||
if (this.__poolMap) {
|
||||
return this._poolMap.get(actorID);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// True if this pool has no children.
|
||||
isEmpty() {
|
||||
return !this.__poolMap || this._poolMap.size == 0;
|
||||
}
|
||||
|
||||
// Generator that yields each non-self child of the pool.
|
||||
* poolChildren() {
|
||||
if (!this.__poolMap) {
|
||||
return;
|
||||
}
|
||||
for (const actor of this.__poolMap.values()) {
|
||||
// Self-owned actors are ok, but don't need visiting twice.
|
||||
if (actor === this) {
|
||||
continue;
|
||||
}
|
||||
yield actor;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy this item, removing it from a parent if it has one,
|
||||
* and destroying all children if necessary.
|
||||
*/
|
||||
destroy() {
|
||||
const parent = this.parent();
|
||||
if (parent) {
|
||||
parent.unmanage(this);
|
||||
}
|
||||
if (!this.__poolMap) {
|
||||
return;
|
||||
}
|
||||
for (const actor of this.__poolMap.values()) {
|
||||
// Self-owned actors are ok, but don't need destroying twice.
|
||||
if (actor === this) {
|
||||
continue;
|
||||
}
|
||||
const destroy = actor.destroy;
|
||||
if (destroy) {
|
||||
// Disconnect destroy while we're destroying in case of (misbehaving)
|
||||
// circular ownership.
|
||||
actor.destroy = null;
|
||||
destroy.call(actor);
|
||||
actor.destroy = destroy;
|
||||
}
|
||||
}
|
||||
this.conn.removeActorPool(this, true);
|
||||
this.__poolMap.clear();
|
||||
this.__poolMap = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* For getting along with the debugger server pools, should be removable
|
||||
* eventually.
|
||||
*/
|
||||
cleanup() {
|
||||
this.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
exports.Pool = Pool;
|
180
devtools/shared/protocol/Request.js
Normal file
180
devtools/shared/protocol/Request.js
Normal file
@ -0,0 +1,180 @@
|
||||
"use strict";
|
||||
|
||||
const { extend } = require("devtools/shared/extend");
|
||||
var { findPlaceholders, describeTemplate, getPath } = require("./utils");
|
||||
var { types } = require("./types");
|
||||
|
||||
/**
|
||||
* Manages a request template.
|
||||
*
|
||||
* @param object template
|
||||
* The request template.
|
||||
* @construcor
|
||||
*/
|
||||
var Request = function(template = {}) {
|
||||
this.type = template.type;
|
||||
this.template = template;
|
||||
this.args = findPlaceholders(template, Arg);
|
||||
};
|
||||
|
||||
Request.prototype = {
|
||||
/**
|
||||
* Write a request.
|
||||
*
|
||||
* @param array fnArgs
|
||||
* The function arguments to place in the request.
|
||||
* @param object ctx
|
||||
* The object making the request.
|
||||
* @returns a request packet.
|
||||
*/
|
||||
write: function(fnArgs, ctx) {
|
||||
const ret = {};
|
||||
for (const key in this.template) {
|
||||
const value = this.template[key];
|
||||
if (value instanceof Arg) {
|
||||
ret[key] = value.write(
|
||||
value.index in fnArgs ? fnArgs[value.index] : undefined,
|
||||
ctx,
|
||||
key
|
||||
);
|
||||
} else if (key == "type") {
|
||||
ret[key] = value;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Request can only an object with `Arg` or `Option` properties"
|
||||
);
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
},
|
||||
|
||||
/**
|
||||
* Read a request.
|
||||
*
|
||||
* @param object packet
|
||||
* The request packet.
|
||||
* @param object ctx
|
||||
* The object making the request.
|
||||
* @returns an arguments array
|
||||
*/
|
||||
read: function(packet, ctx) {
|
||||
const fnArgs = [];
|
||||
for (const templateArg of this.args) {
|
||||
const arg = templateArg.placeholder;
|
||||
const path = templateArg.path;
|
||||
const name = path[path.length - 1];
|
||||
arg.read(getPath(packet, path), ctx, fnArgs, name);
|
||||
}
|
||||
return fnArgs;
|
||||
},
|
||||
|
||||
describe: function() {
|
||||
return describeTemplate(this.template);
|
||||
},
|
||||
};
|
||||
|
||||
exports.Request = Request;
|
||||
|
||||
/**
|
||||
* Request/Response templates and generation
|
||||
*
|
||||
* Request packets are specified as json templates with
|
||||
* Arg and Option placeholders where arguments should be
|
||||
* placed.
|
||||
*
|
||||
* Reponse packets are also specified as json templates,
|
||||
* with a RetVal placeholder where the return value should be
|
||||
* placed.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Placeholder for simple arguments.
|
||||
*
|
||||
* @param number index
|
||||
* The argument index to place at this position.
|
||||
* @param type type
|
||||
* The argument should be marshalled as this type.
|
||||
* @constructor
|
||||
*/
|
||||
var Arg = function(index, type) {
|
||||
this.index = index;
|
||||
// Prevent force loading all Arg types by accessing it only when needed
|
||||
loader.lazyGetter(this, "type", function() {
|
||||
return types.getType(type);
|
||||
});
|
||||
};
|
||||
|
||||
Arg.prototype = {
|
||||
write: function(arg, ctx) {
|
||||
return this.type.write(arg, ctx);
|
||||
},
|
||||
|
||||
read: function(v, ctx, outArgs) {
|
||||
outArgs[this.index] = this.type.read(v, ctx);
|
||||
},
|
||||
|
||||
describe: function() {
|
||||
return {
|
||||
_arg: this.index,
|
||||
type: this.type.name,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
// Outside of protocol.js, Arg is called as factory method, without the new keyword.
|
||||
exports.Arg = function(index, type) {
|
||||
return new Arg(index, type);
|
||||
};
|
||||
|
||||
/**
|
||||
* Placeholder for an options argument value that should be hoisted
|
||||
* into the packet.
|
||||
*
|
||||
* If provided in a method specification:
|
||||
*
|
||||
* { optionArg: Option(1)}
|
||||
*
|
||||
* Then arguments[1].optionArg will be placed in the packet in this
|
||||
* value's place.
|
||||
*
|
||||
* @param number index
|
||||
* The argument index of the options value.
|
||||
* @param type type
|
||||
* The argument should be marshalled as this type.
|
||||
* @constructor
|
||||
*/
|
||||
var Option = function(index, type) {
|
||||
Arg.call(this, index, type);
|
||||
};
|
||||
|
||||
Option.prototype = extend(Arg.prototype, {
|
||||
write: function(arg, ctx, name) {
|
||||
// Ignore if arg is undefined or null; allow other falsy values
|
||||
if (arg == undefined || arg[name] == undefined) {
|
||||
return undefined;
|
||||
}
|
||||
const v = arg[name];
|
||||
return this.type.write(v, ctx);
|
||||
},
|
||||
read: function(v, ctx, outArgs, name) {
|
||||
if (outArgs[this.index] === undefined) {
|
||||
outArgs[this.index] = {};
|
||||
}
|
||||
if (v === undefined) {
|
||||
return;
|
||||
}
|
||||
outArgs[this.index][name] = this.type.read(v, ctx);
|
||||
},
|
||||
|
||||
describe: function() {
|
||||
return {
|
||||
_option: this.index,
|
||||
type: this.type.name,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// Outside of protocol.js, Option is called as factory method, without the new keyword.
|
||||
exports.Option = function(index, type) {
|
||||
return new Option(index, type);
|
||||
};
|
111
devtools/shared/protocol/Response.js
Normal file
111
devtools/shared/protocol/Response.js
Normal file
@ -0,0 +1,111 @@
|
||||
"use strict";
|
||||
|
||||
var { findPlaceholders, getPath, describeTemplate } = require("./utils");
|
||||
var { types } = require("./types");
|
||||
|
||||
/**
|
||||
* Manages a response template.
|
||||
*
|
||||
* @param object template
|
||||
* The response template.
|
||||
* @construcor
|
||||
*/
|
||||
var Response = function(template = {}) {
|
||||
this.template = template;
|
||||
const placeholders = findPlaceholders(template, RetVal);
|
||||
if (placeholders.length > 1) {
|
||||
throw Error("More than one RetVal specified in response");
|
||||
}
|
||||
const placeholder = placeholders.shift();
|
||||
if (placeholder) {
|
||||
this.retVal = placeholder.placeholder;
|
||||
this.path = placeholder.path;
|
||||
}
|
||||
};
|
||||
|
||||
Response.prototype = {
|
||||
/**
|
||||
* Write a response for the given return value.
|
||||
*
|
||||
* @param val ret
|
||||
* The return value.
|
||||
* @param object ctx
|
||||
* The object writing the response.
|
||||
*/
|
||||
write: function(ret, ctx) {
|
||||
// Consider that `template` is either directly a `RetVal`,
|
||||
// or a dictionary with may be one `RetVal`.
|
||||
if (this.template instanceof RetVal) {
|
||||
return this.template.write(ret, ctx);
|
||||
}
|
||||
const result = {};
|
||||
for (const key in this.template) {
|
||||
const value = this.template[key];
|
||||
if (value instanceof RetVal) {
|
||||
result[key] = value.write(ret, ctx);
|
||||
} else {
|
||||
throw new Error(
|
||||
"Response can only be a `RetVal` instance or an object " +
|
||||
"with one property being a `RetVal` instance."
|
||||
);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* Read a return value from the given response.
|
||||
*
|
||||
* @param object packet
|
||||
* The response packet.
|
||||
* @param object ctx
|
||||
* The object reading the response.
|
||||
*/
|
||||
read: function(packet, ctx) {
|
||||
if (!this.retVal) {
|
||||
return undefined;
|
||||
}
|
||||
const v = getPath(packet, this.path);
|
||||
return this.retVal.read(v, ctx);
|
||||
},
|
||||
|
||||
describe: function() {
|
||||
return describeTemplate(this.template);
|
||||
},
|
||||
};
|
||||
|
||||
exports.Response = Response;
|
||||
|
||||
/**
|
||||
* Placeholder for return values in a response template.
|
||||
*
|
||||
* @param type type
|
||||
* The return value should be marshalled as this type.
|
||||
*/
|
||||
var RetVal = function(type) {
|
||||
// Prevent force loading all RetVal types by accessing it only when needed
|
||||
loader.lazyGetter(this, "type", function() {
|
||||
return types.getType(type);
|
||||
});
|
||||
};
|
||||
|
||||
RetVal.prototype = {
|
||||
write: function(v, ctx) {
|
||||
return this.type.write(v, ctx);
|
||||
},
|
||||
|
||||
read: function(v, ctx) {
|
||||
return this.type.read(v, ctx);
|
||||
},
|
||||
|
||||
describe: function() {
|
||||
return {
|
||||
_retval: this.type.name,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
// Outside of protocol.js, RetVal is called as factory method, without the new keyword.
|
||||
exports.RetVal = function(type) {
|
||||
return new RetVal(type);
|
||||
};
|
@ -1,9 +1,20 @@
|
||||
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
DIRS += [
|
||||
'Actor',
|
||||
'Front',
|
||||
]
|
||||
|
||||
DevToolsModules(
|
||||
'Actor.js',
|
||||
'Front.js',
|
||||
'lazy-pool.js',
|
||||
'Pool.js',
|
||||
'Request.js',
|
||||
'Response.js',
|
||||
'types.js',
|
||||
'utils.js',
|
||||
)
|
||||
|
525
devtools/shared/protocol/types.js
Normal file
525
devtools/shared/protocol/types.js
Normal file
@ -0,0 +1,525 @@
|
||||
"use strict";
|
||||
|
||||
var { Actor } = require("./Actor");
|
||||
var { lazyLoadSpec, lazyLoadFront } = require("devtools/shared/specs/index");
|
||||
|
||||
/**
|
||||
* Types: named marshallers/demarshallers.
|
||||
*
|
||||
* Types provide a 'write' function that takes a js representation and
|
||||
* returns a protocol representation, and a "read" function that
|
||||
* takes a protocol representation and returns a js representation.
|
||||
*
|
||||
* The read and write methods are also passed a context object that
|
||||
* represent the actor or front requesting the translation.
|
||||
*
|
||||
* Types are referred to with a typestring. Basic types are
|
||||
* registered by name using addType, and more complex types can
|
||||
* be generated by adding detail to the type name.
|
||||
*/
|
||||
|
||||
var types = Object.create(null);
|
||||
exports.types = types;
|
||||
|
||||
var registeredTypes = (types.registeredTypes = new Map());
|
||||
var registeredLifetimes = (types.registeredLifetimes = new Map());
|
||||
|
||||
exports.registeredTypes = registeredTypes;
|
||||
|
||||
/**
|
||||
* Return the type object associated with a given typestring.
|
||||
* If passed a type object, it will be returned unchanged.
|
||||
*
|
||||
* Types can be registered with addType, or can be created on
|
||||
* the fly with typestrings. Examples:
|
||||
*
|
||||
* boolean
|
||||
* threadActor
|
||||
* threadActor#detail
|
||||
* array:threadActor
|
||||
* array:array:threadActor#detail
|
||||
*
|
||||
* @param [typestring|type] type
|
||||
* Either a typestring naming a type or a type object.
|
||||
*
|
||||
* @returns a type object.
|
||||
*/
|
||||
types.getType = function(type) {
|
||||
if (!type) {
|
||||
return types.Primitive;
|
||||
}
|
||||
|
||||
if (typeof type !== "string") {
|
||||
return type;
|
||||
}
|
||||
|
||||
// If already registered, we're done here.
|
||||
let reg = registeredTypes.get(type);
|
||||
if (reg) {
|
||||
return reg;
|
||||
}
|
||||
|
||||
// Try to lazy load the spec, if not already loaded.
|
||||
if (lazyLoadSpec(type)) {
|
||||
// If a spec module was lazy loaded, it will synchronously call
|
||||
// generateActorSpec, and set the type in `registeredTypes`.
|
||||
reg = registeredTypes.get(type);
|
||||
if (reg) {
|
||||
return reg;
|
||||
}
|
||||
}
|
||||
|
||||
// New type, see if it's a collection/lifetime type:
|
||||
const sep = type.indexOf(":");
|
||||
if (sep >= 0) {
|
||||
const collection = type.substring(0, sep);
|
||||
const subtype = types.getType(type.substring(sep + 1));
|
||||
|
||||
if (collection === "array") {
|
||||
return types.addArrayType(subtype);
|
||||
} else if (collection === "nullable") {
|
||||
return types.addNullableType(subtype);
|
||||
}
|
||||
|
||||
if (registeredLifetimes.has(collection)) {
|
||||
return types.addLifetimeType(collection, subtype);
|
||||
}
|
||||
|
||||
throw Error("Unknown collection type: " + collection);
|
||||
}
|
||||
|
||||
// Not a collection, might be actor detail
|
||||
const pieces = type.split("#", 2);
|
||||
if (pieces.length > 1) {
|
||||
if (pieces[1] != "actorid") {
|
||||
throw new Error(
|
||||
"Unsupported detail, only support 'actorid', got: " + pieces[1]
|
||||
);
|
||||
}
|
||||
return types.addActorDetail(type, pieces[0], pieces[1]);
|
||||
}
|
||||
|
||||
throw Error("Unknown type: " + type);
|
||||
};
|
||||
|
||||
/**
|
||||
* Don't allow undefined when writing primitive types to packets. If
|
||||
* you want to allow undefined, use a nullable type.
|
||||
*/
|
||||
function identityWrite(v) {
|
||||
if (v === undefined) {
|
||||
throw Error("undefined passed where a value is required");
|
||||
}
|
||||
// This has to handle iterator->array conversion because arrays of
|
||||
// primitive types pass through here.
|
||||
if (v && typeof v.next === "function") {
|
||||
return [...v];
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a type to the type system.
|
||||
*
|
||||
* When registering a type, you can provide `read` and `write` methods.
|
||||
*
|
||||
* The `read` method will be passed a JS object value from the JSON
|
||||
* packet and must return a native representation. The `write` method will
|
||||
* be passed a native representation and should provide a JSONable value.
|
||||
*
|
||||
* These methods will both be passed a context. The context is the object
|
||||
* performing or servicing the request - on the server side it will be
|
||||
* an Actor, on the client side it will be a Front.
|
||||
*
|
||||
* @param typestring name
|
||||
* Name to register
|
||||
* @param object typeObject
|
||||
* An object whose properties will be stored in the type, including
|
||||
* the `read` and `write` methods.
|
||||
* @param object options
|
||||
* Can specify `thawed` to prevent the type from being frozen.
|
||||
*
|
||||
* @returns a type object that can be used in protocol definitions.
|
||||
*/
|
||||
types.addType = function(name, typeObject = {}, options = {}) {
|
||||
if (registeredTypes.has(name)) {
|
||||
throw Error("Type '" + name + "' already exists.");
|
||||
}
|
||||
|
||||
const type = Object.assign(
|
||||
{
|
||||
toString() {
|
||||
return "[protocol type:" + name + "]";
|
||||
},
|
||||
name: name,
|
||||
primitive: !(typeObject.read || typeObject.write),
|
||||
read: identityWrite,
|
||||
write: identityWrite,
|
||||
},
|
||||
typeObject
|
||||
);
|
||||
|
||||
registeredTypes.set(name, type);
|
||||
|
||||
return type;
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove a type previously registered with the system.
|
||||
* Primarily useful for types registered by addons.
|
||||
*/
|
||||
types.removeType = function(name) {
|
||||
// This type may still be referenced by other types, make sure
|
||||
// those references don't work.
|
||||
const type = registeredTypes.get(name);
|
||||
|
||||
type.name = "DEFUNCT:" + name;
|
||||
type.category = "defunct";
|
||||
type.primitive = false;
|
||||
type.read = type.write = function() {
|
||||
throw new Error("Using defunct type: " + name);
|
||||
};
|
||||
|
||||
registeredTypes.delete(name);
|
||||
};
|
||||
|
||||
/**
|
||||
* Add an array type to the type system.
|
||||
*
|
||||
* getType() will call this function if provided an "array:<type>"
|
||||
* typestring.
|
||||
*
|
||||
* @param type subtype
|
||||
* The subtype to be held by the array.
|
||||
*/
|
||||
types.addArrayType = function(subtype) {
|
||||
subtype = types.getType(subtype);
|
||||
|
||||
const name = "array:" + subtype.name;
|
||||
|
||||
// Arrays of primitive types are primitive types themselves.
|
||||
if (subtype.primitive) {
|
||||
return types.addType(name);
|
||||
}
|
||||
return types.addType(name, {
|
||||
category: "array",
|
||||
read: (v, ctx) => {
|
||||
if (v && typeof v.next === "function") {
|
||||
v = [...v];
|
||||
}
|
||||
return v.map(i => subtype.read(i, ctx));
|
||||
},
|
||||
write: (v, ctx) => {
|
||||
if (v && typeof v.next === "function") {
|
||||
v = [...v];
|
||||
}
|
||||
return v.map(i => subtype.write(i, ctx));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a dict type to the type system. This allows you to serialize
|
||||
* a JS object that contains non-primitive subtypes.
|
||||
*
|
||||
* Properties of the value that aren't included in the specializations
|
||||
* will be serialized as primitive values.
|
||||
*
|
||||
* @param object specializations
|
||||
* A dict of property names => type
|
||||
*/
|
||||
types.addDictType = function(name, specializations) {
|
||||
const specTypes = {};
|
||||
for (const prop in specializations) {
|
||||
try {
|
||||
specTypes[prop] = types.getType(specializations[prop]);
|
||||
} catch (e) {
|
||||
// Types may not be defined yet. Sometimes, we define the type *after* using it, but
|
||||
// also, we have cyclic definitions on types. So lazily load them when they are not
|
||||
// immediately available.
|
||||
loader.lazyGetter(specTypes, prop, () => {
|
||||
return types.getType(specializations[prop]);
|
||||
});
|
||||
}
|
||||
}
|
||||
return types.addType(name, {
|
||||
category: "dict",
|
||||
specializations,
|
||||
read: (v, ctx) => {
|
||||
const ret = {};
|
||||
for (const prop in v) {
|
||||
if (prop in specTypes) {
|
||||
ret[prop] = specTypes[prop].read(v[prop], ctx);
|
||||
} else {
|
||||
ret[prop] = v[prop];
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
},
|
||||
|
||||
write: (v, ctx) => {
|
||||
const ret = {};
|
||||
for (const prop in v) {
|
||||
if (prop in specTypes) {
|
||||
ret[prop] = specTypes[prop].write(v[prop], ctx);
|
||||
} else {
|
||||
ret[prop] = v[prop];
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Register an actor type with the type system.
|
||||
*
|
||||
* Types are marshalled differently when communicating server->client
|
||||
* than they are when communicating client->server. The server needs
|
||||
* to provide useful information to the client, so uses the actor's
|
||||
* `form` method to get a json representation of the actor. When
|
||||
* making a request from the client we only need the actor ID string.
|
||||
*
|
||||
* This function can be called before the associated actor has been
|
||||
* constructed, but the read and write methods won't work until
|
||||
* the associated addActorImpl or addActorFront methods have been
|
||||
* called during actor/front construction.
|
||||
*
|
||||
* @param string name
|
||||
* The typestring to register.
|
||||
*/
|
||||
types.addActorType = function(name) {
|
||||
// We call addActorType from:
|
||||
// FrontClassWithSpec when registering front synchronously,
|
||||
// generateActorSpec when defining specs,
|
||||
// specs modules to register actor type early to use them in other types
|
||||
if (registeredTypes.has(name)) {
|
||||
return registeredTypes.get(name);
|
||||
}
|
||||
const type = types.addType(name, {
|
||||
_actor: true,
|
||||
category: "actor",
|
||||
read: (v, ctx, detail) => {
|
||||
// If we're reading a request on the server side, just
|
||||
// find the actor registered with this actorID.
|
||||
if (ctx instanceof Actor) {
|
||||
return ctx.conn.getActor(v);
|
||||
}
|
||||
|
||||
// Reading a response on the client side, check for an
|
||||
// existing front on the connection, and create the front
|
||||
// if it isn't found.
|
||||
const actorID = typeof v === "string" ? v : v.actor;
|
||||
let front = ctx.conn.getActor(actorID);
|
||||
if (!front) {
|
||||
// If front isn't instantiated yet, create one.
|
||||
|
||||
// Try lazy loading front if not already loaded.
|
||||
// The front module will synchronously call `FrontClassWithSpec` and
|
||||
// augment `type` with the `frontClass` attribute.
|
||||
if (!type.frontClass) {
|
||||
lazyLoadFront(name);
|
||||
}
|
||||
|
||||
// Use intermediate Class variable to please eslint requiring
|
||||
// a capital letter for all constructors.
|
||||
const Class = type.frontClass;
|
||||
front = new Class(ctx.conn);
|
||||
front.actorID = actorID;
|
||||
ctx.marshallPool().manage(front);
|
||||
}
|
||||
|
||||
// When the type `${name}#actorid` is used, `v` is a string refering to the
|
||||
// actor ID. We only set the actorID just before and so do not need anything else.
|
||||
if (detail != "actorid") {
|
||||
v = identityWrite(v);
|
||||
front.form(v, ctx);
|
||||
}
|
||||
|
||||
return front;
|
||||
},
|
||||
write: (v, ctx, detail) => {
|
||||
// If returning a response from the server side, make sure
|
||||
// the actor is added to a parent object and return its form.
|
||||
if (v instanceof Actor) {
|
||||
if (!v.actorID) {
|
||||
ctx.marshallPool().manage(v);
|
||||
}
|
||||
if (detail == "actorid") {
|
||||
return v.actorID;
|
||||
}
|
||||
return identityWrite(v.form(detail));
|
||||
}
|
||||
|
||||
// Writing a request from the client side, just send the actor id.
|
||||
return v.actorID;
|
||||
},
|
||||
});
|
||||
return type;
|
||||
};
|
||||
|
||||
types.addNullableType = function(subtype) {
|
||||
subtype = types.getType(subtype);
|
||||
return types.addType("nullable:" + subtype.name, {
|
||||
category: "nullable",
|
||||
read: (value, ctx) => {
|
||||
if (value == null) {
|
||||
return value;
|
||||
}
|
||||
return subtype.read(value, ctx);
|
||||
},
|
||||
write: (value, ctx) => {
|
||||
if (value == null) {
|
||||
return value;
|
||||
}
|
||||
return subtype.write(value, ctx);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Register an actor detail type. This is just like an actor type, but
|
||||
* will pass a detail hint to the actor's form method during serialization/
|
||||
* deserialization.
|
||||
*
|
||||
* This is called by getType() when passed an 'actorType#detail' string.
|
||||
*
|
||||
* @param string name
|
||||
* The typestring to register this type as.
|
||||
* @param type actorType
|
||||
* The actor type you'll be detailing.
|
||||
* @param string detail
|
||||
* The detail to pass.
|
||||
*/
|
||||
types.addActorDetail = function(name, actorType, detail) {
|
||||
actorType = types.getType(actorType);
|
||||
if (!actorType._actor) {
|
||||
throw Error(
|
||||
`Details only apply to actor types, tried to add detail '${detail}' ` +
|
||||
`to ${actorType.name}`
|
||||
);
|
||||
}
|
||||
return types.addType(name, {
|
||||
_actor: true,
|
||||
category: "detail",
|
||||
read: (v, ctx) => actorType.read(v, ctx, detail),
|
||||
write: (v, ctx) => actorType.write(v, ctx, detail),
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Register an actor lifetime. This lets the type system find a parent
|
||||
* actor that differs from the actor fulfilling the request.
|
||||
*
|
||||
* @param string name
|
||||
* The lifetime name to use in typestrings.
|
||||
* @param string prop
|
||||
* The property of the actor that holds the parent that should be used.
|
||||
*/
|
||||
types.addLifetime = function(name, prop) {
|
||||
if (registeredLifetimes.has(name)) {
|
||||
throw Error("Lifetime '" + name + "' already registered.");
|
||||
}
|
||||
registeredLifetimes.set(name, prop);
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove a previously-registered lifetime. Useful for lifetimes registered
|
||||
* in addons.
|
||||
*/
|
||||
types.removeLifetime = function(name) {
|
||||
registeredLifetimes.delete(name);
|
||||
};
|
||||
|
||||
/**
|
||||
* Register a lifetime type. This creates an actor type tied to the given
|
||||
* lifetime.
|
||||
*
|
||||
* This is called by getType() when passed a '<lifetimeType>:<actorType>'
|
||||
* typestring.
|
||||
*
|
||||
* @param string lifetime
|
||||
* A lifetime string previously regisered with addLifetime()
|
||||
* @param type subtype
|
||||
* An actor type
|
||||
*/
|
||||
types.addLifetimeType = function(lifetime, subtype) {
|
||||
subtype = types.getType(subtype);
|
||||
if (!subtype._actor) {
|
||||
throw Error(
|
||||
`Lifetimes only apply to actor types, tried to apply ` +
|
||||
`lifetime '${lifetime}' to ${subtype.name}`
|
||||
);
|
||||
}
|
||||
const prop = registeredLifetimes.get(lifetime);
|
||||
return types.addType(lifetime + ":" + subtype.name, {
|
||||
category: "lifetime",
|
||||
read: (value, ctx) => subtype.read(value, ctx[prop]),
|
||||
write: (value, ctx) => subtype.write(value, ctx[prop]),
|
||||
});
|
||||
};
|
||||
|
||||
// Add a few named primitive types.
|
||||
types.Primitive = types.addType("primitive");
|
||||
types.String = types.addType("string");
|
||||
types.Number = types.addType("number");
|
||||
types.Boolean = types.addType("boolean");
|
||||
types.JSON = types.addType("json");
|
||||
|
||||
exports.registerFront = function(cls) {
|
||||
const { typeName } = cls.prototype;
|
||||
if (!registeredTypes.has(typeName)) {
|
||||
types.addActorType(typeName);
|
||||
}
|
||||
registeredTypes.get(typeName).frontClass = cls;
|
||||
};
|
||||
|
||||
/**
|
||||
* Instantiate a global (preference, device) or target-scoped (webconsole, inspector)
|
||||
* front of the given type by picking its actor ID out of either the target or root
|
||||
* front's form.
|
||||
*
|
||||
* @param DebuggerClient client
|
||||
* The DebuggerClient instance to use.
|
||||
* @param string typeName
|
||||
* The type name of the front to instantiate. This is defined in its specifiation.
|
||||
* @param json form
|
||||
* If we want to instantiate a global actor's front, this is the root front's form,
|
||||
* otherwise we are instantiating a target-scoped front from the target front's form.
|
||||
*/
|
||||
function getFront(client, typeName, form) {
|
||||
const type = types.getType(typeName);
|
||||
if (!type) {
|
||||
throw new Error(`No spec for front type '${typeName}'.`);
|
||||
}
|
||||
if (!type.frontClass) {
|
||||
lazyLoadFront(typeName);
|
||||
}
|
||||
// Use intermediate Class variable to please eslint requiring
|
||||
// a capital letter for all constructors.
|
||||
const Class = type.frontClass;
|
||||
const instance = new Class(client);
|
||||
const { formAttributeName } = instance;
|
||||
if (!formAttributeName) {
|
||||
throw new Error(`Can't find the form attribute name for ${typeName}`);
|
||||
}
|
||||
// Retrive the actor ID from root or target actor's form
|
||||
instance.actorID = form[formAttributeName];
|
||||
if (!instance.actorID) {
|
||||
throw new Error(
|
||||
`Can't find the actor ID for ${typeName} from root or target` +
|
||||
` actor's form.`
|
||||
);
|
||||
}
|
||||
// Historically, all global and target scoped front were the first protocol.js in the
|
||||
// hierarchy of fronts. So that they have to self-own themself. But now, Root and Target
|
||||
// are fronts and should own them. The only issue here is that we should manage the
|
||||
// front *before* calling initialize which is going to try managing child fronts.
|
||||
instance.manage(instance);
|
||||
|
||||
if (typeof instance.initialize == "function") {
|
||||
return instance.initialize().then(() => instance);
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
exports.getFront = getFront;
|
76
devtools/shared/protocol/utils.js
Normal file
76
devtools/shared/protocol/utils.js
Normal file
@ -0,0 +1,76 @@
|
||||
"use strict";
|
||||
|
||||
function describeTemplate(template) {
|
||||
return JSON.parse(
|
||||
JSON.stringify(template, (key, value) => {
|
||||
if (value.describe) {
|
||||
return value.describe();
|
||||
}
|
||||
return value;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
exports.describeTemplate = describeTemplate;
|
||||
|
||||
/**
|
||||
* Find Placeholders in the template and save them along with their
|
||||
* paths.
|
||||
*/
|
||||
function findPlaceholders(template, constructor, path = [], placeholders = []) {
|
||||
if (!template || typeof template != "object") {
|
||||
return placeholders;
|
||||
}
|
||||
|
||||
if (template instanceof constructor) {
|
||||
placeholders.push({ placeholder: template, path: [...path] });
|
||||
return placeholders;
|
||||
}
|
||||
|
||||
for (const name in template) {
|
||||
path.push(name);
|
||||
findPlaceholders(template[name], constructor, path, placeholders);
|
||||
path.pop();
|
||||
}
|
||||
|
||||
return placeholders;
|
||||
}
|
||||
|
||||
exports.findPlaceholders = findPlaceholders;
|
||||
|
||||
/**
|
||||
* Get the value at a given path, or undefined if not found.
|
||||
*/
|
||||
function getPath(obj, path) {
|
||||
for (const name of path) {
|
||||
if (!(name in obj)) {
|
||||
return undefined;
|
||||
}
|
||||
obj = obj[name];
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
exports.getPath = getPath;
|
||||
|
||||
/**
|
||||
* Tags a prtotype method as an actor method implementation.
|
||||
*
|
||||
* @param function fn
|
||||
* The implementation function, will be returned.
|
||||
* @param spec
|
||||
* The method specification, with the following (optional) properties:
|
||||
* request (object): a request template.
|
||||
* response (object): a response template.
|
||||
* oneway (bool): 'true' if no response should be sent.
|
||||
*/
|
||||
exports.method = function(fn, spec = {}) {
|
||||
fn._methodSpec = Object.freeze(spec);
|
||||
if (spec.request) {
|
||||
Object.freeze(spec.request);
|
||||
}
|
||||
if (spec.response) {
|
||||
Object.freeze(spec.response);
|
||||
}
|
||||
return fn;
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user