Skip to content

Commit 0eecce3

Browse files
committed
[Flight] Encode references to existing objects by property path (#28996)
Instead of forcing an object to be outlined to be able to refer to it later we can refer to it by the property path inside another parent object. E.g. this encodes such a reference as `'$123:props:children:foo:bar'`. That way we don't have to preemptively outline object and we can dedupe after the first time we've found it. There's no cost on the client if it's not used because we're not storing any additional information preemptively. This works mainly because we only have simple JSON objects from the root reference. Complex objects like Map, FormData etc. are stored as their entries array in the look up and not the complex object. Other complex objects like TypedArrays or imports don't have deeply nested objects in them that can be referenced. This solves the problem that we only dedupe after the third instance. This dedupes at the second instance. It also solves the problem where all nested objects inside deduped instances also are outlined. The property paths can get pretty large. This is why a test on payload size increased. We could potentially outline the reference itself at the first dupe. That way we get a shorter ID to refer to in the third instance. DiffTrain build for [7a78d03](7a78d03)
1 parent 732aef7 commit 0eecce3

15 files changed

+1736
-1358
lines changed

compiled/facebook-www/REVISION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
151cce37401dc2ff609701119d61a17d92fce4ab
1+
7a78d030281f2f0e02e7cdbed5c1fc5c4a1a1823

compiled/facebook-www/ReactDOM-dev.classic.js

Lines changed: 105 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -2461,36 +2461,36 @@ function lanesToEventPriority(lanes) {
24612461
return IdleEventPriority;
24622462
}
24632463

2464-
function noop$3() {}
2464+
function noop$4() {}
24652465

24662466
var DefaultDispatcher = {
24672467
f
24682468
/* flushSyncWork */
2469-
: noop$3,
2469+
: noop$4,
24702470
r
24712471
/* requestFormReset */
2472-
: noop$3,
2472+
: noop$4,
24732473
D
24742474
/* prefetchDNS */
2475-
: noop$3,
2475+
: noop$4,
24762476
C
24772477
/* preconnect */
2478-
: noop$3,
2478+
: noop$4,
24792479
L
24802480
/* preload */
2481-
: noop$3,
2481+
: noop$4,
24822482
m
24832483
/* preloadModule */
2484-
: noop$3,
2484+
: noop$4,
24852485
X
24862486
/* preinitScript */
2487-
: noop$3,
2487+
: noop$4,
24882488
S
24892489
/* preinitStyle */
2490-
: noop$3,
2490+
: noop$4,
24912491
M
24922492
/* preinitModuleScript */
2493-
: noop$3
2493+
: noop$4
24942494
};
24952495
var Internals = {
24962496
Events: null,
@@ -8568,6 +8568,9 @@ transition) {
85688568

85698569
return currentEventTransitionLane;
85708570
}
8571+
function didCurrentEventScheduleTransition() {
8572+
return currentEventTransitionLane !== NoLane;
8573+
}
85718574

85728575
// transition updates that occur while the async action is still in progress
85738576
// are treated as part of the action.
@@ -9570,7 +9573,7 @@ function isThenableResolved(thenable) {
95709573
return status === 'fulfilled' || status === 'rejected';
95719574
}
95729575

9573-
function noop$2() {}
9576+
function noop$3() {}
95749577

95759578
function trackUsedThenable(thenableState, thenable, index) {
95769579
if (ReactSharedInternals.actQueue !== null) {
@@ -9613,7 +9616,7 @@ function trackUsedThenable(thenableState, thenable, index) {
96139616
// intentionally ignore.
96149617

96159618

9616-
thenable.then(noop$2, noop$2);
9619+
thenable.then(noop$3, noop$3);
96179620
thenable = previous;
96189621
}
96199622
} // We use an expando to track the status and result of a thenable so that we
@@ -9646,7 +9649,7 @@ function trackUsedThenable(thenableState, thenable, index) {
96469649
// some custom userspace implementation. We treat it as "pending".
96479650
// Attach a dummy listener, to ensure that any lazy initialization can
96489651
// happen. Flight lazily parses JSON when the value is actually awaited.
9649-
thenable.then(noop$2, noop$2);
9652+
thenable.then(noop$3, noop$3);
96509653
} else {
96519654
// This is an uncached thenable that we haven't seen before.
96529655
// Detect infinite ping loops caused by uncached promises.
@@ -13481,20 +13484,25 @@ function startTransition(fiber, queue, pendingState, finishedState, callback, op
1348113484
}
1348213485
}
1348313486

13484-
function startHostTransition(formFiber, pendingState, callback, formData) {
13487+
var noop$2 = function () {};
13488+
13489+
function startHostTransition(formFiber, pendingState, action, formData) {
1348513490

1348613491
if (formFiber.tag !== HostComponent) {
1348713492
throw new Error('Expected the form instance to be a HostComponent. This ' + 'is a bug in React.');
1348813493
}
1348913494

1349013495
var stateHook = ensureFormComponentIsStateful(formFiber);
1349113496
var queue = stateHook.queue;
13492-
startTransition(formFiber, queue, pendingState, NotPendingTransition, // TODO: We can avoid this extra wrapper, somehow. Figure out layering
13493-
// once more of this function is implemented.
13494-
function () {
13497+
startTransition(formFiber, queue, pendingState, NotPendingTransition, // TODO: `startTransition` both sets the pending state and dispatches
13498+
// the action, if one is provided. Consider refactoring these two
13499+
// concerns to avoid the extra lambda.
13500+
action === null ? // No action was provided, but we still call `startTransition` to
13501+
// set the pending form status.
13502+
noop$2 : function () {
1349513503
// Automatically reset the form when the action completes.
1349613504
requestFormReset$1(formFiber);
13497-
return callback(formData);
13505+
return action(formData);
1349813506
});
1349913507
}
1350013508

@@ -30838,7 +30846,7 @@ identifierPrefix, onUncaughtError, onCaughtError, onRecoverableError, transition
3083830846
return root;
3083930847
}
3084030848

30841-
var ReactVersion = '19.0.0-www-classic-f43a9c2f';
30849+
var ReactVersion = '19.0.0-www-classic-54329650';
3084230850

3084330851
function createPortal$1(children, containerInfo, // TODO: figure out the API for cross-renderer implementation.
3084430852
implementation) {
@@ -33892,11 +33900,49 @@ function extractEvents$2(dispatchQueue, domEventName, targetInst, nativeEvent, n
3389233900
}
3389333901
}
3389433902

33903+
function coerceFormActionProp(actionProp) {
33904+
// This should match the logic in ReactDOMComponent
33905+
if (actionProp == null || typeof actionProp === 'symbol' || typeof actionProp === 'boolean') {
33906+
return null;
33907+
} else if (typeof actionProp === 'function') {
33908+
return actionProp;
33909+
} else {
33910+
{
33911+
checkAttributeStringCoercion(actionProp, 'action');
33912+
}
33913+
33914+
return sanitizeURL(enableTrustedTypesIntegration ? actionProp : '' + actionProp);
33915+
}
33916+
}
33917+
33918+
function createFormDataWithSubmitter(form, submitter) {
33919+
// The submitter's value should be included in the FormData.
33920+
// It should be in the document order in the form.
33921+
// Since the FormData constructor invokes the formdata event it also
33922+
// needs to be available before that happens so after construction it's too
33923+
// late. We use a temporary fake node for the duration of this event.
33924+
// TODO: FormData takes a second argument that it's the submitter but this
33925+
// is fairly new so not all browsers support it yet. Switch to that technique
33926+
// when available.
33927+
var temp = submitter.ownerDocument.createElement('input');
33928+
temp.name = submitter.name;
33929+
temp.value = submitter.value;
33930+
33931+
if (form.id) {
33932+
temp.setAttribute('form', form.id);
33933+
}
33934+
33935+
submitter.parentNode.insertBefore(temp, submitter);
33936+
var formData = new FormData(form);
33937+
temp.parentNode.removeChild(temp);
33938+
return formData;
33939+
}
3389533940
/**
3389633941
* This plugin invokes action functions on forms, inputs and buttons if
3389733942
* the form doesn't prevent default.
3389833943
*/
3389933944

33945+
3390033946
function extractEvents$1(dispatchQueue, domEventName, maybeTargetInst, nativeEvent, nativeEventTarget, eventSystemFlags, targetContainer) {
3390133947
if (domEventName !== 'submit') {
3390233948
return;
@@ -33910,15 +33956,16 @@ function extractEvents$1(dispatchQueue, domEventName, maybeTargetInst, nativeEve
3391033956

3391133957
var formInst = maybeTargetInst;
3391233958
var form = nativeEventTarget;
33913-
var action = getFiberCurrentPropsFromNode(form).action;
33959+
var action = coerceFormActionProp(getFiberCurrentPropsFromNode(form).action);
3391433960
var submitter = nativeEvent.submitter;
3391533961
var submitterAction;
3391633962

3391733963
if (submitter) {
3391833964
var submitterProps = getFiberCurrentPropsFromNode(submitter);
33919-
submitterAction = submitterProps ? submitterProps.formAction : submitter.getAttribute('formAction');
33965+
submitterAction = submitterProps ? coerceFormActionProp(submitterProps.formAction) : // The built-in Flow type is ?string, wider than the spec
33966+
submitter.getAttribute('formAction');
3392033967

33921-
if (submitterAction != null) {
33968+
if (submitterAction !== null) {
3392233969
// The submitter overrides the form action.
3392333970
action = submitterAction; // If the action is a function, we don't want to pass its name
3392433971
// value to the FormData since it's controlled by the server.
@@ -33927,58 +33974,53 @@ function extractEvents$1(dispatchQueue, domEventName, maybeTargetInst, nativeEve
3392733974
}
3392833975
}
3392933976

33930-
if (typeof action !== 'function') {
33931-
return;
33932-
}
33933-
3393433977
var event = new SyntheticEvent('action', 'action', null, nativeEvent, nativeEventTarget);
3393533978

3393633979
function submitForm() {
3393733980
if (nativeEvent.defaultPrevented) {
33938-
// We let earlier events to prevent the action from submitting.
33939-
return;
33940-
} // Prevent native navigation.
33941-
33942-
33943-
event.preventDefault();
33944-
var formData;
33981+
// An earlier event prevented form submission. If a transition update was
33982+
// also scheduled, we should trigger a pending form status — even if
33983+
// no action function was provided.
33984+
if (didCurrentEventScheduleTransition()) {
33985+
// We're going to set the pending form status, but because the submission
33986+
// was prevented, we should not fire the action function.
33987+
var formData = submitter ? createFormDataWithSubmitter(form, submitter) : new FormData(form);
33988+
var pendingState = {
33989+
pending: true,
33990+
data: formData,
33991+
method: form.method,
33992+
action: action
33993+
};
3394533994

33946-
if (submitter) {
33947-
// The submitter's value should be included in the FormData.
33948-
// It should be in the document order in the form.
33949-
// Since the FormData constructor invokes the formdata event it also
33950-
// needs to be available before that happens so after construction it's too
33951-
// late. We use a temporary fake node for the duration of this event.
33952-
// TODO: FormData takes a second argument that it's the submitter but this
33953-
// is fairly new so not all browsers support it yet. Switch to that technique
33954-
// when available.
33955-
var temp = submitter.ownerDocument.createElement('input');
33956-
temp.name = submitter.name;
33957-
temp.value = submitter.value;
33995+
{
33996+
Object.freeze(pendingState);
33997+
}
3395833998

33959-
if (form.id) {
33960-
temp.setAttribute('form', form.id);
33999+
startHostTransition(formInst, pendingState, // Pass `null` as the action
34000+
// TODO: Consider splitting up startHostTransition into two separate
34001+
// functions, one that sets the form status and one that invokes
34002+
// the action.
34003+
null, formData);
3396134004
}
34005+
} else if (typeof action === 'function') {
34006+
// A form action was provided. Prevent native navigation.
34007+
event.preventDefault(); // Dispatch the action and set a pending form status.
3396234008

33963-
submitter.parentNode.insertBefore(temp, submitter);
33964-
formData = new FormData(form);
33965-
temp.parentNode.removeChild(temp);
33966-
} else {
33967-
formData = new FormData(form);
33968-
}
34009+
var _formData = submitter ? createFormDataWithSubmitter(form, submitter) : new FormData(form);
3396934010

33970-
var pendingState = {
33971-
pending: true,
33972-
data: formData,
33973-
method: form.method,
33974-
action: action
33975-
};
34011+
var _pendingState = {
34012+
pending: true,
34013+
data: _formData,
34014+
method: form.method,
34015+
action: action
34016+
};
3397634017

33977-
{
33978-
Object.freeze(pendingState);
33979-
}
34018+
{
34019+
Object.freeze(_pendingState);
34020+
}
3398034021

33981-
startHostTransition(formInst, pendingState, action, formData);
34022+
startHostTransition(formInst, _pendingState, action, _formData);
34023+
} else ;
3398234024
}
3398334025

3398434026
dispatchQueue.push({

0 commit comments

Comments
 (0)