Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,8 @@ module.exports = {
CopyInspectedElementPath: 'readonly',
DOMHighResTimeStamp: 'readonly',
EventListener: 'readonly',
// Flow type
FormDataEntryValue: 'readonly',
Iterable: 'readonly',
AsyncIterable: 'readonly',
$AsyncIterable: 'readonly',
Expand Down
4 changes: 2 additions & 2 deletions flow-typed/environments/bom.js
Original file line number Diff line number Diff line change
Expand Up @@ -682,11 +682,11 @@ declare class FormData {
get(name: string): ?FormDataEntryValue;
getAll(name: string): Array<FormDataEntryValue>;

set(name: string, value: string): void;
set(name: string, value: FormDataEntryValue): void;
set(name: string, value: Blob, filename?: string): void;
set(name: string, value: File, filename?: string): void;

append(name: string, value: string): void;
append(name: string, value: FormDataEntryValue): void;
append(name: string, value: Blob, filename?: string): void;
append(name: string, value: File, filename?: string): void;

Expand Down
4 changes: 3 additions & 1 deletion packages/react-client/src/ReactFlightReplyClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -598,7 +598,9 @@ export function processReply(
// Copy all the form fields with a prefix for this reference.
// These must come first in the form order because we assume that all the
// fields are available before this is referenced.
const prefix = formFieldPrefix + refId + '_';
// We include a special marker so that the Server can detect FormData entries
// that are values in referenced FormData objects.
const prefix = formFieldPrefix + '_' + refId + '_';
// $FlowFixMe[prop-missing]: FormData has forEach.
value.forEach((originalValue: string | File, originalKey: string) => {
// $FlowFixMe[incompatible-call]
Expand Down
107 changes: 107 additions & 0 deletions packages/react-server/src/ReactFlightReplyBackingFormData.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/**
* 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
*/

/**
* Backing FormData is a wrapper around FormData that allows iterating over the
* keys while allowing to evict values from the FormData without affecting the iteration.
* Native FormData.keys() will skip keys if entries with Blob are deleted e.g.
* ```js
* const formData = new FormData();
* formData.append('a', new Blob());
* formData.append('b', 2);
* const keys = formData.keys();
* keys.next().value; // 'a'
* formData.delete('a');
* keys.next().value; // undefined, but we expect 'b'
* ```
*/
export opaque type BackingFormData = {
data: FormData,
keyPointer: number,
// Lazily initialized array of keys. We only need this at the moment
// for referenced FormData.
keys: null | Array<string>,
};

export function peekBackingEntry(backingStore: BackingFormData): string | void {
let keys = backingStore.keys;
if (keys === null) {
keys = backingStore.keys = Array.from(backingStore.data.keys());
backingStore.keyPointer = 0;
}

return keys[backingStore.keyPointer];
}

export function advanceBackingEntryIterator(
backingStore: BackingFormData,
): void {
backingStore.keyPointer++;
}

export function consumeBackingEntry(
backingStore: BackingFormData,
key: string,
): void {
backingStore.data.delete(key);
backingStore.keyPointer++;
}

export function appendBackingEntry(
backingStore: BackingFormData,
key: string,
value: FormDataEntryValue,
): void {
backingStore.data.append(key, value);
let keys = backingStore.keys;
if (keys === null) {
keys = backingStore.keys = Array.from(backingStore.data.keys());
backingStore.keyPointer = 0;
} else {
keys.push(key);
}
}

export function appendBackingFile(
backingStore: BackingFormData,
key: string,
value: Blob,
filename: string,
): void {
backingStore.data.append(key, value, filename);
let keys = backingStore.keys;
if (keys === null) {
keys = backingStore.keys = Array.from(backingStore.data.keys());
backingStore.keyPointer = 0;
} else {
keys.push(key);
}
}

export function getBackingEntry(
backingStore: BackingFormData,
key: string,
): ?FormDataEntryValue {
return backingStore.data.get(key);
}

export function getAllBackingEntries(
backingStore: BackingFormData,
key: string,
): Array<FormDataEntryValue> {
return backingStore.data.getAll(key);
}

export function createBackingFormData(formData: FormData): BackingFormData {
return {
data: formData,
keyPointer: -1,
keys: null,
};
}
Loading
Loading