summaryrefslogtreecommitdiffstats
path: root/toolkit/components/extensions/ConduitsChild.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/extensions/ConduitsChild.sys.mjs')
-rw-r--r--toolkit/components/extensions/ConduitsChild.sys.mjs216
1 files changed, 216 insertions, 0 deletions
diff --git a/toolkit/components/extensions/ConduitsChild.sys.mjs b/toolkit/components/extensions/ConduitsChild.sys.mjs
new file mode 100644
index 0000000000..c5774ab39c
--- /dev/null
+++ b/toolkit/components/extensions/ConduitsChild.sys.mjs
@@ -0,0 +1,216 @@
+/* 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/. */
+
+/**
+ * This @file implements the child side of Conduits, an abstraction over
+ * Fission IPC for extension API subject. See {@link ConduitsParent.jsm}
+ * for more details about the overall design.
+ *
+ * @typedef {object} MessageData
+ * @property {ConduitID} [target]
+ * @property {ConduitID} [sender]
+ * @property {boolean} query
+ * @property {object} arg
+ */
+
+/**
+ * Base class for both child (Point) and parent (Broadcast) side of conduits,
+ * handles setting up send/receive method stubs.
+ */
+export class BaseConduit {
+ /**
+ * @param {object} subject
+ * @param {ConduitAddress} address
+ */
+ constructor(subject, address) {
+ this.subject = subject;
+ this.address = address;
+ this.id = address.id;
+
+ for (let name of address.send || []) {
+ this[`send${name}`] = this._send.bind(this, name, false);
+ }
+ for (let name of address.query || []) {
+ this[`query${name}`] = this._send.bind(this, name, true);
+ }
+
+ this.recv = new Map();
+ for (let name of address.recv || []) {
+ let method = this.subject[`recv${name}`];
+ if (!method) {
+ throw new Error(`recv${name} not found for conduit ${this.id}`);
+ }
+ this.recv.set(name, method.bind(this.subject));
+ }
+ }
+
+ /**
+ * Internal, partially @abstract, uses the actor to send the message/query.
+ *
+ * @param {string} method
+ * @param {boolean} query Flag indicating a response is expected.
+ * @param {JSWindowActor} actor
+ * @param {MessageData} data
+ * @returns {Promise?}
+ */
+ _send(method, query, actor, data) {
+ if (query) {
+ return actor.sendQuery(method, data);
+ }
+ actor.sendAsyncMessage(method, data);
+ }
+
+ /**
+ * Internal, calls the specific recvX method based on the message.
+ *
+ * @param {string} name Message/method name.
+ * @param {object} arg Message data, the one and only method argument.
+ * @param {object} meta Metadata about the method call.
+ */
+ async _recv(name, arg, meta) {
+ let method = this.recv.get(name);
+ if (!method) {
+ throw new Error(`recv${name} not found for conduit ${this.id}`);
+ }
+ try {
+ return await method(arg, meta);
+ } catch (e) {
+ if (meta.query) {
+ return Promise.reject(e);
+ }
+ Cu.reportError(e);
+ }
+ }
+}
+
+/**
+ * Child side conduit, can only send/receive point-to-point messages via the
+ * one specific ConduitsChild actor.
+ */
+export class PointConduit extends BaseConduit {
+ constructor(subject, address, actor) {
+ super(subject, address);
+ this.actor = actor;
+ this.actor.sendAsyncMessage("ConduitOpened", { arg: address });
+ }
+
+ /**
+ * Internal, sends messages via the actor, used by sendX stubs.
+ *
+ * @param {string} method
+ * @param {boolean} query
+ * @param {object?} arg
+ * @returns {Promise?}
+ */
+ _send(method, query, arg = {}) {
+ if (!this.actor) {
+ throw new Error(`send${method} on closed conduit ${this.id}`);
+ }
+ let sender = this.id;
+ return super._send(method, query, this.actor, { arg, query, sender });
+ }
+
+ /**
+ * Closes the conduit from further IPC, notifies the parent side by default.
+ *
+ * @param {boolean} silent
+ */
+ close(silent = false) {
+ let { actor } = this;
+ if (actor) {
+ this.actor = null;
+ actor.conduits.delete(this.id);
+ if (!silent) {
+ // Catch any exceptions that can occur if the conduit is closed while
+ // the actor is being destroyed due to the containing browser being closed.
+ // This should be treated as if the silent flag was passed.
+ try {
+ actor.sendAsyncMessage("ConduitClosed", { sender: this.id });
+ } catch (ex) {}
+ }
+ }
+ this.closeCallback?.();
+ this.closeCallback = null;
+ }
+
+ /**
+ * Set the callback to be called when the conduit is closed.
+ *
+ * @param {Function} callback
+ */
+ setCloseCallback(callback) {
+ this.closeCallback = callback;
+ }
+}
+
+/**
+ * Implements the child side of the Conduits actor, manages conduit lifetimes.
+ */
+export class ConduitsChild extends JSWindowActorChild {
+ constructor() {
+ super();
+ this.conduits = new Map();
+ }
+
+ /**
+ * Public entry point a child-side subject uses to open a conduit.
+ *
+ * @param {object} subject
+ * @param {ConduitAddress} address
+ * @returns {PointConduit}
+ */
+ openConduit(subject, address) {
+ let conduit = new PointConduit(subject, address, this);
+ this.conduits.set(conduit.id, conduit);
+ return conduit;
+ }
+
+ /**
+ * JSWindowActor method, routes the message to the target subject.
+ *
+ * @param {object} options
+ * @param {string} options.name
+ * @param {MessageData | MessageData[]} options.data
+ * @returns {Promise?}
+ */
+ receiveMessage({ name, data }) {
+ // Batch of webRequest events, run each and return results, ignoring errors.
+ if (Array.isArray(data)) {
+ let run = data => this.receiveMessage({ name, data });
+ return Promise.all(data.map(data => run(data).catch(Cu.reportError)));
+ }
+
+ let { target, arg, query, sender } = data;
+ let conduit = this.conduits.get(target);
+ if (!conduit) {
+ throw new Error(`${name} for closed conduit ${target}: ${uneval(arg)}`);
+ }
+ return conduit._recv(name, arg, { sender, query, actor: this });
+ }
+
+ /**
+ * JSWindowActor method, ensure cleanup.
+ */
+ didDestroy() {
+ for (let conduit of this.conduits.values()) {
+ conduit.close(true);
+ }
+ this.conduits.clear();
+ }
+}
+
+/**
+ * Child side of the Conduits process actor. Same code as JSWindowActor.
+ */
+export class ProcessConduitsChild extends JSProcessActorChild {
+ constructor() {
+ super();
+ this.conduits = new Map();
+ }
+
+ openConduit = ConduitsChild.prototype.openConduit;
+ receiveMessage = ConduitsChild.prototype.receiveMessage;
+ willDestroy = ConduitsChild.prototype.willDestroy;
+ didDestroy = ConduitsChild.prototype.didDestroy;
+}