356
textEmbedded: boolean,
357
};
358
359
+// The ordering of these statuses matters. OPENING and OPEN are the only
360
+// statuses in which newly scheduled work may be performed. Any status greater
361
+// than OPEN represents a request that no longer admits work.
362
const OPENING = 10;
363
const OPEN = 11;
364
const CLOSING = 12;
4594
}
4595
}
4596
4594
-function abortTask(task: Task, request: Request, error: mixed): void {
4595
- // This aborts the task and aborts the parent that it blocks, putting it into
4596
- // client rendered mode.
4597
+function abortTask(task: Task, request: Request): void {
4598
+ // Mark pending tasks as aborted synchronously so any work that was already
4599
+ // scheduled cannot begin after abort is called.
4600
if (task === request.currentTask) {
4601
// This is a currently rendering Task. The render itself will abort the task.
4602
return;
4607
segment.status = ABORTED;
4608
}
4609
4607
- const errorInfo = getThrownInfo(task.componentStack);
4610
if (__DEV__ && enableAsyncDebugInfo) {
4609
- // If the task is not rendering, then this is an async abort. Conceptually it's as if
4610
- // the abort happened inside the async gap. The abort reason's stack frame won't have that
4611
- // on the stack so instead we use the owner stack and debug task of any halted async debug info.
4611
+ // Capture async debug information at the point abort begins. The task may
4612
+ // receive more data before finishAbort runs and no longer suspend at the
4613
+ // call site we need to report.
4614
let node: any = task.node;
4615
if (node !== null && typeof node === 'object') {
4614
- // Push a fake component stack frame that represents the await.
4616
let debugInfo = node._debugInfo;
4616
- // First resolve lazy nodes to find debug info that has been transferred
4617
- // to the inner value.
4617
while (
4618
typeof node === 'object' &&
4619
node !== null &&
4644
}
4645
}
4646
4647
+ if (boundary !== null) {
4648
+ boundary.fallbackAbortableTasks.forEach(fallbackTask =>
4649
+ abortTask(fallbackTask, request),
4650
+ );
4651
+ }
4652
+}
4653
+
4654
+function finishAbortedTask(task: Task, request: Request, error: mixed): void {
4655
+ // Report and complete a task that was synchronously claimed by abortTask.
4656
+ // A currently rendering task remains responsible for unwinding itself.
4657
+ if (task === request.currentTask) {
4658
+ return;
4659
+ }
4660
+ const boundary = task.blockedBoundary;
4661
+ const segment = task.blockedSegment;
4662
+ if (segment !== null) {
4663
+ if (segment.status !== ABORTED) {
4664
+ return;
4665
+ }
4666
+ }
4667
+
4668
+ const errorInfo = getThrownInfo(task.componentStack);
4669
+
4670
if (boundary === null) {
4671
const replay: null | ReplaySet = task.replay;
4672
if (replay === null) {
4728
// If this boundary was still pending then we haven't already cancelled its fallbacks.
4729
// We'll need to abort the fallbacks, which will also error that parent boundary.
4730
boundary.fallbackAbortableTasks.forEach(fallbackTask =>
4709
- abortTask(fallbackTask, request, error),
4731
+ finishAbortedTask(fallbackTask, request, error),
4732
);
4733
boundary.fallbackAbortableTasks.clear();
4734
return finishedTask(request, boundary, task.row, segment);
4765
// If this boundary was still pending then we haven't already cancelled its fallbacks.
4766
// We'll need to abort the fallbacks, which will also error that parent boundary.
4767
boundary.fallbackAbortableTasks.forEach(fallbackTask =>
4746
- abortTask(fallbackTask, request, error),
4768
+ finishAbortedTask(fallbackTask, request, error),
4769
);
4770
boundary.fallbackAbortableTasks.clear();
4771
}
4783
}
4784
}
4785
4764
-function abortTaskDEV(task: Task, request: Request, error: mixed): void {
4786
+function finishAbortedTaskDEV(
4787
+ task: Task,
4788
+ request: Request,
4789
+ error: mixed,
4790
+): void {
4791
if (__DEV__) {
4792
const prevTaskInDEV = currentTaskInDEV;
4793
const prevGetCurrentStackImpl = ReactSharedInternals.getCurrentStack;
4794
setCurrentTaskInDEV(task);
4795
ReactSharedInternals.getCurrentStack = getCurrentStackInDEV;
4796
try {
4771
- abortTask(task, request, error);
4797
+ finishAbortedTask(task, request, error);
4798
+ } finally {
4799
+ setCurrentTaskInDEV(prevTaskInDEV);
4800
+ ReactSharedInternals.getCurrentStack = prevGetCurrentStackImpl;
4801
+ }
4802
+ } else {
4803
+ // These errors should never make it into a build so we don't need to encode them in codes.json
4804
+ // eslint-disable-next-line react-internal/prod-error-codes
4805
+ throw new Error(
4806
+ 'finishAbortedTaskDEV should never be called in production mode. This is a bug in React.',
4807
+ );
4808
+ }
4809
+}
4810
+
4811
+function abortTaskDEV(task: Task, request: Request): void {
4812
+ if (__DEV__) {
4813
+ const prevTaskInDEV = currentTaskInDEV;
4814
+ const prevGetCurrentStackImpl = ReactSharedInternals.getCurrentStack;
4815
+ setCurrentTaskInDEV(task);
4816
+ ReactSharedInternals.getCurrentStack = getCurrentStackInDEV;
4817
+ try {
4818
+ abortTask(task, request);
4819
} finally {
4820
setCurrentTaskInDEV(prevTaskInDEV);
4821
ReactSharedInternals.getCurrentStack = prevGetCurrentStackImpl;
5323
}
5324
5325
export function performWork(request: Request): void {
5279
- if (request.status === CLOSED || request.status === CLOSING) {
5326
+ if (request.aborted || request.status > OPEN) {
5327
return;
5328
}
5329
const prevContext = getActiveContext();
6203
request.destination = null;
6204
}
6205
6159
-// This is called to early terminate a request. It puts all pending boundaries in client rendered state.
6160
-export function abort(request: Request, reason: mixed): void {
6161
- if (
6162
- request.aborted ||
6163
- (request.status !== OPEN && request.status !== OPENING)
6164
- ) {
6165
- // Only requests that are not already complete or in the process of aborting
6166
- // can be aborted. in practice this makes abort callable at most once per render.
6167
- return;
6168
- }
6169
- request.aborted = true;
6170
-
6206
+function finishAbort(request: Request, abortableTasks: Set<Task>): void {
6207
try {
6172
- const abortableTasks = request.abortableTasks;
6208
if (abortableTasks.size > 0) {
6174
- const error =
6175
- reason === undefined
6176
- ? new Error('The render was aborted by the server without a reason.')
6177
- : typeof reason === 'object' &&
6178
- reason !== null &&
6179
- typeof reason.then === 'function'
6180
- ? new Error('The render was aborted by the server with a promise.')
6181
- : reason;
6182
- // This error isn't necessarily fatal in this case but we need to stash it
6183
- // so we can use it to abort any pending work
6184
- request.fatalError = error;
6209
+ const error = request.fatalError;
6210
if (__DEV__) {
6186
- abortableTasks.forEach(task => abortTaskDEV(task, request, error));
6211
+ abortableTasks.forEach(task =>
6212
+ finishAbortedTaskDEV(task, request, error),
6213
+ );
6214
} else {
6188
- abortableTasks.forEach(task => abortTask(task, request, error));
6215
+ abortableTasks.forEach(task => finishAbortedTask(task, request, error));
6216
}
6217
abortableTasks.clear();
6218
}
6226
}
6227
}
6228
6229
+// This is called to early terminate a request. It puts all pending boundaries in client rendered state.
6230
+export function abort(request: Request, reason: mixed): void {
6231
+ if (
6232
+ request.aborted ||
6233
+ (request.status !== OPEN && request.status !== OPENING)
6234
+ ) {
6235
+ // Only requests that are not already complete or in the process of aborting
6236
+ // can be aborted. in practice this makes abort callable at most once per render.
6237
+ return;
6238
+ }
6239
+ request.aborted = true;
6240
+ const error =
6241
+ reason === undefined
6242
+ ? new Error('The render was aborted by the server without a reason.')
6243
+ : typeof reason === 'object' &&
6244
+ reason !== null &&
6245
+ typeof reason.then === 'function'
6246
+ ? new Error('The render was aborted by the server with a promise.')
6247
+ : reason;
6248
+ // This error isn't necessarily fatal in this case but we need to stash it
6249
+ // so we can use it to abort any pending work.
6250
+ request.fatalError = error;
6251
+ const abortableTasks = request.abortableTasks;
6252
+ if (__DEV__) {
6253
+ abortableTasks.forEach(task => abortTaskDEV(task, request));
6254
+ } else {
6255
+ abortableTasks.forEach(task => abortTask(task, request));
6256
+ }
6257
+ // Even though this looks async some renderers schedule work sync
6258
+ // So it is important that the finish step does not assume the stack
6259
+ // has unwinded yet
6260
+ scheduleWork(() => finishAbort(request, abortableTasks));
6261
+}
6262
+
6263
export function flushResources(request: Request): void {
6264
enqueueFlush(request);
6265
}