@@ -5733,6 +5733,15 @@ export function attach(
57335733 // to a specific instance will have those appear in order of when that instance was discovered.
57345734 let hooksCacheKey: null | DevToolsInstance = null;
57355735 let hooksCache: null | HooksTree = null;
5736+ // Collect the stream entries with the highest byte offset and end time.
5737+ const streamEntries: Map<
5738+ Promise<mixed>,
5739+ {
5740+ asyncInfo: ReactAsyncInfo,
5741+ instance: DevToolsInstance,
5742+ hooks: null | HooksTree,
5743+ },
5744+ > = new Map();
57365745 suspenseNode.suspendedBy.forEach((set, ioInfo) => {
57375746 let parentNode = suspenseNode.parent;
57385747 while (parentNode !== null) {
@@ -5771,9 +5780,92 @@ export function attach(
57715780 }
57725781 }
57735782 }
5774- result.push(serializeAsyncInfo(asyncInfo, firstInstance, hooks));
5783+ const newIO = asyncInfo.awaited;
5784+ if (newIO.name === 'RSC stream' && newIO.value != null) {
5785+ const streamPromise = newIO.value;
5786+ // Special case RSC stream entries to pick the last entry keyed by the stream.
5787+ const existingEntry = streamEntries.get(streamPromise);
5788+ if (existingEntry === undefined) {
5789+ streamEntries.set(streamPromise, {
5790+ asyncInfo,
5791+ instance: firstInstance,
5792+ hooks,
5793+ });
5794+ } else {
5795+ const existingIO = existingEntry.asyncInfo.awaited;
5796+ if (
5797+ newIO !== existingIO &&
5798+ ((newIO.byteSize !== undefined &&
5799+ existingIO.byteSize !== undefined &&
5800+ newIO.byteSize > existingIO.byteSize) ||
5801+ newIO.end > existingIO.end)
5802+ ) {
5803+ // The new entry is later in the stream that the old entry. Replace it.
5804+ existingEntry.asyncInfo = asyncInfo;
5805+ existingEntry.instance = firstInstance;
5806+ existingEntry.hooks = hooks;
5807+ }
5808+ }
5809+ } else {
5810+ result.push(serializeAsyncInfo(asyncInfo, firstInstance, hooks));
5811+ }
5812+ }
5813+ }
5814+ });
5815+ // Add any deduped stream entries.
5816+ streamEntries.forEach(({asyncInfo, instance, hooks}) => {
5817+ result.push(serializeAsyncInfo(asyncInfo, instance, hooks));
5818+ });
5819+ return result;
5820+ }
5821+
5822+ function getSuspendedByOfInstance(
5823+ devtoolsInstance: DevToolsInstance,
5824+ hooks: null | HooksTree,
5825+ ): Array<SerializedAsyncInfo> {
5826+ const suspendedBy = devtoolsInstance.suspendedBy;
5827+ if (suspendedBy === null) {
5828+ return [];
5829+ }
5830+
5831+ const foundIOEntries: Set<ReactIOInfo> = new Set();
5832+ const streamEntries: Map<Promise<mixed>, ReactAsyncInfo> = new Map();
5833+ const result: Array<SerializedAsyncInfo> = [];
5834+ for (let i = 0; i < suspendedBy.length; i++) {
5835+ const asyncInfo = suspendedBy[i];
5836+ const ioInfo = asyncInfo.awaited;
5837+ if (foundIOEntries.has(ioInfo)) {
5838+ // We have already added this I/O entry to the result. We can dedupe it.
5839+ // This can happen when an instance depends on the same data in mutliple places.
5840+ continue;
5841+ }
5842+ foundIOEntries.add(ioInfo);
5843+ if (ioInfo.name === 'RSC stream' && ioInfo.value != null) {
5844+ const streamPromise = ioInfo.value;
5845+ // Special case RSC stream entries to pick the last entry keyed by the stream.
5846+ const existingEntry = streamEntries.get(streamPromise);
5847+ if (existingEntry === undefined) {
5848+ streamEntries.set(streamPromise, asyncInfo);
5849+ } else {
5850+ const existingIO = existingEntry.awaited;
5851+ if (
5852+ ioInfo !== existingIO &&
5853+ ((ioInfo.byteSize !== undefined &&
5854+ existingIO.byteSize !== undefined &&
5855+ ioInfo.byteSize > existingIO.byteSize) ||
5856+ ioInfo.end > existingIO.end)
5857+ ) {
5858+ // The new entry is later in the stream that the old entry. Replace it.
5859+ streamEntries.set(streamPromise, asyncInfo);
5860+ }
57755861 }
5862+ } else {
5863+ result.push(serializeAsyncInfo(asyncInfo, devtoolsInstance, hooks));
57765864 }
5865+ }
5866+ // Add any deduped stream entries.
5867+ streamEntries.forEach(asyncInfo => {
5868+ result.push(serializeAsyncInfo(asyncInfo, devtoolsInstance, hooks));
57775869 });
57785870 return result;
57795871 }
@@ -6297,11 +6389,7 @@ export function attach(
62976389 // In this case, this becomes associated with the Client/Host Component where as normally
62986390 // you'd expect these to be associated with the Server Component that awaited the data.
62996391 // TODO: Prepend other suspense sources like css, images and use().
6300- fiberInstance.suspendedBy === null
6301- ? []
6302- : fiberInstance.suspendedBy.map(info =>
6303- serializeAsyncInfo(info, fiberInstance, hooks),
6304- );
6392+ getSuspendedByOfInstance(fiberInstance, hooks);
63056393 const suspendedByRange = getSuspendedByRange(
63066394 getNearestSuspenseNode(fiberInstance),
63076395 );
@@ -6446,7 +6534,7 @@ export function attach(
64466534
64476535 const isSuspended = null;
64486536 // Things that Suspended this Server Component (use(), awaits and direct child promises)
6449- const suspendedBy = virtualInstance.suspendedBy ;
6537+ const suspendedBy = getSuspendedByOfInstance( virtualInstance, null) ;
64506538 const suspendedByRange = getSuspendedByRange(
64516539 getNearestSuspenseNode(virtualInstance),
64526540 );
@@ -6497,12 +6585,7 @@ export function attach(
64976585 ? []
64986586 : Array.from(componentLogsEntry.warnings.entries()),
64996587
6500- suspendedBy:
6501- suspendedBy === null
6502- ? []
6503- : suspendedBy.map(info =>
6504- serializeAsyncInfo(info, virtualInstance, null),
6505- ),
6588+ suspendedBy: suspendedBy,
65066589 suspendedByRange: suspendedByRange,
65076590 unknownSuspenders: UNKNOWN_SUSPENDERS_NONE,
65086591
0 commit comments