@samitouri / QOS-React / commits / d012a32f84

feat[devtools]: add method for connecting backend with custom messaging protocol (#28552)

## Summary RDT backend will now expose method `connectWithCustomMessagingProtocol`, which will be similar to the classic `connectToDevTools` one, but with few differences: 1. It delegates the communication management between frontend and backend to the owner (whos injecting RDT backend). Unlike the `connectToDevTools`, which is relying on websocket connection and receives host and port as an arguments. 2. It returns a callback, which can be used for unsubscribing the current backend instance from the global DevTools hook. This is a prerequisite for any non-browser RDT integration, which is not designed to be based on websocket.

Ruslan Lesiutin committed Apr 12, 2024 at 15:17 UTC d012a32f840cf575b9d07bf1ed96af34af99e060
2 files changed +105 -1
packages/react-devtools-core/README.md
+10
@@ -36,6 +36,16 @@ if (process.env.NODE_ENV !== 'production') {
36 | `useHttps` | `false` | Socket connection to frontend should use secure protocol (wss). |
37 | `websocket` | | Custom `WebSocket` connection to frontend; overrides `host` and `port` settings. |
38
39 +
40 +### `connectWithCustomMessagingProtocol` options
41 +| Prop | Description |
42 +|-----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------|
43 +| `onSubscribe` | Function, which receives listener (function, with a single argument) as an argument. Called when backend subscribes to messages from the other end (frontend). |
44 +| `onUnsubscribe` | Function, which receives listener (function) as an argument. Called when backend unsubscribes to messages from the other end (frontend). |
45 +| `onMessage` | Function, which receives 2 arguments: event (string) and payload (any). Called when backend emits a message, which should be sent to the frontend. |
46 +
47 +Unlike `connectToDevTools`, `connectWithCustomMessagingProtocol` returns a callback, which can be used for unsubscribing the backend from the global DevTools hook.
48 +
49 # Frontend API
50
51 Frontend APIs can be used to render the DevTools UI into a DOM node. One example of this is [`react-devtools`](https://github.com/facebook/react/tree/main/packages/react-devtools) which wraps DevTools in an Electron app.
packages/react-devtools-core/src/backend.js
+95 -1
@@ -22,7 +22,10 @@ import {
22 } from './cachedSettings';
23
24 import type {BackendBridge} from 'react-devtools-shared/src/bridge';
25 -import type {ComponentFilter} from 'react-devtools-shared/src/frontend/types';
25 +import type {
26 + ComponentFilter,
27 + Wall,
28 +} from 'react-devtools-shared/src/frontend/types';
29 import type {DevToolsHook} from 'react-devtools-shared/src/backend/types';
30 import type {ResolveNativeStyle} from 'react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor';
31
@@ -310,3 +313,94 @@ export function connectToDevTools(options: ?ConnectOptions) {
313 });
314 }
315 }
316 +
317 +type ConnectWithCustomMessagingOptions = {
318 + onSubscribe: (cb: Function) => void,
319 + onUnsubscribe: (cb: Function) => void,
320 + onMessage: (event: string, payload: any) => void,
321 + settingsManager: ?DevToolsSettingsManager,
322 + nativeStyleEditorValidAttributes?: $ReadOnlyArray<string>,
323 + resolveRNStyle?: ResolveNativeStyle,
324 +};
325 +
326 +export function connectWithCustomMessagingProtocol({
327 + onSubscribe,
328 + onUnsubscribe,
329 + onMessage,
330 + settingsManager,
331 + nativeStyleEditorValidAttributes,
332 + resolveRNStyle,
333 +}: ConnectWithCustomMessagingOptions): Function {
334 + if (hook == null) {
335 + // DevTools didn't get injected into this page (maybe b'c of the contentType).
336 + return;
337 + }
338 +
339 + if (settingsManager != null) {
340 + try {
341 + initializeUsingCachedSettings(settingsManager);
342 + } catch (e) {
343 + // If we call a method on devToolsSettingsManager that throws, or if
344 + // is invalid data read out, don't throw and don't interrupt initialization
345 + console.error(e);
346 + }
347 + }
348 +
349 + const wall: Wall = {
350 + listen(fn: Function) {
351 + onSubscribe(fn);
352 +
353 + return () => {
354 + onUnsubscribe(fn);
355 + };
356 + },
357 + send(event: string, payload: any) {
358 + onMessage(event, payload);
359 + },
360 + };
361 +
362 + const bridge: BackendBridge = new Bridge(wall);
363 +
364 + bridge.addListener(
365 + 'updateComponentFilters',
366 + (componentFilters: Array<ComponentFilter>) => {
367 + // Save filter changes in memory, in case DevTools is reloaded.
368 + // In that case, the renderer will already be using the updated values.
369 + // We'll lose these in between backend reloads but that can't be helped.
370 + savedComponentFilters = componentFilters;
371 + },
372 + );
373 +
374 + if (settingsManager != null) {
375 + bridge.addListener('updateConsolePatchSettings', consolePatchSettings =>
376 + cacheConsolePatchSettings(settingsManager, consolePatchSettings),
377 + );
378 + }
379 +
380 + if (window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ == null) {
381 + bridge.send('overrideComponentFilters', savedComponentFilters);
382 + }
383 +
384 + const agent = new Agent(bridge);
385 + agent.addListener('shutdown', () => {
386 + // If we received 'shutdown' from `agent`, we assume the `bridge` is already shutting down,
387 + // and that caused the 'shutdown' event on the `agent`, so we don't need to call `bridge.shutdown()` here.
388 + hook.emit('shutdown');
389 + });
390 +
391 + const unsubscribeBackend = initBackend(hook, agent, window);
392 +
393 + const nativeStyleResolver: ResolveNativeStyle | void =
394 + resolveRNStyle || hook.resolveRNStyle;
395 +
396 + if (nativeStyleResolver != null) {
397 + const validAttributes =
398 + nativeStyleEditorValidAttributes ||
399 + hook.nativeStyleEditorValidAttributes ||
400 + null;
401 +
402 + setupNativeStyleEditor(bridge, agent, nativeStyleResolver, validAttributes);
403 + }
404 +
405 + return unsubscribeBackend;
406 +}