From d1d30ea7f14dd07c0b8cf1801b8e12de609d324d Mon Sep 17 00:00:00 2001 From: sebmarkbage Date: Mon, 8 Apr 2024 19:44:09 +0000 Subject: [PATCH] [Flight] Allow lazily resolving outlined models (#28780) We used to assume that outlined models are emitted before the reference (which was true before Blobs). However, it still wasn't safe to assume that all the data will be available because an "import" (client reference) can be async and therefore if it's directly a child of an outlined model, it won't be able to update in place. This is a similar problem as the one hit by @unstubbable in #28669 with elements, but a little different since these don't follow the same way of wrapping. I don't love the structuring of this code which now needs to pass a first class mapper instead of just being known code. It also shares the host path which is just an identity function. It wouldn't necessarily pass my own review but I don't have a better one for now. I'd really prefer if this was done at a "row" level but that ends up creating even more code. Add test for Blob in FormData and async modules in Maps. DiffTrain build for [14f50ad1554f0adf20fa1b5bc62859ed32be0bc6](https://github.com/facebook/react/commit/14f50ad1554f0adf20fa1b5bc62859ed32be0bc6) --- compiled/facebook-www/REVISION | 2 +- .../ReactFlightDOMClient-dev.modern.js | 207 ++++++++++-------- .../ReactFlightDOMClient-prod.modern.js | 120 +++++----- 3 files changed, 184 insertions(+), 145 deletions(-) diff --git a/compiled/facebook-www/REVISION b/compiled/facebook-www/REVISION index 156f8812b1efc..ef423112c8cf9 100644 --- a/compiled/facebook-www/REVISION +++ b/compiled/facebook-www/REVISION @@ -1 +1 @@ -4c12339ce3fa398050d1026c616ea43d43dcaf4a +14f50ad1554f0adf20fa1b5bc62859ed32be0bc6 diff --git a/compiled/facebook-www/ReactFlightDOMClient-dev.modern.js b/compiled/facebook-www/ReactFlightDOMClient-dev.modern.js index ff5ddc5206efc..b4215b607a278 100644 --- a/compiled/facebook-www/ReactFlightDOMClient-dev.modern.js +++ b/compiled/facebook-www/ReactFlightDOMClient-dev.modern.js @@ -657,7 +657,14 @@ if (__DEV__) { return chunk; } - function createModelResolver(chunk, parentObject, key, cyclic) { + function createModelResolver( + chunk, + parentObject, + key, + cyclic, + response, + map + ) { var blocked; if (initializingChunkBlockedModel) { @@ -674,11 +681,11 @@ if (__DEV__) { } return function (value) { - parentObject[key] = value; // If this is the root object for a model reference, where `blocked.value` + parentObject[key] = map(response, value); // If this is the root object for a model reference, where `blocked.value` // is a stale `null`, the resolved value can be used directly. if (key === "" && blocked.value === null) { - blocked.value = value; + blocked.value = parentObject[key]; } blocked.deps--; @@ -733,26 +740,95 @@ if (__DEV__) { return proxy; } - function getOutlinedModel(response, id) { + function getOutlinedModel(response, id, parentObject, key, map) { var chunk = getChunk(response, id); switch (chunk.status) { case RESOLVED_MODEL: initializeModelChunk(chunk); break; + + case RESOLVED_MODULE: + initializeModuleChunk(chunk); + break; } // The status might have changed after initialization. switch (chunk.status) { - case INITIALIZED: { - return chunk.value; - } - // We always encode it first in the stream so it won't be pending. + case INITIALIZED: + var chunkValue = map(response, chunk.value); + + if (chunk._debugInfo) { + // If we have a direct reference to an object that was rendered by a synchronous + // server component, it might have some debug info about how it was rendered. + // We forward this to the underlying object. This might be a React Element or + // an Array fragment. + // If this was a string / number return value we lose the debug info. We choose + // that tradeoff to allow sync server components to return plain values and not + // use them as React Nodes necessarily. We could otherwise wrap them in a Lazy. + if ( + typeof chunkValue === "object" && + chunkValue !== null && + (Array.isArray(chunkValue) || + chunkValue.$$typeof === REACT_ELEMENT_TYPE) && + !chunkValue._debugInfo + ) { + // We should maybe use a unique symbol for arrays but this is a React owned array. + // $FlowFixMe[prop-missing]: This should be added to elements. + Object.defineProperty(chunkValue, "_debugInfo", { + configurable: false, + enumerable: false, + writable: true, + value: chunk._debugInfo + }); + } + } + + return chunkValue; + + case PENDING: + case BLOCKED: + case CYCLIC: + var parentChunk = initializingChunk; + chunk.then( + createModelResolver( + parentChunk, + parentObject, + key, + chunk.status === CYCLIC, + response, + map + ), + createModelReject(parentChunk) + ); + return null; default: throw chunk.reason; } } + function createMap(response, model) { + return new Map(model); + } + + function createSet(response, model) { + return new Set(model); + } + + function createFormData(response, model) { + var formData = new FormData(); + + for (var i = 0; i < model.length; i++) { + formData.append(model[i][0], model[i][1]); + } + + return formData; + } + + function createModel(response, model) { + return model; + } + function parseModelString(response, parentObject, key, value) { if (value[0] === "$") { if (value === "$") { @@ -798,8 +874,13 @@ if (__DEV__) { // Server Reference var _id2 = parseInt(value.slice(2), 16); - var metadata = getOutlinedModel(response, _id2); - return createServerReferenceProxy(response, metadata); + return getOutlinedModel( + response, + _id2, + parentObject, + key, + createServerReferenceProxy + ); } case "T": { @@ -822,17 +903,26 @@ if (__DEV__) { // Map var _id4 = parseInt(value.slice(2), 16); - var data = getOutlinedModel(response, _id4); - return new Map(data); + return getOutlinedModel( + response, + _id4, + parentObject, + key, + createMap + ); } case "W": { // Set var _id5 = parseInt(value.slice(2), 16); - var _data = getOutlinedModel(response, _id5); - - return new Set(_data); + return getOutlinedModel( + response, + _id5, + parentObject, + key, + createSet + ); } case "B": { @@ -843,15 +933,13 @@ if (__DEV__) { // FormData var _id7 = parseInt(value.slice(2), 16); - var _data3 = getOutlinedModel(response, _id7); - - var formData = new FormData(); - - for (var i = 0; i < _data3.length; i++) { - formData.append(_data3[i][0], _data3[i][1]); - } - - return formData; + return getOutlinedModel( + response, + _id7, + parentObject, + key, + createFormData + ); } case "I": { @@ -908,70 +996,13 @@ if (__DEV__) { // We assume that anything else is a reference ID. var _id8 = parseInt(value.slice(1), 16); - var _chunk2 = getChunk(response, _id8); - - switch (_chunk2.status) { - case RESOLVED_MODEL: - initializeModelChunk(_chunk2); - break; - - case RESOLVED_MODULE: - initializeModuleChunk(_chunk2); - break; - } // The status might have changed after initialization. - - switch (_chunk2.status) { - case INITIALIZED: - var chunkValue = _chunk2.value; - - if (_chunk2._debugInfo) { - // If we have a direct reference to an object that was rendered by a synchronous - // server component, it might have some debug info about how it was rendered. - // We forward this to the underlying object. This might be a React Element or - // an Array fragment. - // If this was a string / number return value we lose the debug info. We choose - // that tradeoff to allow sync server components to return plain values and not - // use them as React Nodes necessarily. We could otherwise wrap them in a Lazy. - if ( - typeof chunkValue === "object" && - chunkValue !== null && - (Array.isArray(chunkValue) || - chunkValue.$$typeof === REACT_ELEMENT_TYPE) && - !chunkValue._debugInfo - ) { - // We should maybe use a unique symbol for arrays but this is a React owned array. - // $FlowFixMe[prop-missing]: This should be added to elements. - Object.defineProperty(chunkValue, "_debugInfo", { - configurable: false, - enumerable: false, - writable: true, - value: _chunk2._debugInfo - }); - } - } - - return chunkValue; - - case PENDING: - case BLOCKED: - case CYCLIC: - var parentChunk = initializingChunk; - - _chunk2.then( - createModelResolver( - parentChunk, - parentObject, - key, - _chunk2.status === CYCLIC - ), - createModelReject(parentChunk) - ); - - return null; - - default: - throw _chunk2.reason; - } + return getOutlinedModel( + response, + _id8, + parentObject, + key, + createModel + ); } } } diff --git a/compiled/facebook-www/ReactFlightDOMClient-prod.modern.js b/compiled/facebook-www/ReactFlightDOMClient-prod.modern.js index 2869831bc1599..81903bd2a5ed7 100644 --- a/compiled/facebook-www/ReactFlightDOMClient-prod.modern.js +++ b/compiled/facebook-www/ReactFlightDOMClient-prod.modern.js @@ -191,7 +191,7 @@ function getChunk(response, id) { chunks.set(id, chunk)); return chunk; } -function createModelResolver(chunk, parentObject, key, cyclic) { +function createModelResolver(chunk, parentObject, key, cyclic, response, map) { if (initializingChunkBlockedModel) { var blocked = initializingChunkBlockedModel; cyclic || blocked.deps++; @@ -201,8 +201,8 @@ function createModelResolver(chunk, parentObject, key, cyclic) { value: null }; return function (value) { - parentObject[key] = value; - "" === key && null === blocked.value && (blocked.value = value); + parentObject[key] = map(response, value); + "" === key && null === blocked.value && (blocked.value = parentObject[key]); blocked.deps--; 0 === blocked.deps && "blocked" === chunk.status && @@ -233,19 +233,53 @@ function createServerReferenceProxy(response, metaData) { knownServerReferences.set(proxy, metaData); return proxy; } -function getOutlinedModel(response, id) { - response = getChunk(response, id); - switch (response.status) { +function getOutlinedModel(response, id, parentObject, key, map) { + id = getChunk(response, id); + switch (id.status) { case "resolved_model": - initializeModelChunk(response); + initializeModelChunk(id); + break; + case "resolved_module": + initializeModuleChunk(id); } - switch (response.status) { + switch (id.status) { case "fulfilled": - return response.value; + return map(response, id.value); + case "pending": + case "blocked": + case "cyclic": + var parentChunk = initializingChunk; + id.then( + createModelResolver( + parentChunk, + parentObject, + key, + "cyclic" === id.status, + response, + map + ), + createModelReject(parentChunk) + ); + return null; default: - throw response.reason; + throw id.reason; } } +function createMap(response, model) { + return new Map(model); +} +function createSet(response, model) { + return new Set(model); +} +function createFormData(response, model) { + response = new FormData(); + for (var i = 0; i < model.length; i++) + response.append(model[i][0], model[i][1]); + return response; +} +function createModel(response, model) { + return model; +} function parseModelString(response, parentObject, key, value) { if ("$" === value[0]) { if ("$" === value) return REACT_ELEMENT_TYPE; @@ -266,9 +300,14 @@ function parseModelString(response, parentObject, key, value) { return Symbol.for(value.slice(2)); case "F": return ( - (parentObject = parseInt(value.slice(2), 16)), - (parentObject = getOutlinedModel(response, parentObject)), - createServerReferenceProxy(response, parentObject) + (value = parseInt(value.slice(2), 16)), + getOutlinedModel( + response, + value, + parentObject, + key, + createServerReferenceProxy + ) ); case "T": parentObject = parseInt(value.slice(2), 16); @@ -284,25 +323,21 @@ function parseModelString(response, parentObject, key, value) { return response[parentObject]; case "Q": return ( - (parentObject = parseInt(value.slice(2), 16)), - (response = getOutlinedModel(response, parentObject)), - new Map(response) + (value = parseInt(value.slice(2), 16)), + getOutlinedModel(response, value, parentObject, key, createMap) ); case "W": return ( - (parentObject = parseInt(value.slice(2), 16)), - (response = getOutlinedModel(response, parentObject)), - new Set(response) + (value = parseInt(value.slice(2), 16)), + getOutlinedModel(response, value, parentObject, key, createSet) ); case "B": return; case "K": - parentObject = parseInt(value.slice(2), 16); - response = getOutlinedModel(response, parentObject); - parentObject = new FormData(); - for (key = 0; key < response.length; key++) - parentObject.append(response[key][0], response[key][1]); - return parentObject; + return ( + (value = parseInt(value.slice(2), 16)), + getOutlinedModel(response, value, parentObject, key, createFormData) + ); case "I": return Infinity; case "-": @@ -316,37 +351,10 @@ function parseModelString(response, parentObject, key, value) { case "n": return BigInt(value.slice(2)); default: - value = parseInt(value.slice(1), 16); - response = getChunk(response, value); - switch (response.status) { - case "resolved_model": - initializeModelChunk(response); - break; - case "resolved_module": - initializeModuleChunk(response); - } - switch (response.status) { - case "fulfilled": - return response.value; - case "pending": - case "blocked": - case "cyclic": - return ( - (value = initializingChunk), - response.then( - createModelResolver( - value, - parentObject, - key, - "cyclic" === response.status - ), - createModelReject(value) - ), - null - ); - default: - throw response.reason; - } + return ( + (value = parseInt(value.slice(1), 16)), + getOutlinedModel(response, value, parentObject, key, createModel) + ); } } return value;