mirror of
https://github.com/hanzoai/react.git
synced 2026-09-24 17:02:27 +00:00
[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:
committed by
GitHub
parent
8fa88dd75f
commit
6d49bb42e6
+41
@@ -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>;
|
||||
|
||||
+17
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user