Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 1 commit into from
May 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -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.
Expand Down
123 changes: 82 additions & 41 deletions packages/react-server/src/ReactFlightReplyServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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(
Expand All @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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;
}
Expand Down