mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
This adds support in Flight for serializing four kinds of streams: - `ReadableStream` with objects as a model. This is a single shot iterator so you can read it only once. It can contain any value including Server Components. Chunks are encoded as is so if you send in 10 typed arrays, you get the same typed arrays out on the other side. - Binary `ReadableStream` with `type: 'bytes'` option. This supports the BYOB protocol. In this mode, the receiving side just gets `Uint8Array`s and they can be split across any single byte boundary into arbitrary chunks. - `AsyncIterable` where the `AsyncIterator` function is different than the `AsyncIterable` itself. In this case we assume that this might be a multi-shot iterable and so we buffer its value and you can iterate it multiple times on the other side. We support the `return` value as a value in the single completion slot, but you can't pass values in `next()`. If you want single-shot, return the AsyncIterator instead. - `AsyncIterator`. These gets serialized as a single-shot as it's just an iterator. `AsyncIterable`/`AsyncIterator` yield Promises that are instrumented with our `.status`/`.value` convention so that they can be synchronously looped over if available. They are also lazily parsed upon read. We can't do this with `ReadableStream` because we use the native implementation of `ReadableStream` which owns the promises. The format is a leading row that indicates which type of stream it is. Then a new row with the same ID is emitted for every chunk. Followed by either an error or close row. `AsyncIterable`s can also be returned as children of Server Components and then they're conceptually the same as fragment arrays/iterables. They can't actually be used as children in Fizz/Fiber but there's a separate plan for that. Only `AsyncIterable` not `AsyncIterator` will be valid as children - just like sync `Iterable` is already supported but single-shot `Iterator` is not. Notably, neither of these streams represent updates over time to a value. They represent multiple values in a list. When the server stream is aborted we also close the underlying stream. However, closing a stream on the client, doesn't close the underlying stream. A couple of possible follow ups I'm not planning on doing right now: - [ ] Free memory by releasing the buffer if an Iterator has been exhausted. Single shots could be optimized further to release individual items as you go. - [ ] We could clean up the underlying stream if the only pending data that's still flowing is from streams and all the streams have cleaned up. It's not very reliable though. It's better to do cancellation for the whole stream - e.g. at the framework level. - [ ] Implement smarter Binary Stream chunk handling. Currently we wait until we've received a whole row for binary chunks and copy them into consecutive memory. We need this to preserve semantics when passing typed arrays. However, for binary streams we don't need that. We can just send whatever pieces we have so far.
144 lines
4.6 KiB
JavaScript
144 lines
4.6 KiB
JavaScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @emails react-core
|
|
* @jest-environment ./scripts/jest/ReactDOMServerIntegrationEnvironment
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
// Polyfills for test environment
|
|
global.ReadableStream =
|
|
require('web-streams-polyfill/ponyfill/es6').ReadableStream;
|
|
global.TextEncoder = require('util').TextEncoder;
|
|
global.TextDecoder = require('util').TextDecoder;
|
|
|
|
if (typeof Blob === 'undefined') {
|
|
global.Blob = require('buffer').Blob;
|
|
}
|
|
if (typeof File === 'undefined') {
|
|
global.File = require('buffer').File;
|
|
}
|
|
|
|
// let serverExports;
|
|
let webpackServerMap;
|
|
let ReactServerDOMServer;
|
|
let ReactServerDOMClient;
|
|
|
|
describe('ReactFlightDOMReplyEdge', () => {
|
|
beforeEach(() => {
|
|
jest.resetModules();
|
|
// Simulate the condition resolution
|
|
jest.mock('react', () => require('react/react.react-server'));
|
|
jest.mock('react-server-dom-webpack/server', () =>
|
|
require('react-server-dom-webpack/server.edge'),
|
|
);
|
|
const WebpackMock = require('./utils/WebpackMock');
|
|
// serverExports = WebpackMock.serverExports;
|
|
webpackServerMap = WebpackMock.webpackServerMap;
|
|
ReactServerDOMServer = require('react-server-dom-webpack/server.edge');
|
|
jest.resetModules();
|
|
ReactServerDOMClient = require('react-server-dom-webpack/client.edge');
|
|
});
|
|
|
|
if (typeof FormData === 'undefined') {
|
|
// We can't test if we don't have a native FormData implementation because the JSDOM one
|
|
// is missing the arrayBuffer() method.
|
|
it('cannot test', () => {});
|
|
return;
|
|
}
|
|
|
|
it('can encode a reply', async () => {
|
|
const body = await ReactServerDOMClient.encodeReply({some: 'object'});
|
|
const decoded = await ReactServerDOMServer.decodeReply(
|
|
body,
|
|
webpackServerMap,
|
|
);
|
|
|
|
expect(decoded).toEqual({some: 'object'});
|
|
});
|
|
|
|
// @gate enableBinaryFlight
|
|
it('should be able to serialize any kind of typed array', async () => {
|
|
const buffer = new Uint8Array([
|
|
123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
|
|
]).buffer;
|
|
const buffers = [
|
|
buffer,
|
|
new Int8Array(buffer, 1),
|
|
new Uint8Array(buffer, 2),
|
|
new Uint8ClampedArray(buffer, 2),
|
|
new Int16Array(buffer, 2),
|
|
new Uint16Array(buffer, 2),
|
|
new Int32Array(buffer, 4),
|
|
new Uint32Array(buffer, 4),
|
|
new Float32Array(buffer, 4),
|
|
new Float64Array(buffer, 0),
|
|
new BigInt64Array(buffer, 0),
|
|
new BigUint64Array(buffer, 0),
|
|
new DataView(buffer, 3),
|
|
];
|
|
|
|
const body = await ReactServerDOMClient.encodeReply(buffers);
|
|
const result = await ReactServerDOMServer.decodeReply(
|
|
body,
|
|
webpackServerMap,
|
|
);
|
|
|
|
expect(result).toEqual(buffers);
|
|
});
|
|
|
|
// @gate enableBinaryFlight
|
|
it('should be able to serialize a blob', async () => {
|
|
const bytes = new Uint8Array([
|
|
123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
|
|
]);
|
|
const blob = new Blob([bytes, bytes], {
|
|
type: 'application/x-test',
|
|
});
|
|
const body = await ReactServerDOMClient.encodeReply(blob);
|
|
const result = await ReactServerDOMServer.decodeReply(
|
|
body,
|
|
webpackServerMap,
|
|
);
|
|
expect(result instanceof Blob).toBe(true);
|
|
expect(result.size).toBe(bytes.length * 2);
|
|
expect(await result.arrayBuffer()).toEqual(await blob.arrayBuffer());
|
|
});
|
|
|
|
if (typeof FormData !== 'undefined' && typeof File !== 'undefined') {
|
|
it('can transport FormData (blobs)', async () => {
|
|
const bytes = new Uint8Array([
|
|
123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
|
|
]);
|
|
const blob = new Blob([bytes, bytes], {
|
|
type: 'application/x-test',
|
|
});
|
|
|
|
const formData = new FormData();
|
|
formData.append('hi', 'world');
|
|
formData.append('file', blob, 'filename.test');
|
|
|
|
expect(formData.get('file') instanceof File).toBe(true);
|
|
expect(formData.get('file').name).toBe('filename.test');
|
|
|
|
const body = await ReactServerDOMClient.encodeReply(formData);
|
|
const result = await ReactServerDOMServer.decodeReply(
|
|
body,
|
|
webpackServerMap,
|
|
);
|
|
|
|
expect(result instanceof FormData).toBe(true);
|
|
expect(result.get('hi')).toBe('world');
|
|
const resultBlob = result.get('file');
|
|
expect(resultBlob instanceof Blob).toBe(true);
|
|
expect(resultBlob.name).toBe('filename.test'); // In this direction we allow file name to pass through but not other direction.
|
|
expect(resultBlob.size).toBe(bytes.length * 2);
|
|
expect(await resultBlob.arrayBuffer()).toEqual(await blob.arrayBuffer());
|
|
});
|
|
}
|
|
});
|