Compare commits

...

7 Commits

Author SHA1 Message Date
Joe Savona
5eda822159 [compiler] Alternate take on ref validation
Some of the false positives we've seen have to do with the need to align our ref validation with our understanding of which functions may be called during render. The new mutability/aliasing model makes this much more explicit, with the ability to create Impure effects which we then throw as errors if they are reachable during render. This means we can now revisit ref validation by just emitting impure effects.

That's what this new pass does. It's a bit simpler: it implements the check for `ref.current == null` guarded if blocks. Otherwise it disallows access to `ref.current` specifically. Unlike before, we intentionally allow passing ref objects to functions — we just see a lot of many false positives on disallowing things like `children({ref})` or similar.

Open to feedback! This is also still WIP.
2025-06-25 09:49:17 -07:00
Joe Savona
2de9ddac73 [compiler] Consolidate HIRFunction return information
We now have `HIRFunction.returns: Place` as well as `returnType: Type`. I want to add additional return information, so as a first step i'm consolidating everything under an object at `HIRFunction.returns: {place: Place}`. We use the type of this place as the return type. Next step is to add more properties to this object to represent things like the return kind.
2025-06-25 09:49:17 -07:00
Joe Savona
6ef9e5bd49 [compiler] Avoid empty switch cases
Small cosmetic win, found this when i was looking at some code internally with lots of cases that all share the same logic. Previously, all the but last one would have an empty block.
2025-06-25 09:49:17 -07:00
Joe Savona
440d8c2876 [compiler] Fix bug with reassigning function param in destructuring
Closes #33577, a bug with ExtractScopeDeclarationsFromDestructuring and codegen when a function param is reassigned.
2025-06-25 09:49:17 -07:00
Sebastian Markbåge
cee7939b00 [Fizz] Push a stalled await from debug info to the ownerStack/debugTask (#33634)
If an aborted task is not rendering, then this is an async abort.
Conceptually it's as if the abort happened inside the async gap. The
abort reason's stack frame won't have that on the stack so instead we
use the owner stack and debug task of any halted async debug info.

One thing that's a bit awkward is that if you do have a sync abort and
you use that error as the "reason" then that thing still has a sync
stack in a different component. In another approach I was exploring
having different error objects for each component but I don't think
that's worth it.
2025-06-25 11:14:49 -04:00
Sebastian Markbåge
b42341ddc7 [Flight] Use cacheController instead of abortListeners for Streams (#33633)
Now that we have `cacheSignal()` we can just use that instead of the
`abortListeners` concept which was really just the same thing for
cancelling the streams (ReadableStream, Blob, AsyncIterable).
2025-06-25 09:41:21 -04:00
Pieter De Baets
7a3ffef703 [react-native] Consume ReactNativeAttributePayloadFabric from ReactNativePrivateInterface (#33616)
## Summary

ReactNativeAttributePayloadFabric was synced to react-native in
0e42d33cbc.
We should now consume these methods from the
ReactNativePrivateInterface.

Moving these methods to the React Native repo gives us more flexibility
to experiment with new techniques for bridging and diffing props
payloads.

I did have to leave some stub implementations for existing unit tests,
but moved all detailed tests to the React Native repo.

## How did you test this change?

* `yarn prettier`
* `yarn test ReactFabric-test`
2025-06-25 10:23:36 +01:00
42 changed files with 543 additions and 1208 deletions

View File

@@ -285,7 +285,7 @@ function runWithEnvironment(
}
if (env.config.validateRefAccessDuringRender) {
validateNoRefAccessInRender(hir).unwrap();
// validateNoRefAccessInRender(hir).unwrap();
}
if (env.config.validateNoSetStateInRender) {

View File

@@ -221,7 +221,6 @@ export function lower(
params,
fnType: bindings == null ? env.fnType : 'Other',
returnTypeAnnotation: null, // TODO: extract the actual return type node if present
returnType: makeType(),
returns: createTemporaryPlace(env, func.node.loc ?? GeneratedSource),
body: builder.build(),
context,

View File

@@ -279,7 +279,6 @@ export type HIRFunction = {
env: Environment;
params: Array<Place | SpreadPattern>;
returnTypeAnnotation: t.FlowType | t.TSType | null;
returnType: Type;
returns: Place;
context: Array<Place>;
effects: Array<FunctionEffect> | null;

View File

@@ -54,6 +54,8 @@ export function printFunction(fn: HIRFunction): string {
let definition = '';
if (fn.id !== null) {
definition += fn.id;
} else {
definition += '<<anonymous>>';
}
if (fn.params.length !== 0) {
definition +=
@@ -71,10 +73,8 @@ export function printFunction(fn: HIRFunction): string {
} else {
definition += '()';
}
if (definition.length !== 0) {
output.push(definition);
}
output.push(`: ${printType(fn.returnType)} @ ${printPlace(fn.returns)}`);
definition += `: ${printPlace(fn.returns)}`;
output.push(definition);
output.push(...fn.directives);
output.push(printHIR(fn.body));
return output.join('\n');

View File

@@ -28,7 +28,9 @@ import {
isMapType,
isPrimitiveType,
isRefOrRefValue,
isRefValueType,
isSetType,
isUseRefType,
makeIdentifierId,
Phi,
Place,
@@ -219,6 +221,9 @@ export function inferMutationAliasingEffects(
}
}
}
if (fn.env.config.validateRefAccessDuringRender) {
inferRefAccessEffects(fn, isFunctionExpression);
}
return Ok(undefined);
}
@@ -2513,3 +2518,127 @@ export type AbstractValue = {
kind: ValueKind;
reason: ReadonlySet<ValueReason>;
};
function inferRefAccessEffects(
fn: HIRFunction,
_isFunctionExpression: boolean,
): void {
const nullish = new Set<IdentifierId>();
const nullishTest = new Map<IdentifierId, Place>();
let guard: {ref: IdentifierId; fallthrough: BlockId} | null = null;
const temporaries: Map<IdentifierId, Place> = new Map();
function visitOperand(operand: Place): AliasingEffect | null {
const nullTestRef = nullishTest.get(operand.identifier.id);
if (isRefValueType(operand.identifier) || nullTestRef != null) {
const refOperand = nullTestRef ?? operand;
return {
kind: 'Impure',
error: {
severity: ErrorSeverity.InvalidReact,
reason:
'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)',
loc: refOperand.loc,
description:
refOperand.identifier.name !== null &&
refOperand.identifier.name.kind === 'named'
? `Cannot access ref value \`${refOperand.identifier.name.value}\``
: null,
suggestions: null,
},
place: refOperand,
};
}
return null;
}
for (const block of fn.body.blocks.values()) {
if (guard !== null && guard.fallthrough === block.id) {
guard = null;
}
for (const instr of block.instructions) {
const {lvalue, value} = instr;
if (value.kind === 'LoadLocal' && isUseRefType(value.place.identifier)) {
temporaries.set(lvalue.identifier.id, value.place);
} else if (
value.kind === 'StoreLocal' &&
isUseRefType(value.value.identifier)
) {
temporaries.set(value.lvalue.place.identifier.id, value.value);
temporaries.set(lvalue.identifier.id, value.value);
} else if (
value.kind === 'BinaryExpression' &&
((isRefValueType(value.left.identifier) &&
nullish.has(value.right.identifier.id)) ||
(nullish.has(value.left.identifier.id) &&
isRefValueType(value.right.identifier)))
) {
const refOperand = isRefValueType(value.left.identifier)
? value.left
: value.right;
const operand = temporaries.get(refOperand.identifier.id) ?? refOperand;
nullishTest.set(lvalue.identifier.id, operand);
} else if (value.kind === 'Primitive' && value.value == null) {
nullish.add(lvalue.identifier.id);
} else if (
value.kind === 'PropertyLoad' &&
isUseRefType(value.object.identifier) &&
value.property === 'current'
) {
const refOperand =
temporaries.get(value.object.identifier.id) ?? value.object;
temporaries.set(lvalue.identifier.id, refOperand);
} else if (
value.kind === 'PropertyStore' &&
value.property === 'current' &&
isUseRefType(value.object.identifier)
) {
const refOperand =
temporaries.get(value.object.identifier.id) ?? value.object;
if (guard != null && refOperand.identifier.id === guard.ref) {
// Allow a single write within the guard
guard = null;
} else {
instr.effects ??= [];
instr.effects.push({
kind: 'Impure',
error: {
severity: ErrorSeverity.InvalidReact,
reason:
'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)',
loc: value.loc,
description:
value.object.identifier.name !== null &&
value.object.identifier.name.kind === 'named'
? `Cannot access ref value \`${value.object.identifier.name.value}\``
: null,
suggestions: null,
},
place: value.object,
});
}
} else {
for (const operand of eachInstructionValueOperand(value)) {
const error = visitOperand(operand);
if (error) {
instr.effects ??= [];
instr.effects.push(error);
}
}
}
}
if (
guard == null &&
block.terminal.kind === 'if' &&
nullishTest.has(block.terminal.test.identifier.id)
) {
const ref = nullishTest.get(block.terminal.test.identifier.id)!;
guard = {ref: ref.identifier.id, fallthrough: block.terminal.fallthrough};
} else {
for (const operand of eachTerminalOperand(block.terminal)) {
const _effect = visitOperand(operand);
// TODO: need a place to store terminal effects generically
}
}
}
}

View File

@@ -18,6 +18,7 @@ import {
ValueKind,
ValueReason,
Place,
isPrimitiveType,
} from '../HIR/HIR';
import {
eachInstructionLValue,
@@ -471,15 +472,15 @@ export function inferMutationAliasingRanges(
* Here we populate an effect to create the return value as well as populating alias/capture
* effects for how data flows between the params, context vars, and return.
*/
const returns = fn.returns.identifier;
functionEffects.push({
kind: 'Create',
into: fn.returns,
value:
fn.returnType.kind === 'Primitive'
? ValueKind.Primitive
: isJsxType(fn.returnType)
? ValueKind.Frozen
: ValueKind.Mutable,
value: isPrimitiveType(returns)
? ValueKind.Primitive
: isJsxType(returns.type)
? ValueKind.Frozen
: ValueKind.Mutable,
reason: ValueReason.KnownReturnSignature,
});
/**

View File

@@ -25,7 +25,6 @@ import {
makeBlockId,
makeInstructionId,
makePropertyLiteral,
makeType,
markInstructionIds,
promoteTemporary,
reversePostorderBlocks,
@@ -253,7 +252,6 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
env,
params: [obj],
returnTypeAnnotation: null,
returnType: makeType(),
returns: createTemporaryPlace(env, GeneratedSource),
context: [],
effects: null,

View File

@@ -21,7 +21,6 @@ import {
makeBlockId,
makeIdentifierName,
makeInstructionId,
makeType,
ObjectProperty,
Place,
promoteTemporary,
@@ -368,7 +367,6 @@ function emitOutlinedFn(
env,
params: [propsObj],
returnTypeAnnotation: null,
returnType: makeType(),
returns: createTemporaryPlace(env, GeneratedSource),
context: [],
effects: null,

View File

@@ -349,11 +349,9 @@ function codegenReactiveFunction(
fn: ReactiveFunction,
): Result<CodegenFunction, CompilerError> {
for (const param of fn.params) {
if (param.kind === 'Identifier') {
cx.temp.set(param.identifier.declarationId, null);
} else {
cx.temp.set(param.place.identifier.declarationId, null);
}
const place = param.kind === 'Identifier' ? param : param.place;
cx.temp.set(place.identifier.declarationId, null);
cx.declare(place.identifier);
}
const params = fn.params.map(param => convertParameter(param));
@@ -1183,7 +1181,7 @@ function codegenTerminal(
? codegenPlaceToExpression(cx, case_.test)
: null;
const block = codegenBlock(cx, case_.block!);
return t.switchCase(test, [block]);
return t.switchCase(test, block.body.length === 0 ? [] : [block]);
}),
);
}

View File

@@ -79,6 +79,10 @@ export function extractScopeDeclarationsFromDestructuring(
fn: ReactiveFunction,
): void {
const state = new State(fn.env);
for (const param of fn.params) {
const place = param.kind === 'Identifier' ? param : param.place;
state.declared.add(place.identifier.declarationId);
}
visitReactiveFunction(fn, new Visitor(), state);
}

View File

@@ -90,7 +90,8 @@ function apply(func: HIRFunction, unifier: Unifier): void {
}
}
}
func.returnType = unifier.get(func.returnType);
const returns = func.returns.identifier;
returns.type = unifier.get(returns.type);
}
type TypeEquation = {
@@ -143,12 +144,12 @@ function* generate(
}
}
if (returnTypes.length > 1) {
yield equation(func.returnType, {
yield equation(func.returns.identifier.type, {
kind: 'Phi',
operands: returnTypes,
});
} else if (returnTypes.length === 1) {
yield equation(func.returnType, returnTypes[0]!);
yield equation(func.returns.identifier.type, returnTypes[0]!);
}
}
@@ -407,7 +408,7 @@ function* generateInstructionTypes(
yield equation(left, {
kind: 'Function',
shapeId: BuiltInFunctionId,
return: value.loweredFunc.func.returnType,
return: value.loweredFunc.func.returns.identifier.type,
isConstructor: false,
});
break;

View File

@@ -50,8 +50,7 @@ function Component(props) {
console.log(handlers.value);
break bb0;
}
default: {
}
default:
}
t0 = handlers;

View File

@@ -67,8 +67,7 @@ function Component(props) {
case "b": {
break bb1;
}
case "c": {
}
case "c":
default: {
x = 6;
}

View File

@@ -24,8 +24,6 @@ export const FIXTURE_ENTRYPOINT = {
4 | const ref = useRef();
> 5 | useEffect(() => {}, [ref.current]);
| ^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (5:5)
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (5:5)
6 | }
7 |
8 | export const FIXTURE_ENTRYPOINT = {

View File

@@ -19,6 +19,8 @@ function Component(props) {
3 | const ref = useRef(null);
> 4 | const value = ref.current;
| ^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (4:4)
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef). Cannot access ref value `value` (5:5)
5 | return value;
6 | }
7 |

View File

@@ -19,12 +19,17 @@ function Component(props) {
## Error
```
7 | return <Foo item={item} current={current} />;
8 | };
> 9 | return <Items>{props.items.map(item => renderItem(item))}</Items>;
| ^^^^^^^^^^^^^^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (9:9)
10 | }
11 |
4 | const renderItem = item => {
5 | const aliasedRef = ref;
> 6 | const current = aliasedRef.current;
| ^^^^^^^^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (6:6)
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef). Cannot access ref value `current` (7:7)
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (7:7)
7 | return <Foo item={item} current={current} />;
8 | };
9 | return <Items>{props.items.map(item => renderItem(item))}</Items>;
```

View File

@@ -21,15 +21,13 @@ function Component() {
## Error
```
7 | };
8 | const changeRef = setRef;
> 9 | changeRef();
| ^^^^^^^^^ InvalidReact: This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef) (9:9)
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (9:9)
10 |
11 | return <button ref={ref} />;
12 | }
4 |
5 | const setRef = () => {
> 6 | ref.current = false;
| ^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (6:6)
7 | };
8 | const changeRef = setRef;
9 | changeRef();
```

View File

@@ -18,6 +18,10 @@ function Component({ref}) {
2 | function Component({ref}) {
> 3 | const value = ref.current;
| ^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (3:3)
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef). Cannot access ref value `value` (4:4)
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (4:4)
4 | return <div>{value}</div>;
5 | }
6 |

View File

@@ -18,6 +18,10 @@ function Component(props) {
2 | function Component(props) {
> 3 | const value = props.ref.current;
| ^^^^^^^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (3:3)
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef). Cannot access ref value `value` (4:4)
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (4:4)
4 | return <div>{value}</div>;
5 | }
6 |

View File

@@ -18,12 +18,17 @@ function Component(props) {
## Error
```
6 | return <Foo item={item} current={current} />;
7 | };
> 8 | return <Items>{props.items.map(item => renderItem(item))}</Items>;
| ^^^^^^^^^^^^^^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (8:8)
9 | }
10 |
3 | const ref = useRef(null);
4 | const renderItem = item => {
> 5 | const current = ref.current;
| ^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (5:5)
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef). Cannot access ref value `current` (6:6)
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (6:6)
6 | return <Foo item={item} current={current} />;
7 | };
8 | return <Items>{props.items.map(item => renderItem(item))}</Items>;
```

View File

@@ -19,8 +19,6 @@ function Component(props) {
3 | const ref = useRef(null);
> 4 | ref.current = props.value;
| ^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (4:4)
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (5:5)
5 | return ref.current;
6 | }
7 |

View File

@@ -27,9 +27,9 @@ export const FIXTURE_ENTRYPOINT = {
4 | component C() {
5 | const r = useRef(null);
> 6 | const guard = r.current == null;
| ^^^^^^^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (6:6)
| ^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef). Cannot access ref value `r` (6:6)
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef). Cannot access ref value `guard` (7:7)
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (8:8)
7 | if (guard) {
8 | r.current = 1;
9 | }

View File

@@ -34,15 +34,13 @@ export const FIXTURE_ENTRYPOINT = {
## Error
```
15 | ref.current.inner = null;
13 | // The ref is modified later, extending its range and preventing memoization of onChange
14 | const reset = () => {
> 15 | ref.current.inner = null;
| ^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (15:15)
16 | };
> 17 | reset();
| ^^^^^ InvalidReact: This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef) (17:17)
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (17:17)
17 | reset();
18 |
19 | return <input onChange={onChange} />;
20 | }
```

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @validateRefAccessDuringRender false
// @validateRefAccessDuringRender:false
function VideoTab() {
const ref = useRef();
const t = ref.current;
@@ -18,7 +18,7 @@ function VideoTab() {
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateRefAccessDuringRender false
import { c as _c } from "react/compiler-runtime"; // @validateRefAccessDuringRender:false
function VideoTab() {
const $ = _c(1);
const ref = useRef();

View File

@@ -1,4 +1,4 @@
// @validateRefAccessDuringRender false
// @validateRefAccessDuringRender:false
function VideoTab() {
const ref = useRef();
const t = ref.current;

View File

@@ -0,0 +1,65 @@
## Input
```javascript
import {Stringify, useIdentity} from 'shared-runtime';
function Component({other, ...props}, ref) {
[props, ref] = useIdentity([props, ref]);
return <Stringify props={props} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: 0, b: 'hello', children: <div>Hello</div>}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { Stringify, useIdentity } from "shared-runtime";
function Component(t0, ref) {
const $ = _c(7);
let props;
if ($[0] !== t0) {
let { other, ...t1 } = t0;
props = t1;
$[0] = t0;
$[1] = props;
} else {
props = $[1];
}
let t1;
if ($[2] !== props || $[3] !== ref) {
t1 = [props, ref];
$[2] = props;
$[3] = ref;
$[4] = t1;
} else {
t1 = $[4];
}
[props, ref] = useIdentity(t1);
let t2;
if ($[5] !== props) {
t2 = <Stringify props={props} />;
$[5] = props;
$[6] = t2;
} else {
t2 = $[6];
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ a: 0, b: "hello", children: <div>Hello</div> }],
};
```
### Eval output
(kind: ok) <div>{"props":{"a":0,"b":"hello","children":{"type":"div","key":null,"props":{"children":"Hello"},"_owner":"[[ cyclic ref *3 ]]","_store":{}}}}</div>

View File

@@ -0,0 +1,11 @@
import {Stringify, useIdentity} from 'shared-runtime';
function Component({other, ...props}, ref) {
[props, ref] = useIdentity([props, ref]);
return <Stringify props={props} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{a: 0, b: 'hello', children: <div>Hello</div>}],
};

View File

@@ -50,10 +50,8 @@ function Component(props) {
case 1: {
break bb0;
}
case 2: {
}
default: {
}
case 2:
default:
}
} else {
if (props.cond2) {

View File

@@ -41,8 +41,7 @@ function foo() {
case 2: {
break bb0;
}
default: {
}
default:
}
}

View File

@@ -43,22 +43,17 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
function foo(x) {
bb0: switch (x) {
case 0: {
}
case 1: {
}
case 0:
case 1:
case 2: {
break bb0;
}
case 3: {
break bb0;
}
case 4: {
}
case 5: {
}
default: {
}
case 4:
case 5:
default:
}
}

View File

@@ -12,7 +12,6 @@ import type {
TouchedViewDataAtPoint,
ViewConfig,
} from './ReactNativeTypes';
import {create, diff} from './ReactNativeAttributePayloadFabric';
import {dispatchEvent} from './ReactFabricEventEmitter';
import {
NoEventPriority,
@@ -35,6 +34,8 @@ import {
deepFreezeAndThrowOnMutationInDev,
createPublicInstance,
createPublicTextInstance,
createAttributePayload,
diffAttributePayloads,
type PublicInstance as ReactNativePublicInstance,
type PublicTextInstance,
type PublicRootInstance,
@@ -190,7 +191,10 @@ export function createInstance(
}
}
const updatePayload = create(props, viewConfig.validAttributes);
const updatePayload = createAttributePayload(
props,
viewConfig.validAttributes,
);
const node = createNode(
tag, // reactTag
@@ -456,7 +460,11 @@ export function cloneInstance(
newChildSet: ?ChildSet,
): Instance {
const viewConfig = instance.canonical.viewConfig;
const updatePayload = diff(oldProps, newProps, viewConfig.validAttributes);
const updatePayload = diffAttributePayloads(
oldProps,
newProps,
viewConfig.validAttributes,
);
// TODO: If the event handlers have changed, we need to update the current props
// in the commit phase but there is no host config hook to do it yet.
// So instead we hack it by updating it in the render phase.
@@ -505,7 +513,7 @@ export function cloneHiddenInstance(
): Instance {
const viewConfig = instance.canonical.viewConfig;
const node = instance.node;
const updatePayload = create(
const updatePayload = createAttributePayload(
{style: {display: 'none'}},
viewConfig.validAttributes,
);

View File

@@ -1,514 +0,0 @@
/**
* 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.
*
* @flow
*/
// Modules provided by RN:
import {
deepDiffer,
flattenStyle,
} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
import isArray from 'shared/isArray';
import type {AttributeConfiguration} from './ReactNativeTypes';
const emptyObject = {};
/**
* Create a payload that contains all the updates between two sets of props.
*
* These helpers are all encapsulated into a single module, because they use
* mutation as a performance optimization which leads to subtle shared
* dependencies between the code paths. To avoid this mutable state leaking
* across modules, I've kept them isolated to this module.
*/
type NestedNode = Array<NestedNode> | Object;
// Tracks removed keys
let removedKeys: {[string]: boolean} | null = null;
let removedKeyCount = 0;
const deepDifferOptions = {
unsafelyIgnoreFunctions: true,
};
function defaultDiffer(prevProp: mixed, nextProp: mixed): boolean {
if (typeof nextProp !== 'object' || nextProp === null) {
// Scalars have already been checked for equality
return true;
} else {
// For objects and arrays, the default diffing algorithm is a deep compare
return deepDiffer(prevProp, nextProp, deepDifferOptions);
}
}
function restoreDeletedValuesInNestedArray(
updatePayload: Object,
node: NestedNode,
validAttributes: AttributeConfiguration,
) {
if (isArray(node)) {
let i = node.length;
while (i-- && removedKeyCount > 0) {
restoreDeletedValuesInNestedArray(
updatePayload,
node[i],
validAttributes,
);
}
} else if (node && removedKeyCount > 0) {
const obj = node;
for (const propKey in removedKeys) {
// $FlowFixMe[incompatible-use] found when upgrading Flow
if (!removedKeys[propKey]) {
continue;
}
let nextProp = obj[propKey];
if (nextProp === undefined) {
continue;
}
const attributeConfig = validAttributes[propKey];
if (!attributeConfig) {
continue; // not a valid native prop
}
if (typeof nextProp === 'function') {
// $FlowFixMe[incompatible-type] found when upgrading Flow
nextProp = true;
}
if (typeof nextProp === 'undefined') {
// $FlowFixMe[incompatible-type] found when upgrading Flow
nextProp = null;
}
if (typeof attributeConfig !== 'object') {
// case: !Object is the default case
updatePayload[propKey] = nextProp;
} else if (
typeof attributeConfig.diff === 'function' ||
typeof attributeConfig.process === 'function'
) {
// case: CustomAttributeConfiguration
const nextValue =
typeof attributeConfig.process === 'function'
? attributeConfig.process(nextProp)
: nextProp;
updatePayload[propKey] = nextValue;
}
// $FlowFixMe[incompatible-use] found when upgrading Flow
removedKeys[propKey] = false;
removedKeyCount--;
}
}
}
function diffNestedArrayProperty(
updatePayload: null | Object,
prevArray: Array<NestedNode>,
nextArray: Array<NestedNode>,
validAttributes: AttributeConfiguration,
): null | Object {
const minLength =
prevArray.length < nextArray.length ? prevArray.length : nextArray.length;
let i;
for (i = 0; i < minLength; i++) {
// Diff any items in the array in the forward direction. Repeated keys
// will be overwritten by later values.
updatePayload = diffNestedProperty(
updatePayload,
prevArray[i],
nextArray[i],
validAttributes,
);
}
for (; i < prevArray.length; i++) {
// Clear out all remaining properties.
updatePayload = clearNestedProperty(
updatePayload,
prevArray[i],
validAttributes,
);
}
for (; i < nextArray.length; i++) {
// Add all remaining properties
const nextProp = nextArray[i];
if (!nextProp) {
continue;
}
updatePayload = addNestedProperty(updatePayload, nextProp, validAttributes);
}
return updatePayload;
}
function diffNestedProperty(
updatePayload: null | Object,
prevProp: NestedNode,
nextProp: NestedNode,
validAttributes: AttributeConfiguration,
): null | Object {
if (!updatePayload && prevProp === nextProp) {
// If no properties have been added, then we can bail out quickly on object
// equality.
return updatePayload;
}
if (!prevProp || !nextProp) {
if (nextProp) {
return addNestedProperty(updatePayload, nextProp, validAttributes);
}
if (prevProp) {
return clearNestedProperty(updatePayload, prevProp, validAttributes);
}
return updatePayload;
}
if (!isArray(prevProp) && !isArray(nextProp)) {
// Both are leaves, we can diff the leaves.
return diffProperties(updatePayload, prevProp, nextProp, validAttributes);
}
if (isArray(prevProp) && isArray(nextProp)) {
// Both are arrays, we can diff the arrays.
return diffNestedArrayProperty(
updatePayload,
prevProp,
nextProp,
validAttributes,
);
}
if (isArray(prevProp)) {
return diffProperties(
updatePayload,
flattenStyle(prevProp),
nextProp,
validAttributes,
);
}
return diffProperties(
updatePayload,
prevProp,
flattenStyle(nextProp),
validAttributes,
);
}
/**
* clearNestedProperty takes a single set of props and valid attributes. It
* adds a null sentinel to the updatePayload, for each prop key.
*/
function clearNestedProperty(
updatePayload: null | Object,
prevProp: NestedNode,
validAttributes: AttributeConfiguration,
): null | Object {
if (!prevProp) {
return updatePayload;
}
if (!isArray(prevProp)) {
// Add each property of the leaf.
return clearProperties(updatePayload, prevProp, validAttributes);
}
for (let i = 0; i < prevProp.length; i++) {
// Add all the properties of the array.
updatePayload = clearNestedProperty(
updatePayload,
prevProp[i],
validAttributes,
);
}
return updatePayload;
}
/**
* diffProperties takes two sets of props and a set of valid attributes
* and write to updatePayload the values that changed or were deleted.
* If no updatePayload is provided, a new one is created and returned if
* anything changed.
*/
function diffProperties(
updatePayload: null | Object,
prevProps: Object,
nextProps: Object,
validAttributes: AttributeConfiguration,
): null | Object {
let attributeConfig;
let nextProp;
let prevProp;
for (const propKey in nextProps) {
attributeConfig = validAttributes[propKey];
if (!attributeConfig) {
continue; // not a valid native prop
}
prevProp = prevProps[propKey];
nextProp = nextProps[propKey];
if (typeof nextProp === 'function') {
const attributeConfigHasProcess =
typeof attributeConfig === 'object' &&
typeof attributeConfig.process === 'function';
if (!attributeConfigHasProcess) {
// functions are converted to booleans as markers that the associated
// events should be sent from native.
nextProp = (true: any);
// If nextProp is not a function, then don't bother changing prevProp
// since nextProp will win and go into the updatePayload regardless.
if (typeof prevProp === 'function') {
prevProp = (true: any);
}
}
}
// An explicit value of undefined is treated as a null because it overrides
// any other preceding value.
if (typeof nextProp === 'undefined') {
nextProp = (null: any);
if (typeof prevProp === 'undefined') {
prevProp = (null: any);
}
}
if (removedKeys) {
removedKeys[propKey] = false;
}
if (updatePayload && updatePayload[propKey] !== undefined) {
// Something else already triggered an update to this key because another
// value diffed. Since we're now later in the nested arrays our value is
// more important so we need to calculate it and override the existing
// value. It doesn't matter if nothing changed, we'll set it anyway.
// Pattern match on: attributeConfig
if (typeof attributeConfig !== 'object') {
// case: !Object is the default case
updatePayload[propKey] = nextProp;
} else if (
typeof attributeConfig.diff === 'function' ||
typeof attributeConfig.process === 'function'
) {
// case: CustomAttributeConfiguration
const nextValue =
typeof attributeConfig.process === 'function'
? attributeConfig.process(nextProp)
: nextProp;
updatePayload[propKey] = nextValue;
}
continue;
}
if (prevProp === nextProp) {
continue; // nothing changed
}
// Pattern match on: attributeConfig
if (typeof attributeConfig !== 'object') {
// case: !Object is the default case
if (defaultDiffer(prevProp, nextProp)) {
// a normal leaf has changed
(updatePayload || (updatePayload = ({}: {[string]: $FlowFixMe})))[
propKey
] = nextProp;
}
} else if (
typeof attributeConfig.diff === 'function' ||
typeof attributeConfig.process === 'function'
) {
// case: CustomAttributeConfiguration
const shouldUpdate =
prevProp === undefined ||
(typeof attributeConfig.diff === 'function'
? attributeConfig.diff(prevProp, nextProp)
: defaultDiffer(prevProp, nextProp));
if (shouldUpdate) {
const nextValue =
typeof attributeConfig.process === 'function'
? // $FlowFixMe[incompatible-use] found when upgrading Flow
attributeConfig.process(nextProp)
: nextProp;
(updatePayload || (updatePayload = ({}: {[string]: $FlowFixMe})))[
propKey
] = nextValue;
}
} else {
// default: fallthrough case when nested properties are defined
removedKeys = null;
removedKeyCount = 0;
// We think that attributeConfig is not CustomAttributeConfiguration at
// this point so we assume it must be AttributeConfiguration.
updatePayload = diffNestedProperty(
updatePayload,
prevProp,
nextProp,
((attributeConfig: any): AttributeConfiguration),
);
if (removedKeyCount > 0 && updatePayload) {
restoreDeletedValuesInNestedArray(
updatePayload,
nextProp,
((attributeConfig: any): AttributeConfiguration),
);
removedKeys = null;
}
}
}
// Also iterate through all the previous props to catch any that have been
// removed and make sure native gets the signal so it can reset them to the
// default.
for (const propKey in prevProps) {
if (nextProps[propKey] !== undefined) {
continue; // we've already covered this key in the previous pass
}
attributeConfig = validAttributes[propKey];
if (!attributeConfig) {
continue; // not a valid native prop
}
if (updatePayload && updatePayload[propKey] !== undefined) {
// This was already updated to a diff result earlier.
continue;
}
prevProp = prevProps[propKey];
if (prevProp === undefined) {
continue; // was already empty anyway
}
// Pattern match on: attributeConfig
if (
typeof attributeConfig !== 'object' ||
typeof attributeConfig.diff === 'function' ||
typeof attributeConfig.process === 'function'
) {
// case: CustomAttributeConfiguration | !Object
// Flag the leaf property for removal by sending a sentinel.
(updatePayload || (updatePayload = ({}: {[string]: $FlowFixMe})))[
propKey
] = null;
if (!removedKeys) {
removedKeys = ({}: {[string]: boolean});
}
if (!removedKeys[propKey]) {
removedKeys[propKey] = true;
removedKeyCount++;
}
} else {
// default:
// This is a nested attribute configuration where all the properties
// were removed so we need to go through and clear out all of them.
updatePayload = clearNestedProperty(
updatePayload,
prevProp,
((attributeConfig: any): AttributeConfiguration),
);
}
}
return updatePayload;
}
function addNestedProperty(
payload: null | Object,
props: Object,
validAttributes: AttributeConfiguration,
): null | Object {
// Flatten nested style props.
if (isArray(props)) {
for (let i = 0; i < props.length; i++) {
payload = addNestedProperty(payload, props[i], validAttributes);
}
return payload;
}
for (const propKey in props) {
const prop = props[propKey];
const attributeConfig = ((validAttributes[
propKey
]: any): AttributeConfiguration);
if (attributeConfig == null) {
continue;
}
let newValue;
if (prop === undefined) {
// Discard the prop if it was previously defined.
if (payload && payload[propKey] !== undefined) {
newValue = null;
} else {
continue;
}
} else if (typeof attributeConfig === 'object') {
if (typeof attributeConfig.process === 'function') {
// An atomic prop with custom processing.
newValue = attributeConfig.process(prop);
} else if (typeof attributeConfig.diff === 'function') {
// An atomic prop with custom diffing. We don't need to do diffing when adding props.
newValue = prop;
}
} else {
if (typeof prop === 'function') {
// A function prop. It represents an event handler. Pass it to native as 'true'.
newValue = true;
} else {
// An atomic prop. Doesn't need to be flattened.
newValue = prop;
}
}
if (newValue !== undefined) {
if (!payload) {
payload = ({}: {[string]: $FlowFixMe});
}
payload[propKey] = newValue;
continue;
}
payload = addNestedProperty(payload, prop, attributeConfig);
}
return payload;
}
/**
* clearProperties clears all the previous props by adding a null sentinel
* to the payload for each valid key.
*/
function clearProperties(
updatePayload: null | Object,
prevProps: Object,
validAttributes: AttributeConfiguration,
): null | Object {
return diffProperties(updatePayload, prevProps, emptyObject, validAttributes);
}
export function create(
props: Object,
validAttributes: AttributeConfiguration,
): null | Object {
return addNestedProperty(null, props, validAttributes);
}
export function diff(
prevProps: Object,
nextProps: Object,
validAttributes: AttributeConfiguration,
): null | Object {
return diffProperties(
null, // updatePayload
prevProps,
nextProps,
validAttributes,
);
}

View File

@@ -34,15 +34,6 @@ export type AttributeType<T, V> =
export type AnyAttributeType = AttributeType<$FlowFixMe, $FlowFixMe>;
export type AttributeConfiguration = $ReadOnly<{
[propName: string]: AnyAttributeType,
style: $ReadOnly<{
[propName: string]: AnyAttributeType,
...
}>,
...
}>;
export type PartialAttributeConfiguration = $ReadOnly<{
[propName: string]: AnyAttributeType,
style?: $ReadOnly<{
[propName: string]: AnyAttributeType,
@@ -83,7 +74,7 @@ export type PartialViewConfig = $ReadOnly<{
directEventTypes?: ViewConfig['directEventTypes'],
supportsRawText?: boolean,
uiViewClassName: string,
validAttributes?: PartialAttributeConfiguration,
validAttributes?: AttributeConfiguration,
}>;
type InspectorDataProps = $ReadOnly<{

View File

@@ -59,4 +59,10 @@ module.exports = {
get createPublicRootInstance() {
return require('./createPublicRootInstance').default;
},
get createAttributePayload() {
return require('./createAttributePayload').default;
},
get diffAttributePayloads() {
return require('./diffAttributePayloads').default;
},
};

View File

@@ -0,0 +1,18 @@
/**
* 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.
*
* @flow strict
*/
import type {AttributeConfiguration} from '../../../../ReactNativeTypes';
export default function create(
props: Object,
validAttributes: AttributeConfiguration,
): null | Object {
const {children, ...propsToPass} = props;
return propsToPass;
}

View File

@@ -0,0 +1,22 @@
/**
* 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.
*
* @flow strict
*/
import type {AttributeConfiguration} from '../../../../ReactNativeTypes';
import deepDiffer from './deepDiffer';
export default function diff(
prevProps: Object,
nextProps: Object,
validAttributes: AttributeConfiguration,
): null | Object {
const {children: _prevChildren, ...prevPropsPassed} = prevProps;
const {children: _nextChildren, ...nextPropsToPass} = nextProps;
return deepDiffer(prevPropsPassed, nextPropsToPass) ? nextPropsToPass : null;
}

View File

@@ -184,6 +184,10 @@ describe('ReactFabric', () => {
nativeFabricUIManager.cloneNodeWithNewChildrenAndProps,
).not.toBeCalled();
jest
.spyOn(ReactNativePrivateInterface, 'diffAttributePayloads')
.mockReturnValue({bar: 'b'});
await act(() => {
ReactFabric.render(
<Text foo="a" bar="b">
@@ -203,6 +207,9 @@ describe('ReactFabric', () => {
RCTText {"foo":"a","bar":"b"}
RCTRawText {"text":"1"}`);
jest
.spyOn(ReactNativePrivateInterface, 'diffAttributePayloads')
.mockReturnValue({foo: 'b'});
await act(() => {
ReactFabric.render(
<Text foo="b" bar="b">
@@ -612,7 +619,7 @@ describe('ReactFabric', () => {
ReactFabric.render(<Component chars={before} />, 11, null, true);
});
expect(nativeFabricUIManager.__dumpHierarchyForJestTestsOnly()).toBe(`11
RCTView null
RCTView {}
RCTView {"title":"a"}
RCTView {"title":"b"}
RCTView {"title":"c"}
@@ -638,7 +645,7 @@ describe('ReactFabric', () => {
ReactFabric.render(<Component chars={after} />, 11, null, true);
});
expect(nativeFabricUIManager.__dumpHierarchyForJestTestsOnly()).toBe(`11
RCTView null
RCTView {}
RCTView {"title":"m"}
RCTView {"title":"x"}
RCTView {"title":"h"}
@@ -700,8 +707,8 @@ describe('ReactFabric', () => {
});
expect(nativeFabricUIManager.__dumpHierarchyForJestTestsOnly()).toBe(
`11
RCTView null
RCTView null
RCTView {}
RCTView {}
RCTView {"title":"a"}
RCTView {"title":"b"}
RCTView {"title":"c"}
@@ -732,8 +739,8 @@ describe('ReactFabric', () => {
});
});
expect(nativeFabricUIManager.__dumpHierarchyForJestTestsOnly()).toBe(`11
RCTView null
RCTView null
RCTView {}
RCTView {}
RCTView {"title":"m"}
RCTView {"title":"x"}
RCTView {"title":"h"}

View File

@@ -1,480 +0,0 @@
/**
* 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.
*
* @jest-environment node
*/
'use strict';
const {diff, create} = require('../ReactNativeAttributePayloadFabric');
describe('ReactNativeAttributePayloadFabric.create', () => {
it('should work with simple example', () => {
expect(create({b: 2, c: 3}, {a: true, b: true})).toEqual({
b: 2,
});
});
it('should work with complex example', () => {
const validAttributes = {
style: {
position: true,
zIndex: true,
flexGrow: true,
flexShrink: true,
flexDirection: true,
overflow: true,
backgroundColor: true,
},
};
expect(
create(
{
style: [
{
flexGrow: 1,
flexShrink: 1,
flexDirection: 'row',
overflow: 'scroll',
},
[
{position: 'relative', zIndex: 2},
{flexGrow: 0},
{backgroundColor: 'red'},
],
],
},
validAttributes,
),
).toEqual({
flexGrow: 0,
flexShrink: 1,
flexDirection: 'row',
overflow: 'scroll',
position: 'relative',
zIndex: 2,
backgroundColor: 'red',
});
});
it('should nullify previously defined style prop that is subsequently set to null or undefined', () => {
expect(
create({style: [{a: 0}, {a: undefined}]}, {style: {a: true}}),
).toEqual({a: null});
expect(create({style: [{a: 0}, {a: null}]}, {style: {a: true}})).toEqual({
a: null,
});
});
it('should ignore non-style fields that are set to undefined', () => {
expect(create({}, {a: true})).toEqual(null);
expect(create({a: undefined}, {a: true})).toEqual(null);
expect(create({a: undefined, b: undefined}, {a: true, b: true})).toEqual(
null,
);
expect(
create({a: undefined, b: undefined, c: 1}, {a: true, b: true}),
).toEqual(null);
expect(
create({a: undefined, b: undefined, c: 1}, {a: true, b: true, c: true}),
).toEqual({c: 1});
expect(
create({a: 1, b: undefined, c: 2}, {a: true, b: true, c: true}),
).toEqual({a: 1, c: 2});
});
it('should ignore invalid fields', () => {
expect(create({b: 2}, {})).toEqual(null);
});
it('should not use the diff attribute', () => {
const diffA = jest.fn();
expect(create({a: [2]}, {a: {diff: diffA}})).toEqual({a: [2]});
expect(diffA).not.toBeCalled();
});
it('should use the process attribute', () => {
const processA = jest.fn(a => a + 1);
expect(create({a: 2}, {a: {process: processA}})).toEqual({a: 3});
expect(processA).toBeCalledWith(2);
});
it('should use the process attribute for functions as well', () => {
const process = x => x;
const nextFunction = () => {};
expect(create({a: nextFunction}, {a: {process}})).toEqual({
a: nextFunction,
});
});
it('should work with undefined styles', () => {
expect(create({style: undefined}, {style: {b: true}})).toEqual(null);
expect(create({style: {a: '#ffffff', b: 1}}, {style: {b: true}})).toEqual({
b: 1,
});
});
it('should flatten nested styles and predefined styles', () => {
const validStyleAttribute = {someStyle: {foo: true, bar: true}};
expect(
create({someStyle: [{foo: 1}, {bar: 2}]}, validStyleAttribute),
).toEqual({foo: 1, bar: 2});
expect(create({}, validStyleAttribute)).toEqual(null);
const barStyle = {
bar: 3,
};
expect(
create(
{someStyle: [[{foo: 1}, {foo: 2}], barStyle]},
validStyleAttribute,
),
).toEqual({foo: 2, bar: 3});
});
it('should not flatten nested props if attribute config is a primitive or only has diff/process', () => {
expect(create({a: {foo: 1, bar: 2}}, {a: true})).toEqual({
a: {foo: 1, bar: 2},
});
expect(create({a: [{foo: 1}, {bar: 2}]}, {a: true})).toEqual({
a: [{foo: 1}, {bar: 2}],
});
expect(create({a: {foo: 1, bar: 2}}, {a: {diff: a => a}})).toEqual({
a: {foo: 1, bar: 2},
});
expect(
create({a: [{foo: 1}, {bar: 2}]}, {a: {diff: a => a, process: a => a}}),
).toEqual({a: [{foo: 1}, {bar: 2}]});
});
it('handles attributes defined multiple times', () => {
const validAttributes = {foo: true, style: {foo: true}};
expect(create({foo: 4, style: {foo: 2}}, validAttributes)).toEqual({
foo: 2,
});
expect(create({style: {foo: 2}}, validAttributes)).toEqual({
foo: 2,
});
expect(create({style: {foo: 2}, foo: 4}, validAttributes)).toEqual({
foo: 4,
});
expect(create({foo: 4, style: {foo: null}}, validAttributes)).toEqual({
foo: null, // this should ideally be null.
});
expect(
create({foo: 4, style: [{foo: null}, {foo: 5}]}, validAttributes),
).toEqual({
foo: 5,
});
});
// Function properties are just markers to native that events should be sent.
it('should convert functions to booleans', () => {
expect(
create(
{
a: function () {
return 9;
},
b: function () {
return 3;
},
},
{a: true, b: true},
),
).toEqual({a: true, b: true});
});
});
describe('ReactNativeAttributePayloadFabric.diff', () => {
it('should work with simple example', () => {
expect(diff({a: 1, c: 3}, {b: 2, c: 3}, {a: true, b: true})).toEqual({
a: null,
b: 2,
});
});
it('should skip fields that are equal', () => {
expect(
diff(
{a: 1, b: 'two', c: true, d: false, e: undefined, f: 0},
{a: 1, b: 'two', c: true, d: false, e: undefined, f: 0},
{a: true, b: true, c: true, d: true, e: true, f: true},
),
).toEqual(null);
});
it('should remove fields', () => {
expect(diff({a: 1}, {}, {a: true})).toEqual({a: null});
});
it('should remove fields that are set to undefined', () => {
expect(diff({a: 1}, {a: undefined}, {a: true})).toEqual({a: null});
});
it('should ignore invalid fields', () => {
expect(diff({a: 1}, {b: 2}, {})).toEqual(null);
});
it('should use the diff attribute', () => {
const diffA = jest.fn((a, b) => true);
const diffB = jest.fn((a, b) => false);
expect(
diff(
{a: [1], b: [3]},
{a: [2], b: [4]},
{a: {diff: diffA}, b: {diff: diffB}},
),
).toEqual({a: [2]});
expect(diffA).toBeCalledWith([1], [2]);
expect(diffB).toBeCalledWith([3], [4]);
});
it('should not use the diff attribute on addition/removal', () => {
const diffA = jest.fn();
const diffB = jest.fn();
expect(
diff({a: [1]}, {b: [2]}, {a: {diff: diffA}, b: {diff: diffB}}),
).toEqual({a: null, b: [2]});
expect(diffA).not.toBeCalled();
expect(diffB).not.toBeCalled();
});
it('should do deep diffs of Objects by default', () => {
expect(
diff(
{a: [1], b: {k: [3, 4]}, c: {k: [4, 4]}},
{a: [2], b: {k: [3, 4]}, c: {k: [4, 5]}},
{a: true, b: true, c: true},
),
).toEqual({a: [2], c: {k: [4, 5]}});
});
it('should work with undefined styles', () => {
expect(
diff(
{style: {a: '#ffffff', b: 1}},
{style: undefined},
{style: {b: true}},
),
).toEqual({b: null});
expect(
diff(
{style: undefined},
{style: {a: '#ffffff', b: 1}},
{style: {b: true}},
),
).toEqual({b: 1});
expect(
diff({style: undefined}, {style: undefined}, {style: {b: true}}),
).toEqual(null);
});
it('should work with empty styles', () => {
expect(diff({a: 1, c: 3}, {}, {a: true, b: true})).toEqual({a: null});
expect(diff({}, {a: 1, c: 3}, {a: true, b: true})).toEqual({a: 1});
expect(diff({}, {}, {a: true, b: true})).toEqual(null);
});
it('should flatten nested styles and predefined styles', () => {
const validStyleAttribute = {someStyle: {foo: true, bar: true}};
expect(
diff({}, {someStyle: [{foo: 1}, {bar: 2}]}, validStyleAttribute),
).toEqual({foo: 1, bar: 2});
expect(
diff({someStyle: [{foo: 1}, {bar: 2}]}, {}, validStyleAttribute),
).toEqual({foo: null, bar: null});
const barStyle = {
bar: 3,
};
expect(
diff(
{},
{someStyle: [[{foo: 1}, {foo: 2}], barStyle]},
validStyleAttribute,
),
).toEqual({foo: 2, bar: 3});
});
it('should reset a value to a previous if it is removed', () => {
const validStyleAttribute = {someStyle: {foo: true, bar: true}};
expect(
diff(
{someStyle: [{foo: 1}, {foo: 3}]},
{someStyle: [{foo: 1}, {bar: 2}]},
validStyleAttribute,
),
).toEqual({foo: 1, bar: 2});
});
it('should not clear removed props if they are still in another slot', () => {
const validStyleAttribute = {someStyle: {foo: true, bar: true}};
expect(
diff(
{someStyle: [{}, {foo: 3, bar: 2}]},
{someStyle: [{foo: 3}, {bar: 2}]},
validStyleAttribute,
),
).toEqual({foo: 3}); // this should ideally be null. heuristic tradeoff.
expect(
diff(
{someStyle: [{}, {foo: 3, bar: 2}]},
{someStyle: [{foo: 1, bar: 1}, {bar: 2}]},
validStyleAttribute,
),
).toEqual({bar: 2, foo: 1});
});
it('should clear a prop if a later style is explicit null/undefined', () => {
const validStyleAttribute = {someStyle: {foo: true, bar: true}};
expect(
diff(
{someStyle: [{}, {foo: 3, bar: 2}]},
{someStyle: [{foo: 1}, {bar: 2, foo: null}]},
validStyleAttribute,
),
).toEqual({foo: null});
expect(
diff(
{someStyle: [{foo: 3}, {foo: null, bar: 2}]},
{someStyle: [{foo: null}, {bar: 2}]},
validStyleAttribute,
),
).toEqual({foo: null});
expect(
diff(
{someStyle: [{foo: 1}, {foo: null}]},
{someStyle: [{foo: 2}, {foo: null}]},
validStyleAttribute,
),
).toEqual({foo: null}); // this should ideally be null. heuristic.
// Test the same case with object equality because an early bailout doesn't
// work in this case.
const fooObj = {foo: 3};
expect(
diff(
{someStyle: [{foo: 1}, fooObj]},
{someStyle: [{foo: 2}, fooObj]},
validStyleAttribute,
),
).toEqual({foo: 3}); // this should ideally be null. heuristic.
expect(
diff(
{someStyle: [{foo: 1}, {foo: 3}]},
{someStyle: [{foo: 2}, {foo: undefined}]},
validStyleAttribute,
),
).toEqual({foo: null}); // this should ideally be null. heuristic.
});
it('handles attributes defined multiple times', () => {
const validAttributes = {foo: true, style: {foo: true}};
expect(diff({}, {foo: 4, style: {foo: 2}}, validAttributes)).toEqual({
foo: 2,
});
expect(diff({foo: 4}, {style: {foo: 2}}, validAttributes)).toEqual({
foo: 2,
});
expect(diff({style: {foo: 2}}, {foo: 4}, validAttributes)).toEqual({
foo: 4,
});
});
// Function properties are just markers to native that events should be sent.
it('should convert functions to booleans', () => {
// Note that if the property changes from one function to another, we don't
// need to send an update.
expect(
diff(
{
a: function () {
return 1;
},
b: function () {
return 2;
},
c: 3,
},
{
b: function () {
return 9;
},
c: function () {
return 3;
},
},
{a: true, b: true, c: true},
),
).toEqual({a: null, c: true});
});
it('should skip changed functions', () => {
expect(
diff(
{
a: function () {
return 1;
},
},
{
a: function () {
return 9;
},
},
{a: true},
),
).toEqual(null);
});
it('should skip deeply-nested changed functions', () => {
expect(
diff(
{
wrapper: {
a: function () {
return 1;
},
},
},
{
wrapper: {
a: function () {
return 9;
},
},
},
{wrapper: true},
),
).toEqual(null);
});
it('should use the process function config when prop is a function', () => {
const process = jest.fn(a => a);
const nextFunction = function () {};
expect(
diff(
{
a: function () {},
},
{
a: nextFunction,
},
{a: {process}},
),
).toEqual({a: nextFunction});
expect(process).toBeCalled();
});
});

View File

@@ -7,7 +7,7 @@
* @flow
*/
import type {ReactComponentInfo} from 'shared/ReactTypes';
import type {ReactComponentInfo, ReactAsyncInfo} from 'shared/ReactTypes';
import type {LazyComponent} from 'react/src/ReactLazy';
import {
@@ -37,7 +37,8 @@ export type ComponentStackNode = {
| string
| Function
| LazyComponent<any, any>
| ReactComponentInfo,
| ReactComponentInfo
| ReactAsyncInfo,
owner?: null | ReactComponentInfo | ComponentStackNode, // DEV only
stack?: null | string | Error, // DEV only
};

View File

@@ -21,6 +21,7 @@ import type {
ReactFormState,
ReactComponentInfo,
ReactDebugInfo,
ReactAsyncInfo,
ViewTransitionProps,
ActivityProps,
SuspenseProps,
@@ -181,6 +182,7 @@ import {
enableAsyncIterableChildren,
enableViewTransition,
enableFizzBlockingRender,
enableAsyncDebugInfo,
} from 'shared/ReactFeatureFlags';
import assign from 'shared/assign';
@@ -985,6 +987,45 @@ function getStackFromNode(stackNode: ComponentStackNode): string {
return getStackByComponentStackNode(stackNode);
}
function pushHaltedAwaitOnComponentStack(
task: Task,
debugInfo: void | null | ReactDebugInfo,
): void {
if (!__DEV__) {
// eslint-disable-next-line react-internal/prod-error-codes
throw new Error(
'pushHaltedAwaitOnComponentStack should never be called in production. This is a bug in React.',
);
}
if (debugInfo != null) {
for (let i = debugInfo.length - 1; i >= 0; i--) {
const info = debugInfo[i];
if (typeof info.name === 'string') {
// This is a Server Component. Any awaits in previous Server Components already resolved.
break;
}
if (typeof info.time === 'number') {
// This had an end time. Any awaits before this must have already resolved.
break;
}
if (info.awaited != null) {
const asyncInfo: ReactAsyncInfo = (info: any);
const bestStack =
asyncInfo.debugStack == null ? asyncInfo.awaited : asyncInfo;
if (bestStack.debugStack !== undefined) {
task.componentStack = {
parent: task.componentStack,
type: asyncInfo,
owner: bestStack.owner,
stack: bestStack.debugStack,
};
task.debugTask = (bestStack.debugTask: any);
}
}
}
}
}
function pushServerComponentStack(
task: Task,
debugInfo: void | null | ReactDebugInfo,
@@ -4612,6 +4653,20 @@ function abortTask(task: Task, request: Request, error: mixed): void {
}
const errorInfo = getThrownInfo(task.componentStack);
if (__DEV__ && enableAsyncDebugInfo) {
// If the task is not rendering, then this is an async abort. Conceptually it's as if
// the abort happened inside the async gap. The abort reason's stack frame won't have that
// on the stack so instead we use the owner stack and debug task of any halted async debug info.
const node: any = task.node;
if (node !== null && typeof node === 'object') {
// Push a fake component stack frame that represents the await.
pushHaltedAwaitOnComponentStack(task, node._debugInfo);
if (task.thenableState !== null) {
// TODO: If we were stalled inside use() of a Client Component then we should
// rerender to get the stack trace from the use() call.
}
}
}
if (boundary === null) {
if (request.status !== CLOSING && request.status !== CLOSED) {
@@ -4631,7 +4686,12 @@ function abortTask(task: Task, request: Request, error: mixed): void {
if (trackedPostpones !== null && segment !== null) {
// We are prerendering. We don't want to fatal when the shell postpones
// we just need to mark it as postponed.
logPostpone(request, postponeInstance.message, errorInfo, null);
logPostpone(
request,
postponeInstance.message,
errorInfo,
task.debugTask,
);
trackPostpone(request, trackedPostpones, task, segment);
finishedTask(request, null, task.row, segment);
} else {
@@ -4639,8 +4699,8 @@ function abortTask(task: Task, request: Request, error: mixed): void {
'The render was aborted with postpone when the shell is incomplete. Reason: ' +
postponeInstance.message,
);
logRecoverableError(request, fatal, errorInfo, null);
fatalError(request, fatal, errorInfo, null);
logRecoverableError(request, fatal, errorInfo, task.debugTask);
fatalError(request, fatal, errorInfo, task.debugTask);
}
} else if (
enableHalt &&
@@ -4650,12 +4710,12 @@ function abortTask(task: Task, request: Request, error: mixed): void {
const trackedPostpones = request.trackedPostpones;
// We are aborting a prerender and must treat the shell as halted
// We log the error but we still resolve the prerender
logRecoverableError(request, error, errorInfo, null);
logRecoverableError(request, error, errorInfo, task.debugTask);
trackPostpone(request, trackedPostpones, task, segment);
finishedTask(request, null, task.row, segment);
} else {
logRecoverableError(request, error, errorInfo, null);
fatalError(request, error, errorInfo, null);
logRecoverableError(request, error, errorInfo, task.debugTask);
fatalError(request, error, errorInfo, task.debugTask);
}
return;
} else {
@@ -4672,7 +4732,12 @@ function abortTask(task: Task, request: Request, error: mixed): void {
error.$$typeof === REACT_POSTPONE_TYPE
) {
const postponeInstance: Postpone = (error: any);
logPostpone(request, postponeInstance.message, errorInfo, null);
logPostpone(
request,
postponeInstance.message,
errorInfo,
task.debugTask,
);
// TODO: Figure out a better signal than a magic digest value.
errorDigest = 'POSTPONE';
} else {
@@ -4710,11 +4775,16 @@ function abortTask(task: Task, request: Request, error: mixed): void {
error.$$typeof === REACT_POSTPONE_TYPE
) {
const postponeInstance: Postpone = (error: any);
logPostpone(request, postponeInstance.message, errorInfo, null);
logPostpone(
request,
postponeInstance.message,
errorInfo,
task.debugTask,
);
} else {
// We are aborting a prerender and must halt this boundary.
// We treat this like other postpones during prerendering
logRecoverableError(request, error, errorInfo, null);
logRecoverableError(request, error, errorInfo, task.debugTask);
}
trackPostpone(request, trackedPostpones, task, segment);
// If this boundary was still pending then we haven't already cancelled its fallbacks.
@@ -4737,7 +4807,12 @@ function abortTask(task: Task, request: Request, error: mixed): void {
error.$$typeof === REACT_POSTPONE_TYPE
) {
const postponeInstance: Postpone = (error: any);
logPostpone(request, postponeInstance.message, errorInfo, null);
logPostpone(
request,
postponeInstance.message,
errorInfo,
task.debugTask,
);
if (request.trackedPostpones !== null && segment !== null) {
trackPostpone(request, request.trackedPostpones, task, segment);
finishedTask(request, task.blockedBoundary, task.row, segment);
@@ -4753,7 +4828,12 @@ function abortTask(task: Task, request: Request, error: mixed): void {
// TODO: Figure out a better signal than a magic digest value.
errorDigest = 'POSTPONE';
} else {
errorDigest = logRecoverableError(request, error, errorInfo, null);
errorDigest = logRecoverableError(
request,
error,
errorInfo,
task.debugTask,
);
}
boundary.status = CLIENT_RENDERED;
encodeErrorForBoundary(boundary, errorDigest, error, errorInfo, true);

View File

@@ -433,7 +433,6 @@ export type Request = {
nextChunkId: number,
pendingChunks: number,
hints: Hints,
abortListeners: Set<(reason: mixed) => void>,
abortableTasks: Set<Task>,
pingedTasks: Array<Task>,
completedImportChunks: Array<Chunk>,
@@ -547,7 +546,6 @@ function RequestInstance(
this.nextChunkId = 0;
this.pendingChunks = 0;
this.hints = hints;
this.abortListeners = new Set();
this.abortableTasks = abortSet;
this.pingedTasks = pingedTasks;
this.completedImportChunks = ([]: Array<Chunk>);
@@ -839,13 +837,11 @@ function serializeThenable(
if (request.status === ABORTING) {
// We can no longer accept any resolved values
request.abortableTasks.delete(newTask);
newTask.status = ABORTED;
if (enableHalt && request.type === PRERENDER) {
request.pendingChunks--;
haltTask(newTask, request);
} else {
const errorId: number = (request.fatalError: any);
const model = stringify(serializeByValueID(errorId));
emitModelChunk(request, newTask.id, model);
abortTask(newTask, request, errorId);
}
return newTask.id;
}
@@ -936,29 +932,26 @@ function serializeReadableStream(
__DEV__ ? task.debugStack : null,
__DEV__ ? task.debugTask : null,
);
request.abortableTasks.delete(streamTask);
request.pendingChunks++; // The task represents the Start row. This adds a Stop row.
// The task represents the Stop row. This adds a Start row.
request.pendingChunks++;
const startStreamRow =
streamTask.id.toString(16) + ':' + (supportsBYOB ? 'r' : 'R') + '\n';
request.completedRegularChunks.push(stringToChunk(startStreamRow));
// There's a race condition between when the stream is aborted and when the promise
// resolves so we track whether we already aborted it to avoid writing twice.
let aborted = false;
function progress(entry: {done: boolean, value: ReactClientValue, ...}) {
if (aborted) {
if (streamTask.status !== PENDING) {
return;
}
if (entry.done) {
streamTask.status = COMPLETED;
const endStreamRow = streamTask.id.toString(16) + ':C\n';
request.completedRegularChunks.push(stringToChunk(endStreamRow));
request.abortableTasks.delete(streamTask);
request.cacheController.signal.removeEventListener('abort', abortStream);
enqueueFlush(request);
request.abortListeners.delete(abortStream);
callOnAllReadyIfReady(request);
aborted = true;
} else {
try {
streamTask.model = entry.value;
@@ -972,26 +965,28 @@ function serializeReadableStream(
}
}
function error(reason: mixed) {
if (aborted) {
if (streamTask.status !== PENDING) {
return;
}
aborted = true;
request.abortListeners.delete(abortStream);
request.cacheController.signal.removeEventListener('abort', abortStream);
erroredTask(request, streamTask, reason);
enqueueFlush(request);
// $FlowFixMe should be able to pass mixed
reader.cancel(reason).then(error, error);
}
function abortStream(reason: mixed) {
if (aborted) {
function abortStream() {
if (streamTask.status !== PENDING) {
return;
}
aborted = true;
request.abortListeners.delete(abortStream);
const signal = request.cacheController.signal;
signal.removeEventListener('abort', abortStream);
const reason = signal.reason;
if (enableHalt && request.type === PRERENDER) {
request.pendingChunks--;
haltTask(streamTask, request);
request.abortableTasks.delete(streamTask);
} else {
// TODO: Make this use abortTask() instead.
erroredTask(request, streamTask, reason);
enqueueFlush(request);
}
@@ -999,7 +994,7 @@ function serializeReadableStream(
reader.cancel(reason).then(error, error);
}
request.abortListeners.add(abortStream);
request.cacheController.signal.addEventListener('abort', abortStream);
reader.read().then(progress, error);
return serializeByValueID(streamTask.id);
}
@@ -1028,10 +1023,9 @@ function serializeAsyncIterable(
__DEV__ ? task.debugStack : null,
__DEV__ ? task.debugTask : null,
);
request.abortableTasks.delete(streamTask);
request.pendingChunks++; // The task represents the Start row. This adds a Stop row.
// The task represents the Stop row. This adds a Start row.
request.pendingChunks++;
const startStreamRow =
streamTask.id.toString(16) + ':' + (isIterator ? 'x' : 'X') + '\n';
request.completedRegularChunks.push(stringToChunk(startStreamRow));
@@ -1043,19 +1037,17 @@ function serializeAsyncIterable(
}
}
// There's a race condition between when the stream is aborted and when the promise
// resolves so we track whether we already aborted it to avoid writing twice.
let aborted = false;
function progress(
entry:
| {done: false, +value: ReactClientValue, ...}
| {done: true, +value: ReactClientValue, ...},
) {
if (aborted) {
if (streamTask.status !== PENDING) {
return;
}
if (entry.done) {
streamTask.status = COMPLETED;
let endStreamRow;
if (entry.value === undefined) {
endStreamRow = streamTask.id.toString(16) + ':C\n';
@@ -1075,10 +1067,13 @@ function serializeAsyncIterable(
}
}
request.completedRegularChunks.push(stringToChunk(endStreamRow));
request.abortableTasks.delete(streamTask);
request.cacheController.signal.removeEventListener(
'abort',
abortIterable,
);
enqueueFlush(request);
request.abortListeners.delete(abortIterable);
callOnAllReadyIfReady(request);
aborted = true;
} else {
try {
streamTask.model = entry.value;
@@ -1097,11 +1092,10 @@ function serializeAsyncIterable(
}
}
function error(reason: mixed) {
if (aborted) {
if (streamTask.status !== PENDING) {
return;
}
aborted = true;
request.abortListeners.delete(abortIterable);
request.cacheController.signal.removeEventListener('abort', abortIterable);
erroredTask(request, streamTask, reason);
enqueueFlush(request);
if (typeof (iterator: any).throw === 'function') {
@@ -1110,16 +1104,19 @@ function serializeAsyncIterable(
iterator.throw(reason).then(error, error);
}
}
function abortIterable(reason: mixed) {
if (aborted) {
function abortIterable() {
if (streamTask.status !== PENDING) {
return;
}
aborted = true;
request.abortListeners.delete(abortIterable);
const signal = request.cacheController.signal;
signal.removeEventListener('abort', abortIterable);
const reason = signal.reason;
if (enableHalt && request.type === PRERENDER) {
request.pendingChunks--;
haltTask(streamTask, request);
request.abortableTasks.delete(streamTask);
} else {
erroredTask(request, streamTask, reason);
// TODO: Make this use abortTask() instead.
erroredTask(request, streamTask, signal.reason);
enqueueFlush(request);
}
if (typeof (iterator: any).throw === 'function') {
@@ -1128,7 +1125,7 @@ function serializeAsyncIterable(
iterator.throw(reason).then(error, error);
}
}
request.abortListeners.add(abortIterable);
request.cacheController.signal.addEventListener('abort', abortIterable);
if (__DEV__) {
callIteratorInDEV(iterator, progress, error);
} else {
@@ -2152,7 +2149,11 @@ function visitAsyncNode(
owner: node.owner,
stack: filterStackTrace(request, node.stack),
});
markOperationEndTime(request, task, endTime);
// Mark the end time of the await. If we're aborting then we don't emit this
// to signal that this never resolved inside this render.
if (request.status !== ABORTING) {
markOperationEndTime(request, task, endTime);
}
}
}
}
@@ -2213,7 +2214,12 @@ function emitAsyncSequence(
}
}
emitDebugChunk(request, task.id, debugInfo);
markOperationEndTime(request, task, awaitedNode.end);
// Mark the end time of the await. If we're aborting then we don't emit this
// to signal that this never resolved inside this render.
if (request.status !== ABORTING) {
// If we're currently aborting, then this never resolved into user space.
markOperationEndTime(request, task, awaitedNode.end);
}
}
}
@@ -2675,16 +2681,14 @@ function serializeBlob(request: Request, blob: Blob): string {
const reader = blob.stream().getReader();
let aborted = false;
function progress(
entry: {done: false, value: Uint8Array} | {done: true, value: void},
): Promise<void> | void {
if (aborted) {
if (newTask.status !== PENDING) {
return;
}
if (entry.done) {
request.abortListeners.delete(abortBlob);
aborted = true;
request.cacheController.signal.removeEventListener('abort', abortBlob);
pingTask(request, newTask);
return;
}
@@ -2694,25 +2698,26 @@ function serializeBlob(request: Request, blob: Blob): string {
return reader.read().then(progress).catch(error);
}
function error(reason: mixed) {
if (aborted) {
if (newTask.status !== PENDING) {
return;
}
aborted = true;
request.abortListeners.delete(abortBlob);
request.cacheController.signal.removeEventListener('abort', abortBlob);
erroredTask(request, newTask, reason);
enqueueFlush(request);
// $FlowFixMe should be able to pass mixed
reader.cancel(reason).then(error, error);
}
function abortBlob(reason: mixed) {
if (aborted) {
function abortBlob() {
if (newTask.status !== PENDING) {
return;
}
aborted = true;
request.abortListeners.delete(abortBlob);
const signal = request.cacheController.signal;
signal.removeEventListener('abort', abortBlob);
const reason = signal.reason;
if (enableHalt && request.type === PRERENDER) {
request.pendingChunks--;
haltTask(newTask, request);
} else {
// TODO: Make this use abortTask() instead.
erroredTask(request, newTask, reason);
enqueueFlush(request);
}
@@ -2720,7 +2725,7 @@ function serializeBlob(request: Request, blob: Blob): string {
reader.cancel(reason).then(error, error);
}
request.abortListeners.add(abortBlob);
request.cacheController.signal.addEventListener('abort', abortBlob);
// $FlowFixMe[incompatible-call]
reader.read().then(progress).catch(error);
@@ -3914,6 +3919,13 @@ function serializeIONode(
// The environment name may have changed from when the I/O was actually started.
const env = (0, request.environmentName)();
const endTime =
ioNode.tag === UNRESOLVED_PROMISE_NODE
? // Mark the end time as now. It's arbitrary since it's not resolved but this
// marks when we stopped trying.
performance.now()
: ioNode.end;
request.pendingChunks++;
const id = request.nextChunkId++;
emitIOInfoChunk(
@@ -3921,7 +3933,7 @@ function serializeIONode(
id,
name,
ioNode.start,
ioNode.end,
endTime,
value,
env,
owner,
@@ -4745,7 +4757,6 @@ function forwardDebugInfoFromAbortedTask(request: Request, task: Task): void {
env: env,
};
emitDebugChunk(request, task.id, asyncInfo);
markOperationEndTime(request, task, performance.now());
} else {
emitAsyncSequence(request, task, sequence, debugInfo, null, null);
}
@@ -5005,16 +5016,15 @@ function retryTask(request: Request, task: Task): void {
} catch (thrownValue) {
if (request.status === ABORTING) {
request.abortableTasks.delete(task);
task.status = ABORTED;
task.status = PENDING;
if (enableHalt && request.type === PRERENDER) {
// When aborting a prerener with halt semantics we don't emit
// anything into the slot for a task that aborts, it remains unresolved
request.pendingChunks--;
haltTask(task, request);
} else {
// Otherwise we emit an error chunk into the task slot.
const errorId: number = (request.fatalError: any);
const model = stringify(serializeByValueID(errorId));
emitModelChunk(request, task.id, model);
abortTask(task, request, errorId);
}
return;
}
@@ -5257,8 +5267,9 @@ function enqueueFlush(request: Request): void {
}
function callOnAllReadyIfReady(request: Request): void {
if (request.abortableTasks.size === 0 && request.abortListeners.size === 0) {
request.onAllReady();
if (request.abortableTasks.size === 0) {
const onAllReady = request.onAllReady;
onAllReady();
}
}
@@ -5294,6 +5305,7 @@ export function abort(request: Request, reason: mixed): void {
if (request.status <= OPEN) {
request.status = ABORTING;
request.cacheController.abort(reason);
callOnAllReadyIfReady(request);
}
const abortableTasks = request.abortableTasks;
if (abortableTasks.size > 0) {
@@ -5345,37 +5357,6 @@ export function abort(request: Request, reason: mixed): void {
callOnAllReadyIfReady(request);
}
}
const abortListeners = request.abortListeners;
if (abortListeners.size > 0) {
let error;
if (
enablePostpone &&
typeof reason === 'object' &&
reason !== null &&
(reason: any).$$typeof === REACT_POSTPONE_TYPE
) {
// We aborted with a Postpone but since we're passing this to an
// external handler, passing this object would leak it outside React.
// We create an alternative reason for it instead.
error = new Error('The render was aborted due to being postponed.');
} else {
error =
reason === undefined
? new Error(
'The render was aborted by the server without a reason.',
)
: typeof reason === 'object' &&
reason !== null &&
typeof reason.then === 'function'
? new Error(
'The render was aborted by the server with a promise.',
)
: reason;
}
abortListeners.forEach(callback => callback(error));
abortListeners.clear();
callOnAllReadyIfReady(request);
}
if (request.destination !== null) {
flushCompletedChunks(request, request.destination);
}

View File

@@ -32,6 +32,7 @@ type __MeasureLayoutOnSuccessCallback = (
type __ReactNativeBaseComponentViewConfig = any;
type __ViewConfigGetter = any;
type __ViewConfig = any;
type __AttributeConfiguration = any;
// libdefs cannot actually import. This is supposed to be the type imported
// from 'react-native-renderer/src/legacy-events/TopLevelEventTypes';
@@ -203,6 +204,15 @@ declare module 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface'
declare export function getInternalInstanceHandleFromPublicInstance(
publicInstance: PublicInstance,
): ?Object;
declare export function createAttributePayload(
props: Object,
validAttributes: __AttributeConfiguration,
): null | Object;
declare export function diffAttributePayloads(
prevProps: Object,
nextProps: Object,
validAttributes: __AttributeConfiguration,
): null | Object;
}
declare module 'react-native/Libraries/ReactPrivate/ReactNativePrivateInitializeCore' {