import type {Thenable} from 'shared/ReactTypes';
import type {
ServerReferenceId,
ServerManifest,
ClientReference as ServerReference,
} from 'react-client/src/ReactFlightClientConfig';
import type {TemporaryReferenceSet} from './ReactFlightServerTemporaryReferences';
import {
resolveServerReference,
preloadModule,
requireModule,
} from 'react-client/src/ReactFlightClientConfig';
import {
createTemporaryReference,
registerTemporaryReference,
} from './ReactFlightServerTemporaryReferences';
import {ASYNC_ITERATOR} from 'shared/ReactSymbols';
import hasOwnProperty from 'shared/hasOwnProperty';
import getPrototypeOf from 'shared/getPrototypeOf';
import isArray from 'shared/isArray';
interface FlightStreamController {
enqueueModel(json: string): void;
close(json: string): void;
error(error: Error): void;
}
export type JSONValue =
| number
| null
| boolean
| string
| {+[key: string]: JSONValue}
| $ReadOnlyArray<JSONValue>;
const PENDING = 'pending';
const BLOCKED = 'blocked';
const RESOLVED_MODEL = 'resolved_model';
const INITIALIZED = 'fulfilled';
const ERRORED = 'rejected';
const __PROTO__ = '__proto__';
type RESPONSE_SYMBOL_TYPE = 'RESPONSE_SYMBOL';
const RESPONSE_SYMBOL: RESPONSE_SYMBOL_TYPE = (Symbol(): any);
type PendingChunk<T> = {
status: 'pending',
value: null | Array<InitializationReference | (T => mixed)>,
reason: null | Array<InitializationReference | (mixed => mixed)>,
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
};
type BlockedChunk<T> = {
status: 'blocked',
value: null | Array<InitializationReference | (T => mixed)>,
reason: null | Array<InitializationReference | (mixed => mixed)>,
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
};
type ResolvedModelChunk<T> = {
status: 'resolved_model',
value: string,
reason: {id: number, [RESPONSE_SYMBOL_TYPE]: Response},
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
};
type InitializedChunk<T> = {
status: 'fulfilled',
value: T,
reason: null | NestedArrayContext,
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
};
type InitializedStreamChunk<
T: ReadableStream | $AsyncIterable<any, any, void>,
> = {
status: 'fulfilled',
value: T,
reason: FlightStreamController,
then(resolve: (ReadableStream) => mixed, reject?: (mixed) => mixed): void,
};
type ErroredChunk<T> = {
status: 'rejected',
value: null,
reason: mixed,
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
};
type SomeChunk<T> =
| PendingChunk<T>
| BlockedChunk<T>
| ResolvedModelChunk<T>
| InitializedChunk<T>
| ErroredChunk<T>;
function ReactPromise(status: any, value: any, reason: any) {
this.status = status;
this.value = value;
this.reason = reason;
}
ReactPromise.prototype = (Object.create(Promise.prototype): any);
ReactPromise.prototype.then = function <T>(
this: SomeChunk<T>,
resolve: (value: T) => mixed,
reject: ?(reason: mixed) => mixed,
) {
const chunk: SomeChunk<T> = this;
switch (chunk.status) {
case RESOLVED_MODEL:
initializeModelChunk(chunk);
break;
}
switch (chunk.status) {
case INITIALIZED:
if (typeof resolve === 'function') {
let inspectedValue = chunk.value;
let cycleProtection = 0;
const visited = new Set<typeof ReactPromise>();
while (inspectedValue instanceof ReactPromise) {
cycleProtection++;
if (
inspectedValue === chunk ||
visited.has(inspectedValue) ||
cycleProtection > 1000
) {
if (typeof reject === 'function') {
reject(new Error('Cannot have cyclic thenables.'));
}
return;
}
visited.add(inspectedValue);
if (inspectedValue.status === INITIALIZED) {
inspectedValue = inspectedValue.value;
} else {
break;
}
}
resolve(chunk.value);
}
break;
case PENDING:
case BLOCKED:
if (typeof resolve === 'function') {
if (chunk.value === null) {
chunk.value = ([]: Array<InitializationReference | (T => mixed)>);
}
chunk.value.push(resolve);
}
if (typeof reject === 'function') {
if (chunk.reason === null) {
chunk.reason = ([]: Array<
InitializationReference | (mixed => mixed),
>);
}
chunk.reason.push(reject);
}
break;
default:
if (typeof reject === 'function') {
reject(chunk.reason);
}
break;
}
};
const ObjectPrototype = Object.prototype;
const ArrayPrototype = Array.prototype;
export type Response = {
_bundlerConfig: ServerManifest,
_prefix: string,
_formData: FormData,
_chunks: Map<number, SomeChunk<any>>,
_closed: boolean,
_closedReason: mixed,
_temporaryReferences: void | TemporaryReferenceSet,
_rootArrayContexts: WeakMap<$ReadOnlyArray<mixed>, NestedArrayContext>,
_arraySizeLimit: number,
};
export function getRoot<T>(response: Response): Thenable<T> {
const chunk = getChunk(response, 0);
return (chunk: any);
}
function createPendingChunk<T>(response: Response): PendingChunk<T> {
return new ReactPromise(PENDING, null, null);
}
function wakeChunk<T>(
response: Response,
listeners: Array<InitializationReference | (T => mixed)>,
value: T,
chunk: InitializedChunk<T>,
): void {
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i];
if (typeof listener === 'function') {
listener(value);
} else {
fulfillReference(response, listener, value, chunk.reason);
}
}
}
function rejectChunk(
response: Response,
listeners: Array<InitializationReference | (mixed => mixed)>,
error: mixed,
): void {
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i];
if (typeof listener === 'function') {
listener(error);
} else {
rejectReference(response, listener.handler, error);
}
}
}
function wakeChunkIfInitialized<T>(
response: Response,
chunk: SomeChunk<T>,
resolveListeners: Array<InitializationReference | (T => mixed)>,
rejectListeners: null | Array<InitializationReference | (mixed => mixed)>,
): void {
switch (chunk.status) {
case INITIALIZED:
wakeChunk(response, resolveListeners, chunk.value, chunk);
break;
case BLOCKED:
case PENDING:
if (chunk.value) {
for (let i = 0; i < resolveListeners.length; i++) {
chunk.value.push(resolveListeners[i]);
}
} else {
chunk.value = resolveListeners;
}
if (chunk.reason) {
if (rejectListeners) {
for (let i = 0; i < rejectListeners.length; i++) {
chunk.reason.push(rejectListeners[i]);
}
}
} else {
chunk.reason = rejectListeners;
}
break;
case ERRORED:
if (rejectListeners) {
rejectChunk(response, rejectListeners, chunk.reason);
}
break;
}
}
function triggerErrorOnChunk<T>(
response: Response,
chunk: SomeChunk<T>,
error: mixed,
): void {
if (chunk.status !== PENDING && chunk.status !== BLOCKED) {
const streamChunk: InitializedStreamChunk<any> = (chunk: any);
const controller = streamChunk.reason;
controller.error(error);
return;
}
const listeners = chunk.reason;
const erroredChunk: ErroredChunk<T> = (chunk: any);
erroredChunk.status = ERRORED;
erroredChunk.reason = error;
if (listeners !== null) {
rejectChunk(response, listeners, error);
}
}
function createResolvedModelChunk<T>(
response: Response,
value: string,
id: number,
): ResolvedModelChunk<T> {
return new ReactPromise(RESOLVED_MODEL, value, {
id,
[RESPONSE_SYMBOL]: response,
});
}
function createErroredChunk<T>(
response: Response,
reason: mixed,
): ErroredChunk<T> {
return new ReactPromise(ERRORED, null, reason);
}
function resolveModelChunk<T>(
response: Response,
chunk: SomeChunk<T>,
value: string,
id: number,
): void {
if (chunk.status !== PENDING) {
const streamChunk: InitializedStreamChunk<any> = (chunk: any);
const controller = streamChunk.reason;
if (value[0] === 'C') {
controller.close(value === 'C' ? '"$undefined"' : value.slice(1));
} else {
controller.enqueueModel(value);
}
return;
}
const resolveListeners = chunk.value;
const rejectListeners = chunk.reason;
const resolvedChunk: ResolvedModelChunk<T> = (chunk: any);
resolvedChunk.status = RESOLVED_MODEL;
resolvedChunk.value = value;
resolvedChunk.reason = {id, [RESPONSE_SYMBOL]: response};
if (resolveListeners !== null) {
initializeModelChunk(resolvedChunk);
wakeChunkIfInitialized(response, chunk, resolveListeners, rejectListeners);
}
}
function createInitializedStreamChunk<
T: ReadableStream | $AsyncIterable<any, any, void>,
>(
response: Response,
value: T,
controller: FlightStreamController,
): InitializedChunk<T> {
return new ReactPromise(INITIALIZED, value, controller);
}
function createResolvedIteratorResultChunk<T>(
response: Response,
value: string,
done: boolean,
): ResolvedModelChunk<IteratorResult<T, T>> {
const iteratorResultJSON =
(done ? '{"done":true,"value":' : '{"done":false,"value":') + value + '}';
return new ReactPromise(RESOLVED_MODEL, iteratorResultJSON, {
id: -1,
[RESPONSE_SYMBOL]: response,
});
}
function resolveIteratorResultChunk<T>(
response: Response,
chunk: SomeChunk<IteratorResult<T, T>>,
value: string,
done: boolean,
): void {
const iteratorResultJSON =
(done ? '{"done":true,"value":' : '{"done":false,"value":') + value + '}';
resolveModelChunk(response, chunk, iteratorResultJSON, -1);
}
function loadServerReference<A: Iterable<any>, T>(
response: Response,
metaData: {
id: any,
bound: null | Thenable<Array<any>>,
},
parentObject: Object,
key: string,
): (...A) => Promise<T> {
const id: ServerReferenceId = metaData.id;
if (typeof id !== 'string') {
return (null: any);
}
if (key === 'then') {
return (null: any);
}
const cachedPromise: SomeChunk<T> | void = (metaData: any).$$promise;
if (cachedPromise !== undefined) {
if (cachedPromise.status === INITIALIZED) {
const resolvedValue: T = cachedPromise.value;
if (key === __PROTO__) {
return (null: any);
}
parentObject[key] = resolvedValue;
return (resolvedValue: any);
}
let handler: InitializationHandler;
if (initializingHandler) {
handler = initializingHandler;
handler.deps++;
} else {
handler = initializingHandler = {
chunk: null,
value: null,
reason: null,
deps: 1,
errored: false,
};
}
cachedPromise.then(
resolveReference.bind(null, response, handler, parentObject, key),
rejectReference.bind(null, response, handler),
);
return (null: any);
}
const blockedPromise: BlockedChunk<T> = new ReactPromise(BLOCKED, null, null);
(metaData: any).$$promise = blockedPromise;
const serverReference: ServerReference<T> =
resolveServerReference<$FlowFixMe>(response._bundlerConfig, id);
const bound = metaData.bound;
let serverReferencePromise: null | Thenable<any> =
preloadModule(serverReference);
if (!serverReferencePromise) {
if (bound instanceof ReactPromise) {
serverReferencePromise = Promise.resolve(bound);
} else {
const resolvedValue = (requireModule(serverReference): any);
const initializedPromise: InitializedChunk<T> = (blockedPromise: any);
initializedPromise.status = INITIALIZED;
initializedPromise.value = resolvedValue;
return resolvedValue;
}
} else if (bound instanceof ReactPromise) {
serverReferencePromise = Promise.all([serverReferencePromise, bound]);
}
let handler: InitializationHandler;
if (initializingHandler) {
handler = initializingHandler;
handler.deps++;
} else {
handler = initializingHandler = {
chunk: null,
value: null,
reason: null,
deps: 1,
errored: false,
};
}
function fulfill(): void {
let resolvedValue = (requireModule(serverReference): any);
if (metaData.bound) {
const promiseValue = (metaData.bound: any).value;
const boundArgs: Array<any> = isArray(promiseValue)
? promiseValue.slice(0)
: [];
if (boundArgs.length > MAX_BOUND_ARGS) {
reject(
new Error(
'Server Function has too many bound arguments. Received ' +
boundArgs.length +
' but the limit is ' +
MAX_BOUND_ARGS +
'.',
),
);
return;
}
boundArgs.unshift(null);
resolvedValue = resolvedValue.bind.apply(resolvedValue, boundArgs);
}
const resolveListeners = blockedPromise.value;
const initializedPromise: InitializedChunk<T> = (blockedPromise: any);
initializedPromise.status = INITIALIZED;
initializedPromise.value = resolvedValue;
initializedPromise.reason = null;
if (resolveListeners !== null) {
wakeChunk(response, resolveListeners, resolvedValue, initializedPromise);
}
resolveReference(response, handler, parentObject, key, resolvedValue);
}
function reject(error: mixed): void {
const rejectListeners = blockedPromise.reason;
const erroredPromise: ErroredChunk<T> = (blockedPromise: any);
erroredPromise.status = ERRORED;
erroredPromise.value = null;
erroredPromise.reason = error;
if (rejectListeners !== null) {
rejectChunk(response, rejectListeners, error);
}
rejectReference(response, handler, error);
}
serverReferencePromise.then(fulfill, reject);
return (null: any);
}
function reviveModel(
response: Response,
parentObj: any,
parentKey: string,
value: JSONValue,
reference: void | string,
arrayRoot: null | NestedArrayContext,
): any {
if (typeof value === 'string') {
return parseModelString(
response,
parentObj,
parentKey,
value,
reference,
arrayRoot,
);
}
if (typeof value === 'object' && value !== null) {
if (
reference !== undefined &&
response._temporaryReferences !== undefined
) {
registerTemporaryReference(
response._temporaryReferences,
value,
reference,
);
}
if (isArray(value)) {
let childContext: NestedArrayContext;
if (arrayRoot === null) {
childContext = ({
count: 0,
fork: false,
}: NestedArrayContext);
response._rootArrayContexts.set(value, childContext);
} else {
childContext = arrayRoot;
}
if (value.length > 1) {
childContext.fork = true;
}
bumpArrayCount(childContext, value.length + 1, response);
for (let i = 0; i < value.length; i++) {
const childRef =
reference !== undefined ? reference + ':' + i : undefined;
value[i] = reviveModel(
response,
value,
'' + i,
value[i],
childRef,
childContext,
);
}
} else {
for (const key in value) {
if (hasOwnProperty.call(value, key)) {
if (key === __PROTO__) {
delete value[key];
continue;
}
const childRef =
reference !== undefined && key.indexOf(':') === -1
? reference + ':' + key
: undefined;
const newValue = reviveModel(
response,
value,
key,
value[key],
childRef,
null,
);
if (newValue !== undefined) {
value[key] = newValue;
} else {
delete value[key];
}
}
}
}
}
return value;
}
type NestedArrayContext = {
count: number,
fork: boolean,
};
function bumpArrayCount(
arrayContext: NestedArrayContext,
slots: number,
response: Response,
): void {
const newCount = (arrayContext.count += slots);
if (newCount > response._arraySizeLimit && arrayContext.fork) {
throw new Error(
'Maximum array nesting exceeded. Large nested arrays can be dangerous. Try adding intermediate objects.',
);
}
}
type InitializationReference = {
handler: InitializationHandler,
parentObject: Object,
key: string,
map: (
response: Response,
model: any,
parentObject: Object,
key: string,
) => any,
path: Array<string>,
arrayRoot: null | NestedArrayContext,
};
type InitializationHandler = {
chunk: null | BlockedChunk<any>,
value: any,
reason: any,
deps: number,
errored: boolean,
};
let initializingHandler: null | InitializationHandler = null;
function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
const prevHandler = initializingHandler;
initializingHandler = null;
const {[RESPONSE_SYMBOL]: response, id} = chunk.reason;
const rootReference = id === -1 ? undefined : id.toString(16);
const resolvedModel = chunk.value;
const cyclicChunk: BlockedChunk<T> = (chunk: any);
cyclicChunk.status = BLOCKED;
cyclicChunk.value = null;
cyclicChunk.reason = null;
try {
const rawModel = JSON.parse(resolvedModel);
const arrayRoot: NestedArrayContext = {
count: 0,
fork: false,
};
const value: T = reviveModel(
response,
{'': rawModel},
'',
rawModel,
rootReference,
arrayRoot,
);
const resolveListeners = cyclicChunk.value;
if (resolveListeners !== null) {
cyclicChunk.value = null;
cyclicChunk.reason = null;
for (let i = 0; i < resolveListeners.length; i++) {
const listener = resolveListeners[i];
if (typeof listener === 'function') {
listener(value);
} else {
fulfillReference(response, listener, value, arrayRoot);
}
}
}
if (initializingHandler !== null) {
if (initializingHandler.errored) {
throw initializingHandler.reason;
}
if (initializingHandler.deps > 0) {
initializingHandler.value = value;
initializingHandler.reason = arrayRoot;
initializingHandler.chunk = cyclicChunk;
return;
}
}
const initializedChunk: InitializedChunk<T> = (chunk: any);
initializedChunk.status = INITIALIZED;
initializedChunk.value = value;
initializedChunk.reason = arrayRoot;
} catch (error) {
const erroredChunk: ErroredChunk<T> = (chunk: any);
erroredChunk.status = ERRORED;
erroredChunk.reason = error;
} finally {
initializingHandler = prevHandler;
}
}
export function reportGlobalError(response: Response, error: Error): void {
response._closed = true;
response._closedReason = error;
response._chunks.forEach(chunk => {
if (chunk.status === PENDING) {
triggerErrorOnChunk(response, chunk, error);
} else if (chunk.status === INITIALIZED && chunk.reason !== null) {
const maybeController = chunk.reason;
if (typeof maybeController.error === 'function') {
maybeController.error(error);
}
}
});
}
function getChunk(response: Response, id: number): SomeChunk<any> {
const chunks = response._chunks;
let chunk = chunks.get(id);
if (!chunk) {
const prefix = response._prefix;
const key = prefix + id;
const backingEntry = response._formData.get(key);
if (typeof backingEntry === 'string') {
chunk = createResolvedModelChunk(response, backingEntry, id);
} else if (response._closed) {
chunk = createErroredChunk(response, response._closedReason);
} else {
chunk = createPendingChunk(response);
}
chunks.set(id, chunk);
}
return chunk;
}
function fulfillReference(
response: Response,
reference: InitializationReference,
value: any,
arrayRoot: null | NestedArrayContext,
): void {
const {handler, parentObject, key, map, path} = reference;
let resolvedValue;
try {
let localLength: number = 0;
const rootArrayContexts = response._rootArrayContexts;
for (let i = 1; i < path.length; i++) {
const name = path[i];
if (
typeof value === 'object' &&
value !== null &&
(getPrototypeOf(value) === ObjectPrototype ||
getPrototypeOf(value) === ArrayPrototype) &&
hasOwnProperty.call(value, name)
) {
value = value[name];
if (isArray(value)) {
localLength = 0;
arrayRoot = rootArrayContexts.get(value) || arrayRoot;
} else {
arrayRoot = null;
if (typeof value === 'string') {
localLength = value.length;
} else if (typeof value === 'bigint') {
const n = Math.abs(Number(value));
if (n === 0) {
localLength = 1;
} else {
localLength = Math.floor(Math.log10(n)) + 1;
}
} else if (ArrayBuffer.isView(value)) {
localLength = value.byteLength;
} else {
localLength = 0;
}
}
} else {
throw new Error('Invalid reference.');
}
}
resolvedValue = map(response, value, parentObject, key);
const referenceArrayRoot = reference.arrayRoot;
if (referenceArrayRoot !== null) {
if (arrayRoot !== null) {
if (arrayRoot.fork) {
referenceArrayRoot.fork = true;
}
bumpArrayCount(referenceArrayRoot, arrayRoot.count, response);
} else if (localLength > 0) {
bumpArrayCount(referenceArrayRoot, localLength, response);
}
}
} catch (error) {
rejectReference(response, handler, error);
return;
}
resolveReference(response, handler, parentObject, key, resolvedValue);
}
function resolveReference(
response: Response,
handler: InitializationHandler,
parentObject: Object,
key: string,
resolvedValue: mixed,
): void {
if (key !== __PROTO__) {
parentObject[key] = resolvedValue;
}
if (key === '' && handler.value === null) {
handler.value = resolvedValue;
}
handler.deps--;
if (handler.deps === 0) {
const chunk = handler.chunk;
if (chunk === null || chunk.status !== BLOCKED) {
return;
}
const resolveListeners = chunk.value;
const initializedChunk: InitializedChunk<any> = (chunk: any);
initializedChunk.status = INITIALIZED;
initializedChunk.value = handler.value;
initializedChunk.reason = handler.reason;
if (resolveListeners !== null) {
wakeChunk(response, resolveListeners, handler.value, initializedChunk);
}
}
}
function rejectReference(
response: Response,
handler: InitializationHandler,
error: mixed,
): void {
if (handler.errored) {
return;
}
handler.errored = true;
handler.value = null;
handler.reason = error;
const chunk = handler.chunk;
if (chunk === null || chunk.status !== BLOCKED) {
return;
}
triggerErrorOnChunk(response, chunk, error);
}
function waitForReference<T>(
response: Response,
referencedChunk: BlockedChunk<T>,
parentObject: Object,
key: string,
arrayRoot: null | NestedArrayContext,
map: (response: Response, model: any, parentObject: Object, key: string) => T,
path: Array<string>,
): T {
let handler: InitializationHandler;
if (initializingHandler) {
handler = initializingHandler;
handler.deps++;
} else {
handler = initializingHandler = {
chunk: null,
value: null,
reason: null,
deps: 1,
errored: false,
};
}
const reference: InitializationReference = {
handler,
parentObject,
key,
map,
path,
arrayRoot,
};
if (referencedChunk.value === null) {
referencedChunk.value = [reference];
} else {
referencedChunk.value.push(reference);
}
if (referencedChunk.reason === null) {
referencedChunk.reason = [reference];
} else {
referencedChunk.reason.push(reference);
}
return (null: any);
}
function getOutlinedModel<T>(
response: Response,
reference: string,
parentObject: Object,
key: string,
referenceArrayRoot: null | NestedArrayContext,
map: (response: Response, model: any, parentObject: Object, key: string) => T,
): T {
const path = reference.split(':');
const id = parseInt(path[0], 16);
const chunk = getChunk(response, id);
switch (chunk.status) {
case RESOLVED_MODEL:
initializeModelChunk(chunk);
break;
}
switch (chunk.status) {
case INITIALIZED:
let value = chunk.value;
let arrayRoot: null | NestedArrayContext = chunk.reason;
let localLength: number = 0;
const rootArrayContexts = response._rootArrayContexts;
for (let i = 1; i < path.length; i++) {
const name = path[i];
if (
typeof value === 'object' &&
value !== null &&
(getPrototypeOf(value) === ObjectPrototype ||
getPrototypeOf(value) === ArrayPrototype) &&
hasOwnProperty.call(value, name)
) {
value = value[name];
if (isArray(value)) {
localLength = 0;
arrayRoot = rootArrayContexts.get(value) || arrayRoot;
} else {
arrayRoot = null;
if (typeof value === 'string') {
localLength = value.length;
} else if (typeof value === 'bigint') {
const n = Math.abs(Number(value));
if (n === 0) {
localLength = 1;
} else {
localLength = Math.floor(Math.log10(n)) + 1;
}
} else if (ArrayBuffer.isView(value)) {
localLength = value.byteLength;
} else {
localLength = 0;
}
}
} else {
throw new Error('Invalid reference.');
}
}
const chunkValue = map(response, value, parentObject, key);
if (referenceArrayRoot !== null) {
if (arrayRoot !== null) {
if (arrayRoot.fork) {
referenceArrayRoot.fork = true;
}
bumpArrayCount(referenceArrayRoot, arrayRoot.count, response);
} else if (localLength > 0) {
bumpArrayCount(referenceArrayRoot, localLength, response);
}
}
return chunkValue;
case BLOCKED:
return waitForReference(
response,
chunk,
parentObject,
key,
referenceArrayRoot,
map,
path,
);
case PENDING:
throw new Error('Invalid forward reference.');
default:
if (initializingHandler) {
initializingHandler.errored = true;
initializingHandler.value = null;
initializingHandler.reason = chunk.reason;
} else {
initializingHandler = {
chunk: null,
value: null,
reason: chunk.reason,
deps: 0,
errored: true,
};
}
return (null: any);
}
}
function createMap(
response: Response,
model: Array<[any, any]>,
): Map<any, any> {
if (!isArray(model)) {
throw new Error('Invalid Map initializer.');
}
if ((model as any).$$consumed === true) {
throw new Error('Already initialized Map.');
}
const map = new Map(model);
(model as any).$$consumed = true;
return map;
}
function createSet(response: Response, model: Array<any>): Set<any> {
if (!isArray(model)) {
throw new Error('Invalid Set initializer.');
}
if ((model as any).$$consumed === true) {
throw new Error('Already initialized Set.');
}
const set = new Set(model);
(model as any).$$consumed = true;
return set;
}
function extractIterator(response: Response, model: Array<any>): Iterator<any> {
if (!isArray(model)) {
throw new Error('Invalid Iterator initializer.');
}
if ((model as any).$$consumed === true) {
throw new Error('Already initialized Iterator.');
}
const iterator = model[Symbol.iterator]();
(model as any).$$consumed = true;
return iterator;
}
function createModel(
response: Response,
model: any,
parentObject: Object,
key: string,
): any {
if (key === 'then' && typeof model === 'function') {
return null;
}
return model;
}
function parseTypedArray<T: $ArrayBufferView | ArrayBuffer>(
response: Response,
reference: string,
constructor: any,
bytesPerElement: number,
parentObject: Object,
parentKey: string,
referenceArrayRoot: null | NestedArrayContext,
): null {
const id = parseInt(reference.slice(2), 16);
const prefix = response._prefix;
const key = prefix + id;
const chunks = response._chunks;
if (chunks.has(id)) {
throw new Error('Already initialized typed array.');
}
chunks.set(
id,
createErroredChunk(response, new Error('Already initialized typed array.')),
);
const backingEntry: Blob = (response._formData.get(key): any);
const promise: Promise<ArrayBuffer> = backingEntry.arrayBuffer();
let handler: InitializationHandler;
if (initializingHandler) {
handler = initializingHandler;
handler.deps++;
} else {
handler = initializingHandler = {
chunk: null,
value: null,
reason: null,
deps: 1,
errored: false,
};
}
function fulfill(buffer: ArrayBuffer): void {
try {
if (referenceArrayRoot !== null) {
bumpArrayCount(referenceArrayRoot, buffer.byteLength, response);
}
const resolvedValue: T =
constructor === ArrayBuffer
? (buffer: any)
: (new constructor(buffer): any);
if (key !== __PROTO__) {
parentObject[parentKey] = resolvedValue;
}
if (parentKey === '' && handler.value === null) {
handler.value = resolvedValue;
}
} catch (x) {
reject(x);
return;
}
handler.deps--;
if (handler.deps === 0) {
const chunk = handler.chunk;
if (chunk === null || chunk.status !== BLOCKED) {
return;
}
const resolveListeners = chunk.value;
const initializedChunk: InitializedChunk<T> = (chunk: any);
initializedChunk.status = INITIALIZED;
initializedChunk.value = handler.value;
initializedChunk.reason = null;
if (resolveListeners !== null) {
wakeChunk(response, resolveListeners, handler.value, initializedChunk);
}
}
}
function reject(error: mixed): void {
if (handler.errored) {
return;
}
handler.errored = true;
handler.value = null;
handler.reason = error;
const chunk = handler.chunk;
if (chunk === null || chunk.status !== BLOCKED) {
return;
}
triggerErrorOnChunk(response, chunk, error);
}
promise.then(fulfill, reject);
return null;
}
function resolveStream<T: ReadableStream | $AsyncIterable<any, any, void>>(
response: Response,
id: number,
stream: T,
controller: FlightStreamController,
): void {
const chunks = response._chunks;
const chunk = createInitializedStreamChunk(response, stream, controller);
chunks.set(id, chunk);
const prefix = response._prefix;
const key = prefix + id;
const existingEntries = response._formData.getAll(key);
for (let i = 0; i < existingEntries.length; i++) {
const value = existingEntries[i];
if (typeof value === 'string') {
if (value[0] === 'C') {
controller.close(value === 'C' ? '"$undefined"' : value.slice(1));
} else {
controller.enqueueModel(value);
}
}
}
}
function parseReadableStream<T>(
response: Response,
reference: string,
type: void | 'bytes',
parentObject: Object,
parentKey: string,
): ReadableStream {
const id = parseInt(reference.slice(2), 16);
const chunks = response._chunks;
if (chunks.has(id)) {
throw new Error('Already initialized stream.');
}
let controller: ReadableStreamController = (null: any);
let closed = false;
const stream = new ReadableStream({
type: type,
start(c) {
controller = c;
},
});
let previousBlockedChunk: SomeChunk<T> | null = null;
function enqueue(value: T): void {
if (type === 'bytes' && !ArrayBuffer.isView(value)) {
flightController.error(new Error('Invalid data for bytes stream.'));
return;
}
controller.enqueue(value);
}
const flightController = {
enqueueModel(json: string): void {
if (previousBlockedChunk === null) {
const chunk: ResolvedModelChunk<T> = createResolvedModelChunk(
response,
json,
-1,
);
initializeModelChunk(chunk);
const initializedChunk: SomeChunk<T> = chunk;
if (initializedChunk.status === INITIALIZED) {
enqueue(initializedChunk.value);
} else {
chunk.then(enqueue, flightController.error);
previousBlockedChunk = chunk;
}
} else {
const blockedChunk = previousBlockedChunk;
const chunk: SomeChunk<T> = createPendingChunk(response);
chunk.then(enqueue, flightController.error);
previousBlockedChunk = chunk;
blockedChunk.then(function () {
if (previousBlockedChunk === chunk) {
previousBlockedChunk = null;
}
resolveModelChunk(response, chunk, json, -1);
});
}
},
close(json: string): void {
if (closed) {
return;
}
closed = true;
if (previousBlockedChunk === null) {
controller.close();
} else {
const blockedChunk = previousBlockedChunk;
previousBlockedChunk = null;
blockedChunk.then(() => controller.close());
}
},
error(error: mixed): void {
if (closed) {
return;
}
closed = true;
if (previousBlockedChunk === null) {
controller.error(error);
} else {
const blockedChunk = previousBlockedChunk;
previousBlockedChunk = null;
blockedChunk.then(() => controller.error((error: any)));
}
},
};
resolveStream(response, id, stream, flightController);
return stream;
}
function FlightIterator(
this: {next: (arg: void) => SomeChunk<IteratorResult<any, any>>, ...},
next: (arg: void) => SomeChunk<IteratorResult<any, any>>,
) {
this.next = next;
}
FlightIterator.prototype = ({}: any);
FlightIterator.prototype[ASYNC_ITERATOR] = function asyncIterator(
this: $AsyncIterator<any, any, void>,
) {
return this;
};
function parseAsyncIterable<T>(
response: Response,
reference: string,
iterator: boolean,
parentObject: Object,
parentKey: string,
): $AsyncIterable<T, T, void> | $AsyncIterator<T, T, void> {
const id = parseInt(reference.slice(2), 16);
const chunks = response._chunks;
if (chunks.has(id)) {
throw new Error('Already initialized stream.');
}
const buffer: Array<SomeChunk<IteratorResult<T, T>>> = [];
let closed = false;
let nextWriteIndex = 0;
const flightController = {
enqueueModel(value: string): void {
if (nextWriteIndex === buffer.length) {
buffer[nextWriteIndex] = createResolvedIteratorResultChunk(
response,
value,
false,
);
} else {
resolveIteratorResultChunk(
response,
buffer[nextWriteIndex],
value,
false,
);
}
nextWriteIndex++;
},
close(value: string): void {
if (closed) {
return;
}
closed = true;
if (nextWriteIndex === buffer.length) {
buffer[nextWriteIndex] = createResolvedIteratorResultChunk(
response,
value,
true,
);
} else {
resolveIteratorResultChunk(
response,
buffer[nextWriteIndex],
value,
true,
);
}
nextWriteIndex++;
while (nextWriteIndex < buffer.length) {
resolveIteratorResultChunk(
response,
buffer[nextWriteIndex++],
'"$undefined"',
true,
);
}
},
error(error: Error): void {
if (closed) {
return;
}
closed = true;
if (nextWriteIndex === buffer.length) {
buffer[nextWriteIndex] =
createPendingChunk<IteratorResult<T, T>>(response);
}
while (nextWriteIndex < buffer.length) {
triggerErrorOnChunk(response, buffer[nextWriteIndex++], error);
}
},
};
const iterable: $AsyncIterable<T, T, void> = {
[ASYNC_ITERATOR](): $AsyncIterator<T, T, void> {
let nextReadIndex = 0;
return new FlightIterator((arg: void) => {
if (arg !== undefined) {
throw new Error(
'Values cannot be passed to next() of AsyncIterables passed to Client Components.',
);
}
if (nextReadIndex === buffer.length) {
if (closed) {
return new ReactPromise(
INITIALIZED,
{done: true, value: undefined},
null,
);
}
buffer[nextReadIndex] =
createPendingChunk<IteratorResult<T, T>>(response);
}
return buffer[nextReadIndex++];
});
},
};
const stream = iterator ? iterable[ASYNC_ITERATOR]() : iterable;
resolveStream(response, id, stream, flightController);
return stream;
}
function parseModelString(
response: Response,
obj: Object,
key: string,
value: string,
reference: void | string,
arrayRoot: null | NestedArrayContext,
): any {
if (value[0] === '$') {
switch (value[1]) {
case '$': {
if (arrayRoot !== null) {
bumpArrayCount(arrayRoot, value.length - 1, response);
}
return value.slice(1);
}
case '@': {
const id = parseInt(value.slice(2), 16);
const chunk = getChunk(response, id);
return chunk;
}
case 'h': {
const ref = value.slice(2);
return getOutlinedModel(
response,
ref,
obj,
key,
null,
loadServerReference,
);
}
case 'T': {
if (
reference === undefined ||
response._temporaryReferences === undefined
) {
throw new Error(
'Could not reference an opaque temporary reference. ' +
'This is likely due to misconfiguring the temporaryReferences options ' +
'on the server.',
);
}
return createTemporaryReference(
response._temporaryReferences,
reference,
);
}
case 'Q': {
const ref = value.slice(2);
return getOutlinedModel(response, ref, obj, key, null, createMap);
}
case 'W': {
const ref = value.slice(2);
return getOutlinedModel(response, ref, obj, key, null, createSet);
}
case 'K': {
const stringId = value.slice(2);
const formPrefix = response._prefix + stringId + '_';
const data = new FormData();
const backingFormData = response._formData;
const keys = Array.from(backingFormData.keys());
for (let i = 0; i < keys.length; i++) {
const entryKey = keys[i];
if (entryKey.startsWith(formPrefix)) {
const entries = backingFormData.getAll(entryKey);
const newKey = entryKey.slice(formPrefix.length);
for (let j = 0; j < entries.length; j++) {
data.append(newKey, entries[j]);
}
backingFormData.delete(entryKey);
}
}
return data;
}
case 'i': {
const ref = value.slice(2);
return getOutlinedModel(response, ref, obj, key, null, extractIterator);
}
case 'I': {
return Infinity;
}
case '-': {
if (value === '$-0') {
return -0;
} else {
return -Infinity;
}
}
case 'N': {
return NaN;
}
case 'u': {
return undefined;
}
case 'D': {
return new Date(Date.parse(value.slice(2)));
}
case 'n': {
const bigIntStr = value.slice(2);
if (bigIntStr.length > MAX_BIGINT_DIGITS) {
throw new Error(
'BigInt is too large. Received ' +
bigIntStr.length +
' digits but the limit is ' +
MAX_BIGINT_DIGITS +
'.',
);
}
if (arrayRoot !== null) {
bumpArrayCount(arrayRoot, bigIntStr.length, response);
}
return BigInt(bigIntStr);
}
case 'A':
return parseTypedArray(
response,
value,
ArrayBuffer,
1,
obj,
key,
arrayRoot,
);
case 'O':
return parseTypedArray(
response,
value,
Int8Array,
1,
obj,
key,
arrayRoot,
);
case 'o':
return parseTypedArray(
response,
value,
Uint8Array,
1,
obj,
key,
arrayRoot,
);
case 'U':
return parseTypedArray(
response,
value,
Uint8ClampedArray,
1,
obj,
key,
arrayRoot,
);
case 'S':
return parseTypedArray(
response,
value,
Int16Array,
2,
obj,
key,
arrayRoot,
);
case 's':
return parseTypedArray(
response,
value,
Uint16Array,
2,
obj,
key,
arrayRoot,
);
case 'L':
return parseTypedArray(
response,
value,
Int32Array,
4,
obj,
key,
arrayRoot,
);
case 'l':
return parseTypedArray(
response,
value,
Uint32Array,
4,
obj,
key,
arrayRoot,
);
case 'G':
return parseTypedArray(
response,
value,
Float32Array,
4,
obj,
key,
arrayRoot,
);
case 'g':
return parseTypedArray(
response,
value,
Float64Array,
8,
obj,
key,
arrayRoot,
);
case 'M':
return parseTypedArray(
response,
value,
BigInt64Array,
8,
obj,
key,
arrayRoot,
);
case 'm':
return parseTypedArray(
response,
value,
BigUint64Array,
8,
obj,
key,
arrayRoot,
);
case 'V':
return parseTypedArray(
response,
value,
DataView,
1,
obj,
key,
arrayRoot,
);
case 'B': {
const id = parseInt(value.slice(2), 16);
const prefix = response._prefix;
const blobKey = prefix + id;
const backingEntry: Blob = (response._formData.get(blobKey): any);
return backingEntry;
}
case 'R': {
return parseReadableStream(response, value, undefined, obj, key);
}
case 'r': {
return parseReadableStream(response, value, 'bytes', obj, key);
}
case 'X': {
return parseAsyncIterable(response, value, false, obj, key);
}
case 'x': {
return parseAsyncIterable(response, value, true, obj, key);
}
}
const ref = value.slice(1);
return getOutlinedModel(response, ref, obj, key, arrayRoot, createModel);
}
if (arrayRoot !== null) {
bumpArrayCount(arrayRoot, value.length, response);
}
return value;
}
const DEFAULT_MAX_ARRAY_NESTING = 1000000;
const MAX_BIGINT_DIGITS = 300;
export const MAX_BOUND_ARGS = 1000;
export function createResponse(
bundlerConfig: ServerManifest,
formFieldPrefix: string,
temporaryReferences: void | TemporaryReferenceSet,
backingFormData?: FormData = new FormData(),
arraySizeLimit?: number = DEFAULT_MAX_ARRAY_NESTING,
): Response {
const chunks: Map<number, SomeChunk<any>> = new Map();
const response: Response = {
_bundlerConfig: bundlerConfig,
_prefix: formFieldPrefix,
_formData: backingFormData,
_chunks: chunks,
_closed: false,
_closedReason: null,
_temporaryReferences: temporaryReferences,
_rootArrayContexts: new WeakMap(),
_arraySizeLimit: arraySizeLimit,
};
return response;
}
export function resolveField(
response: Response,
key: string,
value: string,
): void {
response._formData.append(key, value);
const prefix = response._prefix;
if (key.startsWith(prefix)) {
const chunks = response._chunks;
const id = +key.slice(prefix.length);
const chunk = chunks.get(id);
if (chunk) {
resolveModelChunk(response, chunk, value, id);
}
}
}
export function resolveFile(response: Response, key: string, file: File): void {
response._formData.append(key, file);
}
export opaque type FileHandle = {
chunks: Array<Uint8Array>,
filename: string,
mime: string,
};
export function resolveFileInfo(
response: Response,
key: string,
filename: string,
mime: string,
): FileHandle {
return {
chunks: [],
filename,
mime,
};
}
export function resolveFileChunk(
response: Response,
handle: FileHandle,
chunk: Uint8Array,
): void {
handle.chunks.push(chunk);
}
export function resolveFileComplete(
response: Response,
key: string,
handle: FileHandle,
): void {
const blob = new Blob(handle.chunks, {type: handle.mime});
response._formData.append(key, blob, handle.filename);
}
export function close(response: Response): void {
reportGlobalError(response, new Error('Connection closed.'));
}