Bug 1539764 - Add a targetFront attribute to the Front class to retrieve the target...
[gecko.git] / devtools / shared / protocol / Front.js
blob6c3b1da8730d341f8553897bc508eeb4a778cf81
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 "use strict";
7 var { settleAll } = require("devtools/shared/DevToolsUtils");
8 var EventEmitter = require("devtools/shared/event-emitter");
10 var { Pool } = require("./Pool");
11 var {
12   getStack,
13   callFunctionWithAsyncStack,
14 } = require("devtools/shared/platform/stack");
15 // Bug 1454373: devtools/shared/defer still uses Promise.jsm which is slower
16 // than DOM Promises. So implement our own copy of `defer` based on DOM Promises.
17 function defer() {
18   let resolve, reject;
19   const promise = new Promise(function() {
20     resolve = arguments[0];
21     reject = arguments[1];
22   });
23   return {
24     resolve: resolve,
25     reject: reject,
26     promise: promise,
27   };
30 /**
31  * Base class for client-side actor fronts.
32  *
33  * @param optional conn
34  *   Either a DebuggerServerConnection or a DebuggerClient.  Must have
35  *   addActorPool, removeActorPool, and poolFor.
36  *   conn can be null if the subclass provides a conn property.
37  * @constructor
38  */
39 class Front extends Pool {
40   constructor(conn = null) {
41     super(conn);
42     this.actorID = null;
43     // The targetFront attribute represents the debuggable context. Only target-scoped
44     // fronts and their children fronts will have the targetFront attribute set.
45     this.targetFront = null;
46     this._requests = [];
48     // Front listener functions registered via `onFront` get notified
49     // of new fronts via this dedicated EventEmitter object.
50     this._frontListeners = new EventEmitter();
52     // List of optional listener for each event, that is processed immediatly on packet
53     // receival, before emitting event via EventEmitter on the Front.
54     // These listeners are register via Front.before function.
55     // Map(Event Name[string] => Event Listener[function])
56     this._beforeListeners = new Map();
57   }
59   destroy() {
60     // Reject all outstanding requests, they won't make sense after
61     // the front is destroyed.
62     while (this._requests && this._requests.length > 0) {
63       const { deferred, to, type, stack } = this._requests.shift();
64       const msg =
65         "Connection closed, pending request to " +
66         to +
67         ", type " +
68         type +
69         " failed" +
70         "\n\nRequest stack:\n" +
71         stack.formattedStack;
72       deferred.reject(new Error(msg));
73     }
74     super.destroy();
75     this.clearEvents();
76     this.actorID = null;
77     this._frontListeners = null;
78     this._beforeListeners = null;
79   }
81   manage(front) {
82     if (!front.actorID) {
83       throw new Error(
84         "Can't manage front without an actor ID.\n" +
85           "Ensure server supports " +
86           front.typeName +
87           "."
88       );
89     }
90     super.manage(front);
92     // Call listeners registered via `onFront` method
93     this._frontListeners.emit(front.typeName, front);
94   }
96   // Run callback on every front of this type that currently exists, and on every
97   // instantiation of front type in the future.
98   onFront(typeName, callback) {
99     // First fire the callback on already instantiated fronts
100     for (const front of this.poolChildren()) {
101       if (front.typeName == typeName) {
102         callback(front);
103       }
104     }
105     // Then register the callback for fronts instantiated in the future
106     this._frontListeners.on(typeName, callback);
107   }
109   /**
110    * Register an event listener that will be called immediately on packer receival.
111    * The given callback is going to be called before emitting the event via EventEmitter
112    * API on the Front. Event emitting will be delayed if the callback is async.
113    * Only one such listener can be registered per type of event.
114    *
115    * @param String type
116    *   Event emitted by the actor to intercept.
117    * @param Function callback
118    *   Function that will process the event.
119    */
120   before(type, callback) {
121     if (this._beforeListeners.has(type)) {
122       throw new Error(
123         `Can't register multiple before listeners for "${type}".`
124       );
125     }
126     this._beforeListeners.set(type, callback);
127   }
129   toString() {
130     return "[Front for " + this.typeName + "/" + this.actorID + "]";
131   }
133   /**
134    * Update the actor from its representation.
135    * Subclasses should override this.
136    */
137   form(form) {}
139   /**
140    * Send a packet on the connection.
141    */
142   send(packet) {
143     if (packet.to) {
144       this.conn._transport.send(packet);
145     } else {
146       packet.to = this.actorID;
147       // The connection might be closed during the promise resolution
148       if (this.conn._transport) {
149         this.conn._transport.send(packet);
150       }
151     }
152   }
154   /**
155    * Send a two-way request on the connection.
156    */
157   request(packet) {
158     const deferred = defer();
159     // Save packet basics for debugging
160     const { to, type } = packet;
161     this._requests.push({
162       deferred,
163       to: to || this.actorID,
164       type,
165       stack: getStack(),
166     });
167     this.send(packet);
168     return deferred.promise;
169   }
171   /**
172    * Handler for incoming packets from the client's actor.
173    */
174   onPacket(packet) {
175     // Pick off event packets
176     const type = packet.type || undefined;
177     if (this._clientSpec.events && this._clientSpec.events.has(type)) {
178       const event = this._clientSpec.events.get(packet.type);
179       let args;
180       try {
181         args = event.request.read(packet, this);
182       } catch (ex) {
183         console.error("Error reading event: " + packet.type);
184         console.exception(ex);
185         throw ex;
186       }
187       // Check for "pre event" callback to be processed before emitting events on fronts
188       // Use event.name instead of packet.type to use specific event name instead of RDP
189       // packet's type.
190       const beforeEvent = this._beforeListeners.get(event.name);
191       if (beforeEvent) {
192         const result = beforeEvent.apply(this, args);
193         // Check to see if the beforeEvent returned a promise -- if so,
194         // wait for their resolution before emitting. Otherwise, emit synchronously.
195         if (result && typeof result.then == "function") {
196           result.then(() => {
197             super.emit(event.name, ...args);
198           });
199           return;
200         }
201       }
203       super.emit(event.name, ...args);
204       return;
205     }
207     // Remaining packets must be responses.
208     if (this._requests.length === 0) {
209       const msg =
210         "Unexpected packet " + this.actorID + ", " + JSON.stringify(packet);
211       const err = Error(msg);
212       console.error(err);
213       throw err;
214     }
216     const { deferred, stack } = this._requests.shift();
217     callFunctionWithAsyncStack(
218       () => {
219         if (packet.error) {
220           // "Protocol error" is here to avoid TBPL heuristics. See also
221           // https://dxr.mozilla.org/webtools-central/source/tbpl/php/inc/GeneralErrorFilter.php
222           let message;
223           if (packet.error && packet.message) {
224             message =
225               "Protocol error (" + packet.error + "): " + packet.message;
226           } else {
227             message = packet.error;
228           }
229           deferred.reject(message);
230         } else {
231           deferred.resolve(packet);
232         }
233       },
234       stack,
235       "DevTools RDP"
236     );
237   }
239   hasRequests() {
240     return !!this._requests.length;
241   }
243   /**
244    * Wait for all current requests from this front to settle.  This is especially useful
245    * for tests and other utility environments that may not have events or mechanisms to
246    * await the completion of requests without this utility.
247    *
248    * @return Promise
249    *         Resolved when all requests have settled.
250    */
251   waitForRequestsToSettle() {
252     return settleAll(this._requests.map(({ deferred }) => deferred.promise));
253   }
256 exports.Front = Front;