Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable app reloads for other runtimes than Hermes React Native #864

Closed
wants to merge 10 commits into from
118 changes: 81 additions & 37 deletions packages/metro-inspector-proxy/src/Device.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,13 @@ type DebuggerInfo = {
...
};

const REACT_NATIVE_RELOADABLE_PAGE_ID = '-1';
type ReloadablePage = {
_lastConnectedPage: Page,
_originalName: string,
_reloadableName: string,
};

const RELOADABLE_PAGE_TITLE_SUFFIX = ' Experimental (Improved Chrome Reloads)';

/**
* Device class represents single device connection to Inspector Proxy. Each device
Expand All @@ -72,12 +78,7 @@ class Device {
// Stores information about currently connected debugger (if any).
_debuggerConnection: ?DebuggerInfo = null;

// Last known Page ID of the React Native page.
// This is used by debugger connections that don't have PageID specified
// (and will interact with the latest React Native page).
_lastConnectedReactNativePage: ?Page = null;

// Whether we are in the middle of a reload in the REACT_NATIVE_RELOADABLE_PAGE.
// Whether we are in the middle of a reload.
_isReloading: boolean = false;

// The previous "GetPages" message, for deduplication in debug logs.
Expand All @@ -89,6 +90,15 @@ class Device {
// Root of the project used for relative to absolute source path conversion.
_projectRoot: string;

// A map from reloadable IDs to the latest available pages.
//
// I was wondering if it should be id -> page or name -> page, because we often
// iterate over the map to find the correct entry. I reached the conclusion that
// it is better this way, as we only search through map values on reloads
// but _mapToDevicePageId is called very often throughout the debugging
// process, so I optimized this use-case.
_reloadablePages: Map<string, ReloadablePage> = new Map();

constructor(
id: string,
name: string,
Expand Down Expand Up @@ -139,17 +149,21 @@ class Device {
}

getPagesList(): Array<Page> {
if (this._lastConnectedReactNativePage) {
const reactNativeReloadablePage = {
id: REACT_NATIVE_RELOADABLE_PAGE_ID,
title: 'React Native Experimental (Improved Chrome Reloads)',
vm: "don't use",
app: this._app,
};
return this._pages.concat(reactNativeReloadablePage);
} else {
return this._pages;
}
const reloadablePagesList = [];

this._reloadablePages.forEach((value, key) => {
if (value._lastConnectedPage) {
const reloadablePage = {
id: key,
title: value._reloadableName,
vm: "don't use",
app: this._app,
};
reloadablePagesList.push(reloadablePage);
}
});

return [...this._pages, ...reloadablePagesList];
}

// Handles new debugger connection to this device:
Expand Down Expand Up @@ -258,17 +272,20 @@ class Device {
if (message.event === 'getPages') {
this._pages = message.payload;

// Check if device have new React Native page.
// Check if device has a new page.
// There is usually no more than 2-3 pages per device so this operation
// is not expensive.
// TODO(hypuk): It is better for VM to send update event when new page is
// created instead of manually checking this on every getPages result.
for (let i = 0; i < this._pages.length; ++i) {
if (this._pages[i].title.indexOf('React') >= 0) {
if (this._pages[i].id != this._lastConnectedReactNativePage?.id) {
this._newReactNativePage(this._pages[i]);
break;
}
const testIfPageAlreadyRegistered = (page: ReloadablePage) =>
page._originalName === this._pages[i].title &&
page._lastConnectedPage.id === this._pages[i].id;

const mapValues = [...this._reloadablePages.values()];

if (!mapValues.some(testIfPageAlreadyRegistered)) {
this._handleNewReloadablePage(this._pages[i]);
}
}
} else if (message.event === 'disconnect') {
Expand All @@ -281,7 +298,7 @@ class Device {
if (debuggerSocket && debuggerSocket.readyState === WS.OPEN) {
if (
this._debuggerConnection != null &&
this._debuggerConnection.pageId !== REACT_NATIVE_RELOADABLE_PAGE_ID
!this._reloadablePages.has(pageId)
) {
debug(`Page ${pageId} is reloading.`);
debuggerSocket.send(JSON.stringify({method: 'reload'}));
Expand Down Expand Up @@ -335,21 +352,48 @@ class Device {
);
}

// We received new React Native Page ID.
_newReactNativePage(page: Page) {
debug(`React Native page updated to ${page.id}`);
// We received a new page ID.
_handleNewReloadablePage(page: Page) {
const reloadablePage = this._reloadablePages.get(
this._debuggerConnection?.pageId || '',
);

if (
this._debuggerConnection == null ||
this._debuggerConnection.pageId !== REACT_NATIVE_RELOADABLE_PAGE_ID
reloadablePage == null ||
reloadablePage._originalName !== page.title
) {
// We can just remember new page ID without any further actions if no
// debugger is currently attached or attached debugger is not
// "Reloadable React Native" connection.
this._lastConnectedReactNativePage = page;
// debugger is currently attached, the debugger is not a reloadable
// connection or the debugger is not currently connected to this page
for (const value of this._reloadablePages.values()) {
if (page.title === value._originalName) {
value._lastConnectedPage = page;
return;
}
}

// The page was not mapped earlier
const newReloadablePageTitle =
page.title === 'Hermes React Native'
? 'React Native' + RELOADABLE_PAGE_TITLE_SUFFIX
: page.title + RELOADABLE_PAGE_TITLE_SUFFIX;
const newReloadablePage: ReloadablePage = {
_lastConnectedPage: page,
_originalName: page.title,
_reloadableName: newReloadablePageTitle,
};
// We want to find the next available negative pageID.
// We assing them in a decreasing order starting from -1. We use negative
// numbers as metro doesn't use them for normal runtimes.
const newReloadableId = -(this._reloadablePages.size + 1);

this._reloadablePages.set(newReloadableId.toString(), newReloadablePage);
return;
}
const oldPageId = this._lastConnectedReactNativePage?.id;
this._lastConnectedReactNativePage = page;

const oldPageId = reloadablePage._lastConnectedPage.id;
reloadablePage._lastConnectedPage = page;
this._isReloading = true;

// We already had a debugger connected to React Native page and a
Expand Down Expand Up @@ -583,10 +627,10 @@ class Device {

_mapToDevicePageId(pageId: string): string {
if (
pageId === REACT_NATIVE_RELOADABLE_PAGE_ID &&
this._lastConnectedReactNativePage != null
this._reloadablePages.has(pageId) &&
this._reloadablePages.get(pageId)?._lastConnectedPage != null
) {
return this._lastConnectedReactNativePage.id;
return this._reloadablePages.get(pageId)?._lastConnectedPage.id || '';
} else {
return pageId;
}
Expand Down