Skip to content

Commit

Permalink
[Flight Reply] Encode binary streams as a single collapsed Blob (#28986)
Browse files Browse the repository at this point in the history
Based on #28893.

For other streams we encode each chunk as a separate form field which is
a bit bloated. Especially for binary chunks since they also have an
indirection. We need some way to encode the chunks as separate anyway.
This way the streaming using busboy actually allows each chunk to stream
in over the network one at a time.

For binary streams the actual chunking is not important. The chunks can
be split and recombined in whatever size chunk makes sense.

Since we buffer the entire content anyway we can combine the chunks to
be consecutive. This PR does that with binary streams and also combine
them into a single Blob. That way there's no extra overhead when passing
through a binary stream.

Ideally, we'd be able to just use the stream from that one Blob but
Node.js doesn't return byob streams from Blob. Additionally, we don't
actually stream the content of Blobs due to the layering with busboy
atm. We could do that for binary streams in particular by replacing the
File layering with a stream and resolving each chunk as it comes in.
That could be a follow up.

If we stop buffering in the future, this set up still allows us to split
them and send other form fields in between while blocked since the
protocol is still the same.

DiffTrain build for [826bf4e](826bf4e)
  • Loading branch information
sebmarkbage committed May 8, 2024
1 parent 86893d4 commit e83564a
Show file tree
Hide file tree
Showing 7 changed files with 72 additions and 147 deletions.
2 changes: 1 addition & 1 deletion compiled/facebook-www/REVISION
@@ -1 +1 @@
6bac4f2f31378cd58dffe6181e00639366a6081a
826bf4e51ecf14904e936ed043392084553ebbaa
31 changes: 7 additions & 24 deletions compiled/facebook-www/ReactDOMServer-dev.classic.js
Expand Up @@ -19,7 +19,7 @@ if (__DEV__) {
var React = require('react');
var ReactDOM = require('react-dom');

var ReactVersion = '19.0.0-www-classic-b547cfd9';
var ReactVersion = '19.0.0-www-classic-f3d84129';

// This refers to a WWW module.
var warningWWW = require('warning');
Expand Down Expand Up @@ -2572,7 +2572,11 @@ var startHiddenInputChunk = stringToPrecomputedChunk('<input type="hidden"');
function pushAdditionalFormField(value, key) {
var target = this;
target.push(startHiddenInputChunk);
validateAdditionalFormField(value);

if (typeof value !== 'string') {
throw new Error('File/Blob fields are not yet supported in progressive forms. ' + 'It probably means you are closing over binary data or FormData in a Server Action.');
}

pushStringAttribute(target, 'name', key);
pushStringAttribute(target, 'value', value);
target.push(endOfStartTagSelfClosing);
Expand All @@ -2585,35 +2589,14 @@ function pushAdditionalFormFields(target, formData) {
}
}

function validateAdditionalFormField(value, key) {
if (typeof value !== 'string') {
throw new Error('File/Blob fields are not yet supported in progressive forms. ' + 'Will fallback to client hydration.');
}
}

function validateAdditionalFormFields(formData) {
if (formData != null) {
// $FlowFixMe[prop-missing]: FormData has forEach.
formData.forEach(validateAdditionalFormField);
}

return formData;
}

function getCustomFormFields(resumableState, formAction) {
var customAction = formAction.$$FORM_ACTION;

if (typeof customAction === 'function') {
var prefix = makeFormFieldPrefix(resumableState);

try {
var customFields = formAction.$$FORM_ACTION(prefix);

if (customFields) {
validateAdditionalFormFields(customFields.data);
}

return customFields;
return formAction.$$FORM_ACTION(prefix);
} catch (x) {
if (typeof x === 'object' && x !== null && typeof x.then === 'function') {
// Rethrow suspense.
Expand Down
31 changes: 7 additions & 24 deletions compiled/facebook-www/ReactDOMServer-dev.modern.js
Expand Up @@ -19,7 +19,7 @@ if (__DEV__) {
var React = require('react');
var ReactDOM = require('react-dom');

var ReactVersion = '19.0.0-www-modern-ce28692a';
var ReactVersion = '19.0.0-www-modern-f9bdfca0';

// This refers to a WWW module.
var warningWWW = require('warning');
Expand Down Expand Up @@ -2572,7 +2572,11 @@ var startHiddenInputChunk = stringToPrecomputedChunk('<input type="hidden"');
function pushAdditionalFormField(value, key) {
var target = this;
target.push(startHiddenInputChunk);
validateAdditionalFormField(value);

if (typeof value !== 'string') {
throw new Error('File/Blob fields are not yet supported in progressive forms. ' + 'It probably means you are closing over binary data or FormData in a Server Action.');
}

pushStringAttribute(target, 'name', key);
pushStringAttribute(target, 'value', value);
target.push(endOfStartTagSelfClosing);
Expand All @@ -2585,35 +2589,14 @@ function pushAdditionalFormFields(target, formData) {
}
}

function validateAdditionalFormField(value, key) {
if (typeof value !== 'string') {
throw new Error('File/Blob fields are not yet supported in progressive forms. ' + 'Will fallback to client hydration.');
}
}

function validateAdditionalFormFields(formData) {
if (formData != null) {
// $FlowFixMe[prop-missing]: FormData has forEach.
formData.forEach(validateAdditionalFormField);
}

return formData;
}

function getCustomFormFields(resumableState, formAction) {
var customAction = formAction.$$FORM_ACTION;

if (typeof customAction === 'function') {
var prefix = makeFormFieldPrefix(resumableState);

try {
var customFields = formAction.$$FORM_ACTION(prefix);

if (customFields) {
validateAdditionalFormFields(customFields.data);
}

return customFields;
return formAction.$$FORM_ACTION(prefix);
} catch (x) {
if (typeof x === 'object' && x !== null && typeof x.then === 'function') {
// Rethrow suspense.
Expand Down
54 changes: 23 additions & 31 deletions compiled/facebook-www/ReactDOMServer-prod.classic.js
Expand Up @@ -445,25 +445,17 @@ var actionJavaScriptURL = escapeTextForBrowser(
);
function pushAdditionalFormField(value, key) {
this.push('<input type="hidden"');
validateAdditionalFormField(value);
if ("string" !== typeof value) throw Error(formatProdErrorMessage(480));
pushStringAttribute(this, "name", key);
pushStringAttribute(this, "value", value);
this.push("/>");
}
function validateAdditionalFormField(value) {
if ("string" !== typeof value) throw Error(formatProdErrorMessage(480));
}
function getCustomFormFields(resumableState, formAction) {
if ("function" === typeof formAction.$$FORM_ACTION) {
var id = resumableState.nextFormID++;
resumableState = resumableState.idPrefix + id;
try {
var customFields = formAction.$$FORM_ACTION(resumableState);
if (customFields) {
var formData = customFields.data;
null != formData && formData.forEach(validateAdditionalFormField);
}
return customFields;
return formAction.$$FORM_ACTION(resumableState);
} catch (x) {
if ("object" === typeof x && null !== x && "function" === typeof x.then)
throw x;
Expand Down Expand Up @@ -2635,16 +2627,16 @@ function createRenderState(resumableState, generateStaticMarkup) {
"\x3c/script>"
);
bootstrapScriptContent = idPrefix + "P:";
var JSCompiler_object_inline_segmentPrefix_1633 = idPrefix + "S:";
var JSCompiler_object_inline_segmentPrefix_1631 = idPrefix + "S:";
idPrefix += "B:";
var JSCompiler_object_inline_preconnects_1647 = new Set(),
JSCompiler_object_inline_fontPreloads_1648 = new Set(),
JSCompiler_object_inline_highImagePreloads_1649 = new Set(),
JSCompiler_object_inline_styles_1650 = new Map(),
JSCompiler_object_inline_bootstrapScripts_1651 = new Set(),
JSCompiler_object_inline_scripts_1652 = new Set(),
JSCompiler_object_inline_bulkPreloads_1653 = new Set(),
JSCompiler_object_inline_preloads_1654 = {
var JSCompiler_object_inline_preconnects_1645 = new Set(),
JSCompiler_object_inline_fontPreloads_1646 = new Set(),
JSCompiler_object_inline_highImagePreloads_1647 = new Set(),
JSCompiler_object_inline_styles_1648 = new Map(),
JSCompiler_object_inline_bootstrapScripts_1649 = new Set(),
JSCompiler_object_inline_scripts_1650 = new Set(),
JSCompiler_object_inline_bulkPreloads_1651 = new Set(),
JSCompiler_object_inline_preloads_1652 = {
images: new Map(),
stylesheets: new Map(),
scripts: new Map(),
Expand Down Expand Up @@ -2681,7 +2673,7 @@ function createRenderState(resumableState, generateStaticMarkup) {
scriptConfig.moduleScriptResources[href] = null;
scriptConfig = [];
pushLinkImpl(scriptConfig, props);
JSCompiler_object_inline_bootstrapScripts_1651.add(scriptConfig);
JSCompiler_object_inline_bootstrapScripts_1649.add(scriptConfig);
bootstrapChunks.push('<script src="', escapeTextForBrowser(src));
"string" === typeof integrity &&
bootstrapChunks.push('" integrity="', escapeTextForBrowser(integrity));
Expand Down Expand Up @@ -2722,7 +2714,7 @@ function createRenderState(resumableState, generateStaticMarkup) {
(props.moduleScriptResources[scriptConfig] = null),
(props = []),
pushLinkImpl(props, integrity),
JSCompiler_object_inline_bootstrapScripts_1651.add(props),
JSCompiler_object_inline_bootstrapScripts_1649.add(props),
bootstrapChunks.push(
'<script type="module" src="',
escapeTextForBrowser(i)
Expand All @@ -2737,7 +2729,7 @@ function createRenderState(resumableState, generateStaticMarkup) {
bootstrapChunks.push('" async="">\x3c/script>');
return {
placeholderPrefix: bootstrapScriptContent,
segmentPrefix: JSCompiler_object_inline_segmentPrefix_1633,
segmentPrefix: JSCompiler_object_inline_segmentPrefix_1631,
boundaryPrefix: idPrefix,
startInlineScript: "<script>",
htmlChunks: null,
Expand All @@ -2757,14 +2749,14 @@ function createRenderState(resumableState, generateStaticMarkup) {
charsetChunks: [],
viewportChunks: [],
hoistableChunks: [],
preconnects: JSCompiler_object_inline_preconnects_1647,
fontPreloads: JSCompiler_object_inline_fontPreloads_1648,
highImagePreloads: JSCompiler_object_inline_highImagePreloads_1649,
styles: JSCompiler_object_inline_styles_1650,
bootstrapScripts: JSCompiler_object_inline_bootstrapScripts_1651,
scripts: JSCompiler_object_inline_scripts_1652,
bulkPreloads: JSCompiler_object_inline_bulkPreloads_1653,
preloads: JSCompiler_object_inline_preloads_1654,
preconnects: JSCompiler_object_inline_preconnects_1645,
fontPreloads: JSCompiler_object_inline_fontPreloads_1646,
highImagePreloads: JSCompiler_object_inline_highImagePreloads_1647,
styles: JSCompiler_object_inline_styles_1648,
bootstrapScripts: JSCompiler_object_inline_bootstrapScripts_1649,
scripts: JSCompiler_object_inline_scripts_1650,
bulkPreloads: JSCompiler_object_inline_bulkPreloads_1651,
preloads: JSCompiler_object_inline_preloads_1652,
stylesToHoist: !1,
generateStaticMarkup: generateStaticMarkup
};
Expand Down Expand Up @@ -5699,4 +5691,4 @@ exports.renderToString = function (children, options) {
'The server used "renderToString" which does not support Suspense. If you intended for this Suspense boundary to render the fallback content on the server consider throwing an Error somewhere within the Suspense boundary. If you intended to have the server wait for the suspended component please switch to "renderToReadableStream" which supports Suspense on the server'
);
};
exports.version = "19.0.0-www-classic-444074d7";
exports.version = "19.0.0-www-classic-a976e819";
54 changes: 23 additions & 31 deletions compiled/facebook-www/ReactDOMServer-prod.modern.js
Expand Up @@ -445,25 +445,17 @@ var actionJavaScriptURL = escapeTextForBrowser(
);
function pushAdditionalFormField(value, key) {
this.push('<input type="hidden"');
validateAdditionalFormField(value);
if ("string" !== typeof value) throw Error(formatProdErrorMessage(480));
pushStringAttribute(this, "name", key);
pushStringAttribute(this, "value", value);
this.push("/>");
}
function validateAdditionalFormField(value) {
if ("string" !== typeof value) throw Error(formatProdErrorMessage(480));
}
function getCustomFormFields(resumableState, formAction) {
if ("function" === typeof formAction.$$FORM_ACTION) {
var id = resumableState.nextFormID++;
resumableState = resumableState.idPrefix + id;
try {
var customFields = formAction.$$FORM_ACTION(resumableState);
if (customFields) {
var formData = customFields.data;
null != formData && formData.forEach(validateAdditionalFormField);
}
return customFields;
return formAction.$$FORM_ACTION(resumableState);
} catch (x) {
if ("object" === typeof x && null !== x && "function" === typeof x.then)
throw x;
Expand Down Expand Up @@ -2635,16 +2627,16 @@ function createRenderState(resumableState, generateStaticMarkup) {
"\x3c/script>"
);
bootstrapScriptContent = idPrefix + "P:";
var JSCompiler_object_inline_segmentPrefix_1620 = idPrefix + "S:";
var JSCompiler_object_inline_segmentPrefix_1618 = idPrefix + "S:";
idPrefix += "B:";
var JSCompiler_object_inline_preconnects_1634 = new Set(),
JSCompiler_object_inline_fontPreloads_1635 = new Set(),
JSCompiler_object_inline_highImagePreloads_1636 = new Set(),
JSCompiler_object_inline_styles_1637 = new Map(),
JSCompiler_object_inline_bootstrapScripts_1638 = new Set(),
JSCompiler_object_inline_scripts_1639 = new Set(),
JSCompiler_object_inline_bulkPreloads_1640 = new Set(),
JSCompiler_object_inline_preloads_1641 = {
var JSCompiler_object_inline_preconnects_1632 = new Set(),
JSCompiler_object_inline_fontPreloads_1633 = new Set(),
JSCompiler_object_inline_highImagePreloads_1634 = new Set(),
JSCompiler_object_inline_styles_1635 = new Map(),
JSCompiler_object_inline_bootstrapScripts_1636 = new Set(),
JSCompiler_object_inline_scripts_1637 = new Set(),
JSCompiler_object_inline_bulkPreloads_1638 = new Set(),
JSCompiler_object_inline_preloads_1639 = {
images: new Map(),
stylesheets: new Map(),
scripts: new Map(),
Expand Down Expand Up @@ -2681,7 +2673,7 @@ function createRenderState(resumableState, generateStaticMarkup) {
scriptConfig.moduleScriptResources[href] = null;
scriptConfig = [];
pushLinkImpl(scriptConfig, props);
JSCompiler_object_inline_bootstrapScripts_1638.add(scriptConfig);
JSCompiler_object_inline_bootstrapScripts_1636.add(scriptConfig);
bootstrapChunks.push('<script src="', escapeTextForBrowser(src));
"string" === typeof integrity &&
bootstrapChunks.push('" integrity="', escapeTextForBrowser(integrity));
Expand Down Expand Up @@ -2722,7 +2714,7 @@ function createRenderState(resumableState, generateStaticMarkup) {
(props.moduleScriptResources[scriptConfig] = null),
(props = []),
pushLinkImpl(props, integrity),
JSCompiler_object_inline_bootstrapScripts_1638.add(props),
JSCompiler_object_inline_bootstrapScripts_1636.add(props),
bootstrapChunks.push(
'<script type="module" src="',
escapeTextForBrowser(i)
Expand All @@ -2737,7 +2729,7 @@ function createRenderState(resumableState, generateStaticMarkup) {
bootstrapChunks.push('" async="">\x3c/script>');
return {
placeholderPrefix: bootstrapScriptContent,
segmentPrefix: JSCompiler_object_inline_segmentPrefix_1620,
segmentPrefix: JSCompiler_object_inline_segmentPrefix_1618,
boundaryPrefix: idPrefix,
startInlineScript: "<script>",
htmlChunks: null,
Expand All @@ -2757,14 +2749,14 @@ function createRenderState(resumableState, generateStaticMarkup) {
charsetChunks: [],
viewportChunks: [],
hoistableChunks: [],
preconnects: JSCompiler_object_inline_preconnects_1634,
fontPreloads: JSCompiler_object_inline_fontPreloads_1635,
highImagePreloads: JSCompiler_object_inline_highImagePreloads_1636,
styles: JSCompiler_object_inline_styles_1637,
bootstrapScripts: JSCompiler_object_inline_bootstrapScripts_1638,
scripts: JSCompiler_object_inline_scripts_1639,
bulkPreloads: JSCompiler_object_inline_bulkPreloads_1640,
preloads: JSCompiler_object_inline_preloads_1641,
preconnects: JSCompiler_object_inline_preconnects_1632,
fontPreloads: JSCompiler_object_inline_fontPreloads_1633,
highImagePreloads: JSCompiler_object_inline_highImagePreloads_1634,
styles: JSCompiler_object_inline_styles_1635,
bootstrapScripts: JSCompiler_object_inline_bootstrapScripts_1636,
scripts: JSCompiler_object_inline_scripts_1637,
bulkPreloads: JSCompiler_object_inline_bulkPreloads_1638,
preloads: JSCompiler_object_inline_preloads_1639,
stylesToHoist: !1,
generateStaticMarkup: generateStaticMarkup
};
Expand Down Expand Up @@ -5677,4 +5669,4 @@ exports.renderToString = function (children, options) {
'The server used "renderToString" which does not support Suspense. If you intended for this Suspense boundary to render the fallback content on the server consider throwing an Error somewhere within the Suspense boundary. If you intended to have the server wait for the suspended component please switch to "renderToReadableStream" which supports Suspense on the server'
);
};
exports.version = "19.0.0-www-modern-df91acfc";
exports.version = "19.0.0-www-modern-54e820e3";
29 changes: 6 additions & 23 deletions compiled/facebook-www/ReactDOMServerStreaming-dev.modern.js
Expand Up @@ -2569,7 +2569,11 @@ var startHiddenInputChunk = stringToPrecomputedChunk('<input type="hidden"');
function pushAdditionalFormField(value, key) {
var target = this;
target.push(startHiddenInputChunk);
validateAdditionalFormField(value);

if (typeof value !== 'string') {
throw new Error('File/Blob fields are not yet supported in progressive forms. ' + 'It probably means you are closing over binary data or FormData in a Server Action.');
}

pushStringAttribute(target, 'name', key);
pushStringAttribute(target, 'value', value);
target.push(endOfStartTagSelfClosing);
Expand All @@ -2582,35 +2586,14 @@ function pushAdditionalFormFields(target, formData) {
}
}

function validateAdditionalFormField(value, key) {
if (typeof value !== 'string') {
throw new Error('File/Blob fields are not yet supported in progressive forms. ' + 'Will fallback to client hydration.');
}
}

function validateAdditionalFormFields(formData) {
if (formData != null) {
// $FlowFixMe[prop-missing]: FormData has forEach.
formData.forEach(validateAdditionalFormField);
}

return formData;
}

function getCustomFormFields(resumableState, formAction) {
var customAction = formAction.$$FORM_ACTION;

if (typeof customAction === 'function') {
var prefix = makeFormFieldPrefix(resumableState);

try {
var customFields = formAction.$$FORM_ACTION(prefix);

if (customFields) {
validateAdditionalFormFields(customFields.data);
}

return customFields;
return formAction.$$FORM_ACTION(prefix);
} catch (x) {
if (typeof x === 'object' && x !== null && typeof x.then === 'function') {
// Rethrow suspense.
Expand Down

0 comments on commit e83564a

Please sign in to comment.