relay-runtime 10.0.0 → 10.1.2

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 (82) hide show
  1. package/handlers/RelayDefaultHandlerProvider.js.flow +6 -0
  2. package/handlers/connection/MutationHandlers.js.flow +121 -3
  3. package/index.js +1 -1
  4. package/index.js.flow +16 -1
  5. package/lib/handlers/RelayDefaultHandlerProvider.js +9 -0
  6. package/lib/handlers/connection/MutationHandlers.js +147 -14
  7. package/lib/index.js +7 -0
  8. package/lib/mutations/RelayDeclarativeMutationConfig.js +5 -7
  9. package/lib/mutations/commitMutation.js +1 -4
  10. package/lib/mutations/validateMutation.js +28 -12
  11. package/lib/network/RelayQueryResponseCache.js +3 -7
  12. package/lib/query/GraphQLTag.js +2 -1
  13. package/lib/query/fetchQuery.js +2 -3
  14. package/lib/query/fetchQueryInternal.js +2 -3
  15. package/lib/store/DataChecker.js +85 -10
  16. package/lib/store/RelayConcreteVariables.js +2 -6
  17. package/lib/store/RelayModernEnvironment.js +81 -72
  18. package/lib/store/RelayModernFragmentSpecResolver.js +14 -7
  19. package/lib/store/RelayModernOperationDescriptor.js +6 -5
  20. package/lib/store/RelayModernQueryExecutor.js +46 -33
  21. package/lib/store/RelayModernRecord.js +3 -7
  22. package/lib/store/RelayModernStore.js +45 -143
  23. package/lib/store/RelayOperationTracker.js +7 -9
  24. package/lib/store/RelayOptimisticRecordSource.js +2 -6
  25. package/lib/store/RelayPublishQueue.js +1 -1
  26. package/lib/store/RelayReader.js +200 -49
  27. package/lib/store/RelayRecordSourceMapImpl.js +3 -5
  28. package/lib/store/RelayReferenceMarker.js +87 -5
  29. package/lib/store/RelayResponseNormalizer.js +123 -54
  30. package/lib/store/RelayStoreReactFlightUtils.js +47 -0
  31. package/lib/store/RelayStoreSubscriptions.js +162 -0
  32. package/lib/store/RelayStoreSubscriptionsUsingMapByID.js +258 -0
  33. package/lib/store/StoreInspector.js +3 -9
  34. package/lib/store/createRelayContext.js +5 -0
  35. package/lib/store/defaultRequiredFieldLogger.js +18 -0
  36. package/lib/store/normalizeRelayPayload.js +2 -6
  37. package/lib/subscription/requestSubscription.js +2 -3
  38. package/lib/util/NormalizationNode.js +1 -5
  39. package/lib/util/RelayConcreteNode.js +2 -0
  40. package/lib/util/RelayFeatureFlags.js +6 -2
  41. package/lib/util/createPayloadFor3DField.js +2 -7
  42. package/lib/util/getFragmentIdentifier.js +12 -3
  43. package/lib/util/getOperation.js +33 -0
  44. package/lib/util/isEmptyObject.js +25 -0
  45. package/lib/util/recycleNodesInto.js +6 -9
  46. package/lib/util/reportMissingRequiredFields.js +48 -0
  47. package/mutations/commitMutation.js.flow +1 -2
  48. package/mutations/validateMutation.js.flow +34 -5
  49. package/network/RelayNetworkTypes.js.flow +22 -0
  50. package/package.json +2 -2
  51. package/query/GraphQLTag.js.flow +3 -1
  52. package/query/fetchQuery.js.flow +2 -2
  53. package/query/fetchQueryInternal.js.flow +0 -5
  54. package/relay-runtime.js +2 -2
  55. package/relay-runtime.min.js +2 -2
  56. package/store/DataChecker.js.flow +68 -2
  57. package/store/RelayModernEnvironment.js.flow +107 -87
  58. package/store/RelayModernFragmentSpecResolver.js.flow +13 -1
  59. package/store/RelayModernOperationDescriptor.js.flow +5 -1
  60. package/store/RelayModernQueryExecutor.js.flow +47 -23
  61. package/store/RelayModernStore.js.flow +40 -114
  62. package/store/RelayPublishQueue.js.flow +1 -1
  63. package/store/RelayReader.js.flow +184 -27
  64. package/store/RelayReferenceMarker.js.flow +72 -5
  65. package/store/RelayResponseNormalizer.js.flow +140 -50
  66. package/store/RelayStoreReactFlightUtils.js.flow +64 -0
  67. package/store/RelayStoreSubscriptions.js.flow +168 -0
  68. package/store/RelayStoreSubscriptionsUsingMapByID.js.flow +259 -0
  69. package/store/RelayStoreTypes.js.flow +130 -37
  70. package/store/StoreInspector.js.flow +1 -3
  71. package/store/createRelayContext.js.flow +3 -0
  72. package/store/defaultRequiredFieldLogger.js.flow +23 -0
  73. package/subscription/requestSubscription.js.flow +5 -2
  74. package/util/NormalizationNode.js.flow +17 -2
  75. package/util/ReaderNode.js.flow +20 -1
  76. package/util/RelayConcreteNode.js.flow +6 -0
  77. package/util/RelayFeatureFlags.js.flow +10 -1
  78. package/util/getFragmentIdentifier.js.flow +33 -9
  79. package/util/getOperation.js.flow +40 -0
  80. package/util/isEmptyObject.js.flow +25 -0
  81. package/util/recycleNodesInto.js.flow +13 -8
  82. package/util/reportMissingRequiredFields.js.flow +51 -0
@@ -16,6 +16,7 @@ const RelayFeatureFlags = require('../util/RelayFeatureFlags');
16
16
  const RelayModernRecord = require('./RelayModernRecord');
17
17
  const RelayProfiler = require('../util/RelayProfiler');
18
18
 
19
+ const areEqual = require('areEqual');
19
20
  const invariant = require('invariant');
20
21
  const warning = require('warning');
21
22
 
@@ -23,6 +24,7 @@ const {
23
24
  CONDITION,
24
25
  CLIENT_EXTENSION,
25
26
  DEFER,
27
+ FLIGHT_FIELD,
26
28
  INLINE_FRAGMENT,
27
29
  LINKED_FIELD,
28
30
  LINKED_HANDLE,
@@ -34,6 +36,12 @@ const {
34
36
  } = require('../util/RelayConcreteNode');
35
37
  const {generateClientID, isClientID} = require('./ClientID');
36
38
  const {createNormalizationSelector} = require('./RelayModernSelector');
39
+ const {
40
+ refineToReactFlightPayloadData,
41
+ REACT_FLIGHT_QUERIES_STORAGE_KEY,
42
+ REACT_FLIGHT_TREE_STORAGE_KEY,
43
+ REACT_FLIGHT_TYPE_NAME,
44
+ } = require('./RelayStoreReactFlightUtils');
37
45
  const {
38
46
  getArgumentValues,
39
47
  getHandleStorageKey,
@@ -42,12 +50,14 @@ const {
42
50
  getStorageKey,
43
51
  TYPENAME_KEY,
44
52
  ROOT_ID,
53
+ ROOT_TYPE,
45
54
  } = require('./RelayStoreUtils');
46
55
  const {generateTypeID, TYPE_SCHEMA_TYPE} = require('./TypeID');
47
56
 
48
57
  import type {PayloadData} from '../network/RelayNetworkTypes';
49
58
  import type {
50
59
  NormalizationDefer,
60
+ NormalizationFlightField,
51
61
  NormalizationLinkedField,
52
62
  NormalizationModuleImport,
53
63
  NormalizationNode,
@@ -61,6 +71,8 @@ import type {
61
71
  ModuleImportPayload,
62
72
  MutableRecordSource,
63
73
  NormalizationSelector,
74
+ ReactFlightReachableQuery,
75
+ ReactFlightPayloadDeserializer,
64
76
  Record,
65
77
  RelayResponsePayload,
66
78
  } from './RelayStoreTypes';
@@ -74,6 +86,7 @@ export type NormalizationOptions = {|
74
86
  +getDataID: GetDataID,
75
87
  +treatMissingFieldsAsNull: boolean,
76
88
  +path?: $ReadOnlyArray<string>,
89
+ +reactFlightPayloadDeserializer?: ?ReactFlightPayloadDeserializer,
77
90
  |};
78
91
 
79
92
  /**
@@ -111,6 +124,7 @@ class RelayResponseNormalizer {
111
124
  _path: Array<string>;
112
125
  _recordSource: MutableRecordSource;
113
126
  _variables: Variables;
127
+ _reactFlightPayloadDeserializer: ?ReactFlightPayloadDeserializer;
114
128
 
115
129
  constructor(
116
130
  recordSource: MutableRecordSource,
@@ -127,6 +141,8 @@ class RelayResponseNormalizer {
127
141
  this._path = options.path ? [...options.path] : [];
128
142
  this._recordSource = recordSource;
129
143
  this._variables = variables;
144
+ this._reactFlightPayloadDeserializer =
145
+ options.reactFlightPayloadDeserializer;
130
146
  }
131
147
 
132
148
  normalizeResponse(
@@ -277,6 +293,13 @@ class RelayResponseNormalizer {
277
293
  this._traverseSelections(selection, record, data);
278
294
  this._isClientExtension = isClientExtension;
279
295
  break;
296
+ case FLIGHT_FIELD:
297
+ if (RelayFeatureFlags.ENABLE_REACT_FLIGHT_COMPONENT_FIELD) {
298
+ this._normalizeFlightField(node, selection, record, data);
299
+ } else {
300
+ throw new Error('Flight fields are not yet supported.');
301
+ }
302
+ break;
280
303
  default:
281
304
  (selection: empty);
282
305
  invariant(
@@ -441,23 +464,27 @@ class RelayResponseNormalizer {
441
464
  return;
442
465
  }
443
466
  }
444
- if (selection.kind === SCALAR_FIELD && __DEV__) {
445
- this._validateConflictingFieldsWithIdenticalId(
446
- record,
447
- storageKey,
448
- fieldValue,
449
- );
467
+ if (__DEV__) {
468
+ if (selection.kind === SCALAR_FIELD) {
469
+ this._validateConflictingFieldsWithIdenticalId(
470
+ record,
471
+ storageKey,
472
+ fieldValue,
473
+ );
474
+ }
450
475
  }
451
476
  RelayModernRecord.setValue(record, storageKey, null);
452
477
  return;
453
478
  }
454
479
 
455
480
  if (selection.kind === SCALAR_FIELD) {
456
- this._validateConflictingFieldsWithIdenticalId(
457
- record,
458
- storageKey,
459
- fieldValue,
460
- );
481
+ if (__DEV__) {
482
+ this._validateConflictingFieldsWithIdenticalId(
483
+ record,
484
+ storageKey,
485
+ fieldValue,
486
+ );
487
+ }
461
488
  RelayModernRecord.setValue(record, storageKey, fieldValue);
462
489
  } else if (selection.kind === LINKED_FIELD) {
463
490
  this._path.push(responseKey);
@@ -477,6 +504,84 @@ class RelayResponseNormalizer {
477
504
  }
478
505
  }
479
506
 
507
+ _normalizeFlightField(
508
+ parent: NormalizationNode,
509
+ selection: NormalizationFlightField,
510
+ record: Record,
511
+ data: PayloadData,
512
+ ) {
513
+ const responseKey = selection.alias || selection.name;
514
+ const storageKey = getStorageKey(selection, this._variables);
515
+ const fieldValue = data[responseKey];
516
+
517
+ if (fieldValue == null) {
518
+ RelayModernRecord.setValue(record, storageKey, null);
519
+ return;
520
+ }
521
+
522
+ const reactFlightPayload = refineToReactFlightPayloadData(fieldValue);
523
+
524
+ invariant(
525
+ reactFlightPayload != null,
526
+ 'RelayResponseNormalizer(): Expected React Flight payload data ' +
527
+ 'to be an object with `tree` and `queries` properties, got `%s`.',
528
+ fieldValue,
529
+ );
530
+ invariant(
531
+ typeof this._reactFlightPayloadDeserializer === 'function',
532
+ 'RelayResponseNormalizer: Expected reactFlightPayloadDeserializer to ' +
533
+ 'be a function, got `%s`.',
534
+ this._reactFlightPayloadDeserializer,
535
+ );
536
+
537
+ // We store the deserialized reactFlightClientResponse in a separate
538
+ // record and link it to the parent record. This is so we can GC the Flight
539
+ // tree later even if the parent record is still reachable.
540
+ const reactFlightClientResponse = this._reactFlightPayloadDeserializer(
541
+ reactFlightPayload.tree,
542
+ );
543
+ const reactFlightID = generateClientID(
544
+ RelayModernRecord.getDataID(record),
545
+ getStorageKey(selection, this._variables),
546
+ );
547
+ let reactFlightClientResponseRecord = this._recordSource.get(reactFlightID);
548
+ if (reactFlightClientResponseRecord == null) {
549
+ reactFlightClientResponseRecord = RelayModernRecord.create(
550
+ reactFlightID,
551
+ REACT_FLIGHT_TYPE_NAME,
552
+ );
553
+ this._recordSource.set(reactFlightID, reactFlightClientResponseRecord);
554
+ }
555
+ RelayModernRecord.setValue(
556
+ reactFlightClientResponseRecord,
557
+ REACT_FLIGHT_TREE_STORAGE_KEY,
558
+ reactFlightClientResponse,
559
+ );
560
+ const reachableQueries: Array<ReactFlightReachableQuery> = [];
561
+ for (const query of reactFlightPayload.queries) {
562
+ if (query.response.data != null) {
563
+ this._moduleImportPayloads.push({
564
+ data: query.response.data,
565
+ dataID: ROOT_ID,
566
+ operationReference: query.module,
567
+ path: [],
568
+ typeName: ROOT_TYPE,
569
+ variables: query.variables,
570
+ });
571
+ }
572
+ reachableQueries.push({
573
+ module: query.module,
574
+ variables: query.variables,
575
+ });
576
+ }
577
+ RelayModernRecord.setValue(
578
+ reactFlightClientResponseRecord,
579
+ REACT_FLIGHT_QUERIES_STORAGE_KEY,
580
+ reachableQueries,
581
+ );
582
+ RelayModernRecord.setLinkedRecordID(record, storageKey, reactFlightID);
583
+ }
584
+
480
585
  _normalizeLink(
481
586
  field: NormalizationLinkedField,
482
587
  record: Record,
@@ -490,15 +595,9 @@ class RelayResponseNormalizer {
490
595
  );
491
596
  const nextID =
492
597
  this._getDataId(
493
- /* $FlowFixMe[incompatible-variance] (>=0.98.0 site=www,mobile,react_
494
- * native_fb,oss) This comment suppresses an error found when Flow
495
- * v0.98 was deployed. To see the error delete this comment and run
496
- * Flow. */
598
+ // $FlowFixMe[incompatible-variance]
497
599
  fieldValue,
498
- /* $FlowFixMe[incompatible-variance] (>=0.98.0 site=www,mobile,react_
499
- * native_fb,oss) This comment suppresses an error found when Flow
500
- * v0.98 was deployed. To see the error delete this comment and run
501
- * Flow. */
600
+ // $FlowFixMe[incompatible-variance]
502
601
  field.concreteType ?? this._getRecordType(fieldValue),
503
602
  ) ||
504
603
  // Reuse previously generated client IDs
@@ -520,18 +619,14 @@ class RelayResponseNormalizer {
520
619
  RelayModernRecord.setLinkedRecordID(record, storageKey, nextID);
521
620
  let nextRecord = this._recordSource.get(nextID);
522
621
  if (!nextRecord) {
523
- /* $FlowFixMe[incompatible-variance] (>=0.98.0 site=www,mobile,react_
524
- * native_fb,oss) This comment suppresses an error found when Flow v0.98
525
- * was deployed. To see the error delete this comment and run Flow. */
622
+ // $FlowFixMe[incompatible-variance]
526
623
  const typeName = field.concreteType || this._getRecordType(fieldValue);
527
624
  nextRecord = RelayModernRecord.create(nextID, typeName);
528
625
  this._recordSource.set(nextID, nextRecord);
529
626
  } else if (__DEV__) {
530
627
  this._validateRecordType(nextRecord, field, fieldValue);
531
628
  }
532
- /* $FlowFixMe[incompatible-variance] (>=0.98.0 site=www,mobile,react_native_
533
- * fb,oss) This comment suppresses an error found when Flow v0.98 was
534
- * deployed. To see the error delete this comment and run Flow. */
629
+ // $FlowFixMe[incompatible-variance]
535
630
  this._traverseSelections(field, nextRecord, fieldValue);
536
631
  }
537
632
 
@@ -564,15 +659,9 @@ class RelayResponseNormalizer {
564
659
  );
565
660
  const nextID =
566
661
  this._getDataId(
567
- /* $FlowFixMe[incompatible-variance] (>=0.98.0 site=www,mobile,react_
568
- * native_fb,oss) This comment suppresses an error found when Flow
569
- * v0.98 was deployed. To see the error delete this comment and run
570
- * Flow. */
662
+ // $FlowFixMe[incompatible-variance]
571
663
  item,
572
- /* $FlowFixMe[incompatible-variance] (>=0.98.0 site=www,mobile,react_
573
- * native_fb,oss) This comment suppresses an error found when Flow
574
- * v0.98 was deployed. To see the error delete this comment and run
575
- * Flow. */
664
+ // $FlowFixMe[incompatible-variance]
576
665
  field.concreteType ?? this._getRecordType(item),
577
666
  ) ||
578
667
  (prevIDs && prevIDs[nextIndex]) || // Reuse previously generated client IDs:
@@ -591,27 +680,26 @@ class RelayResponseNormalizer {
591
680
  nextIDs.push(nextID);
592
681
  let nextRecord = this._recordSource.get(nextID);
593
682
  if (!nextRecord) {
594
- /* $FlowFixMe[incompatible-variance] (>=0.98.0 site=www,mobile,react_
595
- * native_fb,oss) This comment suppresses an error found when Flow
596
- * v0.98 was deployed. To see the error delete this comment and run
597
- * Flow. */
683
+ // $FlowFixMe[incompatible-variance]
598
684
  const typeName = field.concreteType || this._getRecordType(item);
599
685
  nextRecord = RelayModernRecord.create(nextID, typeName);
600
686
  this._recordSource.set(nextID, nextRecord);
601
687
  } else if (__DEV__) {
602
688
  this._validateRecordType(nextRecord, field, item);
603
689
  }
604
- if (prevIDs && __DEV__) {
605
- this._validateConflictingLinkedFieldsWithIdenticalId(
606
- record,
607
- prevIDs[nextIndex],
608
- nextID,
609
- storageKey,
610
- );
690
+ // NOTE: the check to strip __DEV__ code only works for simple
691
+ // `if (__DEV__)`
692
+ if (__DEV__) {
693
+ if (prevIDs) {
694
+ this._validateConflictingLinkedFieldsWithIdenticalId(
695
+ record,
696
+ prevIDs[nextIndex],
697
+ nextID,
698
+ storageKey,
699
+ );
700
+ }
611
701
  }
612
- /* $FlowFixMe[incompatible-variance] (>=0.98.0 site=www,mobile,react_
613
- * native_fb,oss) This comment suppresses an error found when Flow v0.98
614
- * was deployed. To see the error delete this comment and run Flow. */
702
+ // $FlowFixMe[incompatible-variance]
615
703
  this._traverseSelections(field, nextRecord, item);
616
704
  this._path.pop();
617
705
  });
@@ -650,16 +738,17 @@ class RelayResponseNormalizer {
650
738
  storageKey: string,
651
739
  fieldValue: mixed,
652
740
  ): void {
741
+ // NOTE: Only call this function in DEV
653
742
  if (__DEV__) {
654
743
  const dataID = RelayModernRecord.getDataID(record);
655
744
  var previousValue = RelayModernRecord.getValue(record, storageKey);
656
745
  warning(
657
746
  storageKey === TYPENAME_KEY ||
658
747
  previousValue === undefined ||
659
- previousValue === fieldValue,
748
+ areEqual(previousValue, fieldValue),
660
749
  'RelayResponseNormalizer: Invalid record. The record contains two ' +
661
- 'instances of the same id: `%s` with conflicting field, %s and its values: %s and %s.' +
662
- 'If two fields are different but share' +
750
+ 'instances of the same id: `%s` with conflicting field, %s and its values: %s and %s. ' +
751
+ 'If two fields are different but share ' +
663
752
  'the same id, one field will overwrite the other.',
664
753
  dataID,
665
754
  storageKey,
@@ -678,6 +767,7 @@ class RelayResponseNormalizer {
678
767
  nextID: DataID,
679
768
  storageKey: string,
680
769
  ): void {
770
+ // NOTE: Only call this function in DEV
681
771
  if (__DEV__) {
682
772
  warning(
683
773
  prevID === undefined || prevID === nextID,
@@ -0,0 +1,64 @@
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
8
+ * @format
9
+ */
10
+
11
+ // flowlint ambiguous-object-type:error
12
+
13
+ 'use strict';
14
+
15
+ const invariant = require('invariant');
16
+
17
+ const {getType} = require('./RelayModernRecord');
18
+
19
+ import type {ReactFlightPayloadData} from '../network/RelayNetworkTypes';
20
+ import type {ReactFlightClientResponse, Record} from './RelayStoreTypes';
21
+
22
+ const REACT_FLIGHT_QUERIES_STORAGE_KEY = 'queries';
23
+ const REACT_FLIGHT_TREE_STORAGE_KEY = 'tree';
24
+ const REACT_FLIGHT_TYPE_NAME = 'ReactFlightComponent';
25
+
26
+ function refineToReactFlightPayloadData(
27
+ payload: mixed,
28
+ ): ?ReactFlightPayloadData {
29
+ if (
30
+ payload == null ||
31
+ typeof payload !== 'object' ||
32
+ !Array.isArray(payload.tree) ||
33
+ !Array.isArray(payload.queries)
34
+ ) {
35
+ return null;
36
+ }
37
+ return (payload: $FlowFixMe);
38
+ }
39
+
40
+ function getReactFlightClientResponse(
41
+ record: Record,
42
+ ): ?ReactFlightClientResponse {
43
+ invariant(
44
+ getType(record) === REACT_FLIGHT_TYPE_NAME,
45
+ 'getReactFlightClientResponse(): Expected a ReactFlightComponentRecord, ' +
46
+ 'got %s.',
47
+ record,
48
+ );
49
+ const response: ?ReactFlightClientResponse = (record[
50
+ REACT_FLIGHT_TREE_STORAGE_KEY
51
+ ]: $FlowFixMe);
52
+ if (response != null) {
53
+ return response;
54
+ }
55
+ return null;
56
+ }
57
+
58
+ module.exports = {
59
+ REACT_FLIGHT_QUERIES_STORAGE_KEY,
60
+ REACT_FLIGHT_TREE_STORAGE_KEY,
61
+ REACT_FLIGHT_TYPE_NAME,
62
+ getReactFlightClientResponse,
63
+ refineToReactFlightPayloadData,
64
+ };
@@ -0,0 +1,168 @@
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
+ */
10
+
11
+ // flowlint ambiguous-object-type:error
12
+
13
+ 'use strict';
14
+
15
+ const RelayReader = require('./RelayReader');
16
+
17
+ const deepFreeze = require('../util/deepFreeze');
18
+ const hasOverlappingIDs = require('./hasOverlappingIDs');
19
+ const isEmptyObject = require('../util/isEmptyObject');
20
+ const recycleNodesInto = require('../util/recycleNodesInto');
21
+
22
+ import type {Disposable} from '../util/RelayRuntimeTypes';
23
+ import type {
24
+ RecordSource,
25
+ RequestDescriptor,
26
+ Snapshot,
27
+ StoreSubscriptions,
28
+ UpdatedRecords,
29
+ } from './RelayStoreTypes';
30
+
31
+ type Subscription = {|
32
+ callback: (snapshot: Snapshot) => void,
33
+ snapshot: Snapshot,
34
+ stale: boolean,
35
+ backup: ?Snapshot,
36
+ |};
37
+
38
+ class RelayStoreSubscriptions implements StoreSubscriptions {
39
+ _subscriptions: Set<Subscription>;
40
+
41
+ constructor() {
42
+ this._subscriptions = new Set();
43
+ }
44
+
45
+ subscribe(
46
+ snapshot: Snapshot,
47
+ callback: (snapshot: Snapshot) => void,
48
+ ): Disposable {
49
+ const subscription = {backup: null, callback, snapshot, stale: false};
50
+ const dispose = () => {
51
+ this._subscriptions.delete(subscription);
52
+ };
53
+ this._subscriptions.add(subscription);
54
+ return {dispose};
55
+ }
56
+
57
+ snapshotSubscriptions(source: RecordSource) {
58
+ this._subscriptions.forEach(subscription => {
59
+ // Backup occurs after writing a new "final" payload(s) and before (re)applying
60
+ // optimistic changes. Each subscription's `snapshot` represents what was *last
61
+ // published to the subscriber*, which notably may include previous optimistic
62
+ // updates. Therefore a subscription can be in any of the following states:
63
+ // - stale=true: This subscription was restored to a different value than
64
+ // `snapshot`. That means this subscription has changes relative to its base,
65
+ // but its base has changed (we just applied a final payload): recompute
66
+ // a backup so that we can later restore to the state the subscription
67
+ // should be in.
68
+ // - stale=false: This subscription was restored to the same value than
69
+ // `snapshot`. That means this subscription does *not* have changes relative
70
+ // to its base, so the current `snapshot` is valid to use as a backup.
71
+ if (!subscription.stale) {
72
+ subscription.backup = subscription.snapshot;
73
+ return;
74
+ }
75
+ const snapshot = subscription.snapshot;
76
+ const backup = RelayReader.read(source, snapshot.selector);
77
+ const nextData = recycleNodesInto(snapshot.data, backup.data);
78
+ (backup: $FlowFixMe).data = nextData; // backup owns the snapshot and can safely mutate
79
+ subscription.backup = backup;
80
+ });
81
+ }
82
+
83
+ restoreSubscriptions() {
84
+ this._subscriptions.forEach(subscription => {
85
+ const backup = subscription.backup;
86
+ subscription.backup = null;
87
+ if (backup) {
88
+ if (backup.data !== subscription.snapshot.data) {
89
+ subscription.stale = true;
90
+ }
91
+ subscription.snapshot = {
92
+ data: subscription.snapshot.data,
93
+ isMissingData: backup.isMissingData,
94
+ seenRecords: backup.seenRecords,
95
+ selector: backup.selector,
96
+ missingRequiredFields: backup.missingRequiredFields,
97
+ };
98
+ } else {
99
+ subscription.stale = true;
100
+ }
101
+ });
102
+ }
103
+
104
+ updateSubscriptions(
105
+ source: RecordSource,
106
+ updatedRecordIDs: UpdatedRecords,
107
+ updatedOwners: Array<RequestDescriptor>,
108
+ ) {
109
+ const hasUpdatedRecords = !isEmptyObject(updatedRecordIDs);
110
+ this._subscriptions.forEach(subscription => {
111
+ const owner = this._updateSubscription(
112
+ source,
113
+ subscription,
114
+ updatedRecordIDs,
115
+ hasUpdatedRecords,
116
+ );
117
+ if (owner != null) {
118
+ updatedOwners.push(owner);
119
+ }
120
+ });
121
+ }
122
+
123
+ /**
124
+ * Notifies the callback for the subscription if the data for the associated
125
+ * snapshot has changed.
126
+ * Additionally, updates the subscription snapshot with the latest snapshot,
127
+ * and marks it as not stale.
128
+ * Returns the owner (RequestDescriptor) if the subscription was affected by the
129
+ * latest update, or null if it was not affected.
130
+ */
131
+ _updateSubscription(
132
+ source: RecordSource,
133
+ subscription: Subscription,
134
+ updatedRecordIDs: UpdatedRecords,
135
+ hasUpdatedRecords: boolean,
136
+ ): ?RequestDescriptor {
137
+ const {backup, callback, snapshot, stale} = subscription;
138
+ const hasOverlappingUpdates =
139
+ hasUpdatedRecords &&
140
+ hasOverlappingIDs(snapshot.seenRecords, updatedRecordIDs);
141
+ if (!stale && !hasOverlappingUpdates) {
142
+ return;
143
+ }
144
+ let nextSnapshot: Snapshot =
145
+ hasOverlappingUpdates || !backup
146
+ ? RelayReader.read(source, snapshot.selector)
147
+ : backup;
148
+ const nextData = recycleNodesInto(snapshot.data, nextSnapshot.data);
149
+ nextSnapshot = ({
150
+ data: nextData,
151
+ isMissingData: nextSnapshot.isMissingData,
152
+ seenRecords: nextSnapshot.seenRecords,
153
+ selector: nextSnapshot.selector,
154
+ missingRequiredFields: nextSnapshot.missingRequiredFields,
155
+ }: Snapshot);
156
+ if (__DEV__) {
157
+ deepFreeze(nextSnapshot);
158
+ }
159
+ subscription.snapshot = nextSnapshot;
160
+ subscription.stale = false;
161
+ if (nextSnapshot.data !== snapshot.data) {
162
+ callback(nextSnapshot);
163
+ return snapshot.selector.owner;
164
+ }
165
+ }
166
+ }
167
+
168
+ module.exports = RelayStoreSubscriptions;