@samitouri / QOS-React / commits / fa77f52e74

Unify promise switch statements

There are two different switch statements that we use to unwrap a `use`-ed promise, but there really only needs to be one. This was a factoring artifact that arose because I implemented the yieldy `status` instrumentation thing before I implemented `use` (for promises that are thrown directly during render, which is the old Suspense pattern that will be superseded by `use`).

Andrew Clark committed Oct 22, 2022 at 20:39 UTC fa77f52e74d816f3d7ad0e71850932508c1b353e
8 files changed +222 -407
packages/react-reconciler/src/ReactFiberHooks.new.js
+1 -56
@@ -137,7 +137,6 @@ import {now} from './Scheduler';
137 import {
138 prepareThenableState,
139 trackUsedThenable,
140 - getPreviouslyUsedThenableAtIndex,
140 } from './ReactFiberThenable.new';
141
142 const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
@@ -776,8 +775,6 @@ if (enableUseMemoCacheHook) {
775 };
776 }
777
779 -function noop(): void {}
780 -
778 function use<T>(usable: Usable<T>): T {
779 if (usable !== null && typeof usable === 'object') {
780 // $FlowFixMe[method-unbinding]
@@ -788,59 +785,7 @@ function use<T>(usable: Usable<T>): T {
785 // Track the position of the thenable within this fiber.
786 const index = thenableIndexCounter;
787 thenableIndexCounter += 1;
791 -
792 - // TODO: Unify this switch statement with the one in trackUsedThenable.
793 - switch (thenable.status) {
794 - case 'fulfilled': {
795 - const fulfilledValue: T = thenable.value;
796 - return fulfilledValue;
797 - }
798 - case 'rejected': {
799 - const rejectedError = thenable.reason;
800 - throw rejectedError;
801 - }
802 - default: {
803 - const prevThenableAtIndex: Thenable<T> | null = getPreviouslyUsedThenableAtIndex(
804 - index,
805 - );
806 - if (prevThenableAtIndex !== null) {
807 - if (thenable !== prevThenableAtIndex) {
808 - // Avoid an unhandled rejection errors for the Promises that we'll
809 - // intentionally ignore.
810 - thenable.then(noop, noop);
811 - }
812 - switch (prevThenableAtIndex.status) {
813 - case 'fulfilled': {
814 - const fulfilledValue: T = prevThenableAtIndex.value;
815 - return fulfilledValue;
816 - }
817 - case 'rejected': {
818 - const rejectedError: mixed = prevThenableAtIndex.reason;
819 - throw rejectedError;
820 - }
821 - default: {
822 - // The thenable still hasn't resolved. Suspend with the same
823 - // thenable as last time to avoid redundant listeners.
824 - throw prevThenableAtIndex;
825 - }
826 - }
827 - } else {
828 - // This is the first time something has been used at this index.
829 - // Stash the thenable at the current index so we can reuse it during
830 - // the next attempt.
831 - trackUsedThenable(thenable, index);
832 -
833 - // Suspend.
834 - // TODO: Throwing here is an implementation detail that allows us to
835 - // unwind the call stack. But we shouldn't allow it to leak into
836 - // userspace. Throw an opaque placeholder value instead of the
837 - // actual thenable. If it doesn't get captured by the work loop, log
838 - // a warning, because that means something in userspace must have
839 - // caught it.
840 - throw thenable;
841 - }
842 - }
843 - }
788 + return trackUsedThenable(thenable, index);
789 } else if (
790 usable.$$typeof === REACT_CONTEXT_TYPE ||
791 usable.$$typeof === REACT_SERVER_CONTEXT_TYPE
packages/react-reconciler/src/ReactFiberHooks.old.js
+1 -56
@@ -137,7 +137,6 @@ import {now} from './Scheduler';
137 import {
138 prepareThenableState,
139 trackUsedThenable,
140 - getPreviouslyUsedThenableAtIndex,
140 } from './ReactFiberThenable.old';
141
142 const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
@@ -776,8 +775,6 @@ if (enableUseMemoCacheHook) {
775 };
776 }
777
779 -function noop(): void {}
780 -
778 function use<T>(usable: Usable<T>): T {
779 if (usable !== null && typeof usable === 'object') {
780 // $FlowFixMe[method-unbinding]
@@ -788,59 +785,7 @@ function use<T>(usable: Usable<T>): T {
785 // Track the position of the thenable within this fiber.
786 const index = thenableIndexCounter;
787 thenableIndexCounter += 1;
791 -
792 - // TODO: Unify this switch statement with the one in trackUsedThenable.
793 - switch (thenable.status) {
794 - case 'fulfilled': {
795 - const fulfilledValue: T = thenable.value;
796 - return fulfilledValue;
797 - }
798 - case 'rejected': {
799 - const rejectedError = thenable.reason;
800 - throw rejectedError;
801 - }
802 - default: {
803 - const prevThenableAtIndex: Thenable<T> | null = getPreviouslyUsedThenableAtIndex(
804 - index,
805 - );
806 - if (prevThenableAtIndex !== null) {
807 - if (thenable !== prevThenableAtIndex) {
808 - // Avoid an unhandled rejection errors for the Promises that we'll
809 - // intentionally ignore.
810 - thenable.then(noop, noop);
811 - }
812 - switch (prevThenableAtIndex.status) {
813 - case 'fulfilled': {
814 - const fulfilledValue: T = prevThenableAtIndex.value;
815 - return fulfilledValue;
816 - }
817 - case 'rejected': {
818 - const rejectedError: mixed = prevThenableAtIndex.reason;
819 - throw rejectedError;
820 - }
821 - default: {
822 - // The thenable still hasn't resolved. Suspend with the same
823 - // thenable as last time to avoid redundant listeners.
824 - throw prevThenableAtIndex;
825 - }
826 - }
827 - } else {
828 - // This is the first time something has been used at this index.
829 - // Stash the thenable at the current index so we can reuse it during
830 - // the next attempt.
831 - trackUsedThenable(thenable, index);
832 -
833 - // Suspend.
834 - // TODO: Throwing here is an implementation detail that allows us to
835 - // unwind the call stack. But we shouldn't allow it to leak into
836 - // userspace. Throw an opaque placeholder value instead of the
837 - // actual thenable. If it doesn't get captured by the work loop, log
838 - // a warning, because that means something in userspace must have
839 - // caught it.
840 - throw thenable;
841 - }
842 - }
843 - }
788 + return trackUsedThenable(thenable, index);
789 } else if (
790 usable.$$typeof === REACT_CONTEXT_TYPE ||
791 usable.$$typeof === REACT_SERVER_CONTEXT_TYPE
packages/react-reconciler/src/ReactFiberThenable.new.js
+53 -43
@@ -17,8 +17,7 @@ import type {
17 import ReactSharedInternals from 'shared/ReactSharedInternals';
18 const {ReactCurrentActQueue} = ReactSharedInternals;
19
20 -// TODO: Sparse arrays are bad for performance.
21 -export opaque type ThenableState = Array<Thenable<any> | void>;
20 +export opaque type ThenableState = Array<Thenable<any>>;
21
22 let thenableState: ThenableState | null = null;
23
@@ -62,7 +61,9 @@ export function isThenableStateResolved(thenables: ThenableState): boolean {
61 return true;
62 }
63
65 -export function trackUsedThenable<T>(thenable: Thenable<T>, index: number) {
64 +function noop(): void {}
65 +
66 +export function trackUsedThenable<T>(thenable: Thenable<T>, index: number): T {
67 if (__DEV__ && ReactCurrentActQueue.current !== null) {
68 ReactCurrentActQueue.didUsePromise = true;
69 }
@@ -70,7 +71,20 @@ export function trackUsedThenable<T>(thenable: Thenable<T>, index: number) {
71 if (thenableState === null) {
72 thenableState = [thenable];
73 } else {
73 - thenableState[index] = thenable;
74 + const previous = thenableState[index];
75 + if (previous === undefined) {
76 + thenableState.push(thenable);
77 + } else {
78 + if (previous !== thenable) {
79 + // Reuse the previous thenable, and drop the new one. We can assume
80 + // they represent the same value, because components are idempotent.
81 +
82 + // Avoid an unhandled rejection errors for the Promises that we'll
83 + // intentionally ignore.
84 + thenable.then(noop, noop);
85 + thenable = previous;
86 + }
87 + }
88 }
89
90 // We use an expando to track the status and result of a thenable so that we
@@ -80,52 +94,48 @@ export function trackUsedThenable<T>(thenable: Thenable<T>, index: number) {
94 // If the thenable doesn't have a status, set it to "pending" and attach
95 // a listener that will update its status and result when it resolves.
96 switch (thenable.status) {
83 - case 'fulfilled':
84 - case 'rejected':
85 - // A thenable that already resolved shouldn't have been thrown, so this is
86 - // unexpected. Suggests a mistake in a userspace data library. Don't track
87 - // this thenable, because if we keep trying it will likely infinite loop
88 - // without ever resolving.
89 - // TODO: Log a warning?
90 - break;
97 + case 'fulfilled': {
98 + const fulfilledValue: T = thenable.value;
99 + return fulfilledValue;
100 + }
101 + case 'rejected': {
102 + const rejectedError = thenable.reason;
103 + throw rejectedError;
104 + }
105 default: {
106 if (typeof thenable.status === 'string') {
107 // Only instrument the thenable if the status if not defined. If
108 // it's defined, but an unknown value, assume it's been instrumented by
109 // some custom userspace implementation. We treat it as "pending".
96 - break;
110 + } else {
111 + const pendingThenable: PendingThenable<mixed> = (thenable: any);
112 + pendingThenable.status = 'pending';
113 + pendingThenable.then(
114 + fulfilledValue => {
115 + if (thenable.status === 'pending') {
116 + const fulfilledThenable: FulfilledThenable<mixed> = (thenable: any);
117 + fulfilledThenable.status = 'fulfilled';
118 + fulfilledThenable.value = fulfilledValue;
119 + }
120 + },
121 + (error: mixed) => {
122 + if (thenable.status === 'pending') {
123 + const rejectedThenable: RejectedThenable<mixed> = (thenable: any);
124 + rejectedThenable.status = 'rejected';
125 + rejectedThenable.reason = error;
126 + }
127 + },
128 + );
129 }
98 - const pendingThenable: PendingThenable<mixed> = (thenable: any);
99 - pendingThenable.status = 'pending';
100 - pendingThenable.then(
101 - fulfilledValue => {
102 - if (thenable.status === 'pending') {
103 - const fulfilledThenable: FulfilledThenable<mixed> = (thenable: any);
104 - fulfilledThenable.status = 'fulfilled';
105 - fulfilledThenable.value = fulfilledValue;
106 - }
107 - },
108 - (error: mixed) => {
109 - if (thenable.status === 'pending') {
110 - const rejectedThenable: RejectedThenable<mixed> = (thenable: any);
111 - rejectedThenable.status = 'rejected';
112 - rejectedThenable.reason = error;
113 - }
114 - },
115 - );
116 - break;
117 - }
118 - }
119 -}
130
121 -export function getPreviouslyUsedThenableAtIndex<T>(
122 - index: number,
123 -): Thenable<T> | null {
124 - if (thenableState !== null) {
125 - const thenable = thenableState[index];
126 - if (thenable !== undefined) {
127 - return thenable;
131 + // Suspend.
132 + // TODO: Throwing here is an implementation detail that allows us to
133 + // unwind the call stack. But we shouldn't allow it to leak into
134 + // userspace. Throw an opaque placeholder value instead of the
135 + // actual thenable. If it doesn't get captured by the work loop, log
136 + // a warning, because that means something in userspace must have
137 + // caught it.
138 + throw thenable;
139 }
140 }
130 - return null;
141 }
packages/react-reconciler/src/ReactFiberThenable.old.js
+53 -43
@@ -17,8 +17,7 @@ import type {
17 import ReactSharedInternals from 'shared/ReactSharedInternals';
18 const {ReactCurrentActQueue} = ReactSharedInternals;
19
20 -// TODO: Sparse arrays are bad for performance.
21 -export opaque type ThenableState = Array<Thenable<any> | void>;
20 +export opaque type ThenableState = Array<Thenable<any>>;
21
22 let thenableState: ThenableState | null = null;
23
@@ -62,7 +61,9 @@ export function isThenableStateResolved(thenables: ThenableState): boolean {
61 return true;
62 }
63
65 -export function trackUsedThenable<T>(thenable: Thenable<T>, index: number) {
64 +function noop(): void {}
65 +
66 +export function trackUsedThenable<T>(thenable: Thenable<T>, index: number): T {
67 if (__DEV__ && ReactCurrentActQueue.current !== null) {
68 ReactCurrentActQueue.didUsePromise = true;
69 }
@@ -70,7 +71,20 @@ export function trackUsedThenable<T>(thenable: Thenable<T>, index: number) {
71 if (thenableState === null) {
72 thenableState = [thenable];
73 } else {
73 - thenableState[index] = thenable;
74 + const previous = thenableState[index];
75 + if (previous === undefined) {
76 + thenableState.push(thenable);
77 + } else {
78 + if (previous !== thenable) {
79 + // Reuse the previous thenable, and drop the new one. We can assume
80 + // they represent the same value, because components are idempotent.
81 +
82 + // Avoid an unhandled rejection errors for the Promises that we'll
83 + // intentionally ignore.
84 + thenable.then(noop, noop);
85 + thenable = previous;
86 + }
87 + }
88 }
89
90 // We use an expando to track the status and result of a thenable so that we
@@ -80,52 +94,48 @@ export function trackUsedThenable<T>(thenable: Thenable<T>, index: number) {
94 // If the thenable doesn't have a status, set it to "pending" and attach
95 // a listener that will update its status and result when it resolves.
96 switch (thenable.status) {
83 - case 'fulfilled':
84 - case 'rejected':
85 - // A thenable that already resolved shouldn't have been thrown, so this is
86 - // unexpected. Suggests a mistake in a userspace data library. Don't track
87 - // this thenable, because if we keep trying it will likely infinite loop
88 - // without ever resolving.
89 - // TODO: Log a warning?
90 - break;
97 + case 'fulfilled': {
98 + const fulfilledValue: T = thenable.value;
99 + return fulfilledValue;
100 + }
101 + case 'rejected': {
102 + const rejectedError = thenable.reason;
103 + throw rejectedError;
104 + }
105 default: {
106 if (typeof thenable.status === 'string') {
107 // Only instrument the thenable if the status if not defined. If
108 // it's defined, but an unknown value, assume it's been instrumented by
109 // some custom userspace implementation. We treat it as "pending".
96 - break;
110 + } else {
111 + const pendingThenable: PendingThenable<mixed> = (thenable: any);
112 + pendingThenable.status = 'pending';
113 + pendingThenable.then(
114 + fulfilledValue => {
115 + if (thenable.status === 'pending') {
116 + const fulfilledThenable: FulfilledThenable<mixed> = (thenable: any);
117 + fulfilledThenable.status = 'fulfilled';
118 + fulfilledThenable.value = fulfilledValue;
119 + }
120 + },
121 + (error: mixed) => {
122 + if (thenable.status === 'pending') {
123 + const rejectedThenable: RejectedThenable<mixed> = (thenable: any);
124 + rejectedThenable.status = 'rejected';
125 + rejectedThenable.reason = error;
126 + }
127 + },
128 + );
129 }
98 - const pendingThenable: PendingThenable<mixed> = (thenable: any);
99 - pendingThenable.status = 'pending';
100 - pendingThenable.then(
101 - fulfilledValue => {
102 - if (thenable.status === 'pending') {
103 - const fulfilledThenable: FulfilledThenable<mixed> = (thenable: any);
104 - fulfilledThenable.status = 'fulfilled';
105 - fulfilledThenable.value = fulfilledValue;
106 - }
107 - },
108 - (error: mixed) => {
109 - if (thenable.status === 'pending') {
110 - const rejectedThenable: RejectedThenable<mixed> = (thenable: any);
111 - rejectedThenable.status = 'rejected';
112 - rejectedThenable.reason = error;
113 - }
114 - },
115 - );
116 - break;
117 - }
118 - }
119 -}
130
121 -export function getPreviouslyUsedThenableAtIndex<T>(
122 - index: number,
123 -): Thenable<T> | null {
124 - if (thenableState !== null) {
125 - const thenable = thenableState[index];
126 - if (thenable !== undefined) {
127 - return thenable;
131 + // Suspend.
132 + // TODO: Throwing here is an implementation detail that allows us to
133 + // unwind the call stack. But we shouldn't allow it to leak into
134 + // userspace. Throw an opaque placeholder value instead of the
135 + // actual thenable. If it doesn't get captured by the work loop, log
136 + // a warning, because that means something in userspace must have
137 + // caught it.
138 + throw thenable;
139 }
140 }
130 - return null;
141 }
packages/react-server/src/ReactFizzHooks.js
+4 -60
@@ -25,11 +25,7 @@ import type {ThenableState} from './ReactFizzThenable';
25
26 import {readContext as readContextImpl} from './ReactFizzNewContext';
27 import {getTreeId} from './ReactFizzTreeContext';
28 -import {
29 - getPreviouslyUsedThenableAtIndex,
30 - createThenableState,
31 - trackUsedThenable,
32 -} from './ReactFizzThenable';
28 +import {createThenableState, trackUsedThenable} from './ReactFizzThenable';
29
30 import {makeId} from './ReactServerFormatConfig';
31
@@ -593,62 +589,10 @@ function use<T>(usable: Usable<T>): T {
589 const index = thenableIndexCounter;
590 thenableIndexCounter += 1;
591
596 - // TODO: Unify this switch statement with the one in trackUsedThenable.
597 - switch (thenable.status) {
598 - case 'fulfilled': {
599 - const fulfilledValue: T = thenable.value;
600 - return fulfilledValue;
601 - }
602 - case 'rejected': {
603 - const rejectedError = thenable.reason;
604 - throw rejectedError;
605 - }
606 - default: {
607 - const prevThenableAtIndex: Thenable<T> | null = getPreviouslyUsedThenableAtIndex(
608 - thenableState,
609 - index,
610 - );
611 - if (prevThenableAtIndex !== null) {
612 - if (thenable !== prevThenableAtIndex) {
613 - // Avoid an unhandled rejection errors for the Promises that we'll
614 - // intentionally ignore.
615 - thenable.then(noop, noop);
616 - }
617 - switch (prevThenableAtIndex.status) {
618 - case 'fulfilled': {
619 - const fulfilledValue: T = prevThenableAtIndex.value;
620 - return fulfilledValue;
621 - }
622 - case 'rejected': {
623 - const rejectedError: mixed = prevThenableAtIndex.reason;
624 - throw rejectedError;
625 - }
626 - default: {
627 - // The thenable still hasn't resolved. Suspend with the same
628 - // thenable as last time to avoid redundant listeners.
629 - throw prevThenableAtIndex;
630 - }
631 - }
632 - } else {
633 - // This is the first time something has been used at this index.
634 - // Stash the thenable at the current index so we can reuse it during
635 - // the next attempt.
636 - if (thenableState === null) {
637 - thenableState = createThenableState();
638 - }
639 - trackUsedThenable(thenableState, thenable, index);
640 -
641 - // Suspend.
642 - // TODO: Throwing here is an implementation detail that allows us to
643 - // unwind the call stack. But we shouldn't allow it to leak into
644 - // userspace. Throw an opaque placeholder value instead of the
645 - // actual thenable. If it doesn't get captured by the work loop, log
646 - // a warning, because that means something in userspace must have
647 - // caught it.
648 - throw thenable;
649 - }
650 - }
592 + if (thenableState === null) {
593 + thenableState = createThenableState();
594 }
595 + return trackUsedThenable(thenableState, thenable, index);
596 } else if (
597 usable.$$typeof === REACT_CONTEXT_TYPE ||
598 usable.$$typeof === REACT_SERVER_CONTEXT_TYPE
packages/react-server/src/ReactFizzThenable.js
+53 -44
@@ -20,8 +20,7 @@ import type {
20 RejectedThenable,
21 } from 'shared/ReactTypes';
22
23 -// TODO: Sparse arrays are bad for performance.
24 -export opaque type ThenableState = Array<Thenable<any> | void>;
23 +export opaque type ThenableState = Array<Thenable<any>>;
24
25 export function createThenableState(): ThenableState {
26 // The ThenableState is created the first time a component suspends. If it
@@ -29,12 +28,27 @@ export function createThenableState(): ThenableState {
28 return [];
29 }
30
31 +function noop(): void {}
32 +
33 export function trackUsedThenable<T>(
34 thenableState: ThenableState,
35 thenable: Thenable<T>,
36 index: number,
36 -) {
37 - thenableState[index] = thenable;
37 +): T {
38 + const previous = thenableState[index];
39 + if (previous === undefined) {
40 + thenableState.push(thenable);
41 + } else {
42 + if (previous !== thenable) {
43 + // Reuse the previous thenable, and drop the new one. We can assume
44 + // they represent the same value, because components are idempotent.
45 +
46 + // Avoid an unhandled rejection errors for the Promises that we'll
47 + // intentionally ignore.
48 + thenable.then(noop, noop);
49 + thenable = previous;
50 + }
51 + }
52
53 // We use an expando to track the status and result of a thenable so that we
54 // can synchronously unwrap the value. Think of this as an extension of the
@@ -43,53 +57,48 @@ export function trackUsedThenable<T>(
57 // If the thenable doesn't have a status, set it to "pending" and attach
58 // a listener that will update its status and result when it resolves.
59 switch (thenable.status) {
46 - case 'fulfilled':
47 - case 'rejected':
48 - // A thenable that already resolved shouldn't have been thrown, so this is
49 - // unexpected. Suggests a mistake in a userspace data library. Don't track
50 - // this thenable, because if we keep trying it will likely infinite loop
51 - // without ever resolving.
52 - // TODO: Log a warning?
53 - break;
60 + case 'fulfilled': {
61 + const fulfilledValue: T = thenable.value;
62 + return fulfilledValue;
63 + }
64 + case 'rejected': {
65 + const rejectedError = thenable.reason;
66 + throw rejectedError;
67 + }
68 default: {
69 if (typeof thenable.status === 'string') {
70 // Only instrument the thenable if the status if not defined. If
71 // it's defined, but an unknown value, assume it's been instrumented by
72 // some custom userspace implementation. We treat it as "pending".
59 - break;
73 + } else {
74 + const pendingThenable: PendingThenable<mixed> = (thenable: any);
75 + pendingThenable.status = 'pending';
76 + pendingThenable.then(
77 + fulfilledValue => {
78 + if (thenable.status === 'pending') {
79 + const fulfilledThenable: FulfilledThenable<mixed> = (thenable: any);
80 + fulfilledThenable.status = 'fulfilled';
81 + fulfilledThenable.value = fulfilledValue;
82 + }
83 + },
84 + (error: mixed) => {
85 + if (thenable.status === 'pending') {
86 + const rejectedThenable: RejectedThenable<mixed> = (thenable: any);
87 + rejectedThenable.status = 'rejected';
88 + rejectedThenable.reason = error;
89 + }
90 + },
91 + );
92 }
61 - const pendingThenable: PendingThenable<mixed> = (thenable: any);
62 - pendingThenable.status = 'pending';
63 - pendingThenable.then(
64 - fulfilledValue => {
65 - if (thenable.status === 'pending') {
66 - const fulfilledThenable: FulfilledThenable<mixed> = (thenable: any);
67 - fulfilledThenable.status = 'fulfilled';
68 - fulfilledThenable.value = fulfilledValue;
69 - }
70 - },
71 - (error: mixed) => {
72 - if (thenable.status === 'pending') {
73 - const rejectedThenable: RejectedThenable<mixed> = (thenable: any);
74 - rejectedThenable.status = 'rejected';
75 - rejectedThenable.reason = error;
76 - }
77 - },
78 - );
79 - break;
80 - }
81 - }
82 -}
93
84 -export function getPreviouslyUsedThenableAtIndex<T>(
85 - thenableState: ThenableState | null,
86 - index: number,
87 -): Thenable<T> | null {
88 - if (thenableState !== null) {
89 - const thenable = thenableState[index];
90 - if (thenable !== undefined) {
91 - return thenable;
94 + // Suspend.
95 + // TODO: Throwing here is an implementation detail that allows us to
96 + // unwind the call stack. But we shouldn't allow it to leak into
97 + // userspace. Throw an opaque placeholder value instead of the
98 + // actual thenable. If it doesn't get captured by the work loop, log
99 + // a warning, because that means something in userspace must have
100 + // caught it.
101 + throw thenable;
102 }
103 }
94 - return null;
104 }
packages/react-server/src/ReactFlightHooks.js
+4 -61
@@ -17,11 +17,7 @@ import {
17 } from 'shared/ReactSymbols';
18 import {readContext as readContextImpl} from './ReactFlightNewContext';
19 import {enableUseHook} from 'shared/ReactFeatureFlags';
20 -import {
21 - getPreviouslyUsedThenableAtIndex,
22 - createThenableState,
23 - trackUsedThenable,
24 -} from './ReactFlightThenable';
20 +import {createThenableState, trackUsedThenable} from './ReactFlightThenable';
21
22 let currentRequest = null;
23 let thenableIndexCounter = 0;
@@ -121,8 +117,6 @@ function useId(): string {
117 return ':' + currentRequest.identifierPrefix + 'S' + id.toString(32) + ':';
118 }
119
124 -function noop(): void {}
125 -
120 function use<T>(usable: Usable<T>): T {
121 if (usable !== null && typeof usable === 'object') {
122 // $FlowFixMe[method-unbinding]
@@ -134,61 +128,10 @@ function use<T>(usable: Usable<T>): T {
128 const index = thenableIndexCounter;
129 thenableIndexCounter += 1;
130
137 - switch (thenable.status) {
138 - case 'fulfilled': {
139 - const fulfilledValue: T = thenable.value;
140 - return fulfilledValue;
141 - }
142 - case 'rejected': {
143 - const rejectedError = thenable.reason;
144 - throw rejectedError;
145 - }
146 - default: {
147 - const prevThenableAtIndex: Thenable<T> | null = getPreviouslyUsedThenableAtIndex(
148 - thenableState,
149 - index,
150 - );
151 - if (prevThenableAtIndex !== null) {
152 - if (thenable !== prevThenableAtIndex) {
153 - // Avoid an unhandled rejection errors for the Promises that we'll
154 - // intentionally ignore.
155 - thenable.then(noop, noop);
156 - }
157 - switch (prevThenableAtIndex.status) {
158 - case 'fulfilled': {
159 - const fulfilledValue: T = prevThenableAtIndex.value;
160 - return fulfilledValue;
161 - }
162 - case 'rejected': {
163 - const rejectedError: mixed = prevThenableAtIndex.reason;
164 - throw rejectedError;
165 - }
166 - default: {
167 - // The thenable still hasn't resolved. Suspend with the same
168 - // thenable as last time to avoid redundant listeners.
169 - throw prevThenableAtIndex;
170 - }
171 - }
172 - } else {
173 - // This is the first time something has been used at this index.
174 - // Stash the thenable at the current index so we can reuse it during
175 - // the next attempt.
176 - if (thenableState === null) {
177 - thenableState = createThenableState();
178 - }
179 - trackUsedThenable(thenableState, thenable, index);
180 -
181 - // Suspend.
182 - // TODO: Throwing here is an implementation detail that allows us to
183 - // unwind the call stack. But we shouldn't allow it to leak into
184 - // userspace. Throw an opaque placeholder value instead of the
185 - // actual thenable. If it doesn't get captured by the work loop, log
186 - // a warning, because that means something in userspace must have
187 - // caught it.
188 - throw thenable;
189 - }
190 - }
131 + if (thenableState === null) {
132 + thenableState = createThenableState();
133 }
134 + return trackUsedThenable(thenableState, thenable, index);
135 } else if (usable.$$typeof === REACT_SERVER_CONTEXT_TYPE) {
136 const context: ReactServerContext<T> = (usable: any);
137 return readContext(context);
packages/react-server/src/ReactFlightThenable.js
+53 -44
@@ -20,8 +20,7 @@ import type {
20 RejectedThenable,
21 } from 'shared/ReactTypes';
22
23 -// TODO: Sparse arrays are bad for performance.
24 -export opaque type ThenableState = Array<Thenable<any> | void>;
23 +export opaque type ThenableState = Array<Thenable<any>>;
24
25 export function createThenableState(): ThenableState {
26 // The ThenableState is created the first time a component suspends. If it
@@ -29,12 +28,27 @@ export function createThenableState(): ThenableState {
28 return [];
29 }
30
31 +function noop(): void {}
32 +
33 export function trackUsedThenable<T>(
34 thenableState: ThenableState,
35 thenable: Thenable<T>,
36 index: number,
36 -) {
37 - thenableState[index] = thenable;
37 +): T {
38 + const previous = thenableState[index];
39 + if (previous === undefined) {
40 + thenableState.push(thenable);
41 + } else {
42 + if (previous !== thenable) {
43 + // Reuse the previous thenable, and drop the new one. We can assume
44 + // they represent the same value, because components are idempotent.
45 +
46 + // Avoid an unhandled rejection errors for the Promises that we'll
47 + // intentionally ignore.
48 + thenable.then(noop, noop);
49 + thenable = previous;
50 + }
51 + }
52
53 // We use an expando to track the status and result of a thenable so that we
54 // can synchronously unwrap the value. Think of this as an extension of the
@@ -43,53 +57,48 @@ export function trackUsedThenable<T>(
57 // If the thenable doesn't have a status, set it to "pending" and attach
58 // a listener that will update its status and result when it resolves.
59 switch (thenable.status) {
46 - case 'fulfilled':
47 - case 'rejected':
48 - // A thenable that already resolved shouldn't have been thrown, so this is
49 - // unexpected. Suggests a mistake in a userspace data library. Don't track
50 - // this thenable, because if we keep trying it will likely infinite loop
51 - // without ever resolving.
52 - // TODO: Log a warning?
53 - break;
60 + case 'fulfilled': {
61 + const fulfilledValue: T = thenable.value;
62 + return fulfilledValue;
63 + }
64 + case 'rejected': {
65 + const rejectedError = thenable.reason;
66 + throw rejectedError;
67 + }
68 default: {
69 if (typeof thenable.status === 'string') {
70 // Only instrument the thenable if the status if not defined. If
71 // it's defined, but an unknown value, assume it's been instrumented by
72 // some custom userspace implementation. We treat it as "pending".
59 - break;
73 + } else {
74 + const pendingThenable: PendingThenable<mixed> = (thenable: any);
75 + pendingThenable.status = 'pending';
76 + pendingThenable.then(
77 + fulfilledValue => {
78 + if (thenable.status === 'pending') {
79 + const fulfilledThenable: FulfilledThenable<mixed> = (thenable: any);
80 + fulfilledThenable.status = 'fulfilled';
81 + fulfilledThenable.value = fulfilledValue;
82 + }
83 + },
84 + (error: mixed) => {
85 + if (thenable.status === 'pending') {
86 + const rejectedThenable: RejectedThenable<mixed> = (thenable: any);
87 + rejectedThenable.status = 'rejected';
88 + rejectedThenable.reason = error;
89 + }
90 + },
91 + );
92 }
61 - const pendingThenable: PendingThenable<mixed> = (thenable: any);
62 - pendingThenable.status = 'pending';
63 - pendingThenable.then(
64 - fulfilledValue => {
65 - if (thenable.status === 'pending') {
66 - const fulfilledThenable: FulfilledThenable<mixed> = (thenable: any);
67 - fulfilledThenable.status = 'fulfilled';
68 - fulfilledThenable.value = fulfilledValue;
69 - }
70 - },
71 - (error: mixed) => {
72 - if (thenable.status === 'pending') {
73 - const rejectedThenable: RejectedThenable<mixed> = (thenable: any);
74 - rejectedThenable.status = 'rejected';
75 - rejectedThenable.reason = error;
76 - }
77 - },
78 - );
79 - break;
80 - }
81 - }
82 -}
93
84 -export function getPreviouslyUsedThenableAtIndex<T>(
85 - thenableState: ThenableState | null,
86 - index: number,
87 -): Thenable<T> | null {
88 - if (thenableState !== null) {
89 - const thenable = thenableState[index];
90 - if (thenable !== undefined) {
91 - return thenable;
94 + // Suspend.
95 + // TODO: Throwing here is an implementation detail that allows us to
96 + // unwind the call stack. But we shouldn't allow it to leak into
97 + // userspace. Throw an opaque placeholder value instead of the
98 + // actual thenable. If it doesn't get captured by the work loop, log
99 + // a warning, because that means something in userspace must have
100 + // caught it.
101 + throw thenable;
102 }
103 }
94 - return null;
104 }