-
Notifications
You must be signed in to change notification settings - Fork 46.7k
/
ReactFlightActionServer.js
163 lines (150 loc) · 5.11 KB
/
ReactFlightActionServer.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
/**
* 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
*/
import type {Thenable, ReactFormState} from 'shared/ReactTypes';
import type {
ServerManifest,
ClientReference as ServerReference,
} from 'react-client/src/ReactFlightClientConfig';
import {
resolveServerReference,
preloadModule,
requireModule,
} from 'react-client/src/ReactFlightClientConfig';
import {createResponse, close, getRoot} from './ReactFlightReplyServer';
type ServerReferenceId = any;
function bindArgs(fn: any, args: any) {
return fn.bind.apply(fn, [null].concat(args));
}
function loadServerReference<T>(
bundlerConfig: ServerManifest,
id: ServerReferenceId,
bound: null | Thenable<Array<any>>,
): Promise<T> {
const serverReference: ServerReference<T> =
resolveServerReference<$FlowFixMe>(bundlerConfig, id);
// We expect most servers to not really need this because you'd just have all
// the relevant modules already loaded but it allows for lazy loading of code
// if needed.
const preloadPromise = preloadModule(serverReference);
if (bound) {
return Promise.all([(bound: any), preloadPromise]).then(
([args]: Array<any>) => bindArgs(requireModule(serverReference), args),
);
} else if (preloadPromise) {
return Promise.resolve(preloadPromise).then(() =>
requireModule(serverReference),
);
} else {
// Synchronously available
return Promise.resolve(requireModule(serverReference));
}
}
function decodeBoundActionMetaData(
body: FormData,
serverManifest: ServerManifest,
formFieldPrefix: string,
): {id: ServerReferenceId, bound: null | Promise<Array<any>>} {
// The data for this reference is encoded in multiple fields under this prefix.
const actionResponse = createResponse(serverManifest, formFieldPrefix, body);
close(actionResponse);
const refPromise = getRoot<{
id: ServerReferenceId,
bound: null | Promise<Array<any>>,
}>(actionResponse);
// Force it to initialize
// $FlowFixMe
refPromise.then(() => {});
if (refPromise.status !== 'fulfilled') {
// $FlowFixMe
throw refPromise.reason;
}
return refPromise.value;
}
export function decodeAction<T>(
body: FormData,
serverManifest: ServerManifest,
): Promise<() => T> | null {
// We're going to create a new formData object that holds all the fields except
// the implementation details of the action data.
const formData = new FormData();
let action: Promise<(formData: FormData) => T> | null = null;
// $FlowFixMe[prop-missing]
body.forEach((value: string | File, key: string) => {
if (!key.startsWith('$ACTION_')) {
formData.append(key, value);
return;
}
// Later actions may override earlier actions if a button is used to override the default
// form action.
if (key.startsWith('$ACTION_REF_')) {
const formFieldPrefix = '$ACTION_' + key.slice(12) + ':';
const metaData = decodeBoundActionMetaData(
body,
serverManifest,
formFieldPrefix,
);
action = loadServerReference(serverManifest, metaData.id, metaData.bound);
return;
}
if (key.startsWith('$ACTION_ID_')) {
const id = key.slice(11);
action = loadServerReference(serverManifest, id, null);
return;
}
});
if (action === null) {
return null;
}
// Return the action with the remaining FormData bound to the first argument.
return action.then(fn => fn.bind(null, formData));
}
export function decodeFormState<S>(
actionResult: S,
body: FormData,
serverManifest: ServerManifest,
): Promise<ReactFormState<S, ServerReferenceId> | null> {
const keyPath = body.get('$ACTION_KEY');
if (typeof keyPath !== 'string') {
// This form submission did not include any form state.
return Promise.resolve(null);
}
// Search through the form data object to get the reference id and the number
// of bound arguments. This repeats some of the work done in decodeAction.
let metaData = null;
// $FlowFixMe[prop-missing]
body.forEach((value: string | File, key: string) => {
if (key.startsWith('$ACTION_REF_')) {
const formFieldPrefix = '$ACTION_' + key.slice(12) + ':';
metaData = decodeBoundActionMetaData(
body,
serverManifest,
formFieldPrefix,
);
}
// We don't check for the simple $ACTION_ID_ case because form state actions
// are always bound to the state argument.
});
if (metaData === null) {
// Should be unreachable.
return Promise.resolve(null);
}
const referenceId = metaData.id;
return Promise.resolve(metaData.bound).then(bound => {
if (bound === null) {
// Should be unreachable because form state actions are always bound to the
// state argument.
return null;
}
// The form action dispatch method is always bound to the initial state.
// But when comparing signatures, we compare to the original unbound action.
// Subtract one from the arity to account for this.
const boundArity = bound.length - 1;
return [actionResult, keyPath, referenceId, boundArity];
});
}