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