7
* @flow
8
*/
9
10
+import type {Dispatcher as DispatcherType} from 'react-reconciler/src/ReactInternalTypes';
11
import type {Destination} from './ReactServerStreamConfig';
12
import type {ReactNodeList} from 'shared/ReactTypes';
13
+import type {
14
+ SuspenseBoundaryID,
15
+ ResponseState,
16
+} from './ReactServerFormatConfig';
17
18
import {
19
scheduleWork,
23
flushBuffered,
24
close,
25
} from './ReactServerStreamConfig';
21
-import {formatChunk} from './ReactServerFormatConfig';
22
-import {REACT_ELEMENT_TYPE} from 'shared/ReactSymbols';
26
+import {
27
+ writePlaceholder,
28
+ writeStartCompletedSuspenseBoundary,
29
+ writeStartPendingSuspenseBoundary,
30
+ writeStartClientRenderedSuspenseBoundary,
31
+ writeEndSuspenseBoundary,
32
+ writeStartSegment,
33
+ writeEndSegment,
34
+ writeClientRenderBoundaryInstruction,
35
+ writeCompletedBoundaryInstruction,
36
+ writeCompletedSegmentInstruction,
37
+ pushTextInstance,
38
+ pushStartInstance,
39
+ pushEndInstance,
40
+ createSuspenseBoundaryID,
41
+ createResponseState,
42
+} from './ReactServerFormatConfig';
43
+import {REACT_ELEMENT_TYPE, REACT_SUSPENSE_TYPE} from 'shared/ReactSymbols';
44
+import ReactSharedInternals from 'shared/ReactSharedInternals';
45
24
-type OpaqueRequest = {
25
- destination: Destination,
26
- children: ReactNodeList,
27
- completedChunks: Array<Uint8Array>,
28
- flowing: boolean,
29
- ...
46
+import invariant from 'shared/invariant';
47
+
48
+const ReactCurrentDispatcher = ReactSharedInternals.ReactCurrentDispatcher;
49
+
50
+type SuspenseBoundary = {
51
+ +id: SuspenseBoundaryID,
52
+ rootSegmentID: number,
53
+ forceClientRender: boolean, // if it errors or infinitely suspends
54
+ parentFlushed: boolean,
55
+ pendingWork: number, // when it reaches zero we can show this boundary's content
56
+ completedSegments: Array<Segment>, // completed but not yet flushed segments.
57
+ byteSize: number, // used to determine whether to inline children boundaries.
58
+};
59
+
60
+type SuspendedWork = {
61
+ node: ReactNodeList,
62
+ ping: () => void,
63
+ blockedBoundary: Root | SuspenseBoundary,
64
+ blockedSegment: Segment, // the segment we'll write to
65
+ assignID: null | SuspenseBoundaryID, // id to assign to the content
66
+};
67
+
68
+const PENDING = 0;
69
+const COMPLETED = 1;
70
+const FLUSHED = 2;
71
+const ERRORED = 3;
72
+
73
+type Root = null;
74
+
75
+type Segment = {
76
+ status: 0 | 1 | 2 | 3,
77
+ parentFlushed: boolean, // typically a segment will be flushed by its parent, except if its parent was already flushed
78
+ id: number, // starts as 0 and is lazily assigned if the parent flushes early
79
+ +index: number, // the index within the parent's chunks or 0 at the root
80
+ +chunks: Array<Uint8Array>,
81
+ +children: Array<Segment>,
82
+ // If this segment represents a fallback, this is the content that will replace that fallback.
83
+ +boundary: null | SuspenseBoundary,
84
+};
85
+
86
+const BUFFERING = 0;
87
+const FLOWING = 1;
88
+const CLOSED = 2;
89
+
90
+type Request = {
91
+ +destination: Destination,
92
+ +responseState: ResponseState,
93
+ +maxBoundarySize: number,
94
+ status: 0 | 1 | 2,
95
+ nextSegmentId: number,
96
+ allPendingWork: number, // when it reaches zero, we can close the connection.
97
+ pendingRootWork: number, // when this reaches zero, we've finished at least the root boundary.
98
+ completedRootSegment: null | Segment, // Completed but not yet flushed root segments.
99
+ pingedWork: Array<SuspendedWork>,
100
+ // Queues to flush in order of priority
101
+ clientRenderedBoundaries: Array<SuspenseBoundary>, // Errored or client rendered but not yet flushed.
102
+ completedBoundaries: Array<SuspenseBoundary>, // Completed but not yet fully flushed boundaries to show.
103
+ partialBoundaries: Array<SuspenseBoundary>, // Partially completed boundaries that can flush its segments early.
104
};
105
106
export function createRequest(
107
children: ReactNodeList,
108
destination: Destination,
35
-): OpaqueRequest {
36
- return {destination, children, completedChunks: [], flowing: false};
109
+): Request {
110
+ const pingedWork = [];
111
+ const request = {
112
+ destination,
113
+ responseState: createResponseState(),
114
+ maxBoundarySize: 1024,
115
+ status: BUFFERING,
116
+ nextSegmentId: 0,
117
+ allPendingWork: 0,
118
+ pendingRootWork: 0,
119
+ completedRootSegment: null,
120
+ pingedWork: pingedWork,
121
+ clientRenderedBoundaries: [],
122
+ completedBoundaries: [],
123
+ partialBoundaries: [],
124
+ };
125
+ // This segment represents the root fallback.
126
+ const rootSegment = createPendingSegment(request, 0, null);
127
+ // There is no parent so conceptually, we're unblocked to flush this segment.
128
+ rootSegment.parentFlushed = true;
129
+ const rootWork = createSuspendedWork(
130
+ request,
131
+ children,
132
+ null,
133
+ rootSegment,
134
+ null,
135
+ );
136
+ pingedWork.push(rootWork);
137
+ return request;
138
+}
139
+
140
+function pingSuspendedWork(request: Request, work: SuspendedWork): void {
141
+ const pingedWork = request.pingedWork;
142
+ pingedWork.push(work);
143
+ if (pingedWork.length === 1) {
144
+ scheduleWork(() => performWork(request));
145
+ }
146
+}
147
+
148
+function createSuspenseBoundary(request: Request): SuspenseBoundary {
149
+ return {
150
+ id: createSuspenseBoundaryID(request.responseState),
151
+ rootSegmentID: -1,
152
+ parentFlushed: false,
153
+ pendingWork: 0,
154
+ forceClientRender: false,
155
+ completedSegments: [],
156
+ byteSize: 0,
157
+ };
158
}
159
39
-function performWork(request: OpaqueRequest): void {
40
- const element = (request.children: any);
41
- request.children = null;
42
- if (element && element.$$typeof !== REACT_ELEMENT_TYPE) {
160
+function createSuspendedWork(
161
+ request: Request,
162
+ node: ReactNodeList,
163
+ blockedBoundary: Root | SuspenseBoundary,
164
+ blockedSegment: Segment,
165
+ assignID: null | SuspenseBoundaryID,
166
+): SuspendedWork {
167
+ request.allPendingWork++;
168
+ if (blockedBoundary === null) {
169
+ request.pendingRootWork++;
170
+ } else {
171
+ blockedBoundary.pendingWork++;
172
+ }
173
+ const work = {
174
+ node,
175
+ ping: () => pingSuspendedWork(request, work),
176
+ blockedBoundary,
177
+ blockedSegment,
178
+ assignID,
179
+ };
180
+ return work;
181
+}
182
+
183
+function createPendingSegment(
184
+ request: Request,
185
+ index: number,
186
+ boundary: null | SuspenseBoundary,
187
+): Segment {
188
+ return {
189
+ status: PENDING,
190
+ id: -1, // lazily assigned later
191
+ index,
192
+ parentFlushed: false,
193
+ chunks: [],
194
+ children: [],
195
+ boundary,
196
+ };
197
+}
198
+
199
+function reportError(request: Request, error: mixed): void {
200
+ // TODO: Report errors on the server.
201
+}
202
+
203
+function fatalError(request: Request, error: mixed): void {
204
+ // This is called outside error handling code such as if the root errors outside
205
+ // a suspense boundary or if the root suspense boundary's fallback errors.
206
+ // It's also called if React itself or its host configs errors.
207
+ request.status = CLOSED;
208
+ // TODO: Destroy the stream with an error. We weren't able to complete the root.
209
+}
210
+
211
+function renderNode(
212
+ request: Request,
213
+ parentBoundary: Root | SuspenseBoundary,
214
+ segment: Segment,
215
+ node: ReactNodeList,
216
+): void {
217
+ if (typeof node === 'string') {
218
+ pushTextInstance(segment.chunks, node);
219
return;
220
}
221
+ if (
222
+ typeof node !== 'object' ||
223
+ !node ||
224
+ (node: any).$$typeof !== REACT_ELEMENT_TYPE
225
+ ) {
226
+ throw new Error('Not yet implemented node type.');
227
+ }
228
+ const element: React$Element<any> = (node: any);
229
const type = element.type;
230
const props = element.props;
47
- if (typeof type !== 'string') {
231
+ if (typeof type === 'function') {
232
+ try {
233
+ const result = type(props);
234
+ renderNode(request, parentBoundary, segment, result);
235
+ } catch (x) {
236
+ if (typeof x === 'object' && x !== null && typeof x.then === 'function') {
237
+ // Something suspended, we'll need to create a new segment and resolve it later.
238
+ const insertionIndex = segment.chunks.length;
239
+ const newSegment = createPendingSegment(request, insertionIndex, null);
240
+ const suspendedWork = createSuspendedWork(
241
+ request,
242
+ node,
243
+ parentBoundary,
244
+ newSegment,
245
+ null,
246
+ );
247
+ const ping = suspendedWork.ping;
248
+ x.then(ping, ping);
249
+ // TODO: Emit place holder
250
+ } else {
251
+ // We can rethrow to terminate the rest of this tree.
252
+ throw x;
253
+ }
254
+ }
255
+ } else if (typeof type === 'string') {
256
+ pushStartInstance(segment.chunks, type, props);
257
+ renderNode(request, parentBoundary, segment, props.children);
258
+ pushEndInstance(segment.chunks, type, props);
259
+ } else if (type === REACT_SUSPENSE_TYPE) {
260
+ // Each time we enter a suspense boundary, we split out into a new segment for
261
+ // the fallback so that we can later replace that segment with the content.
262
+ // This also lets us split out the main content even if it doesn't suspend,
263
+ // in case it ends up generating a large subtree of content.
264
+ const fallback: ReactNodeList = props.fallback;
265
+ const content: ReactNodeList = props.children;
266
+
267
+ const newBoundary = createSuspenseBoundary(request);
268
+
269
+ const insertionIndex = segment.chunks.length;
270
+ // The children of the boundary segment is actually the fallback.
271
+ const boundarySegment = createPendingSegment(
272
+ request,
273
+ insertionIndex,
274
+ newBoundary,
275
+ );
276
+ // We create suspended work for the fallback because we don't want to actually work
277
+ // on it yet in case we finish the main content, so we queue for later.
278
+ const suspendedFallbackWork = createSuspendedWork(
279
+ request,
280
+ fallback,
281
+ parentBoundary,
282
+ boundarySegment,
283
+ newBoundary.id, // This is the ID we want to give this fallback so we can replace it later.
284
+ );
285
+ // TODO: This should be queued at a separate lower priority queue so that we only work
286
+ // on preparing fallbacks if we don't have any more main content to work on.
287
+ request.pingedWork.push(suspendedFallbackWork);
288
+
289
+ // This segment is the actual child content. We can start rendering that immediately.
290
+ const contentRootSegment = createPendingSegment(request, 0, null);
291
+ // We mark the root segment as having its parent flushed. It's not really flushed but there is
292
+ // no parent segment so there's nothing to wait on.
293
+ contentRootSegment.parentFlushed = true;
294
+ // TODO: Currently this is running synchronously. We could instead schedule this to pingedWork.
295
+ // I suspect that there might be some efficiency benefits from not creating the suspended work
296
+ // and instead just using the stack if possible. Particularly when we add contexts.
297
+ const contentWork = createSuspendedWork(
298
+ request,
299
+ content,
300
+ newBoundary,
301
+ contentRootSegment,
302
+ null,
303
+ );
304
+ retryWork(request, contentWork);
305
+ } else {
306
+ throw new Error('Not yet implemented element type.');
307
+ }
308
+}
309
+
310
+function errorWork(
311
+ request: Request,
312
+ boundary: Root | SuspenseBoundary,
313
+ segment: Segment,
314
+ error: mixed,
315
+) {
316
+ segment.status = ERRORED;
317
+
318
+ request.allPendingWork--;
319
+ if (boundary !== null) {
320
+ boundary.pendingWork--;
321
+ }
322
+
323
+ // Report the error to a global handler.
324
+ reportError(request, error);
325
+ if (boundary === null) {
326
+ fatalError(request, error);
327
+ } else if (!boundary.forceClientRender) {
328
+ boundary.forceClientRender = true;
329
+ // Regardless of what happens next, this boundary won't be displayed,
330
+ // so we can flush it, if the parent already flushed.
331
+ if (boundary.parentFlushed) {
332
+ // We don't have a preference where in the queue this goes since it's likely
333
+ // to error on the client anyway. However, intentionally client-rendered
334
+ // boundaries should be flushed earlier so that they can start on the client.
335
+ // We reuse the same queue for errors.
336
+ request.clientRenderedBoundaries.push(boundary);
337
+ }
338
+ }
339
+}
340
+
341
+function completeWork(
342
+ request: Request,
343
+ boundary: Root | SuspenseBoundary,
344
+ segment: Segment,
345
+) {
346
+ segment.status = COMPLETED;
347
+ request.allPendingWork--;
348
+
349
+ if (boundary === null) {
350
+ request.pendingRootWork--;
351
+ if (segment.parentFlushed) {
352
+ invariant(
353
+ request.completedRootSegment === null,
354
+ 'There can only be one root segment. This is a bug in React.',
355
+ );
356
+ request.completedRootSegment = segment;
357
+ }
358
return;
359
}
50
- request.completedChunks.push(formatChunk(type, props));
51
- if (request.flowing) {
52
- flushCompletedChunks(request);
360
+
361
+ boundary.pendingWork--;
362
+ if (boundary.forceClientRender) {
363
+ // This already errored.
364
+ return;
365
}
366
+ if (boundary.pendingWork === 0) {
367
+ // This must have been the last segment we were waiting on. This boundary is now complete.
368
+ if (segment.parentFlushed) {
369
+ // Our parent segment already flushed, so we need to schedule this segment to be emitted.
370
+ boundary.completedSegments.push(segment);
371
+ }
372
+ if (boundary.parentFlushed) {
373
+ // The segment might be part of a segment that didn't flush yet, but if the boundary's
374
+ // parent flushed, we need to schedule the boundary to be emitted.
375
+ request.completedBoundaries.push(boundary);
376
+ }
377
+ } else {
378
+ if (segment.parentFlushed) {
379
+ // Our parent already flushed, so we need to schedule this segment to be emitted.
380
+ const completedSegments = boundary.completedSegments;
381
+ completedSegments.push(segment);
382
+ if (completedSegments.length === 1) {
383
+ // This is the first time since we last flushed that we completed anything.
384
+ // We can schedule this boundary to emit its partially completed segments early
385
+ // in case the parent has already been flushed.
386
+ if (boundary.parentFlushed) {
387
+ request.partialBoundaries.push(boundary);
388
+ }
389
+ }
390
+ }
391
+ }
392
+}
393
+
394
+function retryWork(request: Request, work: SuspendedWork): void {
395
+ const segment = work.blockedSegment;
396
+ const boundary = work.blockedBoundary;
397
+ try {
398
+ let node = work.node;
399
+ while (
400
+ typeof node === 'object' &&
401
+ node !== null &&
402
+ (node: any).$$typeof === REACT_ELEMENT_TYPE &&
403
+ typeof node.type === 'function'
404
+ ) {
405
+ // Doing this here lets us reuse this same Segment if the next component
406
+ // also suspends.
407
+ const element: React$Element<any> = (node: any);
408
+ work.node = node;
409
+ // TODO: Classes and legacy context etc.
410
+ node = element.type(element.props);
411
+ }
412
+
413
+ renderNode(request, boundary, segment, node);
414
55
- flushBuffered(request.destination);
415
+ completeWork(request, boundary, segment);
416
+ } catch (x) {
417
+ if (typeof x === 'object' && x !== null && typeof x.then === 'function') {
418
+ // Something suspended again, let's pick it back up later.
419
+ const ping = work.ping;
420
+ x.then(ping, ping);
421
+ } else {
422
+ errorWork(request, boundary, segment, x);
423
+ }
424
+ }
425
}
426
58
-function flushCompletedChunks(request: OpaqueRequest) {
59
- const destination = request.destination;
60
- const chunks = request.completedChunks;
61
- request.completedChunks = [];
427
+function performWork(request: Request): void {
428
+ if (request.status === CLOSED) {
429
+ return;
430
+ }
431
+ const prevDispatcher = ReactCurrentDispatcher.current;
432
+ ReactCurrentDispatcher.current = Dispatcher;
433
+
434
+ try {
435
+ const pingedWork = request.pingedWork;
436
+ let i;
437
+ for (i = 0; i < pingedWork.length; i++) {
438
+ const work = pingedWork[i];
439
+ retryWork(request, work);
440
+ }
441
+ pingedWork.splice(0, i);
442
+ if (request.status === FLOWING) {
443
+ flushCompletedQueues(request);
444
+ }
445
+ } catch (error) {
446
+ fatalError(request, error);
447
+ } finally {
448
+ ReactCurrentDispatcher.current = prevDispatcher;
449
+ }
450
+}
451
+
452
+function flushSubtree(
453
+ request: Request,
454
+ destination: Destination,
455
+ segment: Segment,
456
+): boolean {
457
+ segment.parentFlushed = true;
458
+ switch (segment.status) {
459
+ case PENDING: {
460
+ // We're emitting a placeholder for this segment to be filled in later.
461
+ // Therefore we'll need to assign it an ID - to refer to it by.
462
+ const segmentID = (segment.id = request.nextSegmentId++);
463
+ return writePlaceholder(destination, segmentID);
464
+ }
465
+ case COMPLETED: {
466
+ segment.status = FLUSHED;
467
+ let r = true;
468
+ const chunks = segment.chunks;
469
+ let chunkIdx = 0;
470
+ const children = segment.children;
471
+ for (let childIdx = 0; childIdx < children.length; childIdx++) {
472
+ const nextChild = children[childIdx];
473
+ // Write all the chunks up until the next child.
474
+ for (; chunkIdx < nextChild.index; chunkIdx++) {
475
+ writeChunk(destination, chunks[chunkIdx]);
476
+ }
477
+ r = flushSegment(request, destination, nextChild);
478
+ }
479
+ // Finally just write all the remaining chunks
480
+ for (; chunkIdx < chunks.length; chunkIdx++) {
481
+ r = writeChunk(destination, chunks[chunkIdx]);
482
+ }
483
+ return r;
484
+ }
485
+ default: {
486
+ invariant(
487
+ false,
488
+ 'Errored or already flushed boundaries should not be flushed again. This is a bug in React.',
489
+ );
490
+ }
491
+ }
492
+}
493
+
494
+function flushSegment(
495
+ request: Request,
496
+ destination,
497
+ segment: Segment,
498
+): boolean {
499
+ const boundary = segment.boundary;
500
+ if (boundary === null) {
501
+ // Not a suspense boundary.
502
+ return flushSubtree(request, destination, segment);
503
+ }
504
+ boundary.parentFlushed = true;
505
+ // This segment is a Suspense boundary. We need to decide whether to
506
+ // emit the content or the fallback now.
507
+ if (boundary.forceClientRender) {
508
+ // Emit a client rendered suspense boundary wrapper.
509
+ // We never queue the inner boundary so we'll never emit its content or partial segments.
510
+
511
+ writeStartClientRenderedSuspenseBoundary(destination, boundary.id);
512
+
513
+ // Flush the fallback.
514
+ flushSubtree(request, destination, segment);
515
+
516
+ return writeEndSuspenseBoundary(destination);
517
+ } else if (boundary.pendingWork > 0) {
518
+ // This boundary is still loading. Emit a pending suspense boundary wrapper.
519
+
520
+ // Assign an ID to refer to the future content by.
521
+ boundary.rootSegmentID = request.nextSegmentId++;
522
+ if (boundary.completedSegments.length > 0) {
523
+ // If this is at least partially complete, we can queue it to be partially emmitted early.
524
+ request.partialBoundaries.push(boundary);
525
+ }
526
+
527
+ writeStartPendingSuspenseBoundary(destination, boundary.id);
528
+
529
+ // Flush the fallback.
530
+ flushSubtree(request, destination, segment);
531
+
532
+ return writeEndSuspenseBoundary(destination);
533
+ } else if (boundary.byteSize > request.maxBoundarySize) {
534
+ // This boundary is large and will be emitted separately so that we can progressively show
535
+ // other content. We add it to the queue during the flush because we have to ensure that
536
+ // the parent flushes first so that there's something to inject it into.
537
+ // We also have to make sure that it's emitted into the queue in a deterministic slot.
538
+ // I.e. we can't insert it here when it completes.
539
+
540
+ // Assign an ID to refer to the future content by.
541
+ boundary.rootSegmentID = request.nextSegmentId++;
542
+
543
+ request.completedBoundaries.push(boundary);
544
+ // Emit a pending rendered suspense boundary wrapper.
545
+ writeStartPendingSuspenseBoundary(destination, boundary.id);
546
+
547
+ // Flush the fallback.
548
+ flushSubtree(request, destination, segment);
549
+
550
+ return writeEndSuspenseBoundary(destination);
551
+ } else {
552
+ // We can inline this boundary's content as a complete boundary.
553
+
554
+ writeStartCompletedSuspenseBoundary(destination, boundary.id);
555
+
556
+ const completedSegments = boundary.completedSegments;
557
+ invariant(
558
+ completedSegments.length === 1,
559
+ 'A previously unvisited boundary must have exactly one root segment. This is a bug in React.',
560
+ );
561
+ const contentSegment = completedSegments[0];
562
+ flushSegment(request, destination, contentSegment);
563
+
564
+ return writeEndSuspenseBoundary(destination);
565
+ }
566
+}
567
+
568
+function flushClientRenderedBoundary(
569
+ request: Request,
570
+ destination: Destination,
571
+ boundary: SuspenseBoundary,
572
+): boolean {
573
+ return writeClientRenderBoundaryInstruction(
574
+ destination,
575
+ request.responseState,
576
+ boundary.id,
577
+ );
578
+}
579
+
580
+function flushSegmentContainer(
581
+ request: Request,
582
+ destination: Destination,
583
+ segment: Segment,
584
+): boolean {
585
+ writeStartSegment(destination, segment.id);
586
+ flushSegment(request, destination, segment);
587
+ return writeEndSegment(destination);
588
+}
589
+
590
+function flushCompletedBoundary(
591
+ request: Request,
592
+ destination: Destination,
593
+ boundary: SuspenseBoundary,
594
+): boolean {
595
+ const completedSegments = boundary.completedSegments;
596
+ let i = 0;
597
+ for (; i < completedSegments.length; i++) {
598
+ const segment = completedSegments[i];
599
+ flushPartiallyCompletedSegment(request, destination, boundary, segment);
600
+ }
601
+ completedSegments.length = 0;
602
+
603
+ return writeCompletedBoundaryInstruction(
604
+ destination,
605
+ request.responseState,
606
+ boundary.id,
607
+ boundary.rootSegmentID,
608
+ );
609
+}
610
+
611
+function flushPartialBoundary(
612
+ request: Request,
613
+ destination: Destination,
614
+ boundary: SuspenseBoundary,
615
+): boolean {
616
+ const completedSegments = boundary.completedSegments;
617
+ let i = 0;
618
+ for (; i < completedSegments.length; i++) {
619
+ const segment = completedSegments[i];
620
+ if (
621
+ !flushPartiallyCompletedSegment(request, destination, boundary, segment)
622
+ ) {
623
+ i++;
624
+ completedSegments.splice(0, i);
625
+ // Only write as much as the buffer wants. Something higher priority
626
+ // might want to write later.
627
+ return false;
628
+ }
629
+ }
630
+ completedSegments.splice(0, i);
631
+ return true;
632
+}
633
+
634
+function flushPartiallyCompletedSegment(
635
+ request: Request,
636
+ destination: Destination,
637
+ boundary: SuspenseBoundary,
638
+ segment: Segment,
639
+): boolean {
640
+ if (segment.status === FLUSHED) {
641
+ // We've already flushed this inline.
642
+ return true;
643
+ }
644
+
645
+ const segmentID = segment.id;
646
+ if (segmentID === -1) {
647
+ // This segment wasn't previously referred to. This happens at the root of
648
+ // a boundary. We make kind of a leap here and assume this is the root.
649
+ const rootSegmentID = (segment.id = boundary.rootSegmentID);
650
+ invariant(
651
+ rootSegmentID !== -1,
652
+ 'A root segment ID must have been assigned by now. This is a bug in React.',
653
+ );
654
+ return flushSegmentContainer(request, destination, segment);
655
+ } else {
656
+ flushSegmentContainer(request, destination, segment);
657
+ return writeCompletedSegmentInstruction(
658
+ destination,
659
+ request.responseState,
660
+ segmentID,
661
+ );
662
+ }
663
+}
664
665
+let reentrant = false;
666
+function flushCompletedQueues(request: Request): void {
667
+ if (reentrant) {
668
+ return;
669
+ }
670
+ reentrant = true;
671
+
672
+ const destination = request.destination;
673
beginWriting(destination);
674
try {
65
- for (let i = 0; i < chunks.length; i++) {
66
- const chunk = chunks[i];
67
- writeChunk(destination, chunk);
675
+ // The structure of this is to go through each queue one by one and write
676
+ // until the sink tells us to stop. When we should stop, we still finish writing
677
+ // that item fully and then yield. At that point we remove the already completed
678
+ // items up until the point we completed them.
679
+
680
+ // TODO: Emit preloading.
681
+
682
+ // TODO: It's kind of unfortunate to keep checking this array after we've already
683
+ // emitted the root.
684
+ const completedRootSegment = request.completedRootSegment;
685
+ if (completedRootSegment !== null && request.pendingRootWork === 0) {
686
+ flushSegment(request, destination, completedRootSegment);
687
+ request.completedRootSegment = null;
688
+ }
689
+
690
+ // We emit client rendering instructions for already emitted boundaries first.
691
+ // This is so that we can signal to the client to start client rendering them as
692
+ // soon as possible.
693
+ const clientRenderedBoundaries = request.clientRenderedBoundaries;
694
+ let i;
695
+ for (i = 0; i < clientRenderedBoundaries.length; i++) {
696
+ const boundary = clientRenderedBoundaries[i];
697
+ if (!flushClientRenderedBoundary(request, destination, boundary)) {
698
+ request.status = BUFFERING;
699
+ i++;
700
+ clientRenderedBoundaries.splice(0, i);
701
+ return;
702
+ }
703
+ }
704
+ clientRenderedBoundaries.splice(0, i);
705
+
706
+ // Next we emit any complete boundaries. It's better to favor boundaries
707
+ // that are completely done since we can actually show them, than it is to emit
708
+ // any individual segments from a partially complete boundary.
709
+ const completedBoundaries = request.completedBoundaries;
710
+ for (i = 0; i < completedBoundaries.length; i++) {
711
+ const boundary = completedBoundaries[i];
712
+ if (!flushCompletedBoundary(request, destination, boundary)) {
713
+ request.status = BUFFERING;
714
+ i++;
715
+ completedBoundaries.splice(0, i);
716
+ return;
717
+ }
718
}
719
+ completedBoundaries.splice(0, i);
720
+
721
+ // Allow anything written so far to flush to the underlying sink before
722
+ // we continue with lower priorities.
723
+ completeWriting(destination);
724
+ beginWriting(destination);
725
+
726
+ // TODO: Here we'll emit data used by hydration.
727
+
728
+ // Next we emit any segments of any boundaries that are partially complete
729
+ // but not deeply complete.
730
+ const partialBoundaries = request.partialBoundaries;
731
+ for (i = 0; i < partialBoundaries.length; i++) {
732
+ const boundary = partialBoundaries[i];
733
+ if (!flushPartialBoundary(request, destination, boundary)) {
734
+ request.status = BUFFERING;
735
+ i++;
736
+ partialBoundaries.splice(0, i);
737
+ return;
738
+ }
739
+ }
740
+ partialBoundaries.splice(0, i);
741
+
742
+ // Next we check the completed boundaries again. This may have had
743
+ // boundaries added to it in case they were too larged to be inlined.
744
+ // New ones might be added in this loop.
745
+ const largeBoundaries = request.completedBoundaries;
746
+ for (i = 0; i < largeBoundaries.length; i++) {
747
+ const boundary = largeBoundaries[i];
748
+ if (!flushCompletedBoundary(request, destination, boundary)) {
749
+ request.status = BUFFERING;
750
+ i++;
751
+ largeBoundaries.splice(0, i);
752
+ return;
753
+ }
754
+ }
755
+ largeBoundaries.splice(0, i);
756
} finally {
757
+ reentrant = false;
758
completeWriting(destination);
759
+ flushBuffered(destination);
760
+ if (
761
+ request.allPendingWork === 0 &&
762
+ request.pingedWork.length === 0 &&
763
+ request.clientRenderedBoundaries.length === 0 &&
764
+ request.completedBoundaries.length === 0
765
+ // We don't need to check any partially completed segments because
766
+ // either they have pending work or they're complete.
767
+ ) {
768
+ // We're done.
769
+ close(destination);
770
+ }
771
}
72
- close(destination);
772
}
773
75
-export function startWork(request: OpaqueRequest): void {
76
- request.flowing = true;
774
+// TODO: Expose a way to abort further processing, without closing the connection from the outside.
775
+// This would put all waiting boundaries into client-only mode.
776
+
777
+export function startWork(request: Request): void {
778
+ // TODO: Don't automatically start flowing. Expose an explicit signal. Auto-start once everything is done.
779
+ request.status = FLOWING;
780
scheduleWork(() => performWork(request));
781
}
782
80
-export function startFlowing(request: OpaqueRequest): void {
81
- request.flowing = false;
82
- flushCompletedChunks(request);
783
+export function startFlowing(request: Request): void {
784
+ if (request.status === CLOSED) {
785
+ return;
786
+ }
787
+ request.status = FLOWING;
788
+ try {
789
+ flushCompletedQueues(request);
790
+ } catch (error) {
791
+ fatalError(request, error);
792
+ }
793
+}
794
+
795
+function notYetImplemented(): void {
796
+ throw new Error('Not yet implemented.');
797
}
798
+
799
+function unsupportedRefresh() {
800
+ invariant(false, 'Cache cannot be refreshed during server rendering.');
801
+}
802
+
803
+function unsupportedStartTransition() {
804
+ invariant(false, 'startTransition cannot be called during server rendering.');
805
+}
806
+
807
+function noop(): void {}
808
+
809
+const Dispatcher: DispatcherType = {
810
+ useMemo<T>(nextCreate: () => T): T {
811
+ return nextCreate();
812
+ },
813
+ useCallback<T>(callback: T): T {
814
+ return callback;
815
+ },
816
+ useDebugValue(): void {},
817
+ useDeferredValue<T>(value: T): T {
818
+ return value;
819
+ },
820
+ useTransition(): [(callback: () => void) => void, boolean] {
821
+ return [unsupportedStartTransition, false];
822
+ },
823
+ getCacheForType<T>(resourceType: () => T): T {
824
+ throw new Error('Not yet implemented. Should mark as client rendered.');
825
+ },
826
+ readContext: (notYetImplemented: any),
827
+ useContext: (notYetImplemented: any),
828
+ useReducer: (notYetImplemented: any),
829
+ useRef: (notYetImplemented: any),
830
+ useState: (notYetImplemented: any),
831
+ useLayoutEffect: noop,
832
+ // useImperativeHandle is not run in the server environment
833
+ useImperativeHandle: noop,
834
+ // Effects are not run in the server environment.
835
+ useEffect: noop,
836
+ useOpaqueIdentifier: (notYetImplemented: any),
837
+ useMutableSource: (notYetImplemented: any),
838
+ useCacheRefresh(): <T>(?() => T, ?T) => void {
839
+ return unsupportedRefresh;
840
+ },
841
+};