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.
Files changed (135) hide show
  1. package/handlers/RelayDefaultHandlerProvider.js.flow +41 -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 +88 -0
  5. package/index.js +1 -1
  6. package/index.js.flow +320 -0
  7. package/lib/handlers/RelayDefaultHandlerProvider.js +13 -2
  8. package/lib/handlers/connection/{RelayConnectionHandler.js → ConnectionHandler.js} +33 -35
  9. package/lib/handlers/connection/{RelayConnectionInterface.js → ConnectionInterface.js} +2 -2
  10. package/lib/handlers/connection/MutationHandlers.js +86 -0
  11. package/lib/index.js +15 -19
  12. package/lib/mutations/RelayDeclarativeMutationConfig.js +29 -52
  13. package/lib/mutations/RelayRecordProxy.js +1 -3
  14. package/lib/mutations/RelayRecordSourceMutator.js +2 -9
  15. package/lib/mutations/RelayRecordSourceProxy.js +2 -4
  16. package/lib/mutations/RelayRecordSourceSelectorProxy.js +1 -13
  17. package/lib/mutations/commitMutation.js +13 -3
  18. package/lib/mutations/validateMutation.js +16 -9
  19. package/lib/network/RelayObservable.js +9 -9
  20. package/lib/network/RelayQueryResponseCache.js +8 -6
  21. package/lib/query/PreloadableQueryRegistry.js +70 -0
  22. package/lib/query/fetchQueryInternal.js +31 -23
  23. package/lib/store/DataChecker.js +122 -110
  24. package/lib/store/RelayConcreteVariables.js +6 -2
  25. package/lib/store/RelayModernEnvironment.js +121 -67
  26. package/lib/store/RelayModernFragmentSpecResolver.js +12 -16
  27. package/lib/store/RelayModernQueryExecutor.js +389 -314
  28. package/lib/store/RelayModernRecord.js +14 -9
  29. package/lib/store/RelayModernSelector.js +7 -3
  30. package/lib/store/RelayModernStore.js +289 -484
  31. package/lib/store/RelayOperationTracker.js +35 -78
  32. package/lib/store/RelayOptimisticRecordSource.js +7 -5
  33. package/lib/store/RelayPublishQueue.js +6 -33
  34. package/lib/store/RelayReader.js +113 -45
  35. package/lib/store/RelayRecordSource.js +2 -9
  36. package/lib/store/RelayRecordSourceMapImpl.js +13 -18
  37. package/lib/store/RelayReferenceMarker.js +40 -60
  38. package/lib/store/RelayResponseNormalizer.js +158 -193
  39. package/lib/store/RelayStoreUtils.js +1 -0
  40. package/lib/store/StoreInspector.js +8 -8
  41. package/lib/store/TypeID.js +28 -0
  42. package/lib/store/cloneRelayScalarHandleSourceField.js +44 -0
  43. package/lib/store/normalizeRelayPayload.js +6 -2
  44. package/lib/store/readInlineData.js +1 -1
  45. package/lib/subscription/requestSubscription.js +5 -3
  46. package/lib/util/RelayConcreteNode.js +9 -6
  47. package/lib/util/RelayError.js +39 -9
  48. package/lib/util/RelayFeatureFlags.js +2 -5
  49. package/lib/util/RelayReplaySubject.js +3 -3
  50. package/lib/util/createPayloadFor3DField.js +7 -2
  51. package/lib/util/getRequestIdentifier.js +2 -2
  52. package/lib/util/recycleNodesInto.js +2 -6
  53. package/mutations/RelayDeclarativeMutationConfig.js.flow +380 -0
  54. package/mutations/RelayRecordProxy.js.flow +165 -0
  55. package/mutations/RelayRecordSourceMutator.js.flow +238 -0
  56. package/mutations/RelayRecordSourceProxy.js.flow +164 -0
  57. package/mutations/RelayRecordSourceSelectorProxy.js.flow +119 -0
  58. package/mutations/applyOptimisticMutation.js.flow +76 -0
  59. package/mutations/commitLocalUpdate.js.flow +24 -0
  60. package/mutations/commitMutation.js.flow +182 -0
  61. package/mutations/validateMutation.js.flow +213 -0
  62. package/network/ConvertToExecuteFunction.js.flow +49 -0
  63. package/network/RelayNetwork.js.flow +84 -0
  64. package/network/RelayNetworkTypes.js.flow +123 -0
  65. package/network/RelayObservable.js.flow +634 -0
  66. package/network/RelayQueryResponseCache.js.flow +111 -0
  67. package/package.json +1 -1
  68. package/query/GraphQLTag.js.flow +166 -0
  69. package/query/PreloadableQueryRegistry.js.flow +65 -0
  70. package/query/fetchQuery.js.flow +47 -0
  71. package/query/fetchQueryInternal.js.flow +348 -0
  72. package/relay-runtime.js +2 -2
  73. package/relay-runtime.min.js +2 -2
  74. package/store/ClientID.js.flow +43 -0
  75. package/store/DataChecker.js.flow +502 -0
  76. package/store/RelayConcreteVariables.js.flow +96 -0
  77. package/store/RelayModernEnvironment.js.flow +551 -0
  78. package/store/RelayModernFragmentSpecResolver.js.flow +426 -0
  79. package/store/RelayModernOperationDescriptor.js.flow +88 -0
  80. package/store/RelayModernQueryExecutor.js.flow +1321 -0
  81. package/store/RelayModernRecord.js.flow +403 -0
  82. package/store/RelayModernSelector.js.flow +455 -0
  83. package/store/RelayModernStore.js.flow +842 -0
  84. package/store/RelayOperationTracker.js.flow +164 -0
  85. package/store/RelayOptimisticRecordSource.js.flow +119 -0
  86. package/store/RelayPublishQueue.js.flow +401 -0
  87. package/store/RelayReader.js.flow +473 -0
  88. package/store/RelayRecordSource.js.flow +29 -0
  89. package/store/RelayRecordSourceMapImpl.js.flow +87 -0
  90. package/store/RelayRecordState.js.flow +37 -0
  91. package/store/RelayReferenceMarker.js.flow +257 -0
  92. package/store/RelayResponseNormalizer.js.flow +680 -0
  93. package/store/RelayStoreTypes.js.flow +899 -0
  94. package/store/RelayStoreUtils.js.flow +219 -0
  95. package/store/StoreInspector.js.flow +171 -0
  96. package/store/TypeID.js.flow +28 -0
  97. package/store/ViewerPattern.js.flow +26 -0
  98. package/store/cloneRelayHandleSourceField.js.flow +66 -0
  99. package/store/cloneRelayScalarHandleSourceField.js.flow +62 -0
  100. package/store/createFragmentSpecResolver.js.flow +55 -0
  101. package/store/createRelayContext.js.flow +44 -0
  102. package/store/defaultGetDataID.js.flow +27 -0
  103. package/store/hasOverlappingIDs.js.flow +34 -0
  104. package/store/isRelayModernEnvironment.js.flow +27 -0
  105. package/store/normalizeRelayPayload.js.flow +51 -0
  106. package/store/readInlineData.js.flow +75 -0
  107. package/subscription/requestSubscription.js.flow +100 -0
  108. package/util/JSResourceTypes.flow.js.flow +20 -0
  109. package/util/NormalizationNode.js.flow +198 -0
  110. package/util/ReaderNode.js.flow +208 -0
  111. package/util/RelayConcreteNode.js.flow +93 -0
  112. package/util/RelayDefaultHandleKey.js.flow +17 -0
  113. package/util/RelayError.js.flow +62 -0
  114. package/util/RelayFeatureFlags.js.flow +30 -0
  115. package/util/RelayProfiler.js.flow +284 -0
  116. package/util/RelayReplaySubject.js.flow +135 -0
  117. package/util/RelayRuntimeTypes.js.flow +72 -0
  118. package/util/createPayloadFor3DField.js.flow +43 -0
  119. package/util/deepFreeze.js.flow +36 -0
  120. package/util/generateID.js.flow +21 -0
  121. package/util/getFragmentIdentifier.js.flow +52 -0
  122. package/util/getRelayHandleKey.js.flow +41 -0
  123. package/util/getRequestIdentifier.js.flow +42 -0
  124. package/util/isPromise.js.flow +21 -0
  125. package/util/isScalarAndEqual.js.flow +26 -0
  126. package/util/recycleNodesInto.js.flow +76 -0
  127. package/util/resolveImmediate.js.flow +30 -0
  128. package/util/stableCopy.js.flow +35 -0
  129. package/lib/handlers/RelayDefaultMissingFieldHandlers.js +0 -26
  130. package/lib/handlers/getRelayDefaultMissingFieldHandlers.js +0 -36
  131. package/lib/query/RelayModernGraphQLTag.js +0 -104
  132. package/lib/store/RelayConnection.js +0 -37
  133. package/lib/store/RelayConnectionResolver.js +0 -178
  134. package/lib/store/RelayRecordSourceObjectImpl.js +0 -79
  135. 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};