'use strict';
import {patchMessageChannel} from '../../../../scripts/jest/patchMessageChannel';
global.ReadableStream =
require('web-streams-polyfill/ponyfill/es6').ReadableStream;
global.TextEncoder = require('util').TextEncoder;
global.TextDecoder = require('util').TextDecoder;
let serverExports;
let serverObjectExports;
let turbopackMap;
let turbopackServerMap;
let ReactServerDOMServer;
let ReactServerDOMClient;
let ReactServerScheduler;
let serverAct;
describe('ReactFlightTurbopackDOMObjectReferences', () => {
beforeEach(() => {
jest.resetModules();
ReactServerScheduler = require('scheduler');
patchMessageChannel(ReactServerScheduler);
serverAct = require('internal-test-utils').serverAct;
jest.mock('react', () => require('react/react.react-server'));
jest.mock('react-server-dom-turbopack/server', () =>
require('react-server-dom-turbopack/server.browser'),
);
const TurbopackMock = require('./utils/TurbopackMock');
serverExports = TurbopackMock.serverExports;
serverObjectExports = TurbopackMock.serverObjectExports;
turbopackMap = TurbopackMock.turbopackMap;
turbopackServerMap = TurbopackMock.turbopackServerMap;
ReactServerDOMServer = require('react-server-dom-turbopack/server.browser');
__unmockReact();
jest.resetModules();
ReactServerDOMClient = require('react-server-dom-turbopack/client.browser');
});
it('passes an object reference to the client and back to the server where it resolves', async () => {
const settings = {theme: 'dark'};
const ServerModule = serverObjectExports({settings});
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
{ref: ServerModule.settings},
turbopackMap,
),
);
const result = await ReactServerDOMClient.createFromReadableStream(stream);
const token = result.ref;
expect(typeof token).toBe('object');
expect(() => token.theme).toThrow(
'Cannot access theme on the client. ' +
'You cannot read a Server Reference to an object on the client. ' +
'You can only pass it back to the server.',
);
expect(() => token()).toThrow(TypeError);
const React = require('react');
const ReactDOMServer = require('react-dom/server');
function Client() {
const [value] = React.useState(token);
expect(value).toBe(token);
return null;
}
ReactDOMServer.renderToString(React.createElement(Client));
const body = await ReactServerDOMClient.encodeReply({ref: token});
const decoded = await ReactServerDOMServer.decodeReply(
body,
turbopackServerMap,
);
expect(decoded.ref).toBe(settings);
});
it('encodes an object reference at the root, including with temporary references', async () => {
const settings = {theme: 'dark'};
const ServerModule = serverObjectExports({settings});
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
{ref: ServerModule.settings},
turbopackMap,
),
);
const {ref} = await ReactServerDOMClient.createFromReadableStream(stream);
const temporaryReferenceSets = [
undefined,
ReactServerDOMClient.createTemporaryReferenceSet(),
];
for (let i = 0; i < temporaryReferenceSets.length; i++) {
const temporaryReferences = temporaryReferenceSets[i];
const body = await ReactServerDOMClient.encodeReply(ref, {
temporaryReferences,
});
const decoded = await ReactServerDOMServer.decodeReply(
body,
turbopackServerMap,
);
expect(decoded).toBe(settings);
const nestedBody = await ReactServerDOMClient.encodeReply(
{first: ref, second: ref, promised: Promise.resolve(ref)},
{temporaryReferences},
);
const nested = await ReactServerDOMServer.decodeReply(
nestedBody,
turbopackServerMap,
);
expect(nested.first).toBe(settings);
expect(nested.second).toBe(settings);
expect(await nested.promised).toBe(settings);
}
});
it('does not await an object reference that is a Promise', async () => {
const promise = Promise.resolve('server secret');
const ServerModule = serverObjectExports({promise});
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
{ref: ServerModule.promise},
turbopackMap,
),
);
const result = await ReactServerDOMClient.createFromReadableStream(stream);
const token = result.ref;
expect(typeof token).toBe('object');
expect(await token).toBe(token);
expect(() => token.value).toThrow('Cannot access value on the client.');
const body = await ReactServerDOMClient.encodeReply({ref: token});
const decoded = await ReactServerDOMServer.decodeReply(
body,
turbopackServerMap,
);
expect(decoded.ref).toBe(promise);
expect(await decoded.ref).toBe('server secret');
});
it('resolves to the current value of the module export, not a snapshot', async () => {
let current = {version: 1};
const ServerModule = serverObjectExports({
get value() {
return current;
},
});
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
{ref: ServerModule.value},
turbopackMap,
),
);
const result = await ReactServerDOMClient.createFromReadableStream(stream);
const token = result.ref;
expect(typeof token).toBe('object');
const body1 = await ReactServerDOMClient.encodeReply({ref: token});
const decoded1 = await ReactServerDOMServer.decodeReply(
body1,
turbopackServerMap,
);
expect(decoded1.ref).toBe(current);
expect(decoded1.ref.version).toBe(1);
current = {version: 2};
const body2 = await ReactServerDOMClient.encodeReply({ref: token});
const decoded2 = await ReactServerDOMServer.decodeReply(
body2,
turbopackServerMap,
);
expect(decoded2.ref).toBe(current);
expect(decoded2.ref.version).toBe(2);
});
it('is not turned into a temporary reference when a TemporaryReferenceSet is passed', async () => {
const settings = {theme: 'dark'};
const ServerModule = serverObjectExports({settings});
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
{ref: ServerModule.settings},
turbopackMap,
),
);
const result = await ReactServerDOMClient.createFromReadableStream(stream);
const token = result.ref;
expect(typeof token).toBe('object');
const temporaryReferences =
ReactServerDOMClient.createTemporaryReferenceSet();
const body = await ReactServerDOMClient.encodeReply(
{ref: token},
{temporaryReferences},
);
const decoded = await ReactServerDOMServer.decodeReply(
body,
turbopackServerMap,
);
expect(decoded.ref).toBe(settings);
});
it('keeps Dates and objects with toJSON opaque', async () => {
const date = new Date('2026-09-14T00:00:00.000Z');
const custom = {
get toJSON() {
throw new Error('Must not read toJSON on a registered object.');
},
};
const ServerModule = serverObjectExports({date, custom});
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(ServerModule, turbopackMap),
);
const result = await ReactServerDOMClient.createFromReadableStream(stream);
expect(typeof result.date).toBe('object');
expect(() => result.date.toISOString()).toThrow(
'Cannot access toISOString on the client.',
);
expect(typeof result.custom).toBe('object');
const body = await ReactServerDOMClient.encodeReply(result);
const decoded = await ReactServerDOMServer.decodeReply(
body,
turbopackServerMap,
);
expect(decoded.date).toBe(date);
expect(decoded.custom).toBe(custom);
});
it('preserves identity across repeated references and streamed values', async () => {
const settings = {theme: 'dark'};
serverObjectExports({settings});
let resolveLater;
const later = new Promise(resolve => {
resolveLater = resolve;
});
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
{
first: settings,
second: settings,
map: new Map([[settings, 'value']]),
later,
},
turbopackMap,
),
);
const result = await ReactServerDOMClient.createFromReadableStream(stream);
expect(result.first).toBe(result.second);
expect(result.map.get(result.first)).toBe('value');
await serverAct(() => resolveLater(settings));
expect(await result.later).toBe(result.first);
expect(() => result.first.theme).toThrow(
'Cannot access theme on the client.',
);
});
it('round-trips an object reference under then after its module loads', async () => {
let resolveModule;
const blockOnChunk = new Promise(resolve => {
resolveModule = resolve;
});
const settings = {theme: 'dark'};
const ServerModule = serverObjectExports({settings}, blockOnChunk);
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
{ref: ServerModule.settings},
turbopackMap,
),
);
const {ref} = await ReactServerDOMClient.createFromReadableStream(stream);
const body = await ReactServerDOMClient.encodeReply({
first: ref,
then: ref,
});
const pending = ReactServerDOMServer.decodeReply(body, turbopackServerMap);
await resolveModule();
const decoded = await pending;
expect(decoded.first).toBe(settings);
expect(decoded.then).toBe(settings);
const decodedAgain = await ReactServerDOMServer.decodeReply(
body,
turbopackServerMap,
);
expect(decodedAgain.first).toBe(settings);
expect(decodedAgain.then).toBe(settings);
});
async function encodePropertyRead(ref, property) {
const container = {ref};
const body = await ReactServerDOMClient.encodeReply({
map: new Map([['container', container]]),
selected: container,
});
const model = JSON.parse(body.get('0'));
model.selected += ':ref:' + property;
body.set('0', JSON.stringify(model));
return body;
}
describe.each(['object', 'array'])('reply paths into a server %s', shape => {
describe.each(['loaded', 'delayed'])('with a %s module', loading => {
async function checkPropertyRead(registerExports, initialValue) {
let resolveModule;
const blockOnChunk =
loading === 'delayed'
? new Promise(resolve => {
resolveModule = resolve;
})
: undefined;
let current = initialValue;
const ServerModule = registerExports(
{
get value() {
return current;
},
},
blockOnChunk,
);
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
{ref: ServerModule.value},
turbopackMap,
),
);
const {ref} =
await ReactServerDOMClient.createFromReadableStream(stream);
const readProperty = jest.fn(() => 'server value');
const property = shape === 'array' ? '0' : 'value';
current = Object.defineProperty(shape === 'array' ? [] : {}, property, {
get: readProperty,
});
const body = await encodePropertyRead(ref, property);
const pending = ReactServerDOMServer.decodeReply(
body,
turbopackServerMap,
);
if (resolveModule) {
await resolveModule();
}
await expect(Promise.resolve(pending)).rejects.toThrow(
'Invalid reference.',
);
expect(readProperty).not.toHaveBeenCalled();
const container = {ref};
const validBody = await ReactServerDOMClient.encodeReply({
container,
alias: container,
});
const decoded = await ReactServerDOMServer.decodeReply(
validBody,
turbopackServerMap,
);
expect(decoded.container).toBe(decoded.alias);
expect(decoded.container.ref).toBe(current);
}
it('rejects property access through an object reference', async () => {
await checkPropertyRead(serverObjectExports, {});
});
it('rejects property access through a function reference whose export is now an object', async () => {
await checkPropertyRead(serverExports, function action() {});
});
});
});
it('still resolves aliases through client objects and arrays', async () => {
const child = {value: 'client value'};
const container = [child];
const body = await ReactServerDOMClient.encodeReply({
map: new Map([['container', container]]),
container,
child,
});
const decoded = await ReactServerDOMServer.decodeReply(
body,
turbopackServerMap,
);
expect(decoded.container).toBe(decoded.map.get('container'));
expect(decoded.child).toBe(decoded.container[0]);
expect(decoded.child.value).toBe('client value');
});
it('rejects a reply that decodes a server object as a Map', async () => {
const leaked = [['leaked', 'secret']];
const ServerModule = serverObjectExports({value: leaked});
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
{ref: ServerModule.value},
turbopackMap,
),
);
const {ref} = await ReactServerDOMClient.createFromReadableStream(stream);
const body = await ReactServerDOMClient.encodeReply({ref});
const referenceMarker = JSON.parse(body.get('0')).ref;
body.set('0', JSON.stringify('$Q7'));
body.set('7', JSON.stringify(referenceMarker));
const pending = ReactServerDOMServer.decodeReply(body, turbopackServerMap);
await expect(Promise.resolve(pending)).rejects.toThrow(
'Invalid Map initializer.',
);
expect(leaked.$$consumed).toBe(undefined);
});
it('keeps registered binary values opaque when yielded by an async iterable', async () => {
async function* items(value) {
yield value;
yield value;
}
const values = [new Uint8Array([1, 2, 3]), new ArrayBuffer(3)];
for (let i = 0; i < values.length; i++) {
const value = values[i];
serverObjectExports({value});
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
{ref: value, items: items(value)},
turbopackMap,
),
);
const result =
await ReactServerDOMClient.createFromReadableStream(stream);
const iterator = result.items[Symbol.asyncIterator]();
const first = await iterator.next();
const second = await iterator.next();
expect(first.value === result.ref).toBe(true);
expect(second.value === result.ref).toBe(true);
expect((await iterator.next()).done).toBe(true);
expect(() => first.value.byteLength).toThrow(
'Cannot access byteLength on the client.',
);
const body = await ReactServerDOMClient.encodeReply({ref: first.value});
const decoded = await ReactServerDOMServer.decodeReply(
body,
turbopackServerMap,
);
expect(decoded.ref).toBe(value);
}
});
describe.each(['loaded', 'delayed'])(
'invalid object references with a %s module',
loading => {
it('rejects a function encoded as an object reference', async () => {
let resolveModule;
const blockOnChunk =
loading === 'delayed'
? new Promise(resolve => {
resolveModule = resolve;
})
: undefined;
const fn = jest.fn();
const ServerModule = serverExports({fn}, blockOnChunk);
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
{fn: ServerModule.fn},
turbopackMap,
),
);
const result =
await ReactServerDOMClient.createFromReadableStream(stream);
const body = await ReactServerDOMClient.encodeReply({
fn: result.fn,
ref: result.fn,
});
const model = JSON.parse(body.get('0'));
model.then = model.ref.replace('$h', '$H');
delete model.ref;
body.set('0', JSON.stringify(model));
const pending = ReactServerDOMServer.decodeReply(
body,
turbopackServerMap,
);
if (resolveModule) {
await resolveModule();
}
await expect(Promise.resolve(pending)).rejects.toThrow(
'Expected a Server Reference to an object to resolve to an object.',
);
expect(fn).not.toHaveBeenCalled();
});
},
);
it('does not bind arguments supplied with object reference metadata', async () => {
const bind = jest.fn();
const settings = {bind};
const ServerModule = serverObjectExports({settings});
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
{ref: ServerModule.settings},
turbopackMap,
),
);
const {ref} = await ReactServerDOMClient.createFromReadableStream(stream);
const body = await ReactServerDOMClient.encodeReply({
ref,
args: Promise.resolve([]),
});
const model = JSON.parse(body.get('0'));
const metadataKey = model.ref.slice(2);
const metadata = JSON.parse(body.get(metadataKey));
metadata.bound = model.args;
body.set(metadataKey, JSON.stringify(metadata));
const decoded = await ReactServerDOMServer.decodeReply(
body,
turbopackServerMap,
);
expect(decoded.ref).toBe(settings);
expect(bind).not.toHaveBeenCalled();
});
it('rejects an object reference missing from the receiving server manifest', async () => {
const settings = {theme: 'dark'};
const ServerModule = serverObjectExports({settings});
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
{ref: ServerModule.settings},
turbopackMap,
),
);
const {ref} = await ReactServerDOMClient.createFromReadableStream(stream);
const body = await ReactServerDOMClient.encodeReply({ref});
await expect(
Promise.resolve(ReactServerDOMServer.decodeReply(body, {})),
).rejects.toThrow('Could not find the module');
});
it('serializes a registered Promise as a thenable when the flag is off', async () => {
const promise = Promise.resolve('resolved');
const ServerModule = serverObjectExports({promise});
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
{promise: ServerModule.promise},
turbopackMap,
),
);
const result = await ReactServerDOMClient.createFromReadableStream(stream);
expect(await result.promise).toBe('resolved');
});
it('round-trips objects and bound functions together', async () => {
const settings = {theme: 'dark'};
function describe(theme, name) {
return theme + ': ' + name;
}
const ServerModule = serverObjectExports({settings, describe});
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
{
settings: ServerModule.settings,
again: ServerModule.settings,
describe: ServerModule.describe,
bound: ServerModule.describe.bind(null, 'dark'),
},
turbopackMap,
),
);
const result = await ReactServerDOMClient.createFromReadableStream(stream);
expect(typeof result.settings).toBe('object');
expect(() => result.settings.theme).toThrow(
'Cannot access theme on the client.',
);
expect(result.again).toBe(result.settings);
expect(typeof result.describe).toBe('function');
expect(typeof result.bound).toBe('function');
const body = await ReactServerDOMClient.encodeReply(result);
const decoded = await ReactServerDOMServer.decodeReply(
body,
turbopackServerMap,
);
expect(decoded.settings).toBe(settings);
expect(decoded.again).toBe(settings);
expect(decoded.describe).toBe(describe);
expect(decoded.bound('Alice')).toBe('dark: Alice');
});
it('still round-trips function server references', async () => {
function greet(name) {
return 'hi, ' + name;
}
const ServerModule = serverExports({greet});
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
{method: ServerModule.greet},
turbopackMap,
),
);
const result = await ReactServerDOMClient.createFromReadableStream(stream);
expect(typeof result.method).toBe('function');
const body = await ReactServerDOMClient.encodeReply({
method: result.method,
});
const decoded = await ReactServerDOMServer.decodeReply(
body,
turbopackServerMap,
);
expect(decoded.method).toBe(greet);
expect(decoded.method('there')).toBe('hi, there');
});
});