relay-runtime 9.0.0 → 10.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/handlers/RelayDefaultHandlerProvider.js.flow +47 -0
  2. package/handlers/connection/ConnectionHandler.js.flow +549 -0
  3. package/handlers/connection/ConnectionInterface.js.flow +92 -0
  4. package/handlers/connection/MutationHandlers.js.flow +199 -0
  5. package/index.js +1 -1
  6. package/index.js.flow +335 -0
  7. package/lib/handlers/RelayDefaultHandlerProvider.js +20 -0
  8. package/lib/handlers/connection/ConnectionHandler.js +1 -3
  9. package/lib/handlers/connection/MutationHandlers.js +212 -0
  10. package/lib/index.js +14 -2
  11. package/lib/mutations/RelayDeclarativeMutationConfig.js +22 -45
  12. package/lib/mutations/RelayRecordProxy.js +1 -3
  13. package/lib/mutations/RelayRecordSourceMutator.js +1 -3
  14. package/lib/mutations/RelayRecordSourceProxy.js +1 -3
  15. package/lib/mutations/RelayRecordSourceSelectorProxy.js +1 -3
  16. package/lib/mutations/commitMutation.js +2 -3
  17. package/lib/mutations/validateMutation.js +40 -9
  18. package/lib/network/RelayObservable.js +9 -9
  19. package/lib/network/RelayQueryResponseCache.js +8 -6
  20. package/lib/query/GraphQLTag.js +2 -1
  21. package/lib/query/PreloadableQueryRegistry.js +70 -0
  22. package/lib/query/fetchQuery.js +2 -3
  23. package/lib/query/fetchQueryInternal.js +5 -14
  24. package/lib/store/DataChecker.js +200 -71
  25. package/lib/store/RelayConcreteVariables.js +6 -2
  26. package/lib/store/RelayModernEnvironment.js +124 -65
  27. package/lib/store/RelayModernFragmentSpecResolver.js +19 -14
  28. package/lib/store/RelayModernOperationDescriptor.js +6 -5
  29. package/lib/store/RelayModernQueryExecutor.js +122 -73
  30. package/lib/store/RelayModernRecord.js +14 -9
  31. package/lib/store/RelayModernSelector.js +6 -2
  32. package/lib/store/RelayModernStore.js +281 -131
  33. package/lib/store/RelayOperationTracker.js +35 -78
  34. package/lib/store/RelayOptimisticRecordSource.js +7 -5
  35. package/lib/store/RelayPublishQueue.js +2 -4
  36. package/lib/store/RelayReader.js +304 -52
  37. package/lib/store/RelayRecordSource.js +1 -3
  38. package/lib/store/RelayRecordSourceMapImpl.js +13 -18
  39. package/lib/store/RelayReferenceMarker.js +125 -14
  40. package/lib/store/RelayResponseNormalizer.js +261 -66
  41. package/lib/store/RelayStoreReactFlightUtils.js +47 -0
  42. package/lib/store/RelayStoreUtils.js +1 -0
  43. package/lib/store/StoreInspector.js +8 -8
  44. package/lib/store/TypeID.js +28 -0
  45. package/lib/store/cloneRelayScalarHandleSourceField.js +44 -0
  46. package/lib/store/defaultRequiredFieldLogger.js +18 -0
  47. package/lib/store/normalizeRelayPayload.js +6 -2
  48. package/lib/store/readInlineData.js +1 -1
  49. package/lib/subscription/requestSubscription.js +4 -3
  50. package/lib/util/NormalizationNode.js +1 -5
  51. package/lib/util/RelayConcreteNode.js +11 -6
  52. package/lib/util/RelayError.js +39 -9
  53. package/lib/util/RelayFeatureFlags.js +6 -3
  54. package/lib/util/RelayReplaySubject.js +3 -3
  55. package/lib/util/createPayloadFor3DField.js +7 -2
  56. package/lib/util/getFragmentIdentifier.js +12 -3
  57. package/lib/util/getOperation.js +33 -0
  58. package/lib/util/getRequestIdentifier.js +2 -2
  59. package/lib/util/isEmptyObject.js +25 -0
  60. package/lib/util/recycleNodesInto.js +6 -7
  61. package/lib/util/reportMissingRequiredFields.js +48 -0
  62. package/mutations/RelayDeclarativeMutationConfig.js.flow +380 -0
  63. package/mutations/RelayRecordProxy.js.flow +165 -0
  64. package/mutations/RelayRecordSourceMutator.js.flow +238 -0
  65. package/mutations/RelayRecordSourceProxy.js.flow +164 -0
  66. package/mutations/RelayRecordSourceSelectorProxy.js.flow +119 -0
  67. package/mutations/applyOptimisticMutation.js.flow +76 -0
  68. package/mutations/commitLocalUpdate.js.flow +24 -0
  69. package/mutations/commitMutation.js.flow +181 -0
  70. package/mutations/validateMutation.js.flow +242 -0
  71. package/network/ConvertToExecuteFunction.js.flow +49 -0
  72. package/network/RelayNetwork.js.flow +84 -0
  73. package/network/RelayNetworkTypes.js.flow +145 -0
  74. package/network/RelayObservable.js.flow +634 -0
  75. package/network/RelayQueryResponseCache.js.flow +111 -0
  76. package/package.json +2 -2
  77. package/query/GraphQLTag.js.flow +168 -0
  78. package/query/PreloadableQueryRegistry.js.flow +65 -0
  79. package/query/fetchQuery.js.flow +47 -0
  80. package/query/fetchQueryInternal.js.flow +343 -0
  81. package/relay-runtime.js +2 -2
  82. package/relay-runtime.min.js +2 -2
  83. package/store/ClientID.js.flow +43 -0
  84. package/store/DataChecker.js.flow +568 -0
  85. package/store/RelayConcreteVariables.js.flow +96 -0
  86. package/store/RelayModernEnvironment.js.flow +571 -0
  87. package/store/RelayModernFragmentSpecResolver.js.flow +438 -0
  88. package/store/RelayModernOperationDescriptor.js.flow +92 -0
  89. package/store/RelayModernQueryExecutor.js.flow +1345 -0
  90. package/store/RelayModernRecord.js.flow +403 -0
  91. package/store/RelayModernSelector.js.flow +455 -0
  92. package/store/RelayModernStore.js.flow +858 -0
  93. package/store/RelayOperationTracker.js.flow +164 -0
  94. package/store/RelayOptimisticRecordSource.js.flow +119 -0
  95. package/store/RelayPublishQueue.js.flow +401 -0
  96. package/store/RelayReader.js.flow +638 -0
  97. package/store/RelayRecordSource.js.flow +29 -0
  98. package/store/RelayRecordSourceMapImpl.js.flow +87 -0
  99. package/store/RelayRecordState.js.flow +37 -0
  100. package/store/RelayReferenceMarker.js.flow +324 -0
  101. package/store/RelayResponseNormalizer.js.flow +791 -0
  102. package/store/RelayStoreReactFlightUtils.js.flow +64 -0
  103. package/store/RelayStoreTypes.js.flow +958 -0
  104. package/store/RelayStoreUtils.js.flow +219 -0
  105. package/store/StoreInspector.js.flow +171 -0
  106. package/store/TypeID.js.flow +28 -0
  107. package/store/ViewerPattern.js.flow +26 -0
  108. package/store/cloneRelayHandleSourceField.js.flow +66 -0
  109. package/store/cloneRelayScalarHandleSourceField.js.flow +62 -0
  110. package/store/createFragmentSpecResolver.js.flow +55 -0
  111. package/store/createRelayContext.js.flow +44 -0
  112. package/store/defaultGetDataID.js.flow +27 -0
  113. package/store/defaultRequiredFieldLogger.js.flow +23 -0
  114. package/store/hasOverlappingIDs.js.flow +34 -0
  115. package/store/isRelayModernEnvironment.js.flow +27 -0
  116. package/store/normalizeRelayPayload.js.flow +51 -0
  117. package/store/readInlineData.js.flow +75 -0
  118. package/subscription/requestSubscription.js.flow +103 -0
  119. package/util/JSResourceTypes.flow.js.flow +20 -0
  120. package/util/NormalizationNode.js.flow +213 -0
  121. package/util/ReaderNode.js.flow +227 -0
  122. package/util/RelayConcreteNode.js.flow +99 -0
  123. package/util/RelayDefaultHandleKey.js.flow +17 -0
  124. package/util/RelayError.js.flow +62 -0
  125. package/util/RelayFeatureFlags.js.flow +37 -0
  126. package/util/RelayProfiler.js.flow +284 -0
  127. package/util/RelayReplaySubject.js.flow +135 -0
  128. package/util/RelayRuntimeTypes.js.flow +72 -0
  129. package/util/createPayloadFor3DField.js.flow +43 -0
  130. package/util/deepFreeze.js.flow +36 -0
  131. package/util/generateID.js.flow +21 -0
  132. package/util/getFragmentIdentifier.js.flow +76 -0
  133. package/util/getOperation.js.flow +40 -0
  134. package/util/getRelayHandleKey.js.flow +41 -0
  135. package/util/getRequestIdentifier.js.flow +42 -0
  136. package/util/isEmptyObject.js.flow +25 -0
  137. package/util/isPromise.js.flow +21 -0
  138. package/util/isScalarAndEqual.js.flow +26 -0
  139. package/util/recycleNodesInto.js.flow +87 -0
  140. package/util/reportMissingRequiredFields.js.flow +51 -0
  141. package/util/resolveImmediate.js.flow +30 -0
  142. package/util/stableCopy.js.flow +35 -0
@@ -0,0 +1,858 @@
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 DataChecker = require('./DataChecker');
16
+ const RelayModernRecord = require('./RelayModernRecord');
17
+ const RelayOptimisticRecordSource = require('./RelayOptimisticRecordSource');
18
+ const RelayProfiler = require('../util/RelayProfiler');
19
+ const RelayReader = require('./RelayReader');
20
+ const RelayReferenceMarker = require('./RelayReferenceMarker');
21
+ const RelayStoreReactFlightUtils = require('./RelayStoreReactFlightUtils');
22
+ const RelayStoreUtils = require('./RelayStoreUtils');
23
+
24
+ const deepFreeze = require('../util/deepFreeze');
25
+ const defaultGetDataID = require('./defaultGetDataID');
26
+ const hasOverlappingIDs = require('./hasOverlappingIDs');
27
+ const invariant = require('invariant');
28
+ const isEmptyObject = require('../util/isEmptyObject');
29
+ const recycleNodesInto = require('../util/recycleNodesInto');
30
+ const resolveImmediate = require('../util/resolveImmediate');
31
+
32
+ const {ROOT_ID, ROOT_TYPE} = require('./RelayStoreUtils');
33
+
34
+ import type {DataID, Disposable} from '../util/RelayRuntimeTypes';
35
+ import type {Availability} from './DataChecker';
36
+ import type {GetDataID} from './RelayResponseNormalizer';
37
+ import type {
38
+ CheckOptions,
39
+ LogFunction,
40
+ MutableRecordSource,
41
+ OperationAvailability,
42
+ OperationDescriptor,
43
+ OperationLoader,
44
+ RecordSource,
45
+ RequestDescriptor,
46
+ Scheduler,
47
+ SingularReaderSelector,
48
+ Snapshot,
49
+ Store,
50
+ UpdatedRecords,
51
+ } from './RelayStoreTypes';
52
+
53
+ export opaque type InvalidationState = {|
54
+ dataIDs: $ReadOnlyArray<DataID>,
55
+ invalidations: Map<DataID, ?number>,
56
+ |};
57
+
58
+ type Subscription = {|
59
+ callback: (snapshot: Snapshot) => void,
60
+ snapshot: Snapshot,
61
+ stale: boolean,
62
+ backup: ?Snapshot,
63
+ |};
64
+
65
+ type InvalidationSubscription = {|
66
+ callback: () => void,
67
+ invalidationState: InvalidationState,
68
+ |};
69
+
70
+ const DEFAULT_RELEASE_BUFFER_SIZE = 0;
71
+
72
+ /**
73
+ * @public
74
+ *
75
+ * An implementation of the `Store` interface defined in `RelayStoreTypes`.
76
+ *
77
+ * Note that a Store takes ownership of all records provided to it: other
78
+ * objects may continue to hold a reference to such records but may not mutate
79
+ * them. The static Relay core is architected to avoid mutating records that may have been
80
+ * passed to a store: operations that mutate records will either create fresh
81
+ * records or clone existing records and modify the clones. Record immutability
82
+ * is also enforced in development mode by freezing all records passed to a store.
83
+ */
84
+ class RelayModernStore implements Store {
85
+ _currentWriteEpoch: number;
86
+ _gcHoldCounter: number;
87
+ _gcReleaseBufferSize: number;
88
+ _gcRun: ?Generator<void, void, void>;
89
+ _gcScheduler: Scheduler;
90
+ _getDataID: GetDataID;
91
+ _globalInvalidationEpoch: ?number;
92
+ _invalidationSubscriptions: Set<InvalidationSubscription>;
93
+ _invalidatedRecordIDs: Set<DataID>;
94
+ __log: ?LogFunction;
95
+ _queryCacheExpirationTime: ?number;
96
+ _operationLoader: ?OperationLoader;
97
+ _optimisticSource: ?MutableRecordSource;
98
+ _recordSource: MutableRecordSource;
99
+ _releaseBuffer: Array<string>;
100
+ _roots: Map<
101
+ string,
102
+ {|
103
+ operation: OperationDescriptor,
104
+ refCount: number,
105
+ epoch: ?number,
106
+ fetchTime: ?number,
107
+ |},
108
+ >;
109
+ _shouldScheduleGC: boolean;
110
+ _subscriptions: Set<Subscription>;
111
+ _updatedRecordIDs: UpdatedRecords;
112
+
113
+ constructor(
114
+ source: MutableRecordSource,
115
+ options?: {|
116
+ gcScheduler?: ?Scheduler,
117
+ log?: ?LogFunction,
118
+ operationLoader?: ?OperationLoader,
119
+ UNSTABLE_DO_NOT_USE_getDataID?: ?GetDataID,
120
+ gcReleaseBufferSize?: ?number,
121
+ queryCacheExpirationTime?: ?number,
122
+ |},
123
+ ) {
124
+ // Prevent mutation of a record from outside the store.
125
+ if (__DEV__) {
126
+ const storeIDs = source.getRecordIDs();
127
+ for (let ii = 0; ii < storeIDs.length; ii++) {
128
+ const record = source.get(storeIDs[ii]);
129
+ if (record) {
130
+ RelayModernRecord.freeze(record);
131
+ }
132
+ }
133
+ }
134
+ this._currentWriteEpoch = 0;
135
+ this._gcHoldCounter = 0;
136
+ this._gcReleaseBufferSize =
137
+ options?.gcReleaseBufferSize ?? DEFAULT_RELEASE_BUFFER_SIZE;
138
+ this._gcRun = null;
139
+ this._gcScheduler = options?.gcScheduler ?? resolveImmediate;
140
+ this._getDataID =
141
+ options?.UNSTABLE_DO_NOT_USE_getDataID ?? defaultGetDataID;
142
+ this._globalInvalidationEpoch = null;
143
+ this._invalidationSubscriptions = new Set();
144
+ this._invalidatedRecordIDs = new Set();
145
+ this.__log = options?.log ?? null;
146
+ this._queryCacheExpirationTime = options?.queryCacheExpirationTime;
147
+ this._operationLoader = options?.operationLoader ?? null;
148
+ this._optimisticSource = null;
149
+ this._recordSource = source;
150
+ this._releaseBuffer = [];
151
+ this._roots = new Map();
152
+ this._shouldScheduleGC = false;
153
+ this._subscriptions = new Set();
154
+ this._updatedRecordIDs = {};
155
+
156
+ initializeRecordSource(this._recordSource);
157
+ }
158
+
159
+ getSource(): RecordSource {
160
+ return this._optimisticSource ?? this._recordSource;
161
+ }
162
+
163
+ check(
164
+ operation: OperationDescriptor,
165
+ options?: CheckOptions,
166
+ ): OperationAvailability {
167
+ const selector = operation.root;
168
+ const source = this._optimisticSource ?? this._recordSource;
169
+ const globalInvalidationEpoch = this._globalInvalidationEpoch;
170
+
171
+ const rootEntry = this._roots.get(operation.request.identifier);
172
+ const operationLastWrittenAt = rootEntry != null ? rootEntry.epoch : null;
173
+
174
+ // Check if store has been globally invalidated
175
+ if (globalInvalidationEpoch != null) {
176
+ // If so, check if the operation we're checking was last written
177
+ // before or after invalidation occured.
178
+ if (
179
+ operationLastWrittenAt == null ||
180
+ operationLastWrittenAt <= globalInvalidationEpoch
181
+ ) {
182
+ // If the operation was written /before/ global invalidation occurred,
183
+ // or if this operation has never been written to the store before,
184
+ // we will consider the data for this operation to be stale
185
+ // (i.e. not resolvable from the store).
186
+ return {status: 'stale'};
187
+ }
188
+ }
189
+
190
+ const target = options?.target ?? source;
191
+ const handlers = options?.handlers ?? [];
192
+ const operationAvailability = DataChecker.check(
193
+ source,
194
+ target,
195
+ selector,
196
+ handlers,
197
+ this._operationLoader,
198
+ this._getDataID,
199
+ );
200
+
201
+ return getAvailabilityStatus(
202
+ operationAvailability,
203
+ operationLastWrittenAt,
204
+ rootEntry?.fetchTime,
205
+ this._queryCacheExpirationTime,
206
+ );
207
+ }
208
+
209
+ retain(operation: OperationDescriptor): Disposable {
210
+ const id = operation.request.identifier;
211
+ let disposed = false;
212
+ const dispose = () => {
213
+ // Ensure each retain can only dispose once
214
+ if (disposed) {
215
+ return;
216
+ }
217
+ disposed = true;
218
+ // For Flow: guard against the entry somehow not existing
219
+ const rootEntry = this._roots.get(id);
220
+ if (rootEntry == null) {
221
+ return;
222
+ }
223
+ // Decrement the ref count: if it becomes zero it is eligible
224
+ // for release.
225
+ rootEntry.refCount--;
226
+
227
+ if (rootEntry.refCount === 0) {
228
+ const {_queryCacheExpirationTime} = this;
229
+ const rootEntryIsStale =
230
+ rootEntry.fetchTime != null &&
231
+ _queryCacheExpirationTime != null &&
232
+ rootEntry.fetchTime <= Date.now() - _queryCacheExpirationTime;
233
+
234
+ if (rootEntryIsStale) {
235
+ this._roots.delete(id);
236
+ this.scheduleGC();
237
+ } else {
238
+ this._releaseBuffer.push(id);
239
+
240
+ // If the release buffer is now over-full, remove the least-recently
241
+ // added entry and schedule a GC. Note that all items in the release
242
+ // buffer have a refCount of 0.
243
+ if (this._releaseBuffer.length > this._gcReleaseBufferSize) {
244
+ const _id = this._releaseBuffer.shift();
245
+ this._roots.delete(_id);
246
+ this.scheduleGC();
247
+ }
248
+ }
249
+ }
250
+ };
251
+
252
+ const rootEntry = this._roots.get(id);
253
+ if (rootEntry != null) {
254
+ if (rootEntry.refCount === 0) {
255
+ // This entry should be in the release buffer, but it no longer belongs
256
+ // there since it's retained. Remove it to maintain the invariant that
257
+ // all release buffer entries have a refCount of 0.
258
+ this._releaseBuffer = this._releaseBuffer.filter(_id => _id !== id);
259
+ }
260
+ // If we've previously retained this operation, increment the refCount
261
+ rootEntry.refCount += 1;
262
+ } else {
263
+ // Otherwise create a new entry for the operation
264
+ this._roots.set(id, {
265
+ operation,
266
+ refCount: 1,
267
+ epoch: null,
268
+ fetchTime: null,
269
+ });
270
+ }
271
+
272
+ return {dispose};
273
+ }
274
+
275
+ lookup(selector: SingularReaderSelector): Snapshot {
276
+ const source = this.getSource();
277
+ const snapshot = RelayReader.read(source, selector);
278
+ if (__DEV__) {
279
+ deepFreeze(snapshot);
280
+ }
281
+ return snapshot;
282
+ }
283
+
284
+ // This method will return a list of updated owners from the subscriptions
285
+ notify(
286
+ sourceOperation?: OperationDescriptor,
287
+ invalidateStore?: boolean,
288
+ ): $ReadOnlyArray<RequestDescriptor> {
289
+ const log = this.__log;
290
+ if (log != null) {
291
+ log({
292
+ name: 'store.notify.start',
293
+ });
294
+ }
295
+
296
+ // Increment the current write when notifying after executing
297
+ // a set of changes to the store.
298
+ this._currentWriteEpoch++;
299
+
300
+ if (invalidateStore === true) {
301
+ this._globalInvalidationEpoch = this._currentWriteEpoch;
302
+ }
303
+
304
+ const source = this.getSource();
305
+ const updatedOwners = [];
306
+ const hasUpdatedRecords = !isEmptyObject(this._updatedRecordIDs);
307
+ this._subscriptions.forEach(subscription => {
308
+ const owner = this._updateSubscription(
309
+ source,
310
+ subscription,
311
+ hasUpdatedRecords,
312
+ );
313
+ if (owner != null) {
314
+ updatedOwners.push(owner);
315
+ }
316
+ });
317
+ this._invalidationSubscriptions.forEach(subscription => {
318
+ this._updateInvalidationSubscription(
319
+ subscription,
320
+ invalidateStore === true,
321
+ );
322
+ });
323
+ if (log != null) {
324
+ log({
325
+ name: 'store.notify.complete',
326
+ updatedRecordIDs: this._updatedRecordIDs,
327
+ invalidatedRecordIDs: this._invalidatedRecordIDs,
328
+ });
329
+ }
330
+
331
+ this._updatedRecordIDs = {};
332
+ this._invalidatedRecordIDs.clear();
333
+
334
+ // If a source operation was provided (indicating the operation
335
+ // that produced this update to the store), record the current epoch
336
+ // at which this operation was written.
337
+ if (sourceOperation != null) {
338
+ // We only track the epoch at which the operation was written if
339
+ // it was previously retained, to keep the size of our operation
340
+ // epoch map bounded. If a query wasn't retained, we assume it can
341
+ // may be deleted at any moment and thus is not relevant for us to track
342
+ // for the purposes of invalidation.
343
+ const id = sourceOperation.request.identifier;
344
+ const rootEntry = this._roots.get(id);
345
+ if (rootEntry != null) {
346
+ rootEntry.epoch = this._currentWriteEpoch;
347
+ rootEntry.fetchTime = Date.now();
348
+ } else if (
349
+ sourceOperation.request.node.params.operationKind === 'query' &&
350
+ this._gcReleaseBufferSize > 0 &&
351
+ this._releaseBuffer.length < this._gcReleaseBufferSize
352
+ ) {
353
+ // The operation isn't retained but there is space in the release buffer:
354
+ // temporarily track this operation in case the data can be reused soon.
355
+ const temporaryRootEntry = {
356
+ operation: sourceOperation,
357
+ refCount: 0,
358
+ epoch: this._currentWriteEpoch,
359
+ fetchTime: Date.now(),
360
+ };
361
+ this._releaseBuffer.push(id);
362
+ this._roots.set(id, temporaryRootEntry);
363
+ }
364
+ }
365
+
366
+ return updatedOwners;
367
+ }
368
+
369
+ publish(source: RecordSource, idsMarkedForInvalidation?: Set<DataID>): void {
370
+ const target = this._optimisticSource ?? this._recordSource;
371
+ updateTargetFromSource(
372
+ target,
373
+ source,
374
+ // We increment the current epoch at the end of the set of updates,
375
+ // in notify(). Here, we pass what will be the incremented value of
376
+ // the epoch to use to write to invalidated records.
377
+ this._currentWriteEpoch + 1,
378
+ idsMarkedForInvalidation,
379
+ this._updatedRecordIDs,
380
+ this._invalidatedRecordIDs,
381
+ );
382
+ // NOTE: log *after* processing the source so that even if a bad log function
383
+ // mutates the source, it doesn't affect Relay processing of it.
384
+ const log = this.__log;
385
+ if (log != null) {
386
+ log({
387
+ name: 'store.publish',
388
+ source,
389
+ optimistic: target === this._optimisticSource,
390
+ });
391
+ }
392
+ }
393
+
394
+ subscribe(
395
+ snapshot: Snapshot,
396
+ callback: (snapshot: Snapshot) => void,
397
+ ): Disposable {
398
+ const subscription = {backup: null, callback, snapshot, stale: false};
399
+ const dispose = () => {
400
+ this._subscriptions.delete(subscription);
401
+ };
402
+ this._subscriptions.add(subscription);
403
+ return {dispose};
404
+ }
405
+
406
+ holdGC(): Disposable {
407
+ if (this._gcRun) {
408
+ this._gcRun = null;
409
+ this._shouldScheduleGC = true;
410
+ }
411
+ this._gcHoldCounter++;
412
+ const dispose = () => {
413
+ if (this._gcHoldCounter > 0) {
414
+ this._gcHoldCounter--;
415
+ if (this._gcHoldCounter === 0 && this._shouldScheduleGC) {
416
+ this.scheduleGC();
417
+ this._shouldScheduleGC = false;
418
+ }
419
+ }
420
+ };
421
+ return {dispose};
422
+ }
423
+
424
+ toJSON(): mixed {
425
+ return 'RelayModernStore()';
426
+ }
427
+
428
+ // Internal API
429
+ __getUpdatedRecordIDs(): UpdatedRecords {
430
+ return this._updatedRecordIDs;
431
+ }
432
+
433
+ // Returns the owner (RequestDescriptor) if the subscription was affected by the
434
+ // latest update, or null if it was not affected.
435
+ _updateSubscription(
436
+ source: RecordSource,
437
+ subscription: Subscription,
438
+ hasUpdatedRecords: boolean,
439
+ ): ?RequestDescriptor {
440
+ const {backup, callback, snapshot, stale} = subscription;
441
+ const hasOverlappingUpdates =
442
+ hasUpdatedRecords &&
443
+ hasOverlappingIDs(snapshot.seenRecords, this._updatedRecordIDs);
444
+ if (!stale && !hasOverlappingUpdates) {
445
+ return;
446
+ }
447
+ let nextSnapshot: Snapshot =
448
+ hasOverlappingUpdates || !backup
449
+ ? RelayReader.read(source, snapshot.selector)
450
+ : backup;
451
+ const nextData = recycleNodesInto(snapshot.data, nextSnapshot.data);
452
+ nextSnapshot = ({
453
+ data: nextData,
454
+ isMissingData: nextSnapshot.isMissingData,
455
+ seenRecords: nextSnapshot.seenRecords,
456
+ selector: nextSnapshot.selector,
457
+ missingRequiredFields: nextSnapshot.missingRequiredFields,
458
+ }: Snapshot);
459
+ if (__DEV__) {
460
+ deepFreeze(nextSnapshot);
461
+ }
462
+ subscription.snapshot = nextSnapshot;
463
+ subscription.stale = false;
464
+ if (nextSnapshot.data !== snapshot.data) {
465
+ callback(nextSnapshot);
466
+ return snapshot.selector.owner;
467
+ }
468
+ }
469
+
470
+ lookupInvalidationState(dataIDs: $ReadOnlyArray<DataID>): InvalidationState {
471
+ const invalidations = new Map();
472
+ dataIDs.forEach(dataID => {
473
+ const record = this.getSource().get(dataID);
474
+ invalidations.set(
475
+ dataID,
476
+ RelayModernRecord.getInvalidationEpoch(record) ?? null,
477
+ );
478
+ });
479
+ invalidations.set('global', this._globalInvalidationEpoch);
480
+ return {
481
+ dataIDs,
482
+ invalidations,
483
+ };
484
+ }
485
+
486
+ checkInvalidationState(prevInvalidationState: InvalidationState): boolean {
487
+ const latestInvalidationState = this.lookupInvalidationState(
488
+ prevInvalidationState.dataIDs,
489
+ );
490
+ const currentInvalidations = latestInvalidationState.invalidations;
491
+ const prevInvalidations = prevInvalidationState.invalidations;
492
+
493
+ // Check if global invalidation has changed
494
+ if (
495
+ currentInvalidations.get('global') !== prevInvalidations.get('global')
496
+ ) {
497
+ return true;
498
+ }
499
+
500
+ // Check if the invalidation state for any of the ids has changed.
501
+ for (const dataID of prevInvalidationState.dataIDs) {
502
+ if (currentInvalidations.get(dataID) !== prevInvalidations.get(dataID)) {
503
+ return true;
504
+ }
505
+ }
506
+
507
+ return false;
508
+ }
509
+
510
+ subscribeToInvalidationState(
511
+ invalidationState: InvalidationState,
512
+ callback: () => void,
513
+ ): Disposable {
514
+ const subscription = {callback, invalidationState};
515
+ const dispose = () => {
516
+ this._invalidationSubscriptions.delete(subscription);
517
+ };
518
+ this._invalidationSubscriptions.add(subscription);
519
+ return {dispose};
520
+ }
521
+
522
+ _updateInvalidationSubscription(
523
+ subscription: InvalidationSubscription,
524
+ invalidatedStore: boolean,
525
+ ) {
526
+ const {callback, invalidationState} = subscription;
527
+ const {dataIDs} = invalidationState;
528
+ const isSubscribedToInvalidatedIDs =
529
+ invalidatedStore ||
530
+ dataIDs.some(dataID => this._invalidatedRecordIDs.has(dataID));
531
+ if (!isSubscribedToInvalidatedIDs) {
532
+ return;
533
+ }
534
+ callback();
535
+ }
536
+
537
+ snapshot(): void {
538
+ invariant(
539
+ this._optimisticSource == null,
540
+ 'RelayModernStore: Unexpected call to snapshot() while a previous ' +
541
+ 'snapshot exists.',
542
+ );
543
+ const log = this.__log;
544
+ if (log != null) {
545
+ log({
546
+ name: 'store.snapshot',
547
+ });
548
+ }
549
+ this._subscriptions.forEach(subscription => {
550
+ // Backup occurs after writing a new "final" payload(s) and before (re)applying
551
+ // optimistic changes. Each subscription's `snapshot` represents what was *last
552
+ // published to the subscriber*, which notably may include previous optimistic
553
+ // updates. Therefore a subscription can be in any of the following states:
554
+ // - stale=true: This subscription was restored to a different value than
555
+ // `snapshot`. That means this subscription has changes relative to its base,
556
+ // but its base has changed (we just applied a final payload): recompute
557
+ // a backup so that we can later restore to the state the subscription
558
+ // should be in.
559
+ // - stale=false: This subscription was restored to the same value than
560
+ // `snapshot`. That means this subscription does *not* have changes relative
561
+ // to its base, so the current `snapshot` is valid to use as a backup.
562
+ if (!subscription.stale) {
563
+ subscription.backup = subscription.snapshot;
564
+ return;
565
+ }
566
+ const snapshot = subscription.snapshot;
567
+ const backup = RelayReader.read(this.getSource(), snapshot.selector);
568
+ const nextData = recycleNodesInto(snapshot.data, backup.data);
569
+ (backup: $FlowFixMe).data = nextData; // backup owns the snapshot and can safely mutate
570
+ subscription.backup = backup;
571
+ });
572
+ if (this._gcRun) {
573
+ this._gcRun = null;
574
+ this._shouldScheduleGC = true;
575
+ }
576
+ this._optimisticSource = RelayOptimisticRecordSource.create(
577
+ this.getSource(),
578
+ );
579
+ }
580
+
581
+ restore(): void {
582
+ invariant(
583
+ this._optimisticSource != null,
584
+ 'RelayModernStore: Unexpected call to restore(), expected a snapshot ' +
585
+ 'to exist (make sure to call snapshot()).',
586
+ );
587
+ const log = this.__log;
588
+ if (log != null) {
589
+ log({
590
+ name: 'store.restore',
591
+ });
592
+ }
593
+ this._optimisticSource = null;
594
+ if (this._shouldScheduleGC) {
595
+ this.scheduleGC();
596
+ }
597
+ this._subscriptions.forEach(subscription => {
598
+ const backup = subscription.backup;
599
+ subscription.backup = null;
600
+ if (backup) {
601
+ if (backup.data !== subscription.snapshot.data) {
602
+ subscription.stale = true;
603
+ }
604
+ subscription.snapshot = {
605
+ data: subscription.snapshot.data,
606
+ isMissingData: backup.isMissingData,
607
+ seenRecords: backup.seenRecords,
608
+ selector: backup.selector,
609
+ missingRequiredFields: backup.missingRequiredFields,
610
+ };
611
+ } else {
612
+ subscription.stale = true;
613
+ }
614
+ });
615
+ }
616
+
617
+ scheduleGC() {
618
+ if (this._gcHoldCounter > 0) {
619
+ this._shouldScheduleGC = true;
620
+ return;
621
+ }
622
+ if (this._gcRun) {
623
+ return;
624
+ }
625
+ this._gcRun = this._collect();
626
+ this._gcScheduler(this._gcStep);
627
+ }
628
+
629
+ /**
630
+ * Run a full GC synchronously.
631
+ */
632
+ __gc(): void {
633
+ // Don't run GC while there are optimistic updates applied
634
+ if (this._optimisticSource != null) {
635
+ return;
636
+ }
637
+ const gcRun = this._collect();
638
+ while (!gcRun.next().done) {}
639
+ }
640
+
641
+ _gcStep = () => {
642
+ if (this._gcRun) {
643
+ if (this._gcRun.next().done) {
644
+ this._gcRun = null;
645
+ } else {
646
+ this._gcScheduler(this._gcStep);
647
+ }
648
+ }
649
+ };
650
+
651
+ *_collect(): Generator<void, void, void> {
652
+ /* eslint-disable no-labels */
653
+ top: while (true) {
654
+ const startEpoch = this._currentWriteEpoch;
655
+ const references = new Set();
656
+
657
+ // Mark all records that are traversable from a root
658
+ for (const {operation} of this._roots.values()) {
659
+ const selector = operation.root;
660
+ RelayReferenceMarker.mark(
661
+ this._recordSource,
662
+ selector,
663
+ references,
664
+ this._operationLoader,
665
+ );
666
+ // Yield for other work after each operation
667
+ yield;
668
+
669
+ // If the store was updated, restart
670
+ if (startEpoch !== this._currentWriteEpoch) {
671
+ continue top;
672
+ }
673
+ }
674
+
675
+ const log = this.__log;
676
+ if (log != null) {
677
+ log({
678
+ name: 'store.gc',
679
+ references,
680
+ });
681
+ }
682
+
683
+ // Sweep records without references
684
+ if (references.size === 0) {
685
+ // Short-circuit if *nothing* is referenced
686
+ this._recordSource.clear();
687
+ } else {
688
+ // Evict any unreferenced nodes
689
+ const storeIDs = this._recordSource.getRecordIDs();
690
+ for (let ii = 0; ii < storeIDs.length; ii++) {
691
+ const dataID = storeIDs[ii];
692
+ if (!references.has(dataID)) {
693
+ this._recordSource.remove(dataID);
694
+ }
695
+ }
696
+ }
697
+ return;
698
+ }
699
+ }
700
+ }
701
+
702
+ function initializeRecordSource(target: MutableRecordSource) {
703
+ if (!target.has(ROOT_ID)) {
704
+ const rootRecord = RelayModernRecord.create(ROOT_ID, ROOT_TYPE);
705
+ target.set(ROOT_ID, rootRecord);
706
+ }
707
+ }
708
+
709
+ /**
710
+ * Updates the target with information from source, also updating a mapping of
711
+ * which records in the target were changed as a result.
712
+ * Additionally, will mark records as invalidated at the current write epoch
713
+ * given the set of record ids marked as stale in this update.
714
+ */
715
+ function updateTargetFromSource(
716
+ target: MutableRecordSource,
717
+ source: RecordSource,
718
+ currentWriteEpoch: number,
719
+ idsMarkedForInvalidation: ?Set<DataID>,
720
+ updatedRecordIDs: UpdatedRecords,
721
+ invalidatedRecordIDs: Set<DataID>,
722
+ ): void {
723
+ // First, update any records that were marked for invalidation.
724
+ // For each provided dataID that was invalidated, we write the
725
+ // INVALIDATED_AT_KEY on the record, indicating
726
+ // the epoch at which the record was invalidated.
727
+ if (idsMarkedForInvalidation) {
728
+ idsMarkedForInvalidation.forEach(dataID => {
729
+ const targetRecord = target.get(dataID);
730
+ const sourceRecord = source.get(dataID);
731
+
732
+ // If record was deleted during the update (and also invalidated),
733
+ // we don't need to count it as an invalidated id
734
+ if (sourceRecord === null) {
735
+ return;
736
+ }
737
+
738
+ let nextRecord;
739
+ if (targetRecord != null) {
740
+ // If the target record exists, use it to set the epoch
741
+ // at which it was invalidated. This record will be updated with
742
+ // any changes from source in the section below
743
+ // where we update the target records based on the source.
744
+ nextRecord = RelayModernRecord.clone(targetRecord);
745
+ } else {
746
+ // If the target record doesn't exist, it means that a new record
747
+ // in the source was created (and also invalidated), so we use that
748
+ // record to set the epoch at which it was invalidated. This record
749
+ // will be updated with any changes from source in the section below
750
+ // where we update the target records based on the source.
751
+ nextRecord =
752
+ sourceRecord != null ? RelayModernRecord.clone(sourceRecord) : null;
753
+ }
754
+ if (!nextRecord) {
755
+ return;
756
+ }
757
+ RelayModernRecord.setValue(
758
+ nextRecord,
759
+ RelayStoreUtils.INVALIDATED_AT_KEY,
760
+ currentWriteEpoch,
761
+ );
762
+ invalidatedRecordIDs.add(dataID);
763
+ target.set(dataID, nextRecord);
764
+ });
765
+ }
766
+
767
+ // Update the target based on the changes present in source
768
+ const dataIDs = source.getRecordIDs();
769
+ for (let ii = 0; ii < dataIDs.length; ii++) {
770
+ const dataID = dataIDs[ii];
771
+ const sourceRecord = source.get(dataID);
772
+ const targetRecord = target.get(dataID);
773
+
774
+ // Prevent mutation of a record from outside the store.
775
+ if (__DEV__) {
776
+ if (sourceRecord) {
777
+ RelayModernRecord.freeze(sourceRecord);
778
+ }
779
+ }
780
+ if (sourceRecord && targetRecord) {
781
+ // ReactFlightClientResponses are lazy and only materialize when readRoot
782
+ // is called when we read the field, so if the record is a Flight field
783
+ // we always use the new record's data regardless of whether
784
+ // it actually changed. Let React take care of reconciliation instead.
785
+ const nextRecord =
786
+ RelayModernRecord.getType(targetRecord) ===
787
+ RelayStoreReactFlightUtils.REACT_FLIGHT_TYPE_NAME
788
+ ? sourceRecord
789
+ : RelayModernRecord.update(targetRecord, sourceRecord);
790
+ if (nextRecord !== targetRecord) {
791
+ // Prevent mutation of a record from outside the store.
792
+ if (__DEV__) {
793
+ RelayModernRecord.freeze(nextRecord);
794
+ }
795
+ updatedRecordIDs[dataID] = true;
796
+ target.set(dataID, nextRecord);
797
+ }
798
+ } else if (sourceRecord === null) {
799
+ target.delete(dataID);
800
+ if (targetRecord !== null) {
801
+ updatedRecordIDs[dataID] = true;
802
+ }
803
+ } else if (sourceRecord) {
804
+ target.set(dataID, sourceRecord);
805
+ updatedRecordIDs[dataID] = true;
806
+ } // don't add explicit undefined
807
+ }
808
+ }
809
+
810
+ /**
811
+ * Returns an OperationAvailability given the Availability returned
812
+ * by checking an operation, and when that operation was last written to the store.
813
+ * Specifically, the provided Availability of an operation will contain the
814
+ * value of when a record referenced by the operation was most recently
815
+ * invalidated; given that value, and given when this operation was last
816
+ * written to the store, this function will return the overall
817
+ * OperationAvailability for the operation.
818
+ */
819
+ function getAvailabilityStatus(
820
+ operationAvailability: Availability,
821
+ operationLastWrittenAt: ?number,
822
+ operationFetchTime: ?number,
823
+ queryCacheExpirationTime: ?number,
824
+ ): OperationAvailability {
825
+ const {mostRecentlyInvalidatedAt, status} = operationAvailability;
826
+ if (typeof mostRecentlyInvalidatedAt === 'number') {
827
+ // If some record referenced by this operation is stale, then the operation itself is stale
828
+ // if either the operation itself was never written *or* the operation was last written
829
+ // before the most recent invalidation of its reachable records.
830
+ if (
831
+ operationLastWrittenAt == null ||
832
+ mostRecentlyInvalidatedAt > operationLastWrittenAt
833
+ ) {
834
+ return {status: 'stale'};
835
+ }
836
+ }
837
+
838
+ if (status === 'missing') {
839
+ return {status: 'missing'};
840
+ }
841
+
842
+ if (operationFetchTime != null && queryCacheExpirationTime != null) {
843
+ const isStale = operationFetchTime <= Date.now() - queryCacheExpirationTime;
844
+ if (isStale) {
845
+ return {status: 'stale'};
846
+ }
847
+ }
848
+
849
+ // There were no invalidations of any reachable records *or* the operation is known to have
850
+ // been fetched after the most recent record invalidation.
851
+ return {status: 'available', fetchTime: operationFetchTime ?? null};
852
+ }
853
+
854
+ RelayProfiler.instrumentMethods(RelayModernStore.prototype, {
855
+ lookup: 'RelayModernStore.prototype.lookup',
856
+ });
857
+
858
+ module.exports = RelayModernStore;