-
Notifications
You must be signed in to change notification settings - Fork 1.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add RelayContext #698
Add RelayContext #698
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,269 @@ | ||
/** | ||
* 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 RelayContext | ||
* @typechecks | ||
* @flow | ||
*/ | ||
|
||
'use strict'; | ||
|
||
const GraphQLFragmentPointer = require('GraphQLFragmentPointer'); | ||
import type RelayMutation from 'RelayMutation'; | ||
import type RelayMutationTransaction from 'RelayMutationTransaction'; | ||
import type RelayQuery from 'RelayQuery'; | ||
const RelayQueryResultObservable = require('RelayQueryResultObservable'); | ||
const RelayStoreData = require('RelayStoreData'); | ||
const RelayTaskScheduler = require('RelayTaskScheduler'); | ||
|
||
const forEachRootCallArg = require('forEachRootCallArg'); | ||
const invariant = require('invariant'); | ||
const readRelayQueryData = require('readRelayQueryData'); | ||
const warning = require('warning'); | ||
|
||
import type { | ||
Abortable, | ||
Observable, | ||
RelayMutationTransactionCommitCallbacks, | ||
ReadyStateChangeCallback, | ||
StoreReaderData, | ||
StoreReaderOptions, | ||
} from 'RelayTypes'; | ||
|
||
import type { | ||
DataID, | ||
RelayQuerySet, | ||
} from 'RelayInternalTypes'; | ||
|
||
let defaultInstance; | ||
|
||
/** | ||
* @public | ||
* | ||
* RelayContext is a caching layer that records GraphQL response data and enables | ||
* resolving and subscribing to queries. | ||
* | ||
* === onReadyStateChange === | ||
* | ||
* Whenever Relay sends a request for data via GraphQL, an "onReadyStateChange" | ||
* callback can be supplied. This callback is called one or more times with a | ||
* `readyState` object with the following properties: | ||
* | ||
* aborted: Whether the request was aborted. | ||
* done: Whether all response data has been fetched. | ||
* error: An error in the event of a failure, or null if none. | ||
* ready: Whether the queries are at least partially resolvable. | ||
* stale: When resolvable during `forceFetch`, whether data is stale. | ||
* | ||
* If the callback is invoked with `aborted`, `done`, or a non-null `error`, the | ||
* callback will never be called again. Example usage: | ||
* | ||
* function onReadyStateChange(readyState) { | ||
* if (readyState.aborted) { | ||
* // Request was aborted. | ||
* } else if (readyState.error) { | ||
* // Failure occurred. | ||
* } else if (readyState.ready) { | ||
* // Queries are at least partially resolvable. | ||
* if (readyState.done) { | ||
* // Queries are completely resolvable. | ||
* } | ||
* } | ||
* } | ||
* | ||
*/ | ||
class RelayContext { | ||
_storeData: RelayStoreData; | ||
|
||
static getDefaultInstance(): RelayContext { | ||
if (!defaultInstance) { | ||
defaultInstance = new RelayContext(RelayStoreData.getDefaultInstance()); | ||
} | ||
return defaultInstance; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. see above |
||
|
||
constructor(storeData: RelayStoreData = new RelayStoreData()) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This type of "optional with a default" API seem easy to use but are too unpredictable - it's easy to forget to pass an instance somewhere and wonder why tests are failing. Let's require the instance to be passed in for now (which you're already doing in tests). |
||
this._storeData = storeData; | ||
} | ||
|
||
/** | ||
* @internal | ||
*/ | ||
getStoreData(): RelayStoreData { | ||
return this._storeData; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's remove this until we need it for something. |
||
|
||
/** | ||
* Primes the store by sending requests for any missing data that would be | ||
* required to satisfy the supplied set of queries. | ||
*/ | ||
primeCache( | ||
querySet: RelayQuerySet, | ||
callback: ReadyStateChangeCallback | ||
): Abortable { | ||
return this._storeData.getQueryRunner().run(querySet, callback); | ||
} | ||
|
||
/** | ||
* Forces the supplied set of queries to be fetched and written to the store. | ||
* Any data that previously satisfied the queries will be overwritten. | ||
*/ | ||
forceFetch( | ||
querySet: RelayQuerySet, | ||
callback: ReadyStateChangeCallback | ||
): Abortable { | ||
return this._storeData.getQueryRunner().forceFetch(querySet, callback); | ||
} | ||
|
||
/** | ||
* Reads query data anchored at the supplied data ID. | ||
*/ | ||
read( | ||
node: RelayQuery.Node, | ||
dataID: DataID, | ||
options?: StoreReaderOptions | ||
): ?StoreReaderData { | ||
return readRelayQueryData(this._storeData, node, dataID, options).data; | ||
} | ||
|
||
/** | ||
* Reads query data anchored at the supplied data IDs. | ||
*/ | ||
readAll( | ||
node: RelayQuery.Node, | ||
dataIDs: Array<DataID>, | ||
options?: StoreReaderOptions | ||
): Array<?StoreReaderData> { | ||
return dataIDs.map( | ||
dataID => readRelayQueryData(this._storeData, node, dataID, options).data | ||
); | ||
} | ||
|
||
/** | ||
* Reads query data, where each element in the result array corresponds to a | ||
* root call argument. If the root call has no arguments, the result array | ||
* will contain exactly one element. | ||
*/ | ||
readQuery( | ||
root: RelayQuery.Root, | ||
options?: StoreReaderOptions | ||
): Array<?StoreReaderData> { | ||
const storageKey = root.getStorageKey(); | ||
const results = []; | ||
forEachRootCallArg(root, identifyingArgValue => { | ||
let data; | ||
const dataID = this._storeData.getQueuedStore() | ||
.getDataID(storageKey, identifyingArgValue); | ||
if (dataID != null) { | ||
data = this.read(root, dataID, options); | ||
} | ||
results.push(data); | ||
}); | ||
return results; | ||
} | ||
|
||
/** | ||
* Reads and subscribes to query data anchored at the supplied data ID. The | ||
* returned observable emits updates as the data changes over time. | ||
*/ | ||
observe( | ||
fragment: RelayQuery.Fragment, | ||
dataID: DataID | ||
): Observable<?StoreReaderData> { | ||
const fragmentPointer = new GraphQLFragmentPointer( | ||
fragment.isPlural()? [dataID] : dataID, | ||
fragment | ||
); | ||
return new RelayQueryResultObservable(this._storeData, fragmentPointer); | ||
} | ||
|
||
applyUpdate( | ||
mutation: RelayMutation, | ||
callbacks?: RelayMutationTransactionCommitCallbacks | ||
): RelayMutationTransaction { | ||
return this._storeData.getMutationQueue().createTransaction( | ||
mutation, | ||
callbacks | ||
); | ||
} | ||
|
||
update( | ||
mutation: RelayMutation, | ||
callbacks?: RelayMutationTransactionCommitCallbacks | ||
): void { | ||
this.applyUpdate(mutation, callbacks).commit(); | ||
} | ||
|
||
/** | ||
* Initializes garbage collection: must be called before any records are | ||
* fetched. When records are collected after calls to `scheduleCollection` or | ||
* `scheduleCollectionFromNode`, records are collected in steps, with a | ||
* maximum of `stepLength` records traversed in a step. Steps are scheduled | ||
* via `RelayTaskScheduler`. | ||
*/ | ||
initializeGarbageCollection(stepLength: number): void { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm. Let's keep the |
||
invariant( | ||
stepLength > 0, | ||
'RelayGarbageCollection: step length must be greater than zero, got ' + | ||
'`%s`.', | ||
stepLength | ||
); | ||
this._storeData.initializeGarbageCollector(scheduler); | ||
|
||
const pendingQueryTracker = this._storeData.getPendingQueryTracker(); | ||
|
||
function scheduler(run: () => boolean): void { | ||
const runIteration = () => { | ||
// TODO: #9366746: integrate RelayRenderer/Container with GC hold | ||
warning( | ||
!pendingQueryTracker.hasPendingQueries(), | ||
'RelayGarbageCollection: GC is executing during a fetch, but the ' + | ||
'pending query may rely on data that is collected.' | ||
); | ||
let iterations = 0; | ||
let hasNext = true; | ||
while (hasNext && (stepLength < 0 || iterations < stepLength)) { | ||
hasNext = run(); | ||
iterations++; | ||
} | ||
// This is effectively a (possibly async) `while` loop | ||
if (hasNext) { | ||
RelayTaskScheduler.enqueue(runIteration); | ||
} | ||
}; | ||
RelayTaskScheduler.enqueue(runIteration); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for considering this, but let's move the scheduler back to |
||
} | ||
|
||
/** | ||
* Collects any un-referenced records in the store. | ||
*/ | ||
scheduleGarbageCollection(): void { | ||
const garbageCollector = this._storeData.getGarbageCollector(); | ||
|
||
if (garbageCollector) { | ||
garbageCollector.collect(); | ||
} | ||
} | ||
|
||
/** | ||
* Collects any un-referenced records reachable from the given record via | ||
* graph traversal of fields. | ||
* | ||
* NOTE: If the given record is still referenced, no records are collected. | ||
*/ | ||
scheduleGarbageCollectionFromNode(dataID: DataID): void { | ||
const garbageCollector = this._storeData.getGarbageCollector(); | ||
|
||
if (garbageCollector) { | ||
garbageCollector.collectFromNode(dataID); | ||
} | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 for adding these methods here |
||
|
||
module.exports = RelayContext; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,13 +14,7 @@ | |
'use strict'; | ||
|
||
import type {DataID} from 'RelayInternalTypes'; | ||
const RelayStoreData = require('RelayStoreData'); | ||
const RelayTaskScheduler = require('RelayTaskScheduler'); | ||
|
||
const invariant = require('invariant'); | ||
const warning = require('warning'); | ||
|
||
let _stepLength = -1; // collect in a single pass by default | ||
const RelayContext = require('RelayContext'); | ||
|
||
/** | ||
* Public API for controlling garbage collection of `RelayStoreData`. | ||
|
@@ -37,28 +31,14 @@ var RelayGarbageCollection = { | |
* via `RelayTaskScheduler`. | ||
*/ | ||
initialize(stepLength: number): void { | ||
invariant( | ||
stepLength > 0, | ||
'RelayGarbageCollection: step length must be greater than zero, got ' + | ||
'`%s`.', | ||
stepLength | ||
); | ||
_stepLength = stepLength; | ||
RelayStoreData | ||
.getDefaultInstance() | ||
.initializeGarbageCollector(scheduler); | ||
RelayContext.getDefaultInstance().initializeGarbageCollection(stepLength); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For context, this is a helper class and not part of the public API, so it can target the default instance specifically. |
||
}, | ||
|
||
/** | ||
* Collects any un-referenced records in the store. | ||
*/ | ||
scheduleCollection(): void { | ||
var garbageCollector = | ||
RelayStoreData.getDefaultInstance().getGarbageCollector(); | ||
|
||
if (garbageCollector) { | ||
garbageCollector.collect(); | ||
} | ||
RelayContext.getDefaultInstance().scheduleGarbageCollection(); | ||
}, | ||
|
||
/** | ||
|
@@ -68,37 +48,8 @@ var RelayGarbageCollection = { | |
* NOTE: If the given record is still referenced, no records are collected. | ||
*/ | ||
scheduleCollectionFromNode(dataID: DataID): void { | ||
var garbageCollector = | ||
RelayStoreData.getDefaultInstance().getGarbageCollector(); | ||
|
||
if (garbageCollector) { | ||
garbageCollector.collectFromNode(dataID); | ||
} | ||
RelayContext.getDefaultInstance().scheduleGarbageCollectionFromNode(dataID); | ||
}, | ||
}; | ||
|
||
function scheduler(run: () => boolean): void { | ||
const pendingQueryTracker = | ||
RelayStoreData.getDefaultInstance().getPendingQueryTracker(); | ||
const runIteration = () => { | ||
// TODO: #9366746: integrate RelayRenderer/Container with GC hold | ||
warning( | ||
!pendingQueryTracker.hasPendingQueries(), | ||
'RelayGarbageCollection: GC is executing during a fetch, but the ' + | ||
'pending query may rely on data that is collected.' | ||
); | ||
let iterations = 0; | ||
let hasNext = true; | ||
while (hasNext && (_stepLength < 0 || iterations < _stepLength)) { | ||
hasNext = run(); | ||
iterations++; | ||
} | ||
// This is effectively a (possibly async) `while` loop | ||
if (hasNext) { | ||
RelayTaskScheduler.enqueue(runIteration); | ||
} | ||
}; | ||
RelayTaskScheduler.enqueue(runIteration); | ||
} | ||
|
||
module.exports = RelayGarbageCollection; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's use
RelayStore
as the default instance, so we don't need a default instance orgetDefaultInstance
here