import EventEmitter from './events';
import type {ComponentFilter, Wall, WallMessage} from './frontend/types';
import type {
InspectedElementPayload,
OwnersList,
ProfilingDataBackend,
RendererID,
DevToolsHookSettings,
ProfilingSettings,
} from 'react-devtools-shared/src/backend/types';
import type {StyleAndLayout as StyleAndLayoutPayload} from 'react-devtools-shared/src/backend/NativeStyleEditor/types';
export type BridgeProtocol = {
version: number,
minNpmVersion: string,
maxNpmVersion: string | null,
};
export const BRIDGE_PROTOCOL: Array<BridgeProtocol> = [
{
version: 0,
minNpmVersion: '"<4.11.0"',
maxNpmVersion: '"<4.11.0"',
},
{
version: 1,
minNpmVersion: '4.13.0',
maxNpmVersion: '4.21.0',
},
{
version: 2,
minNpmVersion: '4.22.0',
maxNpmVersion: null,
},
];
export const currentBridgeProtocol: BridgeProtocol =
BRIDGE_PROTOCOL[BRIDGE_PROTOCOL.length - 1];
type ElementAndRendererID = {id: number, rendererID: RendererID};
export type Message = WallMessage;
type QueuedMessage = {
event: string,
payload: mixed,
};
type EventArguments<Payload> = Payload extends void ? [] : [Payload];
type EventEmitterEvents<Events: Object> = {
[Event in keyof Events]: EventArguments<Events[Event]>,
};
type HighlightHostInstance = {
...ElementAndRendererID,
displayName: string | null,
hideAfterTimeout: boolean,
openBuiltinElementsPanel: boolean,
scrollIntoView: boolean,
};
type HighlightHostInstances = {
elements: Array<ElementAndRendererID>,
displayName: string | null,
hideAfterTimeout: boolean,
scrollIntoView: boolean,
};
type ScrollToHostInstance = {
...ElementAndRendererID,
};
type OverrideValue = {
...ElementAndRendererID,
path: Array<string | number>,
wasForwarded?: boolean,
value: mixed,
};
type OverrideHookState = {
...OverrideValue,
hookID: number,
};
type PathType = 'props' | 'hooks' | 'state' | 'context';
type DeletePath = {
...ElementAndRendererID,
type: PathType,
hookID?: ?number,
path: Array<string | number>,
};
type RenamePath = {
...ElementAndRendererID,
type: PathType,
hookID?: ?number,
oldPath: Array<string | number>,
newPath: Array<string | number>,
};
type OverrideValueAtPath = {
...ElementAndRendererID,
type: PathType,
hookID?: ?number,
path: Array<string | number>,
value: mixed,
};
type OverrideError = {
...ElementAndRendererID,
forceError: boolean,
};
type OverrideSuspense = {
...ElementAndRendererID,
forceFallback: boolean,
};
type OverrideSuspenseMilestone = {
rendererID: number,
suspendedSet: Array<number>,
};
type CopyElementPathParams = {
...ElementAndRendererID,
path: Array<string | number>,
};
type ViewAttributeSourceParams = {
...ElementAndRendererID,
path: Array<string | number>,
};
type InspectElementParams = {
...ElementAndRendererID,
forceFullData: boolean,
path: Array<number | string> | null,
requestID: number,
};
type InspectScreenParams = {
requestID: number,
id: number,
forceFullData: boolean,
path: Array<number | string> | null,
};
type StoreAsGlobalParams = {
...ElementAndRendererID,
count: number,
path: Array<string | number>,
};
type NativeStyleEditor_RenameAttributeParams = {
...ElementAndRendererID,
oldName: string,
newName: string,
value: string,
};
type NativeStyleEditor_SetValueParams = {
...ElementAndRendererID,
name: string,
value: string,
};
export type SavedPreferencesParams = {
componentFilters: Array<ComponentFilter>,
};
export type BackendEvents = {
backendInitialized: void,
backendVersion: string,
bridgeProtocol: BridgeProtocol,
extensionBackendInitialized: void,
fastRefreshScheduled: void,
getSavedPreferences: void,
inspectedElement: InspectedElementPayload,
inspectedScreen: InspectedElementPayload,
isReloadAndProfileSupportedByBackend: boolean,
operations: Array<number>,
ownersList: OwnersList,
environmentNames: Array<string>,
profilingData: ProfilingDataBackend,
profilingStatus: boolean,
reloadAppForProfiling: void,
saveToClipboard: string,
selectElement: number | null,
shutdown: void,
stopInspectingHost: boolean,
scrollTo: {left: number, top: number, right: number, bottom: number},
syncSelectionToBuiltinElementsPanel: void,
unsupportedRendererVersion: void,
extensionComponentsPanelShown: void,
extensionComponentsPanelHidden: void,
resumeElementPolling: void,
pauseElementPolling: void,
isNativeStyleEditorSupported: {
isSupported: boolean,
validAttributes: ?$ReadOnlyArray<string>,
},
NativeStyleEditor_styleAndLayout: StyleAndLayoutPayload,
hookSettings: $ReadOnly<DevToolsHookSettings>,
};
type StartProfilingParams = ProfilingSettings;
type ReloadAndProfilingParams = ProfilingSettings;
export type FrontendEvents = {
clearErrorsAndWarnings: {rendererID: RendererID},
clearErrorsForElementID: ElementAndRendererID,
clearHostInstanceHighlight: void,
clearWarningsForElementID: ElementAndRendererID,
copyElementPath: CopyElementPathParams,
deletePath: DeletePath,
getBackendVersion: void,
getBridgeProtocol: void,
getIfHasUnsupportedRendererVersion: void,
getOwnersList: ElementAndRendererID,
getProfilingData: {rendererID: RendererID},
getProfilingStatus: void,
highlightHostInstance: HighlightHostInstance,
highlightHostInstances: HighlightHostInstances,
inspectElement: InspectElementParams,
inspectScreen: InspectScreenParams,
logElementToConsole: ElementAndRendererID,
overrideError: OverrideError,
overrideSuspense: OverrideSuspense,
overrideSuspenseMilestone: OverrideSuspenseMilestone,
overrideValueAtPath: OverrideValueAtPath,
profilingData: ProfilingDataBackend,
reloadAndProfile: ReloadAndProfilingParams,
renamePath: RenamePath,
savedPreferences: SavedPreferencesParams,
setTraceUpdatesEnabled: boolean,
shutdown: void,
startInspectingHost: boolean,
startProfiling: StartProfilingParams,
stopInspectingHost: void,
scrollToHostInstance: ScrollToHostInstance,
scrollTo: {left: number, top: number, right: number, bottom: number},
requestScrollPosition: void,
stopProfiling: void,
storeAsGlobal: StoreAsGlobalParams,
updateComponentFilters: Array<ComponentFilter>,
getEnvironmentNames: void,
updateHookSettings: $ReadOnly<DevToolsHookSettings>,
viewAttributeSource: ViewAttributeSourceParams,
viewElementSource: ElementAndRendererID,
syncSelectionFromBuiltinElementsPanel: void,
NativeStyleEditor_measure: ElementAndRendererID,
NativeStyleEditor_renameAttribute: NativeStyleEditor_RenameAttributeParams,
NativeStyleEditor_setValue: NativeStyleEditor_SetValueParams,
overrideContext: OverrideValue,
overrideHookState: OverrideHookState,
overrideProps: OverrideValue,
overrideState: OverrideValue,
getHookSettings: void,
};
class Bridge<
OutgoingEvents: Object,
IncomingEvents: Object,
> extends EventEmitter<EventEmitterEvents<IncomingEvents>> {
_isShutdown: boolean = false;
_messageQueue: Array<QueuedMessage> = [];
_scheduledFlush: boolean = false;
_wall: Wall;
_wallUnlisten: (() => void) | null = null;
constructor(wall: Wall) {
super();
this._wall = wall;
const wallUnlisten = wall.listen(this._handleMessage);
if (typeof wallUnlisten !== 'function') {
throw new TypeError('Wall.listen() must return an unlisten function.');
}
this._wallUnlisten = wallUnlisten;
this.addListener('overrideValueAtPath', this.overrideValueAtPath);
}
get wall(): Wall {
return this._wall;
}
addListener<Event: $Keys<EventEmitterEvents<IncomingEvents>>>(
event: Event,
listener: (...EventEmitterEvents<IncomingEvents>[Event]) => mixed,
): void {
this._assertNotShutdown('add a listener');
super.addListener(event, listener);
}
emit<Event: $Keys<EventEmitterEvents<IncomingEvents>>>(
event: Event,
...args: EventEmitterEvents<IncomingEvents>[Event]
): void {
this._assertNotShutdown('emit an event');
super.emit(event, ...args);
}
send<EventName: $Keys<OutgoingEvents>>(
event: EventName,
payload?: OutgoingEvents[EventName],
): void {
this._assertNotShutdown('send a message');
if (typeof event !== 'string' || event.length === 0) {
throw new TypeError('Bridge event names must be non-empty strings.');
}
this._messageQueue.push({
event,
payload,
});
if (!this._scheduledFlush) {
this._scheduledFlush = true;
if (typeof devtoolsJestTestScheduler === 'function') {
devtoolsJestTestScheduler(this._flush);
} else {
queueMicrotask(this._flush);
}
}
}
shutdown(): void {
this._assertNotShutdown('shut down');
this.emit('shutdown');
this.send('shutdown');
this._isShutdown = true;
this.removeAllListeners();
const wallUnlisten = this._wallUnlisten;
this._wallUnlisten = null;
try {
if (wallUnlisten !== null) {
wallUnlisten();
}
} finally {
do {
this._flush();
} while (this._messageQueue.length);
}
}
_flush: () => void = () => {
try {
if (this._messageQueue.length) {
for (let i = 0; i < this._messageQueue.length; i++) {
const {event, payload} = this._messageQueue[i];
this._wall.send(event, payload);
}
this._messageQueue.length = 0;
}
} finally {
this._scheduledFlush = false;
}
};
_assertNotShutdown(action: string): void {
if (this._isShutdown) {
throw new Error(
`Cannot ${action} through a Bridge that has been shut down.`,
);
}
}
_handleMessage: (message: mixed) => void = message => {
if (
message === null ||
typeof message !== 'object' ||
!('event' in message)
) {
return;
}
const event = message.event;
if (typeof event !== 'string' || event.length === 0) {
throw new TypeError('Bridge event names must be non-empty strings.');
}
this._assertNotShutdown('receive a message');
(this as any).emit(event, message.payload);
};
overrideValueAtPath: OverrideValueAtPath => void = ({
id,
path,
rendererID,
type,
value,
}: OverrideValueAtPath) => {
switch (type) {
case 'context':
this.send('overrideContext', {
id,
path,
rendererID,
wasForwarded: true,
value,
});
break;
case 'hooks':
this.send('overrideHookState', {
id,
path,
rendererID,
wasForwarded: true,
value,
});
break;
case 'props':
this.send('overrideProps', {
id,
path,
rendererID,
wasForwarded: true,
value,
});
break;
case 'state':
this.send('overrideState', {
id,
path,
rendererID,
wasForwarded: true,
value,
});
break;
}
};
}
export type BackendBridge = Bridge<BackendEvents, FrontendEvents>;
export type FrontendBridge = Bridge<FrontendEvents, BackendEvents>;
export default Bridge;