Skip to content

Commit

Permalink
Add Postpone API (#27238)
Browse files Browse the repository at this point in the history
This adds an experimental `unstable_postpone(reason)` API.

Currently we don't have a way to model effectively an Infinite Promise.
I.e. something that suspends but never resolves. The reason this is
useful is because you might have something else that unblocks it later.
E.g. by updating in place later, or by client rendering.

On the client this works to model as an Infinite Promise (in fact,
that's what this implementation does). However, in Fizz and Flight that
doesn't work because the stream needs to end at some point. We don't
have any way of knowing that we're suspended on infinite promises. It's
not enough to tag the promises because you could await those and thus
creating new promises. The only way we really have to signal this
through a series of indirections like async functions, is by throwing.
It's not 100% safe because these values can be caught but it's the best
we can do.

Effectively `postpone(reason)` behaves like a built-in [Catch
Boundary](#26854). It's like
`raise(Postpone, reason)` except it's built-in so it needs to be able to
be encoded and caught by Suspense boundaries.

In Flight and Fizz these behave pretty much the same as errors. Flight
just forwards it to retrigger on the client. In Fizz they just trigger
client rendering which itself might just postpone again or fill in the
value. The difference is how they get logged.

In Flight and Fizz they log to `onPostpone(reason)` instead of
`onError(error)`. This log is meant to help find deopts on the server
like finding places where you fall back to client rendering. The reason
that you pass in is for that purpose to help the reason for any deopts.

I do track the stack trace in DEV but I don't currently expose it to
`onPostpone`. This seems like a limitation. It might be better to expose
the Postpone object which is an Error object but that's more of an
implementation detail. I could also pass it as a second argument.

On the client after hydration they don't get passed to
`onRecoverableError`. There's no global `onPostpone` API to capture
postponed things on the client just like there's no `onError`. At that
point it's just assumed to be intentional. It doesn't have any `digest`
or reason passed to the client since it's not logged.

There are some hacky solutions that currently just tries to reuse as
much of the existing code as possible but should be more properly
implemented.
- Fiber is currently just converting it to a fake Promise object so that
it behaves like an infinite Promise.
- Fizz is encoding the magic digest string `"POSTPONE"` in the HTML so
we know to ignore it but it should probably just be something neater
that doesn't share namespace with digests.

Next I plan on using this in the `/static` entry points for additional
features.

Why "postpone"? It's basically a synonym to "defer" but we plan on using
"defer" for other purposes and it's overloaded anyway.

DiffTrain build for [ac1a16c](ac1a16c)
  • Loading branch information
sebmarkbage committed Aug 17, 2023
1 parent d03f8fb commit 5cc1661
Show file tree
Hide file tree
Showing 24 changed files with 1,992 additions and 1,844 deletions.
2 changes: 1 addition & 1 deletion compiled/facebook-www/REVISION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0fb5b61ac6951a492242618e4ace6c1826335efc
ac1a16c67e268fcb2c52e91717cbc918c7c24446
2 changes: 1 addition & 1 deletion compiled/facebook-www/React-dev.modern.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ if (
}
"use strict";

var ReactVersion = "18.3.0-www-modern-1d530573";
var ReactVersion = "18.3.0-www-modern-00d6344c";

// ATTENTION
// When adding new symbols to this file,
Expand Down
2 changes: 1 addition & 1 deletion compiled/facebook-www/React-prod.modern.js
Original file line number Diff line number Diff line change
Expand Up @@ -615,4 +615,4 @@ exports.useSyncExternalStore = function (
exports.useTransition = function () {
return ReactCurrentDispatcher.current.useTransition();
};
exports.version = "18.3.0-www-modern-8591a478";
exports.version = "18.3.0-www-modern-5d9b9771";
341 changes: 173 additions & 168 deletions compiled/facebook-www/ReactART-dev.classic.js

Large diffs are not rendered by default.

341 changes: 173 additions & 168 deletions compiled/facebook-www/ReactART-dev.modern.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions compiled/facebook-www/ReactART-prod.modern.js
Original file line number Diff line number Diff line change
Expand Up @@ -9759,7 +9759,7 @@ var slice = Array.prototype.slice,
return null;
},
bundleType: 0,
version: "18.3.0-www-modern-c25ea5bc",
version: "18.3.0-www-modern-0328f77f",
rendererPackageName: "react-art"
};
var internals$jscomp$inline_1283 = {
Expand Down Expand Up @@ -9790,7 +9790,7 @@ var internals$jscomp$inline_1283 = {
scheduleRoot: null,
setRefreshHandler: null,
getCurrentFiber: null,
reconcilerVersion: "18.3.0-www-modern-c25ea5bc"
reconcilerVersion: "18.3.0-www-modern-0328f77f"
};
if ("undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__) {
var hook$jscomp$inline_1284 = __REACT_DEVTOOLS_GLOBAL_HOOK__;
Expand Down
396 changes: 200 additions & 196 deletions compiled/facebook-www/ReactDOM-dev.classic.js

Large diffs are not rendered by default.

396 changes: 200 additions & 196 deletions compiled/facebook-www/ReactDOM-dev.modern.js

Large diffs are not rendered by default.

37 changes: 19 additions & 18 deletions compiled/facebook-www/ReactDOM-prod.classic.js
Original file line number Diff line number Diff line change
Expand Up @@ -10528,21 +10528,22 @@ function throwAndUnwindWorkLoop(unitOfWork, thrownValue) {
renderDidSuspendDelayIfPossible();
break a;
} else value = Error(formatProdErrorMessage(426));
} else if (
isHydrating &&
unitOfWork.mode & 1 &&
((wakeable = suspenseHandlerStackCursor.current), null !== wakeable)
) {
0 === (wakeable.flags & 65536) && (wakeable.flags |= 256);
markSuspenseBoundaryShouldCapture(
wakeable,
returnFiber,
unitOfWork,
root,
thrownValue
);
queueHydrationError(createCapturedValueAtFiber(value, unitOfWork));
break a;
}
if (isHydrating && unitOfWork.mode & 1) {
var suspenseBoundary$61 = suspenseHandlerStackCursor.current;
if (null !== suspenseBoundary$61) {
0 === (suspenseBoundary$61.flags & 65536) &&
(suspenseBoundary$61.flags |= 256);
markSuspenseBoundaryShouldCapture(
suspenseBoundary$61,
returnFiber,
unitOfWork,
root,
thrownValue
);
queueHydrationError(createCapturedValueAtFiber(value, unitOfWork));
break a;
}
}
root = value = createCapturedValueAtFiber(value, unitOfWork);
4 !== workInProgressRootExitStatus &&
Expand Down Expand Up @@ -16574,7 +16575,7 @@ Internals.Events = [
var devToolsConfig$jscomp$inline_1800 = {
findFiberByHostInstance: getClosestInstanceFromNode,
bundleType: 0,
version: "18.3.0-www-classic-ac0b40a5",
version: "18.3.0-www-classic-a80847ca",
rendererPackageName: "react-dom"
};
var internals$jscomp$inline_2159 = {
Expand Down Expand Up @@ -16604,7 +16605,7 @@ var internals$jscomp$inline_2159 = {
scheduleRoot: null,
setRefreshHandler: null,
getCurrentFiber: null,
reconcilerVersion: "18.3.0-www-classic-ac0b40a5"
reconcilerVersion: "18.3.0-www-classic-a80847ca"
};
if ("undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__) {
var hook$jscomp$inline_2160 = __REACT_DEVTOOLS_GLOBAL_HOOK__;
Expand Down Expand Up @@ -16832,4 +16833,4 @@ exports.unstable_renderSubtreeIntoContainer = function (
);
};
exports.unstable_runWithPriority = runWithPriority;
exports.version = "18.3.0-www-classic-ac0b40a5";
exports.version = "18.3.0-www-classic-a80847ca";
37 changes: 19 additions & 18 deletions compiled/facebook-www/ReactDOM-prod.modern.js
Original file line number Diff line number Diff line change
Expand Up @@ -10360,21 +10360,22 @@ function throwAndUnwindWorkLoop(unitOfWork, thrownValue) {
renderDidSuspendDelayIfPossible();
break a;
} else value = Error(formatProdErrorMessage(426));
} else if (
isHydrating &&
unitOfWork.mode & 1 &&
((wakeable = suspenseHandlerStackCursor.current), null !== wakeable)
) {
0 === (wakeable.flags & 65536) && (wakeable.flags |= 256);
markSuspenseBoundaryShouldCapture(
wakeable,
returnFiber,
unitOfWork,
root,
thrownValue
);
queueHydrationError(createCapturedValueAtFiber(value, unitOfWork));
break a;
}
if (isHydrating && unitOfWork.mode & 1) {
var suspenseBoundary$61 = suspenseHandlerStackCursor.current;
if (null !== suspenseBoundary$61) {
0 === (suspenseBoundary$61.flags & 65536) &&
(suspenseBoundary$61.flags |= 256);
markSuspenseBoundaryShouldCapture(
suspenseBoundary$61,
returnFiber,
unitOfWork,
root,
thrownValue
);
queueHydrationError(createCapturedValueAtFiber(value, unitOfWork));
break a;
}
}
root = value = createCapturedValueAtFiber(value, unitOfWork);
4 !== workInProgressRootExitStatus &&
Expand Down Expand Up @@ -16104,7 +16105,7 @@ Internals.Events = [
var devToolsConfig$jscomp$inline_1759 = {
findFiberByHostInstance: getClosestInstanceFromNode,
bundleType: 0,
version: "18.3.0-www-modern-c25ea5bc",
version: "18.3.0-www-modern-0328f77f",
rendererPackageName: "react-dom"
};
var internals$jscomp$inline_2123 = {
Expand Down Expand Up @@ -16135,7 +16136,7 @@ var internals$jscomp$inline_2123 = {
scheduleRoot: null,
setRefreshHandler: null,
getCurrentFiber: null,
reconcilerVersion: "18.3.0-www-modern-c25ea5bc"
reconcilerVersion: "18.3.0-www-modern-0328f77f"
};
if ("undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__) {
var hook$jscomp$inline_2124 = __REACT_DEVTOOLS_GLOBAL_HOOK__;
Expand Down Expand Up @@ -16291,4 +16292,4 @@ exports.unstable_createEventHandle = function (type, options) {
return eventHandle;
};
exports.unstable_runWithPriority = runWithPriority;
exports.version = "18.3.0-www-modern-c25ea5bc";
exports.version = "18.3.0-www-modern-0328f77f";
37 changes: 19 additions & 18 deletions compiled/facebook-www/ReactDOM-profiling.classic.js
Original file line number Diff line number Diff line change
Expand Up @@ -11207,21 +11207,22 @@ function throwAndUnwindWorkLoop(unitOfWork, thrownValue) {
renderDidSuspendDelayIfPossible();
break a;
} else value = Error(formatProdErrorMessage(426));
} else if (
isHydrating &&
unitOfWork.mode & 1 &&
((wakeable = suspenseHandlerStackCursor.current), null !== wakeable)
) {
0 === (wakeable.flags & 65536) && (wakeable.flags |= 256);
markSuspenseBoundaryShouldCapture(
wakeable,
returnFiber,
unitOfWork,
root,
thrownValue
);
queueHydrationError(createCapturedValueAtFiber(value, unitOfWork));
break a;
}
if (isHydrating && unitOfWork.mode & 1) {
var suspenseBoundary$66 = suspenseHandlerStackCursor.current;
if (null !== suspenseBoundary$66) {
0 === (suspenseBoundary$66.flags & 65536) &&
(suspenseBoundary$66.flags |= 256);
markSuspenseBoundaryShouldCapture(
suspenseBoundary$66,
returnFiber,
unitOfWork,
root,
thrownValue
);
queueHydrationError(createCapturedValueAtFiber(value, unitOfWork));
break a;
}
}
root = value = createCapturedValueAtFiber(value, unitOfWork);
4 !== workInProgressRootExitStatus &&
Expand Down Expand Up @@ -17348,7 +17349,7 @@ Internals.Events = [
var devToolsConfig$jscomp$inline_1885 = {
findFiberByHostInstance: getClosestInstanceFromNode,
bundleType: 0,
version: "18.3.0-www-classic-dfca71a7",
version: "18.3.0-www-classic-8964de47",
rendererPackageName: "react-dom"
};
(function (internals) {
Expand Down Expand Up @@ -17392,7 +17393,7 @@ var devToolsConfig$jscomp$inline_1885 = {
scheduleRoot: null,
setRefreshHandler: null,
getCurrentFiber: null,
reconcilerVersion: "18.3.0-www-classic-dfca71a7"
reconcilerVersion: "18.3.0-www-classic-8964de47"
});
assign(Internals, {
ReactBrowserEventEmitter: {
Expand Down Expand Up @@ -17607,7 +17608,7 @@ exports.unstable_renderSubtreeIntoContainer = function (
);
};
exports.unstable_runWithPriority = runWithPriority;
exports.version = "18.3.0-www-classic-dfca71a7";
exports.version = "18.3.0-www-classic-8964de47";

/* global __REACT_DEVTOOLS_GLOBAL_HOOK__ */
if (
Expand Down
37 changes: 19 additions & 18 deletions compiled/facebook-www/ReactDOM-profiling.modern.js
Original file line number Diff line number Diff line change
Expand Up @@ -11033,21 +11033,22 @@ function throwAndUnwindWorkLoop(unitOfWork, thrownValue) {
renderDidSuspendDelayIfPossible();
break a;
} else value = Error(formatProdErrorMessage(426));
} else if (
isHydrating &&
unitOfWork.mode & 1 &&
((wakeable = suspenseHandlerStackCursor.current), null !== wakeable)
) {
0 === (wakeable.flags & 65536) && (wakeable.flags |= 256);
markSuspenseBoundaryShouldCapture(
wakeable,
returnFiber,
unitOfWork,
root,
thrownValue
);
queueHydrationError(createCapturedValueAtFiber(value, unitOfWork));
break a;
}
if (isHydrating && unitOfWork.mode & 1) {
var suspenseBoundary$66 = suspenseHandlerStackCursor.current;
if (null !== suspenseBoundary$66) {
0 === (suspenseBoundary$66.flags & 65536) &&
(suspenseBoundary$66.flags |= 256);
markSuspenseBoundaryShouldCapture(
suspenseBoundary$66,
returnFiber,
unitOfWork,
root,
thrownValue
);
queueHydrationError(createCapturedValueAtFiber(value, unitOfWork));
break a;
}
}
root = value = createCapturedValueAtFiber(value, unitOfWork);
4 !== workInProgressRootExitStatus &&
Expand Down Expand Up @@ -16872,7 +16873,7 @@ Internals.Events = [
var devToolsConfig$jscomp$inline_1844 = {
findFiberByHostInstance: getClosestInstanceFromNode,
bundleType: 0,
version: "18.3.0-www-modern-1ea20273",
version: "18.3.0-www-modern-5790f94f",
rendererPackageName: "react-dom"
};
(function (internals) {
Expand Down Expand Up @@ -16917,7 +16918,7 @@ var devToolsConfig$jscomp$inline_1844 = {
scheduleRoot: null,
setRefreshHandler: null,
getCurrentFiber: null,
reconcilerVersion: "18.3.0-www-modern-1ea20273"
reconcilerVersion: "18.3.0-www-modern-5790f94f"
});
exports.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = Internals;
exports.createPortal = function (children, container) {
Expand Down Expand Up @@ -17060,7 +17061,7 @@ exports.unstable_createEventHandle = function (type, options) {
return eventHandle;
};
exports.unstable_runWithPriority = runWithPriority;
exports.version = "18.3.0-www-modern-1ea20273";
exports.version = "18.3.0-www-modern-5790f94f";

/* global __REACT_DEVTOOLS_GLOBAL_HOOK__ */
if (
Expand Down
21 changes: 17 additions & 4 deletions compiled/facebook-www/ReactDOMServer-dev.classic.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ if (__DEV__) {
var React = require("react");
var ReactDOM = require("react-dom");

var ReactVersion = "18.3.0-www-classic-9ea6402e";
var ReactVersion = "18.3.0-www-classic-6c503419";

// This refers to a WWW module.
var warningWWW = require("warning");
Expand Down Expand Up @@ -9553,7 +9553,8 @@ function createRequest(
onAllReady,
onShellReady,
onShellError,
onFatalError
onFatalError,
onPostpone
) {
prepareHostDispatcher();
var pingedTasks = [];
Expand All @@ -9579,6 +9580,7 @@ function createRequest(
completedBoundaries: [],
partialBoundaries: [],
onError: onError === undefined ? defaultErrorHandler : onError,
onPostpone: onPostpone === undefined ? noop : onPostpone,
onAllReady: onAllReady === undefined ? noop : onAllReady,
onShellReady: onShellReady === undefined ? noop : onShellReady,
onShellError: onShellError === undefined ? noop : onShellError,
Expand Down Expand Up @@ -9895,7 +9897,13 @@ function renderSuspenseBoundary(request, task, props) {
} catch (error) {
contentRootSegment.status = ERRORED;
newBoundary.forceClientRender = true;
newBoundary.errorDigest = logRecoverableError(request, error);
var errorDigest;

{
errorDigest = logRecoverableError(request, error);
}

newBoundary.errorDigest = errorDigest;

{
captureBoundaryErrorDetailsDev(newBoundary, error);
Expand Down Expand Up @@ -10849,7 +10857,11 @@ function renderNode(request, task, node) {

function erroredTask(request, boundary, segment, error) {
// Report the error to a global handler.
var errorDigest = logRecoverableError(request, error);
var errorDigest;

{
errorDigest = logRecoverableError(request, error);
}

if (boundary === null) {
fatalError(request, error);
Expand Down Expand Up @@ -11738,6 +11750,7 @@ function renderToStringImpl(
undefined,
onShellReady,
undefined,
undefined,
undefined
);
startWork(request); // If anything suspended and is still pending, we'll abort it before writing.
Expand Down
Loading

0 comments on commit 5cc1661

Please sign in to comment.