16
enableProfilerTimer,
17
enableComponentPerformanceTrack,
18
enableAsyncDebugInfo,
19
+ enableFlightWeakThenables,
20
} from 'shared/ReactFeatureFlags';
21
22
import {
1079
);
1080
}
1081
1081
-function serializeThenable(
1082
+function createThenableTask(
1083
request: Request,
1084
task: Task,
1085
thenable: Thenable<any>,
1085
-): number {
1086
- const newTask = createTask(
1086
+): Task {
1087
+ return createTask(
1088
request,
1089
thenable as any, // will be replaced by the value before we retry. used for debug info.
1090
task.keyPath, // the server component sequence continues through Promise-as-a-child.
1099
__DEV__ ? task.debugStack : null,
1100
__DEV__ ? task.debugTask : null,
1101
);
1102
+}
1103
1104
+function serializeThenable(
1105
+ request: Request,
1106
+ task: Task,
1107
+ thenable: Thenable<any>,
1108
+): number {
1109
switch (thenable.status) {
1110
case 'fulfilled': {
1111
+ const newTask = createThenableTask(request, task, thenable);
1112
forwardDebugInfoFromThenable(request, newTask, thenable, null, null);
1113
// We have the resolved value, we can go ahead and schedule it for serialization.
1114
newTask.model = thenable.value;
1116
return newTask.id;
1117
}
1118
case 'rejected': {
1119
+ const newTask = createThenableTask(request, task, thenable);
1120
forwardDebugInfoFromThenable(request, newTask, thenable, null, null);
1121
const x = thenable.reason;
1122
erroredTask(request, newTask, x);
1123
return newTask.id;
1124
}
1125
+ case 'pending_weak': {
1126
+ if (enableFlightWeakThenables) {
1127
+ // A weak-pending thenable doesn't block the stream from closing, so
1128
+ // we don't create a task for it yet. We only reserve an id for its
1129
+ // reference. If it settles while the stream is still open, we
1130
+ // create the task at that point, the same as if we had serialized
1131
+ // an already settled thenable.
1132
+ //
1133
+ // Delivery is driven by the thenable's notification. If the stream
1134
+ // closes before the listeners are notified, the value is dropped
1135
+ // and the reference is left unfulfilled. Since the stream may close
1136
+ // synchronously when the last task completes, a thenable that
1137
+ // notifies its listeners synchronously (unlike a native Promise,
1138
+ // which notifies in a microtask) is guaranteed delivery of any
1139
+ // value it settles with before the stream closes.
1140
+ const id = request.nextChunkId++;
1141
+ // The parent task is mutated as serialization continues, so we
1142
+ // snapshot the context that the new task needs if it's created
1143
+ // later.
1144
+ const keyPath = task.keyPath;
1145
+ const implicitSlot = task.implicitSlot;
1146
+ const formatContext = task.formatContext;
1147
+ const lastTimestamp =
1148
+ enableProfilerTimer &&
1149
+ (enableComponentPerformanceTrack || enableAsyncDebugInfo)
1150
+ ? task.time
1151
+ : 0;
1152
+ const debugOwner = __DEV__ ? task.debugOwner : null;
1153
+ const debugStack = __DEV__ ? task.debugStack : null;
1154
+ const debugTask = __DEV__ ? task.debugTask : null;
1155
+ let settled = false;
1156
+ thenable.then(
1157
+ (value: any) => {
1158
+ if (settled || request.status > OPEN) {
1159
+ // Too late. The stream already closed (or the request was
1160
+ // aborted), so the reference stays unfulfilled.
1161
+ return;
1162
+ }
1163
+ settled = true;
1164
+ const newTask = createTaskWithID(
1165
+ request,
1166
+ id,
1167
+ value,
1168
+ keyPath,
1169
+ implicitSlot,
1170
+ formatContext,
1171
+ request.abortableTasks,
1172
+ lastTimestamp,
1173
+ debugOwner,
1174
+ debugStack,
1175
+ debugTask,
1176
+ );
1177
+ forwardDebugInfoFromCurrentContext(request, newTask, thenable);
1178
+ pingTask(request, newTask);
1179
+ },
1180
+ (reason: mixed) => {
1181
+ if (settled || request.status > OPEN) {
1182
+ return;
1183
+ }
1184
+ settled = true;
1185
+ const newTask = createTaskWithID(
1186
+ request,
1187
+ id,
1188
+ thenable as any, // never rendered. used for debug info.
1189
+ keyPath,
1190
+ implicitSlot,
1191
+ formatContext,
1192
+ request.abortableTasks,
1193
+ lastTimestamp,
1194
+ debugOwner,
1195
+ debugStack,
1196
+ debugTask,
1197
+ );
1198
+ if (
1199
+ enableProfilerTimer &&
1200
+ (enableComponentPerformanceTrack || enableAsyncDebugInfo)
1201
+ ) {
1202
+ // If this is async we need to time when this task finishes.
1203
+ newTask.timed = true;
1204
+ }
1205
+ erroredTask(request, newTask, reason);
1206
+ enqueueFlush(request);
1207
+ },
1208
+ );
1209
+ return id;
1210
+ }
1211
+ // Fallthrough
1212
+ }
1213
default: {
1214
+ const newTask = createThenableTask(request, task, thenable);
1215
if (request.status === ABORTING) {
1216
// We can no longer accept any resolved values
1217
request.abortableTasks.delete(newTask);
1225
}
1226
return newTask.id;
1227
}
1130
- if (typeof thenable.status === 'string') {
1228
+ if (typeof thenable.status !== 'string') {
1229
// Only instrument the thenable if the status if not defined. If
1230
// it's defined, but an unknown value, assume it's been instrumented by
1231
// some custom userspace implementation. We treat it as "pending".
1134
- break;
1232
+ const pendingThenable: PendingThenable<mixed> = thenable as any;
1233
+ pendingThenable.status = 'pending';
1234
+ pendingThenable.then(
1235
+ fulfilledValue => {
1236
+ if (thenable.status === 'pending') {
1237
+ const fulfilledThenable: FulfilledThenable<mixed> =
1238
+ thenable as any;
1239
+ fulfilledThenable.status = 'fulfilled';
1240
+ fulfilledThenable.value = fulfilledValue;
1241
+ }
1242
+ },
1243
+ (error: mixed) => {
1244
+ if (thenable.status === 'pending') {
1245
+ const rejectedThenable: RejectedThenable<mixed> = thenable as any;
1246
+ rejectedThenable.status = 'rejected';
1247
+ rejectedThenable.reason = error;
1248
+ }
1249
+ },
1250
+ );
1251
}
1136
- const pendingThenable: PendingThenable<mixed> = thenable as any;
1137
- pendingThenable.status = 'pending';
1138
- pendingThenable.then(
1139
- fulfilledValue => {
1140
- if (thenable.status === 'pending') {
1141
- const fulfilledThenable: FulfilledThenable<mixed> = thenable as any;
1142
- fulfilledThenable.status = 'fulfilled';
1143
- fulfilledThenable.value = fulfilledValue;
1144
- }
1252
+ thenable.then(
1253
+ value => {
1254
+ forwardDebugInfoFromCurrentContext(request, newTask, thenable);
1255
+ newTask.model = value;
1256
+ pingTask(request, newTask);
1257
},
1146
- (error: mixed) => {
1147
- if (thenable.status === 'pending') {
1148
- const rejectedThenable: RejectedThenable<mixed> = thenable as any;
1149
- rejectedThenable.status = 'rejected';
1150
- rejectedThenable.reason = error;
1258
+ reason => {
1259
+ if (newTask.status === PENDING) {
1260
+ if (
1261
+ enableProfilerTimer &&
1262
+ (enableComponentPerformanceTrack || enableAsyncDebugInfo)
1263
+ ) {
1264
+ // If this is async we need to time when this task finishes.
1265
+ newTask.timed = true;
1266
+ }
1267
+ // We expect that the only status it might be otherwise is ABORTED.
1268
+ // When we abort we emit chunks in each pending task slot and don't need
1269
+ // to do so again here.
1270
+ erroredTask(request, newTask, reason);
1271
+ enqueueFlush(request);
1272
}
1273
},
1274
);
1154
- break;
1275
+ return newTask.id;
1276
}
1277
}
1157
-
1158
- thenable.then(
1159
- value => {
1160
- forwardDebugInfoFromCurrentContext(request, newTask, thenable);
1161
- newTask.model = value;
1162
- pingTask(request, newTask);
1163
- },
1164
- reason => {
1165
- if (newTask.status === PENDING) {
1166
- if (
1167
- enableProfilerTimer &&
1168
- (enableComponentPerformanceTrack || enableAsyncDebugInfo)
1169
- ) {
1170
- // If this is async we need to time when this task finishes.
1171
- newTask.timed = true;
1172
- }
1173
- // We expect that the only status it might be otherwise is ABORTED.
1174
- // When we abort we emit chunks in each pending task slot and don't need
1175
- // to do so again here.
1176
- erroredTask(request, newTask, reason);
1177
- enqueueFlush(request);
1178
- }
1179
- },
1180
- );
1181
-
1182
- return newTask.id;
1278
}
1279
1280
function serializeReadableStream(
2855
debugOwner: null | ReactComponentInfo, // DEV-only
2856
debugStack: null | Error, // DEV-only
2857
debugTask: null | ConsoleTask, // DEV-only
2858
+): Task {
2859
+ return createTaskWithID(
2860
+ request,
2861
+ request.nextChunkId++,
2862
+ model,
2863
+ keyPath,
2864
+ implicitSlot,
2865
+ formatContext,
2866
+ abortSet,
2867
+ lastTimestamp,
2868
+ debugOwner,
2869
+ debugStack,
2870
+ debugTask,
2871
+ );
2872
+}
2873
+
2874
+function createTaskWithID(
2875
+ request: Request,
2876
+ id: number,
2877
+ model: ReactClientValue,
2878
+ keyPath: ReactKey,
2879
+ implicitSlot: boolean,
2880
+ formatContext: FormatContext,
2881
+ abortSet: Set<Task>,
2882
+ lastTimestamp: number, // Profiling-only
2883
+ debugOwner: null | ReactComponentInfo, // DEV-only
2884
+ debugStack: null | Error, // DEV-only
2885
+ debugTask: null | ConsoleTask, // DEV-only
2886
): Task {
2887
request.pendingChunks++;
2765
- const id = request.nextChunkId++;
2888
if (typeof model === 'object' && model !== null) {
2889
// If we're about to write this into a new task we can assign it an ID early so that
2890
// any other references can refer to the value we're about to write.
3064
return '$@' + id.toString(16);
3065
}
3066
3067
+function serializeWeakPromiseID(id: number): string {
3068
+ return '$w' + id.toString(16);
3069
+}
3070
+
3071
function serializeServerReferenceID(id: number): string {
3072
return '$h' + id.toString(16);
3073
}
3954
const existingReference = writtenObjects.get(value);
3955
// $FlowFixMe[method-unbinding]
3956
if (typeof value.then === 'function') {
3957
+ // A weak-pending thenable may never emit, so its reference is marked
3958
+ // on the wire ($w instead of $@). That way the client knows to leave
3959
+ // it forever pending, instead of erroring it, if the stream closes
3960
+ // first.
3961
if (existingReference !== undefined) {
3962
if (task.keyPath !== null || task.implicitSlot) {
3963
// If we're in some kind of context we can't reuse the result of this render or
3964
// previous renders of this element. We only reuse Promises if they're not wrapped
3965
// by another Server Component.
3966
const promiseId = serializeThenable(request, task, value as any);
3837
- return serializePromiseID(promiseId);
3967
+ return enableFlightWeakThenables &&
3968
+ (value as any).status === 'pending_weak'
3969
+ ? serializeWeakPromiseID(promiseId)
3970
+ : serializePromiseID(promiseId);
3971
} else if (modelRoot === value) {
3972
// This is the ID we're currently emitting so we need to write it
3973
// once but if we discover it again, we refer to it by id.
3980
// We assume that any object with a .then property is a "Thenable" type,
3981
// or a Promise type. Either of which can be represented by a Promise.
3982
const promiseId = serializeThenable(request, task, value as any);
3850
- const promiseReference = serializePromiseID(promiseId);
3983
+ const promiseReference =
3984
+ enableFlightWeakThenables && (value as any).status === 'pending_weak'
3985
+ ? serializeWeakPromiseID(promiseId)
3986
+ : serializePromiseID(promiseId);
3987
writtenObjects.set(value, promiseReference);
3988
return promiseReference;
3989
}
6293
request.completedErrorChunks.push(processedChunk);
6294
}
6295
6296
+// "Halting" a task means finishing it without emitting anything into its
6297
+// slot: the reference is intentionally left unfulfilled and never resolves
6298
+// on the client. This is how an aborted prerender leaves its pending work.
6299
+// It's also the same outcome as a weak-pending thenable that never settles
6300
+// (see serializeThenable) — halting is initiated by the request aborting,
6301
+// weakness by the value itself.
6302
function haltTask(task: Task, request: Request): void {
6303
if (task.status !== PENDING) {
6304
// If this is already completed/errored we don't abort it.