Merge 40ba0571c2 into sapling-pr-archive-josephsavona

This commit is contained in:
Joseph Savona
2025-09-09 20:11:41 -07:00
committed by GitHub
36 changed files with 3462 additions and 467 deletions
@@ -282,6 +282,30 @@ export class CompilerError extends Error {
disabledDetails: Array<CompilerErrorDetail | CompilerDiagnostic> = [];
printedMessage: string | null = null;
static simpleInvariant(
condition: unknown,
options: {
reason: CompilerDiagnosticOptions['reason'];
description?: CompilerDiagnosticOptions['description'];
loc: SourceLocation;
},
): asserts condition {
if (!condition) {
const errors = new CompilerError();
errors.pushDiagnostic(
CompilerDiagnostic.create({
reason: options.reason,
description: options.description ?? null,
category: ErrorCategory.Invariant,
}).withDetails({
kind: 'error',
loc: options.loc,
message: options.reason,
}),
);
throw errors;
}
}
static invariant(
condition: unknown,
options: Omit<CompilerDiagnosticOptions, 'category'>,
@@ -104,6 +104,7 @@ import {inferMutationAliasingEffects} from '../Inference/InferMutationAliasingEf
import {inferMutationAliasingRanges} from '../Inference/InferMutationAliasingRanges';
import {validateNoDerivedComputationsInEffects} from '../Validation/ValidateNoDerivedComputationsInEffects';
import {nameAnonymousFunctions} from '../Transform/NameAnonymousFunctions';
import {validateExhaustiveDependencies} from '../Validation/ValidateExhaustiveDependencies';
export type CompilerPipelineValue =
| {kind: 'ast'; name: string; value: CodegenFunction}
@@ -293,6 +294,11 @@ function runWithEnvironment(
inferReactivePlaces(hir);
log({kind: 'hir', name: 'InferReactivePlaces', value: hir});
if (env.config.validateExhaustiveMemoizationDependencies) {
// NOTE: this relies on reactivity inference running first
validateExhaustiveDependencies(hir).unwrap();
}
rewriteInstructionKindsBasedOnReassignment(hir);
log({
kind: 'hir',
@@ -227,6 +227,11 @@ export const EnvironmentConfigSchema = z.object({
*/
validatePreserveExistingMemoizationGuarantees: z.boolean().default(true),
/**
* Validate that dependencies supplied to manual memoization calls are exhaustive.
*/
validateExhaustiveMemoizationDependencies: z.boolean().default(false),
/**
* When this is true, rather than pruning existing manual memoization but ensuring or validating
* that the memoized values remain memoized, the compiler will simply not prune existing calls to
@@ -1680,6 +1680,28 @@ export function areEqualPaths(a: DependencyPath, b: DependencyPath): boolean {
)
);
}
export function isSubPath(
subpath: DependencyPath,
path: DependencyPath,
): boolean {
return (
subpath.length <= path.length &&
subpath.every(
(item, ix) =>
item.property === path[ix].property &&
item.optional === path[ix].optional,
)
);
}
export function isSubPathIgnoringOptionals(
subpath: DependencyPath,
path: DependencyPath,
): boolean {
return (
subpath.length <= path.length &&
subpath.every((item, ix) => item.property === path[ix].property)
);
}
export function getPlaceScope(
id: InstructionId,
@@ -0,0 +1,624 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import prettyFormat from 'pretty-format';
import {CompilerDiagnostic, CompilerError, SourceLocation} from '..';
import {ErrorCategory} from '../CompilerError';
import {
areEqualPaths,
BlockId,
DependencyPath,
FinishMemoize,
HIRFunction,
Identifier,
IdentifierId,
InstructionKind,
isSubPath,
isSubPathIgnoringOptionals,
LoadGlobal,
ManualMemoDependency,
Place,
StartMemoize,
} from '../HIR';
import {printIdentifier, printManualMemoDependency} from '../HIR/PrintHIR';
import {
eachInstructionLValue,
eachInstructionValueLValue,
eachInstructionValueOperand,
eachTerminalOperand,
} from '../HIR/visitors';
import {Result} from '../Utils/Result';
const DEBUG = false;
/**
* Validates that existing manual memoization had exhaustive dependencies.
* Memoization with missing or extra reactive dependencies is invalid React
* and compilation can change behavior, causing a value to be computed more
* or less times.
*
* TODOs:
* - Better handling of cases where we infer multiple dependencies related to a single
* variable. Eg if the user has dep `x` and we inferred `x.y, x.z`, the user's dep
* is sufficient.
* - Handle cases where the user deps were not simple identifiers + property chains.
* We try to detect this in ValidateUseMemo but we miss some cases. The problem
* is that invalid forms can be value blocks or function calls that don't get
* removed by DCE, leaving a structure like:
*
* StartMemoize
* t0 = <value to memoize>
* ...non-DCE'd code for manual deps...
* FinishMemoize decl=t0
*
* When we go to compute the dependencies, we then think that the user's manual dep
* logic is part of what the memo computation logic.
*/
export function validateExhaustiveDependencies(
fn: HIRFunction,
): Result<void, CompilerError> {
const reactive = collectReactiveIdentifiersHIR(fn);
const temporaries: Map<IdentifierId, Temporary> = new Map();
for (const param of fn.params) {
const place = param.kind === 'Identifier' ? param : param.place;
temporaries.set(place.identifier.id, {
kind: 'Local',
identifier: place.identifier,
path: [],
context: false,
loc: place.loc,
});
}
const error = new CompilerError();
let startMemo: StartMemoize | null = null;
function onStartMemoize(
value: StartMemoize,
dependencies: Set<Temporary>,
locals: Set<IdentifierId>,
): void {
CompilerError.simpleInvariant(startMemo == null, {
reason: 'Unexpected nested memo calls',
loc: value.loc,
});
startMemo = value;
dependencies.clear();
locals.clear();
}
function onFinishMemoize(
value: FinishMemoize,
dependencies: Set<Temporary>,
locals: Set<IdentifierId>,
): void {
CompilerError.simpleInvariant(
startMemo != null && startMemo.manualMemoId === value.manualMemoId,
{
reason: 'Found FinishMemoize without corresponding StartMemoize',
loc: value.loc,
},
);
visitCandidateDependency(value.decl, temporaries, dependencies);
const inferred: Array<Temporary> = [];
for (const dep of dependencies) {
if (inferred.find(x => isEqualTemporary(x, dep)) != null) {
continue;
}
inferred.push(dep);
}
// Validate that all manual dependencies belong there
if (DEBUG) {
console.log('manual');
console.log(
(startMemo.deps ?? [])
.map(x => ' ' + printManualMemoDependency(x, false))
.join('\n'),
);
console.log('inferred');
console.log(inferred.map(x => ' ' + _printTemporary(x)).join('\n'));
}
const manualDependencies = startMemo.deps ?? [];
const matched: Set<ManualMemoDependency> = new Set();
for (const inferredDependency of inferred) {
if (inferredDependency.kind === 'Global') {
continue;
} else if (inferredDependency.kind === 'Function') {
CompilerError.simpleInvariant(false, {
reason: 'Unexpected function dependency',
loc: value.loc,
});
}
let hasMatchingManualDependency = false;
for (const manualDependency of manualDependencies) {
if (
manualDependency.root.kind === 'NamedLocal' &&
manualDependency.root.value.identifier.id ===
inferredDependency.identifier.id &&
(areEqualPaths(manualDependency.path, inferredDependency.path) ||
isSubPath(manualDependency.path, inferredDependency.path))
) {
hasMatchingManualDependency = true;
matched.add(manualDependency);
}
}
if (!hasMatchingManualDependency) {
/**
* Find any "extra" dependencies that are more precise versions of the dependency
* For example, the dep may be `x.y`, if the user specified `x.y.z` we want to give
* a hint
*/
const morePreciseDependencies = [];
for (const manualDependency of manualDependencies) {
if (
manualDependency.root.kind === 'NamedLocal' &&
manualDependency.root.value.identifier.id ===
inferredDependency.identifier.id &&
isSubPathIgnoringOptionals(
inferredDependency.path,
manualDependency.path,
)
) {
matched.add(manualDependency);
morePreciseDependencies.push(manualDependency);
}
}
const diagnostic = CompilerDiagnostic.create({
category: ErrorCategory.PreserveManualMemo,
reason: 'Found missing memoization dependency',
description:
'Missing dependencies can cause a value not to update when those inputs change, ' +
'resulting in stale UI. This memoization cannot be safely rewritten by the compiler.',
}).withDetails({
kind: 'error',
message:
'Missing dependency' + ` ${_printTemporary(inferredDependency)}`,
loc: inferredDependency.loc,
});
for (const extra of morePreciseDependencies) {
diagnostic.withDetails({
kind: 'hint',
message: `Found similar dependency \`${printManualMemoDependency(extra, false)}\``,
});
}
error.pushDiagnostic(diagnostic);
}
}
for (const dep of startMemo.deps ?? []) {
if (
matched.has(dep) ||
(dep.root.kind === 'NamedLocal' &&
!reactive.has(dep.root.value.identifier.id))
) {
continue;
}
error.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.PreserveManualMemo,
reason: 'Found unnecessary memoization dependency',
description:
'Adding unnecessary memoization dependencies can cause a value to recompute ' +
'more often than necessary and change behavior. This memoization cannot be safely rewritten by the compiler.',
}).withDetails({
kind: 'error',
message:
'Unnecessary dependency' +
` ${printManualMemoDependency(dep, false)}`,
loc: startMemo.loc,
}),
);
}
// TODO: validate that all inferred dependencies were in manual deps list too
dependencies.clear();
locals.clear();
startMemo = null;
}
collectTemporaries(fn, temporaries, {
onStartMemoize,
onFinishMemoize,
});
return error.asResult();
}
function visitCandidateDependency(
place: Place,
temporaries: Map<IdentifierId, Temporary>,
dependencies: Set<Temporary>,
): void {
const dep = temporaries.get(place.identifier.id);
if (dep != null) {
if (dep.kind === 'Function') {
dep.dependencies.forEach(x => dependencies.add(x));
} else {
dependencies.add(dep);
}
}
}
function collectTemporaries(
fn: HIRFunction,
temporaries: Map<IdentifierId, Temporary>,
callbacks: {
onStartMemoize: (
startMemo: StartMemoize,
dependencies: Set<Temporary>,
locals: Set<IdentifierId>,
) => void;
onFinishMemoize: (
finishMemo: FinishMemoize,
dependencies: Set<Temporary>,
locals: Set<IdentifierId>,
) => void;
} | null,
): Extract<Temporary, {kind: 'Function'}> {
const optionals = findOptionalPlaces(fn);
if (DEBUG) {
console.log(prettyFormat(optionals));
}
const locals: Set<IdentifierId> = new Set();
const dependencies: Set<Temporary> = new Set();
function visit(place: Place): void {
visitCandidateDependency(place, temporaries, dependencies);
}
for (const block of fn.body.blocks.values()) {
for (const phi of block.phis) {
let deps: Array<Temporary> | null = null;
for (const operand of phi.operands.values()) {
const dep = temporaries.get(operand.identifier.id);
if (dep == null) {
continue;
}
if (deps == null) {
deps = [dep];
} else {
deps.push(dep);
}
}
if (deps == null) {
continue;
} else if (deps.length === 1) {
temporaries.set(phi.place.identifier.id, deps[0]!);
} else {
temporaries.set(phi.place.identifier.id, {
kind: 'Function',
dependencies: new Set(deps),
});
}
}
for (const instr of block.instructions) {
const {lvalue, value} = instr;
switch (value.kind) {
case 'LoadGlobal': {
temporaries.set(lvalue.identifier.id, {
kind: 'Global',
binding: value.binding,
});
break;
}
case 'LoadContext':
case 'LoadLocal': {
if (locals.has(value.place.identifier.id)) {
break;
}
const temp = temporaries.get(value.place.identifier.id);
if (temp != null) {
if (temp.kind === 'Local') {
const local: Temporary = {...temp, loc: value.place.loc};
temporaries.set(lvalue.identifier.id, local);
} else {
temporaries.set(lvalue.identifier.id, temp);
}
}
break;
}
case 'DeclareLocal': {
const local: Temporary = {
kind: 'Local',
identifier: value.lvalue.place.identifier,
path: [],
context: false,
loc: value.lvalue.place.loc,
};
temporaries.set(value.lvalue.place.identifier.id, local);
locals.add(value.lvalue.place.identifier.id);
break;
}
case 'StoreLocal': {
if (value.lvalue.place.identifier.name == null) {
const temp = temporaries.get(value.value.identifier.id);
if (temp != null) {
temporaries.set(value.lvalue.place.identifier.id, temp);
}
break;
}
visit(value.value);
if (value.lvalue.kind !== InstructionKind.Reassign) {
const local: Temporary = {
kind: 'Local',
identifier: value.lvalue.place.identifier,
path: [],
context: false,
loc: value.lvalue.place.loc,
};
temporaries.set(value.lvalue.place.identifier.id, local);
locals.add(value.lvalue.place.identifier.id);
}
break;
}
case 'DeclareContext': {
const local: Temporary = {
kind: 'Local',
identifier: value.lvalue.place.identifier,
path: [],
context: true,
loc: value.lvalue.place.loc,
};
temporaries.set(value.lvalue.place.identifier.id, local);
break;
}
case 'StoreContext': {
visit(value.value);
if (value.lvalue.kind !== InstructionKind.Reassign) {
const local: Temporary = {
kind: 'Local',
identifier: value.lvalue.place.identifier,
path: [],
context: true,
loc: value.lvalue.place.loc,
};
temporaries.set(value.lvalue.place.identifier.id, local);
locals.add(value.lvalue.place.identifier.id);
}
break;
}
case 'Destructure': {
visit(value.value);
if (value.lvalue.kind !== InstructionKind.Reassign) {
for (const lvalue of eachInstructionValueLValue(value)) {
const local: Temporary = {
kind: 'Local',
identifier: lvalue.identifier,
path: [],
context: false,
loc: lvalue.loc,
};
temporaries.set(lvalue.identifier.id, local);
locals.add(lvalue.identifier.id);
}
}
break;
}
case 'PropertyLoad': {
if (typeof value.property === 'number') {
visit(value.object);
break;
}
const object = temporaries.get(value.object.identifier.id);
if (object != null && object.kind === 'Local') {
const optional = optionals.get(value.object.identifier.id) ?? false;
const local: Temporary = {
kind: 'Local',
identifier: object.identifier,
context: object.context,
path: [
...object.path,
{
optional,
property: value.property,
},
],
loc: value.loc,
};
temporaries.set(lvalue.identifier.id, local);
}
break;
}
case 'FunctionExpression':
case 'ObjectMethod': {
const functionDeps = collectTemporaries(
value.loweredFunc.func,
temporaries,
null,
);
temporaries.set(lvalue.identifier.id, functionDeps);
for (const dep of functionDeps.dependencies) {
dependencies.add(dep);
}
break;
}
case 'StartMemoize': {
const onStartMemoize = callbacks?.onStartMemoize;
if (onStartMemoize != null) {
onStartMemoize(value, dependencies, locals);
}
break;
}
case 'FinishMemoize': {
const onFinishMemoize = callbacks?.onFinishMemoize;
if (onFinishMemoize != null) {
onFinishMemoize(value, dependencies, locals);
}
break;
}
case 'MethodCall': {
// Ignore the method itself
for (const operand of eachInstructionValueOperand(value)) {
if (operand.identifier.id === value.property.identifier.id) {
continue;
}
visit(operand);
}
break;
}
default: {
for (const operand of eachInstructionValueOperand(value)) {
visit(operand);
}
for (const lvalue of eachInstructionLValue(instr)) {
locals.add(lvalue.identifier.id);
}
}
}
}
for (const operand of eachTerminalOperand(block.terminal)) {
if (optionals.has(operand.identifier.id)) {
continue;
}
visit(operand);
}
}
return {kind: 'Function', dependencies};
}
function _printTemporary(temporary: Temporary): string {
switch (temporary.kind) {
case 'Global': {
return `Global ${temporary.binding.name} [${temporary.binding.kind}]`;
}
case 'Local': {
return `Local${temporary.context ? ' (Context)' : ''} ${printIdentifier(temporary.identifier)}${temporary.path.map(p => (p.optional ? '?' : '') + '.' + p.property).join('')}`;
}
case 'Function': {
return `Function dependencies=[${Array.from(temporary.dependencies).map(_printTemporary).join(', ')}]`;
}
}
}
function isEqualTemporary(a: Temporary, b: Temporary): boolean {
switch (a.kind) {
case 'Function': {
// TODO: ideally remove Function kind
return false;
}
case 'Global': {
return b.kind === 'Global' && a.binding.name === b.binding.name;
}
case 'Local': {
return (
b.kind === 'Local' &&
a.identifier.id === b.identifier.id &&
areEqualPaths(a.path, b.path)
);
}
}
}
type Temporary =
| {kind: 'Global'; binding: LoadGlobal['binding']}
| {
kind: 'Local';
identifier: Identifier;
path: DependencyPath;
context: boolean;
loc: SourceLocation;
}
| {kind: 'Function'; dependencies: Set<Temporary>};
function collectReactiveIdentifiersHIR(fn: HIRFunction): Set<IdentifierId> {
const reactive = new Set<IdentifierId>();
for (const block of fn.body.blocks.values()) {
for (const instr of block.instructions) {
for (const lvalue of eachInstructionLValue(instr)) {
if (lvalue.reactive) {
reactive.add(lvalue.identifier.id);
}
}
for (const operand of eachInstructionValueOperand(instr.value)) {
if (operand.reactive) {
reactive.add(operand.identifier.id);
}
}
}
for (const operand of eachTerminalOperand(block.terminal)) {
if (operand.reactive) {
reactive.add(operand.identifier.id);
}
}
}
return reactive;
}
export function findOptionalPlaces(
fn: HIRFunction,
): Map<IdentifierId, boolean> {
const optionals = new Map<IdentifierId, boolean>();
const visited: Set<BlockId> = new Set();
for (const [, block] of fn.body.blocks) {
if (visited.has(block.id)) {
continue;
}
if (block.terminal.kind === 'optional') {
visited.add(block.id);
const optionalTerminal = block.terminal;
let testBlock = fn.body.blocks.get(block.terminal.test)!;
const queue: Array<boolean | null> = [block.terminal.optional];
loop: while (true) {
visited.add(testBlock.id);
const terminal = testBlock.terminal;
switch (terminal.kind) {
case 'branch': {
const isOptional = queue.pop();
CompilerError.simpleInvariant(isOptional !== undefined, {
reason:
'Expected an optional value for each optional test condition',
loc: terminal.test.loc,
});
if (isOptional != null) {
optionals.set(terminal.test.identifier.id, isOptional);
}
if (terminal.fallthrough === optionalTerminal.fallthrough) {
// found it
const consequent = fn.body.blocks.get(terminal.consequent)!;
const last = consequent.instructions.at(-1);
if (last !== undefined && last.value.kind === 'StoreLocal') {
if (isOptional != null) {
optionals.set(last.value.value.identifier.id, isOptional);
}
}
break loop;
} else {
testBlock = fn.body.blocks.get(terminal.fallthrough)!;
}
break;
}
case 'optional': {
queue.push(terminal.optional);
testBlock = fn.body.blocks.get(terminal.test)!;
break;
}
case 'logical':
case 'ternary': {
queue.push(null);
testBlock = fn.body.blocks.get(terminal.test)!;
break;
}
case 'sequence': {
// Do we need sequence?? In any case, don't push to queue bc there is no corresponding branch terminal
testBlock = fn.body.blocks.get(terminal.block)!;
break;
}
default: {
CompilerError.simpleInvariant(false, {
reason: `Unexpected terminal in optional`,
loc: terminal.loc,
});
}
}
}
CompilerError.simpleInvariant(queue.length === 0, {
reason:
'Expected a matching number of conditional blocks and branch points',
loc: block.terminal.loc,
});
}
}
return optionals;
}
@@ -18,7 +18,6 @@ import {
IdentifierId,
InstructionValue,
ManualMemoDependency,
Place,
PrunedReactiveScopeBlock,
ReactiveFunction,
ReactiveInstruction,
@@ -29,7 +28,10 @@ import {
SourceLocation,
} from '../HIR';
import {printIdentifier, printManualMemoDependency} from '../HIR/PrintHIR';
import {eachInstructionValueOperand} from '../HIR/visitors';
import {
eachInstructionValueLValue,
eachInstructionValueOperand,
} from '../HIR/visitors';
import {collectMaybeMemoDependencies} from '../Inference/DropManualMemoization';
import {
ReactiveFunctionVisitor,
@@ -337,56 +339,53 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
* @returns a @{ManualMemoDependency} representing the variable +
* property reads represented by @value
*/
recordDepsInValue(
value: ReactiveValue,
state: VisitorState,
): ManualMemoDependency | null {
recordDepsInValue(value: ReactiveValue, state: VisitorState): void {
switch (value.kind) {
case 'SequenceExpression': {
for (const instr of value.instructions) {
this.visitInstruction(instr, state);
}
const result = this.recordDepsInValue(value.value, state);
return result;
this.recordDepsInValue(value.value, state);
break;
}
case 'OptionalExpression': {
return this.recordDepsInValue(value.value, state);
this.recordDepsInValue(value.value, state);
break;
}
case 'ConditionalExpression': {
this.recordDepsInValue(value.test, state);
this.recordDepsInValue(value.consequent, state);
this.recordDepsInValue(value.alternate, state);
return null;
break;
}
case 'LogicalExpression': {
this.recordDepsInValue(value.left, state);
this.recordDepsInValue(value.right, state);
return null;
break;
}
default: {
const dep = collectMaybeMemoDependencies(
value,
this.temporaries,
false,
);
if (value.kind === 'StoreLocal' || value.kind === 'StoreContext') {
const storeTarget = value.lvalue.place;
state.manualMemoState?.decls.add(
storeTarget.identifier.declarationId,
);
if (storeTarget.identifier.name?.kind === 'named' && dep == null) {
const dep: ManualMemoDependency = {
root: {
kind: 'NamedLocal',
value: storeTarget,
},
path: [],
};
this.temporaries.set(storeTarget.identifier.id, dep);
return dep;
collectMaybeMemoDependencies(value, this.temporaries, false);
if (
value.kind === 'StoreLocal' ||
value.kind === 'StoreContext' ||
value.kind === 'Destructure'
) {
for (const storeTarget of eachInstructionValueLValue(value)) {
state.manualMemoState?.decls.add(
storeTarget.identifier.declarationId,
);
if (storeTarget.identifier.name?.kind === 'named') {
this.temporaries.set(storeTarget.identifier.id, {
root: {
kind: 'NamedLocal',
value: storeTarget,
},
path: [],
});
}
}
}
return dep;
break;
}
}
}
@@ -403,19 +402,15 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
state.manualMemoState.decls.add(lvalue.identifier.declarationId);
}
const maybeDep = this.recordDepsInValue(value, state);
if (lvalId != null) {
if (maybeDep != null) {
temporaries.set(lvalId, maybeDep);
} else if (isNamedLocal) {
temporaries.set(lvalId, {
root: {
kind: 'NamedLocal',
value: {...(instr.lvalue as Place)},
},
path: [],
});
}
this.recordDepsInValue(value, state);
if (lvalue != null) {
temporaries.set(lvalue.identifier.id, {
root: {
kind: 'NamedLocal',
value: {...lvalue},
},
path: [],
});
}
}
@@ -0,0 +1,84 @@
## Input
```javascript
// @validateExhaustiveMemoizationDependencies
import {useMemo} from 'react';
import {Stringify} from 'shared-runtime';
function Component({x, y, z}) {
const a = useMemo(() => {
return x?.y.z?.a;
}, [x?.y.z?.a.b]);
const b = useMemo(() => {
return x.y.z?.a;
}, [x.y.z.a]);
const c = useMemo(() => {
return x?.y.z.a?.b;
}, [x?.y.z.a?.b.z]);
const d = useMemo(() => {
return x?.y?.[(console.log(y), z?.b)];
}, [x?.y, y, z?.b]);
const e = useMemo(() => {
const e = [];
e.push(x);
return e;
}, [x]);
return <Stringify results={[a, b, c, d, e]} />;
}
```
## Error
```
Found 3 errors:
Compilation Skipped: Found missing memoization dependency
Missing dependencies can cause a value not to update when those inputs change, resulting in stale UI. This memoization cannot be safely rewritten by the compiler..
error.invalid-exhaustive-deps.ts:7:11
5 | function Component({x, y, z}) {
6 | const a = useMemo(() => {
> 7 | return x?.y.z?.a;
| ^^^^^^^^^ Missing dependency Local x$181?.y.z?.a
8 | }, [x?.y.z?.a.b]);
9 | const b = useMemo(() => {
10 | return x.y.z?.a;
Found similar dependency `x$181?.y.z?.a.b`
Compilation Skipped: Found missing memoization dependency
Missing dependencies can cause a value not to update when those inputs change, resulting in stale UI. This memoization cannot be safely rewritten by the compiler..
error.invalid-exhaustive-deps.ts:10:11
8 | }, [x?.y.z?.a.b]);
9 | const b = useMemo(() => {
> 10 | return x.y.z?.a;
| ^^^^^^^^ Missing dependency Local x$181.y.z?.a
11 | }, [x.y.z.a]);
12 | const c = useMemo(() => {
13 | return x?.y.z.a?.b;
Found similar dependency `x$181.y.z.a`
Compilation Skipped: Found missing memoization dependency
Missing dependencies can cause a value not to update when those inputs change, resulting in stale UI. This memoization cannot be safely rewritten by the compiler..
error.invalid-exhaustive-deps.ts:13:11
11 | }, [x.y.z.a]);
12 | const c = useMemo(() => {
> 13 | return x?.y.z.a?.b;
| ^^^^^^^^^^^ Missing dependency Local x$181?.y.z.a?.b
14 | }, [x?.y.z.a?.b.z]);
15 | const d = useMemo(() => {
16 | return x?.y?.[(console.log(y), z?.b)];
Found similar dependency `x$181?.y.z.a?.b.z`
```
@@ -0,0 +1,24 @@
// @validateExhaustiveMemoizationDependencies
import {useMemo} from 'react';
import {Stringify} from 'shared-runtime';
function Component({x, y, z}) {
const a = useMemo(() => {
return x?.y.z?.a;
}, [x?.y.z?.a.b]);
const b = useMemo(() => {
return x.y.z?.a;
}, [x.y.z.a]);
const c = useMemo(() => {
return x?.y.z.a?.b;
}, [x?.y.z.a?.b.z]);
const d = useMemo(() => {
return x?.y?.[(console.log(y), z?.b)];
}, [x?.y, y, z?.b]);
const e = useMemo(() => {
const e = [];
e.push(x);
return e;
}, [x]);
return <Stringify results={[a, b, c, d, e]} />;
}
@@ -0,0 +1,59 @@
## Input
```javascript
// @validatePreserveExistingMemoizationGuarantees
/**
* Repro from https://github.com/facebook/react/issues/34262
*
* The compiler memoizes more precisely than the original code, with two reactive scopes:
* - One for `transform(input)` with `input` as dep
* - One for `{value}` with `value` as dep
*
* When we validate preserving manual memoization we incorrectly reject this, because
* the original memoization had `object` depending on `input` but our scope depends on
* `value`.
*
* This fixture adds a later potential mutation, which extends the scope and should
* fail validation. This confirms that even though we allow the dependency to diverge,
* we still check that the output value is memoized.
*/
function useInputValue(input) {
const object = React.useMemo(() => {
const {value} = transform(input);
return {value};
}, [input]);
mutate(object);
return object;
}
```
## Error
```
Found 1 error:
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output.
error.repro-preserve-memoization-inner-destructured-value-mistaken-as-dependency-later-mutation.ts:19:17
17 | */
18 | function useInputValue(input) {
> 19 | const object = React.useMemo(() => {
| ^^^^^^^^^^^^^^^^^^^^^
> 20 | const {value} = transform(input);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 21 | return {value};
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 22 | }, [input]);
| ^^^^^^^^^^^^^^ Could not preserve existing memoization
23 | mutate(object);
24 | return object;
25 | }
```
@@ -0,0 +1,25 @@
// @validatePreserveExistingMemoizationGuarantees
/**
* Repro from https://github.com/facebook/react/issues/34262
*
* The compiler memoizes more precisely than the original code, with two reactive scopes:
* - One for `transform(input)` with `input` as dep
* - One for `{value}` with `value` as dep
*
* When we validate preserving manual memoization we incorrectly reject this, because
* the original memoization had `object` depending on `input` but our scope depends on
* `value`.
*
* This fixture adds a later potential mutation, which extends the scope and should
* fail validation. This confirms that even though we allow the dependency to diverge,
* we still check that the output value is memoized.
*/
function useInputValue(input) {
const object = React.useMemo(() => {
const {value} = transform(input);
return {value};
}, [input]);
mutate(object);
return object;
}
@@ -0,0 +1,64 @@
## Input
```javascript
// @validatePreserveExistingMemoizationGuarantees
import {identity, Stringify, useHook} from 'shared-runtime';
/**
* Repro from https://github.com/facebook/react/issues/34262
*
* The compiler memoizes more precisely than the original code, with two reactive scopes:
* - One for `transform(input)` with `input` as dep
* - One for `{value}` with `value` as dep
*
* When we validate preserving manual memoization we incorrectly reject this, because
* the original memoization had `object` depending on `input` but our scope depends on
* `value`.
*/
function useInputValue(input) {
// Conflate the `identity(input, x)` call with something outside the useMemo,
// to try and break memoization of `value`. This gets correctly flagged since
// the dependency is being mutated
let x = {};
useHook();
const object = React.useMemo(() => {
const {value} = identity(input, x);
return {value};
}, [input, x]);
return object;
}
function Component() {
return <Stringify value={useInputValue({value: 42}).value} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};
```
## Error
```
Found 1 error:
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This dependency may be mutated later, which could cause the value to change unexpectedly.
error.repro-preserve-memoization-inner-destructured-value-mistaken-as-dependency-mutated-dep.ts:25:13
23 | const {value} = identity(input, x);
24 | return {value};
> 25 | }, [input, x]);
| ^ This dependency may be modified later
26 | return object;
27 | }
28 |
```
@@ -0,0 +1,36 @@
// @validatePreserveExistingMemoizationGuarantees
import {identity, Stringify, useHook} from 'shared-runtime';
/**
* Repro from https://github.com/facebook/react/issues/34262
*
* The compiler memoizes more precisely than the original code, with two reactive scopes:
* - One for `transform(input)` with `input` as dep
* - One for `{value}` with `value` as dep
*
* When we validate preserving manual memoization we incorrectly reject this, because
* the original memoization had `object` depending on `input` but our scope depends on
* `value`.
*/
function useInputValue(input) {
// Conflate the `identity(input, x)` call with something outside the useMemo,
// to try and break memoization of `value`. This gets correctly flagged since
// the dependency is being mutated
let x = {};
useHook();
const object = React.useMemo(() => {
const {value} = identity(input, x);
return {value};
}, [input, x]);
return object;
}
function Component() {
return <Stringify value={useInputValue({value: 42}).value} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};
@@ -0,0 +1,121 @@
## Input
```javascript
// @validateExhaustiveMemoizationDependencies
import {useMemo} from 'react';
import {Stringify} from 'shared-runtime';
function Component({x, y, z}) {
const a = useMemo(() => {
return x?.y.z?.a;
}, [x?.y.z?.a]);
const b = useMemo(() => {
return x.y.z?.a;
}, [x.y.z?.a]);
const c = useMemo(() => {
return x?.y.z.a?.b;
}, [x?.y.z.a?.b]);
const d = useMemo(() => {
return x?.y?.[(console.log(y), z?.b)];
}, [x?.y, y, z?.b]);
const e = useMemo(() => {
const e = [];
e.push(x);
return e;
}, [x]);
return <Stringify results={[a, b, c, d, e]} />;
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateExhaustiveMemoizationDependencies
import { useMemo } from "react";
import { Stringify } from "shared-runtime";
function Component(t0) {
const $ = _c(18);
const { x, y, z } = t0;
x?.y.z?.a;
const a = x?.y.z?.a;
let t1;
if ($[0] !== x.y.z.a) {
t1 = () => x.y.z?.a;
$[0] = x.y.z.a;
$[1] = t1;
} else {
t1 = $[1];
}
x.y.z?.a;
let t2;
if ($[2] !== t1) {
t2 = t1();
$[2] = t1;
$[3] = t2;
} else {
t2 = $[3];
}
const b = t2;
x?.y.z.a?.b;
const c = x?.y.z.a?.b;
let t3;
if ($[4] !== x.y || $[5] !== y || $[6] !== z?.b) {
t3 = () => x?.y?.[(console.log(y), z?.b)];
$[4] = x.y;
$[5] = y;
$[6] = z?.b;
$[7] = t3;
} else {
t3 = $[7];
}
x?.y;
z?.b;
let t4;
if ($[8] !== t3) {
t4 = t3();
$[8] = t3;
$[9] = t4;
} else {
t4 = $[9];
}
const d = t4;
let e;
if ($[10] !== x) {
e = [];
e.push(x);
$[10] = x;
$[11] = e;
} else {
e = $[11];
}
const e_0 = e;
let t5;
if (
$[12] !== a ||
$[13] !== b ||
$[14] !== c ||
$[15] !== d ||
$[16] !== e_0
) {
t5 = <Stringify results={[a, b, c, d, e_0]} />;
$[12] = a;
$[13] = b;
$[14] = c;
$[15] = d;
$[16] = e_0;
$[17] = t5;
} else {
t5 = $[17];
}
return t5;
}
```
### Eval output
(kind: exception) Fixture not implemented
@@ -0,0 +1,24 @@
// @validateExhaustiveMemoizationDependencies
import {useMemo} from 'react';
import {Stringify} from 'shared-runtime';
function Component({x, y, z}) {
const a = useMemo(() => {
return x?.y.z?.a;
}, [x?.y.z?.a]);
const b = useMemo(() => {
return x.y.z?.a;
}, [x.y.z?.a]);
const c = useMemo(() => {
return x?.y.z.a?.b;
}, [x?.y.z.a?.b]);
const d = useMemo(() => {
return x?.y?.[(console.log(y), z?.b)];
}, [x?.y, y, z?.b]);
const e = useMemo(() => {
const e = [];
e.push(x);
return e;
}, [x]);
return <Stringify results={[a, b, c, d, e]} />;
}
@@ -0,0 +1,109 @@
## Input
```javascript
// @validatePreserveExistingMemoizationGuarantees
import {identity, Stringify} from 'shared-runtime';
/**
* Repro from https://github.com/facebook/react/issues/34262
*
* The compiler memoizes more precisely than the original code, with two reactive scopes:
* - One for `transform(input)` with `input` as dep
* - One for `{value}` with `value` as dep
*
* Previously ValidatePreservedManualMemoization rejected this input, because
* the original memoization had `object` depending on `input` but we split the scope per above,
* and the scope for the FinishMemoize instruction is the second scope which depends on `value`
*/
function useInputValue(input) {
const object = React.useMemo(() => {
const {value} = identity(input);
return {value};
}, [input]);
return object;
}
function Component() {
return <Stringify value={useInputValue({value: 42}).value} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees
import { identity, Stringify } from "shared-runtime";
/**
* Repro from https://github.com/facebook/react/issues/34262
*
* The compiler memoizes more precisely than the original code, with two reactive scopes:
* - One for `transform(input)` with `input` as dep
* - One for `{value}` with `value` as dep
*
* Previously ValidatePreservedManualMemoization rejected this input, because
* the original memoization had `object` depending on `input` but we split the scope per above,
* and the scope for the FinishMemoize instruction is the second scope which depends on `value`
*/
function useInputValue(input) {
const $ = _c(4);
let t0;
if ($[0] !== input) {
t0 = identity(input);
$[0] = input;
$[1] = t0;
} else {
t0 = $[1];
}
const { value } = t0;
let t1;
if ($[2] !== value) {
t1 = { value };
$[2] = value;
$[3] = t1;
} else {
t1 = $[3];
}
const object = t1;
return object;
}
function Component() {
const $ = _c(3);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = { value: 42 };
$[0] = t0;
} else {
t0 = $[0];
}
const t1 = useInputValue(t0);
let t2;
if ($[1] !== t1.value) {
t2 = <Stringify value={t1.value} />;
$[1] = t1.value;
$[2] = t2;
} else {
t2 = $[2];
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};
```
### Eval output
(kind: ok) <div>{"value":42}</div>
@@ -0,0 +1,31 @@
// @validatePreserveExistingMemoizationGuarantees
import {identity, Stringify} from 'shared-runtime';
/**
* Repro from https://github.com/facebook/react/issues/34262
*
* The compiler memoizes more precisely than the original code, with two reactive scopes:
* - One for `transform(input)` with `input` as dep
* - One for `{value}` with `value` as dep
*
* Previously ValidatePreservedManualMemoization rejected this input, because
* the original memoization had `object` depending on `input` but we split the scope per above,
* and the scope for the FinishMemoize instruction is the second scope which depends on `value`
*/
function useInputValue(input) {
const object = React.useMemo(() => {
const {value} = identity(input);
return {value};
}, [input]);
return object;
}
function Component() {
return <Stringify value={useInputValue({value: 42}).value} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};
@@ -34,4 +34,8 @@ const configs = {
},
};
export {configs, allRules as rules, meta};
const rules = Object.fromEntries(
Object.entries(allRules).map(([name, {rule}]) => [name, rule]),
);
export {configs, rules, meta};
+7
View File
@@ -21,6 +21,8 @@ import {Note} from './cjs/Note.js';
import {GenerateImage} from './GenerateImage.js';
import LargeContent from './LargeContent.js';
import {like, greet, increment} from './actions.js';
import {getServerState} from './ServerState.js';
@@ -233,6 +235,11 @@ export default async function App({prerender, noCache}) {
<Foo>{dedupedChild}</Foo>
<Bar>{Promise.resolve([dedupedChild])}</Bar>
<Navigate />
{prerender ? null : ( // TODO: prerender is broken for large content for some reason.
<React.Suspense fallback={null}>
<LargeContent />
</React.Suspense>
)}
</Container>
</body>
</html>
File diff suppressed because it is too large Load Diff
+344 -47
View File
@@ -366,6 +366,7 @@ type Response = {
_debugRootOwner?: null | ReactComponentInfo, // DEV-only
_debugRootStack?: null | Error, // DEV-only
_debugRootTask?: null | ConsoleTask, // DEV-only
_debugStartTime: number, // DEV-only
_debugFindSourceMapURL?: void | FindSourceMapURLCallback, // DEV-only
_debugChannel?: void | DebugChannel, // DEV-only
_blockedConsole?: null | SomeChunk<ConsoleEntry>, // DEV-only
@@ -822,6 +823,7 @@ type InitializationReference = {
key: string,
) => any,
path: Array<string>,
isDebug?: boolean, // DEV-only
};
type InitializationHandler = {
parent: null | InitializationHandler,
@@ -872,6 +874,7 @@ function initializeDebugChunk(
response,
initializeDebugInfo,
[''], // path
true,
);
break;
}
@@ -894,6 +897,7 @@ function initializeDebugChunk(
response,
initializeDebugInfo,
[''], // path
true,
);
break;
}
@@ -1407,8 +1411,6 @@ function fulfillReference(
const mappedValue = map(response, value, parentObject, key);
parentObject[key] = mappedValue;
transferReferencedDebugInfo(handler.chunk, fulfilledChunk, mappedValue);
// If this is the root object for a model reference, where `handler.value`
// is a stale `null`, the resolved value can be used directly.
if (key === '' && handler.value === null) {
@@ -1427,19 +1429,27 @@ function fulfillReference(
const element: any = handler.value;
switch (key) {
case '3':
transferReferencedDebugInfo(handler.chunk, fulfilledChunk, mappedValue);
element.props = mappedValue;
break;
case '4':
// This path doesn't call transferReferencedDebugInfo because this reference is to a debug chunk.
if (__DEV__) {
element._owner = mappedValue;
}
break;
case '5':
// This path doesn't call transferReferencedDebugInfo because this reference is to a debug chunk.
if (__DEV__) {
element._debugStack = mappedValue;
}
break;
default:
transferReferencedDebugInfo(handler.chunk, fulfilledChunk, mappedValue);
break;
}
} else if (__DEV__ && !reference.isDebug) {
transferReferencedDebugInfo(handler.chunk, fulfilledChunk, mappedValue);
}
handler.deps--;
@@ -1518,6 +1528,7 @@ function waitForReference<T>(
response: Response,
map: (response: Response, model: any, parentObject: Object, key: string) => T,
path: Array<string>,
isAwaitingDebugInfo: boolean, // DEV-only
): T {
if (
__DEV__ &&
@@ -1562,6 +1573,9 @@ function waitForReference<T>(
map,
path,
};
if (__DEV__) {
reference.isDebug = isAwaitingDebugInfo;
}
// Add "listener".
if (referencedChunk.value === null) {
@@ -1794,13 +1808,21 @@ function transferReferencedDebugInfo(
existingDebugInfo.push.apply(existingDebugInfo, referencedDebugInfo);
}
}
// We also add it to the initializing chunk since the resolution of that promise is
// also blocked by these. By adding it to both we can track it even if the array/element
// We also add the debug info to the initializing chunk since the resolution of that promise is
// also blocked by the referenced debug info. By adding it to both we can track it even if the array/element
// is extracted, or if the root is rendered as is.
if (parentChunk !== null) {
const parentDebugInfo = parentChunk._debugInfo;
// $FlowFixMe[method-unbinding]
parentDebugInfo.push.apply(parentDebugInfo, referencedDebugInfo);
for (let i = 0; i < referencedDebugInfo.length; ++i) {
const debugInfoEntry = referencedDebugInfo[i];
if (debugInfoEntry.name != null) {
(debugInfoEntry: ReactComponentInfo);
// We're not transferring Component info since we use Component info
// in Debug info to fill in gaps between Fibers for the parent stack.
} else {
parentDebugInfo.push(debugInfoEntry);
}
}
}
}
}
@@ -1857,6 +1879,7 @@ function getOutlinedModel<T>(
response,
map,
path.slice(i - 1),
false,
);
}
case HALTED: {
@@ -1902,11 +1925,27 @@ function getOutlinedModel<T>(
value = value[path[i]];
}
const chunkValue = map(response, value, parentObject, key);
transferReferencedDebugInfo(initializingChunk, chunk, chunkValue);
if (
parentObject[0] === REACT_ELEMENT_TYPE &&
(key === '4' || key === '5')
) {
// If we're resolving the "owner" or "stack" slot of an Element array, we don't call
// transferReferencedDebugInfo because this reference is to a debug chunk.
} else {
transferReferencedDebugInfo(initializingChunk, chunk, chunkValue);
}
return chunkValue;
case PENDING:
case BLOCKED:
return waitForReference(chunk, parentObject, key, response, map, path);
return waitForReference(
chunk,
parentObject,
key,
response,
map,
path,
false,
);
case HALTED: {
// Add a dependency that will never resolve.
// TODO: Mark downstreams as halted too.
@@ -2444,6 +2483,13 @@ function ResponseInstance(
'"use ' + rootEnv.toLowerCase() + '"',
);
}
if (enableAsyncDebugInfo) {
// Track the start of the fetch to the best of our knowledge.
// Note: createFromFetch allows this to be marked at the start of the fetch
// where as if you use createFromReadableStream from the body of the fetch
// then the start time is when the headers resolved.
this._debugStartTime = performance.now();
}
this._debugFindSourceMapURL = findSourceMapURL;
this._debugChannel = debugChannel;
this._blockedConsole = null;
@@ -2512,16 +2558,99 @@ export type StreamState = {
_rowTag: number, // 0 indicates that we're currently parsing the row ID
_rowLength: number, // remaining bytes in the row. 0 indicates that we're looking for a newline.
_buffer: Array<Uint8Array>, // chunks received so far as part of this row
_debugInfo: ReactIOInfo, // DEV-only
_debugTargetChunkSize: number, // DEV-only
};
export function createStreamState(): StreamState {
return {
export function createStreamState(
weakResponse: WeakResponse, // DEV-only
streamDebugValue: mixed, // DEV-only
): StreamState {
const streamState: StreamState = (({
_rowState: 0,
_rowID: 0,
_rowTag: 0,
_rowLength: 0,
_buffer: [],
};
}: Omit<StreamState, '_debugInfo' | '_debugTargetChunkSize'>): any);
if (__DEV__ && enableAsyncDebugInfo) {
const response = unwrapWeakResponse(weakResponse);
// Create an entry for the I/O to load the stream itself.
const debugValuePromise = Promise.resolve(streamDebugValue);
(debugValuePromise: any).status = 'fulfilled';
(debugValuePromise: any).value = streamDebugValue;
streamState._debugInfo = {
name: 'RSC stream',
start: response._debugStartTime,
end: response._debugStartTime, // will be updated once we finish a chunk
byteSize: 0, // will be updated as we resolve a data chunk
value: debugValuePromise,
owner: response._debugRootOwner,
debugStack: response._debugRootStack,
debugTask: response._debugRootTask,
};
streamState._debugTargetChunkSize = MIN_CHUNK_SIZE;
}
return streamState;
}
// Depending on set up the chunks of a TLS connection can vary in size. However in practice it's often
// at 64kb or even multiples of 64kb. It can also be smaller but in practice it also happens that 64kb
// is around what you can download on fast 4G connection in 300ms which is what we throttle reveals at
// anyway. The net effect is that in practice, you won't really reveal anything in smaller units than
// 64kb if they're revealing at maximum speed in production. Therefore we group smaller chunks into
// these larger chunks since in production that's more realistic.
// TODO: If the stream is compressed, then you could fit much more in a single 300ms so maybe it should
// actually be larger.
const MIN_CHUNK_SIZE = 65536;
function incrementChunkDebugInfo(
streamState: StreamState,
chunkLength: number,
): void {
if (__DEV__ && enableAsyncDebugInfo) {
const debugInfo: ReactIOInfo = streamState._debugInfo;
const endTime = performance.now();
const previousEndTime = debugInfo.end;
const newByteLength = ((debugInfo.byteSize: any): number) + chunkLength;
if (
newByteLength > streamState._debugTargetChunkSize ||
endTime > previousEndTime + 10
) {
// This new chunk would overshoot the chunk size so therefore we treat it as its own new chunk
// by cloning the old one. Similarly, if some time has passed we assume that it was actually
// due to the server being unable to flush chunks faster e.g. due to I/O so it would be a
// new chunk in production even if the buffer hasn't been reached.
streamState._debugInfo = {
name: debugInfo.name,
start: debugInfo.start,
end: endTime,
byteSize: newByteLength,
value: debugInfo.value,
owner: debugInfo.owner,
debugStack: debugInfo.debugStack,
debugTask: debugInfo.debugTask,
};
streamState._debugTargetChunkSize = newByteLength + MIN_CHUNK_SIZE;
} else {
// Otherwise we reuse the old chunk but update the end time and byteSize to the latest.
// $FlowFixMe[cannot-write]
debugInfo.end = endTime;
// $FlowFixMe[cannot-write]
debugInfo.byteSize = newByteLength;
}
}
}
function resolveChunkDebugInfo(
streamState: StreamState,
chunk: SomeChunk<any>,
): void {
if (__DEV__ && enableAsyncDebugInfo) {
// Push the currently resolving chunk's debug info representing the stream on the Promise
// that was waiting on the stream.
chunk._debugInfo.push({awaited: streamState._debugInfo});
}
}
function resolveDebugHalt(response: Response, id: number): void {
@@ -2545,17 +2674,33 @@ function resolveModel(
response: Response,
id: number,
model: UninitializedModel,
streamState: StreamState,
): void {
const chunks = response._chunks;
const chunk = chunks.get(id);
if (!chunk) {
chunks.set(id, createResolvedModelChunk(response, model));
const newChunk: ResolvedModelChunk<any> = createResolvedModelChunk(
response,
model,
);
if (__DEV__) {
resolveChunkDebugInfo(streamState, newChunk);
}
chunks.set(id, newChunk);
} else {
if (__DEV__) {
resolveChunkDebugInfo(streamState, chunk);
}
resolveModelChunk(response, chunk, model);
}
}
function resolveText(response: Response, id: number, text: string): void {
function resolveText(
response: Response,
id: number,
text: string,
streamState: StreamState,
): void {
const chunks = response._chunks;
const chunk = chunks.get(id);
if (chunk && chunk.status !== PENDING) {
@@ -2569,13 +2714,18 @@ function resolveText(response: Response, id: number, text: string): void {
if (chunk) {
releasePendingChunk(response, chunk);
}
chunks.set(id, createInitializedTextChunk(response, text));
const newChunk = createInitializedTextChunk(response, text);
if (__DEV__) {
resolveChunkDebugInfo(streamState, newChunk);
}
chunks.set(id, newChunk);
}
function resolveBuffer(
response: Response,
id: number,
buffer: $ArrayBufferView | ArrayBuffer,
streamState: StreamState,
): void {
const chunks = response._chunks;
const chunk = chunks.get(id);
@@ -2590,13 +2740,18 @@ function resolveBuffer(
if (chunk) {
releasePendingChunk(response, chunk);
}
chunks.set(id, createInitializedBufferChunk(response, buffer));
const newChunk = createInitializedBufferChunk(response, buffer);
if (__DEV__) {
resolveChunkDebugInfo(streamState, newChunk);
}
chunks.set(id, newChunk);
}
function resolveModule(
response: Response,
id: number,
model: UninitializedModel,
streamState: StreamState,
): void {
const chunks = response._chunks;
const chunk = chunks.get(id);
@@ -2633,14 +2788,24 @@ function resolveModule(
blockedChunk = (chunk: any);
blockedChunk.status = BLOCKED;
}
if (__DEV__) {
resolveChunkDebugInfo(streamState, blockedChunk);
}
promise.then(
() => resolveModuleChunk(response, blockedChunk, clientReference),
error => triggerErrorOnChunk(response, blockedChunk, error),
);
} else {
if (!chunk) {
chunks.set(id, createResolvedModuleChunk(response, clientReference));
const newChunk = createResolvedModuleChunk(response, clientReference);
if (__DEV__) {
resolveChunkDebugInfo(streamState, newChunk);
}
chunks.set(id, newChunk);
} else {
if (__DEV__) {
resolveChunkDebugInfo(streamState, chunk);
}
// This can't actually happen because we don't have any forward
// references to modules.
resolveModuleChunk(response, chunk, clientReference);
@@ -2653,13 +2818,21 @@ function resolveStream<T: ReadableStream | $AsyncIterable<any, any, void>>(
id: number,
stream: T,
controller: FlightStreamController,
streamState: StreamState,
): void {
const chunks = response._chunks;
const chunk = chunks.get(id);
if (!chunk) {
chunks.set(id, createInitializedStreamChunk(response, stream, controller));
const newChunk = createInitializedStreamChunk(response, stream, controller);
if (__DEV__) {
resolveChunkDebugInfo(streamState, newChunk);
}
chunks.set(id, newChunk);
return;
}
if (__DEV__) {
resolveChunkDebugInfo(streamState, chunk);
}
if (chunk.status !== PENDING) {
// We already resolved. We didn't expect to see this.
return;
@@ -2715,6 +2888,7 @@ function startReadableStream<T>(
response: Response,
id: number,
type: void | 'bytes',
streamState: StreamState,
): void {
let controller: ReadableStreamController = (null: any);
const stream = new ReadableStream({
@@ -2795,7 +2969,7 @@ function startReadableStream<T>(
}
},
};
resolveStream(response, id, stream, flightController);
resolveStream(response, id, stream, flightController, streamState);
}
function asyncIterator(this: $AsyncIterator<any, any, void>) {
@@ -2821,6 +2995,7 @@ function startAsyncIterable<T>(
response: Response,
id: number,
iterator: boolean,
streamState: StreamState,
): void {
const buffer: Array<SomeChunk<IteratorResult<T, T>>> = [];
let closed = false;
@@ -2938,6 +3113,7 @@ function startAsyncIterable<T>(
id,
iterator ? iterable[ASYNC_ITERATOR]() : iterable,
flightController,
streamState,
);
}
@@ -3017,7 +3193,11 @@ function resolveErrorDev(
return error;
}
function resolvePostponeProd(response: Response, id: number): void {
function resolvePostponeProd(
response: Response,
id: number,
streamState: StreamState,
): void {
if (__DEV__) {
// These errors should never make it into a build so we don't need to encode them in codes.json
// eslint-disable-next-line react-internal/prod-error-codes
@@ -3035,7 +3215,11 @@ function resolvePostponeProd(response: Response, id: number): void {
const chunks = response._chunks;
const chunk = chunks.get(id);
if (!chunk) {
chunks.set(id, createErrorChunk(response, postponeInstance));
const newChunk: ErroredChunk<any> = createErrorChunk(
response,
postponeInstance,
);
chunks.set(id, newChunk);
} else {
triggerErrorOnChunk(response, chunk, postponeInstance);
}
@@ -3047,6 +3231,7 @@ function resolvePostponeDev(
reason: string,
stack: ReactStackTrace,
env: string,
streamState: StreamState,
): void {
if (!__DEV__) {
// These errors should never make it into a build so we don't need to encode them in codes.json
@@ -3074,8 +3259,18 @@ function resolvePostponeDev(
const chunks = response._chunks;
const chunk = chunks.get(id);
if (!chunk) {
chunks.set(id, createErrorChunk(response, postponeInstance));
const newChunk: ErroredChunk<any> = createErrorChunk(
response,
postponeInstance,
);
if (__DEV__) {
resolveChunkDebugInfo(streamState, newChunk);
}
chunks.set(id, newChunk);
} else {
if (__DEV__) {
resolveChunkDebugInfo(streamState, chunk);
}
triggerErrorOnChunk(response, chunk, postponeInstance);
}
}
@@ -3084,6 +3279,7 @@ function resolveErrorModel(
response: Response,
id: number,
row: UninitializedModel,
streamState: StreamState,
): void {
const chunks = response._chunks;
const chunk = chunks.get(id);
@@ -3097,8 +3293,18 @@ function resolveErrorModel(
(error: any).digest = errorInfo.digest;
const errorWithDigest: ErrorWithDigest = (error: any);
if (!chunk) {
chunks.set(id, createErrorChunk(response, errorWithDigest));
const newChunk: ErroredChunk<any> = createErrorChunk(
response,
errorWithDigest,
);
if (__DEV__) {
resolveChunkDebugInfo(streamState, newChunk);
}
chunks.set(id, newChunk);
} else {
if (__DEV__) {
resolveChunkDebugInfo(streamState, chunk);
}
triggerErrorOnChunk(response, chunk, errorWithDigest);
}
}
@@ -3833,6 +4039,7 @@ function resolveTypedArray(
lastChunk: Uint8Array,
constructor: any,
bytesPerElement: number,
streamState: StreamState,
): void {
// If the view fits into one original buffer, we just reuse that buffer instead of
// copying it out to a separate copy. This means that it's not always possible to
@@ -3852,7 +4059,7 @@ function resolveTypedArray(
chunk.byteOffset,
chunk.byteLength / bytesPerElement,
);
resolveBuffer(response, id, view);
resolveBuffer(response, id, view, streamState);
}
function logComponentInfo(
@@ -4169,6 +4376,7 @@ function flushInitialRenderPerformance(response: Response): void {
function processFullBinaryRow(
response: Response,
streamState: StreamState,
id: number,
tag: number,
buffer: Array<Uint8Array>,
@@ -4177,47 +4385,125 @@ function processFullBinaryRow(
switch (tag) {
case 65 /* "A" */:
// We must always clone to extract it into a separate buffer instead of just a view.
resolveBuffer(response, id, mergeBuffer(buffer, chunk).buffer);
resolveBuffer(
response,
id,
mergeBuffer(buffer, chunk).buffer,
streamState,
);
return;
case 79 /* "O" */:
resolveTypedArray(response, id, buffer, chunk, Int8Array, 1);
resolveTypedArray(response, id, buffer, chunk, Int8Array, 1, streamState);
return;
case 111 /* "o" */:
resolveBuffer(
response,
id,
buffer.length === 0 ? chunk : mergeBuffer(buffer, chunk),
streamState,
);
return;
case 85 /* "U" */:
resolveTypedArray(response, id, buffer, chunk, Uint8ClampedArray, 1);
resolveTypedArray(
response,
id,
buffer,
chunk,
Uint8ClampedArray,
1,
streamState,
);
return;
case 83 /* "S" */:
resolveTypedArray(response, id, buffer, chunk, Int16Array, 2);
resolveTypedArray(
response,
id,
buffer,
chunk,
Int16Array,
2,
streamState,
);
return;
case 115 /* "s" */:
resolveTypedArray(response, id, buffer, chunk, Uint16Array, 2);
resolveTypedArray(
response,
id,
buffer,
chunk,
Uint16Array,
2,
streamState,
);
return;
case 76 /* "L" */:
resolveTypedArray(response, id, buffer, chunk, Int32Array, 4);
resolveTypedArray(
response,
id,
buffer,
chunk,
Int32Array,
4,
streamState,
);
return;
case 108 /* "l" */:
resolveTypedArray(response, id, buffer, chunk, Uint32Array, 4);
resolveTypedArray(
response,
id,
buffer,
chunk,
Uint32Array,
4,
streamState,
);
return;
case 71 /* "G" */:
resolveTypedArray(response, id, buffer, chunk, Float32Array, 4);
resolveTypedArray(
response,
id,
buffer,
chunk,
Float32Array,
4,
streamState,
);
return;
case 103 /* "g" */:
resolveTypedArray(response, id, buffer, chunk, Float64Array, 8);
resolveTypedArray(
response,
id,
buffer,
chunk,
Float64Array,
8,
streamState,
);
return;
case 77 /* "M" */:
resolveTypedArray(response, id, buffer, chunk, BigInt64Array, 8);
resolveTypedArray(
response,
id,
buffer,
chunk,
BigInt64Array,
8,
streamState,
);
return;
case 109 /* "m" */:
resolveTypedArray(response, id, buffer, chunk, BigUint64Array, 8);
resolveTypedArray(
response,
id,
buffer,
chunk,
BigUint64Array,
8,
streamState,
);
return;
case 86 /* "V" */:
resolveTypedArray(response, id, buffer, chunk, DataView, 1);
resolveTypedArray(response, id, buffer, chunk, DataView, 1, streamState);
return;
}
@@ -4227,18 +4513,19 @@ function processFullBinaryRow(
row += readPartialStringChunk(stringDecoder, buffer[i]);
}
row += readFinalStringChunk(stringDecoder, chunk);
processFullStringRow(response, id, tag, row);
processFullStringRow(response, streamState, id, tag, row);
}
function processFullStringRow(
response: Response,
streamState: StreamState,
id: number,
tag: number,
row: string,
): void {
switch (tag) {
case 73 /* "I" */: {
resolveModule(response, id, row);
resolveModule(response, id, row, streamState);
return;
}
case 72 /* "H" */: {
@@ -4247,11 +4534,11 @@ function processFullStringRow(
return;
}
case 69 /* "E" */: {
resolveErrorModel(response, id, row);
resolveErrorModel(response, id, row, streamState);
return;
}
case 84 /* "T" */: {
resolveText(response, id, row);
resolveText(response, id, row, streamState);
return;
}
case 78 /* "N" */: {
@@ -4296,22 +4583,22 @@ function processFullStringRow(
);
}
case 82 /* "R" */: {
startReadableStream(response, id, undefined);
startReadableStream(response, id, undefined, streamState);
return;
}
// Fallthrough
case 114 /* "r" */: {
startReadableStream(response, id, 'bytes');
startReadableStream(response, id, 'bytes', streamState);
return;
}
// Fallthrough
case 88 /* "X" */: {
startAsyncIterable(response, id, false);
startAsyncIterable(response, id, false, streamState);
return;
}
// Fallthrough
case 120 /* "x" */: {
startAsyncIterable(response, id, true);
startAsyncIterable(response, id, true, streamState);
return;
}
// Fallthrough
@@ -4330,9 +4617,10 @@ function processFullStringRow(
postponeInfo.reason,
postponeInfo.stack,
postponeInfo.env,
streamState,
);
} else {
resolvePostponeProd(response, id);
resolvePostponeProd(response, id, streamState);
}
return;
}
@@ -4344,7 +4632,7 @@ function processFullStringRow(
return;
}
// We assume anything else is JSON.
resolveModel(response, id, row);
resolveModel(response, id, row, streamState);
return;
}
}
@@ -4367,6 +4655,7 @@ export function processBinaryChunk(
let rowLength = streamState._rowLength;
const buffer = streamState._buffer;
const chunkLength = chunk.length;
incrementChunkDebugInfo(streamState, chunkLength);
while (i < chunkLength) {
let lastIdx = -1;
switch (rowState) {
@@ -4446,7 +4735,14 @@ export function processBinaryChunk(
// We found the last chunk of the row
const length = lastIdx - i;
const lastChunk = new Uint8Array(chunk.buffer, offset, length);
processFullBinaryRow(response, rowID, rowTag, buffer, lastChunk);
processFullBinaryRow(
response,
streamState,
rowID,
rowTag,
buffer,
lastChunk,
);
// Reset state machine for a new row
i = lastIdx;
if (rowState === ROW_CHUNK_BY_NEWLINE) {
@@ -4501,6 +4797,7 @@ export function processStringChunk(
let rowLength = streamState._rowLength;
const buffer = streamState._buffer;
const chunkLength = chunk.length;
incrementChunkDebugInfo(streamState, chunkLength);
while (i < chunkLength) {
let lastIdx = -1;
switch (rowState) {
@@ -4599,7 +4896,7 @@ export function processStringChunk(
);
}
const lastChunk = chunk.slice(i, lastIdx);
processFullStringRow(response, rowID, rowTag, lastChunk);
processFullStringRow(response, streamState, rowID, rowTag, lastChunk);
// Reset state machine for a new row
i = lastIdx;
if (rowState === ROW_CHUNK_BY_NEWLINE) {
+132 -29
View File
@@ -85,7 +85,11 @@ function getDebugInfo(obj) {
if (debugInfo) {
const copy = [];
for (let i = 0; i < debugInfo.length; i++) {
copy.push(normalizeComponentInfo(debugInfo[i]));
if (debugInfo[i].awaited && debugInfo[i].awaited.name === 'RSC stream') {
// Ignore RSC stream I/O info.
} else {
copy.push(normalizeComponentInfo(debugInfo[i]));
}
}
return copy;
}
@@ -2832,7 +2836,7 @@ describe('ReactFlight', () => {
transport: expect.arrayContaining([]),
},
},
{time: gate(flags => flags.enableAsyncDebugInfo) ? 23 : 21},
{time: gate(flags => flags.enableAsyncDebugInfo) ? 53 : 21},
]
: undefined,
);
@@ -2843,7 +2847,7 @@ describe('ReactFlight', () => {
expect(getDebugInfo(thirdPartyChildren[0])).toEqual(
__DEV__
? [
{time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22}, // Clamped to the start
{time: gate(flags => flags.enableAsyncDebugInfo) ? 54 : 22}, // Clamped to the start
{
name: 'ThirdPartyComponent',
env: 'third-party',
@@ -2851,15 +2855,15 @@ describe('ReactFlight', () => {
stack: ' in Object.<anonymous> (at **)',
props: {},
},
{time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22},
{time: gate(flags => flags.enableAsyncDebugInfo) ? 25 : 23}, // This last one is when the promise resolved into the first party.
{time: gate(flags => flags.enableAsyncDebugInfo) ? 54 : 22},
{time: gate(flags => flags.enableAsyncDebugInfo) ? 55 : 23}, // This last one is when the promise resolved into the first party.
]
: undefined,
);
expect(getDebugInfo(thirdPartyChildren[1])).toEqual(
__DEV__
? [
{time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22}, // Clamped to the start
{time: gate(flags => flags.enableAsyncDebugInfo) ? 54 : 22}, // Clamped to the start
{
name: 'ThirdPartyLazyComponent',
env: 'third-party',
@@ -2867,14 +2871,14 @@ describe('ReactFlight', () => {
stack: ' in myLazy (at **)\n in lazyInitializer (at **)',
props: {},
},
{time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22},
{time: gate(flags => flags.enableAsyncDebugInfo) ? 54 : 22},
]
: undefined,
);
expect(getDebugInfo(thirdPartyChildren[2])).toEqual(
__DEV__
? [
{time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22},
{time: gate(flags => flags.enableAsyncDebugInfo) ? 54 : 22},
{
name: 'ThirdPartyFragmentComponent',
env: 'third-party',
@@ -2882,7 +2886,7 @@ describe('ReactFlight', () => {
stack: ' in Object.<anonymous> (at **)',
props: {},
},
{time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22},
{time: gate(flags => flags.enableAsyncDebugInfo) ? 54 : 22},
]
: undefined,
);
@@ -2960,17 +2964,10 @@ describe('ReactFlight', () => {
{
time: 16,
},
{
env: 'third-party',
key: null,
name: 'ThirdPartyAsyncIterableComponent',
props: {},
stack: ' in Object.<anonymous> (at **)',
},
{
time: 16,
},
{time: 17},
{time: 31},
]
: undefined,
);
@@ -2979,7 +2976,7 @@ describe('ReactFlight', () => {
expect(getDebugInfo(thirdPartyFragment)).toEqual(
__DEV__
? [
{time: 18},
{time: 32},
{
name: 'Keyed',
env: 'Server',
@@ -2990,19 +2987,14 @@ describe('ReactFlight', () => {
},
},
{
time: 19,
time: 33,
},
{
time: 19,
time: 33,
},
{
env: 'third-party',
key: null,
name: 'ThirdPartyAsyncIterableComponent',
props: {},
stack: ' in Object.<anonymous> (at **)',
time: 33,
},
{time: 19},
]
: undefined,
);
@@ -3010,7 +3002,7 @@ describe('ReactFlight', () => {
expect(getDebugInfo(thirdPartyFragment.props.children)).toEqual(
__DEV__
? [
{time: 19}, // Clamp to the start
{time: 33}, // Clamp to the start
{
name: 'ThirdPartyAsyncIterableComponent',
env: 'third-party',
@@ -3018,7 +3010,7 @@ describe('ReactFlight', () => {
stack: ' in Object.<anonymous> (at **)',
props: {},
},
{time: 19},
{time: 33},
]
: undefined,
);
@@ -3081,7 +3073,7 @@ describe('ReactFlight', () => {
props: {},
},
{time: 16},
{time: 17},
{time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 17},
]
: undefined,
);
@@ -3847,4 +3839,115 @@ describe('ReactFlight', () => {
expect(ReactNoop).toMatchRenderedOutput(<div>not using props</div>);
});
// @gate !__DEV__ || enableComponentPerformanceTrack
it('produces correct parent stacks', async () => {
function Container() {
return ReactServer.createElement('div', null);
}
function ContainerParent() {
return ReactServer.createElement(Container, null);
}
function App() {
return ReactServer.createElement(
'main',
null,
ReactServer.createElement(ContainerParent, null),
);
}
const transport = ReactNoopFlightServer.render({
root: ReactServer.createElement(App, null),
});
await act(async () => {
const {root} = await ReactNoopFlightClient.read(transport);
ReactNoop.render(root);
expect(root.type).toBe('main');
if (__DEV__) {
const div = root.props.children;
expect(getDebugInfo(div)).toEqual([
{
time: 14,
},
{
env: 'Server',
key: null,
name: 'ContainerParent',
owner: {
env: 'Server',
key: null,
name: 'App',
props: {},
stack: ' in Object.<anonymous> (at **)',
},
props: {},
stack: ' in App (at **)',
},
{
time: 15,
},
{
env: 'Server',
key: null,
name: 'Container',
owner: {
env: 'Server',
key: null,
name: 'ContainerParent',
owner: {
env: 'Server',
key: null,
name: 'App',
props: {},
stack: ' in Object.<anonymous> (at **)',
},
props: {},
stack: ' in App (at **)',
},
props: {},
stack: ' in ContainerParent (at **)',
},
{
time: 16,
},
]);
expect(getDebugInfo(root)).toEqual([
{
time: 12,
},
{
env: 'Server',
key: null,
name: 'App',
props: {},
stack: ' in Object.<anonymous> (at **)',
},
{
time: 13,
},
{
time: 14,
},
{
time: 15,
},
{
time: 16,
},
]);
} else {
expect(root._debugInfo).toBe(undefined);
expect(root._owner).toBe(undefined);
}
});
expect(ReactNoop).toMatchRenderedOutput(
<main>
<div />
</main>,
);
});
});
+12 -12
View File
@@ -81,7 +81,18 @@ export function experimental_renderToHTML(
options?: MarkupOptions,
): Promise<string> {
return new Promise((resolve, reject) => {
const streamState = createFlightStreamState();
const flightResponse = createFlightResponse(
null,
null,
null,
noServerCallOrFormAction,
noServerCallOrFormAction,
undefined,
undefined,
undefined,
false,
);
const streamState = createFlightStreamState(flightResponse, null);
const flightDestination = {
push(chunk: string | null): boolean {
if (chunk !== null) {
@@ -175,17 +186,6 @@ export function experimental_renderToHTML(
undefined,
false,
);
const flightResponse = createFlightResponse(
null,
null,
null,
noServerCallOrFormAction,
noServerCallOrFormAction,
undefined,
undefined,
undefined,
false,
);
const resumableState = createResumableState(
options ? options.identifierPrefix : undefined,
undefined,
+1 -1
View File
@@ -77,7 +77,7 @@ function read<T>(source: Source, options: ReadOptions): Thenable<T> {
? options.debugChannel.onMessage
: undefined,
);
const streamState = createStreamState();
const streamState = createStreamState(response, source);
for (let i = 0; i < source.length; i++) {
processBinaryChunk(response, streamState, source[i], 0);
}
@@ -115,7 +115,7 @@ function startReadingFromUniversalStream(
// This is the same as startReadingFromStream except this allows WebSocketStreams which
// return ArrayBuffer and string chunks instead of Uint8Array chunks. We could potentially
// always allow streams with variable chunk types.
const streamState = createStreamState();
const streamState = createStreamState(response, stream);
const reader = stream.getReader();
function progress({
done,
@@ -149,8 +149,9 @@ function startReadingFromStream(
response: FlightResponse,
stream: ReadableStream,
onDone: () => void,
debugValue: mixed,
): void {
const streamState = createStreamState();
const streamState = createStreamState(response, debugValue);
const reader = stream.getReader();
function progress({
done,
@@ -194,9 +195,14 @@ function createFromReadableStream<T>(
options.debugChannel.readable,
handleDone,
);
startReadingFromStream(response, stream, handleDone);
startReadingFromStream(response, stream, handleDone, stream);
} else {
startReadingFromStream(response, stream, close.bind(null, response));
startReadingFromStream(
response,
stream,
close.bind(null, response),
stream,
);
}
return getRoot(response);
}
@@ -225,12 +231,13 @@ function createFromFetch<T>(
options.debugChannel.readable,
handleDone,
);
startReadingFromStream(response, (r.body: any), handleDone);
startReadingFromStream(response, (r.body: any), handleDone, r);
} else {
startReadingFromStream(
response,
(r.body: any),
close.bind(null, response),
r,
);
}
},
@@ -66,7 +66,7 @@ function startReadingFromStream(
stream: Readable,
onEnd: () => void,
): void {
const streamState = createStreamState();
const streamState = createStreamState(response, stream);
stream.on('data', chunk => {
if (typeof chunk === 'string') {
@@ -141,7 +141,7 @@ function startReadingFromUniversalStream(
// This is the same as startReadingFromStream except this allows WebSocketStreams which
// return ArrayBuffer and string chunks instead of Uint8Array chunks. We could potentially
// always allow streams with variable chunk types.
const streamState = createStreamState();
const streamState = createStreamState(response, stream);
const reader = stream.getReader();
function progress({
done,
@@ -175,8 +175,9 @@ function startReadingFromStream(
response: FlightResponse,
stream: ReadableStream,
onDone: () => void,
debugValue: mixed,
): void {
const streamState = createStreamState();
const streamState = createStreamState(response, debugValue);
const reader = stream.getReader();
function progress({
done,
@@ -228,9 +229,14 @@ export function createFromReadableStream<T>(
options.debugChannel.readable,
handleDone,
);
startReadingFromStream(response, stream, handleDone);
startReadingFromStream(response, stream, handleDone, stream);
} else {
startReadingFromStream(response, stream, close.bind(null, response));
startReadingFromStream(
response,
stream,
close.bind(null, response),
stream,
);
}
return getRoot(response);
}
@@ -259,12 +265,13 @@ export function createFromFetch<T>(
options.debugChannel.readable,
handleDone,
);
startReadingFromStream(response, (r.body: any), handleDone);
startReadingFromStream(response, (r.body: any), handleDone, r);
} else {
startReadingFromStream(
response,
(r.body: any),
close.bind(null, response),
r,
);
}
},
@@ -115,8 +115,9 @@ function startReadingFromStream(
response: FlightResponse,
stream: ReadableStream,
onDone: () => void,
debugValue: mixed,
): void {
const streamState = createStreamState();
const streamState = createStreamState(response, debugValue);
const reader = stream.getReader();
function progress({
done,
@@ -158,9 +159,14 @@ export function createFromReadableStream<T>(
}
};
startReadingFromStream(response, options.debugChannel.readable, handleDone);
startReadingFromStream(response, stream, handleDone);
startReadingFromStream(response, stream, handleDone, stream);
} else {
startReadingFromStream(response, stream, close.bind(null, response));
startReadingFromStream(
response,
stream,
close.bind(null, response),
stream,
);
}
return getRoot(response);
@@ -190,12 +196,13 @@ export function createFromFetch<T>(
options.debugChannel.readable,
handleDone,
);
startReadingFromStream(response, (r.body: any), handleDone);
startReadingFromStream(response, (r.body: any), handleDone, r);
} else {
startReadingFromStream(
response,
(r.body: any),
close.bind(null, response),
r,
);
}
},
@@ -61,7 +61,7 @@ function startReadingFromStream(
stream: Readable,
onEnd: () => void,
): void {
const streamState = createStreamState();
const streamState = createStreamState(response, stream);
stream.on('data', chunk => {
if (typeof chunk === 'string') {
@@ -114,7 +114,7 @@ function startReadingFromUniversalStream(
// This is the same as startReadingFromStream except this allows WebSocketStreams which
// return ArrayBuffer and string chunks instead of Uint8Array chunks. We could potentially
// always allow streams with variable chunk types.
const streamState = createStreamState();
const streamState = createStreamState(response, stream);
const reader = stream.getReader();
function progress({
done,
@@ -148,8 +148,9 @@ function startReadingFromStream(
response: FlightResponse,
stream: ReadableStream,
onDone: () => void,
debugValue: mixed,
): void {
const streamState = createStreamState();
const streamState = createStreamState(response, debugValue);
const reader = stream.getReader();
function progress({
done,
@@ -194,9 +195,14 @@ function createFromReadableStream<T>(
options.debugChannel.readable,
handleDone,
);
startReadingFromStream(response, stream, handleDone);
startReadingFromStream(response, stream, handleDone, stream);
} else {
startReadingFromStream(response, stream, close.bind(null, response));
startReadingFromStream(
response,
stream,
close.bind(null, response),
stream,
);
}
return getRoot(response);
}
@@ -225,12 +231,13 @@ function createFromFetch<T>(
options.debugChannel.readable,
handleDone,
);
startReadingFromStream(response, (r.body: any), handleDone);
startReadingFromStream(response, (r.body: any), handleDone, r);
} else {
startReadingFromStream(
response,
(r.body: any),
close.bind(null, response),
r,
);
}
},
@@ -117,8 +117,9 @@ function startReadingFromStream(
response: FlightResponse,
stream: ReadableStream,
onDone: () => void,
debugValue: mixed,
): void {
const streamState = createStreamState();
const streamState = createStreamState(response, debugValue);
const reader = stream.getReader();
function progress({
done,
@@ -160,9 +161,14 @@ function createFromReadableStream<T>(
}
};
startReadingFromStream(response, options.debugChannel.readable, handleDone);
startReadingFromStream(response, stream, handleDone);
startReadingFromStream(response, stream, handleDone, stream);
} else {
startReadingFromStream(response, stream, close.bind(null, response));
startReadingFromStream(
response,
stream,
close.bind(null, response),
stream,
);
}
return getRoot(response);
@@ -192,12 +198,13 @@ function createFromFetch<T>(
options.debugChannel.readable,
handleDone,
);
startReadingFromStream(response, (r.body: any), handleDone);
startReadingFromStream(response, (r.body: any), handleDone, r);
} else {
startReadingFromStream(
response,
(r.body: any),
close.bind(null, response),
r,
);
}
},
@@ -69,7 +69,7 @@ function startReadingFromStream(
stream: Readable,
onEnd: () => void,
): void {
const streamState = createStreamState();
const streamState = createStreamState(response, stream);
stream.on('data', chunk => {
if (typeof chunk === 'string') {
@@ -1239,17 +1239,24 @@ describe('ReactFlightDOMEdge', () => {
name: 'Greeting',
env: 'Server',
});
expect(lazyWrapper._debugInfo).toEqual([
{time: 12},
greetInfo,
{time: 13},
expect.objectContaining({
name: 'Container',
env: 'Server',
owner: greetInfo,
}),
{time: 14},
]);
if (gate(flags => flags.enableAsyncDebugInfo)) {
expect(lazyWrapper._debugInfo).toEqual([
{time: 12},
greetInfo,
{time: 13},
expect.objectContaining({
name: 'Container',
env: 'Server',
owner: greetInfo,
}),
{time: 14},
expect.objectContaining({
awaited: expect.objectContaining({
name: 'RSC stream',
}),
}),
]);
}
// The owner that created the span was the outer server component.
// We expect the debug info to be referentially equal to the owner.
expect(greeting._owner).toBe(lazyWrapper._debugInfo[1]);
@@ -114,7 +114,7 @@ function startReadingFromUniversalStream(
// This is the same as startReadingFromStream except this allows WebSocketStreams which
// return ArrayBuffer and string chunks instead of Uint8Array chunks. We could potentially
// always allow streams with variable chunk types.
const streamState = createStreamState();
const streamState = createStreamState(response, stream);
const reader = stream.getReader();
function progress({
done,
@@ -148,8 +148,9 @@ function startReadingFromStream(
response: FlightResponse,
stream: ReadableStream,
onDone: () => void,
debugValue: mixed,
): void {
const streamState = createStreamState();
const streamState = createStreamState(response, debugValue);
const reader = stream.getReader();
function progress({
done,
@@ -194,9 +195,14 @@ function createFromReadableStream<T>(
options.debugChannel.readable,
handleDone,
);
startReadingFromStream(response, stream, handleDone);
startReadingFromStream(response, stream, handleDone, stream);
} else {
startReadingFromStream(response, stream, close.bind(null, response));
startReadingFromStream(
response,
stream,
close.bind(null, response),
stream,
);
}
return getRoot(response);
}
@@ -225,12 +231,13 @@ function createFromFetch<T>(
options.debugChannel.readable,
handleDone,
);
startReadingFromStream(response, (r.body: any), handleDone);
startReadingFromStream(response, (r.body: any), handleDone, r);
} else {
startReadingFromStream(
response,
(r.body: any),
close.bind(null, response),
r,
);
}
},
@@ -117,8 +117,9 @@ function startReadingFromStream(
response: FlightResponse,
stream: ReadableStream,
onDone: () => void,
debugValue: mixed,
): void {
const streamState = createStreamState();
const streamState = createStreamState(response, debugValue);
const reader = stream.getReader();
function progress({
done,
@@ -160,9 +161,14 @@ function createFromReadableStream<T>(
}
};
startReadingFromStream(response, options.debugChannel.readable, handleDone);
startReadingFromStream(response, stream, handleDone);
startReadingFromStream(response, stream, handleDone, stream);
} else {
startReadingFromStream(response, stream, close.bind(null, response));
startReadingFromStream(
response,
stream,
close.bind(null, response),
stream,
);
}
return getRoot(response);
@@ -192,12 +198,13 @@ function createFromFetch<T>(
options.debugChannel.readable,
handleDone,
);
startReadingFromStream(response, (r.body: any), handleDone);
startReadingFromStream(response, (r.body: any), handleDone, r);
} else {
startReadingFromStream(
response,
(r.body: any),
close.bind(null, response),
r,
);
}
},
@@ -69,7 +69,7 @@ function startReadingFromStream(
stream: Readable,
onEnd: () => void,
): void {
const streamState = createStreamState();
const streamState = createStreamState(response, stream);
stream.on('data', chunk => {
if (typeof chunk === 'string') {
File diff suppressed because it is too large Load Diff