Skip to content

Commit 920e9b8

Browse files
committed
Serialize already resolved Promises as debug models
1 parent ed07719 commit 920e9b8

File tree

4 files changed

+204
-47
lines changed

4 files changed

+204
-47
lines changed

packages/react-client/src/ReactFlightClient.js

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ const RESOLVED_MODEL = 'resolved_model';
155155
const RESOLVED_MODULE = 'resolved_module';
156156
const INITIALIZED = 'fulfilled';
157157
const ERRORED = 'rejected';
158+
const HALTED = 'halted'; // DEV-only. Means it never resolves even if connection closes.
158159

159160
type PendingChunk<T> = {
160161
status: 'pending',
@@ -221,13 +222,23 @@ type ErroredChunk<T> = {
221222
_debugInfo?: null | ReactDebugInfo, // DEV-only
222223
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
223224
};
225+
type HaltedChunk<T> = {
226+
status: 'halted',
227+
value: null,
228+
reason: null,
229+
_response: Response,
230+
_children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
231+
_debugInfo?: null | ReactDebugInfo, // DEV-only
232+
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
233+
};
224234
type SomeChunk<T> =
225235
| PendingChunk<T>
226236
| BlockedChunk<T>
227237
| ResolvedModelChunk<T>
228238
| ResolvedModuleChunk<T>
229239
| InitializedChunk<T>
230-
| ErroredChunk<T>;
240+
| ErroredChunk<T>
241+
| HaltedChunk<T>;
231242

232243
// $FlowFixMe[missing-this-annot]
233244
function ReactPromise(
@@ -311,6 +322,9 @@ ReactPromise.prototype.then = function <T>(
311322
chunk.reason.push(reject);
312323
}
313324
break;
325+
case HALTED: {
326+
break;
327+
}
314328
default:
315329
if (reject) {
316330
reject(chunk.reason);
@@ -368,6 +382,7 @@ function readChunk<T>(chunk: SomeChunk<T>): T {
368382
return chunk.value;
369383
case PENDING:
370384
case BLOCKED:
385+
case HALTED:
371386
// eslint-disable-next-line no-throw-literal
372387
throw ((chunk: any): Thenable<T>);
373388
default:
@@ -1367,6 +1382,7 @@ function getOutlinedModel<T>(
13671382
return chunkValue;
13681383
case PENDING:
13691384
case BLOCKED:
1385+
case HALTED:
13701386
return waitForReference(chunk, parentObject, key, response, map, path);
13711387
default:
13721388
// This is an error. Instead of erroring directly, we're going to encode this on
@@ -1470,10 +1486,6 @@ function parseModelString(
14701486
}
14711487
case '@': {
14721488
// Promise
1473-
if (value.length === 2) {
1474-
// Infinite promise that never resolves.
1475-
return new Promise(() => {});
1476-
}
14771489
const id = parseInt(value.slice(2), 16);
14781490
const chunk = getChunk(response, id);
14791491
if (enableProfilerTimer && enableComponentPerformanceTrack) {
@@ -1769,6 +1781,22 @@ export function createResponse(
17691781
);
17701782
}
17711783

1784+
function resolveDebugHalt(response: Response, id: number): void {
1785+
const chunks = response._chunks;
1786+
let chunk = chunks.get(id);
1787+
if (!chunk) {
1788+
chunks.set(id, (chunk = createPendingChunk(response)));
1789+
} else {
1790+
}
1791+
if (chunk.status !== PENDING && chunk.status !== BLOCKED) {
1792+
return;
1793+
}
1794+
const haltedChunk: HaltedChunk<any> = (chunk: any);
1795+
haltedChunk.status = HALTED;
1796+
haltedChunk.value = null;
1797+
haltedChunk.reason = null;
1798+
}
1799+
17721800
function resolveModel(
17731801
response: Response,
17741802
id: number,
@@ -3337,6 +3365,10 @@ function processFullStringRow(
33373365
}
33383366
// Fallthrough
33393367
default: /* """ "{" "[" "t" "f" "n" "0" - "9" */ {
3368+
if (__DEV__ && row === '') {
3369+
resolveDebugHalt(response, id);
3370+
return;
3371+
}
33403372
// We assume anything else is JSON.
33413373
resolveModel(response, id, row);
33423374
return;

packages/react-client/src/__tests__/ReactFlight-test.js

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3213,7 +3213,8 @@ describe('ReactFlight', () => {
32133213
prop: 123,
32143214
fn: foo,
32153215
map: new Map([['foo', foo]]),
3216-
promise: new Promise(() => {}),
3216+
promise: Promise.resolve('yo'),
3217+
infinitePromise: new Promise(() => {}),
32173218
});
32183219
throw new Error('err');
32193220
}
@@ -3258,9 +3259,14 @@ describe('ReactFlight', () => {
32583259
});
32593260
ownerStacks = [];
32603261

3262+
// Let the Promises resolve.
3263+
await 0;
3264+
await 0;
3265+
await 0;
3266+
32613267
// The error should not actually get logged because we're not awaiting the root
32623268
// so it's not thrown but the server log also shouldn't be replayed.
3263-
await ReactNoopFlightClient.read(transport);
3269+
await ReactNoopFlightClient.read(transport, {close: true});
32643270

32653271
expect(mockConsoleLog).toHaveBeenCalledTimes(1);
32663272
expect(mockConsoleLog.mock.calls[0][0]).toBe('hi');
@@ -3280,6 +3286,23 @@ describe('ReactFlight', () => {
32803286

32813287
const promise = mockConsoleLog.mock.calls[0][1].promise;
32823288
expect(promise).toBeInstanceOf(Promise);
3289+
expect(await promise).toBe('yo');
3290+
3291+
const infinitePromise = mockConsoleLog.mock.calls[0][1].infinitePromise;
3292+
expect(infinitePromise).toBeInstanceOf(Promise);
3293+
let resolved = false;
3294+
infinitePromise.then(
3295+
() => (resolved = true),
3296+
x => {
3297+
console.error(x);
3298+
resolved = true;
3299+
},
3300+
);
3301+
await 0;
3302+
await 0;
3303+
await 0;
3304+
// This should not reject upon aborting the stream.
3305+
expect(resolved).toBe(false);
32833306

32843307
expect(ownerStacks).toEqual(['\n in App (at **)']);
32853308
});

packages/react-noop-renderer/src/ReactNoopFlightClient.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ type Source = Array<Uint8Array>;
2424

2525
const decoderOptions = {stream: true};
2626

27-
const {createResponse, processBinaryChunk, getRoot} = ReactFlightClient({
27+
const {createResponse, processBinaryChunk, getRoot, close} = ReactFlightClient({
2828
createStringDecoder() {
2929
return new TextDecoder();
3030
},
@@ -56,6 +56,7 @@ const {createResponse, processBinaryChunk, getRoot} = ReactFlightClient({
5656

5757
type ReadOptions = {|
5858
findSourceMapURL?: FindSourceMapURLCallback,
59+
close?: boolean,
5960
|};
6061

6162
function read<T>(source: Source, options: ReadOptions): Thenable<T> {
@@ -74,6 +75,9 @@ function read<T>(source: Source, options: ReadOptions): Thenable<T> {
7475
for (let i = 0; i < source.length; i++) {
7576
processBinaryChunk(response, source[i], 0);
7677
}
78+
if (options !== undefined && options.close) {
79+
close(response);
80+
}
7781
return getRoot(response);
7882
}
7983

0 commit comments

Comments
 (0)