mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Merge 40ba0571c2 into sapling-pr-archive-josephsavona
This commit is contained in:
@@ -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,
|
||||
|
||||
+624
@@ -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;
|
||||
}
|
||||
+40
-45
@@ -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: [],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+84
@@ -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`
|
||||
```
|
||||
|
||||
|
||||
+24
@@ -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]} />;
|
||||
}
|
||||
+59
@@ -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 | }
|
||||
```
|
||||
|
||||
|
||||
+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;
|
||||
}
|
||||
+64
@@ -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 |
|
||||
```
|
||||
|
||||
|
||||
+36
@@ -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: [{}],
|
||||
};
|
||||
+121
@@ -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
|
||||
+24
@@ -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]} />;
|
||||
}
|
||||
+109
@@ -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>
|
||||
+31
@@ -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};
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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') {
|
||||
|
||||
+12
-5
@@ -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') {
|
||||
|
||||
+12
-5
@@ -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,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
+11
-4
@@ -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') {
|
||||
|
||||
+18
-11
@@ -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]);
|
||||
|
||||
+12
-5
@@ -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') {
|
||||
|
||||
+445
-285
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user