react-relay
Version:
A framework for building data-driven React applications.
292 lines (245 loc) • 10.8 kB
JavaScript
/**
* Copyright 2013-2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule RelayStoreGarbageCollector
*
* @typechecks
*/
'use strict';
var _classCallCheck = require('babel-runtime/helpers/class-call-check')['default'];
var GraphQLStoreDataHandler = require('./GraphQLStoreDataHandler');
var RelayBufferedNeglectionStateMap = require('./RelayBufferedNeglectionStateMap');
var RelayNeglectionStateMap = require('./RelayNeglectionStateMap');
var RelayProfiler = require('./RelayProfiler');
var RelayTaskScheduler = require('./RelayTaskScheduler');
var forEachObject = require('fbjs/lib/forEachObject');
var resolveImmediate = require('fbjs/lib/resolveImmediate');
var RANGE = '__range__';
/**
* @internal
*
* Provides a garbage collector.
*/
var RelayStoreGarbageCollector = (function () {
function RelayStoreGarbageCollector(relayStoreData) {
_classCallCheck(this, RelayStoreGarbageCollector);
this._directNeglectionStates = new RelayNeglectionStateMap();
this._bufferedNeglectionStates = new RelayBufferedNeglectionStateMap(this._directNeglectionStates);
this._neglectionStates = this._bufferedNeglectionStates;
this._relayStoreData = relayStoreData;
this._cycles = 0;
}
/**
* Checks if the given object is a linked record with a client-site DataID
*/
/**
* Removes all data for registered dataIDs that are set to collectible and
* have no subscriptions
*/
RelayStoreGarbageCollector.prototype.scheduleCollection = function scheduleCollection(stepLength) {
var _this = this;
this._bufferedNeglectionStates.flushBuffer();
var iterator = this._neglectionStates.values();
var currentCycle = ++this._cycles;
// During garbage collection we don't want to buffer any changes.
this._neglectionStates = this._directNeglectionStates;
RelayTaskScheduler.await(function () {
return _this._collectGarbageStep(currentCycle, iterator, iterator.next(), stepLength);
});
};
/**
* Removes registered DataIDs that are eligible for removal in steps of length
* `stepLength`, starting with the record referenced by the `NeglectionState`
* in `offset`.
*
* `_collectGarbageInSteps` will invoke itself using `RelayTaskScheduler`
* until all registered DataIDs have been processed (either removed or flagged
* as collectible). If `collect` is invoked before a run of
* `_collectGarbageInSteps` has recursively processed all DataIDs the
* remaining DataIDs will only be marked as collectible and no attempt to
* remove these DataID will be done.
*/
RelayStoreGarbageCollector.prototype._collectGarbageStep = function _collectGarbageStep(currentCycle, remainingDataIDs, offset, stepLength) {
var _this2 = this;
var iterator = offset;
var neglectionState;
// Check if the current collection cycle is still the most recent one. If
// there was a subsequent call to `collect` we mark all not yet processed
// DataIDs as collectible.
if (currentCycle !== this._cycles) {
for (iterator = offset; !iterator.done; iterator = remainingDataIDs.next()) {
var _iterator = iterator;
neglectionState = _iterator.value;
if (neglectionState) {
neglectionState.collectible = true;
}
}
return;
}
// Iterate over registered DataIDs until `_stepLength` records were seen or
// all registered records were processed.
iterator = offset;
var profileState = {
count: -1,
stepLength: stepLength
};
var profile = RelayProfiler.profile('RelayStoreGarbageCollector.collect', profileState);
var recordsBefore = this._neglectionStates.size();
var seenRecords = 0;
for (iterator = offset; !iterator.done && (stepLength == null || seenRecords < stepLength); iterator = remainingDataIDs.next()) {
var _iterator2 = iterator;
neglectionState = _iterator2.value;
if (neglectionState) {
if (this._isCollectible(neglectionState)) {
seenRecords += this._removeRecordAndDescendentClientRecords(neglectionState.dataID);
} else {
seenRecords++;
}
neglectionState.collectible = true;
}
}
var recordsAfter = this._neglectionStates.size();
profileState.count = recordsBefore - recordsAfter;
profile.stop();
// Schedule next run if there are records left that have not been processed.
if (!iterator.done) {
resolveImmediate(function () {
return RelayTaskScheduler.await(function () {
return _this2._collectGarbageStep(currentCycle, remainingDataIDs, iterator, stepLength);
});
});
} else {
this._neglectionStates = this._bufferedNeglectionStates;
}
};
/**
* Decreases the number of subscriptions for the given dataID by 1
*/
RelayStoreGarbageCollector.prototype.decreaseSubscriptionsFor = function decreaseSubscriptionsFor(dataID) {
this._neglectionStates.decreaseSubscriptionsFor(dataID);
};
/**
* Increases the number of subscriptions for the given dataID by 1. If the
* dataID is not yet registered it will be registered.
*/
RelayStoreGarbageCollector.prototype.increaseSubscriptionsFor = function increaseSubscriptionsFor(dataID) {
this._neglectionStates.increaseSubscriptionsFor(dataID);
};
/**
* Makes the Garbage Collector aware of dataID and make it its responsibility
* to clean the data if possible.
*/
RelayStoreGarbageCollector.prototype.register = function register(dataID) {
this._neglectionStates.register(dataID);
};
/**
* Checks if a record can be garbage collected based on its neglection state.
*
* A record is collectible if the collectible flag is set to true and there
* are no active subscriptions to the record. Due to current limitations we
* are further limited to only collecting records that are refetchable. (I.e.
* have a server ID or are a range of records.)
*/
RelayStoreGarbageCollector.prototype._isCollectible = function _isCollectible(neglectionState) {
var isEligibleForCollection = neglectionState.collectible && !neglectionState.subscriptions;
var queuedStore = this._relayStoreData.getQueuedStore();
return isEligibleForCollection && (!GraphQLStoreDataHandler.isClientID(neglectionState.dataID) || queuedStore.hasRange(neglectionState.dataID));
};
/**
* Removes the record identified by the given DataID and any descendent
* records that have client-site DataIDs.
*/
RelayStoreGarbageCollector.prototype._removeRecordAndDescendentClientRecords = function _removeRecordAndDescendentClientRecords(dataID) {
var records = this._relayStoreData.getNodeData();
var queuedRecords = this._relayStoreData.getQueuedData();
var cachedRecords = this._relayStoreData.getCachedData();
var removalStatusMap = {};
removalStatusMap[dataID] = 'pending';
var removedRecords = 0;
// Since the descendant records in the different stores might differ we
// extract the client-site DataIDs for the record out of all stores.
var remainingRecords = [records[dataID], queuedRecords[dataID], cachedRecords[dataID]];
// If `field` contains a linked record and the linked record has a
// client-site DataID the record will be added to `remainingRecords` and
// it's DataID will be set to `true` in `removalStatusMap`.
function enqueueField(field) {
var dataID = getClientIDFromLinkedRecord(field);
// If we have a dataID we haven't seen before we add it to the remaining
// records
if (dataID && !removalStatusMap[dataID]) {
removalStatusMap[dataID] = 'pending';
remainingRecords.push(records[dataID], queuedRecords[dataID], cachedRecords[dataID]);
}
}
while (remainingRecords.length) {
var currentRecord = remainingRecords.shift();
if (currentRecord && typeof currentRecord === 'object') {
// Special handling for `GraphQLRange` data, which isn't stored like
// other node data.
var range = currentRecord[RANGE];
if (range) {
// Wrapping each dataID in a object that resembles an record so
// `enqueueField` can handle it.
range.getEdgeIDs().forEach(function (id) {
return enqueueField({ __dataID__: id });
});
} else {
// Walk all fields of the record, skipping meta-fields and adding any
// linked records with a client-site DataID to `remainingRecords`.
forEachObject(currentRecord, function (field, fieldName) {
if (GraphQLStoreDataHandler.isMetadataKey(fieldName)) {
return;
}
// Handling plural linked fields
if (Array.isArray(field)) {
field.forEach(enqueueField);
} else {
enqueueField(field);
}
});
}
var currentDataID = GraphQLStoreDataHandler.getID(currentRecord);
if (currentDataID && removalStatusMap[currentDataID] === 'pending') {
this._removeRecord(currentRecord);
removalStatusMap[currentDataID] = 'removed';
removedRecords++;
}
}
}
return removedRecords;
};
/**
* Removes the records identified by `dataID` from the queued-store, the
* query-tracker, and the garbage-collector itself.
*/
RelayStoreGarbageCollector.prototype._removeRecord = function _removeRecord(record) {
var dataID = record.__dataID__;
this._relayStoreData.getQueryTracker().untrackNodesForID(dataID);
this._relayStoreData.getQueuedStore().removeRecord(dataID);
this._neglectionStates.remove(dataID);
};
return RelayStoreGarbageCollector;
})();
function getClientIDFromLinkedRecord(field) {
if (!field || typeof field !== 'object') {
return null;
}
// Downcast to `any`-type since it can be upcasted to any other type.
// We checked field is an object before and can be sure this cast is safe.
var dataID = GraphQLStoreDataHandler.getID(field);
if (dataID && GraphQLStoreDataHandler.isClientID(dataID)) {
return dataID;
}
return null;
}
RelayProfiler.instrumentMethods(RelayStoreGarbageCollector.prototype, {
decreaseSubscriptionsFor: 'RelayStoreGarbageCollector.prototype.decreaseSubscriptionsFor',
increaseSubscriptionsFor: 'RelayStoreGarbageCollector.prototype.increaseSubscriptionsFor',
register: 'RelayStoreGarbageCollector.prototype.register'
});
module.exports = RelayStoreGarbageCollector;