20
enableServerComponentLogs,
21
} from 'shared/ReactFeatureFlags';
22
23
+import {enableFlightReadableStream} from 'shared/ReactFeatureFlags';
24
+
25
import {
26
scheduleWork,
27
flushBuffered,
201
202
const ObjectPrototype = Object.prototype;
203
204
+const ASYNC_ITERATOR = Symbol.asyncIterator;
205
+
206
type JSONValue =
207
| string
208
| boolean
240
| null
241
| void
242
| bigint
243
+ | ReadableStream
244
+ | $AsyncIterable<ReactClientValue, ReactClientValue, void>
245
| Iterable<ReactClientValue>
246
| Array<ReactClientValue>
247
| Map<ReactClientValue, ReactClientValue>
288
nextChunkId: number,
289
pendingChunks: number,
290
hints: Hints,
291
+ abortListeners: Set<(reason: mixed) => void>,
292
abortableTasks: Set<Task>,
293
pingedTasks: Array<Task>,
294
completedImportChunks: Array<Chunk>,
385
nextChunkId: 0,
386
pendingChunks: 0,
387
hints,
388
+ abortListeners: new Set(),
389
abortableTasks: abortSet,
390
pingedTasks: pingedTasks,
391
completedImportChunks: ([]: Array<Chunk>),
517
emitErrorChunk(request, newTask.id, digest, reason);
518
}
519
request.abortableTasks.delete(newTask);
512
- if (request.destination !== null) {
513
- flushCompletedChunks(request, request.destination);
514
- }
520
+ enqueueFlush(request);
521
},
522
);
523
524
return newTask.id;
525
}
526
527
+function serializeReadableStream(
528
+ request: Request,
529
+ task: Task,
530
+ stream: ReadableStream,
531
+): string {
532
+ // Detect if this is a BYOB stream. BYOB streams should be able to be read as bytes on the
533
+ // receiving side. It also implies that different chunks can be split up or merged as opposed
534
+ // to a readable stream that happens to have Uint8Array as the type which might expect it to be
535
+ // received in the same slices.
536
+ // $FlowFixMe: This is a Node.js extension.
537
+ let supportsBYOB: void | boolean = stream.supportsBYOB;
538
+ if (supportsBYOB === undefined) {
539
+ try {
540
+ // $FlowFixMe[extra-arg]: This argument is accepted.
541
+ stream.getReader({mode: 'byob'}).releaseLock();
542
+ supportsBYOB = true;
543
+ } catch (x) {
544
+ supportsBYOB = false;
545
+ }
546
+ }
547
+
548
+ const reader = stream.getReader();
549
+
550
+ // This task won't actually be retried. We just use it to attempt synchronous renders.
551
+ const streamTask = createTask(
552
+ request,
553
+ task.model,
554
+ task.keyPath,
555
+ task.implicitSlot,
556
+ request.abortableTasks,
557
+ );
558
+ request.abortableTasks.delete(streamTask);
559
+
560
+ request.pendingChunks++; // The task represents the Start row. This adds a Stop row.
561
+
562
+ const startStreamRow =
563
+ streamTask.id.toString(16) + ':' + (supportsBYOB ? 'r' : 'R') + '\n';
564
+ request.completedRegularChunks.push(stringToChunk(startStreamRow));
565
+
566
+ // There's a race condition between when the stream is aborted and when the promise
567
+ // resolves so we track whether we already aborted it to avoid writing twice.
568
+ let aborted = false;
569
+ function progress(entry: {done: boolean, value: ReactClientValue, ...}) {
570
+ if (aborted) {
571
+ return;
572
+ }
573
+
574
+ if (entry.done) {
575
+ request.abortListeners.delete(error);
576
+ const endStreamRow = streamTask.id.toString(16) + ':C\n';
577
+ request.completedRegularChunks.push(stringToChunk(endStreamRow));
578
+ enqueueFlush(request);
579
+ aborted = true;
580
+ } else {
581
+ try {
582
+ streamTask.model = entry.value;
583
+ request.pendingChunks++;
584
+ tryStreamTask(request, streamTask);
585
+ enqueueFlush(request);
586
+ reader.read().then(progress, error);
587
+ } catch (x) {
588
+ error(x);
589
+ }
590
+ }
591
+ }
592
+ function error(reason: mixed) {
593
+ if (aborted) {
594
+ return;
595
+ }
596
+ aborted = true;
597
+ request.abortListeners.delete(error);
598
+ if (
599
+ enablePostpone &&
600
+ typeof reason === 'object' &&
601
+ reason !== null &&
602
+ (reason: any).$$typeof === REACT_POSTPONE_TYPE
603
+ ) {
604
+ const postponeInstance: Postpone = (reason: any);
605
+ logPostpone(request, postponeInstance.message);
606
+ emitPostponeChunk(request, streamTask.id, postponeInstance);
607
+ } else {
608
+ const digest = logRecoverableError(request, reason);
609
+ emitErrorChunk(request, streamTask.id, digest, reason);
610
+ }
611
+ enqueueFlush(request);
612
+ // $FlowFixMe should be able to pass mixed
613
+ reader.cancel(reason).then(error, error);
614
+ }
615
+ request.abortListeners.add(error);
616
+ reader.read().then(progress, error);
617
+ return serializeByValueID(streamTask.id);
618
+}
619
+
620
+function serializeAsyncIterable(
621
+ request: Request,
622
+ task: Task,
623
+ iterable: $AsyncIterable<ReactClientValue, ReactClientValue, void>,
624
+ iterator: $AsyncIterator<ReactClientValue, ReactClientValue, void>,
625
+): string {
626
+ // Generators/Iterators are Iterables but they're also their own iterator
627
+ // functions. If that's the case, we treat them as single-shot. Otherwise,
628
+ // we assume that this iterable might be a multi-shot and allow it to be
629
+ // iterated more than once on the client.
630
+ const isIterator = iterable === iterator;
631
+
632
+ // This task won't actually be retried. We just use it to attempt synchronous renders.
633
+ const streamTask = createTask(
634
+ request,
635
+ task.model,
636
+ task.keyPath,
637
+ task.implicitSlot,
638
+ request.abortableTasks,
639
+ );
640
+ request.abortableTasks.delete(streamTask);
641
+
642
+ request.pendingChunks++; // The task represents the Start row. This adds a Stop row.
643
+
644
+ const startStreamRow =
645
+ streamTask.id.toString(16) + ':' + (isIterator ? 'x' : 'X') + '\n';
646
+ request.completedRegularChunks.push(stringToChunk(startStreamRow));
647
+
648
+ if (__DEV__) {
649
+ const debugInfo: ?ReactDebugInfo = (iterable: any)._debugInfo;
650
+ if (debugInfo) {
651
+ forwardDebugInfo(request, streamTask.id, debugInfo);
652
+ }
653
+ }
654
+
655
+ // There's a race condition between when the stream is aborted and when the promise
656
+ // resolves so we track whether we already aborted it to avoid writing twice.
657
+ let aborted = false;
658
+ function progress(
659
+ entry:
660
+ | {done: false, +value: ReactClientValue, ...}
661
+ | {done: true, +value: ReactClientValue, ...},
662
+ ) {
663
+ if (aborted) {
664
+ return;
665
+ }
666
+
667
+ if (entry.done) {
668
+ request.abortListeners.delete(error);
669
+ let endStreamRow;
670
+ if (entry.value === undefined) {
671
+ endStreamRow = streamTask.id.toString(16) + ':C\n';
672
+ } else {
673
+ // Unlike streams, the last value may not be undefined. If it's not
674
+ // we outline it and encode a reference to it in the closing instruction.
675
+ try {
676
+ const chunkId = outlineModel(request, entry.value);
677
+ endStreamRow =
678
+ streamTask.id.toString(16) +
679
+ ':C' +
680
+ stringify(serializeByValueID(chunkId)) +
681
+ '\n';
682
+ } catch (x) {
683
+ error(x);
684
+ return;
685
+ }
686
+ }
687
+ request.completedRegularChunks.push(stringToChunk(endStreamRow));
688
+ enqueueFlush(request);
689
+ aborted = true;
690
+ } else {
691
+ try {
692
+ streamTask.model = entry.value;
693
+ request.pendingChunks++;
694
+ tryStreamTask(request, streamTask);
695
+ enqueueFlush(request);
696
+ iterator.next().then(progress, error);
697
+ } catch (x) {
698
+ error(x);
699
+ return;
700
+ }
701
+ }
702
+ }
703
+ function error(reason: mixed) {
704
+ if (aborted) {
705
+ return;
706
+ }
707
+ aborted = true;
708
+ request.abortListeners.delete(error);
709
+ if (
710
+ enablePostpone &&
711
+ typeof reason === 'object' &&
712
+ reason !== null &&
713
+ (reason: any).$$typeof === REACT_POSTPONE_TYPE
714
+ ) {
715
+ const postponeInstance: Postpone = (reason: any);
716
+ logPostpone(request, postponeInstance.message);
717
+ emitPostponeChunk(request, streamTask.id, postponeInstance);
718
+ } else {
719
+ const digest = logRecoverableError(request, reason);
720
+ emitErrorChunk(request, streamTask.id, digest, reason);
721
+ }
722
+ enqueueFlush(request);
723
+ if (typeof (iterator: any).throw === 'function') {
724
+ // The iterator protocol doesn't necessarily include this but a generator do.
725
+ // $FlowFixMe should be able to pass mixed
726
+ iterator.throw(reason).then(error, error);
727
+ }
728
+ }
729
+ request.abortListeners.add(error);
730
+ iterator.next().then(progress, error);
731
+ return serializeByValueID(streamTask.id);
732
+}
733
+
734
export function emitHint<Code: HintCode>(
735
request: Request,
736
code: Code,
904
task: Task,
905
children: $ReadOnlyArray<ReactClientValue>,
906
): ReactJSONValue {
907
+ if (enableServerComponentKeys && task.keyPath !== null) {
908
+ // We have a Server Component that specifies a key but we're now splitting
909
+ // the tree using a fragment.
910
+ const fragment = [
911
+ REACT_ELEMENT_TYPE,
912
+ REACT_FRAGMENT_TYPE,
913
+ task.keyPath,
914
+ {children},
915
+ ];
916
+ if (!task.implicitSlot) {
917
+ // If this was keyed inside a set. I.e. the outer Server Component was keyed
918
+ // then we need to handle reorders of the whole set. To do this we need to wrap
919
+ // this array in a keyed Fragment.
920
+ return fragment;
921
+ }
922
+ // If the outer Server Component was implicit but then an inner one had a key
923
+ // we don't actually need to be able to move the whole set around. It'll always be
924
+ // in an implicit slot. The key only exists to be able to reset the state of the
925
+ // children. We could achieve the same effect by passing on the keyPath to the next
926
+ // set of components inside the fragment. This would also allow a keyless fragment
927
+ // reconcile against a single child.
928
+ // Unfortunately because of JSON.stringify, we can't call the recursive loop for
929
+ // each child within this context because we can't return a set with already resolved
930
+ // values. E.g. a string would get double encoded. Returning would pop the context.
931
+ // So instead, we wrap it with an unkeyed fragment and inner keyed fragment.
932
+ return [fragment];
933
+ }
934
+ // Since we're yielding here, that implicitly resets the keyPath context on the
935
+ // way up. Which is what we want since we've consumed it. If this changes to
936
+ // be recursive serialization, we need to reset the keyPath and implicitSlot,
937
+ // before recursing here.
938
if (__DEV__) {
939
const debugInfo: ?ReactDebugInfo = (children: any)._debugInfo;
940
if (debugInfo) {
949
// from the server by the time we emit it.
950
forwardDebugInfo(request, debugID, debugInfo);
951
}
952
+ // Since we're rendering this array again, create a copy that doesn't
953
+ // have the debug info so we avoid outlining or emitting debug info again.
954
+ children = Array.from(children);
955
}
956
}
710
- if (!enableServerComponentKeys) {
711
- return children;
712
- }
713
- if (task.keyPath !== null) {
957
+ return children;
958
+}
959
+
960
+function renderAsyncFragment(
961
+ request: Request,
962
+ task: Task,
963
+ children: $AsyncIterable<ReactClientValue, ReactClientValue, void>,
964
+ getAsyncIterator: () => $AsyncIterator<any, any, any>,
965
+): ReactJSONValue {
966
+ if (enableServerComponentKeys && task.keyPath !== null) {
967
// We have a Server Component that specifies a key but we're now splitting
968
// the tree using a fragment.
969
const fragment = [
990
// So instead, we wrap it with an unkeyed fragment and inner keyed fragment.
991
return [fragment];
992
}
993
+
994
// Since we're yielding here, that implicitly resets the keyPath context on the
995
// way up. Which is what we want since we've consumed it. If this changes to
996
// be recursive serialization, we need to reset the keyPath and implicitSlot,
997
// before recursing here.
744
- return children;
998
+ const asyncIterator = getAsyncIterator.call(children);
999
+ return serializeAsyncIterable(request, task, children, asyncIterator);
1000
}
1001
1002
function renderClientElement(
1411
}
1412
1413
function serializeLargeTextString(request: Request, text: string): string {
1159
- request.pendingChunks += 2;
1414
+ request.pendingChunks++;
1415
const textId = request.nextChunkId++;
1161
- const textChunk = stringToChunk(text);
1162
- const binaryLength = byteLengthOfChunk(textChunk);
1163
- const row = textId.toString(16) + ':T' + binaryLength.toString(16) + ',';
1164
- const headerChunk = stringToChunk(row);
1165
- request.completedRegularChunks.push(headerChunk, textChunk);
1416
+ emitTextChunk(request, textId, text);
1417
return serializeByValueID(textId);
1418
}
1419
1463
tag: string,
1464
typedArray: $ArrayBufferView,
1465
): string {
1215
- if (enableTaint) {
1216
- if (TaintRegistryByteLengths.has(typedArray.byteLength)) {
1217
- // If we have had any tainted values of this length, we check
1218
- // to see if these bytes matches any entries in the registry.
1219
- const tainted = TaintRegistryValues.get(
1220
- binaryToComparableString(typedArray),
1221
- );
1222
- if (tainted !== undefined) {
1223
- throwTaintViolation(tainted.message);
1224
- }
1225
- }
1226
- }
1227
- request.pendingChunks += 2;
1466
+ request.pendingChunks++;
1467
const bufferId = request.nextChunkId++;
1229
- // TODO: Convert to little endian if that's not the server default.
1230
- const binaryChunk = typedArrayToBinaryChunk(typedArray);
1231
- const binaryLength = byteLengthOfBinaryChunk(binaryChunk);
1232
- const row =
1233
- bufferId.toString(16) + ':' + tag + binaryLength.toString(16) + ',';
1234
- const headerChunk = stringToChunk(row);
1235
- request.completedRegularChunks.push(headerChunk, binaryChunk);
1468
+ emitTypedArrayChunk(request, bufferId, tag, typedArray);
1469
return serializeByValueID(bufferId);
1470
}
1471
1481
1482
const reader = blob.stream().getReader();
1483
1484
+ let aborted = false;
1485
function progress(
1486
entry: {done: false, value: Uint8Array} | {done: true, value: void},
1487
): Promise<void> | void {
1488
+ if (aborted) {
1489
+ return;
1490
+ }
1491
if (entry.done) {
1492
+ request.abortListeners.delete(error);
1493
+ aborted = true;
1494
pingTask(request, newTask);
1495
return;
1496
}
1501
}
1502
1503
function error(reason: mixed) {
1504
+ if (aborted) {
1505
+ return;
1506
+ }
1507
+ aborted = true;
1508
+ request.abortListeners.delete(error);
1509
const digest = logRecoverableError(request, reason);
1510
emitErrorChunk(request, newTask.id, digest, reason);
1511
request.abortableTasks.delete(newTask);
1268
- if (request.destination !== null) {
1269
- flushCompletedChunks(request, request.destination);
1270
- }
1512
+ enqueueFlush(request);
1513
+ // $FlowFixMe should be able to pass mixed
1514
+ reader.cancel(reason).then(error, error);
1515
}
1516
+
1517
+ request.abortListeners.add(error);
1518
+
1519
// $FlowFixMe[incompatible-call]
1520
reader.read().then(progress).catch(error);
1521
1914
return renderFragment(request, task, Array.from((value: any)));
1915
}
1916
1917
+ if (enableFlightReadableStream) {
1918
+ // TODO: Blob is not available in old Node. Remove the typeof check later.
1919
+ if (
1920
+ typeof ReadableStream === 'function' &&
1921
+ value instanceof ReadableStream
1922
+ ) {
1923
+ return serializeReadableStream(request, task, value);
1924
+ }
1925
+ const getAsyncIterator: void | (() => $AsyncIterator<any, any, any>) =
1926
+ (value: any)[ASYNC_ITERATOR];
1927
+ if (typeof getAsyncIterator === 'function') {
1928
+ // We treat AsyncIterables as a Fragment and as such we might need to key them.
1929
+ return renderAsyncFragment(
1930
+ request,
1931
+ task,
1932
+ (value: any),
1933
+ getAsyncIterator,
1934
+ );
1935
+ }
1936
+ }
1937
+
1938
// Verify that this is a simple plain object.
1939
const proto = getPrototypeOf(value);
1940
if (
2299
request.completedRegularChunks.push(processedChunk);
2300
}
2301
2302
+function emitTypedArrayChunk(
2303
+ request: Request,
2304
+ id: number,
2305
+ tag: string,
2306
+ typedArray: $ArrayBufferView,
2307
+): void {
2308
+ if (enableTaint) {
2309
+ if (TaintRegistryByteLengths.has(typedArray.byteLength)) {
2310
+ // If we have had any tainted values of this length, we check
2311
+ // to see if these bytes matches any entries in the registry.
2312
+ const tainted = TaintRegistryValues.get(
2313
+ binaryToComparableString(typedArray),
2314
+ );
2315
+ if (tainted !== undefined) {
2316
+ throwTaintViolation(tainted.message);
2317
+ }
2318
+ }
2319
+ }
2320
+ request.pendingChunks++; // Extra chunk for the header.
2321
+ // TODO: Convert to little endian if that's not the server default.
2322
+ const binaryChunk = typedArrayToBinaryChunk(typedArray);
2323
+ const binaryLength = byteLengthOfBinaryChunk(binaryChunk);
2324
+ const row = id.toString(16) + ':' + tag + binaryLength.toString(16) + ',';
2325
+ const headerChunk = stringToChunk(row);
2326
+ request.completedRegularChunks.push(headerChunk, binaryChunk);
2327
+}
2328
+
2329
+function emitTextChunk(request: Request, id: number, text: string): void {
2330
+ request.pendingChunks++; // Extra chunk for the header.
2331
+ const textChunk = stringToChunk(text);
2332
+ const binaryLength = byteLengthOfChunk(textChunk);
2333
+ const row = id.toString(16) + ':T' + binaryLength.toString(16) + ',';
2334
+ const headerChunk = stringToChunk(row);
2335
+ request.completedRegularChunks.push(headerChunk, textChunk);
2336
+}
2337
+
2338
function serializeEval(source: string): string {
2339
if (!__DEV__) {
2340
// These errors should never make it into a build so we don't need to encode them in codes.json
2695
}
2696
}
2697
2698
+function emitChunk(
2699
+ request: Request,
2700
+ task: Task,
2701
+ value: ReactClientValue,
2702
+): void {
2703
+ const id = task.id;
2704
+ // For certain types we have special types, we typically outlined them but
2705
+ // we can emit them directly for this row instead of through an indirection.
2706
+ if (typeof value === 'string') {
2707
+ if (enableTaint) {
2708
+ const tainted = TaintRegistryValues.get(value);
2709
+ if (tainted !== undefined) {
2710
+ throwTaintViolation(tainted.message);
2711
+ }
2712
+ }
2713
+ emitTextChunk(request, id, value);
2714
+ return;
2715
+ }
2716
+ if (enableBinaryFlight) {
2717
+ if (value instanceof ArrayBuffer) {
2718
+ emitTypedArrayChunk(request, id, 'A', new Uint8Array(value));
2719
+ return;
2720
+ }
2721
+ if (value instanceof Int8Array) {
2722
+ // char
2723
+ emitTypedArrayChunk(request, id, 'O', value);
2724
+ return;
2725
+ }
2726
+ if (value instanceof Uint8Array) {
2727
+ // unsigned char
2728
+ emitTypedArrayChunk(request, id, 'o', value);
2729
+ return;
2730
+ }
2731
+ if (value instanceof Uint8ClampedArray) {
2732
+ // unsigned clamped char
2733
+ emitTypedArrayChunk(request, id, 'U', value);
2734
+ return;
2735
+ }
2736
+ if (value instanceof Int16Array) {
2737
+ // sort
2738
+ emitTypedArrayChunk(request, id, 'S', value);
2739
+ return;
2740
+ }
2741
+ if (value instanceof Uint16Array) {
2742
+ // unsigned short
2743
+ emitTypedArrayChunk(request, id, 's', value);
2744
+ return;
2745
+ }
2746
+ if (value instanceof Int32Array) {
2747
+ // long
2748
+ emitTypedArrayChunk(request, id, 'L', value);
2749
+ return;
2750
+ }
2751
+ if (value instanceof Uint32Array) {
2752
+ // unsigned long
2753
+ emitTypedArrayChunk(request, id, 'l', value);
2754
+ return;
2755
+ }
2756
+ if (value instanceof Float32Array) {
2757
+ // float
2758
+ emitTypedArrayChunk(request, id, 'G', value);
2759
+ return;
2760
+ }
2761
+ if (value instanceof Float64Array) {
2762
+ // double
2763
+ emitTypedArrayChunk(request, id, 'g', value);
2764
+ return;
2765
+ }
2766
+ if (value instanceof BigInt64Array) {
2767
+ // number
2768
+ emitTypedArrayChunk(request, id, 'M', value);
2769
+ return;
2770
+ }
2771
+ if (value instanceof BigUint64Array) {
2772
+ // unsigned number
2773
+ // We use "m" instead of "n" since JSON can start with "null"
2774
+ emitTypedArrayChunk(request, id, 'm', value);
2775
+ return;
2776
+ }
2777
+ if (value instanceof DataView) {
2778
+ emitTypedArrayChunk(request, id, 'V', value);
2779
+ return;
2780
+ }
2781
+ }
2782
+ // For anything else we need to try to serialize it using JSON.
2783
+ // $FlowFixMe[incompatible-type] stringify can return null for undefined but we never do
2784
+ const json: string = stringify(value, task.toJSON);
2785
+ emitModelChunk(request, task.id, json);
2786
+}
2787
+
2788
const emptyRoot = {};
2789
2790
function retryTask(request: Request, task: Task): void {
2829
task.keyPath = null;
2830
task.implicitSlot = false;
2831
2438
- let json: string;
2832
if (typeof resolvedModel === 'object' && resolvedModel !== null) {
2833
// Object might contain unresolved values like additional elements.
2834
// This is simulating what the JSON loop would do if this was part of it.
2442
- // $FlowFixMe[incompatible-type] stringify can return null for undefined but we never do
2443
- json = stringify(resolvedModel, task.toJSON);
2835
+ emitChunk(request, task, resolvedModel);
2836
} else {
2837
// If the value is a string, it means it's a terminal value and we already escaped it
2838
// We don't need to escape it again so it's not passed the toJSON replacer.
2839
// $FlowFixMe[incompatible-type] stringify can return null for undefined but we never do
2448
- json = stringify(resolvedModel);
2840
+ const json: string = stringify(resolvedModel);
2841
+ emitModelChunk(request, task.id, json);
2842
}
2450
- emitModelChunk(request, task.id, json);
2843
2844
request.abortableTasks.delete(task);
2845
task.status = COMPLETED;
2881
}
2882
}
2883
2884
+function tryStreamTask(request: Request, task: Task): void {
2885
+ // This is used to try to emit something synchronously but if it suspends,
2886
+ // we emit a reference to a new outlined task immediately instead.
2887
+ const prevDebugID = debugID;
2888
+ if (__DEV__) {
2889
+ // We don't use the id of the stream task for debugID. Instead we leave it null
2890
+ // so that we instead outline the row to get a new debugID if needed.
2891
+ debugID = null;
2892
+ }
2893
+ try {
2894
+ emitChunk(request, task, task.model);
2895
+ } finally {
2896
+ if (__DEV__) {
2897
+ debugID = prevDebugID;
2898
+ }
2899
+ }
2900
+}
2901
+
2902
function performWork(request: Request): void {
2903
const prevDispatcher = ReactSharedInternals.H;
2904
ReactSharedInternals.H = HooksDispatcher;
3013
cleanupTaintQueue(request);
3014
}
3015
close(destination);
3016
+ request.destination = null;
3017
}
3018
}
3019
3071
export function abort(request: Request, reason: mixed): void {
3072
try {
3073
const abortableTasks = request.abortableTasks;
3074
+ // We have tasks to abort. We'll emit one error row and then emit a reference
3075
+ // to that row from every row that's still remaining.
3076
if (abortableTasks.size > 0) {
2664
- // We have tasks to abort. We'll emit one error row and then emit a reference
2665
- // to that row from every row that's still remaining.
3077
request.pendingChunks++;
3078
const errorId = request.nextChunkId++;
3079
if (
3098
abortableTasks.forEach(task => abortTask(task, request, errorId));
3099
abortableTasks.clear();
3100
}
3101
+ const abortListeners = request.abortListeners;
3102
+ if (abortListeners.size > 0) {
3103
+ let error;
3104
+ if (
3105
+ enablePostpone &&
3106
+ typeof reason === 'object' &&
3107
+ reason !== null &&
3108
+ (reason: any).$$typeof === REACT_POSTPONE_TYPE
3109
+ ) {
3110
+ // We aborted with a Postpone but since we're passing this to an
3111
+ // external handler, passing this object would leak it outside React.
3112
+ // We create an alternative reason for it instead.
3113
+ error = new Error('The render was aborted due to being postponed.');
3114
+ } else {
3115
+ error =
3116
+ reason === undefined
3117
+ ? new Error(
3118
+ 'The render was aborted by the server without a reason.',
3119
+ )
3120
+ : reason;
3121
+ }
3122
+ abortListeners.forEach(callback => callback(error));
3123
+ abortListeners.clear();
3124
+ }
3125
if (request.destination !== null) {
3126
flushCompletedChunks(request, request.destination);
3127
}