Skip to content
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

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
269 changes: 269 additions & 0 deletions src/store/RelayContext.js
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;
Copy link
Contributor

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 or getDefaultInstance here


/**
* @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;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see above


constructor(storeData: RelayStoreData = new RelayStoreData()) {
Copy link
Contributor

Choose a reason for hiding this comment

The 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;
}
Copy link
Contributor

Choose a reason for hiding this comment

The 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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. Let's keep the scheduler implementation in RelayGarbageCollection and make this a simple proxy method through to the initialize method on RelayStoreData. The signature here can then be initializeGarbageCollection(scheduler).

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);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for considering this, but let's move the scheduler back to RelayGarbageCollection

}

/**
* 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);
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 for adding these methods here


module.exports = RelayContext;
57 changes: 4 additions & 53 deletions src/store/RelayGarbageCollection.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand All @@ -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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RelayStore.initializeGarbageCollection(...)

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();
},

/**
Expand All @@ -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;
Loading