[Flight Reply] Resolve outlined models async in Reply just like in Flight Client (#28988)

This is the same change as #28780 but for the Flight Reply receiver.

While it's not possible to create an "async module" reference in this
case - resolving a server reference can still be async if loading it
requires loading chunks like in a new server instance.

Since extracting a typed array from a Blob is async, that's also a case
where a dependency can be async.
This commit is contained in:
Sebastian Markbåge
2024-05-07 22:19:59 -04:00
committed by GitHub
parent 8fa88dd75f
commit 6d49bb42e6
4 changed files with 154 additions and 43 deletions
@@ -1130,6 +1130,47 @@ describe('ReactFlightDOMBrowser', () => {
expect(result).toBe('Hello world');
});
it('can pass an async server exports that resolves later to an outline object like a Map', async () => {
let resolve;
const chunkPromise = new Promise(r => (resolve = r));
function action() {}
const serverModule = serverExports(
{
action: action,
},
chunkPromise,
);
// Send the action to the client
const stream = ReactServerDOMServer.renderToReadableStream(
{action: serverModule.action},
webpackMap,
);
const response =
await ReactServerDOMClient.createFromReadableStream(stream);
// Pass the action back to the server inside a Map
const map = new Map();
map.set('action', response.action);
const body = await ReactServerDOMClient.encodeReply(map);
const resultPromise = ReactServerDOMServer.decodeReply(
body,
webpackServerMap,
);
// We couldn't yet resolve the server reference because we haven't loaded
// its chunk yet in the new server instance. We now resolve it which loads
// it asynchronously.
await resolve();
const result = await resultPromise;
expect(result instanceof Map).toBe(true);
expect(result.get('action')).toBe(action);
});
it('supports Float hints before the first await in server components in Fiber', async () => {
function Component() {
return <p>hello world</p>;
@@ -85,6 +85,23 @@ describe('ReactFlightDOMReplyEdge', () => {
expect(new Uint8Array(result[0])).toEqual(new Uint8Array(buffers[0]));
});
// @gate enableBinaryFlight
it('should be able to serialize a typed array inside a Map', async () => {
const array = new Uint8Array([
123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
]);
const map = new Map();
map.set('array', array);
const body = await ReactServerDOMClient.encodeReply(map);
const result = await ReactServerDOMServer.decodeReply(
body,
webpackServerMap,
);
expect(result.get('array')).toEqual(array);
});
// @gate enableBinaryFlight
it('should be able to serialize a blob', async () => {
const bytes = new Uint8Array([
@@ -11,11 +11,16 @@ const url = require('url');
const Module = require('module');
let webpackModuleIdx = 0;
let webpackChunkIdx = 0;
const webpackServerModules = {};
const webpackClientModules = {};
const webpackErroredModules = {};
const webpackServerMap = {};
const webpackClientMap = {};
const webpackChunkMap = {};
global.__webpack_chunk_load__ = function (id) {
return webpackChunkMap[id];
};
global.__webpack_require__ = function (id) {
if (webpackErroredModules[id]) {
throw webpackErroredModules[id];
@@ -117,13 +122,20 @@ exports.clientExports = function clientExports(
};
// This tests server to server references. There's another case of client to server references.
exports.serverExports = function serverExports(moduleExports) {
exports.serverExports = function serverExports(moduleExports, blockOnChunk) {
const idx = '' + webpackModuleIdx++;
webpackServerModules[idx] = moduleExports;
const path = url.pathToFileURL(idx).href;
const chunks = [];
if (blockOnChunk) {
const chunkId = webpackChunkIdx++;
webpackChunkMap[chunkId] = blockOnChunk;
chunks.push(chunkId);
}
webpackServerMap[path] = {
id: idx,
chunks: [],
chunks: chunks,
name: '*',
};
// We only add this if this test is testing ESM compat.
+82 -41
View File
@@ -327,7 +327,14 @@ function loadServerReference<T>(
}
}
promise.then(
createModelResolver(parentChunk, parentObject, key),
createModelResolver(
parentChunk,
parentObject,
key,
false,
response,
createModel,
),
createModelReject(parentChunk),
);
// We need a placeholder value that will be replaced later.
@@ -406,19 +413,24 @@ function createModelResolver<T>(
chunk: SomeChunk<T>,
parentObject: Object,
key: string,
cyclic: boolean,
response: Response,
map: (response: Response, model: any) => T,
): (value: any) => void {
let blocked;
if (initializingChunkBlockedModel) {
blocked = initializingChunkBlockedModel;
blocked.deps++;
if (!cyclic) {
blocked.deps++;
}
} else {
blocked = initializingChunkBlockedModel = {
deps: 1,
deps: cyclic ? 0 : 1,
value: (null: any),
};
}
return value => {
parentObject[key] = 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.
@@ -446,16 +458,61 @@ function createModelReject<T>(chunk: SomeChunk<T>): (error: mixed) => void {
return (error: mixed) => triggerErrorOnChunk(chunk, error);
}
function getOutlinedModel(response: Response, id: number): any {
function getOutlinedModel<T>(
response: Response,
id: number,
parentObject: Object,
key: string,
map: (response: Response, model: any) => T,
): T {
const chunk = getChunk(response, id);
if (chunk.status === RESOLVED_MODEL) {
initializeModelChunk(chunk);
switch (chunk.status) {
case RESOLVED_MODEL:
initializeModelChunk(chunk);
break;
}
if (chunk.status !== INITIALIZED) {
// We know that this is emitted earlier so otherwise it's an error.
throw chunk.reason;
// The status might have changed after initialization.
switch (chunk.status) {
case INITIALIZED:
return map(response, chunk.value);
case PENDING:
case BLOCKED:
const parentChunk = initializingChunk;
chunk.then(
createModelResolver(
parentChunk,
parentObject,
key,
false,
response,
map,
),
createModelReject(parentChunk),
);
return (null: any);
default:
throw chunk.reason;
}
return chunk.value;
}
function createMap(
response: Response,
model: Array<[any, any]>,
): Map<any, any> {
return new Map(model);
}
function createSet(response: Response, model: Array<any>): Set<any> {
return new Set(model);
}
function extractIterator(response: Response, model: Array<any>): Iterator<any> {
// $FlowFixMe[incompatible-use]: This uses raw Symbols because we're extracting from a native array.
return model[Symbol.iterator]();
}
function createModel(response: Response, model: any): any {
return model;
}
function parseTypedArray(
@@ -481,10 +538,17 @@ function parseTypedArray(
});
// Since loading the buffer is an async operation we'll be blocking the parent
// chunk. TODO: This is not safe if the parent chunk needs a mapper like Map.
// chunk.
const parentChunk = initializingChunk;
promise.then(
createModelResolver(parentChunk, parentObject, parentKey),
createModelResolver(
parentChunk,
parentObject,
parentKey,
false,
response,
createModel,
),
createModelReject(parentChunk),
);
return null;
@@ -728,7 +792,7 @@ function parseModelString(
const id = parseInt(value.slice(2), 16);
// TODO: Just encode this in the reference inline instead of as a model.
const metaData: {id: ServerReferenceId, bound: Thenable<Array<any>>} =
getOutlinedModel(response, id);
getOutlinedModel(response, id, obj, key, createModel);
return loadServerReference(
response,
metaData.id,
@@ -745,14 +809,12 @@ function parseModelString(
case 'Q': {
// Map
const id = parseInt(value.slice(2), 16);
const data = getOutlinedModel(response, id);
return new Map(data);
return getOutlinedModel(response, id, obj, key, createMap);
}
case 'W': {
// Set
const id = parseInt(value.slice(2), 16);
const data = getOutlinedModel(response, id);
return new Set(data);
return getOutlinedModel(response, id, obj, key, createSet);
}
case 'K': {
// FormData
@@ -774,8 +836,7 @@ function parseModelString(
case 'i': {
// Iterator
const id = parseInt(value.slice(2), 16);
const data = getOutlinedModel(response, id);
return data[Symbol.iterator]();
return getOutlinedModel(response, id, obj, key, extractIterator);
}
case 'I': {
// $Infinity
@@ -873,27 +934,7 @@ function parseModelString(
// We assume that anything else is a reference ID.
const id = parseInt(value.slice(1), 16);
const chunk = getChunk(response, id);
switch (chunk.status) {
case RESOLVED_MODEL:
initializeModelChunk(chunk);
break;
}
// The status might have changed after initialization.
switch (chunk.status) {
case INITIALIZED:
return chunk.value;
case PENDING:
case BLOCKED:
const parentChunk = initializingChunk;
chunk.then(
createModelResolver(parentChunk, obj, key),
createModelReject(parentChunk),
);
return null;
default:
throw chunk.reason;
}
return getOutlinedModel(response, id, obj, key, createModel);
}
return value;
}