9
10
import EventEmitter from './events';
11
12
-import type {ComponentFilter, Wall} from './frontend/types';
12
+import type {ComponentFilter, Wall, WallMessage} from './frontend/types';
13
import type {
14
InspectedElementPayload,
15
OwnersList,
74
75
type ElementAndRendererID = {id: number, rendererID: RendererID};
76
77
-export type Message = {
77
+export type Message = WallMessage;
78
+
79
+type QueuedMessage = {
80
event: string,
79
- payload: any,
81
+ payload: mixed,
82
+};
83
+
84
+type EventArguments<Payload> = Payload extends void ? [] : [Payload];
85
+
86
+type EventEmitterEvents<Events: Object> = {
87
+ [Event in keyof Events]: EventArguments<Events[Event]>,
88
};
89
90
type HighlightHostInstance = {
109
...ElementAndRendererID,
110
path: Array<string | number>,
111
wasForwarded?: boolean,
104
- value: any,
112
+ value: mixed,
113
};
114
115
type OverrideHookState = {
139
type: PathType,
140
hookID?: ?number,
141
path: Array<string | number>,
134
- value: any,
142
+ value: mixed,
143
};
144
145
type OverrideError = {
205
};
206
207
export type BackendEvents = {
200
- backendInitialized: [],
201
- backendVersion: [string],
202
- bridgeProtocol: [BridgeProtocol],
203
- extensionBackendInitialized: [],
204
- fastRefreshScheduled: [],
205
- getSavedPreferences: [],
206
- inspectedElement: [InspectedElementPayload],
207
- inspectedScreen: [InspectedElementPayload],
208
- isReloadAndProfileSupportedByBackend: [boolean],
209
- operations: [Array<number>],
210
- ownersList: [OwnersList],
211
- environmentNames: [Array<string>],
212
- profilingData: [ProfilingDataBackend],
213
- profilingStatus: [boolean],
214
- reloadAppForProfiling: [],
215
- saveToClipboard: [string],
216
- selectElement: [number | null],
217
- shutdown: [],
218
- stopInspectingHost: [boolean],
219
- scrollTo: [{left: number, top: number, right: number, bottom: number}],
220
- syncSelectionToBuiltinElementsPanel: [],
221
- unsupportedRendererVersion: [],
222
-
223
- extensionComponentsPanelShown: [],
224
- extensionComponentsPanelHidden: [],
225
-
226
- resumeElementPolling: [],
227
- pauseElementPolling: [],
208
+ backendInitialized: void,
209
+ backendVersion: string,
210
+ bridgeProtocol: BridgeProtocol,
211
+ extensionBackendInitialized: void,
212
+ fastRefreshScheduled: void,
213
+ getSavedPreferences: void,
214
+ inspectedElement: InspectedElementPayload,
215
+ inspectedScreen: InspectedElementPayload,
216
+ isReloadAndProfileSupportedByBackend: boolean,
217
+ operations: Array<number>,
218
+ ownersList: OwnersList,
219
+ environmentNames: Array<string>,
220
+ profilingData: ProfilingDataBackend,
221
+ profilingStatus: boolean,
222
+ reloadAppForProfiling: void,
223
+ saveToClipboard: string,
224
+ selectElement: number | null,
225
+ shutdown: void,
226
+ stopInspectingHost: boolean,
227
+ scrollTo: {left: number, top: number, right: number, bottom: number},
228
+ syncSelectionToBuiltinElementsPanel: void,
229
+ unsupportedRendererVersion: void,
230
+
231
+ extensionComponentsPanelShown: void,
232
+ extensionComponentsPanelHidden: void,
233
+
234
+ resumeElementPolling: void,
235
+ pauseElementPolling: void,
236
237
// React Native style editor plug-in.
230
- isNativeStyleEditorSupported: [
231
- {isSupported: boolean, validAttributes: ?$ReadOnlyArray<string>},
232
- ],
233
- NativeStyleEditor_styleAndLayout: [StyleAndLayoutPayload],
238
+ isNativeStyleEditorSupported: {
239
+ isSupported: boolean,
240
+ validAttributes: ?$ReadOnlyArray<string>,
241
+ },
242
+ NativeStyleEditor_styleAndLayout: StyleAndLayoutPayload,
243
235
- hookSettings: [$ReadOnly<DevToolsHookSettings>],
244
+ hookSettings: $ReadOnly<DevToolsHookSettings>,
245
};
246
247
type StartProfilingParams = ProfilingSettings;
248
type ReloadAndProfilingParams = ProfilingSettings;
249
250
export type FrontendEvents = {
242
- clearErrorsAndWarnings: [{rendererID: RendererID}],
243
- clearErrorsForElementID: [ElementAndRendererID],
244
- clearHostInstanceHighlight: [],
245
- clearWarningsForElementID: [ElementAndRendererID],
246
- copyElementPath: [CopyElementPathParams],
247
- deletePath: [DeletePath],
248
- getBackendVersion: [],
249
- getBridgeProtocol: [],
250
- getIfHasUnsupportedRendererVersion: [],
251
- getOwnersList: [ElementAndRendererID],
252
- getProfilingData: [{rendererID: RendererID}],
253
- getProfilingStatus: [],
254
- highlightHostInstance: [HighlightHostInstance],
255
- highlightHostInstances: [HighlightHostInstances],
256
- inspectElement: [InspectElementParams],
257
- inspectScreen: [InspectScreenParams],
258
- logElementToConsole: [ElementAndRendererID],
259
- overrideError: [OverrideError],
260
- overrideSuspense: [OverrideSuspense],
261
- overrideSuspenseMilestone: [OverrideSuspenseMilestone],
262
- overrideValueAtPath: [OverrideValueAtPath],
263
- profilingData: [ProfilingDataBackend],
264
- reloadAndProfile: [ReloadAndProfilingParams],
265
- renamePath: [RenamePath],
266
- savedPreferences: [SavedPreferencesParams],
267
- setTraceUpdatesEnabled: [boolean],
268
- shutdown: [],
269
- startInspectingHost: [boolean],
270
- startProfiling: [StartProfilingParams],
271
- stopInspectingHost: [],
272
- scrollToHostInstance: [ScrollToHostInstance],
273
- scrollTo: [{left: number, top: number, right: number, bottom: number}],
274
- requestScrollPosition: [],
275
- stopProfiling: [],
276
- storeAsGlobal: [StoreAsGlobalParams],
277
- updateComponentFilters: [Array<ComponentFilter>],
278
- getEnvironmentNames: [],
279
- updateHookSettings: [$ReadOnly<DevToolsHookSettings>],
280
- viewAttributeSource: [ViewAttributeSourceParams],
281
- viewElementSource: [ElementAndRendererID],
282
-
283
- syncSelectionFromBuiltinElementsPanel: [],
251
+ clearErrorsAndWarnings: {rendererID: RendererID},
252
+ clearErrorsForElementID: ElementAndRendererID,
253
+ clearHostInstanceHighlight: void,
254
+ clearWarningsForElementID: ElementAndRendererID,
255
+ copyElementPath: CopyElementPathParams,
256
+ deletePath: DeletePath,
257
+ getBackendVersion: void,
258
+ getBridgeProtocol: void,
259
+ getIfHasUnsupportedRendererVersion: void,
260
+ getOwnersList: ElementAndRendererID,
261
+ getProfilingData: {rendererID: RendererID},
262
+ getProfilingStatus: void,
263
+ highlightHostInstance: HighlightHostInstance,
264
+ highlightHostInstances: HighlightHostInstances,
265
+ inspectElement: InspectElementParams,
266
+ inspectScreen: InspectScreenParams,
267
+ logElementToConsole: ElementAndRendererID,
268
+ overrideError: OverrideError,
269
+ overrideSuspense: OverrideSuspense,
270
+ overrideSuspenseMilestone: OverrideSuspenseMilestone,
271
+ overrideValueAtPath: OverrideValueAtPath,
272
+ profilingData: ProfilingDataBackend,
273
+ reloadAndProfile: ReloadAndProfilingParams,
274
+ renamePath: RenamePath,
275
+ savedPreferences: SavedPreferencesParams,
276
+ setTraceUpdatesEnabled: boolean,
277
+ shutdown: void,
278
+ startInspectingHost: boolean,
279
+ startProfiling: StartProfilingParams,
280
+ stopInspectingHost: void,
281
+ scrollToHostInstance: ScrollToHostInstance,
282
+ scrollTo: {left: number, top: number, right: number, bottom: number},
283
+ requestScrollPosition: void,
284
+ stopProfiling: void,
285
+ storeAsGlobal: StoreAsGlobalParams,
286
+ updateComponentFilters: Array<ComponentFilter>,
287
+ getEnvironmentNames: void,
288
+ updateHookSettings: $ReadOnly<DevToolsHookSettings>,
289
+ viewAttributeSource: ViewAttributeSourceParams,
290
+ viewElementSource: ElementAndRendererID,
291
+
292
+ syncSelectionFromBuiltinElementsPanel: void,
293
294
// React Native style editor plug-in.
286
- NativeStyleEditor_measure: [ElementAndRendererID],
287
- NativeStyleEditor_renameAttribute: [NativeStyleEditor_RenameAttributeParams],
288
- NativeStyleEditor_setValue: [NativeStyleEditor_SetValueParams],
295
+ NativeStyleEditor_measure: ElementAndRendererID,
296
+ NativeStyleEditor_renameAttribute: NativeStyleEditor_RenameAttributeParams,
297
+ NativeStyleEditor_setValue: NativeStyleEditor_SetValueParams,
298
299
// Temporarily support newer standalone front-ends sending commands to older embedded backends.
300
// We do this because React Native embeds the React DevTools backend,
306
// Note that this approach does no support the combination of a newer backend with an older frontend.
307
// It would be more work to support both approaches (and not run handlers twice)
308
// so I chose to support the more likely/common scenario (and the one more difficult for an end user to "fix").
300
- overrideContext: [OverrideValue],
301
- overrideHookState: [OverrideHookState],
302
- overrideProps: [OverrideValue],
303
- overrideState: [OverrideValue],
309
+ overrideContext: OverrideValue,
310
+ overrideHookState: OverrideHookState,
311
+ overrideProps: OverrideValue,
312
+ overrideState: OverrideValue,
313
305
- getHookSettings: [],
314
+ getHookSettings: void,
315
};
316
317
class Bridge<
318
OutgoingEvents: Object,
319
IncomingEvents: Object,
311
-> extends EventEmitter<IncomingEvents> {
320
+> extends EventEmitter<EventEmitterEvents<IncomingEvents>> {
321
_isShutdown: boolean = false;
313
- _messageQueue: Array<any> = [];
322
+ _messageQueue: Array<QueuedMessage> = [];
323
_scheduledFlush: boolean = false;
324
_wall: Wall;
316
- _wallUnlisten: Function | null = null;
325
+ _wallUnlisten: (() => void) | null = null;
326
327
constructor(wall: Wall) {
328
super();
329
330
this._wall = wall;
331
323
- this._wallUnlisten =
324
- wall.listen((message: Message) => {
325
- if (message && message.event) {
326
- (this as any).emit(message.event, message.payload);
327
- }
328
- }) || null;
332
+ const wallUnlisten = wall.listen(this._handleMessage);
333
+ if (typeof wallUnlisten !== 'function') {
334
+ throw new TypeError('Wall.listen() must return an unlisten function.');
335
+ }
336
+ this._wallUnlisten = wallUnlisten;
337
338
// Temporarily support older standalone front-ends sending commands to newer embedded backends.
339
// We do this because React Native embeds the React DevTools backend,
347
return this._wall;
348
}
349
350
+ addListener<Event: $Keys<EventEmitterEvents<IncomingEvents>>>(
351
+ event: Event,
352
+ listener: (...EventEmitterEvents<IncomingEvents>[Event]) => mixed,
353
+ ): void {
354
+ this._assertNotShutdown('add a listener');
355
+ super.addListener(event, listener);
356
+ }
357
+
358
+ emit<Event: $Keys<EventEmitterEvents<IncomingEvents>>>(
359
+ event: Event,
360
+ ...args: EventEmitterEvents<IncomingEvents>[Event]
361
+ ): void {
362
+ this._assertNotShutdown('emit an event');
363
+ super.emit(event, ...args);
364
+ }
365
+
366
send<EventName: $Keys<OutgoingEvents>>(
367
event: EventName,
344
- ...payload: OutgoingEvents[EventName]
345
- ) {
346
- if (this._isShutdown) {
347
- console.warn(
348
- `Cannot send message "${event}" through a Bridge that has been shutdown.`,
349
- );
350
- return;
368
+ payload?: OutgoingEvents[EventName],
369
+ ): void {
370
+ this._assertNotShutdown('send a message');
371
+
372
+ if (typeof event !== 'string' || event.length === 0) {
373
+ throw new TypeError('Bridge event names must be non-empty strings.');
374
}
375
376
// When we receive a message:
381
// - if there *has* been a message flushed in the last BATCH_DURATION ms
382
// (or we're waiting for our setTimeout-0 to fire), then _timeoutID will
383
// be set, and we'll simply add to the queue and wait for that
361
- this._messageQueue.push(event, payload);
384
+ this._messageQueue.push({
385
+ event,
386
+ payload,
387
+ });
388
if (!this._scheduledFlush) {
389
this._scheduledFlush = true;
390
// $FlowFixMe[cannot-resolve-name]
401
}
402
}
403
378
- shutdown() {
379
- if (this._isShutdown) {
380
- console.warn('Bridge was already shutdown.');
381
- return;
382
- }
404
+ shutdown(): void {
405
+ this._assertNotShutdown('shut down');
406
407
// Queue the shutdown outgoing message for subscribers.
408
this.emit('shutdown');
411
// Mark this bridge as destroyed, i.e. disable its public API.
412
this._isShutdown = true;
413
391
- // Disable the API inherited from EventEmitter that can add more listeners and send more messages.
392
- // $FlowFixMe[cannot-write] This property is not writable.
393
- this.addListener = function () {};
394
- // $FlowFixMe[cannot-write] This property is not writable.
395
- this.emit = function () {};
396
- // NOTE: There's also EventEmitter API like `on` and `prependListener` that we didn't add to our Flow type of EventEmitter.
397
-
414
// Unsubscribe this bridge incoming message listeners to be sure, and so they don't have to do that.
415
this.removeAllListeners();
416
417
// Stop accepting and emitting incoming messages from the wall.
418
const wallUnlisten = this._wallUnlisten;
403
- if (wallUnlisten) {
404
- wallUnlisten();
419
+ this._wallUnlisten = null;
420
+ try {
421
+ if (wallUnlisten !== null) {
422
+ wallUnlisten();
423
+ }
424
+ } finally {
425
+ // Synchronously flush all queued outgoing messages.
426
+ // At this step the subscribers' code may run in this call stack.
427
+ do {
428
+ this._flush();
429
+ } while (this._messageQueue.length);
430
}
406
-
407
- // Synchronously flush all queued outgoing messages.
408
- // At this step the subscribers' code may run in this call stack.
409
- do {
410
- this._flush();
411
- } while (this._messageQueue.length);
431
}
432
433
_flush: () => void = () => {
436
// It is a private method that the bridge ensures is only called at the right times.
437
try {
438
if (this._messageQueue.length) {
420
- for (let i = 0; i < this._messageQueue.length; i += 2) {
421
- // This only supports one argument in practice but the types suggests it should support multiple.
422
- this._wall.send(this._messageQueue[i], this._messageQueue[i + 1][0]);
439
+ for (let i = 0; i < this._messageQueue.length; i++) {
440
+ const {event, payload} = this._messageQueue[i];
441
+ this._wall.send(event, payload);
442
}
443
this._messageQueue.length = 0;
444
}
449
}
450
};
451
452
+ _assertNotShutdown(action: string): void {
453
+ if (this._isShutdown) {
454
+ throw new Error(
455
+ `Cannot ${action} through a Bridge that has been shut down.`,
456
+ );
457
+ }
458
+ }
459
+
460
+ _handleMessage: (message: mixed) => void = message => {
461
+ // Some Walls share a transport with unrelated messages or legacy DevTools
462
+ // protocols. A message without an event field does not belong to this Bridge.
463
+ if (
464
+ message === null ||
465
+ typeof message !== 'object' ||
466
+ !('event' in message)
467
+ ) {
468
+ return;
469
+ }
470
+
471
+ const event = message.event;
472
+ if (typeof event !== 'string' || event.length === 0) {
473
+ throw new TypeError('Bridge event names must be non-empty strings.');
474
+ }
475
+
476
+ this._assertNotShutdown('receive a message');
477
+ // The wire event name cannot be statically refined to a key of IncomingEvents.
478
+ (this as any).emit(event, message.payload);
479
+ };
480
+
481
// Temporarily support older standalone backends by forwarding "overrideValueAtPath" commands
482
// to the older message types they may be listening to.
483
overrideValueAtPath: OverrideValueAtPath => void = ({