mirror of
https://github.com/classilla/tenfourfox.git
synced 2024-10-03 22:55:12 +00:00
817 lines
20 KiB
JavaScript
817 lines
20 KiB
JavaScript
/* 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/. */
|
|
(function(exports) {
|
|
"use strict";
|
|
|
|
|
|
var describe = Object.getOwnPropertyDescriptor;
|
|
var Class = fields => {
|
|
var constructor = fields.constructor || function() {};
|
|
var ancestor = fields.extends || Object;
|
|
|
|
|
|
|
|
var descriptor = {};
|
|
for (var key of Object.keys(fields))
|
|
descriptor[key] = describe(fields, key);
|
|
|
|
var prototype = Object.create(ancestor.prototype, descriptor);
|
|
|
|
constructor.prototype = prototype;
|
|
prototype.constructor = constructor;
|
|
|
|
return constructor;
|
|
};
|
|
|
|
|
|
var bus = function Bus() {
|
|
var parser = new DOMParser();
|
|
return parser.parseFromString("<EventTarget/>", "application/xml").documentElement;
|
|
}();
|
|
|
|
var GUID = new WeakMap();
|
|
GUID.id = 0;
|
|
var guid = x => GUID.get(x);
|
|
var setGUID = x => {
|
|
GUID.set(x, ++ GUID.id);
|
|
};
|
|
|
|
var Emitter = Class({
|
|
extends: EventTarget,
|
|
constructor: function() {
|
|
this.setupEmitter();
|
|
},
|
|
setupEmitter: function() {
|
|
setGUID(this);
|
|
},
|
|
addEventListener: function(type, listener, capture) {
|
|
bus.addEventListener(type + "@" + guid(this),
|
|
listener, capture);
|
|
},
|
|
removeEventListener: function(type, listener, capture) {
|
|
bus.removeEventListener(type + "@" + guid(this),
|
|
listener, capture);
|
|
}
|
|
});
|
|
|
|
function dispatch(target, type, data) {
|
|
var event = new MessageEvent(type + "@" + guid(target), {
|
|
bubbles: true,
|
|
cancelable: false,
|
|
data: data
|
|
});
|
|
bus.dispatchEvent(event);
|
|
}
|
|
|
|
var supervisedWorkers = new WeakMap();
|
|
var supervised = supervisor => {
|
|
if (!supervisedWorkers.has(supervisor)) {
|
|
supervisedWorkers.set(supervisor, new Map());
|
|
supervisor.connection.addActorPool(supervisor);
|
|
}
|
|
return supervisedWorkers.get(supervisor);
|
|
};
|
|
|
|
var Supervisor = Class({
|
|
extends: Emitter,
|
|
constructor: function(...params) {
|
|
this.setupEmitter(...params);
|
|
this.setupSupervisor(...params);
|
|
},
|
|
Supervisor: function(connection) {
|
|
this.connection = connection;
|
|
},
|
|
/**
|
|
* Return the parent pool for this client.
|
|
*/
|
|
supervisor: function() {
|
|
return this.connection.poolFor(this.actorID);
|
|
},
|
|
/**
|
|
* Override this if you want actors returned by this actor
|
|
* to belong to a different actor by default.
|
|
*/
|
|
marshallPool: function() { return this; },
|
|
/**
|
|
* Add an actor as a child of this pool.
|
|
*/
|
|
supervise: function(actor) {
|
|
if (!actor.actorID)
|
|
actor.actorID = this.connection.allocID(actor.actorPrefix ||
|
|
actor.typeName);
|
|
|
|
supervised(this).set(actor.actorID, actor);
|
|
return actor;
|
|
},
|
|
/**
|
|
* Remove an actor as a child of this pool.
|
|
*/
|
|
abandon: function(actor) {
|
|
supervised(this).delete(actor.actorID);
|
|
},
|
|
// true if the given actor ID exists in the pool.
|
|
has: function(actorID) {
|
|
return supervised(this).has(actorID);
|
|
},
|
|
// Same as actor, should update debugger connection to use 'actor'
|
|
// and then remove this.
|
|
get: function(actorID) {
|
|
return supervised(this).get(actorID);
|
|
},
|
|
actor: function(actorID) {
|
|
return supervised(this).get(actorID);
|
|
},
|
|
isEmpty: function() {
|
|
return supervised(this).size === 0;
|
|
},
|
|
/**
|
|
* For getting along with the debugger server pools, should be removable
|
|
* eventually.
|
|
*/
|
|
cleanup: function() {
|
|
this.destroy();
|
|
},
|
|
destroy: function() {
|
|
var supervisor = this.supervisor();
|
|
if (supervisor)
|
|
supervisor.abandon(this);
|
|
|
|
for (var actor of supervised(this).values()) {
|
|
if (actor !== this) {
|
|
var destroy = actor.destroy;
|
|
// Disconnect destroy while we're destroying in case of (misbehaving)
|
|
// circular ownership.
|
|
if (destroy) {
|
|
actor.destroy = null;
|
|
destroy.call(actor);
|
|
actor.destroy = destroy;
|
|
}
|
|
}
|
|
}
|
|
|
|
this.connection.removeActorPool(this);
|
|
supervised(this).clear();
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
var mailbox = new WeakMap();
|
|
var clientRequests = new WeakMap();
|
|
|
|
var inbox = client => mailbox.get(client).inbox;
|
|
var outbox = client => mailbox.get(client).outbox;
|
|
var requests = client => clientRequests.get(client);
|
|
|
|
|
|
var Receiver = Class({
|
|
receive: function(packet) {
|
|
if (packet.error)
|
|
this.reject(packet.error);
|
|
else
|
|
this.resolve(this.read(packet));
|
|
}
|
|
});
|
|
|
|
var Connection = Class({
|
|
constructor: function() {
|
|
// Queue of the outgoing messages.
|
|
this.outbox = [];
|
|
// Map of pending requests.
|
|
this.pending = new Map();
|
|
this.pools = new Set();
|
|
},
|
|
isConnected: function() {
|
|
return !!this.port
|
|
},
|
|
connect: function(port) {
|
|
this.port = port;
|
|
port.addEventListener("message", this);
|
|
port.start();
|
|
|
|
this.flush();
|
|
},
|
|
addPool: function(pool) {
|
|
this.pools.add(pool);
|
|
},
|
|
removePool: function(pool) {
|
|
this.pools.delete(pool);
|
|
},
|
|
poolFor: function(id) {
|
|
for (let pool of this.pools.values()) {
|
|
if (pool.has(id))
|
|
return pool;
|
|
}
|
|
},
|
|
get: function(id) {
|
|
var pool = this.poolFor(id);
|
|
return pool && pool.get(id);
|
|
},
|
|
disconnect: function() {
|
|
this.port.stop();
|
|
this.port = null;
|
|
for (var request of this.pending.values()) {
|
|
request.catch(new Error("Connection closed"));
|
|
}
|
|
this.pending.clear();
|
|
|
|
var requests = this.outbox.splice(0);
|
|
for (var request of request) {
|
|
requests.catch(new Error("Connection closed"));
|
|
}
|
|
},
|
|
handleEvent: function(event) {
|
|
this.receive(event.data);
|
|
},
|
|
flush: function() {
|
|
if (this.isConnected()) {
|
|
for (var request of this.outbox) {
|
|
if (!this.pending.has(request.to)) {
|
|
this.outbox.splice(this.outbox.indexOf(request), 1);
|
|
this.pending.set(request.to, request);
|
|
this.send(request.packet);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
send: function(packet) {
|
|
this.port.postMessage(packet);
|
|
},
|
|
request: function(packet) {
|
|
return new Promise(function(resolve, reject) {
|
|
this.outbox.push({
|
|
to: packet.to,
|
|
packet: packet,
|
|
receive: resolve,
|
|
catch: reject
|
|
});
|
|
this.flush();
|
|
});
|
|
},
|
|
receive: function(packet) {
|
|
var { from, type, why } = packet;
|
|
var receiver = this.pending.get(from);
|
|
if (!receiver) {
|
|
console.warn("Unable to handle received packet", data);
|
|
} else {
|
|
this.pending.delete(from);
|
|
if (packet.error)
|
|
receiver.catch(packet.error);
|
|
else
|
|
receiver.receive(packet);
|
|
}
|
|
this.flush();
|
|
},
|
|
});
|
|
|
|
/**
|
|
* Base class for client-side actor fronts.
|
|
*/
|
|
var Client = Class({
|
|
extends: Supervisor,
|
|
constructor: function(from=null, detail=null, connection=null) {
|
|
this.Client(from, detail, connection);
|
|
},
|
|
Client: function(form, detail, connection) {
|
|
this.Supervisor(connection);
|
|
|
|
if (form) {
|
|
this.actorID = form.actor;
|
|
this.from(form, detail);
|
|
}
|
|
},
|
|
connect: function(port) {
|
|
this.connection = new Connection(port);
|
|
},
|
|
actorID: null,
|
|
actor: function() {
|
|
return this.actorID;
|
|
},
|
|
/**
|
|
* Update the actor from its representation.
|
|
* Subclasses should override this.
|
|
*/
|
|
form: function(form) {
|
|
},
|
|
/**
|
|
* Method is invokeid when packet received constitutes an
|
|
* event. By default such packets are demarshalled and
|
|
* dispatched on the client instance.
|
|
*/
|
|
dispatch: function(packet) {
|
|
},
|
|
/**
|
|
* Method is invoked when packet is returned in response to
|
|
* a request. By default respond delivers response to a first
|
|
* request in a queue.
|
|
*/
|
|
read: function(input) {
|
|
throw new TypeError("Subclass must implement read method");
|
|
},
|
|
write: function(input) {
|
|
throw new TypeError("Subclass must implement write method");
|
|
},
|
|
respond: function(packet) {
|
|
var [resolve, reject] = requests(this).shift();
|
|
if (packet.error)
|
|
reject(packet.error);
|
|
else
|
|
resolve(this.read(packet));
|
|
},
|
|
receive: function(packet) {
|
|
if (this.isEventPacket(packet)) {
|
|
this.dispatch(packet);
|
|
}
|
|
else if (requests(this).length) {
|
|
this.respond(packet);
|
|
}
|
|
else {
|
|
this.catch(packet);
|
|
}
|
|
},
|
|
send: function(packet) {
|
|
Promise.cast(packet.to || this.actor()).then(id => {
|
|
packet.to = id;
|
|
this.connection.send(packet);
|
|
})
|
|
},
|
|
request: function(packet) {
|
|
return this.connection.request(packet);
|
|
}
|
|
});
|
|
|
|
|
|
var Destructor = method => {
|
|
return function(...args) {
|
|
return method.apply(this, args).then(result => {
|
|
this.destroy();
|
|
return result;
|
|
});
|
|
};
|
|
};
|
|
|
|
var Profiled = (method, id) => {
|
|
return function(...args) {
|
|
var start = new Date();
|
|
return method.apply(this, args).then(result => {
|
|
var end = new Date();
|
|
this.telemetry.add(id, +end - start);
|
|
return result;
|
|
});
|
|
};
|
|
};
|
|
|
|
var Method = (request, response) => {
|
|
return response ? new BidirectionalMethod(request, response) :
|
|
new UnidirecationalMethod(request);
|
|
};
|
|
|
|
var UnidirecationalMethod = request => {
|
|
return function(...args) {
|
|
var packet = request.write(args, this);
|
|
this.connection.send(packet);
|
|
return Promise.resolve(void(0));
|
|
};
|
|
};
|
|
|
|
var BidirectionalMethod = (request, response) => {
|
|
return function(...args) {
|
|
var packet = request.write(args, this);
|
|
return this.connection.request(packet).then(packet => {
|
|
return response.read(packet, this);
|
|
});
|
|
};
|
|
};
|
|
|
|
|
|
Client.from = ({category, typeName, methods, events}) => {
|
|
var proto = {
|
|
constructor: function(...args) {
|
|
this.Client(...args);
|
|
},
|
|
extends: Client,
|
|
name: typeName
|
|
};
|
|
|
|
methods.forEach(({telemetry, request, response, name, oneway, release}) => {
|
|
var [reader, writer] = oneway ? [, new Request(request)] :
|
|
[new Request(request), new Response(response)];
|
|
var method = new Method(request, response);
|
|
var profiler = telemetry ? new Profiler(method) : method;
|
|
var destructor = release ? new Destructor(profiler) : profiler;
|
|
proto[name] = destructor;
|
|
});
|
|
|
|
return Class(proto);
|
|
};
|
|
|
|
|
|
var defineType = (client, descriptor) => {
|
|
var type = void(0)
|
|
if (typeof(descriptor) === "string") {
|
|
if (name.indexOf(":") > 0)
|
|
type = makeCompoundType(descriptor);
|
|
else if (name.indexOf("#") > 0)
|
|
type = new ActorDetail(descriptor);
|
|
else if (client.specification[descriptor])
|
|
type = makeCategoryType(client.specification[descriptor]);
|
|
} else {
|
|
type = makeCategoryType(descriptor);
|
|
}
|
|
|
|
if (type)
|
|
client.types.set(type.name, type);
|
|
else
|
|
throw TypeError("Invalid type: " + descriptor);
|
|
};
|
|
|
|
|
|
var makeCompoundType = name => {
|
|
var index = name.indexOf(":");
|
|
var [baseType, subType] = [name.slice(0, index), parts.slice(1)];
|
|
return baseType === "array" ? new ArrayOf(subType) :
|
|
baseType === "nullable" ? new Maybe(subType) :
|
|
null;
|
|
};
|
|
|
|
var makeCategoryType = (descriptor) => {
|
|
var { category } = descriptor;
|
|
return category === "dict" ? new Dictionary(descriptor) :
|
|
category === "actor" ? new Actor(descriptor) :
|
|
null;
|
|
};
|
|
|
|
|
|
var typeFor = (client, type="primitive") => {
|
|
if (!client.types.has(type))
|
|
defineType(client, type);
|
|
|
|
return client.types.get(type);
|
|
};
|
|
|
|
|
|
var Client = Class({
|
|
constructor: function() {
|
|
},
|
|
setupTypes: function(specification) {
|
|
this.specification = specification;
|
|
this.types = new Map();
|
|
},
|
|
read: function(input, type) {
|
|
return typeFor(this, type).read(input, this);
|
|
},
|
|
write: function(input, type) {
|
|
return typeFor(this, type).write(input, this);
|
|
}
|
|
});
|
|
|
|
|
|
var Type = Class({
|
|
get name() {
|
|
return this.category ? this.category + ":" + this.type :
|
|
this.type;
|
|
},
|
|
read: function(input, client) {
|
|
throw new TypeError("`Type` subclass must implement `read`");
|
|
},
|
|
write: function(input, client) {
|
|
throw new TypeError("`Type` subclass must implement `write`");
|
|
}
|
|
});
|
|
|
|
|
|
var Primitve = Class({
|
|
extends: Type,
|
|
constuctor: function(type) {
|
|
this.type = type;
|
|
},
|
|
read: function(input, client) {
|
|
return input;
|
|
},
|
|
write: function(input, client) {
|
|
return input;
|
|
}
|
|
});
|
|
|
|
var Maybe = Class({
|
|
extends: Type,
|
|
category: "nullable",
|
|
constructor: function(type) {
|
|
this.type = type;
|
|
},
|
|
read: function(input, client) {
|
|
return input === null ? null :
|
|
input === void(0) ? void(0) :
|
|
client.read(input, this.type);
|
|
},
|
|
write: function(input, client) {
|
|
return input === null ? null :
|
|
input === void(0) ? void(0) :
|
|
client.write(input, this.type);
|
|
}
|
|
});
|
|
|
|
var ArrayOf = Class({
|
|
extends: Type,
|
|
category: "array",
|
|
constructor: function(type) {
|
|
this.type = type;
|
|
},
|
|
read: function(input, client) {
|
|
return input.map($ => client.read($, this.type));
|
|
},
|
|
write: function(input, client) {
|
|
return input.map($ => client.write($, this.type));
|
|
}
|
|
});
|
|
|
|
var Dictionary = Class({
|
|
exteds: Type,
|
|
category: "dict",
|
|
get name() { return this.type; },
|
|
constructor: function({typeName, specializations}) {
|
|
this.type = typeName;
|
|
this.types = specifications;
|
|
},
|
|
read: function(input, client) {
|
|
var output = {};
|
|
for (var key in input) {
|
|
output[key] = client.read(input[key], this.types[key]);
|
|
}
|
|
return output;
|
|
},
|
|
write: function(input, client) {
|
|
var output = {};
|
|
for (var key in input) {
|
|
output[key] = client.write(value, this.types[key]);
|
|
}
|
|
return output;
|
|
}
|
|
});
|
|
|
|
var Actor = Class({
|
|
exteds: Type,
|
|
category: "actor",
|
|
get name() { return this.type; },
|
|
constructor: function({typeName}) {
|
|
this.type = typeName;
|
|
},
|
|
read: function(input, client, detail) {
|
|
var id = value.actor;
|
|
var actor = void(0);
|
|
if (client.connection.has(id)) {
|
|
return client.connection.get(id).form(input, detail, client);
|
|
} else {
|
|
actor = Client.from(detail, client);
|
|
actor.actorID = id;
|
|
client.supervise(actor);
|
|
}
|
|
},
|
|
write: function(input, client, detail) {
|
|
if (input instanceof Actor) {
|
|
if (!input.actorID) {
|
|
client.supervise(input);
|
|
}
|
|
return input.from(detail);
|
|
}
|
|
return input.actorID;
|
|
}
|
|
});
|
|
|
|
var Root = Client.from({
|
|
"category": "actor",
|
|
"typeName": "root",
|
|
"methods": [
|
|
{"name": "listTabs",
|
|
"request": {},
|
|
"response": {
|
|
}
|
|
},
|
|
{"name": "listAddons"
|
|
},
|
|
{"name": "echo",
|
|
|
|
},
|
|
{"name": "protocolDescription",
|
|
|
|
}
|
|
]
|
|
});
|
|
|
|
|
|
var ActorDetail = Class({
|
|
extends: Actor,
|
|
constructor: function(name, actor, detail) {
|
|
this.detail = detail;
|
|
this.actor = actor;
|
|
},
|
|
read: function(input, client) {
|
|
this.actor.read(input, client, this.detail);
|
|
},
|
|
write: function(input, client) {
|
|
this.actor.write(input, client, this.detail);
|
|
}
|
|
});
|
|
|
|
var registeredLifetimes = new Map();
|
|
var LifeTime = Class({
|
|
extends: Type,
|
|
category: "lifetime",
|
|
constructor: function(lifetime, type) {
|
|
this.name = lifetime + ":" + type.name;
|
|
this.field = registeredLifetimes.get(lifetime);
|
|
},
|
|
read: function(input, client) {
|
|
return this.type.read(input, client[this.field]);
|
|
},
|
|
write: function(input, client) {
|
|
return this.type.write(input, client[this.field]);
|
|
}
|
|
});
|
|
|
|
var primitive = new Primitve("primitive");
|
|
var string = new Primitve("string");
|
|
var number = new Primitve("number");
|
|
var boolean = new Primitve("boolean");
|
|
var json = new Primitve("json");
|
|
var array = new Primitve("array");
|
|
|
|
|
|
var TypedValue = Class({
|
|
extends: Type,
|
|
constructor: function(name, type) {
|
|
this.TypedValue(name, type);
|
|
},
|
|
TypedValue: function(name, type) {
|
|
this.name = name;
|
|
this.type = type;
|
|
},
|
|
read: function(input, client) {
|
|
return this.client.read(input, this.type);
|
|
},
|
|
write: function(input, client) {
|
|
return this.client.write(input, this.type);
|
|
}
|
|
});
|
|
|
|
var Return = Class({
|
|
extends: TypedValue,
|
|
constructor: function(type) {
|
|
this.type = type
|
|
}
|
|
});
|
|
|
|
var Argument = Class({
|
|
extends: TypedValue,
|
|
constructor: function(...args) {
|
|
this.Argument(...args);
|
|
},
|
|
Argument: function(index, type) {
|
|
this.index = index;
|
|
this.TypedValue("argument[" + index + "]", type);
|
|
},
|
|
read: function(input, client, target) {
|
|
return target[this.index] = client.read(input, this.type);
|
|
}
|
|
});
|
|
|
|
var Option = Class({
|
|
extends: Argument,
|
|
constructor: function(...args) {
|
|
return this.Argument(...args);
|
|
},
|
|
read: function(input, client, target, name) {
|
|
var param = target[this.index] || (target[this.index] = {});
|
|
param[name] = input === void(0) ? input : client.read(input, this.type);
|
|
},
|
|
write: function(input, client, name) {
|
|
var value = input && input[name];
|
|
return value === void(0) ? value : client.write(value, this.type);
|
|
}
|
|
});
|
|
|
|
var Request = Class({
|
|
extends: Type,
|
|
constructor: function(template={}) {
|
|
this.type = template.type;
|
|
this.template = template;
|
|
this.params = findPlaceholders(template, Argument);
|
|
},
|
|
read: function(packet, client) {
|
|
var args = [];
|
|
for (var param of this.params) {
|
|
var {placeholder, path} = param;
|
|
var name = path[path.length - 1];
|
|
placeholder.read(getPath(packet, path), client, args, name);
|
|
// TODO:
|
|
// args[placeholder.index] = placeholder.read(query(packet, path), client);
|
|
}
|
|
return args;
|
|
},
|
|
write: function(input, client) {
|
|
return JSON.parse(JSON.stringify(this.template, (key, value) => {
|
|
return value instanceof Argument ? value.write(input[value.index],
|
|
client, key) :
|
|
value;
|
|
}));
|
|
}
|
|
});
|
|
|
|
var Response = Class({
|
|
extends: Type,
|
|
constructor: function(template={}) {
|
|
this.template = template;
|
|
var [x] = findPlaceholders(template, Return);
|
|
var {placeholder, path} = x;
|
|
this.return = placeholder;
|
|
this.path = path;
|
|
},
|
|
read: function(packet, client) {
|
|
var value = query(packet, this.path);
|
|
return this.return.read(value, client);
|
|
},
|
|
write: function(input, client) {
|
|
return JSON.parse(JSON.stringify(this.template, (key, value) => {
|
|
return value instanceof Return ? value.write(input) :
|
|
input
|
|
}));
|
|
}
|
|
});
|
|
|
|
// Returns array of values for the given object.
|
|
var values = object => Object.keys(object).map(key => object[key]);
|
|
// Returns [key, value] pairs for the given object.
|
|
var pairs = object => Object.keys(object).map(key => [key, object[key]]);
|
|
// Queries an object for the field nested with in it.
|
|
var query = (object, path) => path.reduce((object, entry) => object && object[entry],
|
|
object);
|
|
|
|
|
|
var Root = Client.from({
|
|
"category": "actor",
|
|
"typeName": "root",
|
|
"methods": [
|
|
{
|
|
"name": "echo",
|
|
"request": {
|
|
"string": { "_arg": 0, "type": "string" }
|
|
},
|
|
"response": {
|
|
"string": { "_retval": "string" }
|
|
}
|
|
},
|
|
{
|
|
"name": "listTabs",
|
|
"request": {},
|
|
"response": { "_retval": "tablist" }
|
|
},
|
|
{
|
|
"name": "actorDescriptions",
|
|
"request": {},
|
|
"response": { "_retval": "json" }
|
|
}
|
|
],
|
|
"events": {
|
|
"tabListChanged": {}
|
|
}
|
|
});
|
|
|
|
var Tab = Client.from({
|
|
"category": "dict",
|
|
"typeName": "tab",
|
|
"specifications": {
|
|
"title": "string",
|
|
"url": "string",
|
|
"outerWindowID": "number",
|
|
"console": "console",
|
|
"inspectorActor": "inspector",
|
|
"callWatcherActor": "call-watcher",
|
|
"canvasActor": "canvas",
|
|
"webglActor": "webgl",
|
|
"webaudioActor": "webaudio",
|
|
"styleSheetsActor": "stylesheets",
|
|
"styleEditorActor": "styleeditor",
|
|
"storageActor": "storage",
|
|
"gcliActor": "gcli",
|
|
"memoryActor": "memory",
|
|
"eventLoopLag": "eventLoopLag",
|
|
|
|
"trace": "trace", // missing
|
|
}
|
|
});
|
|
|
|
var tablist = Client.from({
|
|
"category": "dict",
|
|
"typeName": "tablist",
|
|
"specializations": {
|
|
"selected": "number",
|
|
"tabs": "array:tab"
|
|
}
|
|
});
|
|
|
|
})(this);
|
|
|