332
333
export type DebugChannelCallback = (message: string) => void;
334
335
-export type Response = {
335
+type Response = {
336
_bundlerConfig: ServerConsumerModuleMap,
337
_serverReferenceConfig: null | ServerManifest,
338
_moduleLoading: ModuleLoading,
351
_closedReason: mixed,
352
_tempRefs: void | TemporaryReferenceSet, // the set temporary references can be resolved from
353
_timeOrigin: number, // Profiling-only
354
+ _pendingChunks: number, // DEV-only
355
+ _weakResponse: WeakResponse, // DEV-only
356
_debugRootOwner?: null | ReactComponentInfo, // DEV-only
357
_debugRootStack?: null | Error, // DEV-only
358
_debugRootTask?: null | ConsoleTask, // DEV-only
362
_rootEnvironmentName: string, // DEV-only, the requested environment name.
363
};
364
365
+// This indirection exists only to clean up DebugChannel when all Lazy References are GC:ed.
366
+// Therefore we only use the indirection in DEV.
367
+type WeakResponse = {
368
+ weak: WeakRef<Response>,
369
+ response: null | Response, // This is null when there are no pending chunks.
370
+};
371
+
372
+export type {WeakResponse as Response};
373
+
374
+function hasGCedResponse(weakResponse: WeakResponse): boolean {
375
+ return __DEV__ && weakResponse.weak.deref() === undefined;
376
+}
377
+
378
+function unwrapWeakResponse(weakResponse: WeakResponse): Response {
379
+ if (__DEV__) {
380
+ const response = weakResponse.weak.deref();
381
+ if (response === undefined) {
382
+ // eslint-disable-next-line react-internal/prod-error-codes
383
+ throw new Error(
384
+ 'We did not expect to receive new data after GC:ing the response.',
385
+ );
386
+ }
387
+ return response;
388
+ } else {
389
+ return (weakResponse: any); // In prod we just use the real Response directly.
390
+ }
391
+}
392
+
393
+function getWeakResponse(response: Response): WeakResponse {
394
+ if (__DEV__) {
395
+ return response._weakResponse;
396
+ } else {
397
+ return (response: any); // In prod we just use the real Response directly.
398
+ }
399
+}
400
+
401
+function cleanupDebugChannel(debugChannel: DebugChannelCallback): void {
402
+ // When a Response gets GC:ed because nobody is referring to any of the objects that lazily
403
+ // loads from the Response anymore, then we can close the debug channel.
404
+ debugChannel('');
405
+}
406
+
407
+// If FinalizationRegistry doesn't exist, we cannot use the debugChannel.
408
+const debugChannelRegistry =
409
+ __DEV__ && typeof FinalizationRegistry === 'function'
410
+ ? new FinalizationRegistry(cleanupDebugChannel)
411
+ : null;
412
+
413
function readChunk<T>(chunk: SomeChunk<T>): T {
414
// If we have resolved content, we try to initialize it first which
415
// might put us back into one of the other states.
435
}
436
}
437
388
-export function getRoot<T>(response: Response): Thenable<T> {
438
+export function getRoot<T>(weakResponse: WeakResponse): Thenable<T> {
439
+ const response = unwrapWeakResponse(weakResponse);
440
const chunk = getChunk(response, 0);
441
return (chunk: any);
442
}
443
444
function createPendingChunk<T>(response: Response): PendingChunk<T> {
445
+ if (__DEV__) {
446
+ // Retain a strong reference to the Response while we wait for the result.
447
+ response._pendingChunks++;
448
+ response._weakResponse.response = response;
449
+ }
450
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
451
return new ReactPromise(PENDING, null, null);
452
}
453
454
+function releasePendingChunk(response: Response, chunk: SomeChunk<any>): void {
455
+ if (__DEV__ && chunk.status === PENDING) {
456
+ if (--response._pendingChunks === 0) {
457
+ // We're no longer waiting for any more chunks. We can release the strong reference
458
+ // to the response. We'll regain it if we ask for any more data later on.
459
+ response._weakResponse.response = null;
460
+ }
461
+ }
462
+}
463
+
464
function createBlockedChunk<T>(response: Response): BlockedChunk<T> {
465
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
466
return new ReactPromise(BLOCKED, null, null);
591
}
592
}
593
528
-function triggerErrorOnChunk<T>(chunk: SomeChunk<T>, error: mixed): void {
594
+function triggerErrorOnChunk<T>(
595
+ response: Response,
596
+ chunk: SomeChunk<T>,
597
+ error: mixed,
598
+): void {
599
if (chunk.status !== PENDING && chunk.status !== BLOCKED) {
600
// If we get more data to an already resolved ID, we assume that it's
601
// a stream chunk since any other row shouldn't have more than one entry.
605
controller.error(error);
606
return;
607
}
608
+ releasePendingChunk(response, chunk);
609
const listeners = chunk.reason;
610
const erroredChunk: ErroredChunk<T> = (chunk: any);
611
erroredChunk.status = ERRORED;
706
controller.enqueueModel(value);
707
return;
708
}
709
+ releasePendingChunk(response, chunk);
710
const resolveListeners = chunk.value;
711
const rejectListeners = chunk.reason;
712
const resolvedChunk: ResolvedModelChunk<T> = (chunk: any);
724
}
725
726
function resolveModuleChunk<T>(
727
+ response: Response,
728
chunk: SomeChunk<T>,
729
value: ClientReference<T>,
730
): void {
732
// We already resolved. We didn't expect to see this.
733
return;
734
}
735
+ releasePendingChunk(response, chunk);
736
const resolveListeners = chunk.value;
737
const rejectListeners = chunk.reason;
738
const resolvedChunk: ResolvedModuleChunk<T> = (chunk: any);
840
841
// Report that any missing chunks in the model is now going to throw this
842
// error upon read. Also notify any pending promises.
769
-export function reportGlobalError(response: Response, error: Error): void {
843
+export function reportGlobalError(
844
+ weakResponse: WeakResponse,
845
+ error: Error,
846
+): void {
847
+ if (hasGCedResponse(weakResponse)) {
848
+ // Ignore close signal if we are not awaiting any more pending chunks.
849
+ return;
850
+ }
851
+ const response = unwrapWeakResponse(weakResponse);
852
response._closed = true;
853
response._closedReason = error;
854
response._chunks.forEach(chunk => {
856
// trigger an error but if it wasn't then we need to
857
// because we won't be getting any new data to resolve it.
858
if (chunk.status === PENDING) {
777
- triggerErrorOnChunk(chunk, error);
859
+ triggerErrorOnChunk(response, chunk, error);
860
}
861
});
862
if (__DEV__) {
1300
reference: InitializationReference,
1301
error: mixed,
1302
): void {
1221
- const {handler} = reference;
1303
+ const {handler, response} = reference;
1304
1305
if (handler.errored) {
1306
// We've already errored. We could instead build up an AggregateError
1345
}
1346
}
1347
1266
- triggerErrorOnChunk(chunk, error);
1348
+ triggerErrorOnChunk(response, chunk, error);
1349
}
1350
1351
function waitForReference<T>(
1564
}
1565
}
1566
1485
- triggerErrorOnChunk(chunk, error);
1567
+ triggerErrorOnChunk(response, chunk, error);
1568
}
1569
1570
promise.then(fulfill, reject);
2107
this._timeOrigin = 0;
2108
}
2109
if (__DEV__) {
2110
+ this._pendingChunks = 0;
2111
+ this._weakResponse = {
2112
+ weak: new WeakRef(this),
2113
+ response: this,
2114
+ };
2115
// TODO: The Flight Client can be used in a Client Environment too and we should really support
2116
// getting the owner there as well, but currently the owner of ReactComponentInfo is typed as only
2117
// supporting other ReactComponentInfo as owners (and not Fiber or Fizz's ComponentStackNode).
2146
this._debugChannel = debugChannel;
2147
this._replayConsole = replayConsole;
2148
this._rootEnvironmentName = rootEnv;
2149
+ if (debugChannel) {
2150
+ if (debugChannelRegistry === null) {
2151
+ // We can't safely clean things up later, so we immediately close the debug channel.
2152
+ debugChannel('');
2153
+ this._debugChannel = undefined;
2154
+ } else {
2155
+ debugChannelRegistry.register(this, debugChannel);
2156
+ }
2157
+ }
2158
}
2159
if (enableProfilerTimer && enableComponentPerformanceTrack) {
2160
// Since we don't know when recording of profiles will start and stop, we have to
2180
replayConsole: boolean, // DEV-only
2181
environmentName: void | string, // DEV-only
2182
debugChannel: void | DebugChannelCallback, // DEV-only
2087
-): Response {
2088
- // $FlowFixMe[invalid-constructor]: the shapes are exact here but Flow doesn't like constructors
2089
- return new ResponseInstance(
2090
- bundlerConfig,
2091
- serverReferenceConfig,
2092
- moduleLoading,
2093
- callServer,
2094
- encodeFormAction,
2095
- nonce,
2096
- temporaryReferences,
2097
- findSourceMapURL,
2098
- replayConsole,
2099
- environmentName,
2100
- debugChannel,
2183
+): WeakResponse {
2184
+ return getWeakResponse(
2185
+ // $FlowFixMe[invalid-constructor]: the shapes are exact here but Flow doesn't like constructors
2186
+ new ResponseInstance(
2187
+ bundlerConfig,
2188
+ serverReferenceConfig,
2189
+ moduleLoading,
2190
+ callServer,
2191
+ encodeFormAction,
2192
+ nonce,
2193
+ temporaryReferences,
2194
+ findSourceMapURL,
2195
+ replayConsole,
2196
+ environmentName,
2197
+ debugChannel,
2198
+ ),
2199
);
2200
}
2201
2209
if (chunk.status !== PENDING && chunk.status !== BLOCKED) {
2210
return;
2211
}
2212
+ releasePendingChunk(response, chunk);
2213
const haltedChunk: HaltedChunk<any> = (chunk: any);
2214
haltedChunk.status = HALTED;
2215
haltedChunk.value = null;
2241
controller.enqueueValue(text);
2242
return;
2243
}
2244
+ if (chunk) {
2245
+ releasePendingChunk(response, chunk);
2246
+ }
2247
chunks.set(id, createInitializedTextChunk(response, text));
2248
}
2249
2262
controller.enqueueValue(buffer);
2263
return;
2264
}
2265
+ if (chunk) {
2266
+ releasePendingChunk(response, chunk);
2267
+ }
2268
chunks.set(id, createInitializedBufferChunk(response, buffer));
2269
}
2270
2302
blockedChunk = createBlockedChunk(response);
2303
chunks.set(id, blockedChunk);
2304
} else {
2305
+ releasePendingChunk(response, chunk);
2306
// This can't actually happen because we don't have any forward
2307
// references to modules.
2308
blockedChunk = (chunk: any);
2309
blockedChunk.status = BLOCKED;
2310
}
2311
promise.then(
2206
- () => resolveModuleChunk(blockedChunk, clientReference),
2207
- error => triggerErrorOnChunk(blockedChunk, error),
2312
+ () => resolveModuleChunk(response, blockedChunk, clientReference),
2313
+ error => triggerErrorOnChunk(response, blockedChunk, error),
2314
);
2315
} else {
2316
if (!chunk) {
2318
} else {
2319
// This can't actually happen because we don't have any forward
2320
// references to modules.
2215
- resolveModuleChunk(chunk, clientReference);
2321
+ resolveModuleChunk(response, chunk, clientReference);
2322
}
2323
}
2324
}
2339
// We already resolved. We didn't expect to see this.
2340
return;
2341
}
2342
+ releasePendingChunk(response, chunk);
2343
const resolveListeners = chunk.value;
2344
const resolvedChunk: InitializedStreamChunk<T> = (chunk: any);
2345
resolvedChunk.status = INITIALIZED;
2539
createPendingChunk<IteratorResult<T, T>>(response);
2540
}
2541
while (nextWriteIndex < buffer.length) {
2435
- triggerErrorOnChunk(buffer[nextWriteIndex++], error);
2542
+ triggerErrorOnChunk(response, buffer[nextWriteIndex++], error);
2543
}
2544
},
2545
};
2676
if (!chunk) {
2677
chunks.set(id, createErrorChunk(response, postponeInstance));
2678
} else {
2572
- triggerErrorOnChunk(chunk, postponeInstance);
2679
+ triggerErrorOnChunk(response, chunk, postponeInstance);
2680
}
2681
}
2682
2715
if (!chunk) {
2716
chunks.set(id, createErrorChunk(response, postponeInstance));
2717
} else {
2611
- triggerErrorOnChunk(chunk, postponeInstance);
2718
+ triggerErrorOnChunk(response, chunk, postponeInstance);
2719
}
2720
}
2721
3797
if (!chunk) {
3798
chunks.set(id, createErrorChunk(response, errorWithDigest));
3799
} else {
3693
- triggerErrorOnChunk(chunk, errorWithDigest);
3800
+ triggerErrorOnChunk(response, chunk, errorWithDigest);
3801
}
3802
return;
3803
}
3926
}
3927
3928
export function processBinaryChunk(
3822
- response: Response,
3929
+ weakResponse: WeakResponse,
3930
chunk: Uint8Array,
3931
): void {
3932
+ if (hasGCedResponse(weakResponse)) {
3933
+ // Ignore more chunks if we've already GC:ed all listeners.
3934
+ return;
3935
+ }
3936
+ const response = unwrapWeakResponse(weakResponse);
3937
let i = 0;
3938
let rowState = response._rowState;
3939
let rowID = response._rowID;
4050
response._rowLength = rowLength;
4051
}
4052
3941
-export function processStringChunk(response: Response, chunk: string): void {
4053
+export function processStringChunk(
4054
+ weakResponse: WeakResponse,
4055
+ chunk: string,
4056
+): void {
4057
+ if (hasGCedResponse(weakResponse)) {
4058
+ // Ignore more chunks if we've already GC:ed all listeners.
4059
+ return;
4060
+ }
4061
+ const response = unwrapWeakResponse(weakResponse);
4062
// This is a fork of processBinaryChunk that takes a string as input.
4063
// This can't be just any binary chunk coverted to a string. It needs to be
4064
// in the same offsets given from the Flight Server. E.g. if it's shifted by
4220
};
4221
}
4222
4103
-export function close(response: Response): void {
4223
+export function close(weakResponse: WeakResponse): void {
4224
// In case there are any remaining unresolved chunks, they won't
4225
// be resolved now. So we need to issue an error to those.
4226
// Ideally we should be able to early bail out if we kept a
4227
// ref count of pending chunks.
4108
- reportGlobalError(response, new Error('Connection closed.'));
4228
+ reportGlobalError(weakResponse, new Error('Connection closed.'));
4229
}
4230
4231
function getCurrentOwnerInDEV(): null | ReactComponentInfo {