relay-runtime 9.0.0 → 10.1.0

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