From 9080248d2eba23623059fbc0bcb5b1c8a0cb901a Mon Sep 17 00:00:00 2001 From: sebmarkbage Date: Tue, 16 Apr 2024 16:25:08 +0000 Subject: [PATCH] [Flight] Encode ReadableStream and AsyncIterables (#28847) This adds support in Flight for serializing four kinds of streams: - `ReadableStream` with objects as a model. This is a single shot iterator so you can read it only once. It can contain any value including Server Components. Chunks are encoded as is so if you send in 10 typed arrays, you get the same typed arrays out on the other side. - Binary `ReadableStream` with `type: 'bytes'` option. This supports the BYOB protocol. In this mode, the receiving side just gets `Uint8Array`s and they can be split across any single byte boundary into arbitrary chunks. - `AsyncIterable` where the `AsyncIterator` function is different than the `AsyncIterable` itself. In this case we assume that this might be a multi-shot iterable and so we buffer its value and you can iterate it multiple times on the other side. We support the `return` value as a value in the single completion slot, but you can't pass values in `next()`. If you want single-shot, return the AsyncIterator instead. - `AsyncIterator`. These gets serialized as a single-shot as it's just an iterator. `AsyncIterable`/`AsyncIterator` yield Promises that are instrumented with our `.status`/`.value` convention so that they can be synchronously looped over if available. They are also lazily parsed upon read. We can't do this with `ReadableStream` because we use the native implementation of `ReadableStream` which owns the promises. The format is a leading row that indicates which type of stream it is. Then a new row with the same ID is emitted for every chunk. Followed by either an error or close row. `AsyncIterable`s can also be returned as children of Server Components and then they're conceptually the same as fragment arrays/iterables. They can't actually be used as children in Fizz/Fiber but there's a separate plan for that. Only `AsyncIterable` not `AsyncIterator` will be valid as children - just like sync `Iterable` is already supported but single-shot `Iterator` is not. Notably, neither of these streams represent updates over time to a value. They represent multiple values in a list. When the server stream is aborted we also close the underlying stream. However, closing a stream on the client, doesn't close the underlying stream. A couple of possible follow ups I'm not planning on doing right now: - [ ] Free memory by releasing the buffer if an Iterator has been exhausted. Single shots could be optimized further to release individual items as you go. - [ ] We could clean up the underlying stream if the only pending data that's still flowing is from streams and all the streams have cleaned up. It's not very reliable though. It's better to do cancellation for the whole stream - e.g. at the framework level. - [ ] Implement smarter Binary Stream chunk handling. Currently we wait until we've received a whole row for binary chunks and copy them into consecutive memory. We need this to preserve semantics when passing typed arrays. However, for binary streams we don't need that. We can just send whatever pieces we have so far. DiffTrain build for [7909d8eabb7a702618f51e16a351df41aa8da88e](https://github.com/facebook/react/commit/7909d8eabb7a702618f51e16a351df41aa8da88e) --- compiled/facebook-www/REVISION | 2 +- .../ReactDOMServer-dev.classic.js | 32 +++-- .../facebook-www/ReactDOMServer-dev.modern.js | 32 +++-- .../ReactDOMServer-prod.classic.js | 23 ++-- .../ReactDOMServer-prod.modern.js | 23 ++-- .../ReactDOMServerStreaming-dev.modern.js | 30 +++-- .../ReactDOMServerStreaming-prod.modern.js | 21 ++-- .../ReactFlightDOMClient-dev.modern.js | 40 ++++-- .../ReactFlightDOMClient-prod.modern.js | 6 +- .../ReactFlightDOMServer-dev.modern.js | 118 +++++++++++------- .../ReactFlightDOMServer-prod.modern.js | 96 ++++++++------ 11 files changed, 256 insertions(+), 167 deletions(-) diff --git a/compiled/facebook-www/REVISION b/compiled/facebook-www/REVISION index 7f50772978111..9b70bd74e793e 100644 --- a/compiled/facebook-www/REVISION +++ b/compiled/facebook-www/REVISION @@ -1 +1 @@ -9defcd56bc3cd53ac2901ed93f29218007010434 +7909d8eabb7a702618f51e16a351df41aa8da88e diff --git a/compiled/facebook-www/ReactDOMServer-dev.classic.js b/compiled/facebook-www/ReactDOMServer-dev.classic.js index e0ab0fba07001..269a9659a2e72 100644 --- a/compiled/facebook-www/ReactDOMServer-dev.classic.js +++ b/compiled/facebook-www/ReactDOMServer-dev.classic.js @@ -19,7 +19,7 @@ if (__DEV__) { var React = require("react"); var ReactDOM = require("react-dom"); - var ReactVersion = "19.0.0-www-classic-bf1258d7"; + var ReactVersion = "19.0.0-www-classic-9835bfc2"; // This refers to a WWW module. var warningWWW = require("warning"); @@ -9940,8 +9940,14 @@ if (__DEV__) { } default: { - if (typeof thenable.status === "string"); - else { + if (typeof thenable.status === "string") { + // Only instrument the thenable if the status if not defined. If + // it's defined, but an unknown value, assume it's been instrumented by + // some custom userspace implementation. We treat it as "pending". + // Attach a dummy listener, to ensure that any lazy initialization can + // happen. Flight lazily parses JSON when the value is actually awaited. + thenable.then(noop$2, noop$2); + } else { var pendingThenable = thenable; pendingThenable.status = "pending"; pendingThenable.then( @@ -9959,18 +9965,18 @@ if (__DEV__) { rejectedThenable.reason = error; } } - ); // Check one more time in case the thenable resolved synchronously + ); + } // Check one more time in case the thenable resolved synchronously - switch (thenable.status) { - case "fulfilled": { - var fulfilledThenable = thenable; - return fulfilledThenable.value; - } + switch (thenable.status) { + case "fulfilled": { + var fulfilledThenable = thenable; + return fulfilledThenable.value; + } - case "rejected": { - var rejectedThenable = thenable; - throw rejectedThenable.reason; - } + case "rejected": { + var rejectedThenable = thenable; + throw rejectedThenable.reason; } } // Suspend. // diff --git a/compiled/facebook-www/ReactDOMServer-dev.modern.js b/compiled/facebook-www/ReactDOMServer-dev.modern.js index 0c6803072526b..55cfe3b0bfda7 100644 --- a/compiled/facebook-www/ReactDOMServer-dev.modern.js +++ b/compiled/facebook-www/ReactDOMServer-dev.modern.js @@ -19,7 +19,7 @@ if (__DEV__) { var React = require("react"); var ReactDOM = require("react-dom"); - var ReactVersion = "19.0.0-www-modern-d9b30156"; + var ReactVersion = "19.0.0-www-modern-72ca4dea"; // This refers to a WWW module. var warningWWW = require("warning"); @@ -9861,8 +9861,14 @@ if (__DEV__) { } default: { - if (typeof thenable.status === "string"); - else { + if (typeof thenable.status === "string") { + // Only instrument the thenable if the status if not defined. If + // it's defined, but an unknown value, assume it's been instrumented by + // some custom userspace implementation. We treat it as "pending". + // Attach a dummy listener, to ensure that any lazy initialization can + // happen. Flight lazily parses JSON when the value is actually awaited. + thenable.then(noop$2, noop$2); + } else { var pendingThenable = thenable; pendingThenable.status = "pending"; pendingThenable.then( @@ -9880,18 +9886,18 @@ if (__DEV__) { rejectedThenable.reason = error; } } - ); // Check one more time in case the thenable resolved synchronously + ); + } // Check one more time in case the thenable resolved synchronously - switch (thenable.status) { - case "fulfilled": { - var fulfilledThenable = thenable; - return fulfilledThenable.value; - } + switch (thenable.status) { + case "fulfilled": { + var fulfilledThenable = thenable; + return fulfilledThenable.value; + } - case "rejected": { - var rejectedThenable = thenable; - throw rejectedThenable.reason; - } + case "rejected": { + var rejectedThenable = thenable; + throw rejectedThenable.reason; } } // Suspend. // diff --git a/compiled/facebook-www/ReactDOMServer-prod.classic.js b/compiled/facebook-www/ReactDOMServer-prod.classic.js index b8fc4d3d45cab..91851af057b63 100644 --- a/compiled/facebook-www/ReactDOMServer-prod.classic.js +++ b/compiled/facebook-www/ReactDOMServer-prod.classic.js @@ -2952,9 +2952,9 @@ function trackUsedThenable(thenableState, thenable, index) { case "rejected": throw thenable.reason; default: - if ("string" !== typeof thenable.status) - switch ( - ((thenableState = thenable), + "string" === typeof thenable.status + ? thenable.then(noop$2, noop$2) + : ((thenableState = thenable), (thenableState.status = "pending"), thenableState.then( function (fulfilledValue) { @@ -2971,14 +2971,13 @@ function trackUsedThenable(thenableState, thenable, index) { rejectedThenable.reason = error; } } - ), - thenable.status) - ) { - case "fulfilled": - return thenable.value; - case "rejected": - throw thenable.reason; - } + )); + switch (thenable.status) { + case "fulfilled": + return thenable.value; + case "rejected": + throw thenable.reason; + } suspendedThenable = thenable; throw SuspenseException; } @@ -5681,4 +5680,4 @@ exports.renderToString = function (children, options) { 'The server used "renderToString" which does not support Suspense. If you intended for this Suspense boundary to render the fallback content on the server consider throwing an Error somewhere within the Suspense boundary. If you intended to have the server wait for the suspended component please switch to "renderToReadableStream" which supports Suspense on the server' ); }; -exports.version = "19.0.0-www-classic-c54d680a"; +exports.version = "19.0.0-www-classic-fef33f20"; diff --git a/compiled/facebook-www/ReactDOMServer-prod.modern.js b/compiled/facebook-www/ReactDOMServer-prod.modern.js index 32ef989481a00..8150dcd5150e1 100644 --- a/compiled/facebook-www/ReactDOMServer-prod.modern.js +++ b/compiled/facebook-www/ReactDOMServer-prod.modern.js @@ -2944,9 +2944,9 @@ function trackUsedThenable(thenableState, thenable, index) { case "rejected": throw thenable.reason; default: - if ("string" !== typeof thenable.status) - switch ( - ((thenableState = thenable), + "string" === typeof thenable.status + ? thenable.then(noop$2, noop$2) + : ((thenableState = thenable), (thenableState.status = "pending"), thenableState.then( function (fulfilledValue) { @@ -2963,14 +2963,13 @@ function trackUsedThenable(thenableState, thenable, index) { rejectedThenable.reason = error; } } - ), - thenable.status) - ) { - case "fulfilled": - return thenable.value; - case "rejected": - throw thenable.reason; - } + )); + switch (thenable.status) { + case "fulfilled": + return thenable.value; + case "rejected": + throw thenable.reason; + } suspendedThenable = thenable; throw SuspenseException; } @@ -5659,4 +5658,4 @@ exports.renderToString = function (children, options) { 'The server used "renderToString" which does not support Suspense. If you intended for this Suspense boundary to render the fallback content on the server consider throwing an Error somewhere within the Suspense boundary. If you intended to have the server wait for the suspended component please switch to "renderToReadableStream" which supports Suspense on the server' ); }; -exports.version = "19.0.0-www-modern-4944636f"; +exports.version = "19.0.0-www-modern-39d2e934"; diff --git a/compiled/facebook-www/ReactDOMServerStreaming-dev.modern.js b/compiled/facebook-www/ReactDOMServerStreaming-dev.modern.js index de4a90ac49b7c..16237c03d18ca 100644 --- a/compiled/facebook-www/ReactDOMServerStreaming-dev.modern.js +++ b/compiled/facebook-www/ReactDOMServerStreaming-dev.modern.js @@ -9743,8 +9743,14 @@ if (__DEV__) { } default: { - if (typeof thenable.status === "string"); - else { + if (typeof thenable.status === "string") { + // Only instrument the thenable if the status if not defined. If + // it's defined, but an unknown value, assume it's been instrumented by + // some custom userspace implementation. We treat it as "pending". + // Attach a dummy listener, to ensure that any lazy initialization can + // happen. Flight lazily parses JSON when the value is actually awaited. + thenable.then(noop$2, noop$2); + } else { var pendingThenable = thenable; pendingThenable.status = "pending"; pendingThenable.then( @@ -9762,18 +9768,18 @@ if (__DEV__) { rejectedThenable.reason = error; } } - ); // Check one more time in case the thenable resolved synchronously + ); + } // Check one more time in case the thenable resolved synchronously - switch (thenable.status) { - case "fulfilled": { - var fulfilledThenable = thenable; - return fulfilledThenable.value; - } + switch (thenable.status) { + case "fulfilled": { + var fulfilledThenable = thenable; + return fulfilledThenable.value; + } - case "rejected": { - var rejectedThenable = thenable; - throw rejectedThenable.reason; - } + case "rejected": { + var rejectedThenable = thenable; + throw rejectedThenable.reason; } } // Suspend. // diff --git a/compiled/facebook-www/ReactDOMServerStreaming-prod.modern.js b/compiled/facebook-www/ReactDOMServerStreaming-prod.modern.js index c9a453c4a29da..7c8ca2f3e9ef9 100644 --- a/compiled/facebook-www/ReactDOMServerStreaming-prod.modern.js +++ b/compiled/facebook-www/ReactDOMServerStreaming-prod.modern.js @@ -2818,9 +2818,9 @@ function trackUsedThenable(thenableState, thenable, index) { case "rejected": throw thenable.reason; default: - if ("string" !== typeof thenable.status) - switch ( - ((thenableState = thenable), + "string" === typeof thenable.status + ? thenable.then(noop$2, noop$2) + : ((thenableState = thenable), (thenableState.status = "pending"), thenableState.then( function (fulfilledValue) { @@ -2837,14 +2837,13 @@ function trackUsedThenable(thenableState, thenable, index) { rejectedThenable.reason = error; } } - ), - thenable.status) - ) { - case "fulfilled": - return thenable.value; - case "rejected": - throw thenable.reason; - } + )); + switch (thenable.status) { + case "fulfilled": + return thenable.value; + case "rejected": + throw thenable.reason; + } suspendedThenable = thenable; throw SuspenseException; } diff --git a/compiled/facebook-www/ReactFlightDOMClient-dev.modern.js b/compiled/facebook-www/ReactFlightDOMClient-dev.modern.js index c3249fbbabb49..00d69c98aa0e9 100644 --- a/compiled/facebook-www/ReactFlightDOMClient-dev.modern.js +++ b/compiled/facebook-www/ReactFlightDOMClient-dev.modern.js @@ -388,7 +388,10 @@ if (__DEV__) { break; default: - reject(chunk.reason); + if (reject) { + reject(chunk.reason); + } + break; } }; @@ -472,7 +475,6 @@ if (__DEV__) { function triggerErrorOnChunk(chunk, error) { if (chunk.status !== PENDING && chunk.status !== BLOCKED) { - // We already resolved. We didn't expect to see this. return; } @@ -503,7 +505,6 @@ if (__DEV__) { function resolveModelChunk(chunk, value) { if (chunk.status !== PENDING) { - // We already resolved. We didn't expect to see this. return; } @@ -816,6 +817,7 @@ if (__DEV__) { typeof chunkValue === "object" && chunkValue !== null && (Array.isArray(chunkValue) || + typeof chunkValue[ASYNC_ITERATOR] === "function" || chunkValue.$$typeof === REACT_ELEMENT_TYPE) && !chunkValue._debugInfo ) { @@ -1118,8 +1120,7 @@ if (__DEV__) { } function resolveText(response, id, text) { - var chunks = response._chunks; // We assume that we always reference large strings after they've been - // emitted. + var chunks = response._chunks; chunks.set(id, createInitializedTextChunk(response, text)); } @@ -1171,6 +1172,8 @@ if (__DEV__) { } } + var ASYNC_ITERATOR = Symbol.asyncIterator; + function resolveErrorDev(response, id, digest, message, stack) { var error = new Error( message || @@ -1275,6 +1278,26 @@ if (__DEV__) { } } + case 82: + /* "R" */ + // Fallthrough + + case 114: + /* "r" */ + // Fallthrough + + case 88: + /* "X" */ + // Fallthrough + + case 120: + /* "x" */ + // Fallthrough + + case 67: + /* "C" */ + // Fallthrough + case 80: /* "P" */ // Fallthrough @@ -1330,9 +1353,12 @@ if (__DEV__) { rowState = ROW_LENGTH; i++; } else if ( - resolvedRowTag > 64 && - resolvedRowTag < 91 + (resolvedRowTag > 64 && resolvedRowTag < 91) || /* "A"-"Z" */ + resolvedRowTag === 114 || + /* "r" */ + resolvedRowTag === 120 + /* "x" */ ) { rowTag = resolvedRowTag; rowState = ROW_CHUNK_BY_NEWLINE; diff --git a/compiled/facebook-www/ReactFlightDOMClient-prod.modern.js b/compiled/facebook-www/ReactFlightDOMClient-prod.modern.js index 3552d1cadfa5f..7ea8b3ac454b9 100644 --- a/compiled/facebook-www/ReactFlightDOMClient-prod.modern.js +++ b/compiled/facebook-www/ReactFlightDOMClient-prod.modern.js @@ -73,7 +73,7 @@ Chunk.prototype.then = function (resolve, reject) { (null === this.reason && (this.reason = []), this.reason.push(reject)); break; default: - reject(this.reason); + reject && reject(this.reason); } }; function readChunk(chunk) { @@ -438,7 +438,9 @@ function startReadingFromStream(response, stream) { rowState = value[i]; 84 === rowState ? ((rowTag = rowState), (rowState = 2), i++) - : 64 < rowState && 91 > rowState + : (64 < rowState && 91 > rowState) || + 114 === rowState || + 120 === rowState ? ((rowTag = rowState), (rowState = 3), i++) : ((rowTag = 0), (rowState = 3)); continue; diff --git a/compiled/facebook-www/ReactFlightDOMServer-dev.modern.js b/compiled/facebook-www/ReactFlightDOMServer-dev.modern.js index 75dc31f15548b..ae2d235337cf1 100644 --- a/compiled/facebook-www/ReactFlightDOMServer-dev.modern.js +++ b/compiled/facebook-www/ReactFlightDOMServer-dev.modern.js @@ -567,8 +567,14 @@ if (__DEV__) { } default: { - if (typeof thenable.status === "string"); - else { + if (typeof thenable.status === "string") { + // Only instrument the thenable if the status if not defined. If + // it's defined, but an unknown value, assume it's been instrumented by + // some custom userspace implementation. We treat it as "pending". + // Attach a dummy listener, to ensure that any lazy initialization can + // happen. Flight lazily parses JSON when the value is actually awaited. + thenable.then(noop, noop); + } else { var pendingThenable = thenable; pendingThenable.status = "pending"; pendingThenable.then( @@ -586,18 +592,18 @@ if (__DEV__) { rejectedThenable.reason = error; } } - ); // Check one more time in case the thenable resolved synchronously + ); + } // Check one more time in case the thenable resolved synchronously - switch (thenable.status) { - case "fulfilled": { - var fulfilledThenable = thenable; - return fulfilledThenable.value; - } + switch (thenable.status) { + case "fulfilled": { + var fulfilledThenable = thenable; + return fulfilledThenable.value; + } - case "rejected": { - var rejectedThenable = thenable; - throw rejectedThenable.reason; - } + case "rejected": { + var rejectedThenable = thenable; + throw rejectedThenable.reason; } } // Suspend. // @@ -1299,6 +1305,7 @@ if (__DEV__) { nextChunkId: 0, pendingChunks: 0, hints: hints, + abortListeners: new Set(), abortableTasks: abortSet, pingedTasks: pingedTasks, completedImportChunks: [], @@ -1415,10 +1422,7 @@ if (__DEV__) { } request.abortableTasks.delete(newTask); - - if (request.destination !== null) { - flushCompletedChunks(request, request.destination); - } + enqueueFlush(request); } ); return newTask.id; @@ -1596,24 +1600,6 @@ if (__DEV__) { } function renderFragment(request, task, children) { - { - var debugInfo = children._debugInfo; - - if (debugInfo) { - // If this came from Flight, forward any debug info into this new row. - if (debugID === null) { - // We don't have a chunk to assign debug info. We need to outline this - // component to assign it an ID. - return outlineTask(request, task); - } else { - // Forward any debug info we have the first time we see it. - // We do this after init so that we have received all the debug info - // from the server by the time we emit it. - forwardDebugInfo(request, debugID, debugInfo); - } - } - } - if (task.keyPath !== null) { // We have a Server Component that specifies a key but we're now splitting // the tree using a fragment. @@ -1648,6 +1634,27 @@ if (__DEV__) { // be recursive serialization, we need to reset the keyPath and implicitSlot, // before recursing here. + { + var debugInfo = children._debugInfo; + + if (debugInfo) { + // If this came from Flight, forward any debug info into this new row. + if (debugID === null) { + // We don't have a chunk to assign debug info. We need to outline this + // component to assign it an ID. + return outlineTask(request, task); + } else { + // Forward any debug info we have the first time we see it. + // We do this after init so that we have received all the debug info + // from the server by the time we emit it. + forwardDebugInfo(request, debugID, debugInfo); + } // Since we're rendering this array again, create a copy that doesn't + // have the debug info so we avoid outlining or emitting debug info again. + + children = Array.from(children); + } + } + return children; } @@ -2049,13 +2056,9 @@ if (__DEV__) { } function serializeLargeTextString(request, text) { - request.pendingChunks += 2; + request.pendingChunks++; var textId = request.nextChunkId++; - var textChunk = stringToChunk(text); - var binaryLength = byteLengthOfChunk(textChunk); - var row = textId.toString(16) + ":T" + binaryLength.toString(16) + ","; - var headerChunk = stringToChunk(row); - request.completedRegularChunks.push(headerChunk, textChunk); + emitTextChunk(request, textId, text); return serializeByValueID(textId); } @@ -2401,7 +2404,7 @@ if (__DEV__) { if (iteratorFn) { return renderFragment(request, task, Array.from(value)); - } // Verify that this is a simple plain object. + } var proto = getPrototypeOf(value); @@ -2693,6 +2696,16 @@ if (__DEV__) { request.completedRegularChunks.push(processedChunk); } + function emitTextChunk(request, id, text) { + request.pendingChunks++; // Extra chunk for the header. + + var textChunk = stringToChunk(text); + var binaryLength = byteLengthOfChunk(textChunk); + var row = id.toString(16) + ":T" + binaryLength.toString(16) + ","; + var headerChunk = stringToChunk(row); + request.completedRegularChunks.push(headerChunk, textChunk); + } + function serializeEval(source) { return "$E" + source; } // This is a forked version of renderModel which should never error, never suspend and is limited @@ -2944,6 +2957,20 @@ if (__DEV__) { } } + function emitChunk(request, task, value) { + var id = task.id; // For certain types we have special types, we typically outlined them but + // we can emit them directly for this row instead of through an indirection. + + if (typeof value === "string") { + emitTextChunk(request, id, value); + return; + } + // $FlowFixMe[incompatible-type] stringify can return null for undefined but we never do + + var json = stringify(value, task.toJSON); + emitModelChunk(request, task.id, json); + } + var emptyRoot = {}; function retryTask(request, task) { @@ -2984,21 +3011,19 @@ if (__DEV__) { task.keyPath = null; task.implicitSlot = false; - var json; if (typeof resolvedModel === "object" && resolvedModel !== null) { // Object might contain unresolved values like additional elements. // This is simulating what the JSON loop would do if this was part of it. - // $FlowFixMe[incompatible-type] stringify can return null for undefined but we never do - json = stringify(resolvedModel, task.toJSON); + emitChunk(request, task, resolvedModel); } else { // If the value is a string, it means it's a terminal value and we already escaped it // We don't need to escape it again so it's not passed the toJSON replacer. // $FlowFixMe[incompatible-type] stringify can return null for undefined but we never do - json = stringify(resolvedModel); + var json = stringify(resolvedModel); + emitModelChunk(request, task.id, json); } - emitModelChunk(request, task.id, json); request.abortableTasks.delete(task); task.status = COMPLETED; } catch (thrownValue) { @@ -3148,6 +3173,7 @@ if (__DEV__) { if (request.pendingChunks === 0) { close(destination); + request.destination = null; } } diff --git a/compiled/facebook-www/ReactFlightDOMServer-prod.modern.js b/compiled/facebook-www/ReactFlightDOMServer-prod.modern.js index f79f19fe88507..4d767bd6b9a31 100644 --- a/compiled/facebook-www/ReactFlightDOMServer-prod.modern.js +++ b/compiled/facebook-www/ReactFlightDOMServer-prod.modern.js @@ -191,9 +191,9 @@ function trackUsedThenable(thenableState, thenable, index) { case "rejected": throw thenable.reason; default: - if ("string" !== typeof thenable.status) - switch ( - ((thenableState = thenable), + "string" === typeof thenable.status + ? thenable.then(noop, noop) + : ((thenableState = thenable), (thenableState.status = "pending"), thenableState.then( function (fulfilledValue) { @@ -210,14 +210,13 @@ function trackUsedThenable(thenableState, thenable, index) { rejectedThenable.reason = error; } } - ), - thenable.status) - ) { - case "fulfilled": - return thenable.value; - case "rejected": - throw thenable.reason; - } + )); + switch (thenable.status) { + case "fulfilled": + return thenable.value; + case "rejected": + throw thenable.reason; + } suspendedThenable = thenable; throw SuspenseException; } @@ -488,8 +487,7 @@ function serializeThenable(request, task, thenable) { reason = logRecoverableError(request, reason); emitErrorChunk(request, newTask.id, reason); request.abortableTasks.delete(newTask); - null !== request.destination && - flushCompletedChunks(request, request.destination); + enqueueFlush(request); } ); return newTask.id; @@ -500,12 +498,7 @@ function emitHint(request, code, model) { code = "H" + code; code = id.toString(16) + ":" + code; request.completedHintChunks.push(code + model + "\n"); - !1 === request.flushScheduled && - 0 === request.pingedTasks.length && - null !== request.destination && - ((model = request.destination), - (request.flushScheduled = !0), - flushCompletedChunks(request, model)); + enqueueFlush(request); } function readThenable(thenable) { if ("fulfilled" === thenable.status) return thenable.value; @@ -887,18 +880,13 @@ function renderModelDestructive( parent[parentPropertyName] instanceof Date ) return "$D" + value; - if (1024 <= value.length) { - request.pendingChunks += 2; - task = request.nextChunkId++; - if (null == byteLengthImpl) - throw Error( - "byteLengthOfChunk implementation is not configured. Please, provide the implementation via ReactFlightDOMServer.setConfig(...);" - ); - parent = byteLengthImpl(value); - parent = task.toString(16) + ":T" + parent.toString(16) + ","; - request.completedRegularChunks.push(parent, value); - return serializeByValueID(task); - } + if (1024 <= value.length) + return ( + request.pendingChunks++, + (task = request.nextChunkId++), + emitTextChunk(request, task, value), + serializeByValueID(task) + ); request = "$" === value[0] ? "$" + value : value; return request; } @@ -981,6 +969,16 @@ function emitErrorChunk(request, id, digest) { id = id.toString(16) + ":E" + stringify(digest) + "\n"; request.completedErrorChunks.push(id); } +function emitTextChunk(request, id, text) { + request.pendingChunks++; + if (null == byteLengthImpl) + throw Error( + "byteLengthOfChunk implementation is not configured. Please, provide the implementation via ReactFlightDOMServer.setConfig(...);" + ); + var JSCompiler_inline_result = byteLengthImpl(text); + id = id.toString(16) + ":T" + JSCompiler_inline_result.toString(16) + ","; + request.completedRegularChunks.push(id, text); +} var emptyRoot = {}; function retryTask(request, task) { if (0 === task.status) @@ -996,12 +994,21 @@ function retryTask(request, task) { modelRoot = resolvedModel; task.keyPath = null; task.implicitSlot = !1; - var json = - "object" === typeof resolvedModel && null !== resolvedModel - ? stringify(resolvedModel, task.toJSON) - : stringify(resolvedModel), - processedChunk = task.id.toString(16) + ":" + json + "\n"; - request.completedRegularChunks.push(processedChunk); + if ("object" === typeof resolvedModel && null !== resolvedModel) { + var id = task.id; + if ("string" === typeof resolvedModel) + emitTextChunk(request, id, resolvedModel); + else { + var json = stringify(resolvedModel, task.toJSON), + processedChunk = task.id.toString(16) + ":" + json + "\n"; + request.completedRegularChunks.push(processedChunk); + } + } else { + var json$jscomp$0 = stringify(resolvedModel), + processedChunk$jscomp$0 = + task.id.toString(16) + ":" + json$jscomp$0 + "\n"; + request.completedRegularChunks.push(processedChunk$jscomp$0); + } request.abortableTasks.delete(task); task.status = 1; } catch (thrownValue) { @@ -1070,7 +1077,19 @@ function flushCompletedChunks(request, destination) { (request.flushScheduled = !1), destination.completeWriting(); } destination.flushBuffered(); - 0 === request.pendingChunks && destination.close(); + 0 === request.pendingChunks && + (destination.close(), (request.destination = null)); +} +function enqueueFlush(request) { + if ( + !1 === request.flushScheduled && + 0 === request.pingedTasks.length && + null !== request.destination + ) { + var destination = request.destination; + request.flushScheduled = !0; + flushCompletedChunks(request, destination); + } } var configured = !1; exports.clearRequestedClientReferencesKeysSet = function () { @@ -1108,6 +1127,7 @@ exports.renderToDestination = function (destination, model, options) { nextChunkId: 0, pendingChunks: 0, hints: hints, + abortListeners: new Set(), abortableTasks: abortSet, pingedTasks: options, completedImportChunks: [],