react/src/bridge.js

223 lines
6.6 KiB
JavaScript
Raw Normal View History

2019-01-23 03:04:37 +08:00
// @flow
import EventEmitter from 'events';
import type { ComponentFilter, Wall } from './types';
import type {
InspectedElementPayload,
OwnersList,
ProfilingDataBackend,
RendererID,
} from 'src/backend/types';
import type { StyleAndLayout as StyleAndLayoutPayload } from 'src/backend/NativeStyleEditor/types';
2019-01-23 03:04:37 +08:00
const BATCH_DURATION = 100;
type ElementAndRendererID = {| id: number, rendererID: RendererID |};
2019-01-23 03:04:37 +08:00
type Message = {|
event: string,
payload: any,
|};
type HighlightElementInDOM = {|
...ElementAndRendererID,
displayName: string,
hideAfterTimeout: boolean,
openNativeElementsPanel: boolean,
scrollIntoView: boolean,
|};
type OverrideValue = {|
...ElementAndRendererID,
path: Array<string | number>,
value: any,
|};
type OverrideHookState = {|
...OverrideValue,
hookID: number,
|};
type OverrideSuspense = {|
...ElementAndRendererID,
forceFallback: boolean,
|};
type InspectElementParams = {|
...ElementAndRendererID,
path?: Array<string | number>,
|};
type NativeStyleEditor_RenameAttributeParams = {|
...ElementAndRendererID,
oldName: string,
newName: string,
value: string,
|};
type NativeStyleEditor_SetValueParams = {|
...ElementAndRendererID,
name: string,
value: string,
|};
export default class Bridge extends EventEmitter<{|
captureScreenshot: [{| commitIndex: number, rootID: number |}],
clearNativeElementHighlight: [],
getOwnersList: [ElementAndRendererID],
getProfilingData: [{| rendererID: RendererID |}],
getProfilingStatus: [],
highlightNativeElement: [HighlightElementInDOM],
init: [],
inspectElement: [InspectElementParams],
inspectedElement: [InspectedElementPayload],
isBackendStorageAPISupported: [boolean],
logElementToConsole: [ElementAndRendererID],
operations: [Array<number>],
ownersList: [OwnersList],
overrideComponentFilters: [Array<ComponentFilter>],
overrideContext: [OverrideValue],
overrideHookState: [OverrideHookState],
overrideProps: [OverrideValue],
overrideState: [OverrideValue],
overrideSuspense: [OverrideSuspense],
profilingData: [ProfilingDataBackend],
profilingStatus: [boolean],
reloadAndProfile: [boolean],
reloadAppForProfiling: [],
screenshotCaptured: [
{| commitIndex: number, dataURL: string, rootID: number |},
],
selectElement: [ElementAndRendererID],
selectFiber: [number],
shutdown: [],
startInspectingNative: [],
startProfiling: [boolean],
stopInspectingNative: [boolean],
stopProfiling: [],
syncSelectionFromNativeElementsPanel: [],
syncSelectionToNativeElementsPanel: [],
updateAppendComponentStack: [boolean],
updateComponentFilters: [Array<ComponentFilter>],
viewElementSource: [ElementAndRendererID],
// React Native style editor plug-in.
isNativeStyleEditorSupported: [
{| isSupported: boolean, validAttributes: $ReadOnlyArray<string> |},
],
NativeStyleEditor_measure: [ElementAndRendererID],
NativeStyleEditor_renameAttribute: [NativeStyleEditor_RenameAttributeParams],
NativeStyleEditor_setValue: [NativeStyleEditor_SetValueParams],
NativeStyleEditor_styleAndLayout: [StyleAndLayoutPayload],
|}> {
_isShutdown: boolean = false;
_messageQueue: Array<any> = [];
2019-01-23 03:04:37 +08:00
_timeoutID: TimeoutID | null = null;
_wall: Wall;
Fix for 'Attempting to use a disconnected port object' Fixes https://github.com/bvaughn/react-devtools-experimental/issues/217 The error reproduces with any two React websites, e.g. `https://reactjs.org` and `https://nextjs.org`, by keeping the DevTools Components tab open and switching between these websites in the same browser tab. There are several issues with the code that contribute to this: 1. `Bridge` leaves behind a dangling timer that fires `_flush` after the bridge has been abandoned ("shutdown"). 2. `bridge.send('shutdown')` is asynchronous, so the event handlers do not get unsubscribed in time. 3. `port.onDisconnect` does not trigger on in-tab navigation like new URL or back/forward navigation. 4. State management design of the code that uses shared variables and callbacks makes it hard to handle race conditions originating from the browser. This commit cleans up some of the lacking symmetry when using `addListener`/`removeListener`, but the code in `shells/browser/shared/src/main.js` is hard to reason about with regards to race conditions, and there are many possible race conditions originating from the browser, so maybe there could be a better design paradigm (like a formal state machine) to manage the state changes in response to sequences of events than plain old event listeners, callbacks, and shared variables. Unrelated, but clicking Chrome Back/Forward/Back/Forward very fast makes the browser and the DevTools and the DevTools of DevTools stall and become unresponsive for some time, then recovers but the Back/Forward/Stop/Refresh button and favicon loading indicator may remain broken. Looks like a Chrome bug, some kind of a temporary deadlock in handling the browser history.
2019-04-26 19:30:04 +08:00
_wallUnlisten: Function | null = null;
2019-01-23 03:04:37 +08:00
constructor(wall: Wall) {
2019-01-23 03:04:37 +08:00
super();
this._wall = wall;
this._wallUnlisten =
wall.listen((message: Message) => {
(this: any).emit(message.event, message.payload);
}) || null;
2019-01-23 03:04:37 +08:00
}
// Listening directly to the wall isn't advised.
// It can be used to listen for legacy (v3) messages (since they use a different format).
get wall(): Wall {
return this._wall;
}
send(event: string, payload: any, transferable?: Array<any>) {
if (this._isShutdown) {
console.warn(
`Cannot send message "${event}" through a Bridge that has been shutdown.`
);
Fix for 'Attempting to use a disconnected port object' Fixes https://github.com/bvaughn/react-devtools-experimental/issues/217 The error reproduces with any two React websites, e.g. `https://reactjs.org` and `https://nextjs.org`, by keeping the DevTools Components tab open and switching between these websites in the same browser tab. There are several issues with the code that contribute to this: 1. `Bridge` leaves behind a dangling timer that fires `_flush` after the bridge has been abandoned ("shutdown"). 2. `bridge.send('shutdown')` is asynchronous, so the event handlers do not get unsubscribed in time. 3. `port.onDisconnect` does not trigger on in-tab navigation like new URL or back/forward navigation. 4. State management design of the code that uses shared variables and callbacks makes it hard to handle race conditions originating from the browser. This commit cleans up some of the lacking symmetry when using `addListener`/`removeListener`, but the code in `shells/browser/shared/src/main.js` is hard to reason about with regards to race conditions, and there are many possible race conditions originating from the browser, so maybe there could be a better design paradigm (like a formal state machine) to manage the state changes in response to sequences of events than plain old event listeners, callbacks, and shared variables. Unrelated, but clicking Chrome Back/Forward/Back/Forward very fast makes the browser and the DevTools and the DevTools of DevTools stall and become unresponsive for some time, then recovers but the Back/Forward/Stop/Refresh button and favicon loading indicator may remain broken. Looks like a Chrome bug, some kind of a temporary deadlock in handling the browser history.
2019-04-26 19:30:04 +08:00
return;
}
// When we receive a message:
// - we add it to our queue of messages to be sent
// - if there hasn't been a message recently, we set a timer for 0 ms in
// the future, allowing all messages created in the same tick to be sent
// together
// - if there *has* been a message flushed in the last BATCH_DURATION ms
// (or we're waiting for our setTimeout-0 to fire), then _timeoutID will
// be set, and we'll simply add to the queue and wait for that
this._messageQueue.push(event, payload, transferable);
if (!this._timeoutID) {
this._timeoutID = setTimeout(this._flush, 0);
2019-01-23 03:04:37 +08:00
}
}
Fix for 'Attempting to use a disconnected port object' Fixes https://github.com/bvaughn/react-devtools-experimental/issues/217 The error reproduces with any two React websites, e.g. `https://reactjs.org` and `https://nextjs.org`, by keeping the DevTools Components tab open and switching between these websites in the same browser tab. There are several issues with the code that contribute to this: 1. `Bridge` leaves behind a dangling timer that fires `_flush` after the bridge has been abandoned ("shutdown"). 2. `bridge.send('shutdown')` is asynchronous, so the event handlers do not get unsubscribed in time. 3. `port.onDisconnect` does not trigger on in-tab navigation like new URL or back/forward navigation. 4. State management design of the code that uses shared variables and callbacks makes it hard to handle race conditions originating from the browser. This commit cleans up some of the lacking symmetry when using `addListener`/`removeListener`, but the code in `shells/browser/shared/src/main.js` is hard to reason about with regards to race conditions, and there are many possible race conditions originating from the browser, so maybe there could be a better design paradigm (like a formal state machine) to manage the state changes in response to sequences of events than plain old event listeners, callbacks, and shared variables. Unrelated, but clicking Chrome Back/Forward/Back/Forward very fast makes the browser and the DevTools and the DevTools of DevTools stall and become unresponsive for some time, then recovers but the Back/Forward/Stop/Refresh button and favicon loading indicator may remain broken. Looks like a Chrome bug, some kind of a temporary deadlock in handling the browser history.
2019-04-26 19:30:04 +08:00
shutdown() {
if (this._isShutdown) {
console.warn('Bridge was already shutdown.');
Fix for 'Attempting to use a disconnected port object' Fixes https://github.com/bvaughn/react-devtools-experimental/issues/217 The error reproduces with any two React websites, e.g. `https://reactjs.org` and `https://nextjs.org`, by keeping the DevTools Components tab open and switching between these websites in the same browser tab. There are several issues with the code that contribute to this: 1. `Bridge` leaves behind a dangling timer that fires `_flush` after the bridge has been abandoned ("shutdown"). 2. `bridge.send('shutdown')` is asynchronous, so the event handlers do not get unsubscribed in time. 3. `port.onDisconnect` does not trigger on in-tab navigation like new URL or back/forward navigation. 4. State management design of the code that uses shared variables and callbacks makes it hard to handle race conditions originating from the browser. This commit cleans up some of the lacking symmetry when using `addListener`/`removeListener`, but the code in `shells/browser/shared/src/main.js` is hard to reason about with regards to race conditions, and there are many possible race conditions originating from the browser, so maybe there could be a better design paradigm (like a formal state machine) to manage the state changes in response to sequences of events than plain old event listeners, callbacks, and shared variables. Unrelated, but clicking Chrome Back/Forward/Back/Forward very fast makes the browser and the DevTools and the DevTools of DevTools stall and become unresponsive for some time, then recovers but the Back/Forward/Stop/Refresh button and favicon loading indicator may remain broken. Looks like a Chrome bug, some kind of a temporary deadlock in handling the browser history.
2019-04-26 19:30:04 +08:00
return;
}
2019-05-31 00:27:28 +08:00
// Queue the shutdown outgoing message for subscribers.
this.send('shutdown');
// Mark this bridge as destroyed, i.e. disable its public API.
this._isShutdown = true;
Fix for 'Attempting to use a disconnected port object' Fixes https://github.com/bvaughn/react-devtools-experimental/issues/217 The error reproduces with any two React websites, e.g. `https://reactjs.org` and `https://nextjs.org`, by keeping the DevTools Components tab open and switching between these websites in the same browser tab. There are several issues with the code that contribute to this: 1. `Bridge` leaves behind a dangling timer that fires `_flush` after the bridge has been abandoned ("shutdown"). 2. `bridge.send('shutdown')` is asynchronous, so the event handlers do not get unsubscribed in time. 3. `port.onDisconnect` does not trigger on in-tab navigation like new URL or back/forward navigation. 4. State management design of the code that uses shared variables and callbacks makes it hard to handle race conditions originating from the browser. This commit cleans up some of the lacking symmetry when using `addListener`/`removeListener`, but the code in `shells/browser/shared/src/main.js` is hard to reason about with regards to race conditions, and there are many possible race conditions originating from the browser, so maybe there could be a better design paradigm (like a formal state machine) to manage the state changes in response to sequences of events than plain old event listeners, callbacks, and shared variables. Unrelated, but clicking Chrome Back/Forward/Back/Forward very fast makes the browser and the DevTools and the DevTools of DevTools stall and become unresponsive for some time, then recovers but the Back/Forward/Stop/Refresh button and favicon loading indicator may remain broken. Looks like a Chrome bug, some kind of a temporary deadlock in handling the browser history.
2019-04-26 19:30:04 +08:00
// Disable the API inherited from EventEmitter that can add more listeners and send more messages.
// $FlowFixMe This property is not writable.
Fix for 'Attempting to use a disconnected port object' Fixes https://github.com/bvaughn/react-devtools-experimental/issues/217 The error reproduces with any two React websites, e.g. `https://reactjs.org` and `https://nextjs.org`, by keeping the DevTools Components tab open and switching between these websites in the same browser tab. There are several issues with the code that contribute to this: 1. `Bridge` leaves behind a dangling timer that fires `_flush` after the bridge has been abandoned ("shutdown"). 2. `bridge.send('shutdown')` is asynchronous, so the event handlers do not get unsubscribed in time. 3. `port.onDisconnect` does not trigger on in-tab navigation like new URL or back/forward navigation. 4. State management design of the code that uses shared variables and callbacks makes it hard to handle race conditions originating from the browser. This commit cleans up some of the lacking symmetry when using `addListener`/`removeListener`, but the code in `shells/browser/shared/src/main.js` is hard to reason about with regards to race conditions, and there are many possible race conditions originating from the browser, so maybe there could be a better design paradigm (like a formal state machine) to manage the state changes in response to sequences of events than plain old event listeners, callbacks, and shared variables. Unrelated, but clicking Chrome Back/Forward/Back/Forward very fast makes the browser and the DevTools and the DevTools of DevTools stall and become unresponsive for some time, then recovers but the Back/Forward/Stop/Refresh button and favicon loading indicator may remain broken. Looks like a Chrome bug, some kind of a temporary deadlock in handling the browser history.
2019-04-26 19:30:04 +08:00
this.addListener = function() {};
// $FlowFixMe This property is not writable.
Fix for 'Attempting to use a disconnected port object' Fixes https://github.com/bvaughn/react-devtools-experimental/issues/217 The error reproduces with any two React websites, e.g. `https://reactjs.org` and `https://nextjs.org`, by keeping the DevTools Components tab open and switching between these websites in the same browser tab. There are several issues with the code that contribute to this: 1. `Bridge` leaves behind a dangling timer that fires `_flush` after the bridge has been abandoned ("shutdown"). 2. `bridge.send('shutdown')` is asynchronous, so the event handlers do not get unsubscribed in time. 3. `port.onDisconnect` does not trigger on in-tab navigation like new URL or back/forward navigation. 4. State management design of the code that uses shared variables and callbacks makes it hard to handle race conditions originating from the browser. This commit cleans up some of the lacking symmetry when using `addListener`/`removeListener`, but the code in `shells/browser/shared/src/main.js` is hard to reason about with regards to race conditions, and there are many possible race conditions originating from the browser, so maybe there could be a better design paradigm (like a formal state machine) to manage the state changes in response to sequences of events than plain old event listeners, callbacks, and shared variables. Unrelated, but clicking Chrome Back/Forward/Back/Forward very fast makes the browser and the DevTools and the DevTools of DevTools stall and become unresponsive for some time, then recovers but the Back/Forward/Stop/Refresh button and favicon loading indicator may remain broken. Looks like a Chrome bug, some kind of a temporary deadlock in handling the browser history.
2019-04-26 19:30:04 +08:00
this.emit = function() {};
// NOTE: There's also EventEmitter API like `on` and `prependListener` that we didn't add to our Flow type of EventEmitter.
// Unsubscribe this bridge incoming message listeners to be sure, and so they don't have to do that.
this.removeAllListeners();
// Stop accepting and emitting incoming messages from the wall.
const wallUnlisten = this._wallUnlisten;
if (wallUnlisten) {
wallUnlisten();
}
// Synchronously flush all queued outgoing messages.
// At this step the subscribers' code may run in this call stack.
do {
this._flush();
} while (this._messageQueue.length);
// Make sure once again that there is no dangling timer.
clearTimeout(this._timeoutID);
this._timeoutID = null;
}
_flush = () => {
Fix for 'Attempting to use a disconnected port object' Fixes https://github.com/bvaughn/react-devtools-experimental/issues/217 The error reproduces with any two React websites, e.g. `https://reactjs.org` and `https://nextjs.org`, by keeping the DevTools Components tab open and switching between these websites in the same browser tab. There are several issues with the code that contribute to this: 1. `Bridge` leaves behind a dangling timer that fires `_flush` after the bridge has been abandoned ("shutdown"). 2. `bridge.send('shutdown')` is asynchronous, so the event handlers do not get unsubscribed in time. 3. `port.onDisconnect` does not trigger on in-tab navigation like new URL or back/forward navigation. 4. State management design of the code that uses shared variables and callbacks makes it hard to handle race conditions originating from the browser. This commit cleans up some of the lacking symmetry when using `addListener`/`removeListener`, but the code in `shells/browser/shared/src/main.js` is hard to reason about with regards to race conditions, and there are many possible race conditions originating from the browser, so maybe there could be a better design paradigm (like a formal state machine) to manage the state changes in response to sequences of events than plain old event listeners, callbacks, and shared variables. Unrelated, but clicking Chrome Back/Forward/Back/Forward very fast makes the browser and the DevTools and the DevTools of DevTools stall and become unresponsive for some time, then recovers but the Back/Forward/Stop/Refresh button and favicon loading indicator may remain broken. Looks like a Chrome bug, some kind of a temporary deadlock in handling the browser history.
2019-04-26 19:30:04 +08:00
// This method is used after the bridge is marked as destroyed in shutdown sequence,
// so we do not bail out if the bridge marked as destroyed.
// It is a private method that the bridge ensures is only called at the right times.
clearTimeout(this._timeoutID);
this._timeoutID = null;
if (this._messageQueue.length) {
for (let i = 0; i < this._messageQueue.length; i += 3) {
this._wall.send(
this._messageQueue[i],
this._messageQueue[i + 1],
this._messageQueue[i + 2]
);
}
this._messageQueue.length = 0;
// Check again for queued messages in BATCH_DURATION ms. This will keep
// flushing in a loop as long as messages continue to be added. Once no
// more are, the timer expires.
this._timeoutID = setTimeout(this._flush, BATCH_DURATION);
}
};
2019-01-24 00:45:19 +08:00
}