summaryrefslogtreecommitdiffstats
path: root/toolkit/components/extensions/ConduitsChild.sys.mjs
blob: c5774ab39c67b923d74b88629970e3a6cd077c19 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
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;
}