Fixed potential race cases in bridge/store/backend initialization
Brian Vaughn committed
Jan 31, 2019 at 14:27 UTC
bdfaeede6dd3715228ff34415b90d2322ec16b05
5 files changed
+114
-96
shells/browser/shared/src/contentScript.js
+36
-15
@@ -1,21 +1,17 @@
1
/* global chrome */
2
3
-// proxy from main page to devtools (via the background page)
4
-var port = chrome.runtime.connect({
5
- name: 'content-script',
6
-});
7
-
8
-port.onMessage.addListener(handleMessageFromDevtools);
9
-port.onDisconnect.addListener(handleDisconnect);
10
-window.addEventListener('message', handleMessageFromPage);
3
+let backendDisconnected: boolean = false;
4
+let backendInitialized: boolean = false;
5
12
-window.postMessage(
13
- {
14
- source: 'react-devtools-content-script',
15
- hello: true,
16
- },
17
- '*'
18
-);
6
+function sayHelloToBackend() {
7
+ window.postMessage(
8
+ {
9
+ source: 'react-devtools-content-script',
10
+ hello: true,
11
+ },
12
+ '*'
13
+ );
14
+}
15
16
function handleMessageFromDevtools(message) {
17
window.postMessage(
@@ -33,12 +29,17 @@ function handleMessageFromPage(evt) {
29
evt.data &&
30
evt.data.source === 'react-devtools-bridge'
31
) {
32
+ backendInitialized = true;
33
+
34
port.postMessage(evt.data.payload);
35
}
36
}
37
38
function handleDisconnect() {
39
+ backendDisconnected = true;
40
+
41
window.removeEventListener('message', handleMessageFromPage);
42
+
43
window.postMessage(
44
{
45
source: 'react-devtools-content-script',
@@ -50,3 +51,23 @@ function handleDisconnect() {
51
'*'
52
);
53
}
54
+
55
+// proxy from main page to devtools (via the background page)
56
+var port = chrome.runtime.connect({
57
+ name: 'content-script',
58
+});
59
+port.onMessage.addListener(handleMessageFromDevtools);
60
+port.onDisconnect.addListener(handleDisconnect);
61
+
62
+window.addEventListener('message', handleMessageFromPage);
63
+
64
+// The backend waits to install the global hook until notified by the content script.
65
+// In the event of a page reload, the content script might be loaded before the backend is injected.
66
+// Because of this we need to poll the backend until it has been initialized.
67
+const intervalID = setInterval(() => {
68
+ if (backendInitialized || backendDisconnected) {
69
+ clearInterval(intervalID);
70
+ } else {
71
+ sayHelloToBackend();
72
+ }
73
+}, 500);
shells/browser/shared/src/inject.js
+9
-6
@@ -1,7 +1,7 @@
1
/* global chrome */
2
3
-export default function inject(scriptName: string, done: () => void) {
4
- const src = `
3
+export default function inject(scriptName: string, done: ?Function) {
4
+ const source = `
5
// the prototype stuff is in case document.createElement has been modified
6
(function () {
7
var script = document.constructor.prototype.createElement.call(document, 'script');
@@ -12,10 +12,13 @@ export default function inject(scriptName: string, done: () => void) {
12
})()
13
`;
14
15
- chrome.devtools.inspectedWindow.eval(src, function(res, err) {
16
- if (err) {
17
- console.log(err);
15
+ chrome.devtools.inspectedWindow.eval(source, function(response, error) {
16
+ if (error) {
17
+ console.log(error);
18
+ }
19
+
20
+ if (typeof done === 'function') {
21
+ done();
22
}
19
- done();
23
});
24
}
shells/browser/shared/src/panel.js
+46
-49
@@ -1,7 +1,7 @@
1
/* global chrome */
2
3
import { createElement } from 'react';
4
-import { render, unmountComponentAtNode } from 'react-dom';
4
+import { createRoot, flushSync } from 'react-dom';
5
import Bridge from 'src/bridge';
6
import Elements from 'src/devtools/views/Elements';
7
import inject from './inject';
@@ -34,58 +34,55 @@ if (IS_CHROME) {
34
}
35
}
36
37
-const node = ((document.getElementById('container'): any): HTMLElement);
37
+const container = ((document.getElementById('container'): any): HTMLElement);
38
39
-function reloadDevTools() {
40
- setTimeout(() => {
41
- unmountComponentAtNode(node);
42
-
43
- node.innerHTML = '';
39
+function injectAndInit() {
40
+ let disconnected = false;
41
45
- injectAndInit();
46
- }, 100);
47
-}
42
+ const port = chrome.runtime.connect({
43
+ name: '' + chrome.devtools.inspectedWindow.tabId,
44
+ });
45
+ port.onDisconnect.addListener(() => {
46
+ disconnected = true;
47
+ });
48
49
-function injectAndInit() {
50
- inject(chrome.runtime.getURL('build/backend.js'), () => {
51
- let disconnected = false;
52
-
53
- const port = chrome.runtime.connect({
54
- name: '' + chrome.devtools.inspectedWindow.tabId,
55
- });
56
- port.onDisconnect.addListener(() => {
57
- disconnected = true;
58
- });
59
-
60
- const bridge = new Bridge({
61
- listen(fn) {
62
- port.onMessage.addListener(message => fn(message));
63
- },
64
- send(event: string, payload: any, transferable?: Array<any>) {
65
- if (disconnected) {
66
- return;
67
- }
68
- port.postMessage({ event, payload }, transferable);
69
- },
70
- });
71
-
72
- // Reload the DevTools extension when the user navigates to a new page.
73
- function onNavigated() {
74
- chrome.devtools.network.onNavigated.removeListener(onNavigated);
75
- bridge.send('shutdown');
76
- reloadDevTools();
77
- }
78
- chrome.devtools.network.onNavigated.addListener(onNavigated);
79
-
80
- render(
81
- createElement(Elements, {
82
- bridge,
83
- browserName,
84
- themeName,
85
- }),
86
- node
87
- );
49
+ const bridge = new Bridge({
50
+ listen(fn) {
51
+ port.onMessage.addListener(message => fn(message));
52
+ },
53
+ send(event: string, payload: any, transferable?: Array<any>) {
54
+ if (disconnected) {
55
+ return;
56
+ }
57
+ port.postMessage({ event, payload }, transferable);
58
+ },
59
});
60
+
61
+ // Clear the "React not found" initial message before rendering.
62
+ container.innerHTML = '';
63
+
64
+ const root = createRoot(container);
65
+ root.render(
66
+ createElement(Elements, {
67
+ bridge,
68
+ browserName,
69
+ themeName,
70
+ })
71
+ );
72
+
73
+ // Initialize the backend only once the DevTools frontend Store has been initialized.
74
+ // Otherwise the Store may miss important initial tree op codes.
75
+ inject(chrome.runtime.getURL('build/backend.js'));
76
+
77
+ // Reload the DevTools extension when the user navigates to a new page.
78
+ function onNavigated() {
79
+ chrome.devtools.network.onNavigated.removeListener(onNavigated);
80
+
81
+ bridge.send('shutdown');
82
+
83
+ flushSync(() => root.unmount(injectAndInit));
84
+ }
85
+ chrome.devtools.network.onNavigated.addListener(onNavigated);
86
}
87
88
injectAndInit();
shells/dev/src/devtools.js
+17
-11
@@ -37,9 +37,9 @@ mountButton.addEventListener('click', function() {
37
}
38
});
39
40
-initDevTools({
41
- connect(cb) {
42
- inject('./build/backend.js', () => {
40
+inject('./build/App.js', () => {
41
+ initDevTools({
42
+ connect(cb) {
43
const bridge = new Bridge({
44
listen(fn) {
45
contentWindow.parent.addEventListener('message', ({ data }) => {
@@ -54,22 +54,28 @@ initDevTools({
54
cb(bridge);
55
56
const root = createRoot(container);
57
- root.render(
57
+ const batch = root.createBatch();
58
+ batch.render(
59
createElement(Elements, {
60
bridge,
61
browserName: 'Chrome',
62
themeName: 'light',
63
})
64
);
64
- });
65
- },
65
+ batch.then(() => {
66
+ batch.commit();
67
67
- onReload(reloadFn) {
68
- iframe.onload = reloadFn;
69
- },
70
-});
68
+ // Initialize the backend only once the DevTools frontend Store has been initialized.
69
+ // Otherwise the Store may miss important initial tree op codes.
70
+ inject('./build/backend.js');
71
+ });
72
+ },
73
72
-inject('./build/App.js');
74
+ onReload(reloadFn) {
75
+ iframe.onload = reloadFn;
76
+ },
77
+ });
78
+});
79
80
function inject(sourcePath, callback) {
81
const script = contentDocument.createElement('script');
src/bridge.js
+6
-15
@@ -15,13 +15,12 @@ export default class Bridge extends EventEmitter {
15
_messageQueue: Array<any> = [];
16
_time: number | null = null;
17
_timeoutID: TimeoutID | null = null;
18
-
19
- wall: Wall;
18
+ _wall: Wall;
19
20
constructor(wall: Wall) {
21
super();
22
24
- this.wall = wall;
23
+ this._wall = wall;
24
25
wall.listen((message: Message) => {
26
this._emit(message);
@@ -32,7 +31,7 @@ export default class Bridge extends EventEmitter {
31
const time = this._time;
32
33
if (time === null) {
35
- this.wall.send(event, payload, transferable);
34
+ this._wall.send(event, payload, transferable);
35
this._time = Date.now();
36
} else {
37
this._messageQueue.push(event, payload, transferable);
@@ -46,13 +45,9 @@ export default class Bridge extends EventEmitter {
45
}
46
}
47
49
- log(message: string): void {
50
- this.send('log', message);
51
- }
52
-
48
_flush() {
49
while (this._messageQueue.length) {
55
- this.wall.send.apply(this.wall, this._messageQueue.splice(0, 3));
50
+ this._wall.send.apply(this._wall, this._messageQueue.splice(0, 3));
51
}
52
53
if (this._timeoutID !== null) {
@@ -64,11 +59,7 @@ export default class Bridge extends EventEmitter {
59
this._time = null;
60
}
61
67
- _emit(message: string | Message) {
68
- if (typeof message === 'string') {
69
- this.emit(message);
70
- } else {
71
- this.emit(message.event, message.payload);
72
- }
62
+ _emit(message: Message) {
63
+ this.emit(message.event, message.payload);
64
}
65
}