relay-runtime 11.0.2 → 12.0.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 (100) hide show
  1. package/index.js +1 -1
  2. package/index.js.flow +16 -1
  3. package/lib/index.js +15 -0
  4. package/lib/multi-actor-environment/ActorIdentifier.js +11 -1
  5. package/lib/multi-actor-environment/ActorSpecificEnvironment.js +59 -19
  6. package/lib/multi-actor-environment/ActorUtils.js +27 -0
  7. package/lib/multi-actor-environment/MultiActorEnvironment.js +305 -55
  8. package/lib/multi-actor-environment/index.js +5 -1
  9. package/lib/mutations/RelayRecordSourceSelectorProxy.js +6 -1
  10. package/lib/mutations/commitMutation.js +4 -1
  11. package/lib/mutations/validateMutation.js +6 -1
  12. package/lib/network/RelayObservable.js +3 -1
  13. package/lib/network/RelayQueryResponseCache.js +19 -3
  14. package/lib/network/wrapNetworkWithLogObserver.js +78 -0
  15. package/lib/store/DataChecker.js +110 -40
  16. package/lib/store/OperationExecutor.js +478 -204
  17. package/lib/store/RelayConcreteVariables.js +21 -0
  18. package/lib/store/RelayModernEnvironment.js +41 -85
  19. package/lib/store/RelayModernFragmentSpecResolver.js +48 -22
  20. package/lib/store/RelayModernRecord.js +35 -1
  21. package/lib/store/RelayModernStore.js +48 -14
  22. package/lib/store/RelayOperationTracker.js +33 -23
  23. package/lib/store/RelayPublishQueue.js +23 -5
  24. package/lib/store/RelayReader.js +138 -44
  25. package/lib/store/RelayRecordSource.js +87 -3
  26. package/lib/store/RelayReferenceMarker.js +28 -15
  27. package/lib/store/RelayResponseNormalizer.js +164 -91
  28. package/lib/store/RelayStoreReactFlightUtils.js +1 -7
  29. package/lib/store/RelayStoreSubscriptions.js +8 -5
  30. package/lib/store/RelayStoreUtils.js +7 -2
  31. package/lib/store/ResolverCache.js +213 -0
  32. package/lib/store/ResolverFragments.js +1 -1
  33. package/lib/store/createRelayContext.js +1 -1
  34. package/lib/subscription/requestSubscription.js +27 -29
  35. package/lib/util/RelayConcreteNode.js +1 -0
  36. package/lib/util/RelayFeatureFlags.js +3 -5
  37. package/lib/util/RelayReplaySubject.js +21 -6
  38. package/lib/util/getPaginationMetadata.js +41 -0
  39. package/lib/util/getPaginationVariables.js +67 -0
  40. package/lib/util/getPendingOperationsForFragment.js +55 -0
  41. package/lib/util/getRefetchMetadata.js +36 -0
  42. package/lib/util/getValueAtPath.js +51 -0
  43. package/lib/util/isEmptyObject.js +1 -1
  44. package/lib/util/registerEnvironmentWithDevTools.js +26 -0
  45. package/lib/util/withDuration.js +31 -0
  46. package/multi-actor-environment/ActorIdentifier.js.flow +17 -1
  47. package/multi-actor-environment/ActorSpecificEnvironment.js.flow +72 -44
  48. package/multi-actor-environment/ActorUtils.js.flow +33 -0
  49. package/multi-actor-environment/MultiActorEnvironment.js.flow +332 -80
  50. package/multi-actor-environment/MultiActorEnvironmentTypes.js.flow +61 -12
  51. package/multi-actor-environment/index.js.flow +3 -0
  52. package/mutations/RelayRecordSourceSelectorProxy.js.flow +7 -2
  53. package/mutations/commitMutation.js.flow +2 -0
  54. package/mutations/validateMutation.js.flow +8 -0
  55. package/network/RelayObservable.js.flow +2 -0
  56. package/network/RelayQueryResponseCache.js.flow +31 -18
  57. package/network/wrapNetworkWithLogObserver.js.flow +99 -0
  58. package/package.json +1 -1
  59. package/relay-runtime.js +2 -2
  60. package/relay-runtime.min.js +2 -2
  61. package/store/ClientID.js.flow +5 -1
  62. package/store/DataChecker.js.flow +126 -35
  63. package/store/OperationExecutor.js.flow +528 -265
  64. package/store/RelayConcreteVariables.js.flow +26 -1
  65. package/store/RelayModernEnvironment.js.flow +41 -94
  66. package/store/RelayModernFragmentSpecResolver.js.flow +40 -14
  67. package/store/RelayModernOperationDescriptor.js.flow +9 -3
  68. package/store/RelayModernRecord.js.flow +49 -0
  69. package/store/RelayModernStore.js.flow +50 -12
  70. package/store/RelayOperationTracker.js.flow +56 -34
  71. package/store/RelayPublishQueue.js.flow +31 -8
  72. package/store/RelayReader.js.flow +148 -42
  73. package/store/RelayRecordSource.js.flow +72 -6
  74. package/store/RelayReferenceMarker.js.flow +29 -12
  75. package/store/RelayResponseNormalizer.js.flow +164 -48
  76. package/store/RelayStoreReactFlightUtils.js.flow +1 -7
  77. package/store/RelayStoreSubscriptions.js.flow +10 -3
  78. package/store/RelayStoreTypes.js.flow +128 -12
  79. package/store/RelayStoreUtils.js.flow +17 -3
  80. package/store/ResolverCache.js.flow +247 -0
  81. package/store/ResolverFragments.js.flow +6 -3
  82. package/store/createRelayContext.js.flow +1 -1
  83. package/subscription/requestSubscription.js.flow +41 -29
  84. package/util/NormalizationNode.js.flow +10 -3
  85. package/util/ReaderNode.js.flow +15 -1
  86. package/util/RelayConcreteNode.js.flow +1 -0
  87. package/util/RelayFeatureFlags.js.flow +8 -10
  88. package/util/RelayReplaySubject.js.flow +7 -6
  89. package/util/getPaginationMetadata.js.flow +74 -0
  90. package/util/getPaginationVariables.js.flow +112 -0
  91. package/util/getPendingOperationsForFragment.js.flow +62 -0
  92. package/util/getRefetchMetadata.js.flow +80 -0
  93. package/util/getValueAtPath.js.flow +46 -0
  94. package/util/isEmptyObject.js.flow +1 -0
  95. package/util/registerEnvironmentWithDevTools.js.flow +33 -0
  96. package/util/withDuration.js.flow +32 -0
  97. package/lib/store/RelayRecordSourceMapImpl.js +0 -107
  98. package/lib/store/RelayStoreSubscriptionsUsingMapByID.js +0 -318
  99. package/store/RelayRecordSourceMapImpl.js.flow +0 -91
  100. package/store/RelayStoreSubscriptionsUsingMapByID.js.flow +0 -283
@@ -20,29 +20,36 @@ const RelayObservable = require('../network/RelayObservable');
20
20
  const RelayRecordSource = require('./RelayRecordSource');
21
21
  const RelayResponseNormalizer = require('./RelayResponseNormalizer');
22
22
 
23
+ const generateID = require('../util/generateID');
23
24
  const getOperation = require('../util/getOperation');
24
25
  const invariant = require('invariant');
25
26
  const stableCopy = require('../util/stableCopy');
26
27
  const warning = require('warning');
28
+ const withDuration = require('../util/withDuration');
27
29
 
28
30
  const {generateClientID, generateUniqueClientID} = require('./ClientID');
31
+ const {getLocalVariables} = require('./RelayConcreteVariables');
29
32
  const {
30
33
  createNormalizationSelector,
31
34
  createReaderSelector,
32
35
  } = require('./RelayModernSelector');
33
36
  const {ROOT_TYPE, TYPENAME_KEY, getStorageKey} = require('./RelayStoreUtils');
34
37
 
38
+ import type {ActorIdentifier} from '../multi-actor-environment/ActorIdentifier';
35
39
  import type {
36
40
  GraphQLResponse,
37
41
  GraphQLSingularResponse,
38
42
  GraphQLResponseWithData,
43
+ ReactFlightServerTree,
39
44
  } from '../network/RelayNetworkTypes';
40
45
  import type {Sink, Subscription} from '../network/RelayObservable';
41
46
  import type {
42
47
  DeferPlaceholder,
48
+ FollowupPayload,
43
49
  RequestDescriptor,
44
50
  HandleFieldPayload,
45
51
  IncrementalDataPlaceholder,
52
+ LogFunction,
46
53
  ModuleImportPayload,
47
54
  NormalizationSelector,
48
55
  OperationDescriptor,
@@ -53,6 +60,7 @@ import type {
53
60
  PublishQueue,
54
61
  ReactFlightPayloadDeserializer,
55
62
  ReactFlightServerErrorHandler,
63
+ ReactFlightClientResponse,
56
64
  Record,
57
65
  RelayResponsePayload,
58
66
  SelectorStoreUpdater,
@@ -71,23 +79,25 @@ import type {GetDataID} from './RelayResponseNormalizer';
71
79
  import type {NormalizationOptions} from './RelayResponseNormalizer';
72
80
 
73
81
  export type ExecuteConfig = {|
82
+ +actorIdentifier: ActorIdentifier,
74
83
  +getDataID: GetDataID,
75
- +treatMissingFieldsAsNull: boolean,
84
+ +getPublishQueue: (actorIdentifier: ActorIdentifier) => PublishQueue,
85
+ +getStore: (actorIdentifier: ActorIdentifier) => Store,
86
+ +isClientPayload?: boolean,
76
87
  +operation: OperationDescriptor,
77
88
  +operationExecutions: Map<string, ActiveState>,
78
89
  +operationLoader: ?OperationLoader,
79
90
  +operationTracker: OperationTracker,
80
91
  +optimisticConfig: ?OptimisticResponseConfig,
81
- +publishQueue: PublishQueue,
82
92
  +reactFlightPayloadDeserializer?: ?ReactFlightPayloadDeserializer,
83
93
  +reactFlightServerErrorHandler?: ?ReactFlightServerErrorHandler,
84
94
  +scheduler?: ?TaskScheduler,
95
+ +shouldProcessClientComponents?: ?boolean,
85
96
  +sink: Sink<GraphQLResponse>,
86
97
  +source: RelayObservable<GraphQLResponse>,
87
- +store: Store,
98
+ +treatMissingFieldsAsNull: boolean,
88
99
  +updater?: ?SelectorStoreUpdater,
89
- +isClientPayload?: boolean,
90
- +shouldProcessClientComponents?: ?boolean,
100
+ +log: LogFunction,
91
101
  |};
92
102
 
93
103
  export type ActiveState = 'active' | 'inactive';
@@ -125,10 +135,13 @@ function execute(config: ExecuteConfig): Executor {
125
135
  * dependencies, etc.
126
136
  */
127
137
  class Executor {
138
+ _actorIdentifier: ActorIdentifier;
128
139
  _getDataID: GetDataID;
129
140
  _treatMissingFieldsAsNull: boolean;
130
141
  _incrementalPayloadsPending: boolean;
131
142
  _incrementalResults: Map<Label, Map<PathKey, IncrementalResults>>;
143
+ _log: LogFunction;
144
+ _executeId: number;
132
145
  _nextSubscriptionId: number;
133
146
  _operation: OperationDescriptor;
134
147
  _operationExecutions: Map<string, ActiveState>;
@@ -137,7 +150,7 @@ class Executor {
137
150
  _operationUpdateEpochs: Map<string, number>;
138
151
  _optimisticUpdates: null | Array<OptimisticUpdate>;
139
152
  _pendingModulePayloadsCount: number;
140
- _publishQueue: PublishQueue;
153
+ +_getPublishQueue: (actorIdentifier: ActorIdentifier) => PublishQueue;
141
154
  _reactFlightPayloadDeserializer: ?ReactFlightPayloadDeserializer;
142
155
  _reactFlightServerErrorHandler: ?ReactFlightServerErrorHandler;
143
156
  _shouldProcessClientComponents: ?boolean;
@@ -148,36 +161,44 @@ class Executor {
148
161
  {|+record: Record, +fieldPayloads: Array<HandleFieldPayload>|},
149
162
  >;
150
163
  _state: 'started' | 'loading_incremental' | 'loading_final' | 'completed';
151
- _store: Store;
164
+ +_getStore: (actorIdentifier: ActorIdentifier) => Store;
152
165
  _subscriptions: Map<number, Subscription>;
153
166
  _updater: ?SelectorStoreUpdater;
154
- _retainDisposable: ?Disposable;
167
+ _asyncStoreUpdateDisposable: ?Disposable;
168
+ _completeFns: Array<() => void>;
169
+ +_retainDisposables: Map<ActorIdentifier, Disposable>;
155
170
  +_isClientPayload: boolean;
156
171
  +_isSubscriptionOperation: boolean;
172
+ +_seenActors: Set<ActorIdentifier>;
157
173
 
158
174
  constructor({
175
+ actorIdentifier,
176
+ getDataID,
177
+ getPublishQueue,
178
+ getStore,
179
+ isClientPayload,
159
180
  operation,
160
181
  operationExecutions,
161
182
  operationLoader,
183
+ operationTracker,
162
184
  optimisticConfig,
163
- publishQueue,
185
+ reactFlightPayloadDeserializer,
186
+ reactFlightServerErrorHandler,
164
187
  scheduler,
188
+ shouldProcessClientComponents,
165
189
  sink,
166
190
  source,
167
- store,
168
- updater,
169
- operationTracker,
170
191
  treatMissingFieldsAsNull,
171
- getDataID,
172
- isClientPayload,
173
- reactFlightPayloadDeserializer,
174
- reactFlightServerErrorHandler,
175
- shouldProcessClientComponents,
192
+ updater,
193
+ log,
176
194
  }: ExecuteConfig): void {
195
+ this._actorIdentifier = actorIdentifier;
177
196
  this._getDataID = getDataID;
178
197
  this._treatMissingFieldsAsNull = treatMissingFieldsAsNull;
179
198
  this._incrementalPayloadsPending = false;
180
199
  this._incrementalResults = new Map();
200
+ this._log = log;
201
+ this._executeId = generateID();
181
202
  this._nextSubscriptionId = 0;
182
203
  this._operation = operation;
183
204
  this._operationExecutions = operationExecutions;
@@ -186,12 +207,12 @@ class Executor {
186
207
  this._operationUpdateEpochs = new Map();
187
208
  this._optimisticUpdates = null;
188
209
  this._pendingModulePayloadsCount = 0;
189
- this._publishQueue = publishQueue;
210
+ this._getPublishQueue = getPublishQueue;
190
211
  this._scheduler = scheduler;
191
212
  this._sink = sink;
192
213
  this._source = new Map();
193
214
  this._state = 'started';
194
- this._store = store;
215
+ this._getStore = getStore;
195
216
  this._subscriptions = new Map();
196
217
  this._updater = updater;
197
218
  this._isClientPayload = isClientPayload === true;
@@ -200,6 +221,9 @@ class Executor {
200
221
  this._isSubscriptionOperation =
201
222
  this._operation.request.node.params.operationKind === 'subscription';
202
223
  this._shouldProcessClientComponents = shouldProcessClientComponents;
224
+ this._retainDisposables = new Map();
225
+ this._seenActors = new Set();
226
+ this._completeFns = [];
203
227
 
204
228
  const id = this._nextSubscriptionId++;
205
229
  source.subscribe({
@@ -212,7 +236,16 @@ class Executor {
212
236
  sink.error(error);
213
237
  }
214
238
  },
215
- start: subscription => this._start(id, subscription),
239
+ start: subscription => {
240
+ this._start(id, subscription);
241
+ this._log({
242
+ name: 'execute.start',
243
+ executeId: this._executeId,
244
+ params: this._operation.request.node.params,
245
+ variables: this._operation.request.variables,
246
+ cacheConfig: this._operation.request.cacheConfig ?? {},
247
+ });
248
+ },
216
249
  });
217
250
 
218
251
  if (optimisticConfig != null) {
@@ -242,19 +275,41 @@ class Executor {
242
275
  if (optimisticUpdates !== null) {
243
276
  this._optimisticUpdates = null;
244
277
  optimisticUpdates.forEach(update =>
245
- this._publishQueue.revertUpdate(update),
278
+ this._getPublishQueueAndSaveActor().revertUpdate(update),
246
279
  );
247
280
  // OK: run revert on cancel
248
- this._publishQueue.run();
281
+ this._runPublishQueue();
249
282
  }
250
283
  this._incrementalResults.clear();
251
- this._completeOperationTracker();
252
- if (this._retainDisposable) {
253
- this._retainDisposable.dispose();
254
- this._retainDisposable = null;
284
+ if (this._asyncStoreUpdateDisposable != null) {
285
+ this._asyncStoreUpdateDisposable.dispose();
286
+ this._asyncStoreUpdateDisposable = null;
255
287
  }
288
+ this._completeFns = [];
289
+ this._completeOperationTracker();
290
+ this._disposeRetainedData();
256
291
  }
257
292
 
293
+ _deserializeReactFlightPayloadWithLogging = (
294
+ tree: ReactFlightServerTree,
295
+ ): ReactFlightClientResponse => {
296
+ const reactFlightPayloadDeserializer = this._reactFlightPayloadDeserializer;
297
+ invariant(
298
+ typeof reactFlightPayloadDeserializer === 'function',
299
+ 'OperationExecutor: Expected reactFlightPayloadDeserializer to be available when calling _deserializeReactFlightPayloadWithLogging.',
300
+ );
301
+ const [duration, result] = withDuration(() => {
302
+ return reactFlightPayloadDeserializer(tree);
303
+ });
304
+ this._log({
305
+ name: 'execute.flight.payload_deserialize',
306
+ executeId: this._executeId,
307
+ operationName: this._operation.request.node.params.name,
308
+ duration,
309
+ });
310
+ return result;
311
+ };
312
+
258
313
  _updateActiveState(): void {
259
314
  let activeState;
260
315
  switch (this._state) {
@@ -314,12 +369,21 @@ class Executor {
314
369
  if (this._subscriptions.size === 0) {
315
370
  this.cancel();
316
371
  this._sink.complete();
372
+ this._log({
373
+ name: 'execute.complete',
374
+ executeId: this._executeId,
375
+ });
317
376
  }
318
377
  }
319
378
 
320
379
  _error(error: Error): void {
321
380
  this.cancel();
322
381
  this._sink.error(error);
382
+ this._log({
383
+ name: 'execute.error',
384
+ executeId: this._executeId,
385
+ error,
386
+ });
323
387
  }
324
388
 
325
389
  _start(id: number, subscription: Subscription): void {
@@ -330,8 +394,16 @@ class Executor {
330
394
  // Handle a raw GraphQL response.
331
395
  _next(_id: number, response: GraphQLResponse): void {
332
396
  this._schedule(() => {
333
- this._handleNext(response);
334
- this._maybeCompleteSubscriptionOperationTracking();
397
+ const [duration] = withDuration(() => {
398
+ this._handleNext(response);
399
+ this._maybeCompleteSubscriptionOperationTracking();
400
+ });
401
+ this._log({
402
+ name: 'execute.next',
403
+ executeId: this._executeId,
404
+ response,
405
+ duration,
406
+ });
335
407
  });
336
408
  }
337
409
 
@@ -395,7 +467,10 @@ class Executor {
395
467
  responsePart => responsePart.extensions?.isOptimistic === true,
396
468
  )
397
469
  ) {
398
- invariant(false, 'Optimistic responses cannot be batched.');
470
+ invariant(
471
+ false,
472
+ 'OperationExecutor: Optimistic responses cannot be batched.',
473
+ );
399
474
  }
400
475
  return false;
401
476
  }
@@ -423,6 +498,7 @@ class Executor {
423
498
  if (this._state === 'completed') {
424
499
  return;
425
500
  }
501
+ this._seenActors.clear();
426
502
 
427
503
  const responses = Array.isArray(response) ? response : [response];
428
504
  const responsesWithData = this._handleErrorResponse(responses);
@@ -460,39 +536,39 @@ class Executor {
460
536
  // with the initial payload followed by some early-to-resolve incremental
461
537
  // payloads (although, can that even happen?)
462
538
  if (hasNonIncrementalResponses) {
463
- const payloadFollowups = this._processResponses(nonIncrementalResponses);
464
-
465
- if (!RelayFeatureFlags.ENABLE_BATCHED_STORE_UPDATES) {
466
- const updatedOwners = this._publishQueue.run(this._operation);
467
- this._updateOperationTracker(updatedOwners);
539
+ // For subscriptions, to avoid every new payload from overwriting existing
540
+ // data from previous payloads, assign a unique rootID for every new
541
+ // non-incremental payload.
542
+ if (this._isSubscriptionOperation) {
543
+ const nextID = generateUniqueClientID();
544
+ this._operation = {
545
+ request: this._operation.request,
546
+ fragment: createReaderSelector(
547
+ this._operation.fragment.node,
548
+ nextID,
549
+ this._operation.fragment.variables,
550
+ this._operation.fragment.owner,
551
+ ),
552
+ root: createNormalizationSelector(
553
+ this._operation.root.node,
554
+ nextID,
555
+ this._operation.root.variables,
556
+ ),
557
+ };
468
558
  }
469
559
 
560
+ const payloadFollowups = this._processResponses(nonIncrementalResponses);
470
561
  this._processPayloadFollowups(payloadFollowups);
471
-
472
- if (!RelayFeatureFlags.ENABLE_BATCHED_STORE_UPDATES) {
473
- if (this._incrementalPayloadsPending && !this._retainDisposable) {
474
- this._retainDisposable = this._store.retain(this._operation);
475
- }
476
- }
477
562
  }
478
563
 
479
564
  if (incrementalResponses.length > 0) {
480
565
  const payloadFollowups = this._processIncrementalResponses(
481
566
  incrementalResponses,
482
567
  );
483
- if (!RelayFeatureFlags.ENABLE_BATCHED_STORE_UPDATES) {
484
- // For the incremental case, we're only handling follow-up responses
485
- // for already initiated operation (and we're not passing it to
486
- // the run(...) call)
487
- const updatedOwners = this._publishQueue.run();
488
- this._updateOperationTracker(updatedOwners);
489
- }
568
+
490
569
  this._processPayloadFollowups(payloadFollowups);
491
570
  }
492
- if (
493
- this._isSubscriptionOperation &&
494
- RelayFeatureFlags.ENABLE_UNIQUE_SUBSCRIPTION_ROOT
495
- ) {
571
+ if (this._isSubscriptionOperation) {
496
572
  // We attach the id to allow the `requestSubscription` to read from the store using
497
573
  // the current id in its `onNext` callback
498
574
  if (responsesWithData[0].extensions == null) {
@@ -505,21 +581,20 @@ class Executor {
505
581
  }
506
582
  }
507
583
 
508
- if (RelayFeatureFlags.ENABLE_BATCHED_STORE_UPDATES) {
509
- // OK: run once after each new payload
510
- // If we have non-incremental responses, we passing `this._operation` to
511
- // the publish queue here, which will later be passed to the store (via
512
- // notify) to indicate that this operation caused the store to update
513
- const updatedOwners = this._publishQueue.run(
514
- hasNonIncrementalResponses ? this._operation : undefined,
515
- );
516
- if (hasNonIncrementalResponses) {
517
- if (this._incrementalPayloadsPending && !this._retainDisposable) {
518
- this._retainDisposable = this._store.retain(this._operation);
519
- }
584
+ // OK: run once after each new payload
585
+ // If we have non-incremental responses, we passing `this._operation` to
586
+ // the publish queue here, which will later be passed to the store (via
587
+ // notify) to indicate that this operation caused the store to update
588
+ const updatedOwners = this._runPublishQueue(
589
+ hasNonIncrementalResponses ? this._operation : undefined,
590
+ );
591
+
592
+ if (hasNonIncrementalResponses) {
593
+ if (this._incrementalPayloadsPending) {
594
+ this._retainData();
520
595
  }
521
- this._updateOperationTracker(updatedOwners);
522
596
  }
597
+ this._updateOperationTracker(updatedOwners);
523
598
  this._sink.next(response);
524
599
  }
525
600
 
@@ -530,7 +605,7 @@ class Executor {
530
605
  ): void {
531
606
  invariant(
532
607
  this._optimisticUpdates === null,
533
- 'environment.execute: only support one optimistic response per ' +
608
+ 'OperationExecutor: environment.execute: only support one optimistic response per ' +
534
609
  'execute.',
535
610
  );
536
611
  if (response == null && updater == null) {
@@ -543,9 +618,13 @@ class Executor {
543
618
  this._operation.root,
544
619
  ROOT_TYPE,
545
620
  {
621
+ actorIdentifier: this._actorIdentifier,
546
622
  getDataID: this._getDataID,
547
623
  path: [],
548
- reactFlightPayloadDeserializer: this._reactFlightPayloadDeserializer,
624
+ reactFlightPayloadDeserializer:
625
+ this._reactFlightPayloadDeserializer != null
626
+ ? this._deserializeReactFlightPayloadWithLogging
627
+ : null,
549
628
  reactFlightServerErrorHandler: this._reactFlightServerErrorHandler,
550
629
  shouldProcessClientComponents: this._shouldProcessClientComponents,
551
630
  treatMissingFieldsAsNull,
@@ -565,7 +644,7 @@ class Executor {
565
644
  errors: null,
566
645
  fieldPayloads: null,
567
646
  incrementalPlaceholders: null,
568
- moduleImportPayloads: null,
647
+ followupPayloads: null,
569
648
  source: RelayRecordSource.create(),
570
649
  isFinal: false,
571
650
  },
@@ -573,61 +652,92 @@ class Executor {
573
652
  });
574
653
  }
575
654
  this._optimisticUpdates = optimisticUpdates;
576
- optimisticUpdates.forEach(update => this._publishQueue.applyUpdate(update));
655
+ optimisticUpdates.forEach(update =>
656
+ this._getPublishQueueAndSaveActor().applyUpdate(update),
657
+ );
577
658
  // OK: only called on construction and when receiving an optimistic payload from network,
578
659
  // which doesn't fall-through to the regular next() handling
579
- this._publishQueue.run();
660
+ this._runPublishQueue();
580
661
  }
581
662
 
582
663
  _processOptimisticFollowups(
583
664
  payload: RelayResponsePayload,
584
665
  optimisticUpdates: Array<OptimisticUpdate>,
585
666
  ): void {
586
- if (payload.moduleImportPayloads && payload.moduleImportPayloads.length) {
587
- const moduleImportPayloads = payload.moduleImportPayloads;
588
- const operationLoader = this._operationLoader;
589
- invariant(
590
- operationLoader,
591
- 'RelayModernEnvironment: Expected an operationLoader to be ' +
592
- 'configured when using `@match`.',
593
- );
594
- for (const moduleImportPayload of moduleImportPayloads) {
595
- const operation = operationLoader.get(
596
- moduleImportPayload.operationReference,
597
- );
598
- if (operation == null) {
599
- this._processAsyncOptimisticModuleImport(
600
- operationLoader,
601
- moduleImportPayload,
602
- );
603
- } else {
604
- const moduleImportOptimisticUpdates = this._processOptimisticModuleImport(
605
- operation,
606
- moduleImportPayload,
607
- );
608
- optimisticUpdates.push(...moduleImportOptimisticUpdates);
667
+ if (payload.followupPayloads && payload.followupPayloads.length) {
668
+ const followupPayloads = payload.followupPayloads;
669
+ for (const followupPayload of followupPayloads) {
670
+ switch (followupPayload.kind) {
671
+ case 'ModuleImportPayload':
672
+ const operationLoader = this._expectOperationLoader();
673
+ const operation = operationLoader.get(
674
+ followupPayload.operationReference,
675
+ );
676
+ if (operation == null) {
677
+ this._processAsyncOptimisticModuleImport(followupPayload);
678
+ } else {
679
+ const moduleImportOptimisticUpdates = this._processOptimisticModuleImport(
680
+ operation,
681
+ followupPayload,
682
+ );
683
+ optimisticUpdates.push(...moduleImportOptimisticUpdates);
684
+ }
685
+ break;
686
+ case 'ActorPayload':
687
+ warning(
688
+ false,
689
+ 'OperationExecutor: Unexpected optimistic ActorPayload. These updates are not supported.',
690
+ );
691
+ break;
692
+ default:
693
+ (followupPayload: empty);
694
+ invariant(
695
+ false,
696
+ 'OperationExecutor: Unexpected followup kind `%s`. when processing optimistic updates.',
697
+ followupPayload.kind,
698
+ );
609
699
  }
610
700
  }
611
701
  }
612
702
  }
613
703
 
614
- _normalizeModuleImport(
615
- moduleImportPayload: ModuleImportPayload,
616
- operation: NormalizationSelectableNode,
704
+ /**
705
+ * Normalize Data for @module payload, and actor-specific payload
706
+ */
707
+ _normalizeFollowupPayload(
708
+ followupPayload: FollowupPayload,
709
+ normalizationNode: NormalizationSelectableNode,
617
710
  ) {
711
+ let variables;
712
+ if (
713
+ normalizationNode.kind === 'SplitOperation' &&
714
+ followupPayload.kind === 'ModuleImportPayload'
715
+ ) {
716
+ variables = getLocalVariables(
717
+ followupPayload.variables,
718
+ normalizationNode.argumentDefinitions,
719
+ followupPayload.args,
720
+ );
721
+ } else {
722
+ variables = followupPayload.variables;
723
+ }
618
724
  const selector = createNormalizationSelector(
619
- operation,
620
- moduleImportPayload.dataID,
621
- moduleImportPayload.variables,
725
+ normalizationNode,
726
+ followupPayload.dataID,
727
+ variables,
622
728
  );
623
729
  return normalizeResponse(
624
- {data: moduleImportPayload.data},
730
+ {data: followupPayload.data},
625
731
  selector,
626
- moduleImportPayload.typeName,
732
+ followupPayload.typeName,
627
733
  {
734
+ actorIdentifier: this._actorIdentifier,
628
735
  getDataID: this._getDataID,
629
- path: moduleImportPayload.path,
630
- reactFlightPayloadDeserializer: this._reactFlightPayloadDeserializer,
736
+ path: followupPayload.path,
737
+ reactFlightPayloadDeserializer:
738
+ this._reactFlightPayloadDeserializer != null
739
+ ? this._deserializeReactFlightPayloadWithLogging
740
+ : null,
631
741
  reactFlightServerErrorHandler: this._reactFlightServerErrorHandler,
632
742
  treatMissingFieldsAsNull: this._treatMissingFieldsAsNull,
633
743
  shouldProcessClientComponents: this._shouldProcessClientComponents,
@@ -641,7 +751,7 @@ class Executor {
641
751
  ): $ReadOnlyArray<OptimisticUpdate> {
642
752
  const operation = getOperation(normalizationRootNode);
643
753
  const optimisticUpdates = [];
644
- const modulePayload = this._normalizeModuleImport(
754
+ const modulePayload = this._normalizeFollowupPayload(
645
755
  moduleImportPayload,
646
756
  operation,
647
757
  );
@@ -656,10 +766,9 @@ class Executor {
656
766
  }
657
767
 
658
768
  _processAsyncOptimisticModuleImport(
659
- operationLoader: OperationLoader,
660
769
  moduleImportPayload: ModuleImportPayload,
661
770
  ): void {
662
- operationLoader
771
+ this._expectOperationLoader()
663
772
  .load(moduleImportPayload.operationReference)
664
773
  .then(operation => {
665
774
  if (operation == null || this._state !== 'started') {
@@ -670,7 +779,7 @@ class Executor {
670
779
  moduleImportPayload,
671
780
  );
672
781
  moduleImportOptimisticUpdates.forEach(update =>
673
- this._publishQueue.applyUpdate(update),
782
+ this._getPublishQueueAndSaveActor().applyUpdate(update),
674
783
  );
675
784
  if (this._optimisticUpdates == null) {
676
785
  warning(
@@ -682,16 +791,18 @@ class Executor {
682
791
  } else {
683
792
  this._optimisticUpdates.push(...moduleImportOptimisticUpdates);
684
793
  // OK: always have to run() after an module import resolves async
685
- this._publishQueue.run();
794
+ this._runPublishQueue();
686
795
  }
687
796
  });
688
797
  }
689
798
 
690
- _processResponses(responses: $ReadOnlyArray<GraphQLResponseWithData>) {
799
+ _processResponses(
800
+ responses: $ReadOnlyArray<GraphQLResponseWithData>,
801
+ ): $ReadOnlyArray<RelayResponsePayload> {
691
802
  if (this._optimisticUpdates !== null) {
692
- this._optimisticUpdates.forEach(update =>
693
- this._publishQueue.revertUpdate(update),
694
- );
803
+ this._optimisticUpdates.forEach(update => {
804
+ this._getPublishQueueAndSaveActor().revertUpdate(update);
805
+ });
695
806
  this._optimisticUpdates = null;
696
807
  }
697
808
 
@@ -704,19 +815,24 @@ class Executor {
704
815
  this._operation.root,
705
816
  ROOT_TYPE,
706
817
  {
818
+ actorIdentifier: this._actorIdentifier,
707
819
  getDataID: this._getDataID,
708
820
  path: [],
709
- reactFlightPayloadDeserializer: this._reactFlightPayloadDeserializer,
821
+ reactFlightPayloadDeserializer:
822
+ this._reactFlightPayloadDeserializer != null
823
+ ? this._deserializeReactFlightPayloadWithLogging
824
+ : null,
710
825
  reactFlightServerErrorHandler: this._reactFlightServerErrorHandler,
711
826
  treatMissingFieldsAsNull: this._treatMissingFieldsAsNull,
712
827
  shouldProcessClientComponents: this._shouldProcessClientComponents,
713
828
  },
714
829
  );
715
- this._publishQueue.commitPayload(
830
+ this._getPublishQueueAndSaveActor().commitPayload(
716
831
  this._operation,
717
832
  relayPayload,
718
833
  this._updater,
719
834
  );
835
+
720
836
  return relayPayload;
721
837
  });
722
838
  }
@@ -732,30 +848,30 @@ class Executor {
732
848
  return;
733
849
  }
734
850
  payloads.forEach(payload => {
735
- const {incrementalPlaceholders, moduleImportPayloads, isFinal} = payload;
851
+ const {incrementalPlaceholders, followupPayloads, isFinal} = payload;
736
852
  this._state = isFinal ? 'loading_final' : 'loading_incremental';
737
853
  this._updateActiveState();
738
854
  if (isFinal) {
739
855
  this._incrementalPayloadsPending = false;
740
856
  }
741
- if (moduleImportPayloads && moduleImportPayloads.length !== 0) {
742
- const operationLoader = this._operationLoader;
743
- invariant(
744
- operationLoader,
745
- 'RelayModernEnvironment: Expected an operationLoader to be ' +
746
- 'configured when using `@match`.',
747
- );
748
- moduleImportPayloads.forEach(moduleImportPayload => {
749
- this._processModuleImportPayload(
750
- moduleImportPayload,
751
- operationLoader,
752
- );
857
+ if (followupPayloads && followupPayloads.length !== 0) {
858
+ followupPayloads.forEach(followupPayload => {
859
+ const prevActorIdentifier = this._actorIdentifier;
860
+ this._actorIdentifier =
861
+ followupPayload.actorIdentifier ?? this._actorIdentifier;
862
+ this._processFollowupPayload(followupPayload);
863
+ this._actorIdentifier = prevActorIdentifier;
753
864
  });
754
865
  }
866
+
755
867
  if (incrementalPlaceholders && incrementalPlaceholders.length !== 0) {
756
868
  this._incrementalPayloadsPending = this._state !== 'loading_final';
757
869
  incrementalPlaceholders.forEach(incrementalPlaceholder => {
870
+ const prevActorIdentifier = this._actorIdentifier;
871
+ this._actorIdentifier =
872
+ incrementalPlaceholder.actorIdentifier ?? this._actorIdentifier;
758
873
  this._processIncrementalPlaceholder(payload, incrementalPlaceholder);
874
+ this._actorIdentifier = prevActorIdentifier;
759
875
  });
760
876
 
761
877
  if (this._isClientPayload || this._state === 'loading_final') {
@@ -787,10 +903,6 @@ class Executor {
787
903
  }
788
904
  });
789
905
  if (relayPayloads.length > 0) {
790
- if (!RelayFeatureFlags.ENABLE_BATCHED_STORE_UPDATES) {
791
- const updatedOwners = this._publishQueue.run();
792
- this._updateOperationTracker(updatedOwners);
793
- }
794
906
  this._processPayloadFollowups(relayPayloads);
795
907
  }
796
908
  }
@@ -808,23 +920,6 @@ class Executor {
808
920
  ) {
809
921
  this._completeOperationTracker();
810
922
  }
811
- if (RelayFeatureFlags.ENABLE_UNIQUE_SUBSCRIPTION_ROOT) {
812
- const nextID = generateUniqueClientID();
813
- this._operation = {
814
- request: this._operation.request,
815
- fragment: createReaderSelector(
816
- this._operation.fragment.node,
817
- nextID,
818
- this._operation.fragment.variables,
819
- this._operation.fragment.owner,
820
- ),
821
- root: createNormalizationSelector(
822
- this._operation.root.node,
823
- nextID,
824
- this._operation.root.variables,
825
- ),
826
- };
827
- }
828
923
  }
829
924
 
830
925
  /**
@@ -834,78 +929,154 @@ class Executor {
834
929
  * defer, stream, etc); these are handled by calling
835
930
  * `_processPayloadFollowups()`.
836
931
  */
837
- _processModuleImportPayload(
838
- moduleImportPayload: ModuleImportPayload,
839
- operationLoader: OperationLoader,
840
- ): void {
841
- const node = operationLoader.get(moduleImportPayload.operationReference);
842
- if (node != null) {
843
- const operation = getOperation(node);
844
- // If the operation module is available synchronously, normalize the
845
- // data synchronously.
846
- this._handleModuleImportPayload(moduleImportPayload, operation);
847
- this._maybeCompleteSubscriptionOperationTracking();
848
- } else {
849
- // Otherwise load the operation module and schedule a task to normalize
850
- // the data when the module is available.
851
- const id = this._nextSubscriptionId++;
852
- this._pendingModulePayloadsCount++;
853
-
854
- const decrementPendingCount = () => {
855
- this._pendingModulePayloadsCount--;
856
- this._maybeCompleteSubscriptionOperationTracking();
857
- };
858
-
859
- // Observable.from(operationLoader.load()) wouldn't catch synchronous
860
- // errors thrown by the load function, which is user-defined. Guard
861
- // against that with Observable.from(new Promise(<work>)).
862
- RelayObservable.from(
863
- new Promise((resolve, reject) => {
864
- operationLoader
865
- .load(moduleImportPayload.operationReference)
866
- .then(resolve, reject);
867
- }),
868
- )
869
- .map((operation: ?NormalizationRootNode) => {
870
- if (operation != null) {
871
- this._schedule(() => {
872
- this._handleModuleImportPayload(
873
- moduleImportPayload,
874
- getOperation(operation),
875
- );
876
- // OK: always have to run after an async module import resolves
877
- const updatedOwners = this._publishQueue.run();
878
- this._updateOperationTracker(updatedOwners);
932
+ _processFollowupPayload(followupPayload: FollowupPayload): void {
933
+ switch (followupPayload.kind) {
934
+ case 'ModuleImportPayload':
935
+ const operationLoader = this._expectOperationLoader();
936
+ const node = operationLoader.get(followupPayload.operationReference);
937
+ if (node != null) {
938
+ // If the operation module is available synchronously, normalize the
939
+ // data synchronously.
940
+ this._processFollowupPayloadWithNormalizationNode(
941
+ followupPayload,
942
+ getOperation(node),
943
+ );
944
+ } else {
945
+ // Otherwise load the operation module and schedule a task to normalize
946
+ // the data when the module is available.
947
+ const id = this._nextSubscriptionId++;
948
+ this._pendingModulePayloadsCount++;
949
+
950
+ const decrementPendingCount = () => {
951
+ this._pendingModulePayloadsCount--;
952
+ this._maybeCompleteSubscriptionOperationTracking();
953
+ };
954
+
955
+ // Observable.from(operationLoader.load()) wouldn't catch synchronous
956
+ // errors thrown by the load function, which is user-defined. Guard
957
+ // against that with Observable.from(new Promise(<work>)).
958
+ const networkObservable = RelayObservable.from(
959
+ new Promise((resolve, reject) => {
960
+ operationLoader
961
+ .load(followupPayload.operationReference)
962
+ .then(resolve, reject);
963
+ }),
964
+ );
965
+ RelayObservable.create(sink => {
966
+ let cancellationToken;
967
+ const subscription = networkObservable.subscribe({
968
+ next: (loadedNode: ?NormalizationRootNode) => {
969
+ if (loadedNode != null) {
970
+ const publishModuleImportPayload = () => {
971
+ try {
972
+ const operation = getOperation(loadedNode);
973
+ const batchAsyncModuleUpdatesFN =
974
+ RelayFeatureFlags.BATCH_ASYNC_MODULE_UPDATES_FN;
975
+ const shouldScheduleAsyncStoreUpdate =
976
+ batchAsyncModuleUpdatesFN != null &&
977
+ this._pendingModulePayloadsCount > 1;
978
+ const [duration] = withDuration(() => {
979
+ this._handleFollowupPayload(followupPayload, operation);
980
+ // OK: always have to run after an async module import resolves
981
+ if (shouldScheduleAsyncStoreUpdate) {
982
+ this._scheduleAsyncStoreUpdate(
983
+ // $FlowFixMe[incompatible-call] `shouldScheduleAsyncStoreUpdate` check should cover `null` case
984
+ batchAsyncModuleUpdatesFN,
985
+ sink.complete,
986
+ );
987
+ } else {
988
+ const updatedOwners = this._runPublishQueue();
989
+ this._updateOperationTracker(updatedOwners);
990
+ }
991
+ });
992
+ this._log({
993
+ name: 'execute.async.module',
994
+ executeId: this._executeId,
995
+ operationName: operation.name,
996
+ duration,
997
+ });
998
+ if (!shouldScheduleAsyncStoreUpdate) {
999
+ sink.complete();
1000
+ }
1001
+ } catch (error) {
1002
+ sink.error(error);
1003
+ }
1004
+ };
1005
+ const scheduler = this._scheduler;
1006
+ if (scheduler == null) {
1007
+ publishModuleImportPayload();
1008
+ } else {
1009
+ cancellationToken = scheduler.schedule(
1010
+ publishModuleImportPayload,
1011
+ );
1012
+ }
1013
+ } else {
1014
+ sink.complete();
1015
+ }
1016
+ },
1017
+ error: sink.error,
879
1018
  });
880
- }
881
- })
882
- .subscribe({
883
- complete: () => {
884
- this._complete(id);
885
- decrementPendingCount();
886
- },
887
- error: error => {
888
- this._error(error);
889
- decrementPendingCount();
890
- },
891
- start: subscription => this._start(id, subscription),
892
- });
1019
+ return () => {
1020
+ subscription.unsubscribe();
1021
+ if (this._scheduler != null && cancellationToken != null) {
1022
+ this._scheduler.cancel(cancellationToken);
1023
+ }
1024
+ };
1025
+ }).subscribe({
1026
+ complete: () => {
1027
+ this._complete(id);
1028
+ decrementPendingCount();
1029
+ },
1030
+ error: error => {
1031
+ this._error(error);
1032
+ decrementPendingCount();
1033
+ },
1034
+ start: subscription => this._start(id, subscription),
1035
+ });
1036
+ }
1037
+ break;
1038
+ case 'ActorPayload':
1039
+ this._processFollowupPayloadWithNormalizationNode(
1040
+ followupPayload,
1041
+ followupPayload.node,
1042
+ );
1043
+ break;
1044
+ default:
1045
+ (followupPayload: empty);
1046
+ invariant(
1047
+ false,
1048
+ 'OperationExecutor: Unexpected followup kind `%s`.',
1049
+ followupPayload.kind,
1050
+ );
893
1051
  }
894
1052
  }
895
1053
 
896
- _handleModuleImportPayload(
897
- moduleImportPayload: ModuleImportPayload,
898
- operation: NormalizationSplitOperation | NormalizationOperation,
1054
+ _processFollowupPayloadWithNormalizationNode(
1055
+ followupPayload: FollowupPayload,
1056
+ normalizationNode:
1057
+ | NormalizationSplitOperation
1058
+ | NormalizationOperation
1059
+ | NormalizationLinkedField,
1060
+ ) {
1061
+ this._handleFollowupPayload(followupPayload, normalizationNode);
1062
+ this._maybeCompleteSubscriptionOperationTracking();
1063
+ }
1064
+
1065
+ _handleFollowupPayload(
1066
+ followupPayload: FollowupPayload,
1067
+ normalizationNode:
1068
+ | NormalizationSplitOperation
1069
+ | NormalizationOperation
1070
+ | NormalizationLinkedField,
899
1071
  ): void {
900
- const relayPayload = this._normalizeModuleImport(
901
- moduleImportPayload,
902
- operation,
1072
+ const relayPayload = this._normalizeFollowupPayload(
1073
+ followupPayload,
1074
+ normalizationNode,
1075
+ );
1076
+ this._getPublishQueueAndSaveActor().commitPayload(
1077
+ this._operation,
1078
+ relayPayload,
903
1079
  );
904
- this._publishQueue.commitPayload(this._operation, relayPayload);
905
- if (!RelayFeatureFlags.ENABLE_BATCHED_STORE_UPDATES) {
906
- const updatedOwners = this._publishQueue.run();
907
- this._updateOperationTracker(updatedOwners);
908
- }
909
1080
  this._processPayloadFollowups([relayPayload]);
910
1081
  }
911
1082
 
@@ -951,7 +1122,7 @@ class Executor {
951
1122
  (placeholder: empty);
952
1123
  invariant(
953
1124
  false,
954
- 'Unsupported incremental placeholder kind `%s`.',
1125
+ 'OperationExecutor: Unsupported incremental placeholder kind `%s`.',
955
1126
  placeholder.kind,
956
1127
  );
957
1128
  }
@@ -975,7 +1146,7 @@ class Executor {
975
1146
  // exist.
976
1147
  invariant(
977
1148
  parentRecord != null,
978
- 'RelayModernEnvironment: Expected record `%s` to exist.',
1149
+ 'OperationExecutor: Expected record `%s` to exist.',
979
1150
  parentID,
980
1151
  );
981
1152
  let nextParentRecord;
@@ -1004,16 +1175,13 @@ class Executor {
1004
1175
  record: nextParentRecord,
1005
1176
  fieldPayloads: nextParentPayloads,
1006
1177
  });
1178
+
1007
1179
  // If there were any queued responses, process them now that placeholders
1008
1180
  // are in place
1009
1181
  if (pendingResponses != null) {
1010
1182
  const payloadFollowups = this._processIncrementalResponses(
1011
1183
  pendingResponses,
1012
1184
  );
1013
- if (!RelayFeatureFlags.ENABLE_BATCHED_STORE_UPDATES) {
1014
- const updatedOwners = this._publishQueue.run();
1015
- this._updateOperationTracker(updatedOwners);
1016
- }
1017
1185
  this._processPayloadFollowups(payloadFollowups);
1018
1186
  }
1019
1187
  }
@@ -1049,7 +1217,7 @@ class Executor {
1049
1217
  const placeholder = resultForPath.placeholder;
1050
1218
  invariant(
1051
1219
  placeholder.kind === 'defer',
1052
- 'RelayModernEnvironment: Expected data for path `%s` for label `%s` ' +
1220
+ 'OperationExecutor: Expected data for path `%s` for label `%s` ' +
1053
1221
  'to be data for @defer, was `@%s`.',
1054
1222
  pathKey,
1055
1223
  label,
@@ -1079,7 +1247,7 @@ class Executor {
1079
1247
  const placeholder = resultForPath.placeholder;
1080
1248
  invariant(
1081
1249
  placeholder.kind === 'stream',
1082
- 'RelayModernEnvironment: Expected data for path `%s` for label `%s` ' +
1250
+ 'OperationExecutor: Expected data for path `%s` for label `%s` ' +
1083
1251
  'to be data for @stream, was `@%s`.',
1084
1252
  pathKey,
1085
1253
  label,
@@ -1100,27 +1268,37 @@ class Executor {
1100
1268
  response: GraphQLResponseWithData,
1101
1269
  ): RelayResponsePayload {
1102
1270
  const {dataID: parentID} = placeholder.selector;
1271
+ const prevActorIdentifier = this._actorIdentifier;
1272
+ this._actorIdentifier =
1273
+ placeholder.actorIdentifier ?? this._actorIdentifier;
1103
1274
  const relayPayload = normalizeResponse(
1104
1275
  response,
1105
1276
  placeholder.selector,
1106
1277
  placeholder.typeName,
1107
1278
  {
1279
+ actorIdentifier: this._actorIdentifier,
1108
1280
  getDataID: this._getDataID,
1109
1281
  path: placeholder.path,
1110
- reactFlightPayloadDeserializer: this._reactFlightPayloadDeserializer,
1282
+ reactFlightPayloadDeserializer:
1283
+ this._reactFlightPayloadDeserializer != null
1284
+ ? this._deserializeReactFlightPayloadWithLogging
1285
+ : null,
1111
1286
  reactFlightServerErrorHandler: this._reactFlightServerErrorHandler,
1112
1287
  treatMissingFieldsAsNull: this._treatMissingFieldsAsNull,
1113
1288
  shouldProcessClientComponents: this._shouldProcessClientComponents,
1114
1289
  },
1115
1290
  );
1116
- this._publishQueue.commitPayload(this._operation, relayPayload);
1291
+ this._getPublishQueueAndSaveActor().commitPayload(
1292
+ this._operation,
1293
+ relayPayload,
1294
+ );
1117
1295
 
1118
1296
  // Load the version of the parent record from which this incremental data
1119
1297
  // was derived
1120
1298
  const parentEntry = this._source.get(parentID);
1121
1299
  invariant(
1122
1300
  parentEntry != null,
1123
- 'RelayModernEnvironment: Expected the parent record `%s` for @defer ' +
1301
+ 'OperationExecutor: Expected the parent record `%s` for @defer ' +
1124
1302
  'data to exist.',
1125
1303
  parentID,
1126
1304
  );
@@ -1130,15 +1308,17 @@ class Executor {
1130
1308
  errors: null,
1131
1309
  fieldPayloads,
1132
1310
  incrementalPlaceholders: null,
1133
- moduleImportPayloads: null,
1311
+ followupPayloads: null,
1134
1312
  source: RelayRecordSource.create(),
1135
1313
  isFinal: response.extensions?.is_final === true,
1136
1314
  };
1137
- this._publishQueue.commitPayload(
1315
+ this._getPublishQueueAndSaveActor().commitPayload(
1138
1316
  this._operation,
1139
1317
  handleFieldsRelayPayload,
1140
1318
  );
1141
1319
  }
1320
+
1321
+ this._actorIdentifier = prevActorIdentifier;
1142
1322
  return relayPayload;
1143
1323
  }
1144
1324
 
@@ -1151,12 +1331,14 @@ class Executor {
1151
1331
  placeholder: StreamPlaceholder,
1152
1332
  response: GraphQLResponseWithData,
1153
1333
  ): RelayResponsePayload {
1154
- const {parentID, node, variables} = placeholder;
1334
+ const {parentID, node, variables, actorIdentifier} = placeholder;
1335
+ const prevActorIdentifier = this._actorIdentifier;
1336
+ this._actorIdentifier = actorIdentifier ?? this._actorIdentifier;
1155
1337
  // Find the LinkedField where @stream was applied
1156
1338
  const field = node.selections[0];
1157
1339
  invariant(
1158
1340
  field != null && field.kind === 'LinkedField' && field.plural === true,
1159
- 'RelayModernEnvironment: Expected @stream to be used on a plural field.',
1341
+ 'OperationExecutor: Expected @stream to be used on a plural field.',
1160
1342
  );
1161
1343
  const {
1162
1344
  fieldPayloads,
@@ -1176,34 +1358,38 @@ class Executor {
1176
1358
  // Publish the new item and update the parent record to set
1177
1359
  // field[index] = item *if* the parent record hasn't been concurrently
1178
1360
  // modified.
1179
- this._publishQueue.commitPayload(this._operation, relayPayload, store => {
1180
- const currentParentRecord = store.get(parentID);
1181
- if (currentParentRecord == null) {
1182
- // parent has since been deleted, stream data is stale
1183
- return;
1184
- }
1185
- const currentItems = currentParentRecord.getLinkedRecords(storageKey);
1186
- if (currentItems == null) {
1187
- // field has since been deleted, stream data is stale
1188
- return;
1189
- }
1190
- if (
1191
- currentItems.length !== prevIDs.length ||
1192
- currentItems.some(
1193
- (currentItem, index) =>
1194
- prevIDs[index] !== (currentItem && currentItem.getDataID()),
1195
- )
1196
- ) {
1197
- // field has been modified by something other than this query,
1198
- // stream data is stale
1199
- return;
1200
- }
1201
- // parent.field has not been concurrently modified:
1202
- // update `parent.field[index] = item`
1203
- const nextItems = [...currentItems];
1204
- nextItems[itemIndex] = store.get(itemID);
1205
- currentParentRecord.setLinkedRecords(nextItems, storageKey);
1206
- });
1361
+ this._getPublishQueueAndSaveActor().commitPayload(
1362
+ this._operation,
1363
+ relayPayload,
1364
+ store => {
1365
+ const currentParentRecord = store.get(parentID);
1366
+ if (currentParentRecord == null) {
1367
+ // parent has since been deleted, stream data is stale
1368
+ return;
1369
+ }
1370
+ const currentItems = currentParentRecord.getLinkedRecords(storageKey);
1371
+ if (currentItems == null) {
1372
+ // field has since been deleted, stream data is stale
1373
+ return;
1374
+ }
1375
+ if (
1376
+ currentItems.length !== prevIDs.length ||
1377
+ currentItems.some(
1378
+ (currentItem, index) =>
1379
+ prevIDs[index] !== (currentItem && currentItem.getDataID()),
1380
+ )
1381
+ ) {
1382
+ // field has been modified by something other than this query,
1383
+ // stream data is stale
1384
+ return;
1385
+ }
1386
+ // parent.field has not been concurrently modified:
1387
+ // update `parent.field[index] = item`
1388
+ const nextItems = [...currentItems];
1389
+ nextItems[itemIndex] = store.get(itemID);
1390
+ currentParentRecord.setLinkedRecords(nextItems, storageKey);
1391
+ },
1392
+ );
1207
1393
 
1208
1394
  // Now that the parent record has been updated to include the new item,
1209
1395
  // also update any handle fields that are derived from the parent record.
@@ -1212,15 +1398,17 @@ class Executor {
1212
1398
  errors: null,
1213
1399
  fieldPayloads,
1214
1400
  incrementalPlaceholders: null,
1215
- moduleImportPayloads: null,
1401
+ followupPayloads: null,
1216
1402
  source: RelayRecordSource.create(),
1217
1403
  isFinal: false,
1218
1404
  };
1219
- this._publishQueue.commitPayload(
1405
+ this._getPublishQueueAndSaveActor().commitPayload(
1220
1406
  this._operation,
1221
1407
  handleFieldsRelayPayload,
1222
1408
  );
1223
1409
  }
1410
+
1411
+ this._actorIdentifier = prevActorIdentifier;
1224
1412
  return relayPayload;
1225
1413
  }
1226
1414
 
@@ -1242,7 +1430,7 @@ class Executor {
1242
1430
  const {data} = response;
1243
1431
  invariant(
1244
1432
  typeof data === 'object',
1245
- 'RelayModernEnvironment: Expected the GraphQL @stream payload `data` ' +
1433
+ 'OperationExecutor: Expected the GraphQL @stream payload `data` ' +
1246
1434
  'value to be an object.',
1247
1435
  );
1248
1436
  const responseKey = field.alias ?? field.name;
@@ -1253,7 +1441,7 @@ class Executor {
1253
1441
  const parentEntry = this._source.get(parentID);
1254
1442
  invariant(
1255
1443
  parentEntry != null,
1256
- 'RelayModernEnvironment: Expected the parent record `%s` for @stream ' +
1444
+ 'OperationExecutor: Expected the parent record `%s` for @stream ' +
1257
1445
  'data to exist.',
1258
1446
  parentID,
1259
1447
  );
@@ -1268,7 +1456,7 @@ class Executor {
1268
1456
  );
1269
1457
  invariant(
1270
1458
  prevIDs != null,
1271
- 'RelayModernEnvironment: Expected record `%s` to have fetched field ' +
1459
+ 'OperationExecutor: Expected record `%s` to have fetched field ' +
1272
1460
  '`%s` with @stream.',
1273
1461
  parentID,
1274
1462
  field.name,
@@ -1279,7 +1467,7 @@ class Executor {
1279
1467
  const itemIndex = parseInt(finalPathEntry, 10);
1280
1468
  invariant(
1281
1469
  itemIndex === finalPathEntry && itemIndex >= 0,
1282
- 'RelayModernEnvironment: Expected path for @stream to end in a ' +
1470
+ 'OperationExecutor: Expected path for @stream to end in a ' +
1283
1471
  'positive integer index, got `%s`',
1284
1472
  finalPathEntry,
1285
1473
  );
@@ -1287,7 +1475,7 @@ class Executor {
1287
1475
  const typeName = field.concreteType ?? data[TYPENAME_KEY];
1288
1476
  invariant(
1289
1477
  typeof typeName === 'string',
1290
- 'RelayModernEnvironment: Expected @stream field `%s` to have a ' +
1478
+ 'OperationExecutor: Expected @stream field `%s` to have a ' +
1291
1479
  '__typename.',
1292
1480
  field.name,
1293
1481
  );
@@ -1302,7 +1490,7 @@ class Executor {
1302
1490
  generateClientID(parentID, storageKey, itemIndex);
1303
1491
  invariant(
1304
1492
  typeof itemID === 'string',
1305
- 'RelayModernEnvironment: Expected id of elements of field `%s` to ' +
1493
+ 'OperationExecutor: Expected id of elements of field `%s` to ' +
1306
1494
  'be strings.',
1307
1495
  storageKey,
1308
1496
  );
@@ -1322,9 +1510,13 @@ class Executor {
1322
1510
  fieldPayloads,
1323
1511
  });
1324
1512
  const relayPayload = normalizeResponse(response, selector, typeName, {
1513
+ actorIdentifier: this._actorIdentifier,
1325
1514
  getDataID: this._getDataID,
1326
1515
  path: [...normalizationPath, responseKey, String(itemIndex)],
1327
- reactFlightPayloadDeserializer: this._reactFlightPayloadDeserializer,
1516
+ reactFlightPayloadDeserializer:
1517
+ this._reactFlightPayloadDeserializer != null
1518
+ ? this._deserializeReactFlightPayloadWithLogging
1519
+ : null,
1328
1520
  reactFlightServerErrorHandler: this._reactFlightServerErrorHandler,
1329
1521
  treatMissingFieldsAsNull: this._treatMissingFieldsAsNull,
1330
1522
  shouldProcessClientComponents: this._shouldProcessClientComponents,
@@ -1339,6 +1531,25 @@ class Executor {
1339
1531
  };
1340
1532
  }
1341
1533
 
1534
+ _scheduleAsyncStoreUpdate(
1535
+ scheduleFn: (() => void) => Disposable,
1536
+ completeFn: () => void,
1537
+ ): void {
1538
+ this._completeFns.push(completeFn);
1539
+ if (this._asyncStoreUpdateDisposable != null) {
1540
+ return;
1541
+ }
1542
+ this._asyncStoreUpdateDisposable = scheduleFn(() => {
1543
+ this._asyncStoreUpdateDisposable = null;
1544
+ const updatedOwners = this._runPublishQueue();
1545
+ this._updateOperationTracker(updatedOwners);
1546
+ for (const complete of this._completeFns) {
1547
+ complete();
1548
+ }
1549
+ this._completeFns = [];
1550
+ });
1551
+ }
1552
+
1342
1553
  _updateOperationTracker(
1343
1554
  updatedOwners: ?$ReadOnlyArray<RequestDescriptor>,
1344
1555
  ): void {
@@ -1353,6 +1564,58 @@ class Executor {
1353
1564
  _completeOperationTracker() {
1354
1565
  this._operationTracker.complete(this._operation.request);
1355
1566
  }
1567
+
1568
+ _getPublishQueueAndSaveActor(): PublishQueue {
1569
+ this._seenActors.add(this._actorIdentifier);
1570
+ return this._getPublishQueue(this._actorIdentifier);
1571
+ }
1572
+
1573
+ _getActorsToVisit(): $ReadOnlySet<ActorIdentifier> {
1574
+ if (this._seenActors.size === 0) {
1575
+ return new Set([this._actorIdentifier]);
1576
+ } else {
1577
+ return this._seenActors;
1578
+ }
1579
+ }
1580
+
1581
+ _runPublishQueue(
1582
+ operation?: OperationDescriptor,
1583
+ ): $ReadOnlyArray<RequestDescriptor> {
1584
+ const updatedOwners = new Set();
1585
+ for (const actorIdentifier of this._getActorsToVisit()) {
1586
+ const owners = this._getPublishQueue(actorIdentifier).run(operation);
1587
+ owners.forEach(owner => updatedOwners.add(owner));
1588
+ }
1589
+ return Array.from(updatedOwners);
1590
+ }
1591
+
1592
+ _retainData() {
1593
+ for (const actorIdentifier of this._getActorsToVisit()) {
1594
+ if (!this._retainDisposables.has(actorIdentifier)) {
1595
+ this._retainDisposables.set(
1596
+ actorIdentifier,
1597
+ this._getStore(actorIdentifier).retain(this._operation),
1598
+ );
1599
+ }
1600
+ }
1601
+ }
1602
+
1603
+ _disposeRetainedData() {
1604
+ for (const disposable of this._retainDisposables.values()) {
1605
+ disposable.dispose();
1606
+ }
1607
+ this._retainDisposables.clear();
1608
+ }
1609
+
1610
+ _expectOperationLoader(): OperationLoader {
1611
+ const operationLoader = this._operationLoader;
1612
+ invariant(
1613
+ operationLoader,
1614
+ 'OperationExecutor: Expected an operationLoader to be ' +
1615
+ 'configured when using `@match`.',
1616
+ );
1617
+ return operationLoader;
1618
+ }
1356
1619
  }
1357
1620
 
1358
1621
  function partitionGraphQLResponses(