Added Flow type coverage for all EventEmitter subclasses

This commit is contained in:
Brian Vaughn
2019-06-09 15:50:17 -07:00
parent b3046c6e19
commit ea03ddd583
13 changed files with 150 additions and 68 deletions
+11 -5
View File
@@ -1,11 +1,17 @@
// @flow
declare module 'events' {
declare class EventEmitter {
addListener: (type: string, fn: Function) => void;
emit: (type: string, data: any) => void;
removeListener: (type: string, fn: Function) => void;
removeAllListeners: (type?: string) => void;
declare class EventEmitter<Events: Object> {
addListener<Event: $Keys<Events>>(
event: Event,
listener: (...$ElementType<Events, Event>) => any
): void;
emit: <Event: $Keys<Events>>(
event: Event,
...$ElementType<Events, Event>
) => void;
removeListener(event: $Keys<Events>, listener: Function): void;
removeAllListeners(event?: $Keys<Events>): void;
}
declare export default typeof EventEmitter;
+7 -2
View File
@@ -3,6 +3,7 @@
import EventEmitter from 'events';
import memoize from 'memoize-one';
import throttle from 'lodash.throttle';
import Bridge from 'src/bridge';
import {
SESSION_STORAGE_LAST_SELECTION_KEY,
SESSION_STORAGE_RELOAD_AND_PROFILE_KEY,
@@ -23,7 +24,7 @@ import type {
RendererID,
RendererInterface,
} from './types';
import type { Bridge, ComponentFilter } from '../types';
import type { ComponentFilter } from '../types';
const debug = (methodName, ...args) => {
if (__DEBUG__) {
@@ -67,7 +68,9 @@ type PersistedSelection = {|
path: Array<PathFrame>,
|};
export default class Agent extends EventEmitter {
export default class Agent extends EventEmitter<{|
shutdown: [],
|}> {
_bridge: Bridge;
_isProfiling: boolean = false;
_recordChangeDescriptions: boolean = false;
@@ -273,9 +276,11 @@ export default class Agent extends EventEmitter {
screenshotCaptured = ({
commitIndex,
dataURL,
rootID,
}: {|
commitIndex: number,
dataURL: string,
rootID: number,
|}) => {
this._bridge.send('screenshotCaptured', { commitIndex, dataURL });
};
+2 -2
View File
@@ -13,10 +13,10 @@ export type ExpirationTime = number;
export type RefObject = {|
current: any,
|};
export type Source = {
export type Source = {|
fileName: string,
lineNumber: number,
};
|};
export type HookType =
| 'useState'
| 'useReducer'
+74 -3
View File
@@ -2,16 +2,85 @@
import EventEmitter from 'events';
import type { Wall } from './types';
import type { ComponentFilter, Wall } from './types';
import type {
InspectedElement,
OwnersList,
ProfilingDataBackend,
RendererID,
} from 'src/backend/types';
const BATCH_DURATION = 100;
type ElementAndRendererID = {| id: number, rendererID: RendererID |};
type Message = {|
event: string,
payload: any,
|};
export default class Bridge extends EventEmitter {
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,
|};
export default class Bridge extends EventEmitter<{|
captureScreenshot: [{| commitIndex: number, rootID: number |}],
clearHighlightedElementInDOM: [],
getOwnersList: [ElementAndRendererID],
getProfilingData: [{| rendererID: RendererID |}],
getProfilingStatus: [],
highlightElementInDOM: [HighlightElementInDOM],
init: [],
inspectElement: [ElementAndRendererID],
inspectedElement: [InspectedElement | number | null],
isBackendStorageAPISupported: [boolean],
logElementToConsole: [ElementAndRendererID],
operations: [Uint32Array],
ownersList: [OwnersList],
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: [],
startInspectingDOM: [],
startProfiling: [boolean],
stopInspectingDOM: [boolean],
stopProfiling: [],
syncSelectionFromNativeElementsPanel: [],
syncSelectionToNativeElementsPanel: [],
updateComponentFilters: [Array<ComponentFilter>],
viewElementSource: [ElementAndRendererID],
|}> {
_isShutdown: boolean = false;
_messageQueue: Array<any> = [];
_timeoutID: TimeoutID | null = null;
@@ -25,7 +94,7 @@ export default class Bridge extends EventEmitter {
this._wallUnlisten =
wall.listen((message: Message) => {
this.emit(message.event, message.payload);
(this: any).emit(message.event, message.payload);
}) || null;
}
@@ -64,7 +133,9 @@ export default class Bridge extends EventEmitter {
this._isShutdown = true;
// Disable the API inherited from EventEmitter that can add more listeners and send more messages.
// $FlowFixMe This property is not writable.
this.addListener = function() {};
// $FlowFixMe This property is not writable.
this.emit = function() {};
// NOTE: There's also EventEmitter API like `on` and `prependListener` that we didn't add to our Flow type of EventEmitter.
+6 -2
View File
@@ -3,6 +3,7 @@
import EventEmitter from 'events';
import memoize from 'memoize-one';
import throttle from 'lodash.throttle';
import Bridge from 'src/bridge';
import { prepareProfilingDataFrontendFromBackendAndStore } from './views/Profiler/utils';
import ProfilingCache from './ProfilingCache';
import Store from './store';
@@ -14,11 +15,14 @@ import type {
ProfilingDataFrontend,
SnapshotNode,
} from './views/Profiler/types';
import type { Bridge } from '../types';
const THROTTLE_CAPTURE_SCREENSHOT_DURATION = 500;
export default class ProfilerStore extends EventEmitter {
export default class ProfilerStore extends EventEmitter<{|
isProcessingData: [],
isProfiling: [],
profilingData: [],
|}> {
_bridge: Bridge;
// Suspense cache for lazily calculating derived profiling data.
+1 -1
View File
@@ -1,6 +1,6 @@
// @flow
import type { Bridge } from '../types';
import Bridge from 'src/bridge';
type Shell = {|
connect: (callback: Function) => void,
+12 -2
View File
@@ -2,6 +2,7 @@
import EventEmitter from 'events';
import { inspect } from 'util';
import Bridge from 'src/bridge';
import {
TREE_OPERATION_ADD,
TREE_OPERATION_REMOVE,
@@ -21,7 +22,7 @@ import { printStore } from 'src/__tests__/storeSerializer';
import ProfilerStore from './ProfilerStore';
import type { Element } from './views/Components/types';
import type { Bridge, ComponentFilter, ElementType } from '../types';
import type { ComponentFilter, ElementType } from '../types';
const debug = (methodName, ...args) => {
if (__DEBUG__) {
@@ -57,7 +58,16 @@ export type Capabilities = {|
* The store is the single source of truth for updates from the backend.
* ContextProviders can subscribe to the Store for specific things they want to provide.
*/
export default class Store extends EventEmitter {
export default class Store extends EventEmitter<{|
captureScreenshots: [],
collapseNodesByDefault: [],
componentFilters: [],
mutated: [[Array<number>, Map<number, number>]],
recordChangeDescriptions: [],
roots: [],
supportsProfiling: [],
supportsReloadAndProfile: [],
|}> {
_bridge: Bridge;
_captureScreenshots: boolean = false;
@@ -14,28 +14,33 @@ import { hydrate } from 'src/hydration';
import { TreeStateContext } from './TreeContext';
import { separateDisplayNameAndHOCs } from 'src/utils';
import type { InspectedElement as InspectedElementBackend } from 'src/backend/types';
import type {
DehydratedData,
Element,
InspectedElement,
InspectedElement as InspectedElementFrontend,
} from 'src/devtools/views/Components/types';
import type { Resource, Thenable } from '../../cache';
type Context = {|
read(id: number): InspectedElement | null,
read(id: number): InspectedElementFrontend | null,
|};
const InspectedElementContext = createContext<Context>(((null: any): Context));
InspectedElementContext.displayName = 'InspectedElementContext';
type ResolveFn = (inspectedElement: InspectedElement) => void;
type ResolveFn = (inspectedElement: InspectedElementFrontend) => void;
type InProgressRequest = {|
promise: Thenable<InspectedElement>,
promise: Thenable<InspectedElementFrontend>,
resolveFn: ResolveFn,
|};
const inProgressRequests: WeakMap<Element, InProgressRequest> = new WeakMap();
const resource: Resource<Element, Element, InspectedElement> = createResource(
const resource: Resource<
Element,
Element,
InspectedElementFrontend
> = createResource(
(element: Element) => {
let request = inProgressRequests.get(element);
if (request != null) {
@@ -85,7 +90,7 @@ function InspectedElementContextController({ children }: Props) {
// This effect handler invalidates the suspense cache and schedules rendering updates with React.
useEffect(() => {
const onInspectedElement = (
inspectedElement: InspectedElement | number | null
data: InspectedElementBackend | number | null
) => {
// A null value means that the element no longer exists in the backend.
// If it's the same element that's currently selected, that selection will be removed once the Store updates.
@@ -93,23 +98,21 @@ function InspectedElementContextController({ children }: Props) {
// Either way there is nothing we need to do in this case.
// A numeric value indicates that the element hasn't changed since we last requested its data,
// in which case we don't need to invalidate the cache and re-render anything in the DevTools.
if (inspectedElement !== null && typeof inspectedElement === 'object') {
const id = inspectedElement.id;
if (data !== null && typeof data === 'object') {
const id = data.id;
const { displayName, type } = inspectedElement;
const [
displayNameWithoutHOCs,
hocDisplayNames,
] = separateDisplayNameAndHOCs(displayName, type);
inspectedElement = (({
...inspectedElement,
displayName: displayNameWithoutHOCs,
const inspectedElement: InspectedElementFrontend = {
canEditFunctionProps: data.canEditFunctionProps,
canEditHooks: data.canEditHooks,
canToggleSuspense: data.canToggleSuspense,
canViewSource: data.canViewSource,
id: data.id,
source: data.source,
type: data.type,
owners:
inspectedElement.owners === null
data.owners === null
? null
: inspectedElement.owners.map(owner => {
: data.owners.map(owner => {
const [
displayName,
hocDisplayNames,
@@ -120,13 +123,12 @@ function InspectedElementContextController({ children }: Props) {
hocDisplayNames,
};
}),
hocDisplayNames,
context: hydrateHelper(inspectedElement.context),
events: hydrateHelper(inspectedElement.events),
hooks: hydrateHelper(inspectedElement.hooks),
props: hydrateHelper(inspectedElement.props),
state: hydrateHelper(inspectedElement.state),
}: any): InspectedElement);
context: hydrateHelper(data.context),
events: hydrateHelper(data.events),
hooks: hydrateHelper(data.hooks),
props: hydrateHelper(data.props),
state: hydrateHelper(data.state),
};
const element = store.getElementByID(id);
if (element !== null) {
@@ -174,16 +176,13 @@ function InspectedElementContextController({ children }: Props) {
bridge.send('selectElement', { id: selectedElementID, rendererID });
const onInspectedElement = (
inspectedElement: InspectedElement | number | null
data: InspectedElementBackend | number | null
) => {
if (inspectedElement !== null) {
if (data !== null) {
// If this is the element we requested, wait a little bit and then ask for an update.
if (inspectedElement === selectedElementID) {
if (data === selectedElementID) {
timeoutID = setTimeout(sendRequest, 1000);
} else if (
typeof inspectedElement === 'object' &&
inspectedElement.id === selectedElementID
) {
} else if (typeof data === 'object' && data.id === selectedElementID) {
timeoutID = setTimeout(sendRequest, 1000);
}
}
@@ -11,11 +11,8 @@ import { BridgeContext, StoreContext } from '../context';
import { TreeStateContext } from './TreeContext';
import { separateDisplayNameAndHOCs } from 'src/utils';
import type {
Element,
Owner,
OwnersList,
} from 'src/devtools/views/Components/types';
import type { OwnersList } from 'src/backend/types';
import type { Element, Owner } from 'src/devtools/views/Components/types';
import type { Resource, Thenable } from '../../cache';
type Context = (id: number) => Array<Owner> | null;
-2
View File
@@ -52,8 +52,6 @@ export type Source = {|
export type InspectedElement = {|
id: number,
displayName: string | null,
// Does the current renderer support editable hooks?
canEditHooks: boolean,
+1 -2
View File
@@ -6,6 +6,7 @@ import '@reach/menu-button/styles.css';
import '@reach/tooltip/styles.css';
import React, { useState } from 'react';
import Bridge from 'src/bridge';
import Store from '../store';
import { BridgeContext, StoreContext } from './context';
import Components from './Components/Components';
@@ -23,8 +24,6 @@ import styles from './DevTools.css';
import './root.css';
import type { Bridge } from '../../types';
export type BrowserName = 'Chrome' | 'Firefox';
export type BrowserTheme = 'dark' | 'light';
export type TabID = 'components' | 'profiler' | 'settings';
+1 -2
View File
@@ -1,8 +1,7 @@
// @flow
import { createContext } from 'react';
import type { Bridge } from '../../types';
import Bridge from 'src/bridge';
import Store from '../store';
-6
View File
@@ -1,11 +1,5 @@
// @flow
export type Bridge = {
addListener(type: string, callback: Function): void,
removeListener(type: string, callback: Function): void,
send(event: string, payload: any, transferable?: Array<any>): void,
};
export type Wall = {|
// `listen` returns the "unlisten" function.
listen: (fn: Function) => Function,