diff --git a/packages/relay-runtime/store/RelayModernStore.js b/packages/relay-runtime/store/RelayModernStore.js index b2989a3b02136..112f6d1d89da5 100644 --- a/packages/relay-runtime/store/RelayModernStore.js +++ b/packages/relay-runtime/store/RelayModernStore.js @@ -19,13 +19,13 @@ const RelayProfiler = require('../util/RelayProfiler'); const RelayReader = require('./RelayReader'); const RelayReferenceMarker = require('./RelayReferenceMarker'); const RelayStoreReactFlightUtils = require('./RelayStoreReactFlightUtils'); +const RelayStoreSubscriptions = require('./RelayStoreSubscriptions'); const RelayStoreUtils = require('./RelayStoreUtils'); const deepFreeze = require('../util/deepFreeze'); const defaultGetDataID = require('./defaultGetDataID'); const hasOverlappingIDs = require('./hasOverlappingIDs'); const invariant = require('invariant'); -const isEmptyObject = require('../util/isEmptyObject'); const recycleNodesInto = require('../util/recycleNodesInto'); const resolveImmediate = require('../util/resolveImmediate'); @@ -55,13 +55,6 @@ export opaque type InvalidationState = {| invalidations: Map, |}; -type Subscription = {| - callback: (snapshot: Snapshot) => void, - snapshot: Snapshot, - stale: boolean, - backup: ?Snapshot, -|}; - type InvalidationSubscription = {| callback: () => void, invalidationState: InvalidationState, @@ -107,7 +100,7 @@ class RelayModernStore implements Store { |}, >; _shouldScheduleGC: boolean; - _subscriptions: Set; + _storeSubscriptions: RelayStoreSubscriptions; _updatedRecordIDs: UpdatedRecords; constructor( @@ -150,7 +143,7 @@ class RelayModernStore implements Store { this._releaseBuffer = []; this._roots = new Map(); this._shouldScheduleGC = false; - this._subscriptions = new Set(); + this._storeSubscriptions = new RelayStoreSubscriptions(); this._updatedRecordIDs = {}; initializeRecordSource(this._recordSource); @@ -303,17 +296,11 @@ class RelayModernStore implements Store { const source = this.getSource(); const updatedOwners = []; - const hasUpdatedRecords = !isEmptyObject(this._updatedRecordIDs); - this._subscriptions.forEach(subscription => { - const owner = this._updateSubscription( - source, - subscription, - hasUpdatedRecords, - ); - if (owner != null) { - updatedOwners.push(owner); - } - }); + this._storeSubscriptions.updateSubscriptions( + source, + this._updatedRecordIDs, + updatedOwners, + ); this._invalidationSubscriptions.forEach(subscription => { this._updateInvalidationSubscription( subscription, @@ -395,12 +382,7 @@ class RelayModernStore implements Store { snapshot: Snapshot, callback: (snapshot: Snapshot) => void, ): Disposable { - const subscription = {backup: null, callback, snapshot, stale: false}; - const dispose = () => { - this._subscriptions.delete(subscription); - }; - this._subscriptions.add(subscription); - return {dispose}; + return this._storeSubscriptions.subscribe(snapshot, callback); } holdGC(): Disposable { @@ -430,43 +412,6 @@ class RelayModernStore implements Store { return this._updatedRecordIDs; } - // Returns the owner (RequestDescriptor) if the subscription was affected by the - // latest update, or null if it was not affected. - _updateSubscription( - source: RecordSource, - subscription: Subscription, - hasUpdatedRecords: boolean, - ): ?RequestDescriptor { - const {backup, callback, snapshot, stale} = subscription; - const hasOverlappingUpdates = - hasUpdatedRecords && - hasOverlappingIDs(snapshot.seenRecords, this._updatedRecordIDs); - if (!stale && !hasOverlappingUpdates) { - return; - } - let nextSnapshot: Snapshot = - hasOverlappingUpdates || !backup - ? RelayReader.read(source, snapshot.selector) - : backup; - const nextData = recycleNodesInto(snapshot.data, nextSnapshot.data); - nextSnapshot = ({ - data: nextData, - isMissingData: nextSnapshot.isMissingData, - seenRecords: nextSnapshot.seenRecords, - selector: nextSnapshot.selector, - missingRequiredFields: nextSnapshot.missingRequiredFields, - }: Snapshot); - if (__DEV__) { - deepFreeze(nextSnapshot); - } - subscription.snapshot = nextSnapshot; - subscription.stale = false; - if (nextSnapshot.data !== snapshot.data) { - callback(nextSnapshot); - return snapshot.selector.owner; - } - } - lookupInvalidationState(dataIDs: $ReadOnlyArray): InvalidationState { const invalidations = new Map(); dataIDs.forEach(dataID => { @@ -546,29 +491,7 @@ class RelayModernStore implements Store { name: 'store.snapshot', }); } - this._subscriptions.forEach(subscription => { - // Backup occurs after writing a new "final" payload(s) and before (re)applying - // optimistic changes. Each subscription's `snapshot` represents what was *last - // published to the subscriber*, which notably may include previous optimistic - // updates. Therefore a subscription can be in any of the following states: - // - stale=true: This subscription was restored to a different value than - // `snapshot`. That means this subscription has changes relative to its base, - // but its base has changed (we just applied a final payload): recompute - // a backup so that we can later restore to the state the subscription - // should be in. - // - stale=false: This subscription was restored to the same value than - // `snapshot`. That means this subscription does *not* have changes relative - // to its base, so the current `snapshot` is valid to use as a backup. - if (!subscription.stale) { - subscription.backup = subscription.snapshot; - return; - } - const snapshot = subscription.snapshot; - const backup = RelayReader.read(this.getSource(), snapshot.selector); - const nextData = recycleNodesInto(snapshot.data, backup.data); - (backup: $FlowFixMe).data = nextData; // backup owns the snapshot and can safely mutate - subscription.backup = backup; - }); + this._storeSubscriptions.snapshotSubscriptions(this.getSource()); if (this._gcRun) { this._gcRun = null; this._shouldScheduleGC = true; @@ -594,24 +517,7 @@ class RelayModernStore implements Store { if (this._shouldScheduleGC) { this.scheduleGC(); } - this._subscriptions.forEach(subscription => { - const backup = subscription.backup; - subscription.backup = null; - if (backup) { - if (backup.data !== subscription.snapshot.data) { - subscription.stale = true; - } - subscription.snapshot = { - data: subscription.snapshot.data, - isMissingData: backup.isMissingData, - seenRecords: backup.seenRecords, - selector: backup.selector, - missingRequiredFields: backup.missingRequiredFields, - }; - } else { - subscription.stale = true; - } - }); + this._storeSubscriptions.restoreSubscriptions(); } scheduleGC() { diff --git a/packages/relay-runtime/store/RelayStoreSubscriptions.js b/packages/relay-runtime/store/RelayStoreSubscriptions.js new file mode 100644 index 0000000000000..e2ad6326c53c0 --- /dev/null +++ b/packages/relay-runtime/store/RelayStoreSubscriptions.js @@ -0,0 +1,161 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +// flowlint ambiguous-object-type:error + +'use strict'; + +const RelayReader = require('./RelayReader'); + +const deepFreeze = require('../util/deepFreeze'); +const hasOverlappingIDs = require('./hasOverlappingIDs'); +const isEmptyObject = require('../util/isEmptyObject'); +const recycleNodesInto = require('../util/recycleNodesInto'); + +import type {Disposable} from '../util/RelayRuntimeTypes'; +import type { + RecordSource, + RequestDescriptor, + Snapshot, + UpdatedRecords, +} from './RelayStoreTypes'; + +export type Subscription = {| + callback: (snapshot: Snapshot) => void, + snapshot: Snapshot, + stale: boolean, + backup: ?Snapshot, +|}; + +class RelayStoreSubscriptions { + _subscriptions: Set; + + constructor() { + this._subscriptions = new Set(); + } + + subscribe( + snapshot: Snapshot, + callback: (snapshot: Snapshot) => void, + ): Disposable { + const subscription = {backup: null, callback, snapshot, stale: false}; + const dispose = () => { + this._subscriptions.delete(subscription); + }; + this._subscriptions.add(subscription); + return {dispose}; + } + + snapshotSubscriptions(source: RecordSource) { + this._subscriptions.forEach(subscription => { + // Backup occurs after writing a new "final" payload(s) and before (re)applying + // optimistic changes. Each subscription's `snapshot` represents what was *last + // published to the subscriber*, which notably may include previous optimistic + // updates. Therefore a subscription can be in any of the following states: + // - stale=true: This subscription was restored to a different value than + // `snapshot`. That means this subscription has changes relative to its base, + // but its base has changed (we just applied a final payload): recompute + // a backup so that we can later restore to the state the subscription + // should be in. + // - stale=false: This subscription was restored to the same value than + // `snapshot`. That means this subscription does *not* have changes relative + // to its base, so the current `snapshot` is valid to use as a backup. + if (!subscription.stale) { + subscription.backup = subscription.snapshot; + return; + } + const snapshot = subscription.snapshot; + const backup = RelayReader.read(source, snapshot.selector); + const nextData = recycleNodesInto(snapshot.data, backup.data); + (backup: $FlowFixMe).data = nextData; // backup owns the snapshot and can safely mutate + subscription.backup = backup; + }); + } + + restoreSubscriptions() { + this._subscriptions.forEach(subscription => { + const backup = subscription.backup; + subscription.backup = null; + if (backup) { + if (backup.data !== subscription.snapshot.data) { + subscription.stale = true; + } + subscription.snapshot = { + data: subscription.snapshot.data, + isMissingData: backup.isMissingData, + seenRecords: backup.seenRecords, + selector: backup.selector, + missingRequiredFields: backup.missingRequiredFields, + }; + } else { + subscription.stale = true; + } + }); + } + + updateSubscriptions( + source: RecordSource, + updatedRecordIDs: UpdatedRecords, + updatedOwners: Array, + ) { + const hasUpdatedRecords = !isEmptyObject(updatedRecordIDs); + this._subscriptions.forEach(subscription => { + const owner = this._updateSubscription( + source, + subscription, + updatedRecordIDs, + hasUpdatedRecords, + ); + if (owner != null) { + updatedOwners.push(owner); + } + }); + } + + // Returns the owner (RequestDescriptor) if the subscription was affected by the + // latest update, or null if it was not affected. + _updateSubscription( + source: RecordSource, + subscription: Subscription, + updatedRecordIDs: UpdatedRecords, + hasUpdatedRecords: boolean, + ): ?RequestDescriptor { + const {backup, callback, snapshot, stale} = subscription; + const hasOverlappingUpdates = + hasUpdatedRecords && + hasOverlappingIDs(snapshot.seenRecords, updatedRecordIDs); + if (!stale && !hasOverlappingUpdates) { + return; + } + let nextSnapshot: Snapshot = + hasOverlappingUpdates || !backup + ? RelayReader.read(source, snapshot.selector) + : backup; + const nextData = recycleNodesInto(snapshot.data, nextSnapshot.data); + nextSnapshot = ({ + data: nextData, + isMissingData: nextSnapshot.isMissingData, + seenRecords: nextSnapshot.seenRecords, + selector: nextSnapshot.selector, + missingRequiredFields: nextSnapshot.missingRequiredFields, + }: Snapshot); + if (__DEV__) { + deepFreeze(nextSnapshot); + } + subscription.snapshot = nextSnapshot; + subscription.stale = false; + if (nextSnapshot.data !== snapshot.data) { + callback(nextSnapshot); + return snapshot.selector.owner; + } + } +} + +module.exports = RelayStoreSubscriptions;