mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
5309f10285
The bindings upstream in Relay has been removed so we don't need these builds anymore. The idea is to revisit an FB integration of Flight but it wouldn't use the Relay specific bindings. It's a bit unclear how it would look but likely more like the OSS version so not worth keeping these around. The `dom-relay` name also included the FB specific Fizz implementation of the streaming config so I renamed that to `dom-fb`. There's no Fizz implementation for Native yet so I just removed `native-relay`. We created a configurable fork for how to encode the output of Flight and the Relay implementation encoded it as JSON objects instead of strings/streams. The new implementation would likely be more stream-like and just encode it directly as string/binary chunks. So I removed those indirections so that this can just be declared inline in ReactFlightServer/Client.
919 lines
26 KiB
JavaScript
919 lines
26 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.
|
|
*
|
|
* @flow
|
|
*/
|
|
|
|
import type {Thenable} from 'shared/ReactTypes';
|
|
import type {LazyComponent} from 'react/src/ReactLazy';
|
|
|
|
import type {
|
|
ClientReference,
|
|
ClientReferenceMetadata,
|
|
SSRManifest,
|
|
StringDecoder,
|
|
} from './ReactFlightClientConfig';
|
|
|
|
import type {HintModel} from 'react-server/src/ReactFlightServerConfig';
|
|
|
|
import type {CallServerCallback} from './ReactFlightReplyClient';
|
|
|
|
import {
|
|
resolveClientReference,
|
|
preloadModule,
|
|
requireModule,
|
|
dispatchHint,
|
|
readPartialStringChunk,
|
|
readFinalStringChunk,
|
|
supportsBinaryStreams,
|
|
createStringDecoder,
|
|
} from './ReactFlightClientConfig';
|
|
|
|
import {
|
|
encodeFormAction,
|
|
knownServerReferences,
|
|
} from './ReactFlightReplyClient';
|
|
|
|
import {REACT_LAZY_TYPE, REACT_ELEMENT_TYPE} from 'shared/ReactSymbols';
|
|
|
|
import {getOrCreateServerContext} from 'shared/ReactServerContextRegistry';
|
|
|
|
export type {CallServerCallback};
|
|
|
|
type UninitializedModel = string;
|
|
|
|
export type JSONValue =
|
|
| number
|
|
| null
|
|
| boolean
|
|
| string
|
|
| {+[key: string]: JSONValue}
|
|
| $ReadOnlyArray<JSONValue>;
|
|
|
|
const PENDING = 'pending';
|
|
const BLOCKED = 'blocked';
|
|
const RESOLVED_MODEL = 'resolved_model';
|
|
const RESOLVED_MODULE = 'resolved_module';
|
|
const INITIALIZED = 'fulfilled';
|
|
const ERRORED = 'rejected';
|
|
|
|
type PendingChunk<T> = {
|
|
status: 'pending',
|
|
value: null | Array<(T) => mixed>,
|
|
reason: null | Array<(mixed) => mixed>,
|
|
_response: Response,
|
|
then(resolve: (T) => mixed, reject: (mixed) => mixed): void,
|
|
};
|
|
type BlockedChunk<T> = {
|
|
status: 'blocked',
|
|
value: null | Array<(T) => mixed>,
|
|
reason: null | Array<(mixed) => mixed>,
|
|
_response: Response,
|
|
then(resolve: (T) => mixed, reject: (mixed) => mixed): void,
|
|
};
|
|
type ResolvedModelChunk<T> = {
|
|
status: 'resolved_model',
|
|
value: UninitializedModel,
|
|
reason: null,
|
|
_response: Response,
|
|
then(resolve: (T) => mixed, reject: (mixed) => mixed): void,
|
|
};
|
|
type ResolvedModuleChunk<T> = {
|
|
status: 'resolved_module',
|
|
value: ClientReference<T>,
|
|
reason: null,
|
|
_response: Response,
|
|
then(resolve: (T) => mixed, reject: (mixed) => mixed): void,
|
|
};
|
|
type InitializedChunk<T> = {
|
|
status: 'fulfilled',
|
|
value: T,
|
|
reason: null,
|
|
_response: Response,
|
|
then(resolve: (T) => mixed, reject: (mixed) => mixed): void,
|
|
};
|
|
type ErroredChunk<T> = {
|
|
status: 'rejected',
|
|
value: null,
|
|
reason: mixed,
|
|
_response: Response,
|
|
then(resolve: (T) => mixed, reject: (mixed) => mixed): void,
|
|
};
|
|
type SomeChunk<T> =
|
|
| PendingChunk<T>
|
|
| BlockedChunk<T>
|
|
| ResolvedModelChunk<T>
|
|
| ResolvedModuleChunk<T>
|
|
| InitializedChunk<T>
|
|
| ErroredChunk<T>;
|
|
|
|
// $FlowFixMe[missing-this-annot]
|
|
function Chunk(status: any, value: any, reason: any, response: Response) {
|
|
this.status = status;
|
|
this.value = value;
|
|
this.reason = reason;
|
|
this._response = response;
|
|
}
|
|
// We subclass Promise.prototype so that we get other methods like .catch
|
|
Chunk.prototype = (Object.create(Promise.prototype): any);
|
|
// TODO: This doesn't return a new Promise chain unlike the real .then
|
|
Chunk.prototype.then = function <T>(
|
|
this: SomeChunk<T>,
|
|
resolve: (value: T) => mixed,
|
|
reject: (reason: mixed) => mixed,
|
|
) {
|
|
const chunk: SomeChunk<T> = this;
|
|
// If we have resolved content, we try to initialize it first which
|
|
// might put us back into one of the other states.
|
|
switch (chunk.status) {
|
|
case RESOLVED_MODEL:
|
|
initializeModelChunk(chunk);
|
|
break;
|
|
case RESOLVED_MODULE:
|
|
initializeModuleChunk(chunk);
|
|
break;
|
|
}
|
|
// The status might have changed after initialization.
|
|
switch (chunk.status) {
|
|
case INITIALIZED:
|
|
resolve(chunk.value);
|
|
break;
|
|
case PENDING:
|
|
case BLOCKED:
|
|
if (resolve) {
|
|
if (chunk.value === null) {
|
|
chunk.value = ([]: Array<(T) => mixed>);
|
|
}
|
|
chunk.value.push(resolve);
|
|
}
|
|
if (reject) {
|
|
if (chunk.reason === null) {
|
|
chunk.reason = ([]: Array<(mixed) => mixed>);
|
|
}
|
|
chunk.reason.push(reject);
|
|
}
|
|
break;
|
|
default:
|
|
reject(chunk.reason);
|
|
break;
|
|
}
|
|
};
|
|
|
|
export type Response = {
|
|
_bundlerConfig: SSRManifest,
|
|
_callServer: CallServerCallback,
|
|
_chunks: Map<number, SomeChunk<any>>,
|
|
_partialRow: string,
|
|
_fromJSON: (key: string, value: JSONValue) => any,
|
|
_stringDecoder: StringDecoder,
|
|
};
|
|
|
|
function readChunk<T>(chunk: SomeChunk<T>): T {
|
|
// If we have resolved content, we try to initialize it first which
|
|
// might put us back into one of the other states.
|
|
switch (chunk.status) {
|
|
case RESOLVED_MODEL:
|
|
initializeModelChunk(chunk);
|
|
break;
|
|
case RESOLVED_MODULE:
|
|
initializeModuleChunk(chunk);
|
|
break;
|
|
}
|
|
// The status might have changed after initialization.
|
|
switch (chunk.status) {
|
|
case INITIALIZED:
|
|
return chunk.value;
|
|
case PENDING:
|
|
case BLOCKED:
|
|
// eslint-disable-next-line no-throw-literal
|
|
throw ((chunk: any): Thenable<T>);
|
|
default:
|
|
throw chunk.reason;
|
|
}
|
|
}
|
|
|
|
export function getRoot<T>(response: Response): Thenable<T> {
|
|
const chunk = getChunk(response, 0);
|
|
return (chunk: any);
|
|
}
|
|
|
|
function createPendingChunk<T>(response: Response): PendingChunk<T> {
|
|
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
|
|
return new Chunk(PENDING, null, null, response);
|
|
}
|
|
|
|
function createBlockedChunk<T>(response: Response): BlockedChunk<T> {
|
|
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
|
|
return new Chunk(BLOCKED, null, null, response);
|
|
}
|
|
|
|
function createErrorChunk<T>(
|
|
response: Response,
|
|
error: ErrorWithDigest,
|
|
): ErroredChunk<T> {
|
|
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
|
|
return new Chunk(ERRORED, null, error, response);
|
|
}
|
|
|
|
function wakeChunk<T>(listeners: Array<(T) => mixed>, value: T): void {
|
|
for (let i = 0; i < listeners.length; i++) {
|
|
const listener = listeners[i];
|
|
listener(value);
|
|
}
|
|
}
|
|
|
|
function wakeChunkIfInitialized<T>(
|
|
chunk: SomeChunk<T>,
|
|
resolveListeners: Array<(T) => mixed>,
|
|
rejectListeners: null | Array<(mixed) => mixed>,
|
|
): void {
|
|
switch (chunk.status) {
|
|
case INITIALIZED:
|
|
wakeChunk(resolveListeners, chunk.value);
|
|
break;
|
|
case PENDING:
|
|
case BLOCKED:
|
|
chunk.value = resolveListeners;
|
|
chunk.reason = rejectListeners;
|
|
break;
|
|
case ERRORED:
|
|
if (rejectListeners) {
|
|
wakeChunk(rejectListeners, chunk.reason);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
function triggerErrorOnChunk<T>(chunk: SomeChunk<T>, error: mixed): void {
|
|
if (chunk.status !== PENDING && chunk.status !== BLOCKED) {
|
|
// We already resolved. We didn't expect to see this.
|
|
return;
|
|
}
|
|
const listeners = chunk.reason;
|
|
const erroredChunk: ErroredChunk<T> = (chunk: any);
|
|
erroredChunk.status = ERRORED;
|
|
erroredChunk.reason = error;
|
|
if (listeners !== null) {
|
|
wakeChunk(listeners, error);
|
|
}
|
|
}
|
|
|
|
function createResolvedModelChunk<T>(
|
|
response: Response,
|
|
value: UninitializedModel,
|
|
): ResolvedModelChunk<T> {
|
|
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
|
|
return new Chunk(RESOLVED_MODEL, value, null, response);
|
|
}
|
|
|
|
function createResolvedModuleChunk<T>(
|
|
response: Response,
|
|
value: ClientReference<T>,
|
|
): ResolvedModuleChunk<T> {
|
|
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
|
|
return new Chunk(RESOLVED_MODULE, value, null, response);
|
|
}
|
|
|
|
function resolveModelChunk<T>(
|
|
chunk: SomeChunk<T>,
|
|
value: UninitializedModel,
|
|
): void {
|
|
if (chunk.status !== PENDING) {
|
|
// We already resolved. We didn't expect to see this.
|
|
return;
|
|
}
|
|
const resolveListeners = chunk.value;
|
|
const rejectListeners = chunk.reason;
|
|
const resolvedChunk: ResolvedModelChunk<T> = (chunk: any);
|
|
resolvedChunk.status = RESOLVED_MODEL;
|
|
resolvedChunk.value = value;
|
|
if (resolveListeners !== null) {
|
|
// This is unfortunate that we're reading this eagerly if
|
|
// we already have listeners attached since they might no
|
|
// longer be rendered or might not be the highest pri.
|
|
initializeModelChunk(resolvedChunk);
|
|
// The status might have changed after initialization.
|
|
wakeChunkIfInitialized(chunk, resolveListeners, rejectListeners);
|
|
}
|
|
}
|
|
|
|
function resolveModuleChunk<T>(
|
|
chunk: SomeChunk<T>,
|
|
value: ClientReference<T>,
|
|
): void {
|
|
if (chunk.status !== PENDING && chunk.status !== BLOCKED) {
|
|
// We already resolved. We didn't expect to see this.
|
|
return;
|
|
}
|
|
const resolveListeners = chunk.value;
|
|
const rejectListeners = chunk.reason;
|
|
const resolvedChunk: ResolvedModuleChunk<T> = (chunk: any);
|
|
resolvedChunk.status = RESOLVED_MODULE;
|
|
resolvedChunk.value = value;
|
|
if (resolveListeners !== null) {
|
|
initializeModuleChunk(resolvedChunk);
|
|
wakeChunkIfInitialized(chunk, resolveListeners, rejectListeners);
|
|
}
|
|
}
|
|
|
|
let initializingChunk: ResolvedModelChunk<any> = (null: any);
|
|
let initializingChunkBlockedModel: null | {deps: number, value: any} = null;
|
|
function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
|
|
const prevChunk = initializingChunk;
|
|
const prevBlocked = initializingChunkBlockedModel;
|
|
initializingChunk = chunk;
|
|
initializingChunkBlockedModel = null;
|
|
try {
|
|
const value: T = parseModel(chunk._response, chunk.value);
|
|
if (
|
|
initializingChunkBlockedModel !== null &&
|
|
initializingChunkBlockedModel.deps > 0
|
|
) {
|
|
initializingChunkBlockedModel.value = value;
|
|
// We discovered new dependencies on modules that are not yet resolved.
|
|
// We have to go the BLOCKED state until they're resolved.
|
|
const blockedChunk: BlockedChunk<T> = (chunk: any);
|
|
blockedChunk.status = BLOCKED;
|
|
blockedChunk.value = null;
|
|
blockedChunk.reason = null;
|
|
} else {
|
|
const initializedChunk: InitializedChunk<T> = (chunk: any);
|
|
initializedChunk.status = INITIALIZED;
|
|
initializedChunk.value = value;
|
|
}
|
|
} catch (error) {
|
|
const erroredChunk: ErroredChunk<T> = (chunk: any);
|
|
erroredChunk.status = ERRORED;
|
|
erroredChunk.reason = error;
|
|
} finally {
|
|
initializingChunk = prevChunk;
|
|
initializingChunkBlockedModel = prevBlocked;
|
|
}
|
|
}
|
|
|
|
function initializeModuleChunk<T>(chunk: ResolvedModuleChunk<T>): void {
|
|
try {
|
|
const value: T = requireModule(chunk.value);
|
|
const initializedChunk: InitializedChunk<T> = (chunk: any);
|
|
initializedChunk.status = INITIALIZED;
|
|
initializedChunk.value = value;
|
|
} catch (error) {
|
|
const erroredChunk: ErroredChunk<T> = (chunk: any);
|
|
erroredChunk.status = ERRORED;
|
|
erroredChunk.reason = error;
|
|
}
|
|
}
|
|
|
|
// Report that any missing chunks in the model is now going to throw this
|
|
// error upon read. Also notify any pending promises.
|
|
export function reportGlobalError(response: Response, error: Error): void {
|
|
response._chunks.forEach(chunk => {
|
|
// If this chunk was already resolved or errored, it won't
|
|
// trigger an error but if it wasn't then we need to
|
|
// because we won't be getting any new data to resolve it.
|
|
if (chunk.status === PENDING) {
|
|
triggerErrorOnChunk(chunk, error);
|
|
}
|
|
});
|
|
}
|
|
|
|
function createElement(
|
|
type: mixed,
|
|
key: mixed,
|
|
props: mixed,
|
|
): React$Element<any> {
|
|
const element: any = {
|
|
// This tag allows us to uniquely identify this as a React Element
|
|
$$typeof: REACT_ELEMENT_TYPE,
|
|
|
|
// Built-in properties that belong on the element
|
|
type: type,
|
|
key: key,
|
|
ref: null,
|
|
props: props,
|
|
|
|
// Record the component responsible for creating this element.
|
|
_owner: null,
|
|
};
|
|
if (__DEV__) {
|
|
// We don't really need to add any of these but keeping them for good measure.
|
|
// Unfortunately, _store is enumerable in jest matchers so for equality to
|
|
// work, I need to keep it or make _store non-enumerable in the other file.
|
|
element._store = ({}: {
|
|
validated?: boolean,
|
|
});
|
|
Object.defineProperty(element._store, 'validated', {
|
|
configurable: false,
|
|
enumerable: false,
|
|
writable: true,
|
|
value: true, // This element has already been validated on the server.
|
|
});
|
|
Object.defineProperty(element, '_self', {
|
|
configurable: false,
|
|
enumerable: false,
|
|
writable: false,
|
|
value: null,
|
|
});
|
|
Object.defineProperty(element, '_source', {
|
|
configurable: false,
|
|
enumerable: false,
|
|
writable: false,
|
|
value: null,
|
|
});
|
|
}
|
|
return element;
|
|
}
|
|
|
|
function createLazyChunkWrapper<T>(
|
|
chunk: SomeChunk<T>,
|
|
): LazyComponent<T, SomeChunk<T>> {
|
|
const lazyType: LazyComponent<T, SomeChunk<T>> = {
|
|
$$typeof: REACT_LAZY_TYPE,
|
|
_payload: chunk,
|
|
_init: readChunk,
|
|
};
|
|
return lazyType;
|
|
}
|
|
|
|
function getChunk(response: Response, id: number): SomeChunk<any> {
|
|
const chunks = response._chunks;
|
|
let chunk = chunks.get(id);
|
|
if (!chunk) {
|
|
chunk = createPendingChunk(response);
|
|
chunks.set(id, chunk);
|
|
}
|
|
return chunk;
|
|
}
|
|
|
|
function createModelResolver<T>(
|
|
chunk: SomeChunk<T>,
|
|
parentObject: Object,
|
|
key: string,
|
|
): (value: any) => void {
|
|
let blocked;
|
|
if (initializingChunkBlockedModel) {
|
|
blocked = initializingChunkBlockedModel;
|
|
blocked.deps++;
|
|
} else {
|
|
blocked = initializingChunkBlockedModel = {
|
|
deps: 1,
|
|
value: null,
|
|
};
|
|
}
|
|
return value => {
|
|
parentObject[key] = value;
|
|
blocked.deps--;
|
|
if (blocked.deps === 0) {
|
|
if (chunk.status !== BLOCKED) {
|
|
return;
|
|
}
|
|
const resolveListeners = chunk.value;
|
|
const initializedChunk: InitializedChunk<T> = (chunk: any);
|
|
initializedChunk.status = INITIALIZED;
|
|
initializedChunk.value = blocked.value;
|
|
if (resolveListeners !== null) {
|
|
wakeChunk(resolveListeners, blocked.value);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
function createModelReject<T>(chunk: SomeChunk<T>): (error: mixed) => void {
|
|
return (error: mixed) => triggerErrorOnChunk(chunk, error);
|
|
}
|
|
|
|
function createServerReferenceProxy<A: Iterable<any>, T>(
|
|
response: Response,
|
|
metaData: {id: any, bound: null | Thenable<Array<any>>},
|
|
): (...A) => Promise<T> {
|
|
const callServer = response._callServer;
|
|
const proxy = function (): Promise<T> {
|
|
// $FlowFixMe[method-unbinding]
|
|
const args = Array.prototype.slice.call(arguments);
|
|
const p = metaData.bound;
|
|
if (!p) {
|
|
return callServer(metaData.id, args);
|
|
}
|
|
if (p.status === INITIALIZED) {
|
|
const bound = p.value;
|
|
return callServer(metaData.id, bound.concat(args));
|
|
}
|
|
// Since this is a fake Promise whose .then doesn't chain, we have to wrap it.
|
|
// TODO: Remove the wrapper once that's fixed.
|
|
return ((Promise.resolve(p): any): Promise<Array<any>>).then(function (
|
|
bound,
|
|
) {
|
|
return callServer(metaData.id, bound.concat(args));
|
|
});
|
|
};
|
|
// Expose encoder for use by SSR.
|
|
// TODO: Only expose this in SSR builds and not the browser client.
|
|
proxy.$$FORM_ACTION = encodeFormAction;
|
|
knownServerReferences.set(proxy, metaData);
|
|
return proxy;
|
|
}
|
|
|
|
function parseModelString(
|
|
response: Response,
|
|
parentObject: Object,
|
|
key: string,
|
|
value: string,
|
|
): any {
|
|
if (value[0] === '$') {
|
|
if (value === '$') {
|
|
// A very common symbol.
|
|
return REACT_ELEMENT_TYPE;
|
|
}
|
|
switch (value[1]) {
|
|
case '$': {
|
|
// This was an escaped string value.
|
|
return value.slice(1);
|
|
}
|
|
case 'L': {
|
|
// Lazy node
|
|
const id = parseInt(value.slice(2), 16);
|
|
const chunk = getChunk(response, id);
|
|
// We create a React.lazy wrapper around any lazy values.
|
|
// When passed into React, we'll know how to suspend on this.
|
|
return createLazyChunkWrapper(chunk);
|
|
}
|
|
case '@': {
|
|
// Promise
|
|
const id = parseInt(value.slice(2), 16);
|
|
const chunk = getChunk(response, id);
|
|
return chunk;
|
|
}
|
|
case 'S': {
|
|
// Symbol
|
|
return Symbol.for(value.slice(2));
|
|
}
|
|
case 'P': {
|
|
// Server Context Provider
|
|
return getOrCreateServerContext(value.slice(2)).Provider;
|
|
}
|
|
case 'F': {
|
|
// Server Reference
|
|
const id = parseInt(value.slice(2), 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: {
|
|
const metadata = chunk.value;
|
|
return createServerReferenceProxy(response, metadata);
|
|
}
|
|
// We always encode it first in the stream so it won't be pending.
|
|
default:
|
|
throw chunk.reason;
|
|
}
|
|
}
|
|
case 'I': {
|
|
// $Infinity
|
|
return Infinity;
|
|
}
|
|
case '-': {
|
|
// $-0 or $-Infinity
|
|
if (value === '$-0') {
|
|
return -0;
|
|
} else {
|
|
return -Infinity;
|
|
}
|
|
}
|
|
case 'N': {
|
|
// $NaN
|
|
return NaN;
|
|
}
|
|
case 'u': {
|
|
// matches "$undefined"
|
|
// Special encoding for `undefined` which can't be serialized as JSON otherwise.
|
|
return undefined;
|
|
}
|
|
case 'D': {
|
|
// Date
|
|
return new Date(Date.parse(value.slice(2)));
|
|
}
|
|
case 'n': {
|
|
// BigInt
|
|
return BigInt(value.slice(2));
|
|
}
|
|
default: {
|
|
// 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;
|
|
case RESOLVED_MODULE:
|
|
initializeModuleChunk(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, parentObject, key),
|
|
createModelReject(parentChunk),
|
|
);
|
|
return null;
|
|
default:
|
|
throw chunk.reason;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function parseModelTuple(
|
|
response: Response,
|
|
value: {+[key: string]: JSONValue} | $ReadOnlyArray<JSONValue>,
|
|
): any {
|
|
const tuple: [mixed, mixed, mixed, mixed] = (value: any);
|
|
|
|
if (tuple[0] === REACT_ELEMENT_TYPE) {
|
|
// TODO: Consider having React just directly accept these arrays as elements.
|
|
// Or even change the ReactElement type to be an array.
|
|
return createElement(tuple[1], tuple[2], tuple[3]);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function missingCall() {
|
|
throw new Error(
|
|
'Trying to call a function from "use server" but the callServer option ' +
|
|
'was not implemented in your router runtime.',
|
|
);
|
|
}
|
|
|
|
export function createResponse(
|
|
bundlerConfig: SSRManifest,
|
|
callServer: void | CallServerCallback,
|
|
): Response {
|
|
const chunks: Map<number, SomeChunk<any>> = new Map();
|
|
const response: Response = {
|
|
_bundlerConfig: bundlerConfig,
|
|
_callServer: callServer !== undefined ? callServer : missingCall,
|
|
_chunks: chunks,
|
|
_partialRow: '',
|
|
_stringDecoder: (null: any),
|
|
_fromJSON: (null: any),
|
|
};
|
|
if (supportsBinaryStreams) {
|
|
response._stringDecoder = createStringDecoder();
|
|
}
|
|
// Don't inline this call because it causes closure to outline the call above.
|
|
response._fromJSON = createFromJSONCallback(response);
|
|
return response;
|
|
}
|
|
|
|
function resolveModel(
|
|
response: Response,
|
|
id: number,
|
|
model: UninitializedModel,
|
|
): void {
|
|
const chunks = response._chunks;
|
|
const chunk = chunks.get(id);
|
|
if (!chunk) {
|
|
chunks.set(id, createResolvedModelChunk(response, model));
|
|
} else {
|
|
resolveModelChunk(chunk, model);
|
|
}
|
|
}
|
|
|
|
function resolveModule(
|
|
response: Response,
|
|
id: number,
|
|
model: UninitializedModel,
|
|
): void {
|
|
const chunks = response._chunks;
|
|
const chunk = chunks.get(id);
|
|
const clientReferenceMetadata: ClientReferenceMetadata = parseModel(
|
|
response,
|
|
model,
|
|
);
|
|
const clientReference = resolveClientReference<$FlowFixMe>(
|
|
response._bundlerConfig,
|
|
clientReferenceMetadata,
|
|
);
|
|
|
|
// TODO: Add an option to encode modules that are lazy loaded.
|
|
// For now we preload all modules as early as possible since it's likely
|
|
// that we'll need them.
|
|
const promise = preloadModule(clientReference);
|
|
if (promise) {
|
|
let blockedChunk: BlockedChunk<any>;
|
|
if (!chunk) {
|
|
// Technically, we should just treat promise as the chunk in this
|
|
// case. Because it'll just behave as any other promise.
|
|
blockedChunk = createBlockedChunk(response);
|
|
chunks.set(id, blockedChunk);
|
|
} else {
|
|
// This can't actually happen because we don't have any forward
|
|
// references to modules.
|
|
blockedChunk = (chunk: any);
|
|
blockedChunk.status = BLOCKED;
|
|
}
|
|
promise.then(
|
|
() => resolveModuleChunk(blockedChunk, clientReference),
|
|
error => triggerErrorOnChunk(blockedChunk, error),
|
|
);
|
|
} else {
|
|
if (!chunk) {
|
|
chunks.set(id, createResolvedModuleChunk(response, clientReference));
|
|
} else {
|
|
// This can't actually happen because we don't have any forward
|
|
// references to modules.
|
|
resolveModuleChunk(chunk, clientReference);
|
|
}
|
|
}
|
|
}
|
|
|
|
type ErrorWithDigest = Error & {digest?: string};
|
|
function resolveErrorProd(
|
|
response: Response,
|
|
id: number,
|
|
digest: string,
|
|
): void {
|
|
if (__DEV__) {
|
|
// These errors should never make it into a build so we don't need to encode them in codes.json
|
|
// eslint-disable-next-line react-internal/prod-error-codes
|
|
throw new Error(
|
|
'resolveErrorProd should never be called in development mode. Use resolveErrorDev instead. This is a bug in React.',
|
|
);
|
|
}
|
|
const error = new Error(
|
|
'An error occurred in the Server Components render. The specific message is omitted in production' +
|
|
' builds to avoid leaking sensitive details. A digest property is included on this error instance which' +
|
|
' may provide additional details about the nature of the error.',
|
|
);
|
|
error.stack = 'Error: ' + error.message;
|
|
(error: any).digest = digest;
|
|
const errorWithDigest: ErrorWithDigest = (error: any);
|
|
const chunks = response._chunks;
|
|
const chunk = chunks.get(id);
|
|
if (!chunk) {
|
|
chunks.set(id, createErrorChunk(response, errorWithDigest));
|
|
} else {
|
|
triggerErrorOnChunk(chunk, errorWithDigest);
|
|
}
|
|
}
|
|
|
|
function resolveErrorDev(
|
|
response: Response,
|
|
id: number,
|
|
digest: string,
|
|
message: string,
|
|
stack: string,
|
|
): void {
|
|
if (!__DEV__) {
|
|
// These errors should never make it into a build so we don't need to encode them in codes.json
|
|
// eslint-disable-next-line react-internal/prod-error-codes
|
|
throw new Error(
|
|
'resolveErrorDev should never be called in production mode. Use resolveErrorProd instead. This is a bug in React.',
|
|
);
|
|
}
|
|
// eslint-disable-next-line react-internal/prod-error-codes
|
|
const error = new Error(
|
|
message ||
|
|
'An error occurred in the Server Components render but no message was provided',
|
|
);
|
|
error.stack = stack;
|
|
(error: any).digest = digest;
|
|
const errorWithDigest: ErrorWithDigest = (error: any);
|
|
const chunks = response._chunks;
|
|
const chunk = chunks.get(id);
|
|
if (!chunk) {
|
|
chunks.set(id, createErrorChunk(response, errorWithDigest));
|
|
} else {
|
|
triggerErrorOnChunk(chunk, errorWithDigest);
|
|
}
|
|
}
|
|
|
|
function resolveHint(
|
|
response: Response,
|
|
code: string,
|
|
model: UninitializedModel,
|
|
): void {
|
|
const hintModel = parseModel<HintModel>(response, model);
|
|
dispatchHint(code, hintModel);
|
|
}
|
|
|
|
function processFullRow(response: Response, row: string): void {
|
|
if (row === '') {
|
|
return;
|
|
}
|
|
const colon = row.indexOf(':', 0);
|
|
const id = parseInt(row.slice(0, colon), 16);
|
|
const tag = row[colon + 1];
|
|
// When tags that are not text are added, check them here before
|
|
// parsing the row as text.
|
|
// switch (tag) {
|
|
// }
|
|
switch (tag) {
|
|
case 'I': {
|
|
resolveModule(response, id, row.slice(colon + 2));
|
|
return;
|
|
}
|
|
case 'H': {
|
|
const code = row[colon + 2];
|
|
resolveHint(response, code, row.slice(colon + 3));
|
|
return;
|
|
}
|
|
case 'E': {
|
|
const errorInfo = JSON.parse(row.slice(colon + 2));
|
|
if (__DEV__) {
|
|
resolveErrorDev(
|
|
response,
|
|
id,
|
|
errorInfo.digest,
|
|
errorInfo.message,
|
|
errorInfo.stack,
|
|
);
|
|
} else {
|
|
resolveErrorProd(response, id, errorInfo.digest);
|
|
}
|
|
return;
|
|
}
|
|
default: {
|
|
// We assume anything else is JSON.
|
|
resolveModel(response, id, row.slice(colon + 1));
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
export function processStringChunk(
|
|
response: Response,
|
|
chunk: string,
|
|
offset: number,
|
|
): void {
|
|
let linebreak = chunk.indexOf('\n', offset);
|
|
while (linebreak > -1) {
|
|
const fullrow = response._partialRow + chunk.slice(offset, linebreak);
|
|
processFullRow(response, fullrow);
|
|
response._partialRow = '';
|
|
offset = linebreak + 1;
|
|
linebreak = chunk.indexOf('\n', offset);
|
|
}
|
|
response._partialRow += chunk.slice(offset);
|
|
}
|
|
|
|
export function processBinaryChunk(
|
|
response: Response,
|
|
chunk: Uint8Array,
|
|
): void {
|
|
if (!supportsBinaryStreams) {
|
|
throw new Error("This environment don't support binary chunks.");
|
|
}
|
|
const stringDecoder = response._stringDecoder;
|
|
let linebreak = chunk.indexOf(10); // newline
|
|
while (linebreak > -1) {
|
|
const fullrow =
|
|
response._partialRow +
|
|
readFinalStringChunk(stringDecoder, chunk.subarray(0, linebreak));
|
|
processFullRow(response, fullrow);
|
|
response._partialRow = '';
|
|
chunk = chunk.subarray(linebreak + 1);
|
|
linebreak = chunk.indexOf(10); // newline
|
|
}
|
|
response._partialRow += readPartialStringChunk(stringDecoder, chunk);
|
|
}
|
|
|
|
function parseModel<T>(response: Response, json: UninitializedModel): T {
|
|
return JSON.parse(json, response._fromJSON);
|
|
}
|
|
|
|
function createFromJSONCallback(response: Response) {
|
|
// $FlowFixMe[missing-this-annot]
|
|
return function (key: string, value: JSONValue) {
|
|
if (typeof value === 'string') {
|
|
// We can't use .bind here because we need the "this" value.
|
|
return parseModelString(response, this, key, value);
|
|
}
|
|
if (typeof value === 'object' && value !== null) {
|
|
return parseModelTuple(response, value);
|
|
}
|
|
return value;
|
|
};
|
|
}
|
|
|
|
export function close(response: Response): void {
|
|
// In case there are any remaining unresolved chunks, they won't
|
|
// be resolved now. So we need to issue an error to those.
|
|
// Ideally we should be able to early bail out if we kept a
|
|
// ref count of pending chunks.
|
|
reportGlobalError(response, new Error('Connection closed.'));
|
|
}
|