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/. */
9 } = require("resource://devtools/client/shared/vendor/react.js");
10 const ReactDOM
= require("resource://devtools/client/shared/vendor/react-dom.js");
14 } = require("resource://devtools/client/shared/vendor/react-redux.js");
16 const actions
= require("resource://devtools/client/webconsole/actions/index.js");
19 } = require("resource://devtools/client/webconsole/store.js");
23 } = require("resource://devtools/client/webconsole/utils/messages.js");
25 getMutableMessagesById
,
27 getAllNetworkMessagesUpdateById
,
28 } = require("resource://devtools/client/webconsole/selectors/messages.js");
30 const EventEmitter
= require("resource://devtools/shared/event-emitter.js");
31 const App
= createFactory(
32 require("resource://devtools/client/webconsole/components/App.js")
36 } = require("resource://devtools/client/webconsole/selectors/filters.js");
38 loader
.lazyGetter(this, "AppErrorBoundary", () =>
40 require("resource://devtools/client/shared/components/AppErrorBoundary.js")
45 setupServiceContainer
,
46 } = require("resource://devtools/client/webconsole/service-container.js");
48 loader
.lazyRequireGetter(
51 "resource://devtools/client/webconsole/constants.js"
54 // Localized strings for (devtools/client/locales/en-US/startup.properties)
55 loader
.lazyGetter(this, "L10N", function () {
56 const { LocalizationHelper
} = require("resource://devtools/shared/l10n.js");
57 return new LocalizationHelper("devtools/client/locales/startup.properties");
60 // Only Browser Console needs Fluent bundles at the moment
61 loader
.lazyRequireGetter(
64 "resource://devtools/client/shared/fluent-l10n/fluent-l10n.js",
67 loader
.lazyRequireGetter(
69 "LocalizationProvider",
70 "resource://devtools/client/shared/vendor/fluent-react.js",
76 class WebConsoleWrapper
{
79 * @param {HTMLElement} parentNode
80 * @param {WebConsoleUI} webConsoleUI
81 * @param {Toolbox} toolbox
82 * @param {Document} document
85 constructor(parentNode
, webConsoleUI
, toolbox
, document
) {
86 EventEmitter
.decorate(this);
88 this.parentNode
= parentNode
;
89 this.webConsoleUI
= webConsoleUI
;
90 this.toolbox
= toolbox
;
91 this.hud
= this.webConsoleUI
.hud
;
92 this.document
= document
;
94 this.init
= this.init
.bind(this);
96 this.queuedMessageAdds
= [];
97 this.queuedMessageUpdates
= [];
98 this.queuedRequestUpdates
= [];
99 this.throttledDispatchPromise
= null;
101 this.telemetry
= this.hud
.telemetry
;
107 const { webConsoleUI
} = this;
110 if (webConsoleUI
.isBrowserConsole
) {
111 const fluentL10n
= new FluentL10n();
112 await fluentL10n
.init(["devtools/client/toolbox.ftl"]);
113 fluentBundles
= fluentL10n
.getBundles();
116 return new Promise(resolve
=> {
117 store
= configureStore(this.webConsoleUI
, {
118 // We may not have access to the toolbox (e.g. in the browser console).
119 telemetry
: this.telemetry
,
123 toolbox
: this.toolbox
,
124 commands
: this.hud
.commands
,
128 const app
= AppErrorBoundary(
130 componentName
: "Console",
131 panel
: L10N
.getStr("ToolboxTabWebconsole.label"),
134 serviceContainer
: this.getServiceContainer(),
136 onFirstMeaningfulPaint
: resolve
,
137 closeSplitConsole
: this.closeSplitConsole
.bind(this),
139 !webConsoleUI
.isBrowserConsole
||
140 Services
.prefs
.getBoolPref("devtools.chrome.enabled"),
144 // Render the root Application component.
145 if (this.parentNode
) {
146 const maybeLocalizedElement
= fluentBundles
147 ? createElement(LocalizationProvider
, { bundles
: fluentBundles
}, app
)
150 this.body
= ReactDOM
.render(
155 createProvider(this.hud
.commands
.targetCommand
.storeId
),
156 { store
: this.hud
.commands
.targetCommand
.store
},
157 maybeLocalizedElement
163 // If there's no parentNode, we are in a test. So we can resolve immediately.
170 // This component can be instantiated from jest test, in which case we don't have
171 // a parentNode reference.
172 if (this.parentNode
) {
173 ReactDOM
.unmountComponentAtNode(this.parentNode
);
178 * Query the reducer store for the current state of filtering
179 * a given type of message
181 * @param {String} filter
182 * Type of message to be filtered.
184 * True if this type of message should be displayed.
186 getFilterState(filter
) {
187 return getAllFilters(this.getStore().getState())[filter
];
190 dispatchMessageAdd(packet
) {
191 this.batchedMessagesAdd([packet
]);
194 dispatchMessagesAdd(messages
) {
195 this.batchedMessagesAdd(messages
);
198 dispatchNetworkMessagesDisable() {
199 const networkMessageIds
= Object
.keys(
200 getAllNetworkMessagesUpdateById(store
.getState())
202 store
.dispatch(actions
.messagesDisable(networkMessageIds
));
205 dispatchMessagesClear() {
206 // We might still have pending message additions and updates when the clear action is
207 // triggered, so we need to flush them to make sure we don't have unexpected behavior
208 // in the ConsoleOutput. *But* we want to keep any pending navigation request,
209 // as we want to keep displaying them even if we received a clear request.
211 return l
.filter(update
=> update
.isNavigationRequest
);
213 this.queuedMessageAdds
= filter(this.queuedMessageAdds
);
214 this.queuedMessageUpdates
= filter(this.queuedMessageUpdates
);
215 this.queuedRequestUpdates
= this.queuedRequestUpdates
.filter(
216 update
=> update
.data
.isNavigationRequest
219 store
?.dispatch(actions
.messagesClear());
220 this.webConsoleUI
.emitForTests("messages-cleared");
223 dispatchPrivateMessagesClear() {
224 // We might still have pending private message additions when the private messages
225 // clear action is triggered. We need to remove any private-window-issued packets from
226 // the queue so they won't appear in the output.
228 // For (network) message updates, we need to check both messages queue and the state
229 // since we can receive updates even if the message isn't rendered yet.
230 const messages
= [...getMutableMessagesById(store
.getState()).values()];
231 this.queuedMessageUpdates
= this.queuedMessageUpdates
.filter(
233 const queuedNetworkMessage
= this.queuedMessageAdds
.find(
234 p
=> p
.actor
=== actor
236 if (queuedNetworkMessage
&& isPacketPrivate(queuedNetworkMessage
)) {
240 const requestMessage
= messages
.find(
241 message
=> actor
=== message
.actor
243 if (requestMessage
&& requestMessage
.private === true) {
251 // For (network) requests updates, we can check only the state, since there must be a
252 // user interaction to get an update (i.e. the network message is displayed and thus
254 this.queuedRequestUpdates
= this.queuedRequestUpdates
.filter(({ id
}) => {
255 const requestMessage
= getMessage(store
.getState(), id
);
256 if (requestMessage
&& requestMessage
.private === true) {
263 // Finally we clear the messages queue. This needs to be done here since we use it to
264 // clean the other queues.
265 this.queuedMessageAdds
= this.queuedMessageAdds
.filter(
266 p
=> !isPacketPrivate(p
)
269 store
.dispatch(actions
.privateMessagesClear());
272 dispatchTargetMessagesRemove(targetFront
) {
273 // We might still have pending packets in the queues from the target that we need to remove
274 // to prevent messages appearing in the output.
276 for (let i
= this.queuedMessageUpdates
.length
- 1; i
>= 0; i
--) {
277 const packet
= this.queuedMessageUpdates
[i
];
278 if (packet
.targetFront
== targetFront
) {
279 this.queuedMessageUpdates
.splice(i
, 1);
283 for (let i
= this.queuedRequestUpdates
.length
- 1; i
>= 0; i
--) {
284 const packet
= this.queuedRequestUpdates
[i
];
285 if (packet
.data
.targetFront
== targetFront
) {
286 this.queuedRequestUpdates
.splice(i
, 1);
290 for (let i
= this.queuedMessageAdds
.length
- 1; i
>= 0; i
--) {
291 const packet
= this.queuedMessageAdds
[i
];
292 // Keep in sync with the check done in the reducer for the TARGET_MESSAGES_REMOVE action.
294 packet
.targetFront
== targetFront
&&
295 packet
.type
!== Constants
.MESSAGE_TYPE
.COMMAND
&&
296 packet
.type
!== Constants
.MESSAGE_TYPE
.RESULT
298 this.queuedMessageAdds
.splice(i
, 1);
302 store
.dispatch(actions
.targetMessagesRemove(targetFront
));
305 dispatchMessagesUpdate(messages
) {
306 this.batchedMessagesUpdates(messages
);
309 dispatchSidebarClose() {
310 store
.dispatch(actions
.sidebarClose());
313 dispatchSplitConsoleCloseButtonToggle() {
315 actions
.splitConsoleCloseButtonToggle(
316 this.toolbox
&& this.toolbox
.currentToolId
!== "webconsole"
321 dispatchTabWillNavigate(packet
) {
322 const { ui
} = store
.getState();
324 // For the browser console, we receive tab navigation
325 // when the original top level window we attached to is closed,
326 // but we don't want to reset console history and just switch to
327 // the next available window.
328 if (ui
.persistLogs
|| this.webConsoleUI
.isBrowserConsole
) {
329 // Add a type in order for this event packet to be identified by
330 // utils/messages.js's `transformPacket`
331 packet
.type
= "will-navigate";
332 this.dispatchMessageAdd(packet
);
334 this.dispatchMessagesClear();
336 type
: Constants
.WILL_NAVIGATE
,
341 batchedMessagesUpdates(messages
) {
342 if (messages
.length
) {
343 this.queuedMessageUpdates
.push(...messages
);
344 this.setTimeoutIfNeeded();
348 batchedRequestUpdates(message
) {
349 this.queuedRequestUpdates
.push(message
);
350 return this.setTimeoutIfNeeded();
353 batchedMessagesAdd(messages
) {
354 if (messages
.length
) {
355 this.queuedMessageAdds
.push(...messages
);
356 this.setTimeoutIfNeeded();
360 dispatchClearHistory() {
361 store
.dispatch(actions
.clearHistory());
366 * @param {String} expression: The expression to evaluate
368 dispatchEvaluateExpression(expression
) {
369 store
.dispatch(actions
.evaluateExpression(expression
));
372 dispatchUpdateInstantEvaluationResultForCurrentExpression() {
373 store
.dispatch(actions
.updateInstantEvaluationResultForCurrentExpression());
377 * Returns a Promise that resolves once any async dispatch is finally dispatched.
379 waitAsyncDispatches() {
380 if (!this.throttledDispatchPromise
) {
381 return Promise
.resolve();
383 // When closing the console during initialization,
384 // setTimeoutIfNeeded may never resolve its promise
385 // as window.setTimeout will be disabled on document destruction.
386 const onUnload
= new Promise(r
=>
387 window
.addEventListener("unload", r
, { once
: true })
389 return Promise
.race([this.throttledDispatchPromise
, onUnload
]);
392 setTimeoutIfNeeded() {
393 if (this.throttledDispatchPromise
) {
394 return this.throttledDispatchPromise
;
396 this.throttledDispatchPromise
= new Promise(done
=> {
397 setTimeout(async () => {
398 this.throttledDispatchPromise
= null;
401 // The store is not initialized yet, we can call setTimeoutIfNeeded so the
402 // messages will be handled in the next timeout when the store is ready.
403 this.setTimeoutIfNeeded();
408 const { ui
} = store
.getState();
410 actions
.messagesAdd(this.queuedMessageAdds
, null, ui
.persistLogs
)
413 const { length
} = this.queuedMessageAdds
;
415 // This telemetry event is only useful when we have a toolbox so only
416 // send it when we have one.
418 this.telemetry
.addEventProperty(
428 this.queuedMessageAdds
= [];
430 if (this.queuedMessageUpdates
.length
) {
431 await store
.dispatch(
432 actions
.networkMessageUpdates(this.queuedMessageUpdates
, null)
434 this.webConsoleUI
.emitForTests("network-messages-updated");
435 this.queuedMessageUpdates
= [];
437 if (this.queuedRequestUpdates
.length
) {
438 await store
.dispatch(
439 actions
.networkUpdateRequests(this.queuedRequestUpdates
)
441 const updateCount
= this.queuedRequestUpdates
.length
;
442 this.queuedRequestUpdates
= [];
444 // Fire an event indicating that all data fetched from
445 // the backend has been received. This is based on
446 // 'FirefoxDataProvider.isQueuePayloadReady', see more
447 // comments in that method.
448 // (netmonitor/src/connector/firefox-data-provider).
449 // This event might be utilized in tests to find the right
450 // time when to finish.
452 this.webConsoleUI
.emitForTests(
453 "network-request-payload-ready",
460 return this.throttledDispatchPromise
;
467 getServiceContainer() {
468 if (!this.#serviceContainer
) {
469 this.#serviceContainer
= setupServiceContainer({
470 webConsoleUI
: this.webConsoleUI
,
471 toolbox
: this.toolbox
,
473 webConsoleWrapper
: this,
476 return this.#serviceContainer
;
479 subscribeToStore(callback
) {
480 store
.subscribe(() => callback(store
.getState()));
483 createElement(nodename
) {
484 return this.document
.createElement(nodename
);
487 // Called by pushing close button.
488 closeSplitConsole() {
489 this.toolbox
.closeSplitConsole();
492 toggleOriginalVariableMappingEvaluationNotification(show
) {
494 actions
.showEvaluationNotification(
495 show
? Constants
.ORIGINAL_VARIABLE_MAPPING
: ""
501 // Exports from this module
502 module
.exports
= WebConsoleWrapper
;