mirror of
https://github.com/zebrajr/react.git
synced 2026-01-15 12:15:22 +00:00
[Flight] Support streaming of decodeReply in Edge environments (#31852)
We support streaming `multipart/form-data` in Node.js using Busboy since that's kind of the idiomatic ecosystem way for handling these stream there. There's not really anything idiomatic like that for Edge that's universal yet. This adds a version that's basically just `AsyncIterable.from(formData)`. It could also be a `ReadableStream` of those entries since those are also `AsyncIterable`. I imagine that in the future we might add one from a binary `ReadableStream` that does the parsing built-in.
This commit is contained in:
committed by
GitHub
parent
8f92ea467e
commit
9f540fcc51
@@ -9,6 +9,7 @@ if (process.env.NODE_ENV === 'production') {
|
||||
|
||||
exports.renderToReadableStream = s.renderToReadableStream;
|
||||
exports.decodeReply = s.decodeReply;
|
||||
exports.decodeReplyFromAsyncIterable = s.decodeReplyFromAsyncIterable;
|
||||
exports.decodeAction = s.decodeAction;
|
||||
exports.decodeFormState = s.decodeFormState;
|
||||
exports.createClientReference = s.createClientReference;
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
export {
|
||||
renderToReadableStream,
|
||||
decodeReply,
|
||||
decodeReplyFromAsyncIterable,
|
||||
decodeAction,
|
||||
decodeFormState,
|
||||
createClientReference,
|
||||
|
||||
@@ -17,6 +17,8 @@ import {
|
||||
type ServerReferenceId,
|
||||
} from '../client/ReactFlightClientConfigBundlerParcel';
|
||||
|
||||
import {ASYNC_ITERATOR} from 'shared/ReactSymbols';
|
||||
|
||||
import {
|
||||
createRequest,
|
||||
createPrerenderRequest,
|
||||
@@ -30,6 +32,9 @@ import {
|
||||
createResponse,
|
||||
close,
|
||||
getRoot,
|
||||
reportGlobalError,
|
||||
resolveField,
|
||||
resolveFile,
|
||||
} from 'react-server/src/ReactFlightReplyServer';
|
||||
|
||||
import {
|
||||
@@ -189,6 +194,50 @@ export function decodeReply<T>(
|
||||
return root;
|
||||
}
|
||||
|
||||
export function decodeReplyFromAsyncIterable<T>(
|
||||
iterable: AsyncIterable<[string, string | File]>,
|
||||
options?: {temporaryReferences?: TemporaryReferenceSet},
|
||||
): Thenable<T> {
|
||||
const iterator: AsyncIterator<[string, string | File]> =
|
||||
iterable[ASYNC_ITERATOR]();
|
||||
|
||||
const response = createResponse(
|
||||
serverManifest,
|
||||
'',
|
||||
options ? options.temporaryReferences : undefined,
|
||||
);
|
||||
|
||||
function progress(
|
||||
entry:
|
||||
| {done: false, +value: [string, string | File], ...}
|
||||
| {done: true, +value: void, ...},
|
||||
) {
|
||||
if (entry.done) {
|
||||
close(response);
|
||||
} else {
|
||||
const [name, value] = entry.value;
|
||||
if (typeof value === 'string') {
|
||||
resolveField(response, name, value);
|
||||
} else {
|
||||
resolveFile(response, name, value);
|
||||
}
|
||||
iterator.next().then(progress, error);
|
||||
}
|
||||
}
|
||||
function error(reason: Error) {
|
||||
reportGlobalError(response, reason);
|
||||
if (typeof (iterator: any).throw === 'function') {
|
||||
// The iterator protocol doesn't necessarily include this but a generator do.
|
||||
// $FlowFixMe should be able to pass mixed
|
||||
iterator.throw(reason).then(error, error);
|
||||
}
|
||||
}
|
||||
|
||||
iterator.next().then(progress, error);
|
||||
|
||||
return getRoot(response);
|
||||
}
|
||||
|
||||
export function decodeAction<T>(body: FormData): Promise<() => T> | null {
|
||||
return decodeActionImpl(body, serverManifest);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ export {
|
||||
renderToReadableStream,
|
||||
prerender as unstable_prerender,
|
||||
decodeReply,
|
||||
decodeReplyFromAsyncIterable,
|
||||
decodeAction,
|
||||
decodeFormState,
|
||||
createClientReference,
|
||||
|
||||
@@ -9,6 +9,7 @@ if (process.env.NODE_ENV === 'production') {
|
||||
|
||||
exports.renderToReadableStream = s.renderToReadableStream;
|
||||
exports.decodeReply = s.decodeReply;
|
||||
exports.decodeReplyFromAsyncIterable = s.decodeReplyFromAsyncIterable;
|
||||
exports.decodeAction = s.decodeAction;
|
||||
exports.decodeFormState = s.decodeFormState;
|
||||
exports.registerServerReference = s.registerServerReference;
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
export {
|
||||
renderToReadableStream,
|
||||
decodeReply,
|
||||
decodeReplyFromAsyncIterable,
|
||||
decodeAction,
|
||||
decodeFormState,
|
||||
registerServerReference,
|
||||
|
||||
@@ -12,6 +12,8 @@ import type {Thenable} from 'shared/ReactTypes';
|
||||
import type {ClientManifest} from './ReactFlightServerConfigTurbopackBundler';
|
||||
import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig';
|
||||
|
||||
import {ASYNC_ITERATOR} from 'shared/ReactSymbols';
|
||||
|
||||
import {
|
||||
createRequest,
|
||||
createPrerenderRequest,
|
||||
@@ -25,6 +27,9 @@ import {
|
||||
createResponse,
|
||||
close,
|
||||
getRoot,
|
||||
reportGlobalError,
|
||||
resolveField,
|
||||
resolveFile,
|
||||
} from 'react-server/src/ReactFlightReplyServer';
|
||||
|
||||
import {
|
||||
@@ -183,10 +188,56 @@ function decodeReply<T>(
|
||||
return root;
|
||||
}
|
||||
|
||||
function decodeReplyFromAsyncIterable<T>(
|
||||
iterable: AsyncIterable<[string, string | File]>,
|
||||
turbopackMap: ServerManifest,
|
||||
options?: {temporaryReferences?: TemporaryReferenceSet},
|
||||
): Thenable<T> {
|
||||
const iterator: AsyncIterator<[string, string | File]> =
|
||||
iterable[ASYNC_ITERATOR]();
|
||||
|
||||
const response = createResponse(
|
||||
turbopackMap,
|
||||
'',
|
||||
options ? options.temporaryReferences : undefined,
|
||||
);
|
||||
|
||||
function progress(
|
||||
entry:
|
||||
| {done: false, +value: [string, string | File], ...}
|
||||
| {done: true, +value: void, ...},
|
||||
) {
|
||||
if (entry.done) {
|
||||
close(response);
|
||||
} else {
|
||||
const [name, value] = entry.value;
|
||||
if (typeof value === 'string') {
|
||||
resolveField(response, name, value);
|
||||
} else {
|
||||
resolveFile(response, name, value);
|
||||
}
|
||||
iterator.next().then(progress, error);
|
||||
}
|
||||
}
|
||||
function error(reason: Error) {
|
||||
reportGlobalError(response, reason);
|
||||
if (typeof (iterator: any).throw === 'function') {
|
||||
// The iterator protocol doesn't necessarily include this but a generator do.
|
||||
// $FlowFixMe should be able to pass mixed
|
||||
iterator.throw(reason).then(error, error);
|
||||
}
|
||||
}
|
||||
|
||||
iterator.next().then(progress, error);
|
||||
|
||||
return getRoot(response);
|
||||
}
|
||||
|
||||
export {
|
||||
renderToReadableStream,
|
||||
prerender,
|
||||
decodeReply,
|
||||
decodeReplyFromAsyncIterable,
|
||||
decodeAction,
|
||||
decodeFormState,
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ export {
|
||||
renderToReadableStream,
|
||||
prerender as unstable_prerender,
|
||||
decodeReply,
|
||||
decodeReplyFromAsyncIterable,
|
||||
decodeAction,
|
||||
decodeFormState,
|
||||
registerServerReference,
|
||||
|
||||
@@ -9,6 +9,7 @@ if (process.env.NODE_ENV === 'production') {
|
||||
|
||||
exports.renderToReadableStream = s.renderToReadableStream;
|
||||
exports.decodeReply = s.decodeReply;
|
||||
exports.decodeReplyFromAsyncIterable = s.decodeReplyFromAsyncIterable;
|
||||
exports.decodeAction = s.decodeAction;
|
||||
exports.decodeFormState = s.decodeFormState;
|
||||
exports.registerServerReference = s.registerServerReference;
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
export {
|
||||
renderToReadableStream,
|
||||
decodeReply,
|
||||
decodeReplyFromAsyncIterable,
|
||||
decodeAction,
|
||||
decodeFormState,
|
||||
registerServerReference,
|
||||
|
||||
@@ -272,4 +272,40 @@ describe('ReactFlightDOMReplyEdge', () => {
|
||||
expect(error).not.toBe(null);
|
||||
expect(error.message).toBe('Connection closed.');
|
||||
});
|
||||
|
||||
it('can stream the decoding using an async iterable', async () => {
|
||||
let resolve;
|
||||
const promise = new Promise(r => (resolve = r));
|
||||
|
||||
const buffer = new Uint8Array([
|
||||
123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
|
||||
]);
|
||||
|
||||
const formData = await ReactServerDOMClient.encodeReply({
|
||||
a: Promise.resolve('hello'),
|
||||
b: Promise.resolve(buffer),
|
||||
});
|
||||
|
||||
const iterable = {
|
||||
async *[Symbol.asyncIterator]() {
|
||||
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
|
||||
for (const entry of formData) {
|
||||
yield entry;
|
||||
await promise;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const decoded = await ReactServerDOMServer.decodeReplyFromAsyncIterable(
|
||||
iterable,
|
||||
webpackServerMap,
|
||||
);
|
||||
|
||||
expect(Object.keys(decoded)).toEqual(['a', 'b']);
|
||||
|
||||
await resolve();
|
||||
|
||||
expect(await decoded.a).toBe('hello');
|
||||
expect(Array.from(await decoded.b)).toEqual(Array.from(buffer));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,6 +12,8 @@ import type {Thenable} from 'shared/ReactTypes';
|
||||
import type {ClientManifest} from './ReactFlightServerConfigWebpackBundler';
|
||||
import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig';
|
||||
|
||||
import {ASYNC_ITERATOR} from 'shared/ReactSymbols';
|
||||
|
||||
import {
|
||||
createRequest,
|
||||
createPrerenderRequest,
|
||||
@@ -25,6 +27,9 @@ import {
|
||||
createResponse,
|
||||
close,
|
||||
getRoot,
|
||||
reportGlobalError,
|
||||
resolveField,
|
||||
resolveFile,
|
||||
} from 'react-server/src/ReactFlightReplyServer';
|
||||
|
||||
import {
|
||||
@@ -183,10 +188,56 @@ function decodeReply<T>(
|
||||
return root;
|
||||
}
|
||||
|
||||
function decodeReplyFromAsyncIterable<T>(
|
||||
iterable: AsyncIterable<[string, string | File]>,
|
||||
webpackMap: ServerManifest,
|
||||
options?: {temporaryReferences?: TemporaryReferenceSet},
|
||||
): Thenable<T> {
|
||||
const iterator: AsyncIterator<[string, string | File]> =
|
||||
iterable[ASYNC_ITERATOR]();
|
||||
|
||||
const response = createResponse(
|
||||
webpackMap,
|
||||
'',
|
||||
options ? options.temporaryReferences : undefined,
|
||||
);
|
||||
|
||||
function progress(
|
||||
entry:
|
||||
| {done: false, +value: [string, string | File], ...}
|
||||
| {done: true, +value: void, ...},
|
||||
) {
|
||||
if (entry.done) {
|
||||
close(response);
|
||||
} else {
|
||||
const [name, value] = entry.value;
|
||||
if (typeof value === 'string') {
|
||||
resolveField(response, name, value);
|
||||
} else {
|
||||
resolveFile(response, name, value);
|
||||
}
|
||||
iterator.next().then(progress, error);
|
||||
}
|
||||
}
|
||||
function error(reason: Error) {
|
||||
reportGlobalError(response, reason);
|
||||
if (typeof (iterator: any).throw === 'function') {
|
||||
// The iterator protocol doesn't necessarily include this but a generator do.
|
||||
// $FlowFixMe should be able to pass mixed
|
||||
iterator.throw(reason).then(error, error);
|
||||
}
|
||||
}
|
||||
|
||||
iterator.next().then(progress, error);
|
||||
|
||||
return getRoot(response);
|
||||
}
|
||||
|
||||
export {
|
||||
renderToReadableStream,
|
||||
prerender,
|
||||
decodeReply,
|
||||
decodeReplyFromAsyncIterable,
|
||||
decodeAction,
|
||||
decodeFormState,
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ export {
|
||||
renderToReadableStream,
|
||||
prerender as unstable_prerender,
|
||||
decodeReply,
|
||||
decodeReplyFromAsyncIterable,
|
||||
decodeAction,
|
||||
decodeFormState,
|
||||
registerServerReference,
|
||||
|
||||
Reference in New Issue
Block a user