Chain Promises properly to avoid callback hell
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
1/**2 * Copyright (c) Meta Platforms, Inc. and affiliates.3 *4 * This source code is licensed under the MIT license found in the5 * LICENSE file in the root directory of this source tree.6 *7 * @flow8 */910import type {Thenable} from 'shared/ReactTypes';1112// The server acts as a Client of itself when resolving Server References.13// That's why we import the Client configuration from the Server.14// Everything is aliased as their Server equivalence for clarity.15import type {16 ServerReferenceId,17 ServerManifest,18 ClientReference as ServerReference,19} from 'react-client/src/ReactFlightClientConfig';2021import type {BackingFormData} from './ReactFlightReplyBackingFormData';22import type {TemporaryReferenceSet} from './ReactFlightServerTemporaryReferences';2324import {25 resolveServerReference,26 preloadModule,27 requireModule,28} from 'react-client/src/ReactFlightClientConfig';2930import {31 createBackingFormData,32 advanceBackingEntryIterator,33 appendBackingEntry,34 appendBackingFile,35 consumeBackingEntry,36 getBackingEntry,37 getAllBackingEntries,38 peekBackingEntry,39} from './ReactFlightReplyBackingFormData';40import {41 createTemporaryReference,42 registerTemporaryReference,43} from './ReactFlightServerTemporaryReferences';44import {ASYNC_ITERATOR} from 'shared/ReactSymbols';4546import hasOwnProperty from 'shared/hasOwnProperty';47import getPrototypeOf from 'shared/getPrototypeOf';48import isArray from 'shared/isArray';4950interface FlightStreamController {51 enqueueModel(json: string): void;52 close(json: string): void;53 error(error: Error): void;54}5556export type JSONValue =57 | number58 | null59 | boolean60 | string61 | {+[key: string]: JSONValue}62 | $ReadOnlyArray<JSONValue>;6364const PENDING = 'pending';65const BLOCKED = 'blocked';66const RESOLVED_MODEL = 'resolved_model';67const INITIALIZED = 'fulfilled';68const ERRORED = 'rejected';6970const __PROTO__ = '__proto__';7172type RESPONSE_SYMBOL_TYPE = 'RESPONSE_SYMBOL'; // Fake symbol type.73const RESPONSE_SYMBOL: RESPONSE_SYMBOL_TYPE = Symbol() as any;7475type PendingChunk<T> = {76 status: 'pending',77 value: null | Array<InitializationReference | (T => mixed)>,78 reason: null | Array<InitializationReference | (mixed => mixed)>,79 then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,80};81type BlockedChunk<T> = {82 status: 'blocked',83 value: null | Array<InitializationReference | (T => mixed)>,84 reason: null | Array<InitializationReference | (mixed => mixed)>,85 then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,86};87type ResolvedModelChunk<T> = {88 status: 'resolved_model',89 value: string,90 reason: {id: number, [RESPONSE_SYMBOL_TYPE]: Response},91 then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,92};93type InitializedChunk<T> = {94 status: 'fulfilled',95 value: T,96 reason: null | NestedArrayContext,97 then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,98};99type InitializedStreamChunk<100 T: ReadableStream | $AsyncIterable<any, any, void>,101> = {102 status: 'fulfilled',103 value: T,104 reason: FlightStreamController,105 then(resolve: (ReadableStream) => mixed, reject?: (mixed) => mixed): void,106};107type ErroredChunk<T> = {108 status: 'rejected',109 value: null,110 reason: mixed,111 then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,112};113type SomeChunk<T> =114 | PendingChunk<T>115 | BlockedChunk<T>116 | ResolvedModelChunk<T>117 | InitializedChunk<T>118 | ErroredChunk<T>;119120// $FlowFixMe[missing-this-annot]121function ReactPromise(status: any, value: any, reason: any) {122 this.status = status;123 this.value = value;124 this.reason = reason;125}126// We subclass Promise.prototype so that we get other methods like .catch127ReactPromise.prototype = Object.create(Promise.prototype) as any;128// TODO: This doesn't return a new Promise chain unlike the real .then129function reactPromiseThen<T>(130 this: SomeChunk<T>,131 resolve: (value: T) => mixed,132 reject: ?(reason: mixed) => mixed,133) {134 const chunk: SomeChunk<T> = this;135 // If we have resolved content, we try to initialize it first which136 // might put us back into one of the other states.137 switch (chunk.status) {138 case RESOLVED_MODEL:139 initializeModelChunk(chunk);140 break;141 }142 // The status might have changed after initialization.143 switch (chunk.status) {144 case INITIALIZED:145 if (typeof resolve === 'function') {146 let inspectedValue = chunk.value;147 // Recursively check if the value is itself a ReactPromise and if so if it points148 // back to itself. This helps catch recursive thenables early error.149 let cycleProtection = 0;150 const visited = new Set<typeof ReactPromise>();151 while (inspectedValue instanceof ReactPromise) {152 cycleProtection++;153 if (154 // $FlowFixMe[invalid-compare]155 inspectedValue === chunk ||156 visited.has(inspectedValue) ||157 cycleProtection > 1000158 ) {159 if (typeof reject === 'function') {160 reject(new Error('Cannot have cyclic thenables.'));161 }162 return;163 }164 visited.add(inspectedValue);165 // $FlowFixMe[invalid-compare]166 if (inspectedValue.status === INITIALIZED) {167 inspectedValue = inspectedValue.value;168 } else {169 // If this is lazily resolved, pending or blocked, it'll eventually become170 // initialized and break the loop. Rejected also breaks it.171 break;172 }173 }174 resolve(chunk.value);175 }176 break;177 case PENDING:178 case BLOCKED:179 if (typeof resolve === 'function') {180 if (chunk.value === null) {181 chunk.value = [] as Array<InitializationReference | (T => mixed)>;182 }183 chunk.value.push(resolve);184 }185 if (typeof reject === 'function') {186 if (chunk.reason === null) {187 chunk.reason = [] as Array<188 InitializationReference | (mixed => mixed),189 >;190 }191 chunk.reason.push(reject);192 }193 break;194 default:195 if (typeof reject === 'function') {196 reject(chunk.reason);197 }198 break;199 }200}201// The shadowing `then` must be defined with `Object.defineProperty` instead of202// assignment. Assignment would throw when `Promise.prototype` is frozen (e.g.203// by SES lockdown) because assigning over an inherited non-writable property204// is rejected.205Object.defineProperty(ReactPromise.prototype, 'then', {206 writable: true,207 enumerable: true,208 configurable: true,209 value: reactPromiseThen,210});211212const ObjectPrototype = Object.prototype;213const ArrayPrototype = Array.prototype;214215export type Response = {216 _bundlerConfig: ServerManifest,217 _prefix: string,218 _formData: BackingFormData,219 _chunks: Map<number, SomeChunk<any>>,220 _closed: boolean,221 _closedReason: mixed,222 _temporaryReferences: void | TemporaryReferenceSet,223 _rootArrayContexts: WeakMap<$ReadOnlyArray<mixed>, NestedArrayContext>,224 _arraySizeLimit: number,225};226227export function getRoot<T>(response: Response): Thenable<T> {228 const chunk = getChunk(response, 0);229 return chunk as any;230}231232function createPendingChunk<T>(response: Response): PendingChunk<T> {233 // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors234 return new ReactPromise(PENDING, null, null);235}236237function wakeChunk<T>(238 response: Response,239 listeners: Array<InitializationReference | (T => mixed)>,240 value: T,241 chunk: InitializedChunk<T>,242): void {243 for (let i = 0; i < listeners.length; i++) {244 const listener = listeners[i];245 if (typeof listener === 'function') {246 listener(value);247 } else {248 fulfillReference(response, listener, value, chunk.reason);249 }250 }251}252253function rejectChunk(254 response: Response,255 listeners: Array<InitializationReference | (mixed => mixed)>,256 error: mixed,257): void {258 for (let i = 0; i < listeners.length; i++) {259 const listener = listeners[i];260 if (typeof listener === 'function') {261 listener(error);262 } else {263 rejectReference(response, listener.handler, error);264 }265 }266}267268function wakeChunkIfInitialized<T>(269 response: Response,270 chunk: SomeChunk<T>,271 resolveListeners: Array<InitializationReference | (T => mixed)>,272 rejectListeners: null | Array<InitializationReference | (mixed => mixed)>,273): void {274 switch (chunk.status) {275 case INITIALIZED:276 wakeChunk(response, resolveListeners, chunk.value, chunk);277 break;278 case BLOCKED:279 case PENDING:280 if (chunk.value) {281 for (let i = 0; i < resolveListeners.length; i++) {282 chunk.value.push(resolveListeners[i]);283 }284 } else {285 chunk.value = resolveListeners;286 }287288 if (chunk.reason) {289 if (rejectListeners) {290 for (let i = 0; i < rejectListeners.length; i++) {291 chunk.reason.push(rejectListeners[i]);292 }293 }294 } else {295 chunk.reason = rejectListeners;296 }297 break;298 case ERRORED:299 if (rejectListeners) {300 rejectChunk(response, rejectListeners, chunk.reason);301 }302 break;303 }304}305306function triggerErrorOnChunk<T>(307 response: Response,308 chunk: SomeChunk<T>,309 error: mixed,310): void {311 if (chunk.status !== PENDING && chunk.status !== BLOCKED) {312 // If we get more data to an already resolved ID, we assume that it's313 // a stream chunk since any other row shouldn't have more than one entry.314 const streamChunk: InitializedStreamChunk<any> = chunk as any;315 const controller = streamChunk.reason;316 // $FlowFixMe[incompatible-type]: The error method should accept mixed.317 controller.error(error);318 return;319 }320 const listeners = chunk.reason;321 const erroredChunk: ErroredChunk<T> = chunk as any;322 erroredChunk.status = ERRORED;323 erroredChunk.reason = error;324 if (listeners !== null) {325 rejectChunk(response, listeners, error);326 }327}328329function createResolvedModelChunk<T>(330 response: Response,331 value: string,332 id: number,333): ResolvedModelChunk<T> {334 // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors335 return new ReactPromise(RESOLVED_MODEL, value, {336 id,337 [RESPONSE_SYMBOL]: response,338 });339}340341function createErroredChunk<T>(342 response: Response,343 reason: mixed,344): ErroredChunk<T> {345 // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors346 return new ReactPromise(ERRORED, null, reason);347}348349function resolveModelChunk<T>(350 response: Response,351 chunk: SomeChunk<T>,352 value: string,353 id: number,354): void {355 if (chunk.status !== PENDING) {356 // If we get more data to an already resolved ID, we assume that it's357 // a stream chunk since any other row shouldn't have more than one entry.358 const streamChunk: InitializedStreamChunk<any> = chunk as any;359 const controller = streamChunk.reason;360 if (value[0] === 'C') {361 controller.close(value === 'C' ? '"$undefined"' : value.slice(1));362 } else {363 controller.enqueueModel(value);364 }365 return;366 }367 const resolveListeners = chunk.value;368 const rejectListeners = chunk.reason;369 const resolvedChunk: ResolvedModelChunk<T> = chunk as any;370 resolvedChunk.status = RESOLVED_MODEL;371 resolvedChunk.value = value;372 resolvedChunk.reason = {id, [RESPONSE_SYMBOL]: response};373 if (resolveListeners !== null) {374 // This is unfortunate that we're reading this eagerly if375 // we already have listeners attached since they might no376 // longer be rendered or might not be the highest pri.377 initializeModelChunk(resolvedChunk);378 // The status might have changed after initialization.379 wakeChunkIfInitialized(response, chunk, resolveListeners, rejectListeners);380 }381}382383function createInitializedStreamChunk<384 T: ReadableStream | $AsyncIterable<any, any, void>,385>(386 response: Response,387 value: T,388 controller: FlightStreamController,389): InitializedChunk<T> {390 // We use the reason field to stash the controller since we already have that391 // field. It's a bit of a hack but efficient.392 // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors393 return new ReactPromise(INITIALIZED, value, controller);394}395396function createResolvedIteratorResultChunk<T>(397 response: Response,398 value: string,399 done: boolean,400): ResolvedModelChunk<IteratorResult<T, T>> {401 // To reuse code as much code as possible we add the wrapper element as part of the JSON.402 const iteratorResultJSON =403 (done ? '{"done":true,"value":' : '{"done":false,"value":') + value + '}';404 // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors405 return new ReactPromise(RESOLVED_MODEL, iteratorResultJSON, {406 id: -1,407 [RESPONSE_SYMBOL]: response,408 });409}410411function resolveIteratorResultChunk<T>(412 response: Response,413 chunk: SomeChunk<IteratorResult<T, T>>,414 value: string,415 done: boolean,416): void {417 // To reuse code as much code as possible we add the wrapper element as part of the JSON.418 const iteratorResultJSON =419 (done ? '{"done":true,"value":' : '{"done":false,"value":') + value + '}';420 resolveModelChunk(response, chunk, iteratorResultJSON, -1);421}422423function loadServerReference<A: Iterable<any>, T>(424 response: Response,425 metaData: {426 id: any,427 bound: null | Thenable<Array<any>>,428 },429 parentObject: Object,430 key: string,431): (...A) => Promise<T> {432 const id: ServerReferenceId = metaData.id;433 if (typeof id !== 'string') {434 return null as any;435 }436 if (key === 'then') {437 // This should never happen because we always serialize objects with then-functions438 // as "thenable" which reduces to ReactPromise with no other fields.439 return null as any;440 }441442 // Check for a cached promise from a previous call with the same metadata.443 // This handles deduplication when the same server reference appears multiple444 // times in the payload.445 const cachedPromise: SomeChunk<T> | void = (metaData as any).$$promise;446 if (cachedPromise !== undefined) {447 if (cachedPromise.status === INITIALIZED) {448 // The value was already resolved by a previous call.449 const resolvedValue: T = cachedPromise.value;450 if (key === __PROTO__) {451 return null as any;452 }453 parentObject[key] = resolvedValue;454 return resolvedValue as any;455 }456457 // The promise is still blocked. Increment the handler dependency count ...458 let handler: InitializationHandler;459 if (initializingHandler) {460 handler = initializingHandler;461 handler.deps++;462 } else {463 handler = initializingHandler = {464 chunk: null,465 value: null,466 reason: null,467 deps: 1,468 errored: false,469 };470 }471 // ... and register resolve and reject listeners on the promise.472 cachedPromise.then(473 resolveReference.bind(null, response, handler, parentObject, key),474 rejectReference.bind(null, response, handler),475 );476477 // Return a place holder value for now.478 return null as any;479 }480481 // This is the first call for this server reference metadata. Create a cached482 // promise to be used for subsequent calls.483 // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors484 const blockedPromise: BlockedChunk<T> = new ReactPromise(BLOCKED, null, null);485 (metaData as any).$$promise = blockedPromise;486487 const serverReference: ServerReference<T> =488 resolveServerReference<$FlowFixMe>(response._bundlerConfig, id);489 // We expect most servers to not really need this because you'd just have all490 // the relevant modules already loaded but it allows for lazy loading of code491 // if needed.492 const bound = metaData.bound;493 let serverReferencePromise: null | Thenable<any> =494 preloadModule(serverReference);495 if (!serverReferencePromise) {496 if (bound instanceof ReactPromise) {497 serverReferencePromise = Promise.resolve(bound);498 } else {499 const resolvedValue = requireModule(serverReference) as any;500 // Resolve the cached promise synchronously.501 const initializedPromise: InitializedChunk<T> = blockedPromise as any;502 initializedPromise.status = INITIALIZED;503 initializedPromise.value = resolvedValue;504 initializedPromise.reason = null;505 return resolvedValue;506 }507 } else if (bound instanceof ReactPromise) {508 serverReferencePromise = Promise.all([serverReferencePromise, bound]);509 }510511 let handler: InitializationHandler;512 if (initializingHandler) {513 handler = initializingHandler;514 handler.deps++;515 } else {516 handler = initializingHandler = {517 chunk: null,518 value: null,519 reason: null,520 deps: 1,521 errored: false,522 };523 }524525 function fulfill(): void {526 let resolvedValue = requireModule(serverReference) as any;527528 if (metaData.bound) {529 // This promise is coming from us and should have initialized by now.530 const promiseValue = (metaData.bound as any).value;531 const boundArgs: Array<any> = isArray(promiseValue)532 ? promiseValue.slice(0)533 : [];534 if (boundArgs.length > MAX_BOUND_ARGS) {535 reject(536 new Error(537 'Server Function has too many bound arguments. Received ' +538 boundArgs.length +539 ' but the limit is ' +540 MAX_BOUND_ARGS +541 '.',542 ),543 );544 return;545 }546 boundArgs.unshift(null); // this547 resolvedValue = resolvedValue.bind.apply(resolvedValue, boundArgs);548 }549550 // Resolve the cached promise so subsequent references can use the value.551 const resolveListeners = blockedPromise.value;552 const initializedPromise: InitializedChunk<T> = blockedPromise as any;553 initializedPromise.status = INITIALIZED;554 initializedPromise.value = resolvedValue;555 initializedPromise.reason = null;556 if (resolveListeners !== null) {557 // Notify any resolve listeners that were added via .then() from558 // subsequent loadServerReference calls for the same reference.559 wakeChunk(response, resolveListeners, resolvedValue, initializedPromise);560 }561562 resolveReference(response, handler, parentObject, key, resolvedValue);563 }564565 function reject(error: mixed): void {566 // Mark the cached promise as errored so subsequent references fail too.567 const rejectListeners = blockedPromise.reason;568 const erroredPromise: ErroredChunk<T> = blockedPromise as any;569 erroredPromise.status = ERRORED;570 erroredPromise.value = null;571 erroredPromise.reason = error;572 if (rejectListeners !== null) {573 // Notify any reject listeners that were added via .then() from subsequent574 // loadServerReference calls for the same reference.575 rejectChunk(response, rejectListeners, error);576 }577578 rejectReference(response, handler, error);579 }580581 serverReferencePromise.then(fulfill, reject);582583 // Return a place holder value for now.584 return null as any;585}586587function reviveModel(588 response: Response,589 parentObj: any,590 parentKey: string,591 value: JSONValue,592 reference: void | string,593 arrayRoot: null | NestedArrayContext,594): any {595 if (typeof value === 'string') {596 // We can't use .bind here because we need the "this" value.597 return parseModelString(598 response,599 parentObj,600 parentKey,601 value,602 reference,603 arrayRoot,604 );605 }606 if (typeof value === 'object' && value !== null) {607 if (608 reference !== undefined &&609 response._temporaryReferences !== undefined610 ) {611 // Store this object's reference in case it's returned later.612 registerTemporaryReference(613 response._temporaryReferences,614 value,615 reference,616 );617 }618 if (isArray(value)) {619 let childContext: NestedArrayContext;620 if (arrayRoot === null) {621 childContext = {622 count: 0,623 fork: false,624 } as NestedArrayContext;625 response._rootArrayContexts.set(value, childContext);626 } else {627 childContext = arrayRoot;628 }629 if (value.length > 1) {630 childContext.fork = true;631 }632 bumpArrayCount(633 childContext,634 // Number of commas + square brackets635 // value.length - 1 + 2636 value.length + 1,637 response,638 );639 for (let i = 0; i < value.length; i++) {640 const childRef =641 reference !== undefined ? reference + ':' + i : undefined;642 // $FlowFixMe[cannot-write]643 value[i] = reviveModel(644 response,645 value,646 '' + i,647 value[i],648 childRef,649 childContext,650 );651 }652 } else {653 for (const key in value) {654 if (hasOwnProperty.call(value, key)) {655 if (key === __PROTO__) {656 // $FlowFixMe[cannot-write]657 delete value[key];658 continue;659 }660 const childRef =661 reference !== undefined && key.indexOf(':') === -1662 ? reference + ':' + key663 : undefined;664 const newValue = reviveModel(665 response,666 value,667 key,668 value[key],669 childRef,670 null, // The array context resets when we're entering a non-array671 );672 if (newValue !== undefined) {673 // $FlowFixMe[cannot-write]674 value[key] = newValue;675 } else {676 // $FlowFixMe[cannot-write]677 delete value[key];678 }679 }680 }681 }682 }683 return value;684}685686type NestedArrayContext = {687 // Keeps track of how many slots, bytes or characters are in nested arrays/strings/typed arrays.688 count: number,689 // A single child is itself not harmful. There needs to be at least one parent array with more690 // than one child.691 fork: boolean,692};693694function bumpArrayCount(695 arrayContext: NestedArrayContext,696 slots: number,697 response: Response,698): void {699 const newCount = (arrayContext.count += slots);700 if (newCount > response._arraySizeLimit && arrayContext.fork) {701 throw new Error(702 'Maximum array nesting exceeded. Large nested arrays can be dangerous. Try adding intermediate objects.',703 );704 }705}706707type InitializationReference = {708 handler: InitializationHandler,709 parentObject: Object,710 key: string,711 map: (712 response: Response,713 model: any,714 parentObject: Object,715 key: string,716 ) => any,717 path: Array<string>,718 arrayRoot: null | NestedArrayContext,719};720type InitializationHandler = {721 chunk: null | BlockedChunk<any>,722 value: any,723 // TODO: Split type to make it impossible to treat a thrown value as NestedArrayContext.724 // thrown value if errored, otherwise array context725 reason: mixed | NestedArrayContext,726 deps: number,727 errored: boolean,728};729let initializingHandler: null | InitializationHandler = null;730731function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {732 const prevHandler = initializingHandler;733 initializingHandler = null;734735 const {[RESPONSE_SYMBOL]: response, id} = chunk.reason;736737 const rootReference = id === -1 ? undefined : id.toString(16);738739 const resolvedModel = chunk.value;740741 // We go to the BLOCKED state until we've fully resolved this.742 // We do this before parsing in case we try to initialize the same chunk743 // while parsing the model. Such as in a cyclic reference.744 const cyclicChunk: BlockedChunk<T> = chunk as any;745 cyclicChunk.status = BLOCKED;746 cyclicChunk.value = null;747 cyclicChunk.reason = null;748749 try {750 const rawModel = JSON.parse(resolvedModel);751752 // The root might not be an array but if it is we want to track the count of entries.753 const arrayRoot: NestedArrayContext = {754 count: 0,755 fork: false,756 };757758 const value: T = reviveModel(759 response,760 {'': rawModel},761 '',762 rawModel,763 rootReference,764 arrayRoot,765 );766767 // Invoke any listeners added while resolving this model. I.e. cyclic768 // references. This may or may not fully resolve the model depending on769 // if they were blocked.770 const resolveListeners = cyclicChunk.value;771 if (resolveListeners !== null) {772 cyclicChunk.value = null;773 cyclicChunk.reason = null;774 for (let i = 0; i < resolveListeners.length; i++) {775 const listener = resolveListeners[i];776 if (typeof listener === 'function') {777 listener(value);778 } else {779 fulfillReference(response, listener, value, arrayRoot);780 }781 }782 }783 if (initializingHandler !== null) {784 if (initializingHandler.errored) {785 throw initializingHandler.reason;786 }787 if (initializingHandler.deps > 0) {788 // We discovered new dependencies on modules that are not yet resolved.789 // We have to keep the BLOCKED state until they're resolved.790 initializingHandler.value = value;791 initializingHandler.reason = arrayRoot;792 initializingHandler.chunk = cyclicChunk;793 return;794 }795 }796 const initializedChunk: InitializedChunk<T> = chunk as any;797 initializedChunk.status = INITIALIZED;798 initializedChunk.value = value;799 initializedChunk.reason = arrayRoot;800 } catch (error) {801 const erroredChunk: ErroredChunk<T> = chunk as any;802 erroredChunk.status = ERRORED;803 erroredChunk.reason = error;804 } finally {805 initializingHandler = prevHandler;806 }807}808809// Report that any missing chunks in the model is now going to throw this810// error upon read. Also notify any pending promises.811export function reportGlobalError(response: Response, error: Error): void {812 response._closed = true;813 response._closedReason = error;814 response._chunks.forEach(chunk => {815 // If this chunk was already resolved or errored, it won't816 // trigger an error but if it wasn't then we need to817 // because we won't be getting any new data to resolve it.818 if (chunk.status === PENDING) {819 triggerErrorOnChunk(response, chunk, error);820 } else if (chunk.status === INITIALIZED) {821 const initializedChunk:822 | InitializedChunk<any>823 | InitializedStreamChunk<any> = chunk as any;824 if (initializedChunk.reason !== null) {825 const maybeController = initializedChunk.reason;826 // $FlowFixMe[method-unbinding] Just doing a typeof check827 if (typeof maybeController.error === 'function') {828 maybeController.error(error);829 }830 }831 }832 });833}834835function getChunk(response: Response, id: number): SomeChunk<any> {836 const chunks = response._chunks;837 let chunk = chunks.get(id);838 if (!chunk) {839 const prefix = response._prefix;840 const key = prefix + id;841 // Check if we have this field in the backing store already.842 const backingEntry = getBackingEntry(response._formData, key);843 if (typeof backingEntry === 'string') {844 chunk = createResolvedModelChunk(response, backingEntry, id);845 } else if (response._closed) {846 // We have already errored the response and we're not going to get847 // anything more streaming in so this will immediately error.848 chunk = createErroredChunk(response, response._closedReason);849 } else {850 // We're still waiting on this entry to stream in.851 chunk = createPendingChunk(response);852 }853 chunks.set(id, chunk);854 }855 return chunk;856}857858function fulfillReference(859 response: Response,860 reference: InitializationReference,861 value: any,862 arrayRoot: null | NestedArrayContext,863): void {864 const {handler, parentObject, key, map, path} = reference;865866 let resolvedValue;867 try {868 let localLength: number = 0;869 const rootArrayContexts = response._rootArrayContexts;870 for (let i = 1; i < path.length; i++) {871 // The server doesn't have any lazy references so we don't expect to go through a Promise.872 const name = path[i];873 if (874 typeof value === 'object' &&875 value !== null &&876 (getPrototypeOf(value) === ObjectPrototype ||877 getPrototypeOf(value) === ArrayPrototype) &&878 hasOwnProperty.call(value, name)879 ) {880 value = value[name];881 if (isArray(value)) {882 localLength = 0;883 arrayRoot = rootArrayContexts.get(value) || arrayRoot;884 } else {885 arrayRoot = null;886 if (typeof value === 'string') {887 localLength = value.length;888 } else if (typeof value === 'bigint') {889 // Estimate the length to avoid expensive toString() calls on large890 // BigInt values. If the value is too large, we get Infinity, which891 // will trigger the array size limit error.892 // eslint-disable-next-line react-internal/no-primitive-constructors893 const n = Math.abs(Number(value));894 if (n === 0) {895 localLength = 1;896 } else {897 localLength = Math.floor(Math.log10(n)) + 1;898 }899 } else if (ArrayBuffer.isView(value)) {900 localLength = value.byteLength;901 } else {902 localLength = 0;903 }904 }905 } else {906 throw new Error('Invalid reference.');907 }908 }909910 resolvedValue = map(response, value, parentObject, key);911912 // Add any array counts to the reference's array root. The value that we're913 // resolving might have deep nesting that we need to resolve.914 const referenceArrayRoot = reference.arrayRoot;915 if (referenceArrayRoot !== null) {916 if (arrayRoot !== null) {917 if (arrayRoot.fork) {918 referenceArrayRoot.fork = true;919 }920 bumpArrayCount(referenceArrayRoot, arrayRoot.count, response);921 } else if (localLength > 0) {922 bumpArrayCount(referenceArrayRoot, localLength, response);923 }924 }925 } catch (error) {926 rejectReference(response, handler, error);927 return;928 }929930 // There are no Elements or Debug Info to transfer here.931932 resolveReference(response, handler, parentObject, key, resolvedValue);933}934935function resolveReference(936 response: Response,937 handler: InitializationHandler,938 parentObject: Object,939 key: string,940 resolvedValue: mixed,941): void {942 if (key !== __PROTO__) {943 parentObject[key] = resolvedValue;944 }945946 // If this is the root object for a model reference, where `handler.value`947 // is a stale `null`, the resolved value can be used directly.948 if (key === '' && handler.value === null) {949 handler.value = resolvedValue;950 }951952 handler.deps--;953954 if (handler.deps === 0) {955 const chunk = handler.chunk;956 if (chunk === null || chunk.status !== BLOCKED) {957 return;958 }959 const resolveListeners = chunk.value;960 const initializedChunk: InitializedChunk<any> = chunk as any;961 initializedChunk.status = INITIALIZED;962 initializedChunk.value = handler.value;963 initializedChunk.reason =964 // $FlowFixMe[incompatible-type] Assuming handler.errored is false.965 handler.reason;966 if (resolveListeners !== null) {967 wakeChunk(response, resolveListeners, handler.value, initializedChunk);968 }969 }970}971972function rejectReference(973 response: Response,974 handler: InitializationHandler,975 error: mixed,976): void {977 if (handler.errored) {978 // We've already errored. We could instead build up an AggregateError979 // but if there are multiple errors we just take the first one like980 // Promise.all.981 return;982 }983 handler.errored = true;984 handler.value = null;985 handler.reason = error;986 const chunk = handler.chunk;987 if (chunk === null || chunk.status !== BLOCKED) {988 return;989 }990 // There's no debug info to forward in this direction.991 triggerErrorOnChunk(response, chunk, error);992}993994function waitForReference<T>(995 response: Response,996 referencedChunk: BlockedChunk<T>,997 parentObject: Object,998 key: string,999 arrayRoot: null | NestedArrayContext,1000 map: (response: Response, model: any, parentObject: Object, key: string) => T,1001 path: Array<string>,1002): T {1003 let handler: InitializationHandler;1004 if (initializingHandler) {1005 handler = initializingHandler;1006 handler.deps++;1007 } else {1008 handler = initializingHandler = {1009 chunk: null,1010 value: null,1011 reason: null,1012 deps: 1,1013 errored: false,1014 };1015 }10161017 const reference: InitializationReference = {1018 handler,1019 parentObject,1020 key,1021 map,1022 path,1023 arrayRoot,1024 };10251026 // Add "listener".1027 if (referencedChunk.value === null) {1028 referencedChunk.value = [reference];1029 } else {1030 referencedChunk.value.push(reference);1031 }1032 if (referencedChunk.reason === null) {1033 referencedChunk.reason = [reference];1034 } else {1035 referencedChunk.reason.push(reference);1036 }10371038 // Return a place holder value for now.1039 return null as any;1040}10411042function getOutlinedModel<T>(1043 response: Response,1044 reference: string,1045 parentObject: Object,1046 key: string,1047 referenceArrayRoot: null | NestedArrayContext,1048 map: (response: Response, model: any, parentObject: Object, key: string) => T,1049): T {1050 const path = reference.split(':');1051 const id = parseInt(path[0], 16);1052 let chunk = getChunk(response, id);1053 switch (chunk.status) {1054 case RESOLVED_MODEL:1055 initializeModelChunk(chunk);1056 // $FlowFixMe[incompatible-type] We just initialized this chunk so it can't be a ResolvedModelChunk anymore.1057 chunk = chunk as Exclude<SomeChunk<T>, ResolvedModelChunk<T>>;1058 break;1059 }1060 // The status might have changed after initialization.1061 switch (chunk.status) {1062 case INITIALIZED:1063 let value = chunk.value;1064 const arrayRootOrController:1065 | null1066 | NestedArrayContext1067 | FlightStreamController = chunk.reason;1068 if (arrayRootOrController !== null && 'error' in arrayRootOrController) {1069 throw new Error(1070 'Expected an initialized chunk but got an initialized stream chunk instead. ' +1071 'This payload may have been submitted by an older version of React.',1072 );1073 }1074 let arrayRoot = arrayRootOrController;10751076 let localLength: number = 0;1077 const rootArrayContexts = response._rootArrayContexts;1078 for (let i = 1; i < path.length; i++) {1079 const name = path[i];1080 if (1081 typeof value === 'object' &&1082 // $FlowFixMe[invalid-compare] This check is still needed at runtime.1083 value !== null &&1084 (getPrototypeOf(value) === ObjectPrototype ||1085 getPrototypeOf(value) === ArrayPrototype) &&1086 hasOwnProperty.call(value, name)1087 ) {1088 value = value[name];1089 if (isArray(value)) {1090 localLength = 0;1091 arrayRoot =1092 rootArrayContexts.get(1093 // $FlowFixMe[incompatible-type] Our `isArray` typing can't narrow `mixed`1094 value as $ReadOnlyArray<mixed>,1095 ) || arrayRoot;1096 } else {1097 arrayRoot = null;1098 if (typeof value === 'string') {1099 localLength = value.length;1100 } else if (typeof value === 'bigint') {1101 // Estimate the length to avoid expensive toString() calls on large1102 // BigInt values. If the value is too large, we get Infinity, which1103 // will trigger the array size limit error.1104 // eslint-disable-next-line react-internal/no-primitive-constructors1105 const n = Math.abs(Number(value));1106 if (n === 0) {1107 localLength = 1;1108 } else {1109 localLength = Math.floor(Math.log10(n)) + 1;1110 }1111 } else if (ArrayBuffer.isView(value)) {1112 localLength = value.byteLength;1113 } else {1114 localLength = 0;1115 }1116 }1117 } else {1118 throw new Error('Invalid reference.');1119 }1120 }1121 const chunkValue = map(response, value, parentObject, key);11221123 // Add any array counts to the reference's array root. The value that we're1124 // resolving might have deep nesting that we need to resolve.1125 if (referenceArrayRoot !== null) {1126 if (arrayRoot !== null) {1127 if (arrayRoot.fork) {1128 referenceArrayRoot.fork = true;1129 }1130 bumpArrayCount(referenceArrayRoot, arrayRoot.count, response);1131 } else if (localLength > 0) {1132 bumpArrayCount(referenceArrayRoot, localLength, response);1133 }1134 }1135 // There's no Element nor Debug Info in the ReplyServer so we don't have to check those here.1136 return chunkValue;1137 case BLOCKED:1138 return waitForReference(1139 response,1140 chunk,1141 parentObject,1142 key,1143 referenceArrayRoot,1144 map,1145 path,1146 );1147 case PENDING:1148 // If we don't have the referenced chunk yet, then this must be a forward reference,1149 // which is not allowed.1150 throw new Error('Invalid forward reference.');1151 default:1152 // This is an error. Instead of erroring directly, we're going to encode this on1153 // an initialization handler.1154 if (initializingHandler) {1155 initializingHandler.errored = true;1156 initializingHandler.value = null;1157 initializingHandler.reason = chunk.reason;1158 } else {1159 initializingHandler = {1160 chunk: null,1161 value: null,1162 reason: chunk.reason,1163 deps: 0,1164 errored: true,1165 };1166 }1167 // Placeholder1168 return null as any;1169 }1170}11711172function createMap(1173 response: Response,1174 model: Array<[any, any]>,1175): Map<any, any> {1176 if (!isArray(model)) {1177 throw new Error('Invalid Map initializer.');1178 }1179 if ((model as any).$$consumed === true) {1180 throw new Error('Already initialized Map.');1181 }1182 // This needs to come first to prevent the model from being consumed again in case of a cyclic reference.1183 (model as any).$$consumed = true;1184 const map = new Map(model);1185 return map;1186}11871188function createSet(response: Response, model: Array<any>): Set<any> {1189 if (!isArray(model)) {1190 throw new Error('Invalid Set initializer.');1191 }1192 if ((model as any).$$consumed === true) {1193 throw new Error('Already initialized Set.');1194 }1195 // This needs to come first to prevent the model from being consumed again in case of a cyclic reference.1196 (model as any).$$consumed = true;1197 const set = new Set(model);1198 return set;1199}12001201function extractIterator(response: Response, model: Array<any>): Iterator<any> {1202 if (!isArray(model)) {1203 throw new Error('Invalid Iterator initializer.');1204 }1205 if ((model as any).$$consumed === true) {1206 throw new Error('Already initialized Iterator.');1207 }1208 // This needs to come first to prevent the model from being consumed again in case of a cyclic reference.1209 (model as any).$$consumed = true;1210 // $FlowFixMe[incompatible-use]: This uses raw Symbols because we're extracting from a native array.1211 const iterator = model[Symbol.iterator]();1212 return iterator;1213}12141215function createModel(1216 response: Response,1217 model: any,1218 parentObject: Object,1219 key: string,1220): any {1221 if (key === 'then' && typeof model === 'function') {1222 // This should never happen because we always serialize objects with then-functions1223 // as "thenable" which reduces to ReactPromise with no other fields.1224 return null;1225 }1226 return model;1227}12281229function parseTypedArray<T: $ArrayBufferView | ArrayBuffer>(1230 response: Response,1231 reference: string,1232 constructor: any,1233 bytesPerElement: number,1234 parentObject: Object,1235 parentKey: string,1236 referenceArrayRoot: null | NestedArrayContext,1237): null {1238 const id = parseInt(reference.slice(2), 16);1239 const prefix = response._prefix;1240 const key = prefix + id;1241 const chunks = response._chunks;1242 if (chunks.has(id)) {1243 throw new Error('Already initialized typed array.');1244 }1245 chunks.set(1246 id,1247 // We don't need to put the actual Blob in the chunk,1248 // because it shouldn't be accessed by anything else.1249 createErroredChunk(response, new Error('Already initialized typed array.')),1250 );12511252 // We should have this backingEntry in the store already because we emitted1253 // it before referencing it. It should be a Blob.1254 const backingEntry: Blob = getBackingEntry(response._formData, key) as any;12551256 const promise: Promise<ArrayBuffer> = backingEntry.arrayBuffer();12571258 // Since loading the buffer is an async operation we'll be blocking the parent1259 // chunk.12601261 let handler: InitializationHandler;1262 if (initializingHandler) {1263 handler = initializingHandler;1264 handler.deps++;1265 } else {1266 handler = initializingHandler = {1267 chunk: null,1268 value: null,1269 reason: null,1270 deps: 1,1271 errored: false,1272 };1273 }12741275 function fulfill(buffer: ArrayBuffer): void {1276 try {1277 if (referenceArrayRoot !== null) {1278 bumpArrayCount(referenceArrayRoot, buffer.byteLength, response);1279 }12801281 const resolvedValue: T =1282 constructor === ArrayBuffer1283 ? (buffer as any)1284 : (new constructor(buffer) as any);12851286 if (key !== __PROTO__) {1287 parentObject[parentKey] = resolvedValue;1288 }12891290 // If this is the root object for a model reference, where `handler.value`1291 // is a stale `null`, the resolved value can be used directly.1292 if (parentKey === '' && handler.value === null) {1293 handler.value = resolvedValue;1294 }1295 } catch (x) {1296 reject(x);1297 return;1298 }12991300 handler.deps--;13011302 if (handler.deps === 0) {1303 const chunk = handler.chunk;1304 if (chunk === null || chunk.status !== BLOCKED) {1305 return;1306 }1307 const resolveListeners = chunk.value;1308 const initializedChunk: InitializedChunk<T> = chunk as any;1309 initializedChunk.status = INITIALIZED;1310 initializedChunk.value = handler.value;1311 // We don't keep an array count for this since it won't be referenced again.1312 // In fact, we don't really need to store this chunk at all.1313 initializedChunk.reason = null;1314 if (resolveListeners !== null) {1315 wakeChunk(response, resolveListeners, handler.value, initializedChunk);1316 }1317 }1318 }13191320 function reject(error: mixed): void {1321 if (handler.errored) {1322 // We've already errored. We could instead build up an AggregateError1323 // but if there are multiple errors we just take the first one like1324 // Promise.all.1325 return;1326 }1327 handler.errored = true;1328 handler.value = null;1329 handler.reason = error;1330 const chunk = handler.chunk;1331 if (chunk === null || chunk.status !== BLOCKED) {1332 return;1333 }1334 triggerErrorOnChunk(response, chunk, error);1335 }13361337 promise.then(fulfill, reject);13381339 return null;1340}13411342function resolveStream<T: ReadableStream | $AsyncIterable<any, any, void>>(1343 response: Response,1344 id: number,1345 stream: T,1346 controller: FlightStreamController,1347): void {1348 const chunks = response._chunks;1349 const chunk = createInitializedStreamChunk(response, stream, controller);1350 chunks.set(id, chunk);13511352 const prefix = response._prefix;1353 const key = prefix + id;1354 const existingEntries = getAllBackingEntries(response._formData, key);1355 for (let i = 0; i < existingEntries.length; i++) {1356 const value = existingEntries[i];1357 if (typeof value === 'string') {1358 if (value[0] === 'C') {1359 controller.close(value === 'C' ? '"$undefined"' : value.slice(1));1360 } else {1361 controller.enqueueModel(value);1362 }1363 }1364 }1365}13661367function parseReadableStream<T>(1368 response: Response,1369 reference: string,1370 type: void | 'bytes',1371 parentObject: Object,1372 parentKey: string,1373): ReadableStream {1374 const id = parseInt(reference.slice(2), 16);1375 const chunks = response._chunks;1376 if (chunks.has(id)) {1377 throw new Error('Already initialized stream.');1378 }13791380 let controller: ReadableStreamController = null as any;1381 let closed = false;1382 const stream = new ReadableStream({1383 type: type,1384 start(c) {1385 controller = c;1386 },1387 });1388 let previousBlockedChunk: SomeChunk<T> | null = null;1389 function enqueue(value: T): void {1390 if (type === 'bytes' && !ArrayBuffer.isView(value)) {1391 flightController.error(new Error('Invalid data for bytes stream.'));1392 return;1393 }1394 controller.enqueue(value);1395 }1396 const flightController = {1397 enqueueModel(json: string): void {1398 if (previousBlockedChunk === null) {1399 // If we're not blocked on any other chunks, we can try to eagerly initialize1400 // this as a fast-path to avoid awaiting them.1401 const chunk: ResolvedModelChunk<T> = createResolvedModelChunk(1402 response,1403 json,1404 -1,1405 );1406 initializeModelChunk(chunk);1407 const initializedChunk: SomeChunk<T> = chunk;1408 if (initializedChunk.status === INITIALIZED) {1409 enqueue(initializedChunk.value);1410 } else {1411 chunk.then(enqueue, flightController.error);1412 previousBlockedChunk = chunk;1413 }1414 } else {1415 // We're still waiting on a previous chunk so we can't enqueue quite yet.1416 const blockedChunk = previousBlockedChunk;1417 const chunk: SomeChunk<T> = createPendingChunk(response);1418 chunk.then(enqueue, flightController.error);1419 previousBlockedChunk = chunk;1420 blockedChunk.then(function () {1421 if (previousBlockedChunk === chunk) {1422 // We were still the last chunk so we can now clear the queue and return1423 // to synchronous emitting.1424 previousBlockedChunk = null;1425 }1426 resolveModelChunk(response, chunk, json, -1);1427 });1428 }1429 },1430 close(json: string): void {1431 if (closed) {1432 return;1433 }1434 closed = true;1435 if (previousBlockedChunk === null) {1436 controller.close();1437 } else {1438 const blockedChunk = previousBlockedChunk;1439 // We shouldn't get any more enqueues after this so we can set it back to null.1440 previousBlockedChunk = null;1441 blockedChunk.then(() => controller.close());1442 }1443 },1444 error(error: mixed): void {1445 if (closed) {1446 return;1447 }1448 closed = true;1449 if (previousBlockedChunk === null) {1450 // $FlowFixMe[incompatible-type]1451 controller.error(error);1452 } else {1453 const blockedChunk = previousBlockedChunk;1454 // We shouldn't get any more enqueues after this so we can set it back to null.1455 previousBlockedChunk = null;1456 blockedChunk.then(() => controller.error(error as any));1457 }1458 },1459 };1460 resolveStream(response, id, stream, flightController);1461 return stream;1462}14631464function FlightIterator(1465 this: {next: (arg: void) => SomeChunk<IteratorResult<any, any>>, ...},1466 next: (arg: void) => SomeChunk<IteratorResult<any, any>>,1467) {1468 this.next = next;1469 // TODO: Add return/throw as options for aborting.1470}1471// TODO: The iterator could inherit the AsyncIterator prototype which is not exposed as1472// a global but exists as a prototype of an AsyncGenerator. However, it's not needed1473// to satisfy the iterable protocol.1474FlightIterator.prototype = {} as any;1475FlightIterator.prototype[ASYNC_ITERATOR] = function asyncIterator(1476 this: $AsyncIterator<any, any, void>,1477) {1478 // Self referencing iterator.1479 return this;1480};14811482function parseAsyncIterable<T>(1483 response: Response,1484 reference: string,1485 iterator: boolean,1486 parentObject: Object,1487 parentKey: string,1488): $AsyncIterable<T, T, void> | $AsyncIterator<T, T, void> {1489 const id = parseInt(reference.slice(2), 16);1490 const chunks = response._chunks;1491 if (chunks.has(id)) {1492 throw new Error('Already initialized stream.');1493 }14941495 const buffer: Array<SomeChunk<IteratorResult<T, T>>> = [];1496 let closed = false;1497 let nextWriteIndex = 0;1498 const flightController = {1499 enqueueModel(value: string): void {1500 if (nextWriteIndex === buffer.length) {1501 buffer[nextWriteIndex] = createResolvedIteratorResultChunk(1502 response,1503 value,1504 false,1505 );1506 } else {1507 resolveIteratorResultChunk(1508 response,1509 buffer[nextWriteIndex],1510 value,1511 false,1512 );1513 }1514 nextWriteIndex++;1515 },1516 close(value: string): void {1517 if (closed) {1518 return;1519 }1520 closed = true;1521 if (nextWriteIndex === buffer.length) {1522 buffer[nextWriteIndex] = createResolvedIteratorResultChunk(1523 response,1524 value,1525 true,1526 );1527 } else {1528 resolveIteratorResultChunk(1529 response,1530 buffer[nextWriteIndex],1531 value,1532 true,1533 );1534 }1535 nextWriteIndex++;1536 while (nextWriteIndex < buffer.length) {1537 // In generators, any extra reads from the iterator have the value undefined.1538 resolveIteratorResultChunk(1539 response,1540 buffer[nextWriteIndex++],1541 '"$undefined"',1542 true,1543 );1544 }1545 },1546 error(error: Error): void {1547 if (closed) {1548 return;1549 }1550 closed = true;1551 if (nextWriteIndex === buffer.length) {1552 buffer[nextWriteIndex] =1553 createPendingChunk<IteratorResult<T, T>>(response);1554 }1555 while (nextWriteIndex < buffer.length) {1556 triggerErrorOnChunk(response, buffer[nextWriteIndex++], error);1557 }1558 },1559 };1560 const iterable: $AsyncIterable<T, T, void> = {1561 [ASYNC_ITERATOR](): $AsyncIterator<T, T, void> {1562 let nextReadIndex = 0;1563 // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors1564 return new FlightIterator((arg: void) => {1565 if (arg !== undefined) {1566 throw new Error(1567 'Values cannot be passed to next() of AsyncIterables passed to Client Components.',1568 );1569 }1570 if (nextReadIndex === buffer.length) {1571 if (closed) {1572 // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors1573 return new ReactPromise(1574 INITIALIZED,1575 {done: true, value: undefined},1576 null,1577 );1578 }1579 buffer[nextReadIndex] =1580 createPendingChunk<IteratorResult<T, T>>(response);1581 }1582 return buffer[nextReadIndex++];1583 });1584 },1585 };1586 // TODO: If it's a single shot iterator we can optimize memory by cleaning up the buffer after1587 // reading through the end, but currently we favor code size over this optimization.1588 const stream = iterator ? iterable[ASYNC_ITERATOR]() : iterable;1589 resolveStream(response, id, stream, flightController);1590 return stream;1591}15921593function parseModelString(1594 response: Response,1595 obj: Object,1596 key: string,1597 value: string,1598 reference: void | string,1599 arrayRoot: null | NestedArrayContext,1600): any {1601 if (value[0] === '$') {1602 switch (value[1]) {1603 case '$': {1604 // This was an escaped string value.1605 if (arrayRoot !== null) {1606 bumpArrayCount(arrayRoot, value.length - 1, response);1607 }1608 return value.slice(1);1609 }1610 case '@': {1611 // Promise1612 const id = parseInt(value.slice(2), 16);1613 const chunk = getChunk(response, id);1614 return chunk;1615 }1616 case 'h': {1617 // Server Reference1618 const ref = value.slice(2);1619 return getOutlinedModel(1620 response,1621 ref,1622 obj,1623 key,1624 null,1625 loadServerReference,1626 );1627 }1628 case 'T': {1629 // Temporary Reference1630 if (1631 reference === undefined ||1632 response._temporaryReferences === undefined1633 ) {1634 throw new Error(1635 'Could not reference an opaque temporary reference. ' +1636 'This is likely due to misconfiguring the temporaryReferences options ' +1637 'on the server.',1638 );1639 }1640 return createTemporaryReference(1641 response._temporaryReferences,1642 reference,1643 );1644 }1645 case 'Q': {1646 // Map1647 const ref = value.slice(2);1648 return getOutlinedModel(response, ref, obj, key, null, createMap);1649 }1650 case 'W': {1651 // Set1652 const ref = value.slice(2);1653 return getOutlinedModel(response, ref, obj, key, null, createSet);1654 }1655 case 'K': {1656 // FormData1657 const stringId = value.slice(2);16581659 const responsePrefix = response._prefix;1660 // Use the special marker from the Client to distinguish keys that should1661 // be consumed by referenced FormData.1662 const anyFormPrefix = responsePrefix + '_';1663 const formPrefix = anyFormPrefix + stringId + '_';16641665 const data = new FormData();1666 const backingFormData = response._formData;1667 // We're still transpiling for-of loops, so we have to use the iterator directly instead of a for-of loop.1668 while (true) {1669 const formDataKey = peekBackingEntry(backingFormData);1670 if (formDataKey === undefined) {1671 break;1672 }1673 if (formDataKey.startsWith(formPrefix)) {1674 const referencedFormDataValue = getAllBackingEntries(1675 backingFormData,1676 formDataKey,1677 );1678 const referencedFormDataKey = formDataKey.slice(formPrefix.length);1679 for (let i = 0; i < referencedFormDataValue.length; i++) {1680 // $FlowFixMe[incompatible-type]1681 data.append(referencedFormDataKey, referencedFormDataValue[i]);1682 }1683 consumeBackingEntry(backingFormData, formDataKey);1684 } else if (formDataKey.startsWith(anyFormPrefix)) {1685 // The FormData values are continuous and before the FormData reference.1686 // If we see something that doesn't look like a value for a referenced1687 // FormData, we can assume we're past the values for this FormData1688 // reference and stop iterating.1689 break;1690 } else {1691 // Either an outlined value or something not owned by this Reply.1692 advanceBackingEntryIterator(backingFormData);1693 }1694 }1695 return data;1696 }1697 case 'i': {1698 // Iterator1699 const ref = value.slice(2);1700 return getOutlinedModel(response, ref, obj, key, null, extractIterator);1701 }1702 case 'I': {1703 // $Infinity1704 return Infinity;1705 }1706 case '-': {1707 // $-0 or $-Infinity1708 if (value === '$-0') {1709 return -0;1710 } else {1711 return -Infinity;1712 }1713 }1714 case 'N': {1715 // $NaN1716 return NaN;1717 }1718 case 'u': {1719 // matches "$undefined"1720 // Special encoding for `undefined` which can't be serialized as JSON otherwise.1721 return undefined;1722 }1723 case 'D': {1724 // Date1725 return new Date(Date.parse(value.slice(2)));1726 }1727 case 'n': {1728 // BigInt1729 const bigIntStr = value.slice(2);1730 if (bigIntStr.length > MAX_BIGINT_DIGITS) {1731 throw new Error(1732 'BigInt is too large. Received ' +1733 bigIntStr.length +1734 ' digits but the limit is ' +1735 MAX_BIGINT_DIGITS +1736 '.',1737 );1738 }1739 if (arrayRoot !== null) {1740 bumpArrayCount(arrayRoot, bigIntStr.length, response);1741 }1742 return BigInt(bigIntStr);1743 }1744 case 'A':1745 return parseTypedArray(1746 response,1747 value,1748 ArrayBuffer,1749 1,1750 obj,1751 key,1752 arrayRoot,1753 );1754 case 'O':1755 return parseTypedArray(1756 response,1757 value,1758 Int8Array,1759 1,1760 obj,1761 key,1762 arrayRoot,1763 );1764 case 'o':1765 return parseTypedArray(1766 response,1767 value,1768 Uint8Array,1769 1,1770 obj,1771 key,1772 arrayRoot,1773 );1774 case 'U':1775 return parseTypedArray(1776 response,1777 value,1778 Uint8ClampedArray,1779 1,1780 obj,1781 key,1782 arrayRoot,1783 );1784 case 'S':1785 return parseTypedArray(1786 response,1787 value,1788 Int16Array,1789 2,1790 obj,1791 key,1792 arrayRoot,1793 );1794 case 's':1795 return parseTypedArray(1796 response,1797 value,1798 Uint16Array,1799 2,1800 obj,1801 key,1802 arrayRoot,1803 );1804 case 'L':1805 return parseTypedArray(1806 response,1807 value,1808 Int32Array,1809 4,1810 obj,1811 key,1812 arrayRoot,1813 );1814 case 'l':1815 return parseTypedArray(1816 response,1817 value,1818 Uint32Array,1819 4,1820 obj,1821 key,1822 arrayRoot,1823 );1824 case 'G':1825 return parseTypedArray(1826 response,1827 value,1828 Float32Array,1829 4,1830 obj,1831 key,1832 arrayRoot,1833 );1834 case 'g':1835 return parseTypedArray(1836 response,1837 value,1838 Float64Array,1839 8,1840 obj,1841 key,1842 arrayRoot,1843 );1844 case 'M':1845 return parseTypedArray(1846 response,1847 value,1848 BigInt64Array,1849 8,1850 obj,1851 key,1852 arrayRoot,1853 );1854 case 'm':1855 return parseTypedArray(1856 response,1857 value,1858 BigUint64Array,1859 8,1860 obj,1861 key,1862 arrayRoot,1863 );1864 case 'V':1865 return parseTypedArray(1866 response,1867 value,1868 DataView,1869 1,1870 obj,1871 key,1872 arrayRoot,1873 );1874 case 'B': {1875 // Blob1876 const id = parseInt(value.slice(2), 16);1877 const prefix = response._prefix;1878 const blobKey = prefix + id;1879 // We should have this backingEntry in the store already because we emitted1880 // it before referencing it. It should be a Blob.1881 const backingEntry: Blob = getBackingEntry(1882 response._formData,1883 blobKey,1884 ) as any;1885 if (!(backingEntry instanceof Blob)) {1886 throw new Error('Referenced Blob is not a Blob.');1887 }1888 return backingEntry;1889 }1890 case 'R': {1891 return parseReadableStream(response, value, undefined, obj, key);1892 }1893 case 'r': {1894 return parseReadableStream(response, value, 'bytes', obj, key);1895 }1896 case 'X': {1897 return parseAsyncIterable(response, value, false, obj, key);1898 }1899 case 'x': {1900 return parseAsyncIterable(response, value, true, obj, key);1901 }1902 }1903 // We assume that anything else is a reference ID.1904 const ref = value.slice(1);1905 return getOutlinedModel(response, ref, obj, key, arrayRoot, createModel);1906 }1907 if (arrayRoot !== null) {1908 bumpArrayCount(arrayRoot, value.length, response);1909 }1910 return value;1911}19121913const DEFAULT_MAX_ARRAY_NESTING = 1000000;19141915// Limit BigInt size to prevent CPU exhaustion from parsing very large values.1916// 300 digits covers most practical use cases (even 512-bit integers need only1917// ~154 digits) and aligns with the implicit limit from the Number approximation1918// checks in fulfillReference and getOutlinedModel.1919const MAX_BIGINT_DIGITS = 300;19201921export const MAX_BOUND_ARGS = 1000;19221923export function createResponse(1924 bundlerConfig: ServerManifest,1925 formFieldPrefix: string,1926 temporaryReferences: void | TemporaryReferenceSet,1927 backingFormData?: FormData = new FormData(),1928 arraySizeLimit?: number = DEFAULT_MAX_ARRAY_NESTING,1929): Response {1930 const chunks: Map<number, SomeChunk<any>> = new Map();19311932 const response: Response = {1933 _bundlerConfig: bundlerConfig,1934 _prefix: formFieldPrefix,1935 _formData: createBackingFormData(backingFormData),1936 _chunks: chunks,1937 _closed: false,1938 _closedReason: null,1939 _temporaryReferences: temporaryReferences,1940 _rootArrayContexts: new WeakMap(),1941 _arraySizeLimit: arraySizeLimit,1942 };1943 return response;1944}19451946export function resolveField(1947 response: Response,1948 key: string,1949 value: string,1950): void {1951 // Add this field to the backing store.1952 appendBackingEntry(response._formData, key, value);1953 const prefix = response._prefix;1954 if (key.startsWith(prefix)) {1955 const chunks = response._chunks;1956 const id = +key.slice(prefix.length);1957 const chunk = chunks.get(id);1958 if (chunk) {1959 // We were waiting on this key so now we can resolve it.1960 resolveModelChunk(response, chunk, value, id);1961 }1962 }1963}19641965export function resolveFile(response: Response, key: string, file: File): void {1966 // Add this field to the backing store.1967 appendBackingEntry(response._formData, key, file);1968}19691970export opaque type FileHandle = {1971 chunks: Array<Uint8Array>,1972 filename: string,1973 mime: string,1974};19751976export function resolveFileInfo(1977 response: Response,1978 key: string,1979 filename: string,1980 mime: string,1981): FileHandle {1982 return {1983 chunks: [],1984 filename,1985 mime,1986 };1987}19881989export function resolveFileChunk(1990 response: Response,1991 handle: FileHandle,1992 chunk: Uint8Array,1993): void {1994 handle.chunks.push(chunk);1995}19961997export function resolveFileComplete(1998 response: Response,1999 key: string,2000 handle: FileHandle,
Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.