Skip to content

Commit

Permalink
Extract store subscription management into separate module
Browse files Browse the repository at this point in the history
Reviewed By: josephsavona, kassens

Differential Revision: D24964756

fbshipit-source-id: ef93462be9bb745f0cff47dd2500f72763820d43
  • Loading branch information
Juan Tejada authored and facebook-github-bot committed Nov 19, 2020
1 parent f29f08f commit d64b579
Show file tree
Hide file tree
Showing 2 changed files with 172 additions and 105 deletions.
116 changes: 11 additions & 105 deletions packages/relay-runtime/store/RelayModernStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -55,13 +55,6 @@ export opaque type InvalidationState = {|
invalidations: Map<DataID, ?number>,
|};

type Subscription = {|
callback: (snapshot: Snapshot) => void,
snapshot: Snapshot,
stale: boolean,
backup: ?Snapshot,
|};

type InvalidationSubscription = {|
callback: () => void,
invalidationState: InvalidationState,
Expand Down Expand Up @@ -107,7 +100,7 @@ class RelayModernStore implements Store {
|},
>;
_shouldScheduleGC: boolean;
_subscriptions: Set<Subscription>;
_storeSubscriptions: RelayStoreSubscriptions;
_updatedRecordIDs: UpdatedRecords;

constructor(
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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<DataID>): InvalidationState {
const invalidations = new Map();
dataIDs.forEach(dataID => {
Expand Down Expand Up @@ -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;
Expand All @@ -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() {
Expand Down
161 changes: 161 additions & 0 deletions packages/relay-runtime/store/RelayStoreSubscriptions.js
Original file line number Diff line number Diff line change
@@ -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<Subscription>;

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<RequestDescriptor>,
) {
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;

0 comments on commit d64b579

Please sign in to comment.