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 .then129ReactPromise.prototype.then = function <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};201202const ObjectPrototype = Object.prototype;203const ArrayPrototype = Array.prototype;204205export type Response = {206 _bundlerConfig: ServerManifest,207 _prefix: string,208 _formData: BackingFormData,209 _chunks: Map<number, SomeChunk<any>>,210 _closed: boolean,211 _closedReason: mixed,212 _temporaryReferences: void | TemporaryReferenceSet,213 _rootArrayContexts: WeakMap<$ReadOnlyArray<mixed>, NestedArrayContext>,214 _arraySizeLimit: number,215};216217export function getRoot<T>(response: Response): Thenable<T> {218 const chunk = getChunk(response, 0);219 return chunk as any;220}221222function createPendingChunk<T>(response: Response): PendingChunk<T> {223 // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors224 return new ReactPromise(PENDING, null, null);225}226227function wakeChunk<T>(228 response: Response,229 listeners: Array<InitializationReference | (T => mixed)>,230 value: T,231 chunk: InitializedChunk<T>,232): void {233 for (let i = 0; i < listeners.length; i++) {234 const listener = listeners[i];235 if (typeof listener === 'function') {236 listener(value);237 } else {238 fulfillReference(response, listener, value, chunk.reason);239 }240 }241}242243function rejectChunk(244 response: Response,245 listeners: Array<InitializationReference | (mixed => mixed)>,246 error: mixed,247): void {248 for (let i = 0; i < listeners.length; i++) {249 const listener = listeners[i];250 if (typeof listener === 'function') {251 listener(error);252 } else {253 rejectReference(response, listener.handler, error);254 }255 }256}257258function wakeChunkIfInitialized<T>(259 response: Response,260 chunk: SomeChunk<T>,261 resolveListeners: Array<InitializationReference | (T => mixed)>,262 rejectListeners: null | Array<InitializationReference | (mixed => mixed)>,263): void {264 switch (chunk.status) {265 case INITIALIZED:266 wakeChunk(response, resolveListeners, chunk.value, chunk);267 break;268 case BLOCKED:269 case PENDING:270 if (chunk.value) {271 for (let i = 0; i < resolveListeners.length; i++) {272 chunk.value.push(resolveListeners[i]);273 }274 } else {275 chunk.value = resolveListeners;276 }277278 if (chunk.reason) {279 if (rejectListeners) {280 for (let i = 0; i < rejectListeners.length; i++) {281 chunk.reason.push(rejectListeners[i]);282 }283 }284 } else {285 chunk.reason = rejectListeners;286 }287 break;288 case ERRORED:289 if (rejectListeners) {290 rejectChunk(response, rejectListeners, chunk.reason);291 }292 break;293 }294}295296function triggerErrorOnChunk<T>(297 response: Response,298 chunk: SomeChunk<T>,299 error: mixed,300): void {301 if (chunk.status !== PENDING && chunk.status !== BLOCKED) {302 // If we get more data to an already resolved ID, we assume that it's303 // a stream chunk since any other row shouldn't have more than one entry.304 const streamChunk: InitializedStreamChunk<any> = chunk as any;305 const controller = streamChunk.reason;306 // $FlowFixMe[incompatible-type]: The error method should accept mixed.307 controller.error(error);308 return;309 }310 const listeners = chunk.reason;311 const erroredChunk: ErroredChunk<T> = chunk as any;312 erroredChunk.status = ERRORED;313 erroredChunk.reason = error;314 if (listeners !== null) {315 rejectChunk(response, listeners, error);316 }317}318319function createResolvedModelChunk<T>(320 response: Response,321 value: string,322 id: number,323): ResolvedModelChunk<T> {324 // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors325 return new ReactPromise(RESOLVED_MODEL, value, {326 id,327 [RESPONSE_SYMBOL]: response,328 });329}330331function createErroredChunk<T>(332 response: Response,333 reason: mixed,334): ErroredChunk<T> {335 // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors336 return new ReactPromise(ERRORED, null, reason);337}338339function resolveModelChunk<T>(340 response: Response,341 chunk: SomeChunk<T>,342 value: string,343 id: number,344): void {345 if (chunk.status !== PENDING) {346 // If we get more data to an already resolved ID, we assume that it's347 // a stream chunk since any other row shouldn't have more than one entry.348 const streamChunk: InitializedStreamChunk<any> = chunk as any;349 const controller = streamChunk.reason;350 if (value[0] === 'C') {351 controller.close(value === 'C' ? '"$undefined"' : value.slice(1));352 } else {353 controller.enqueueModel(value);354 }355 return;356 }357 const resolveListeners = chunk.value;358 const rejectListeners = chunk.reason;359 const resolvedChunk: ResolvedModelChunk<T> = chunk as any;360 resolvedChunk.status = RESOLVED_MODEL;361 resolvedChunk.value = value;362 resolvedChunk.reason = {id, [RESPONSE_SYMBOL]: response};363 if (resolveListeners !== null) {364 // This is unfortunate that we're reading this eagerly if365 // we already have listeners attached since they might no366 // longer be rendered or might not be the highest pri.367 initializeModelChunk(resolvedChunk);368 // The status might have changed after initialization.369 wakeChunkIfInitialized(response, chunk, resolveListeners, rejectListeners);370 }371}372373function createInitializedStreamChunk<374 T: ReadableStream | $AsyncIterable<any, any, void>,375>(376 response: Response,377 value: T,378 controller: FlightStreamController,379): InitializedChunk<T> {380 // We use the reason field to stash the controller since we already have that381 // field. It's a bit of a hack but efficient.382 // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors383 return new ReactPromise(INITIALIZED, value, controller);384}385386function createResolvedIteratorResultChunk<T>(387 response: Response,388 value: string,389 done: boolean,390): ResolvedModelChunk<IteratorResult<T, T>> {391 // To reuse code as much code as possible we add the wrapper element as part of the JSON.392 const iteratorResultJSON =393 (done ? '{"done":true,"value":' : '{"done":false,"value":') + value + '}';394 // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors395 return new ReactPromise(RESOLVED_MODEL, iteratorResultJSON, {396 id: -1,397 [RESPONSE_SYMBOL]: response,398 });399}400401function resolveIteratorResultChunk<T>(402 response: Response,403 chunk: SomeChunk<IteratorResult<T, T>>,404 value: string,405 done: boolean,406): void {407 // To reuse code as much code as possible we add the wrapper element as part of the JSON.408 const iteratorResultJSON =409 (done ? '{"done":true,"value":' : '{"done":false,"value":') + value + '}';410 resolveModelChunk(response, chunk, iteratorResultJSON, -1);411}412413function loadServerReference<A: Iterable<any>, T>(414 response: Response,415 metaData: {416 id: any,417 bound: null | Thenable<Array<any>>,418 },419 parentObject: Object,420 key: string,421): (...A) => Promise<T> {422 const id: ServerReferenceId = metaData.id;423 if (typeof id !== 'string') {424 return null as any;425 }426 if (key === 'then') {427 // This should never happen because we always serialize objects with then-functions428 // as "thenable" which reduces to ReactPromise with no other fields.429 return null as any;430 }431432 // Check for a cached promise from a previous call with the same metadata.433 // This handles deduplication when the same server reference appears multiple434 // times in the payload.435 const cachedPromise: SomeChunk<T> | void = (metaData as any).$$promise;436 if (cachedPromise !== undefined) {437 if (cachedPromise.status === INITIALIZED) {438 // The value was already resolved by a previous call.439 const resolvedValue: T = cachedPromise.value;440 if (key === __PROTO__) {441 return null as any;442 }443 parentObject[key] = resolvedValue;444 return resolvedValue as any;445 }446447 // The promise is still blocked. Increment the handler dependency count ...448 let handler: InitializationHandler;449 if (initializingHandler) {450 handler = initializingHandler;451 handler.deps++;452 } else {453 handler = initializingHandler = {454 chunk: null,455 value: null,456 reason: null,457 deps: 1,458 errored: false,459 };460 }461 // ... and register resolve and reject listeners on the promise.462 cachedPromise.then(463 resolveReference.bind(null, response, handler, parentObject, key),464 rejectReference.bind(null, response, handler),465 );466467 // Return a place holder value for now.468 return null as any;469 }470471 // This is the first call for this server reference metadata. Create a cached472 // promise to be used for subsequent calls.473 // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors474 const blockedPromise: BlockedChunk<T> = new ReactPromise(BLOCKED, null, null);475 (metaData as any).$$promise = blockedPromise;476477 const serverReference: ServerReference<T> =478 resolveServerReference<$FlowFixMe>(response._bundlerConfig, id);479 // We expect most servers to not really need this because you'd just have all480 // the relevant modules already loaded but it allows for lazy loading of code481 // if needed.482 const bound = metaData.bound;483 let serverReferencePromise: null | Thenable<any> =484 preloadModule(serverReference);485 if (!serverReferencePromise) {486 if (bound instanceof ReactPromise) {487 serverReferencePromise = Promise.resolve(bound);488 } else {489 const resolvedValue = requireModule(serverReference) as any;490 // Resolve the cached promise synchronously.491 const initializedPromise: InitializedChunk<T> = blockedPromise as any;492 initializedPromise.status = INITIALIZED;493 initializedPromise.value = resolvedValue;494 initializedPromise.reason = null;495 return resolvedValue;496 }497 } else if (bound instanceof ReactPromise) {498 serverReferencePromise = Promise.all([serverReferencePromise, bound]);499 }500501 let handler: InitializationHandler;502 if (initializingHandler) {503 handler = initializingHandler;504 handler.deps++;505 } else {506 handler = initializingHandler = {507 chunk: null,508 value: null,509 reason: null,510 deps: 1,511 errored: false,512 };513 }514515 function fulfill(): void {516 let resolvedValue = requireModule(serverReference) as any;517518 if (metaData.bound) {519 // This promise is coming from us and should have initialized by now.520 const promiseValue = (metaData.bound as any).value;521 const boundArgs: Array<any> = isArray(promiseValue)522 ? promiseValue.slice(0)523 : [];524 if (boundArgs.length > MAX_BOUND_ARGS) {525 reject(526 new Error(527 'Server Function has too many bound arguments. Received ' +528 boundArgs.length +529 ' but the limit is ' +530 MAX_BOUND_ARGS +531 '.',532 ),533 );534 return;535 }536 boundArgs.unshift(null); // this537 resolvedValue = resolvedValue.bind.apply(resolvedValue, boundArgs);538 }539540 // Resolve the cached promise so subsequent references can use the value.541 const resolveListeners = blockedPromise.value;542 const initializedPromise: InitializedChunk<T> = blockedPromise as any;543 initializedPromise.status = INITIALIZED;544 initializedPromise.value = resolvedValue;545 initializedPromise.reason = null;546 if (resolveListeners !== null) {547 // Notify any resolve listeners that were added via .then() from548 // subsequent loadServerReference calls for the same reference.549 wakeChunk(response, resolveListeners, resolvedValue, initializedPromise);550 }551552 resolveReference(response, handler, parentObject, key, resolvedValue);553 }554555 function reject(error: mixed): void {556 // Mark the cached promise as errored so subsequent references fail too.557 const rejectListeners = blockedPromise.reason;558 const erroredPromise: ErroredChunk<T> = blockedPromise as any;559 erroredPromise.status = ERRORED;560 erroredPromise.value = null;561 erroredPromise.reason = error;562 if (rejectListeners !== null) {563 // Notify any reject listeners that were added via .then() from subsequent564 // loadServerReference calls for the same reference.565 rejectChunk(response, rejectListeners, error);566 }567568 rejectReference(response, handler, error);569 }570571 serverReferencePromise.then(fulfill, reject);572573 // Return a place holder value for now.574 return null as any;575}576577function reviveModel(578 response: Response,579 parentObj: any,580 parentKey: string,581 value: JSONValue,582 reference: void | string,583 arrayRoot: null | NestedArrayContext,584): any {585 if (typeof value === 'string') {586 // We can't use .bind here because we need the "this" value.587 return parseModelString(588 response,589 parentObj,590 parentKey,591 value,592 reference,593 arrayRoot,594 );595 }596 if (typeof value === 'object' && value !== null) {597 if (598 reference !== undefined &&599 response._temporaryReferences !== undefined600 ) {601 // Store this object's reference in case it's returned later.602 registerTemporaryReference(603 response._temporaryReferences,604 value,605 reference,606 );607 }608 if (isArray(value)) {609 let childContext: NestedArrayContext;610 if (arrayRoot === null) {611 childContext = {612 count: 0,613 fork: false,614 } as NestedArrayContext;615 response._rootArrayContexts.set(value, childContext);616 } else {617 childContext = arrayRoot;618 }619 if (value.length > 1) {620 childContext.fork = true;621 }622 bumpArrayCount(623 childContext,624 // Number of commas + square brackets625 // value.length - 1 + 2626 value.length + 1,627 response,628 );629 for (let i = 0; i < value.length; i++) {630 const childRef =631 reference !== undefined ? reference + ':' + i : undefined;632 // $FlowFixMe[cannot-write]633 value[i] = reviveModel(634 response,635 value,636 '' + i,637 value[i],638 childRef,639 childContext,640 );641 }642 } else {643 for (const key in value) {644 if (hasOwnProperty.call(value, key)) {645 if (key === __PROTO__) {646 // $FlowFixMe[cannot-write]647 delete value[key];648 continue;649 }650 const childRef =651 reference !== undefined && key.indexOf(':') === -1652 ? reference + ':' + key653 : undefined;654 const newValue = reviveModel(655 response,656 value,657 key,658 value[key],659 childRef,660 null, // The array context resets when we're entering a non-array661 );662 if (newValue !== undefined) {663 // $FlowFixMe[cannot-write]664 value[key] = newValue;665 } else {666 // $FlowFixMe[cannot-write]667 delete value[key];668 }669 }670 }671 }672 }673 return value;674}675676type NestedArrayContext = {677 // Keeps track of how many slots, bytes or characters are in nested arrays/strings/typed arrays.678 count: number,679 // A single child is itself not harmful. There needs to be at least one parent array with more680 // than one child.681 fork: boolean,682};683684function bumpArrayCount(685 arrayContext: NestedArrayContext,686 slots: number,687 response: Response,688): void {689 const newCount = (arrayContext.count += slots);690 if (newCount > response._arraySizeLimit && arrayContext.fork) {691 throw new Error(692 'Maximum array nesting exceeded. Large nested arrays can be dangerous. Try adding intermediate objects.',693 );694 }695}696697type InitializationReference = {698 handler: InitializationHandler,699 parentObject: Object,700 key: string,701 map: (702 response: Response,703 model: any,704 parentObject: Object,705 key: string,706 ) => any,707 path: Array<string>,708 arrayRoot: null | NestedArrayContext,709};710type InitializationHandler = {711 chunk: null | BlockedChunk<any>,712 value: any,713 // TODO: Split type to make it impossible to treat a thrown value as NestedArrayContext.714 // thrown value if errored, otherwise array context715 reason: mixed | NestedArrayContext,716 deps: number,717 errored: boolean,718};719let initializingHandler: null | InitializationHandler = null;720721function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {722 const prevHandler = initializingHandler;723 initializingHandler = null;724725 const {[RESPONSE_SYMBOL]: response, id} = chunk.reason;726727 const rootReference = id === -1 ? undefined : id.toString(16);728729 const resolvedModel = chunk.value;730731 // We go to the BLOCKED state until we've fully resolved this.732 // We do this before parsing in case we try to initialize the same chunk733 // while parsing the model. Such as in a cyclic reference.734 const cyclicChunk: BlockedChunk<T> = chunk as any;735 cyclicChunk.status = BLOCKED;736 cyclicChunk.value = null;737 cyclicChunk.reason = null;738739 try {740 const rawModel = JSON.parse(resolvedModel);741742 // The root might not be an array but if it is we want to track the count of entries.743 const arrayRoot: NestedArrayContext = {744 count: 0,745 fork: false,746 };747748 const value: T = reviveModel(749 response,750 {'': rawModel},751 '',752 rawModel,753 rootReference,754 arrayRoot,755 );756757 // Invoke any listeners added while resolving this model. I.e. cyclic758 // references. This may or may not fully resolve the model depending on759 // if they were blocked.760 const resolveListeners = cyclicChunk.value;761 if (resolveListeners !== null) {762 cyclicChunk.value = null;763 cyclicChunk.reason = null;764 for (let i = 0; i < resolveListeners.length; i++) {765 const listener = resolveListeners[i];766 if (typeof listener === 'function') {767 listener(value);768 } else {769 fulfillReference(response, listener, value, arrayRoot);770 }771 }772 }773 if (initializingHandler !== null) {774 if (initializingHandler.errored) {775 throw initializingHandler.reason;776 }777 if (initializingHandler.deps > 0) {778 // We discovered new dependencies on modules that are not yet resolved.779 // We have to keep the BLOCKED state until they're resolved.780 initializingHandler.value = value;781 initializingHandler.reason = arrayRoot;782 initializingHandler.chunk = cyclicChunk;783 return;784 }785 }786 const initializedChunk: InitializedChunk<T> = chunk as any;787 initializedChunk.status = INITIALIZED;788 initializedChunk.value = value;789 initializedChunk.reason = arrayRoot;790 } catch (error) {791 const erroredChunk: ErroredChunk<T> = chunk as any;792 erroredChunk.status = ERRORED;793 erroredChunk.reason = error;794 } finally {795 initializingHandler = prevHandler;796 }797}798799// Report that any missing chunks in the model is now going to throw this800// error upon read. Also notify any pending promises.801export function reportGlobalError(response: Response, error: Error): void {802 response._closed = true;803 response._closedReason = error;804 response._chunks.forEach(chunk => {805 // If this chunk was already resolved or errored, it won't806 // trigger an error but if it wasn't then we need to807 // because we won't be getting any new data to resolve it.808 if (chunk.status === PENDING) {809 triggerErrorOnChunk(response, chunk, error);810 } else if (chunk.status === INITIALIZED) {811 const initializedChunk:812 | InitializedChunk<any>813 | InitializedStreamChunk<any> = chunk as any;814 if (initializedChunk.reason !== null) {815 const maybeController = initializedChunk.reason;816 // $FlowFixMe[method-unbinding] Just doing a typeof check817 if (typeof maybeController.error === 'function') {818 maybeController.error(error);819 }820 }821 }822 });823}824825function getChunk(response: Response, id: number): SomeChunk<any> {826 const chunks = response._chunks;827 let chunk = chunks.get(id);828 if (!chunk) {829 const prefix = response._prefix;830 const key = prefix + id;831 // Check if we have this field in the backing store already.832 const backingEntry = getBackingEntry(response._formData, key);833 if (typeof backingEntry === 'string') {834 chunk = createResolvedModelChunk(response, backingEntry, id);835 } else if (response._closed) {836 // We have already errored the response and we're not going to get837 // anything more streaming in so this will immediately error.838 chunk = createErroredChunk(response, response._closedReason);839 } else {840 // We're still waiting on this entry to stream in.841 chunk = createPendingChunk(response);842 }843 chunks.set(id, chunk);844 }845 return chunk;846}847848function fulfillReference(849 response: Response,850 reference: InitializationReference,851 value: any,852 arrayRoot: null | NestedArrayContext,853): void {854 const {handler, parentObject, key, map, path} = reference;855856 let resolvedValue;857 try {858 let localLength: number = 0;859 const rootArrayContexts = response._rootArrayContexts;860 for (let i = 1; i < path.length; i++) {861 // The server doesn't have any lazy references so we don't expect to go through a Promise.862 const name = path[i];863 if (864 typeof value === 'object' &&865 value !== null &&866 (getPrototypeOf(value) === ObjectPrototype ||867 getPrototypeOf(value) === ArrayPrototype) &&868 hasOwnProperty.call(value, name)869 ) {870 value = value[name];871 if (isArray(value)) {872 localLength = 0;873 arrayRoot = rootArrayContexts.get(value) || arrayRoot;874 } else {875 arrayRoot = null;876 if (typeof value === 'string') {877 localLength = value.length;878 } else if (typeof value === 'bigint') {879 // Estimate the length to avoid expensive toString() calls on large880 // BigInt values. If the value is too large, we get Infinity, which881 // will trigger the array size limit error.882 // eslint-disable-next-line react-internal/no-primitive-constructors883 const n = Math.abs(Number(value));884 if (n === 0) {885 localLength = 1;886 } else {887 localLength = Math.floor(Math.log10(n)) + 1;888 }889 } else if (ArrayBuffer.isView(value)) {890 localLength = value.byteLength;891 } else {892 localLength = 0;893 }894 }895 } else {896 throw new Error('Invalid reference.');897 }898 }899900 resolvedValue = map(response, value, parentObject, key);901902 // Add any array counts to the reference's array root. The value that we're903 // resolving might have deep nesting that we need to resolve.904 const referenceArrayRoot = reference.arrayRoot;905 if (referenceArrayRoot !== null) {906 if (arrayRoot !== null) {907 if (arrayRoot.fork) {908 referenceArrayRoot.fork = true;909 }910 bumpArrayCount(referenceArrayRoot, arrayRoot.count, response);911 } else if (localLength > 0) {912 bumpArrayCount(referenceArrayRoot, localLength, response);913 }914 }915 } catch (error) {916 rejectReference(response, handler, error);917 return;918 }919920 // There are no Elements or Debug Info to transfer here.921922 resolveReference(response, handler, parentObject, key, resolvedValue);923}924925function resolveReference(926 response: Response,927 handler: InitializationHandler,928 parentObject: Object,929 key: string,930 resolvedValue: mixed,931): void {932 if (key !== __PROTO__) {933 parentObject[key] = resolvedValue;934 }935936 // If this is the root object for a model reference, where `handler.value`937 // is a stale `null`, the resolved value can be used directly.938 if (key === '' && handler.value === null) {939 handler.value = resolvedValue;940 }941942 handler.deps--;943944 if (handler.deps === 0) {945 const chunk = handler.chunk;946 if (chunk === null || chunk.status !== BLOCKED) {947 return;948 }949 const resolveListeners = chunk.value;950 const initializedChunk: InitializedChunk<any> = chunk as any;951 initializedChunk.status = INITIALIZED;952 initializedChunk.value = handler.value;953 initializedChunk.reason =954 // $FlowFixMe[incompatible-type] Assuming handler.errored is false.955 handler.reason;956 if (resolveListeners !== null) {957 wakeChunk(response, resolveListeners, handler.value, initializedChunk);958 }959 }960}961962function rejectReference(963 response: Response,964 handler: InitializationHandler,965 error: mixed,966): void {967 if (handler.errored) {968 // We've already errored. We could instead build up an AggregateError969 // but if there are multiple errors we just take the first one like970 // Promise.all.971 return;972 }973 handler.errored = true;974 handler.value = null;975 handler.reason = error;976 const chunk = handler.chunk;977 if (chunk === null || chunk.status !== BLOCKED) {978 return;979 }980 // There's no debug info to forward in this direction.981 triggerErrorOnChunk(response, chunk, error);982}983984function waitForReference<T>(985 response: Response,986 referencedChunk: BlockedChunk<T>,987 parentObject: Object,988 key: string,989 arrayRoot: null | NestedArrayContext,990 map: (response: Response, model: any, parentObject: Object, key: string) => T,991 path: Array<string>,992): T {993 let handler: InitializationHandler;994 if (initializingHandler) {995 handler = initializingHandler;996 handler.deps++;997 } else {998 handler = initializingHandler = {999 chunk: null,1000 value: null,1001 reason: null,1002 deps: 1,1003 errored: false,1004 };1005 }10061007 const reference: InitializationReference = {1008 handler,1009 parentObject,1010 key,1011 map,1012 path,1013 arrayRoot,1014 };10151016 // Add "listener".1017 if (referencedChunk.value === null) {1018 referencedChunk.value = [reference];1019 } else {1020 referencedChunk.value.push(reference);1021 }1022 if (referencedChunk.reason === null) {1023 referencedChunk.reason = [reference];1024 } else {1025 referencedChunk.reason.push(reference);1026 }10271028 // Return a place holder value for now.1029 return null as any;1030}10311032function getOutlinedModel<T>(1033 response: Response,1034 reference: string,1035 parentObject: Object,1036 key: string,1037 referenceArrayRoot: null | NestedArrayContext,1038 map: (response: Response, model: any, parentObject: Object, key: string) => T,1039): T {1040 const path = reference.split(':');1041 const id = parseInt(path[0], 16);1042 let chunk = getChunk(response, id);1043 switch (chunk.status) {1044 case RESOLVED_MODEL:1045 initializeModelChunk(chunk);1046 // $FlowFixMe[incompatible-type] We just initialized this chunk so it can't be a ResolvedModelChunk anymore.1047 chunk = chunk as Exclude<SomeChunk<T>, ResolvedModelChunk<T>>;1048 break;1049 }1050 // The status might have changed after initialization.1051 switch (chunk.status) {1052 case INITIALIZED:1053 let value = chunk.value;1054 const arrayRootOrController:1055 | null1056 | NestedArrayContext1057 | FlightStreamController = chunk.reason;1058 if (arrayRootOrController !== null && 'error' in arrayRootOrController) {1059 throw new Error(1060 'Expected an initialized chunk but got an initialized stream chunk instead. ' +1061 'This payload may have been submitted by an older version of React.',1062 );1063 }1064 let arrayRoot = arrayRootOrController;10651066 let localLength: number = 0;1067 const rootArrayContexts = response._rootArrayContexts;1068 for (let i = 1; i < path.length; i++) {1069 const name = path[i];1070 if (1071 typeof value === 'object' &&1072 // $FlowFixMe[invalid-compare] This check is still needed at runtime.1073 value !== null &&1074 (getPrototypeOf(value) === ObjectPrototype ||1075 getPrototypeOf(value) === ArrayPrototype) &&1076 hasOwnProperty.call(value, name)1077 ) {1078 value = value[name];1079 if (isArray(value)) {1080 localLength = 0;1081 arrayRoot =1082 rootArrayContexts.get(1083 // $FlowFixMe[incompatible-type] Our `isArray` typing can't narrow `mixed`1084 value as $ReadOnlyArray<mixed>,1085 ) || arrayRoot;1086 } else {1087 arrayRoot = null;1088 if (typeof value === 'string') {1089 localLength = value.length;1090 } else if (typeof value === 'bigint') {1091 // Estimate the length to avoid expensive toString() calls on large1092 // BigInt values. If the value is too large, we get Infinity, which1093 // will trigger the array size limit error.1094 // eslint-disable-next-line react-internal/no-primitive-constructors1095 const n = Math.abs(Number(value));1096 if (n === 0) {1097 localLength = 1;1098 } else {1099 localLength = Math.floor(Math.log10(n)) + 1;1100 }1101 } else if (ArrayBuffer.isView(value)) {1102 localLength = value.byteLength;1103 } else {1104 localLength = 0;1105 }1106 }1107 } else {1108 throw new Error('Invalid reference.');1109 }1110 }1111 const chunkValue = map(response, value, parentObject, key);11121113 // Add any array counts to the reference's array root. The value that we're1114 // resolving might have deep nesting that we need to resolve.1115 if (referenceArrayRoot !== null) {1116 if (arrayRoot !== null) {1117 if (arrayRoot.fork) {1118 referenceArrayRoot.fork = true;1119 }1120 bumpArrayCount(referenceArrayRoot, arrayRoot.count, response);1121 } else if (localLength > 0) {1122 bumpArrayCount(referenceArrayRoot, localLength, response);1123 }1124 }1125 // There's no Element nor Debug Info in the ReplyServer so we don't have to check those here.1126 return chunkValue;1127 case BLOCKED:1128 return waitForReference(1129 response,1130 chunk,1131 parentObject,1132 key,1133 referenceArrayRoot,1134 map,1135 path,1136 );1137 case PENDING:1138 // If we don't have the referenced chunk yet, then this must be a forward reference,1139 // which is not allowed.1140 throw new Error('Invalid forward reference.');1141 default:1142 // This is an error. Instead of erroring directly, we're going to encode this on1143 // an initialization handler.1144 if (initializingHandler) {1145 initializingHandler.errored = true;1146 initializingHandler.value = null;1147 initializingHandler.reason = chunk.reason;1148 } else {1149 initializingHandler = {1150 chunk: null,1151 value: null,1152 reason: chunk.reason,1153 deps: 0,1154 errored: true,1155 };1156 }1157 // Placeholder1158 return null as any;1159 }1160}11611162function createMap(1163 response: Response,1164 model: Array<[any, any]>,1165): Map<any, any> {1166 if (!isArray(model)) {1167 throw new Error('Invalid Map initializer.');1168 }1169 if ((model as any).$$consumed === true) {1170 throw new Error('Already initialized Map.');1171 }1172 // This needs to come first to prevent the model from being consumed again in case of a cyclic reference.1173 (model as any).$$consumed = true;1174 const map = new Map(model);1175 return map;1176}11771178function createSet(response: Response, model: Array<any>): Set<any> {1179 if (!isArray(model)) {1180 throw new Error('Invalid Set initializer.');1181 }1182 if ((model as any).$$consumed === true) {1183 throw new Error('Already initialized Set.');1184 }1185 // This needs to come first to prevent the model from being consumed again in case of a cyclic reference.1186 (model as any).$$consumed = true;1187 const set = new Set(model);1188 return set;1189}11901191function extractIterator(response: Response, model: Array<any>): Iterator<any> {1192 if (!isArray(model)) {1193 throw new Error('Invalid Iterator initializer.');1194 }1195 if ((model as any).$$consumed === true) {1196 throw new Error('Already initialized Iterator.');1197 }1198 // This needs to come first to prevent the model from being consumed again in case of a cyclic reference.1199 (model as any).$$consumed = true;1200 // $FlowFixMe[incompatible-use]: This uses raw Symbols because we're extracting from a native array.1201 const iterator = model[Symbol.iterator]();1202 return iterator;1203}12041205function createModel(1206 response: Response,1207 model: any,1208 parentObject: Object,1209 key: string,1210): any {1211 if (key === 'then' && typeof model === 'function') {1212 // This should never happen because we always serialize objects with then-functions1213 // as "thenable" which reduces to ReactPromise with no other fields.1214 return null;1215 }1216 return model;1217}12181219function parseTypedArray<T: $ArrayBufferView | ArrayBuffer>(1220 response: Response,1221 reference: string,1222 constructor: any,1223 bytesPerElement: number,1224 parentObject: Object,1225 parentKey: string,1226 referenceArrayRoot: null | NestedArrayContext,1227): null {1228 const id = parseInt(reference.slice(2), 16);1229 const prefix = response._prefix;1230 const key = prefix + id;1231 const chunks = response._chunks;1232 if (chunks.has(id)) {1233 throw new Error('Already initialized typed array.');1234 }1235 chunks.set(1236 id,1237 // We don't need to put the actual Blob in the chunk,1238 // because it shouldn't be accessed by anything else.1239 createErroredChunk(response, new Error('Already initialized typed array.')),1240 );12411242 // We should have this backingEntry in the store already because we emitted1243 // it before referencing it. It should be a Blob.1244 const backingEntry: Blob = getBackingEntry(response._formData, key) as any;12451246 const promise: Promise<ArrayBuffer> = backingEntry.arrayBuffer();12471248 // Since loading the buffer is an async operation we'll be blocking the parent1249 // chunk.12501251 let handler: InitializationHandler;1252 if (initializingHandler) {1253 handler = initializingHandler;1254 handler.deps++;1255 } else {1256 handler = initializingHandler = {1257 chunk: null,1258 value: null,1259 reason: null,1260 deps: 1,1261 errored: false,1262 };1263 }12641265 function fulfill(buffer: ArrayBuffer): void {1266 try {1267 if (referenceArrayRoot !== null) {1268 bumpArrayCount(referenceArrayRoot, buffer.byteLength, response);1269 }12701271 const resolvedValue: T =1272 constructor === ArrayBuffer1273 ? (buffer as any)1274 : (new constructor(buffer) as any);12751276 if (key !== __PROTO__) {1277 parentObject[parentKey] = resolvedValue;1278 }12791280 // If this is the root object for a model reference, where `handler.value`1281 // is a stale `null`, the resolved value can be used directly.1282 if (parentKey === '' && handler.value === null) {1283 handler.value = resolvedValue;1284 }1285 } catch (x) {1286 reject(x);1287 return;1288 }12891290 handler.deps--;12911292 if (handler.deps === 0) {1293 const chunk = handler.chunk;1294 if (chunk === null || chunk.status !== BLOCKED) {1295 return;1296 }1297 const resolveListeners = chunk.value;1298 const initializedChunk: InitializedChunk<T> = chunk as any;1299 initializedChunk.status = INITIALIZED;1300 initializedChunk.value = handler.value;1301 // We don't keep an array count for this since it won't be referenced again.1302 // In fact, we don't really need to store this chunk at all.1303 initializedChunk.reason = null;1304 if (resolveListeners !== null) {1305 wakeChunk(response, resolveListeners, handler.value, initializedChunk);1306 }1307 }1308 }13091310 function reject(error: mixed): void {1311 if (handler.errored) {1312 // We've already errored. We could instead build up an AggregateError1313 // but if there are multiple errors we just take the first one like1314 // Promise.all.1315 return;1316 }1317 handler.errored = true;1318 handler.value = null;1319 handler.reason = error;1320 const chunk = handler.chunk;1321 if (chunk === null || chunk.status !== BLOCKED) {1322 return;1323 }1324 triggerErrorOnChunk(response, chunk, error);1325 }13261327 promise.then(fulfill, reject);13281329 return null;1330}13311332function resolveStream<T: ReadableStream | $AsyncIterable<any, any, void>>(1333 response: Response,1334 id: number,1335 stream: T,1336 controller: FlightStreamController,1337): void {1338 const chunks = response._chunks;1339 const chunk = createInitializedStreamChunk(response, stream, controller);1340 chunks.set(id, chunk);13411342 const prefix = response._prefix;1343 const key = prefix + id;1344 const existingEntries = getAllBackingEntries(response._formData, key);1345 for (let i = 0; i < existingEntries.length; i++) {1346 const value = existingEntries[i];1347 if (typeof value === 'string') {1348 if (value[0] === 'C') {1349 controller.close(value === 'C' ? '"$undefined"' : value.slice(1));1350 } else {1351 controller.enqueueModel(value);1352 }1353 }1354 }1355}13561357function parseReadableStream<T>(1358 response: Response,1359 reference: string,1360 type: void | 'bytes',1361 parentObject: Object,1362 parentKey: string,1363): ReadableStream {1364 const id = parseInt(reference.slice(2), 16);1365 const chunks = response._chunks;1366 if (chunks.has(id)) {1367 throw new Error('Already initialized stream.');1368 }13691370 let controller: ReadableStreamController = null as any;1371 let closed = false;1372 const stream = new ReadableStream({1373 type: type,1374 start(c) {1375 controller = c;1376 },1377 });1378 let previousBlockedChunk: SomeChunk<T> | null = null;1379 function enqueue(value: T): void {1380 if (type === 'bytes' && !ArrayBuffer.isView(value)) {1381 flightController.error(new Error('Invalid data for bytes stream.'));1382 return;1383 }1384 controller.enqueue(value);1385 }1386 const flightController = {1387 enqueueModel(json: string): void {1388 if (previousBlockedChunk === null) {1389 // If we're not blocked on any other chunks, we can try to eagerly initialize1390 // this as a fast-path to avoid awaiting them.1391 const chunk: ResolvedModelChunk<T> = createResolvedModelChunk(1392 response,1393 json,1394 -1,1395 );1396 initializeModelChunk(chunk);1397 const initializedChunk: SomeChunk<T> = chunk;1398 if (initializedChunk.status === INITIALIZED) {1399 enqueue(initializedChunk.value);1400 } else {1401 chunk.then(enqueue, flightController.error);1402 previousBlockedChunk = chunk;1403 }1404 } else {1405 // We're still waiting on a previous chunk so we can't enqueue quite yet.1406 const blockedChunk = previousBlockedChunk;1407 const chunk: SomeChunk<T> = createPendingChunk(response);1408 chunk.then(enqueue, flightController.error);1409 previousBlockedChunk = chunk;1410 blockedChunk.then(function () {1411 if (previousBlockedChunk === chunk) {1412 // We were still the last chunk so we can now clear the queue and return1413 // to synchronous emitting.1414 previousBlockedChunk = null;1415 }1416 resolveModelChunk(response, chunk, json, -1);1417 });1418 }1419 },1420 close(json: string): void {1421 if (closed) {1422 return;1423 }1424 closed = true;1425 if (previousBlockedChunk === null) {1426 controller.close();1427 } else {1428 const blockedChunk = previousBlockedChunk;1429 // We shouldn't get any more enqueues after this so we can set it back to null.1430 previousBlockedChunk = null;1431 blockedChunk.then(() => controller.close());1432 }1433 },1434 error(error: mixed): void {1435 if (closed) {1436 return;1437 }1438 closed = true;1439 if (previousBlockedChunk === null) {1440 // $FlowFixMe[incompatible-type]1441 controller.error(error);1442 } else {1443 const blockedChunk = previousBlockedChunk;1444 // We shouldn't get any more enqueues after this so we can set it back to null.1445 previousBlockedChunk = null;1446 blockedChunk.then(() => controller.error(error as any));1447 }1448 },1449 };1450 resolveStream(response, id, stream, flightController);1451 return stream;1452}14531454function FlightIterator(1455 this: {next: (arg: void) => SomeChunk<IteratorResult<any, any>>, ...},1456 next: (arg: void) => SomeChunk<IteratorResult<any, any>>,1457) {1458 this.next = next;1459 // TODO: Add return/throw as options for aborting.1460}1461// TODO: The iterator could inherit the AsyncIterator prototype which is not exposed as1462// a global but exists as a prototype of an AsyncGenerator. However, it's not needed1463// to satisfy the iterable protocol.1464FlightIterator.prototype = {} as any;1465FlightIterator.prototype[ASYNC_ITERATOR] = function asyncIterator(1466 this: $AsyncIterator<any, any, void>,1467) {1468 // Self referencing iterator.1469 return this;1470};14711472function parseAsyncIterable<T>(1473 response: Response,1474 reference: string,1475 iterator: boolean,1476 parentObject: Object,1477 parentKey: string,1478): $AsyncIterable<T, T, void> | $AsyncIterator<T, T, void> {1479 const id = parseInt(reference.slice(2), 16);1480 const chunks = response._chunks;1481 if (chunks.has(id)) {1482 throw new Error('Already initialized stream.');1483 }14841485 const buffer: Array<SomeChunk<IteratorResult<T, T>>> = [];1486 let closed = false;1487 let nextWriteIndex = 0;1488 const flightController = {1489 enqueueModel(value: string): void {1490 if (nextWriteIndex === buffer.length) {1491 buffer[nextWriteIndex] = createResolvedIteratorResultChunk(1492 response,1493 value,1494 false,1495 );1496 } else {1497 resolveIteratorResultChunk(1498 response,1499 buffer[nextWriteIndex],1500 value,1501 false,1502 );1503 }1504 nextWriteIndex++;1505 },1506 close(value: string): void {1507 if (closed) {1508 return;1509 }1510 closed = true;1511 if (nextWriteIndex === buffer.length) {1512 buffer[nextWriteIndex] = createResolvedIteratorResultChunk(1513 response,1514 value,1515 true,1516 );1517 } else {1518 resolveIteratorResultChunk(1519 response,1520 buffer[nextWriteIndex],1521 value,1522 true,1523 );1524 }1525 nextWriteIndex++;1526 while (nextWriteIndex < buffer.length) {1527 // In generators, any extra reads from the iterator have the value undefined.1528 resolveIteratorResultChunk(1529 response,1530 buffer[nextWriteIndex++],1531 '"$undefined"',1532 true,1533 );1534 }1535 },1536 error(error: Error): void {1537 if (closed) {1538 return;1539 }1540 closed = true;1541 if (nextWriteIndex === buffer.length) {1542 buffer[nextWriteIndex] =1543 createPendingChunk<IteratorResult<T, T>>(response);1544 }1545 while (nextWriteIndex < buffer.length) {1546 triggerErrorOnChunk(response, buffer[nextWriteIndex++], error);1547 }1548 },1549 };1550 const iterable: $AsyncIterable<T, T, void> = {1551 [ASYNC_ITERATOR](): $AsyncIterator<T, T, void> {1552 let nextReadIndex = 0;1553 // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors1554 return new FlightIterator((arg: void) => {1555 if (arg !== undefined) {1556 throw new Error(1557 'Values cannot be passed to next() of AsyncIterables passed to Client Components.',1558 );1559 }1560 if (nextReadIndex === buffer.length) {1561 if (closed) {1562 // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors1563 return new ReactPromise(1564 INITIALIZED,1565 {done: true, value: undefined},1566 null,1567 );1568 }1569 buffer[nextReadIndex] =1570 createPendingChunk<IteratorResult<T, T>>(response);1571 }1572 return buffer[nextReadIndex++];1573 });1574 },1575 };1576 // TODO: If it's a single shot iterator we can optimize memory by cleaning up the buffer after1577 // reading through the end, but currently we favor code size over this optimization.1578 const stream = iterator ? iterable[ASYNC_ITERATOR]() : iterable;1579 resolveStream(response, id, stream, flightController);1580 return stream;1581}15821583function parseModelString(1584 response: Response,1585 obj: Object,1586 key: string,1587 value: string,1588 reference: void | string,1589 arrayRoot: null | NestedArrayContext,1590): any {1591 if (value[0] === '$') {1592 switch (value[1]) {1593 case '$': {1594 // This was an escaped string value.1595 if (arrayRoot !== null) {1596 bumpArrayCount(arrayRoot, value.length - 1, response);1597 }1598 return value.slice(1);1599 }1600 case '@': {1601 // Promise1602 const id = parseInt(value.slice(2), 16);1603 const chunk = getChunk(response, id);1604 return chunk;1605 }1606 case 'h': {1607 // Server Reference1608 const ref = value.slice(2);1609 return getOutlinedModel(1610 response,1611 ref,1612 obj,1613 key,1614 null,1615 loadServerReference,1616 );1617 }1618 case 'T': {1619 // Temporary Reference1620 if (1621 reference === undefined ||1622 response._temporaryReferences === undefined1623 ) {1624 throw new Error(1625 'Could not reference an opaque temporary reference. ' +1626 'This is likely due to misconfiguring the temporaryReferences options ' +1627 'on the server.',1628 );1629 }1630 return createTemporaryReference(1631 response._temporaryReferences,1632 reference,1633 );1634 }1635 case 'Q': {1636 // Map1637 const ref = value.slice(2);1638 return getOutlinedModel(response, ref, obj, key, null, createMap);1639 }1640 case 'W': {1641 // Set1642 const ref = value.slice(2);1643 return getOutlinedModel(response, ref, obj, key, null, createSet);1644 }1645 case 'K': {1646 // FormData1647 const stringId = value.slice(2);16481649 const responsePrefix = response._prefix;1650 // Use the special marker from the Client to distinguish keys that should1651 // be consumed by referenced FormData.1652 const anyFormPrefix = responsePrefix + '_';1653 const formPrefix = anyFormPrefix + stringId + '_';16541655 const data = new FormData();1656 const backingFormData = response._formData;1657 // We're still transpiling for-of loops, so we have to use the iterator directly instead of a for-of loop.1658 while (true) {1659 const formDataKey = peekBackingEntry(backingFormData);1660 if (formDataKey === undefined) {1661 break;1662 }1663 if (formDataKey.startsWith(formPrefix)) {1664 const referencedFormDataValue = getAllBackingEntries(1665 backingFormData,1666 formDataKey,1667 );1668 const referencedFormDataKey = formDataKey.slice(formPrefix.length);1669 for (let i = 0; i < referencedFormDataValue.length; i++) {1670 // $FlowFixMe[incompatible-type]1671 data.append(referencedFormDataKey, referencedFormDataValue[i]);1672 }1673 consumeBackingEntry(backingFormData, formDataKey);1674 } else if (formDataKey.startsWith(anyFormPrefix)) {1675 // The FormData values are continuous and before the FormData reference.1676 // If we see something that doesn't look like a value for a referenced1677 // FormData, we can assume we're past the values for this FormData1678 // reference and stop iterating.1679 break;1680 } else {1681 // Either an outlined value or something not owned by this Reply.1682 advanceBackingEntryIterator(backingFormData);1683 }1684 }1685 return data;1686 }1687 case 'i': {1688 // Iterator1689 const ref = value.slice(2);1690 return getOutlinedModel(response, ref, obj, key, null, extractIterator);1691 }1692 case 'I': {1693 // $Infinity1694 return Infinity;1695 }1696 case '-': {1697 // $-0 or $-Infinity1698 if (value === '$-0') {1699 return -0;1700 } else {1701 return -Infinity;1702 }1703 }1704 case 'N': {1705 // $NaN1706 return NaN;1707 }1708 case 'u': {1709 // matches "$undefined"1710 // Special encoding for `undefined` which can't be serialized as JSON otherwise.1711 return undefined;1712 }1713 case 'D': {1714 // Date1715 return new Date(Date.parse(value.slice(2)));1716 }1717 case 'n': {1718 // BigInt1719 const bigIntStr = value.slice(2);1720 if (bigIntStr.length > MAX_BIGINT_DIGITS) {1721 throw new Error(1722 'BigInt is too large. Received ' +1723 bigIntStr.length +1724 ' digits but the limit is ' +1725 MAX_BIGINT_DIGITS +1726 '.',1727 );1728 }1729 if (arrayRoot !== null) {1730 bumpArrayCount(arrayRoot, bigIntStr.length, response);1731 }1732 return BigInt(bigIntStr);1733 }1734 case 'A':1735 return parseTypedArray(1736 response,1737 value,1738 ArrayBuffer,1739 1,1740 obj,1741 key,1742 arrayRoot,1743 );1744 case 'O':1745 return parseTypedArray(1746 response,1747 value,1748 Int8Array,1749 1,1750 obj,1751 key,1752 arrayRoot,1753 );1754 case 'o':1755 return parseTypedArray(1756 response,1757 value,1758 Uint8Array,1759 1,1760 obj,1761 key,1762 arrayRoot,1763 );1764 case 'U':1765 return parseTypedArray(1766 response,1767 value,1768 Uint8ClampedArray,1769 1,1770 obj,1771 key,1772 arrayRoot,1773 );1774 case 'S':1775 return parseTypedArray(1776 response,1777 value,1778 Int16Array,1779 2,1780 obj,1781 key,1782 arrayRoot,1783 );1784 case 's':1785 return parseTypedArray(1786 response,1787 value,1788 Uint16Array,1789 2,1790 obj,1791 key,1792 arrayRoot,1793 );1794 case 'L':1795 return parseTypedArray(1796 response,1797 value,1798 Int32Array,1799 4,1800 obj,1801 key,1802 arrayRoot,1803 );1804 case 'l':1805 return parseTypedArray(1806 response,1807 value,1808 Uint32Array,1809 4,1810 obj,1811 key,1812 arrayRoot,1813 );1814 case 'G':1815 return parseTypedArray(1816 response,1817 value,1818 Float32Array,1819 4,1820 obj,1821 key,1822 arrayRoot,1823 );1824 case 'g':1825 return parseTypedArray(1826 response,1827 value,1828 Float64Array,1829 8,1830 obj,1831 key,1832 arrayRoot,1833 );1834 case 'M':1835 return parseTypedArray(1836 response,1837 value,1838 BigInt64Array,1839 8,1840 obj,1841 key,1842 arrayRoot,1843 );1844 case 'm':1845 return parseTypedArray(1846 response,1847 value,1848 BigUint64Array,1849 8,1850 obj,1851 key,1852 arrayRoot,1853 );1854 case 'V':1855 return parseTypedArray(1856 response,1857 value,1858 DataView,1859 1,1860 obj,1861 key,1862 arrayRoot,1863 );1864 case 'B': {1865 // Blob1866 const id = parseInt(value.slice(2), 16);1867 const prefix = response._prefix;1868 const blobKey = prefix + id;1869 // We should have this backingEntry in the store already because we emitted1870 // it before referencing it. It should be a Blob.1871 const backingEntry: Blob = getBackingEntry(1872 response._formData,1873 blobKey,1874 ) as any;1875 if (!(backingEntry instanceof Blob)) {1876 throw new Error('Referenced Blob is not a Blob.');1877 }1878 return backingEntry;1879 }1880 case 'R': {1881 return parseReadableStream(response, value, undefined, obj, key);1882 }1883 case 'r': {1884 return parseReadableStream(response, value, 'bytes', obj, key);1885 }1886 case 'X': {1887 return parseAsyncIterable(response, value, false, obj, key);1888 }1889 case 'x': {1890 return parseAsyncIterable(response, value, true, obj, key);1891 }1892 }1893 // We assume that anything else is a reference ID.1894 const ref = value.slice(1);1895 return getOutlinedModel(response, ref, obj, key, arrayRoot, createModel);1896 }1897 if (arrayRoot !== null) {1898 bumpArrayCount(arrayRoot, value.length, response);1899 }1900 return value;1901}19021903const DEFAULT_MAX_ARRAY_NESTING = 1000000;19041905// Limit BigInt size to prevent CPU exhaustion from parsing very large values.1906// 300 digits covers most practical use cases (even 512-bit integers need only1907// ~154 digits) and aligns with the implicit limit from the Number approximation1908// checks in fulfillReference and getOutlinedModel.1909const MAX_BIGINT_DIGITS = 300;19101911export const MAX_BOUND_ARGS = 1000;19121913export function createResponse(1914 bundlerConfig: ServerManifest,1915 formFieldPrefix: string,1916 temporaryReferences: void | TemporaryReferenceSet,1917 backingFormData?: FormData = new FormData(),1918 arraySizeLimit?: number = DEFAULT_MAX_ARRAY_NESTING,1919): Response {1920 const chunks: Map<number, SomeChunk<any>> = new Map();19211922 const response: Response = {1923 _bundlerConfig: bundlerConfig,1924 _prefix: formFieldPrefix,1925 _formData: createBackingFormData(backingFormData),1926 _chunks: chunks,1927 _closed: false,1928 _closedReason: null,1929 _temporaryReferences: temporaryReferences,1930 _rootArrayContexts: new WeakMap(),1931 _arraySizeLimit: arraySizeLimit,1932 };1933 return response;1934}19351936export function resolveField(1937 response: Response,1938 key: string,1939 value: string,1940): void {1941 // Add this field to the backing store.1942 appendBackingEntry(response._formData, key, value);1943 const prefix = response._prefix;1944 if (key.startsWith(prefix)) {1945 const chunks = response._chunks;1946 const id = +key.slice(prefix.length);1947 const chunk = chunks.get(id);1948 if (chunk) {1949 // We were waiting on this key so now we can resolve it.1950 resolveModelChunk(response, chunk, value, id);1951 }1952 }1953}19541955export function resolveFile(response: Response, key: string, file: File): void {1956 // Add this field to the backing store.1957 appendBackingEntry(response._formData, key, file);1958}19591960export opaque type FileHandle = {1961 chunks: Array<Uint8Array>,1962 filename: string,1963 mime: string,1964};19651966export function resolveFileInfo(1967 response: Response,1968 key: string,1969 filename: string,1970 mime: string,1971): FileHandle {1972 return {1973 chunks: [],1974 filename,1975 mime,1976 };1977}19781979export function resolveFileChunk(1980 response: Response,1981 handle: FileHandle,1982 chunk: Uint8Array,1983): void {1984 handle.chunks.push(chunk);1985}19861987export function resolveFileComplete(1988 response: Response,1989 key: string,1990 handle: FileHandle,1991): void {1992 // Add this file to the backing store.1993 // Node.js doesn't expose a global File constructor so we need to use1994 // the append() form that takes the file name as the third argument,1995 // to create a File object.1996 const blob = new Blob(handle.chunks, {type: handle.mime});1997 appendBackingFile(response._formData, key, blob, handle.filename);1998}19992000export function close(response: Response): void {
Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.