@samitouri / QOS-React-2 / commits / bdfaeede6d

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 }