-
Notifications
You must be signed in to change notification settings - Fork 47.3k
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
refactor[devtools/extension]: more stable element updates polling to avoid timed out errors #27357
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
/** | ||
* Copyright (c) Meta Platforms, Inc. and affiliates. | ||
* | ||
* This source code is licensed under the MIT license found in the | ||
* LICENSE file in the root directory of this source tree. | ||
* | ||
* @flow | ||
*/ | ||
|
||
export default class ElementPollingCancellationError extends Error { | ||
constructor() { | ||
super(); | ||
|
||
// Maintains proper stack trace for where our error was thrown (only available on V8) | ||
if (Error.captureStackTrace) { | ||
Error.captureStackTrace(this, ElementPollingCancellationError); | ||
} | ||
|
||
this.name = 'ElementPollingCancellationError'; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,8 +11,9 @@ import { | |
unstable_getCacheForType as getCacheForType, | ||
startTransition, | ||
} from 'react'; | ||
import Store from './devtools/store'; | ||
import {inspectElement as inspectElementMutableSource} from './inspectedElementMutableSource'; | ||
import Store from 'react-devtools-shared/src/devtools/store'; | ||
import {inspectElement as inspectElementMutableSource} from 'react-devtools-shared/src/inspectedElementMutableSource'; | ||
import ElementPollingCancellationError from 'react-devtools-shared/src//errors/ElementPollingCancellationError'; | ||
|
||
import type {FrontendBridge} from 'react-devtools-shared/src/bridge'; | ||
import type {Wakeable} from 'shared/ReactTypes'; | ||
|
@@ -177,15 +178,15 @@ export function checkForUpdate({ | |
element: Element, | ||
refresh: RefreshFunction, | ||
store: Store, | ||
}): void { | ||
}): void | Promise<void> { | ||
const {id} = element; | ||
const rendererID = store.getRendererIDForElement(id); | ||
|
||
if (rendererID == null) { | ||
return; | ||
} | ||
|
||
inspectElementMutableSource({ | ||
return inspectElementMutableSource({ | ||
bridge, | ||
element, | ||
path: null, | ||
|
@@ -202,15 +203,88 @@ export function checkForUpdate({ | |
}); | ||
} | ||
}, | ||
|
||
// There isn't much to do about errors in this case, | ||
// but we should at least log them so they aren't silent. | ||
error => { | ||
console.error(error); | ||
}, | ||
); | ||
} | ||
|
||
function createPromiseWhichResolvesInOneSecond() { | ||
return new Promise(resolve => setTimeout(resolve, 1000)); | ||
} | ||
|
||
type PollingStatus = 'idle' | 'running' | 'paused' | 'aborted'; | ||
|
||
export function startElementUpdatesPolling({ | ||
bridge, | ||
element, | ||
refresh, | ||
store, | ||
}: { | ||
bridge: FrontendBridge, | ||
element: Element, | ||
refresh: RefreshFunction, | ||
store: Store, | ||
}): {abort: () => void, pause: () => void, resume: () => void} { | ||
let status: PollingStatus = 'idle'; | ||
|
||
function abort() { | ||
status = 'aborted'; | ||
} | ||
|
||
function resume() { | ||
if (status === 'running' || status === 'aborted') { | ||
return; | ||
} | ||
|
||
status = 'idle'; | ||
poll(); | ||
} | ||
|
||
function pause() { | ||
if (status === 'paused' || status === 'aborted') { | ||
return; | ||
} | ||
|
||
status = 'paused'; | ||
} | ||
|
||
function poll(): Promise<void> { | ||
status = 'running'; | ||
|
||
return Promise.allSettled([ | ||
checkForUpdate({bridge, element, refresh, store}), | ||
createPromiseWhichResolvesInOneSecond(), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. feel free to ignore, don't feel too strongly: should this per second throttling be part of this or the caller? it's not entirely clear this function does this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The idea here is to throttle Still, if Promise returned by Also, if |
||
]) | ||
.then(([{status: updateStatus, reason}]) => { | ||
// There isn't much to do about errors in this case, | ||
// but we should at least log them, so they aren't silent. | ||
// Log only if polling is still active, we can't handle the case when | ||
// request was sent, and then bridge was remounted (for example, when user did navigate to a new page), | ||
// but at least we can mark that polling was aborted | ||
if (updateStatus === 'rejected' && status !== 'aborted') { | ||
// This is expected Promise rejection, no need to log it | ||
if (reason instanceof ElementPollingCancellationError) { | ||
return; | ||
} | ||
|
||
console.error(reason); | ||
} | ||
}) | ||
.finally(() => { | ||
const shouldContinuePolling = | ||
status !== 'aborted' && status !== 'paused'; | ||
|
||
status = 'idle'; | ||
|
||
if (shouldContinuePolling) { | ||
return poll(); | ||
} | ||
}); | ||
} | ||
|
||
poll(); | ||
|
||
return {abort, resume, pause}; | ||
} | ||
|
||
export function clearCacheBecauseOfError(refresh: RefreshFunction): void { | ||
startTransition(() => { | ||
const map = createMap(); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice nice