Skip to content

Commit

Permalink
[Flight] Allow lazily resolving outlined models (#28780)
Browse files Browse the repository at this point in the history
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 [14f50ad](14f50ad)
  • Loading branch information
sebmarkbage committed Apr 8, 2024
1 parent b1a0af9 commit d1d30ea
Show file tree
Hide file tree
Showing 3 changed files with 184 additions and 145 deletions.
2 changes: 1 addition & 1 deletion compiled/facebook-www/REVISION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
4c12339ce3fa398050d1026c616ea43d43dcaf4a
14f50ad1554f0adf20fa1b5bc62859ed32be0bc6
207 changes: 119 additions & 88 deletions compiled/facebook-www/ReactFlightDOMClient-dev.modern.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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--;
Expand Down Expand Up @@ -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 === "$") {
Expand Down Expand Up @@ -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": {
Expand All @@ -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": {
Expand All @@ -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": {
Expand Down Expand Up @@ -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
);
}
}
}
Expand Down
Loading

0 comments on commit d1d30ea

Please sign in to comment.