relay-runtime 8.0.0 → 10.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/handlers/RelayDefaultHandlerProvider.js.flow +41 -0
- package/handlers/connection/ConnectionHandler.js.flow +549 -0
- package/handlers/connection/ConnectionInterface.js.flow +92 -0
- package/handlers/connection/MutationHandlers.js.flow +88 -0
- package/index.js +1 -1
- package/index.js.flow +320 -0
- package/lib/handlers/RelayDefaultHandlerProvider.js +13 -2
- package/lib/handlers/connection/{RelayConnectionHandler.js → ConnectionHandler.js} +33 -35
- package/lib/handlers/connection/{RelayConnectionInterface.js → ConnectionInterface.js} +2 -2
- package/lib/handlers/connection/MutationHandlers.js +86 -0
- package/lib/index.js +15 -19
- package/lib/mutations/RelayDeclarativeMutationConfig.js +29 -52
- package/lib/mutations/RelayRecordProxy.js +1 -3
- package/lib/mutations/RelayRecordSourceMutator.js +2 -9
- package/lib/mutations/RelayRecordSourceProxy.js +2 -4
- package/lib/mutations/RelayRecordSourceSelectorProxy.js +1 -13
- package/lib/mutations/commitMutation.js +13 -3
- package/lib/mutations/validateMutation.js +16 -9
- package/lib/network/RelayObservable.js +9 -9
- package/lib/network/RelayQueryResponseCache.js +8 -6
- package/lib/query/PreloadableQueryRegistry.js +70 -0
- package/lib/query/fetchQueryInternal.js +31 -23
- package/lib/store/DataChecker.js +122 -110
- package/lib/store/RelayConcreteVariables.js +6 -2
- package/lib/store/RelayModernEnvironment.js +121 -67
- package/lib/store/RelayModernFragmentSpecResolver.js +12 -16
- package/lib/store/RelayModernQueryExecutor.js +389 -314
- package/lib/store/RelayModernRecord.js +14 -9
- package/lib/store/RelayModernSelector.js +7 -3
- package/lib/store/RelayModernStore.js +289 -484
- package/lib/store/RelayOperationTracker.js +35 -78
- package/lib/store/RelayOptimisticRecordSource.js +7 -5
- package/lib/store/RelayPublishQueue.js +6 -33
- package/lib/store/RelayReader.js +113 -45
- package/lib/store/RelayRecordSource.js +2 -9
- package/lib/store/RelayRecordSourceMapImpl.js +13 -18
- package/lib/store/RelayReferenceMarker.js +40 -60
- package/lib/store/RelayResponseNormalizer.js +158 -193
- package/lib/store/RelayStoreUtils.js +1 -0
- package/lib/store/StoreInspector.js +8 -8
- package/lib/store/TypeID.js +28 -0
- package/lib/store/cloneRelayScalarHandleSourceField.js +44 -0
- package/lib/store/normalizeRelayPayload.js +6 -2
- package/lib/store/readInlineData.js +1 -1
- package/lib/subscription/requestSubscription.js +5 -3
- package/lib/util/RelayConcreteNode.js +9 -6
- package/lib/util/RelayError.js +39 -9
- package/lib/util/RelayFeatureFlags.js +2 -5
- package/lib/util/RelayReplaySubject.js +3 -3
- package/lib/util/createPayloadFor3DField.js +7 -2
- package/lib/util/getRequestIdentifier.js +2 -2
- package/lib/util/recycleNodesInto.js +2 -6
- package/mutations/RelayDeclarativeMutationConfig.js.flow +380 -0
- package/mutations/RelayRecordProxy.js.flow +165 -0
- package/mutations/RelayRecordSourceMutator.js.flow +238 -0
- package/mutations/RelayRecordSourceProxy.js.flow +164 -0
- package/mutations/RelayRecordSourceSelectorProxy.js.flow +119 -0
- package/mutations/applyOptimisticMutation.js.flow +76 -0
- package/mutations/commitLocalUpdate.js.flow +24 -0
- package/mutations/commitMutation.js.flow +182 -0
- package/mutations/validateMutation.js.flow +213 -0
- package/network/ConvertToExecuteFunction.js.flow +49 -0
- package/network/RelayNetwork.js.flow +84 -0
- package/network/RelayNetworkTypes.js.flow +123 -0
- package/network/RelayObservable.js.flow +634 -0
- package/network/RelayQueryResponseCache.js.flow +111 -0
- package/package.json +1 -1
- package/query/GraphQLTag.js.flow +166 -0
- package/query/PreloadableQueryRegistry.js.flow +65 -0
- package/query/fetchQuery.js.flow +47 -0
- package/query/fetchQueryInternal.js.flow +348 -0
- package/relay-runtime.js +2 -2
- package/relay-runtime.min.js +2 -2
- package/store/ClientID.js.flow +43 -0
- package/store/DataChecker.js.flow +502 -0
- package/store/RelayConcreteVariables.js.flow +96 -0
- package/store/RelayModernEnvironment.js.flow +551 -0
- package/store/RelayModernFragmentSpecResolver.js.flow +426 -0
- package/store/RelayModernOperationDescriptor.js.flow +88 -0
- package/store/RelayModernQueryExecutor.js.flow +1321 -0
- package/store/RelayModernRecord.js.flow +403 -0
- package/store/RelayModernSelector.js.flow +455 -0
- package/store/RelayModernStore.js.flow +842 -0
- package/store/RelayOperationTracker.js.flow +164 -0
- package/store/RelayOptimisticRecordSource.js.flow +119 -0
- package/store/RelayPublishQueue.js.flow +401 -0
- package/store/RelayReader.js.flow +473 -0
- package/store/RelayRecordSource.js.flow +29 -0
- package/store/RelayRecordSourceMapImpl.js.flow +87 -0
- package/store/RelayRecordState.js.flow +37 -0
- package/store/RelayReferenceMarker.js.flow +257 -0
- package/store/RelayResponseNormalizer.js.flow +680 -0
- package/store/RelayStoreTypes.js.flow +899 -0
- package/store/RelayStoreUtils.js.flow +219 -0
- package/store/StoreInspector.js.flow +171 -0
- package/store/TypeID.js.flow +28 -0
- package/store/ViewerPattern.js.flow +26 -0
- package/store/cloneRelayHandleSourceField.js.flow +66 -0
- package/store/cloneRelayScalarHandleSourceField.js.flow +62 -0
- package/store/createFragmentSpecResolver.js.flow +55 -0
- package/store/createRelayContext.js.flow +44 -0
- package/store/defaultGetDataID.js.flow +27 -0
- package/store/hasOverlappingIDs.js.flow +34 -0
- package/store/isRelayModernEnvironment.js.flow +27 -0
- package/store/normalizeRelayPayload.js.flow +51 -0
- package/store/readInlineData.js.flow +75 -0
- package/subscription/requestSubscription.js.flow +100 -0
- package/util/JSResourceTypes.flow.js.flow +20 -0
- package/util/NormalizationNode.js.flow +198 -0
- package/util/ReaderNode.js.flow +208 -0
- package/util/RelayConcreteNode.js.flow +93 -0
- package/util/RelayDefaultHandleKey.js.flow +17 -0
- package/util/RelayError.js.flow +62 -0
- package/util/RelayFeatureFlags.js.flow +30 -0
- package/util/RelayProfiler.js.flow +284 -0
- package/util/RelayReplaySubject.js.flow +135 -0
- package/util/RelayRuntimeTypes.js.flow +72 -0
- package/util/createPayloadFor3DField.js.flow +43 -0
- package/util/deepFreeze.js.flow +36 -0
- package/util/generateID.js.flow +21 -0
- package/util/getFragmentIdentifier.js.flow +52 -0
- package/util/getRelayHandleKey.js.flow +41 -0
- package/util/getRequestIdentifier.js.flow +42 -0
- package/util/isPromise.js.flow +21 -0
- package/util/isScalarAndEqual.js.flow +26 -0
- package/util/recycleNodesInto.js.flow +76 -0
- package/util/resolveImmediate.js.flow +30 -0
- package/util/stableCopy.js.flow +35 -0
- package/lib/handlers/RelayDefaultMissingFieldHandlers.js +0 -26
- package/lib/handlers/getRelayDefaultMissingFieldHandlers.js +0 -36
- package/lib/query/RelayModernGraphQLTag.js +0 -104
- package/lib/store/RelayConnection.js +0 -37
- package/lib/store/RelayConnectionResolver.js +0 -178
- package/lib/store/RelayRecordSourceObjectImpl.js +0 -79
- package/lib/util/getFragmentSpecIdentifier.js +0 -27
|
@@ -0,0 +1,1321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
* @flow strict-local
|
|
8
|
+
* @format
|
|
9
|
+
* @emails oncall+relay
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// flowlint ambiguous-object-type:error
|
|
13
|
+
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
const RelayError = require('../util/RelayError');
|
|
17
|
+
const RelayModernRecord = require('./RelayModernRecord');
|
|
18
|
+
const RelayObservable = require('../network/RelayObservable');
|
|
19
|
+
const RelayRecordSource = require('./RelayRecordSource');
|
|
20
|
+
const RelayResponseNormalizer = require('./RelayResponseNormalizer');
|
|
21
|
+
|
|
22
|
+
const invariant = require('invariant');
|
|
23
|
+
const stableCopy = require('../util/stableCopy');
|
|
24
|
+
const warning = require('warning');
|
|
25
|
+
|
|
26
|
+
const {generateClientID} = require('./ClientID');
|
|
27
|
+
const {createNormalizationSelector} = require('./RelayModernSelector');
|
|
28
|
+
const {ROOT_TYPE, TYPENAME_KEY, getStorageKey} = require('./RelayStoreUtils');
|
|
29
|
+
|
|
30
|
+
import type {
|
|
31
|
+
GraphQLResponse,
|
|
32
|
+
GraphQLSingularResponse,
|
|
33
|
+
GraphQLResponseWithData,
|
|
34
|
+
} from '../network/RelayNetworkTypes';
|
|
35
|
+
import type {Sink, Subscription} from '../network/RelayObservable';
|
|
36
|
+
import type {
|
|
37
|
+
DeferPlaceholder,
|
|
38
|
+
RequestDescriptor,
|
|
39
|
+
HandleFieldPayload,
|
|
40
|
+
IncrementalDataPlaceholder,
|
|
41
|
+
ModuleImportPayload,
|
|
42
|
+
NormalizationSelector,
|
|
43
|
+
OperationDescriptor,
|
|
44
|
+
OperationLoader,
|
|
45
|
+
OperationTracker,
|
|
46
|
+
OptimisticResponseConfig,
|
|
47
|
+
OptimisticUpdate,
|
|
48
|
+
PublishQueue,
|
|
49
|
+
Record,
|
|
50
|
+
RelayResponsePayload,
|
|
51
|
+
SelectorStoreUpdater,
|
|
52
|
+
Store,
|
|
53
|
+
StreamPlaceholder,
|
|
54
|
+
} from '../store/RelayStoreTypes';
|
|
55
|
+
import type {
|
|
56
|
+
NormalizationLinkedField,
|
|
57
|
+
NormalizationSplitOperation,
|
|
58
|
+
NormalizationSelectableNode,
|
|
59
|
+
} from '../util/NormalizationNode';
|
|
60
|
+
import type {DataID, Variables} from '../util/RelayRuntimeTypes';
|
|
61
|
+
import type {GetDataID} from './RelayResponseNormalizer';
|
|
62
|
+
import type {NormalizationOptions} from './RelayResponseNormalizer';
|
|
63
|
+
|
|
64
|
+
export type ExecuteConfig = {|
|
|
65
|
+
+getDataID: GetDataID,
|
|
66
|
+
+treatMissingFieldsAsNull: boolean,
|
|
67
|
+
+operation: OperationDescriptor,
|
|
68
|
+
+operationExecutions: Map<string, ActiveState>,
|
|
69
|
+
+operationLoader: ?OperationLoader,
|
|
70
|
+
+operationTracker?: ?OperationTracker,
|
|
71
|
+
+optimisticConfig: ?OptimisticResponseConfig,
|
|
72
|
+
+publishQueue: PublishQueue,
|
|
73
|
+
+scheduler?: ?TaskScheduler,
|
|
74
|
+
+sink: Sink<GraphQLResponse>,
|
|
75
|
+
+source: RelayObservable<GraphQLResponse>,
|
|
76
|
+
+store: Store,
|
|
77
|
+
+updater?: ?SelectorStoreUpdater,
|
|
78
|
+
+isClientPayload?: boolean,
|
|
79
|
+
|};
|
|
80
|
+
|
|
81
|
+
export type ActiveState = 'active' | 'inactive';
|
|
82
|
+
|
|
83
|
+
export type TaskScheduler = {|
|
|
84
|
+
+cancel: (id: string) => void,
|
|
85
|
+
+schedule: (fn: () => void) => string,
|
|
86
|
+
|};
|
|
87
|
+
|
|
88
|
+
type Label = string;
|
|
89
|
+
type PathKey = string;
|
|
90
|
+
type IncrementalResults =
|
|
91
|
+
| {|
|
|
92
|
+
+kind: 'placeholder',
|
|
93
|
+
+placeholder: IncrementalDataPlaceholder,
|
|
94
|
+
|}
|
|
95
|
+
| {|
|
|
96
|
+
+kind: 'response',
|
|
97
|
+
+responses: Array<IncrementalGraphQLResponse>,
|
|
98
|
+
|};
|
|
99
|
+
|
|
100
|
+
type IncrementalGraphQLResponse = {|
|
|
101
|
+
label: string,
|
|
102
|
+
path: $ReadOnlyArray<mixed>,
|
|
103
|
+
response: GraphQLResponseWithData,
|
|
104
|
+
|};
|
|
105
|
+
|
|
106
|
+
function execute(config: ExecuteConfig): Executor {
|
|
107
|
+
return new Executor(config);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Coordinates the execution of a query, handling network callbacks
|
|
112
|
+
* including optimistic payloads, standard payloads, resolution of match
|
|
113
|
+
* dependencies, etc.
|
|
114
|
+
*/
|
|
115
|
+
class Executor {
|
|
116
|
+
_getDataID: GetDataID;
|
|
117
|
+
_treatMissingFieldsAsNull: boolean;
|
|
118
|
+
_incrementalPayloadsPending: boolean;
|
|
119
|
+
_incrementalResults: Map<Label, Map<PathKey, IncrementalResults>>;
|
|
120
|
+
_nextSubscriptionId: number;
|
|
121
|
+
_operation: OperationDescriptor;
|
|
122
|
+
_operationExecutions: Map<string, ActiveState>;
|
|
123
|
+
_operationLoader: ?OperationLoader;
|
|
124
|
+
_operationTracker: ?OperationTracker;
|
|
125
|
+
_operationUpdateEpochs: Map<string, number>;
|
|
126
|
+
_optimisticUpdates: null | Array<OptimisticUpdate>;
|
|
127
|
+
_pendingModulePayloadsCount: number;
|
|
128
|
+
_publishQueue: PublishQueue;
|
|
129
|
+
_scheduler: ?TaskScheduler;
|
|
130
|
+
_sink: Sink<GraphQLResponse>;
|
|
131
|
+
_source: Map<
|
|
132
|
+
string,
|
|
133
|
+
{|+record: Record, +fieldPayloads: Array<HandleFieldPayload>|},
|
|
134
|
+
>;
|
|
135
|
+
_state: 'started' | 'loading_incremental' | 'loading_final' | 'completed';
|
|
136
|
+
_store: Store;
|
|
137
|
+
_subscriptions: Map<number, Subscription>;
|
|
138
|
+
_updater: ?SelectorStoreUpdater;
|
|
139
|
+
+_isClientPayload: boolean;
|
|
140
|
+
|
|
141
|
+
constructor({
|
|
142
|
+
operation,
|
|
143
|
+
operationExecutions,
|
|
144
|
+
operationLoader,
|
|
145
|
+
optimisticConfig,
|
|
146
|
+
publishQueue,
|
|
147
|
+
scheduler,
|
|
148
|
+
sink,
|
|
149
|
+
source,
|
|
150
|
+
store,
|
|
151
|
+
updater,
|
|
152
|
+
operationTracker,
|
|
153
|
+
treatMissingFieldsAsNull,
|
|
154
|
+
getDataID,
|
|
155
|
+
isClientPayload,
|
|
156
|
+
}: ExecuteConfig): void {
|
|
157
|
+
this._getDataID = getDataID;
|
|
158
|
+
this._treatMissingFieldsAsNull = treatMissingFieldsAsNull;
|
|
159
|
+
this._incrementalPayloadsPending = false;
|
|
160
|
+
this._incrementalResults = new Map();
|
|
161
|
+
this._nextSubscriptionId = 0;
|
|
162
|
+
this._operation = operation;
|
|
163
|
+
this._operationExecutions = operationExecutions;
|
|
164
|
+
this._operationLoader = operationLoader;
|
|
165
|
+
this._operationTracker = operationTracker;
|
|
166
|
+
this._operationUpdateEpochs = new Map();
|
|
167
|
+
this._optimisticUpdates = null;
|
|
168
|
+
this._pendingModulePayloadsCount = 0;
|
|
169
|
+
this._publishQueue = publishQueue;
|
|
170
|
+
this._scheduler = scheduler;
|
|
171
|
+
this._sink = sink;
|
|
172
|
+
this._source = new Map();
|
|
173
|
+
this._state = 'started';
|
|
174
|
+
this._store = store;
|
|
175
|
+
this._subscriptions = new Map();
|
|
176
|
+
this._updater = updater;
|
|
177
|
+
this._isClientPayload = isClientPayload === true;
|
|
178
|
+
|
|
179
|
+
const id = this._nextSubscriptionId++;
|
|
180
|
+
source.subscribe({
|
|
181
|
+
complete: () => this._complete(id),
|
|
182
|
+
error: error => this._error(error),
|
|
183
|
+
next: response => {
|
|
184
|
+
try {
|
|
185
|
+
this._next(id, response);
|
|
186
|
+
} catch (error) {
|
|
187
|
+
sink.error(error);
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
start: subscription => this._start(id, subscription),
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
if (optimisticConfig != null) {
|
|
194
|
+
this._processOptimisticResponse(
|
|
195
|
+
optimisticConfig.response != null
|
|
196
|
+
? {data: optimisticConfig.response}
|
|
197
|
+
: null,
|
|
198
|
+
optimisticConfig.updater,
|
|
199
|
+
false,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Cancel any pending execution tasks and mark the executor as completed.
|
|
205
|
+
cancel(): void {
|
|
206
|
+
if (this._state === 'completed') {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
this._state = 'completed';
|
|
210
|
+
this._operationExecutions.delete(this._operation.request.identifier);
|
|
211
|
+
|
|
212
|
+
if (this._subscriptions.size !== 0) {
|
|
213
|
+
this._subscriptions.forEach(sub => sub.unsubscribe());
|
|
214
|
+
this._subscriptions.clear();
|
|
215
|
+
}
|
|
216
|
+
const optimisticUpdates = this._optimisticUpdates;
|
|
217
|
+
if (optimisticUpdates !== null) {
|
|
218
|
+
this._optimisticUpdates = null;
|
|
219
|
+
optimisticUpdates.forEach(update =>
|
|
220
|
+
this._publishQueue.revertUpdate(update),
|
|
221
|
+
);
|
|
222
|
+
this._publishQueue.run();
|
|
223
|
+
}
|
|
224
|
+
this._incrementalResults.clear();
|
|
225
|
+
this._completeOperationTracker();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
_updateActiveState(): void {
|
|
229
|
+
let activeState;
|
|
230
|
+
switch (this._state) {
|
|
231
|
+
case 'started': {
|
|
232
|
+
activeState = 'active';
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
case 'loading_incremental': {
|
|
236
|
+
activeState = 'active';
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
case 'completed': {
|
|
240
|
+
activeState = 'inactive';
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
case 'loading_final': {
|
|
244
|
+
activeState =
|
|
245
|
+
this._pendingModulePayloadsCount > 0 ? 'active' : 'inactive';
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
default:
|
|
249
|
+
(this._state: empty);
|
|
250
|
+
invariant(false, 'RelayModernQueryExecutor: invalid executor state.');
|
|
251
|
+
}
|
|
252
|
+
this._operationExecutions.set(
|
|
253
|
+
this._operation.request.identifier,
|
|
254
|
+
activeState,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
_schedule(task: () => void): void {
|
|
259
|
+
const scheduler = this._scheduler;
|
|
260
|
+
if (scheduler != null) {
|
|
261
|
+
const id = this._nextSubscriptionId++;
|
|
262
|
+
RelayObservable.create(sink => {
|
|
263
|
+
const cancellationToken = scheduler.schedule(() => {
|
|
264
|
+
try {
|
|
265
|
+
task();
|
|
266
|
+
sink.complete();
|
|
267
|
+
} catch (error) {
|
|
268
|
+
sink.error(error);
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
return () => scheduler.cancel(cancellationToken);
|
|
272
|
+
}).subscribe({
|
|
273
|
+
complete: () => this._complete(id),
|
|
274
|
+
error: error => this._error(error),
|
|
275
|
+
start: subscription => this._start(id, subscription),
|
|
276
|
+
});
|
|
277
|
+
} else {
|
|
278
|
+
task();
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
_complete(id: number): void {
|
|
283
|
+
this._subscriptions.delete(id);
|
|
284
|
+
if (this._subscriptions.size === 0) {
|
|
285
|
+
this.cancel();
|
|
286
|
+
this._sink.complete();
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
_error(error: Error): void {
|
|
291
|
+
this.cancel();
|
|
292
|
+
this._sink.error(error);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
_start(id: number, subscription: Subscription): void {
|
|
296
|
+
this._subscriptions.set(id, subscription);
|
|
297
|
+
this._updateActiveState();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Handle a raw GraphQL response.
|
|
301
|
+
_next(_id: number, response: GraphQLResponse): void {
|
|
302
|
+
this._schedule(() => {
|
|
303
|
+
this._handleNext(response);
|
|
304
|
+
this._maybeCompleteSubscriptionOperationTracking();
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
_handleErrorResponse(
|
|
309
|
+
responses: $ReadOnlyArray<GraphQLSingularResponse>,
|
|
310
|
+
): $ReadOnlyArray<GraphQLResponseWithData> {
|
|
311
|
+
const results = [];
|
|
312
|
+
responses.forEach(response => {
|
|
313
|
+
if (
|
|
314
|
+
response.data === null &&
|
|
315
|
+
response.extensions != null &&
|
|
316
|
+
!response.hasOwnProperty('errors')
|
|
317
|
+
) {
|
|
318
|
+
// Skip extensions-only payloads
|
|
319
|
+
return;
|
|
320
|
+
} else if (response.data == null) {
|
|
321
|
+
// Error if any other payload in the batch is missing data, regardless of whether
|
|
322
|
+
// it had `errors` or not.
|
|
323
|
+
const errors =
|
|
324
|
+
response.hasOwnProperty('errors') && response.errors != null
|
|
325
|
+
? response.errors
|
|
326
|
+
: null;
|
|
327
|
+
const messages = errors
|
|
328
|
+
? errors.map(({message}) => message).join('\n')
|
|
329
|
+
: '(No errors)';
|
|
330
|
+
const error = RelayError.create(
|
|
331
|
+
'RelayNetwork',
|
|
332
|
+
'No data returned for operation `' +
|
|
333
|
+
this._operation.request.node.params.name +
|
|
334
|
+
'`, got error(s):\n' +
|
|
335
|
+
messages +
|
|
336
|
+
'\n\nSee the error `source` property for more information.',
|
|
337
|
+
);
|
|
338
|
+
(error: $FlowFixMe).source = {
|
|
339
|
+
errors,
|
|
340
|
+
operation: this._operation.request.node,
|
|
341
|
+
variables: this._operation.request.variables,
|
|
342
|
+
};
|
|
343
|
+
// In V8, Error objects keep the closure scope chain alive until the
|
|
344
|
+
// err.stack property is accessed.
|
|
345
|
+
error.stack;
|
|
346
|
+
throw error;
|
|
347
|
+
} else {
|
|
348
|
+
const responseWithData: GraphQLResponseWithData = (response: $FlowFixMe);
|
|
349
|
+
results.push(responseWithData);
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
return results;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* This method return boolean to indicate if the optimistic
|
|
357
|
+
* response has been handled
|
|
358
|
+
*/
|
|
359
|
+
_handleOptimisticResponses(
|
|
360
|
+
responses: $ReadOnlyArray<GraphQLResponseWithData>,
|
|
361
|
+
): boolean {
|
|
362
|
+
if (responses.length > 1) {
|
|
363
|
+
if (
|
|
364
|
+
responses.some(
|
|
365
|
+
responsePart => responsePart.extensions?.isOptimistic === true,
|
|
366
|
+
)
|
|
367
|
+
) {
|
|
368
|
+
invariant(false, 'Optimistic responses cannot be batched.');
|
|
369
|
+
}
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
const response = responses[0];
|
|
373
|
+
const isOptimistic = response.extensions?.isOptimistic === true;
|
|
374
|
+
if (isOptimistic && this._state !== 'started') {
|
|
375
|
+
invariant(
|
|
376
|
+
false,
|
|
377
|
+
'RelayModernQueryExecutor: optimistic payload received after server payload.',
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
if (isOptimistic) {
|
|
381
|
+
this._processOptimisticResponse(
|
|
382
|
+
response,
|
|
383
|
+
null,
|
|
384
|
+
this._treatMissingFieldsAsNull,
|
|
385
|
+
);
|
|
386
|
+
this._sink.next(response);
|
|
387
|
+
return true;
|
|
388
|
+
}
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
_handleNext(response: GraphQLResponse): void {
|
|
393
|
+
if (this._state === 'completed') {
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const responses = Array.isArray(response) ? response : [response];
|
|
398
|
+
const responsesWithData = this._handleErrorResponse(responses);
|
|
399
|
+
|
|
400
|
+
if (responsesWithData.length === 0) {
|
|
401
|
+
// no results with data, nothing to process
|
|
402
|
+
// this can occur with extensions-only payloads
|
|
403
|
+
const isFinal = responses.some(x => x.extensions?.is_final === true);
|
|
404
|
+
if (isFinal) {
|
|
405
|
+
this._state = 'loading_final';
|
|
406
|
+
this._updateActiveState();
|
|
407
|
+
this._incrementalPayloadsPending = false;
|
|
408
|
+
}
|
|
409
|
+
this._sink.next(response);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Next, handle optimistic responses
|
|
414
|
+
const isOptimistic = this._handleOptimisticResponses(responsesWithData);
|
|
415
|
+
if (isOptimistic) {
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const [
|
|
420
|
+
nonIncrementalResponses,
|
|
421
|
+
incrementalResponses,
|
|
422
|
+
] = partitionGraphQLResponses(responsesWithData);
|
|
423
|
+
|
|
424
|
+
// In theory this doesn't preserve the ordering of the batch.
|
|
425
|
+
// The idea is that a batch is always:
|
|
426
|
+
// * at-most one non-incremental payload
|
|
427
|
+
// * followed zero or more incremental payloads
|
|
428
|
+
// The non-incremental payload can appear if the server sends a batch
|
|
429
|
+
// w the initial payload followed by some early-to-resolve incremental
|
|
430
|
+
// payloads (although, can that even happen?)
|
|
431
|
+
if (nonIncrementalResponses.length > 0) {
|
|
432
|
+
const payloadFollowups = this._processResponses(nonIncrementalResponses);
|
|
433
|
+
// Please note, that we're passing `this._operation` to the publish
|
|
434
|
+
// queue here, which will later passed to the store (via notify)
|
|
435
|
+
// to indicate that this is an operation that cause the store to update
|
|
436
|
+
const updatedOwners = this._publishQueue.run(this._operation);
|
|
437
|
+
this._updateOperationTracker(updatedOwners);
|
|
438
|
+
this._processPayloadFollowups(payloadFollowups);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (incrementalResponses.length > 0) {
|
|
442
|
+
const payloadFollowups = this._processIncrementalResponses(
|
|
443
|
+
incrementalResponses,
|
|
444
|
+
);
|
|
445
|
+
// For the incremental case, we're only handling follow-up responses
|
|
446
|
+
// for already initiated operation (and we're not passing it to
|
|
447
|
+
// the run(...) call)
|
|
448
|
+
const updatedOwners = this._publishQueue.run();
|
|
449
|
+
this._updateOperationTracker(updatedOwners);
|
|
450
|
+
this._processPayloadFollowups(payloadFollowups);
|
|
451
|
+
}
|
|
452
|
+
this._sink.next(response);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
_processOptimisticResponse(
|
|
456
|
+
response: ?GraphQLResponseWithData,
|
|
457
|
+
updater: ?SelectorStoreUpdater,
|
|
458
|
+
treatMissingFieldsAsNull: boolean,
|
|
459
|
+
): void {
|
|
460
|
+
invariant(
|
|
461
|
+
this._optimisticUpdates === null,
|
|
462
|
+
'environment.execute: only support one optimistic response per ' +
|
|
463
|
+
'execute.',
|
|
464
|
+
);
|
|
465
|
+
if (response == null && updater == null) {
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
const optimisticUpdates: Array<OptimisticUpdate> = [];
|
|
469
|
+
if (response) {
|
|
470
|
+
const payload = normalizeResponse(
|
|
471
|
+
response,
|
|
472
|
+
this._operation.root,
|
|
473
|
+
ROOT_TYPE,
|
|
474
|
+
{
|
|
475
|
+
getDataID: this._getDataID,
|
|
476
|
+
path: [],
|
|
477
|
+
treatMissingFieldsAsNull,
|
|
478
|
+
},
|
|
479
|
+
);
|
|
480
|
+
validateOptimisticResponsePayload(payload);
|
|
481
|
+
optimisticUpdates.push({
|
|
482
|
+
operation: this._operation,
|
|
483
|
+
payload,
|
|
484
|
+
updater,
|
|
485
|
+
});
|
|
486
|
+
this._processOptimisticFollowups(payload, optimisticUpdates);
|
|
487
|
+
} else if (updater) {
|
|
488
|
+
optimisticUpdates.push({
|
|
489
|
+
operation: this._operation,
|
|
490
|
+
payload: {
|
|
491
|
+
errors: null,
|
|
492
|
+
fieldPayloads: null,
|
|
493
|
+
incrementalPlaceholders: null,
|
|
494
|
+
moduleImportPayloads: null,
|
|
495
|
+
source: RelayRecordSource.create(),
|
|
496
|
+
isFinal: false,
|
|
497
|
+
},
|
|
498
|
+
updater: updater,
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
this._optimisticUpdates = optimisticUpdates;
|
|
502
|
+
optimisticUpdates.forEach(update => this._publishQueue.applyUpdate(update));
|
|
503
|
+
this._publishQueue.run();
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
_processOptimisticFollowups(
|
|
507
|
+
payload: RelayResponsePayload,
|
|
508
|
+
optimisticUpdates: Array<OptimisticUpdate>,
|
|
509
|
+
): void {
|
|
510
|
+
if (payload.moduleImportPayloads && payload.moduleImportPayloads.length) {
|
|
511
|
+
const moduleImportPayloads = payload.moduleImportPayloads;
|
|
512
|
+
const operationLoader = this._operationLoader;
|
|
513
|
+
invariant(
|
|
514
|
+
operationLoader,
|
|
515
|
+
'RelayModernEnvironment: Expected an operationLoader to be ' +
|
|
516
|
+
'configured when using `@match`.',
|
|
517
|
+
);
|
|
518
|
+
for (const moduleImportPayload of moduleImportPayloads) {
|
|
519
|
+
const operation = operationLoader.get(
|
|
520
|
+
moduleImportPayload.operationReference,
|
|
521
|
+
);
|
|
522
|
+
if (operation == null) {
|
|
523
|
+
this._processAsyncOptimisticModuleImport(
|
|
524
|
+
operationLoader,
|
|
525
|
+
moduleImportPayload,
|
|
526
|
+
);
|
|
527
|
+
} else {
|
|
528
|
+
const moduleImportOptimisitcUpdates = this._processOptimisticModuleImport(
|
|
529
|
+
operation,
|
|
530
|
+
moduleImportPayload,
|
|
531
|
+
);
|
|
532
|
+
optimisticUpdates.push(...moduleImportOptimisitcUpdates);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
_normalizeModuleImport(
|
|
539
|
+
moduleImportPayload: ModuleImportPayload,
|
|
540
|
+
operation: NormalizationSelectableNode,
|
|
541
|
+
) {
|
|
542
|
+
const selector = createNormalizationSelector(
|
|
543
|
+
operation,
|
|
544
|
+
moduleImportPayload.dataID,
|
|
545
|
+
moduleImportPayload.variables,
|
|
546
|
+
);
|
|
547
|
+
return normalizeResponse(
|
|
548
|
+
{data: moduleImportPayload.data},
|
|
549
|
+
selector,
|
|
550
|
+
moduleImportPayload.typeName,
|
|
551
|
+
{
|
|
552
|
+
getDataID: this._getDataID,
|
|
553
|
+
path: moduleImportPayload.path,
|
|
554
|
+
treatMissingFieldsAsNull: this._treatMissingFieldsAsNull,
|
|
555
|
+
},
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
_processOptimisticModuleImport(
|
|
560
|
+
operation: NormalizationSplitOperation,
|
|
561
|
+
moduleImportPayload: ModuleImportPayload,
|
|
562
|
+
): $ReadOnlyArray<OptimisticUpdate> {
|
|
563
|
+
const optimisticUpdates = [];
|
|
564
|
+
const modulePayload = this._normalizeModuleImport(
|
|
565
|
+
moduleImportPayload,
|
|
566
|
+
operation,
|
|
567
|
+
);
|
|
568
|
+
validateOptimisticResponsePayload(modulePayload);
|
|
569
|
+
optimisticUpdates.push({
|
|
570
|
+
operation: this._operation,
|
|
571
|
+
payload: modulePayload,
|
|
572
|
+
updater: null,
|
|
573
|
+
});
|
|
574
|
+
this._processOptimisticFollowups(modulePayload, optimisticUpdates);
|
|
575
|
+
return optimisticUpdates;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
_processAsyncOptimisticModuleImport(
|
|
579
|
+
operationLoader: OperationLoader,
|
|
580
|
+
moduleImportPayload: ModuleImportPayload,
|
|
581
|
+
): void {
|
|
582
|
+
operationLoader
|
|
583
|
+
.load(moduleImportPayload.operationReference)
|
|
584
|
+
.then(operation => {
|
|
585
|
+
if (operation == null || this._state !== 'started') {
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
const moduleImportOptimisitcUpdates = this._processOptimisticModuleImport(
|
|
589
|
+
operation,
|
|
590
|
+
moduleImportPayload,
|
|
591
|
+
);
|
|
592
|
+
moduleImportOptimisitcUpdates.forEach(update =>
|
|
593
|
+
this._publishQueue.applyUpdate(update),
|
|
594
|
+
);
|
|
595
|
+
if (this._optimisticUpdates == null) {
|
|
596
|
+
warning(
|
|
597
|
+
false,
|
|
598
|
+
'RelayModernQueryExecutor: Unexpected ModuleImport optimisitc ' +
|
|
599
|
+
'update in operation %s.' +
|
|
600
|
+
this._operation.request.node.params.name,
|
|
601
|
+
);
|
|
602
|
+
} else {
|
|
603
|
+
this._optimisticUpdates.push(...moduleImportOptimisitcUpdates);
|
|
604
|
+
this._publishQueue.run();
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
_processResponses(responses: $ReadOnlyArray<GraphQLResponseWithData>) {
|
|
610
|
+
if (this._optimisticUpdates !== null) {
|
|
611
|
+
this._optimisticUpdates.forEach(update =>
|
|
612
|
+
this._publishQueue.revertUpdate(update),
|
|
613
|
+
);
|
|
614
|
+
this._optimisticUpdates = null;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
this._incrementalPayloadsPending = false;
|
|
618
|
+
this._incrementalResults.clear();
|
|
619
|
+
this._source.clear();
|
|
620
|
+
return responses.map(payloadPart => {
|
|
621
|
+
const relayPayload = normalizeResponse(
|
|
622
|
+
payloadPart,
|
|
623
|
+
this._operation.root,
|
|
624
|
+
ROOT_TYPE,
|
|
625
|
+
{
|
|
626
|
+
getDataID: this._getDataID,
|
|
627
|
+
treatMissingFieldsAsNull: this._treatMissingFieldsAsNull,
|
|
628
|
+
path: [],
|
|
629
|
+
},
|
|
630
|
+
);
|
|
631
|
+
this._publishQueue.commitPayload(
|
|
632
|
+
this._operation,
|
|
633
|
+
relayPayload,
|
|
634
|
+
this._updater,
|
|
635
|
+
);
|
|
636
|
+
return relayPayload;
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Handles any follow-up actions for a Relay payload for @match, @defer,
|
|
642
|
+
* and @stream directives.
|
|
643
|
+
*/
|
|
644
|
+
_processPayloadFollowups(
|
|
645
|
+
payloads: $ReadOnlyArray<RelayResponsePayload>,
|
|
646
|
+
): void {
|
|
647
|
+
if (this._state === 'completed') {
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
payloads.forEach(payload => {
|
|
651
|
+
const {incrementalPlaceholders, moduleImportPayloads, isFinal} = payload;
|
|
652
|
+
this._state = isFinal ? 'loading_final' : 'loading_incremental';
|
|
653
|
+
this._updateActiveState();
|
|
654
|
+
if (isFinal) {
|
|
655
|
+
this._incrementalPayloadsPending = false;
|
|
656
|
+
}
|
|
657
|
+
if (moduleImportPayloads && moduleImportPayloads.length !== 0) {
|
|
658
|
+
const operationLoader = this._operationLoader;
|
|
659
|
+
invariant(
|
|
660
|
+
operationLoader,
|
|
661
|
+
'RelayModernEnvironment: Expected an operationLoader to be ' +
|
|
662
|
+
'configured when using `@match`.',
|
|
663
|
+
);
|
|
664
|
+
moduleImportPayloads.forEach(moduleImportPayload => {
|
|
665
|
+
this._processModuleImportPayload(
|
|
666
|
+
moduleImportPayload,
|
|
667
|
+
operationLoader,
|
|
668
|
+
);
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
if (incrementalPlaceholders && incrementalPlaceholders.length !== 0) {
|
|
672
|
+
this._incrementalPayloadsPending = this._state !== 'loading_final';
|
|
673
|
+
incrementalPlaceholders.forEach(incrementalPlaceholder => {
|
|
674
|
+
this._processIncrementalPlaceholder(payload, incrementalPlaceholder);
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
if (this._isClientPayload || this._state === 'loading_final') {
|
|
678
|
+
// The query has defer/stream selections that are enabled, but either
|
|
679
|
+
// the server indicated that this is a "final" payload: no incremental
|
|
680
|
+
// payloads will be delivered, then warn that the query was (likely)
|
|
681
|
+
// executed on the server in non-streaming mode, with incremental
|
|
682
|
+
// delivery disabled; or this is a client payload, and there will be
|
|
683
|
+
// no incremental payload.
|
|
684
|
+
warning(
|
|
685
|
+
this._isClientPayload,
|
|
686
|
+
'RelayModernEnvironment: Operation `%s` contains @defer/@stream ' +
|
|
687
|
+
'directives but was executed in non-streaming mode. See ' +
|
|
688
|
+
'https://fburl.com/relay-incremental-delivery-non-streaming-warning.',
|
|
689
|
+
this._operation.request.node.params.name,
|
|
690
|
+
);
|
|
691
|
+
// But eagerly process any deferred payloads
|
|
692
|
+
const relayPayloads = [];
|
|
693
|
+
incrementalPlaceholders.forEach(placeholder => {
|
|
694
|
+
if (placeholder.kind === 'defer') {
|
|
695
|
+
relayPayloads.push(
|
|
696
|
+
this._processDeferResponse(
|
|
697
|
+
placeholder.label,
|
|
698
|
+
placeholder.path,
|
|
699
|
+
placeholder,
|
|
700
|
+
{data: placeholder.data},
|
|
701
|
+
),
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
if (relayPayloads.length > 0) {
|
|
706
|
+
const updatedOwners = this._publishQueue.run();
|
|
707
|
+
this._updateOperationTracker(updatedOwners);
|
|
708
|
+
this._processPayloadFollowups(relayPayloads);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
_maybeCompleteSubscriptionOperationTracking() {
|
|
716
|
+
const isSubscriptionOperation =
|
|
717
|
+
this._operation.request.node.params.operationKind === 'subscription';
|
|
718
|
+
if (!isSubscriptionOperation) {
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
if (
|
|
722
|
+
this._pendingModulePayloadsCount === 0 &&
|
|
723
|
+
this._incrementalPayloadsPending === false
|
|
724
|
+
) {
|
|
725
|
+
this._completeOperationTracker();
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Processes a ModuleImportPayload, asynchronously resolving the normalization
|
|
731
|
+
* AST and using it to normalize the field data into a RelayResponsePayload.
|
|
732
|
+
* The resulting payload may contain other incremental payloads (match,
|
|
733
|
+
* defer, stream, etc); these are handled by calling
|
|
734
|
+
* `_processPayloadFollowups()`.
|
|
735
|
+
*/
|
|
736
|
+
_processModuleImportPayload(
|
|
737
|
+
moduleImportPayload: ModuleImportPayload,
|
|
738
|
+
operationLoader: OperationLoader,
|
|
739
|
+
): void {
|
|
740
|
+
const syncOperation = operationLoader.get(
|
|
741
|
+
moduleImportPayload.operationReference,
|
|
742
|
+
);
|
|
743
|
+
if (syncOperation != null) {
|
|
744
|
+
// If the operation module is available synchronously, normalize the
|
|
745
|
+
// data synchronously.
|
|
746
|
+
this._handleModuleImportPayload(moduleImportPayload, syncOperation);
|
|
747
|
+
this._maybeCompleteSubscriptionOperationTracking();
|
|
748
|
+
} else {
|
|
749
|
+
// Otherwise load the operation module and schedule a task to normalize
|
|
750
|
+
// the data when the module is available.
|
|
751
|
+
const id = this._nextSubscriptionId++;
|
|
752
|
+
this._pendingModulePayloadsCount++;
|
|
753
|
+
|
|
754
|
+
const decrementPendingCount = () => {
|
|
755
|
+
this._pendingModulePayloadsCount--;
|
|
756
|
+
this._maybeCompleteSubscriptionOperationTracking();
|
|
757
|
+
};
|
|
758
|
+
|
|
759
|
+
// Observable.from(operationLoader.load()) wouldn't catch synchronous
|
|
760
|
+
// errors thrown by the load function, which is user-defined. Guard
|
|
761
|
+
// against that with Observable.from(new Promise(<work>)).
|
|
762
|
+
RelayObservable.from(
|
|
763
|
+
new Promise((resolve, reject) => {
|
|
764
|
+
operationLoader
|
|
765
|
+
.load(moduleImportPayload.operationReference)
|
|
766
|
+
.then(resolve, reject);
|
|
767
|
+
}),
|
|
768
|
+
)
|
|
769
|
+
.map((operation: ?NormalizationSplitOperation) => {
|
|
770
|
+
if (operation != null) {
|
|
771
|
+
this._schedule(() => {
|
|
772
|
+
this._handleModuleImportPayload(moduleImportPayload, operation);
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
})
|
|
776
|
+
.subscribe({
|
|
777
|
+
complete: () => {
|
|
778
|
+
this._complete(id);
|
|
779
|
+
decrementPendingCount();
|
|
780
|
+
},
|
|
781
|
+
error: error => {
|
|
782
|
+
this._error(error);
|
|
783
|
+
decrementPendingCount();
|
|
784
|
+
},
|
|
785
|
+
start: subscription => this._start(id, subscription),
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
_handleModuleImportPayload(
|
|
791
|
+
moduleImportPayload: ModuleImportPayload,
|
|
792
|
+
operation: NormalizationSplitOperation,
|
|
793
|
+
): void {
|
|
794
|
+
const relayPayload = this._normalizeModuleImport(
|
|
795
|
+
moduleImportPayload,
|
|
796
|
+
operation,
|
|
797
|
+
);
|
|
798
|
+
this._publishQueue.commitPayload(this._operation, relayPayload);
|
|
799
|
+
const updatedOwners = this._publishQueue.run();
|
|
800
|
+
this._updateOperationTracker(updatedOwners);
|
|
801
|
+
this._processPayloadFollowups([relayPayload]);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* The executor now knows that GraphQL responses are expected for a given
|
|
806
|
+
* label/path:
|
|
807
|
+
* - Store the placeholder in order to process any future responses that may
|
|
808
|
+
* arrive.
|
|
809
|
+
* - Then process any responses that had already arrived.
|
|
810
|
+
*
|
|
811
|
+
* The placeholder contains the normalization selector, path (for nested
|
|
812
|
+
* defer/stream), and other metadata used to normalize the incremental
|
|
813
|
+
* response(s).
|
|
814
|
+
*/
|
|
815
|
+
_processIncrementalPlaceholder(
|
|
816
|
+
relayPayload: RelayResponsePayload,
|
|
817
|
+
placeholder: IncrementalDataPlaceholder,
|
|
818
|
+
): void {
|
|
819
|
+
// Update the label => path => placeholder map
|
|
820
|
+
const {label, path} = placeholder;
|
|
821
|
+
const pathKey = path.map(String).join('.');
|
|
822
|
+
let resultForLabel = this._incrementalResults.get(label);
|
|
823
|
+
if (resultForLabel == null) {
|
|
824
|
+
resultForLabel = new Map();
|
|
825
|
+
this._incrementalResults.set(label, resultForLabel);
|
|
826
|
+
}
|
|
827
|
+
const resultForPath = resultForLabel.get(pathKey);
|
|
828
|
+
const pendingResponses =
|
|
829
|
+
resultForPath != null && resultForPath.kind === 'response'
|
|
830
|
+
? resultForPath.responses
|
|
831
|
+
: null;
|
|
832
|
+
resultForLabel.set(pathKey, {kind: 'placeholder', placeholder});
|
|
833
|
+
|
|
834
|
+
// Store references to the parent node to allow detecting concurrent
|
|
835
|
+
// modifications to the parent before items arrive and to replay
|
|
836
|
+
// handle field payloads to account for new information on source records.
|
|
837
|
+
let parentID;
|
|
838
|
+
if (placeholder.kind === 'stream') {
|
|
839
|
+
parentID = placeholder.parentID;
|
|
840
|
+
} else if (placeholder.kind === 'defer') {
|
|
841
|
+
parentID = placeholder.selector.dataID;
|
|
842
|
+
} else {
|
|
843
|
+
(placeholder: empty);
|
|
844
|
+
invariant(
|
|
845
|
+
false,
|
|
846
|
+
'Unsupported incremental placeholder kind `%s`.',
|
|
847
|
+
placeholder.kind,
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
const parentRecord = relayPayload.source.get(parentID);
|
|
851
|
+
const parentPayloads = (relayPayload.fieldPayloads ?? []).filter(
|
|
852
|
+
fieldPayload => {
|
|
853
|
+
const fieldID = generateClientID(
|
|
854
|
+
fieldPayload.dataID,
|
|
855
|
+
fieldPayload.fieldKey,
|
|
856
|
+
);
|
|
857
|
+
return (
|
|
858
|
+
// handlers applied to the streamed field itself
|
|
859
|
+
fieldPayload.dataID === parentID ||
|
|
860
|
+
// handlers applied to a field on an ancestor object, where
|
|
861
|
+
// ancestor.field links to the parent record (example: connections)
|
|
862
|
+
fieldID === parentID
|
|
863
|
+
);
|
|
864
|
+
},
|
|
865
|
+
);
|
|
866
|
+
// If an incremental payload exists for some id that record should also
|
|
867
|
+
// exist.
|
|
868
|
+
invariant(
|
|
869
|
+
parentRecord != null,
|
|
870
|
+
'RelayModernEnvironment: Expected record `%s` to exist.',
|
|
871
|
+
parentID,
|
|
872
|
+
);
|
|
873
|
+
let nextParentRecord;
|
|
874
|
+
let nextParentPayloads;
|
|
875
|
+
const previousParentEntry = this._source.get(parentID);
|
|
876
|
+
if (previousParentEntry != null) {
|
|
877
|
+
// If a previous entry exists, merge the previous/next records and
|
|
878
|
+
// payloads together.
|
|
879
|
+
nextParentRecord = RelayModernRecord.update(
|
|
880
|
+
previousParentEntry.record,
|
|
881
|
+
parentRecord,
|
|
882
|
+
);
|
|
883
|
+
const handlePayloads = new Map();
|
|
884
|
+
const dedupePayload = payload => {
|
|
885
|
+
const key = stableStringify(payload);
|
|
886
|
+
handlePayloads.set(key, payload);
|
|
887
|
+
};
|
|
888
|
+
previousParentEntry.fieldPayloads.forEach(dedupePayload);
|
|
889
|
+
parentPayloads.forEach(dedupePayload);
|
|
890
|
+
nextParentPayloads = Array.from(handlePayloads.values());
|
|
891
|
+
} else {
|
|
892
|
+
nextParentRecord = parentRecord;
|
|
893
|
+
nextParentPayloads = parentPayloads;
|
|
894
|
+
}
|
|
895
|
+
this._source.set(parentID, {
|
|
896
|
+
record: nextParentRecord,
|
|
897
|
+
fieldPayloads: nextParentPayloads,
|
|
898
|
+
});
|
|
899
|
+
// If there were any queued responses, process them now that placeholders
|
|
900
|
+
// are in place
|
|
901
|
+
if (pendingResponses != null) {
|
|
902
|
+
const payloadFollowups = this._processIncrementalResponses(
|
|
903
|
+
pendingResponses,
|
|
904
|
+
);
|
|
905
|
+
const updatedOwners = this._publishQueue.run();
|
|
906
|
+
this._updateOperationTracker(updatedOwners);
|
|
907
|
+
this._processPayloadFollowups(payloadFollowups);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/**
|
|
912
|
+
* Lookup the placeholder the describes how to process an incremental
|
|
913
|
+
* response, normalize/publish it, and process any nested defer/match/stream
|
|
914
|
+
* metadata.
|
|
915
|
+
*/
|
|
916
|
+
_processIncrementalResponses(
|
|
917
|
+
incrementalResponses: $ReadOnlyArray<IncrementalGraphQLResponse>,
|
|
918
|
+
): $ReadOnlyArray<RelayResponsePayload> {
|
|
919
|
+
const relayPayloads = [];
|
|
920
|
+
incrementalResponses.forEach(incrementalResponse => {
|
|
921
|
+
const {label, path, response} = incrementalResponse;
|
|
922
|
+
let resultForLabel = this._incrementalResults.get(label);
|
|
923
|
+
if (resultForLabel == null) {
|
|
924
|
+
resultForLabel = new Map();
|
|
925
|
+
this._incrementalResults.set(label, resultForLabel);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
if (label.indexOf('$defer$') !== -1) {
|
|
929
|
+
const pathKey = path.map(String).join('.');
|
|
930
|
+
let resultForPath = resultForLabel.get(pathKey);
|
|
931
|
+
if (resultForPath == null) {
|
|
932
|
+
resultForPath = {kind: 'response', responses: [incrementalResponse]};
|
|
933
|
+
resultForLabel.set(pathKey, resultForPath);
|
|
934
|
+
return;
|
|
935
|
+
} else if (resultForPath.kind === 'response') {
|
|
936
|
+
resultForPath.responses.push(incrementalResponse);
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
const placeholder = resultForPath.placeholder;
|
|
940
|
+
invariant(
|
|
941
|
+
placeholder.kind === 'defer',
|
|
942
|
+
'RelayModernEnvironment: Expected data for path `%s` for label `%s` ' +
|
|
943
|
+
'to be data for @defer, was `@%s`.',
|
|
944
|
+
pathKey,
|
|
945
|
+
label,
|
|
946
|
+
placeholder.kind,
|
|
947
|
+
);
|
|
948
|
+
relayPayloads.push(
|
|
949
|
+
this._processDeferResponse(label, path, placeholder, response),
|
|
950
|
+
);
|
|
951
|
+
} else {
|
|
952
|
+
// @stream payload path values end in the field name and item index,
|
|
953
|
+
// but Relay records paths relative to the parent of the stream node:
|
|
954
|
+
// therefore we strip the last two elements just to lookup the path
|
|
955
|
+
// (the item index is used later to insert the element in the list)
|
|
956
|
+
const pathKey = path
|
|
957
|
+
.slice(0, -2)
|
|
958
|
+
.map(String)
|
|
959
|
+
.join('.');
|
|
960
|
+
let resultForPath = resultForLabel.get(pathKey);
|
|
961
|
+
if (resultForPath == null) {
|
|
962
|
+
resultForPath = {kind: 'response', responses: [incrementalResponse]};
|
|
963
|
+
resultForLabel.set(pathKey, resultForPath);
|
|
964
|
+
return;
|
|
965
|
+
} else if (resultForPath.kind === 'response') {
|
|
966
|
+
resultForPath.responses.push(incrementalResponse);
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
const placeholder = resultForPath.placeholder;
|
|
970
|
+
invariant(
|
|
971
|
+
placeholder.kind === 'stream',
|
|
972
|
+
'RelayModernEnvironment: Expected data for path `%s` for label `%s` ' +
|
|
973
|
+
'to be data for @stream, was `@%s`.',
|
|
974
|
+
pathKey,
|
|
975
|
+
label,
|
|
976
|
+
placeholder.kind,
|
|
977
|
+
);
|
|
978
|
+
relayPayloads.push(
|
|
979
|
+
this._processStreamResponse(label, path, placeholder, response),
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
});
|
|
983
|
+
return relayPayloads;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
_processDeferResponse(
|
|
987
|
+
label: string,
|
|
988
|
+
path: $ReadOnlyArray<mixed>,
|
|
989
|
+
placeholder: DeferPlaceholder,
|
|
990
|
+
response: GraphQLResponseWithData,
|
|
991
|
+
): RelayResponsePayload {
|
|
992
|
+
const {dataID: parentID} = placeholder.selector;
|
|
993
|
+
const relayPayload = normalizeResponse(
|
|
994
|
+
response,
|
|
995
|
+
placeholder.selector,
|
|
996
|
+
placeholder.typeName,
|
|
997
|
+
{
|
|
998
|
+
getDataID: this._getDataID,
|
|
999
|
+
path: placeholder.path,
|
|
1000
|
+
treatMissingFieldsAsNull: this._treatMissingFieldsAsNull,
|
|
1001
|
+
},
|
|
1002
|
+
);
|
|
1003
|
+
this._publishQueue.commitPayload(this._operation, relayPayload);
|
|
1004
|
+
|
|
1005
|
+
// Load the version of the parent record from which this incremental data
|
|
1006
|
+
// was derived
|
|
1007
|
+
const parentEntry = this._source.get(parentID);
|
|
1008
|
+
invariant(
|
|
1009
|
+
parentEntry != null,
|
|
1010
|
+
'RelayModernEnvironment: Expected the parent record `%s` for @defer ' +
|
|
1011
|
+
'data to exist.',
|
|
1012
|
+
parentID,
|
|
1013
|
+
);
|
|
1014
|
+
const {fieldPayloads} = parentEntry;
|
|
1015
|
+
if (fieldPayloads.length !== 0) {
|
|
1016
|
+
const handleFieldsRelayPayload = {
|
|
1017
|
+
errors: null,
|
|
1018
|
+
fieldPayloads,
|
|
1019
|
+
incrementalPlaceholders: null,
|
|
1020
|
+
moduleImportPayloads: null,
|
|
1021
|
+
source: RelayRecordSource.create(),
|
|
1022
|
+
isFinal: response.extensions?.is_final === true,
|
|
1023
|
+
};
|
|
1024
|
+
this._publishQueue.commitPayload(
|
|
1025
|
+
this._operation,
|
|
1026
|
+
handleFieldsRelayPayload,
|
|
1027
|
+
);
|
|
1028
|
+
}
|
|
1029
|
+
return relayPayload;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
/**
|
|
1033
|
+
* Process the data for one item in a @stream field.
|
|
1034
|
+
*/
|
|
1035
|
+
_processStreamResponse(
|
|
1036
|
+
label: string,
|
|
1037
|
+
path: $ReadOnlyArray<mixed>,
|
|
1038
|
+
placeholder: StreamPlaceholder,
|
|
1039
|
+
response: GraphQLResponseWithData,
|
|
1040
|
+
): RelayResponsePayload {
|
|
1041
|
+
const {parentID, node, variables} = placeholder;
|
|
1042
|
+
// Find the LinkedField where @stream was applied
|
|
1043
|
+
const field = node.selections[0];
|
|
1044
|
+
invariant(
|
|
1045
|
+
field != null && field.kind === 'LinkedField' && field.plural === true,
|
|
1046
|
+
'RelayModernEnvironment: Expected @stream to be used on a plural field.',
|
|
1047
|
+
);
|
|
1048
|
+
const {
|
|
1049
|
+
fieldPayloads,
|
|
1050
|
+
itemID,
|
|
1051
|
+
itemIndex,
|
|
1052
|
+
prevIDs,
|
|
1053
|
+
relayPayload,
|
|
1054
|
+
storageKey,
|
|
1055
|
+
} = this._normalizeStreamItem(
|
|
1056
|
+
response,
|
|
1057
|
+
parentID,
|
|
1058
|
+
field,
|
|
1059
|
+
variables,
|
|
1060
|
+
path,
|
|
1061
|
+
placeholder.path,
|
|
1062
|
+
);
|
|
1063
|
+
// Publish the new item and update the parent record to set
|
|
1064
|
+
// field[index] = item *if* the parent record hasn't been concurrently
|
|
1065
|
+
// modified.
|
|
1066
|
+
this._publishQueue.commitPayload(this._operation, relayPayload, store => {
|
|
1067
|
+
const currentParentRecord = store.get(parentID);
|
|
1068
|
+
if (currentParentRecord == null) {
|
|
1069
|
+
// parent has since been deleted, stream data is stale
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
const currentItems = currentParentRecord.getLinkedRecords(storageKey);
|
|
1073
|
+
if (currentItems == null) {
|
|
1074
|
+
// field has since been deleted, stream data is stale
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
if (
|
|
1078
|
+
currentItems.length !== prevIDs.length ||
|
|
1079
|
+
currentItems.some(
|
|
1080
|
+
(currentItem, index) =>
|
|
1081
|
+
prevIDs[index] !== (currentItem && currentItem.getDataID()),
|
|
1082
|
+
)
|
|
1083
|
+
) {
|
|
1084
|
+
// field has been modified by something other than this query,
|
|
1085
|
+
// stream data is stale
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
// parent.field has not been concurrently modified:
|
|
1089
|
+
// update `parent.field[index] = item`
|
|
1090
|
+
const nextItems = [...currentItems];
|
|
1091
|
+
nextItems[itemIndex] = store.get(itemID);
|
|
1092
|
+
currentParentRecord.setLinkedRecords(nextItems, storageKey);
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
// Now that the parent record has been updated to include the new item,
|
|
1096
|
+
// also update any handle fields that are derived from the parent record.
|
|
1097
|
+
if (fieldPayloads.length !== 0) {
|
|
1098
|
+
const handleFieldsRelayPayload = {
|
|
1099
|
+
errors: null,
|
|
1100
|
+
fieldPayloads,
|
|
1101
|
+
incrementalPlaceholders: null,
|
|
1102
|
+
moduleImportPayloads: null,
|
|
1103
|
+
source: RelayRecordSource.create(),
|
|
1104
|
+
isFinal: false,
|
|
1105
|
+
};
|
|
1106
|
+
this._publishQueue.commitPayload(
|
|
1107
|
+
this._operation,
|
|
1108
|
+
handleFieldsRelayPayload,
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
return relayPayload;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
_normalizeStreamItem(
|
|
1115
|
+
response: GraphQLResponseWithData,
|
|
1116
|
+
parentID: DataID,
|
|
1117
|
+
field: NormalizationLinkedField,
|
|
1118
|
+
variables: Variables,
|
|
1119
|
+
path: $ReadOnlyArray<mixed>,
|
|
1120
|
+
normalizationPath: $ReadOnlyArray<string>,
|
|
1121
|
+
): {|
|
|
1122
|
+
fieldPayloads: Array<HandleFieldPayload>,
|
|
1123
|
+
itemID: DataID,
|
|
1124
|
+
itemIndex: number,
|
|
1125
|
+
prevIDs: Array<?DataID>,
|
|
1126
|
+
relayPayload: RelayResponsePayload,
|
|
1127
|
+
storageKey: string,
|
|
1128
|
+
|} {
|
|
1129
|
+
const {data} = response;
|
|
1130
|
+
invariant(
|
|
1131
|
+
typeof data === 'object',
|
|
1132
|
+
'RelayModernEnvironment: Expected the GraphQL @stream payload `data` ' +
|
|
1133
|
+
'value to be an object.',
|
|
1134
|
+
);
|
|
1135
|
+
const responseKey = field.alias ?? field.name;
|
|
1136
|
+
const storageKey = getStorageKey(field, variables);
|
|
1137
|
+
|
|
1138
|
+
// Load the version of the parent record from which this incremental data
|
|
1139
|
+
// was derived
|
|
1140
|
+
const parentEntry = this._source.get(parentID);
|
|
1141
|
+
invariant(
|
|
1142
|
+
parentEntry != null,
|
|
1143
|
+
'RelayModernEnvironment: Expected the parent record `%s` for @stream ' +
|
|
1144
|
+
'data to exist.',
|
|
1145
|
+
parentID,
|
|
1146
|
+
);
|
|
1147
|
+
const {record: parentRecord, fieldPayloads} = parentEntry;
|
|
1148
|
+
|
|
1149
|
+
// Load the field value (items) that were created by *this* query executor
|
|
1150
|
+
// in order to check if there has been any concurrent modifications by some
|
|
1151
|
+
// other operation.
|
|
1152
|
+
const prevIDs = RelayModernRecord.getLinkedRecordIDs(
|
|
1153
|
+
parentRecord,
|
|
1154
|
+
storageKey,
|
|
1155
|
+
);
|
|
1156
|
+
invariant(
|
|
1157
|
+
prevIDs != null,
|
|
1158
|
+
'RelayModernEnvironment: Expected record `%s` to have fetched field ' +
|
|
1159
|
+
'`%s` with @stream.',
|
|
1160
|
+
parentID,
|
|
1161
|
+
field.name,
|
|
1162
|
+
);
|
|
1163
|
+
|
|
1164
|
+
// Determine the index in the field of the new item
|
|
1165
|
+
const finalPathEntry = path[path.length - 1];
|
|
1166
|
+
const itemIndex = parseInt(finalPathEntry, 10);
|
|
1167
|
+
invariant(
|
|
1168
|
+
itemIndex === finalPathEntry && itemIndex >= 0,
|
|
1169
|
+
'RelayModernEnvironment: Expected path for @stream to end in a ' +
|
|
1170
|
+
'positive integer index, got `%s`',
|
|
1171
|
+
finalPathEntry,
|
|
1172
|
+
);
|
|
1173
|
+
|
|
1174
|
+
const typeName = field.concreteType ?? data[TYPENAME_KEY];
|
|
1175
|
+
invariant(
|
|
1176
|
+
typeof typeName === 'string',
|
|
1177
|
+
'RelayModernEnvironment: Expected @stream field `%s` to have a ' +
|
|
1178
|
+
'__typename.',
|
|
1179
|
+
field.name,
|
|
1180
|
+
);
|
|
1181
|
+
|
|
1182
|
+
// Determine the __id of the new item: this must equal the value that would
|
|
1183
|
+
// be assigned had the item not been streamed
|
|
1184
|
+
const itemID =
|
|
1185
|
+
// https://github.com/prettier/prettier/issues/6403
|
|
1186
|
+
// prettier-ignore
|
|
1187
|
+
(this._getDataID(data, typeName) ??
|
|
1188
|
+
(prevIDs && prevIDs[itemIndex])) || // Reuse previously generated client IDs
|
|
1189
|
+
generateClientID(parentID, storageKey, itemIndex);
|
|
1190
|
+
invariant(
|
|
1191
|
+
typeof itemID === 'string',
|
|
1192
|
+
'RelayModernEnvironment: Expected id of elements of field `%s` to ' +
|
|
1193
|
+
'be strings.',
|
|
1194
|
+
storageKey,
|
|
1195
|
+
);
|
|
1196
|
+
|
|
1197
|
+
// Build a selector to normalize the item data with
|
|
1198
|
+
const selector = createNormalizationSelector(field, itemID, variables);
|
|
1199
|
+
|
|
1200
|
+
// Update the cached version of the parent record to reflect the new item:
|
|
1201
|
+
// this is used when subsequent stream payloads arrive to see if there
|
|
1202
|
+
// have been concurrent modifications to the list
|
|
1203
|
+
const nextParentRecord = RelayModernRecord.clone(parentRecord);
|
|
1204
|
+
const nextIDs = [...prevIDs];
|
|
1205
|
+
nextIDs[itemIndex] = itemID;
|
|
1206
|
+
RelayModernRecord.setLinkedRecordIDs(nextParentRecord, storageKey, nextIDs);
|
|
1207
|
+
this._source.set(parentID, {
|
|
1208
|
+
record: nextParentRecord,
|
|
1209
|
+
fieldPayloads,
|
|
1210
|
+
});
|
|
1211
|
+
const relayPayload = normalizeResponse(response, selector, typeName, {
|
|
1212
|
+
getDataID: this._getDataID,
|
|
1213
|
+
path: [...normalizationPath, responseKey, String(itemIndex)],
|
|
1214
|
+
treatMissingFieldsAsNull: this._treatMissingFieldsAsNull,
|
|
1215
|
+
});
|
|
1216
|
+
return {
|
|
1217
|
+
fieldPayloads,
|
|
1218
|
+
itemID,
|
|
1219
|
+
itemIndex,
|
|
1220
|
+
prevIDs,
|
|
1221
|
+
relayPayload,
|
|
1222
|
+
storageKey,
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
_updateOperationTracker(
|
|
1227
|
+
updatedOwners: ?$ReadOnlyArray<RequestDescriptor>,
|
|
1228
|
+
): void {
|
|
1229
|
+
if (
|
|
1230
|
+
this._operationTracker != null &&
|
|
1231
|
+
updatedOwners != null &&
|
|
1232
|
+
updatedOwners.length > 0
|
|
1233
|
+
) {
|
|
1234
|
+
this._operationTracker.update(
|
|
1235
|
+
this._operation.request,
|
|
1236
|
+
new Set(updatedOwners),
|
|
1237
|
+
);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
_completeOperationTracker() {
|
|
1242
|
+
if (this._operationTracker != null) {
|
|
1243
|
+
this._operationTracker.complete(this._operation.request);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
function partitionGraphQLResponses(
|
|
1249
|
+
responses: $ReadOnlyArray<GraphQLResponseWithData>,
|
|
1250
|
+
): [
|
|
1251
|
+
$ReadOnlyArray<GraphQLResponseWithData>,
|
|
1252
|
+
$ReadOnlyArray<IncrementalGraphQLResponse>,
|
|
1253
|
+
] {
|
|
1254
|
+
const nonIncrementalResponses: Array<GraphQLResponseWithData> = [];
|
|
1255
|
+
const incrementalResponses: Array<IncrementalGraphQLResponse> = [];
|
|
1256
|
+
responses.forEach(response => {
|
|
1257
|
+
if (response.path != null || response.label != null) {
|
|
1258
|
+
const {label, path} = response;
|
|
1259
|
+
if (label == null || path == null) {
|
|
1260
|
+
invariant(
|
|
1261
|
+
false,
|
|
1262
|
+
'RelayModernQueryExecutor: invalid incremental payload, expected ' +
|
|
1263
|
+
'`path` and `label` to either both be null/undefined, or ' +
|
|
1264
|
+
'`path` to be an `Array<string | number>` and `label` to be a ' +
|
|
1265
|
+
'`string`.',
|
|
1266
|
+
);
|
|
1267
|
+
}
|
|
1268
|
+
incrementalResponses.push({
|
|
1269
|
+
label,
|
|
1270
|
+
path,
|
|
1271
|
+
response,
|
|
1272
|
+
});
|
|
1273
|
+
} else {
|
|
1274
|
+
nonIncrementalResponses.push(response);
|
|
1275
|
+
}
|
|
1276
|
+
});
|
|
1277
|
+
return [nonIncrementalResponses, incrementalResponses];
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
function normalizeResponse(
|
|
1281
|
+
response: GraphQLResponseWithData,
|
|
1282
|
+
selector: NormalizationSelector,
|
|
1283
|
+
typeName: string,
|
|
1284
|
+
options: NormalizationOptions,
|
|
1285
|
+
): RelayResponsePayload {
|
|
1286
|
+
const {data, errors} = response;
|
|
1287
|
+
const source = RelayRecordSource.create();
|
|
1288
|
+
const record = RelayModernRecord.create(selector.dataID, typeName);
|
|
1289
|
+
source.set(selector.dataID, record);
|
|
1290
|
+
const relayPayload = RelayResponseNormalizer.normalize(
|
|
1291
|
+
source,
|
|
1292
|
+
selector,
|
|
1293
|
+
data,
|
|
1294
|
+
options,
|
|
1295
|
+
);
|
|
1296
|
+
return {
|
|
1297
|
+
...relayPayload,
|
|
1298
|
+
errors,
|
|
1299
|
+
isFinal: response.extensions?.is_final === true,
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
function stableStringify(value: mixed): string {
|
|
1304
|
+
return JSON.stringify(stableCopy(value)) ?? ''; // null-check for flow
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
function validateOptimisticResponsePayload(
|
|
1308
|
+
payload: RelayResponsePayload,
|
|
1309
|
+
): void {
|
|
1310
|
+
const {incrementalPlaceholders} = payload;
|
|
1311
|
+
if (incrementalPlaceholders != null && incrementalPlaceholders.length !== 0) {
|
|
1312
|
+
invariant(
|
|
1313
|
+
false,
|
|
1314
|
+
'RelayModernQueryExecutor: optimistic responses cannot be returned ' +
|
|
1315
|
+
'for operations that use incremental data delivery (@defer, ' +
|
|
1316
|
+
'@stream, and @stream_connection).',
|
|
1317
|
+
);
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
module.exports = {execute};
|