Skip to content

Commit

Permalink
Make GraphQLStoreChangeEmitter contextual
Browse files Browse the repository at this point in the history
Summary: Converts `ChangeEmitter` to a class instead of singleton and holds an instance in `RelayStoreData`. Note that `QueryResolver` was already contextualized (it accepts an instance of `RelayRecordStore`) but it now needs the `RelayStoreData` instance to get access to the change emitter.

Original:Builds on #562. Part of #558
Closes #565

Reviewed By: yungsters

Differential Revision: D2632148

fb-gh-sync-id: b5073151d6459c018bc56654a9deb8bb407fcc41
  • Loading branch information
devknoll authored and facebook-github-bot-9 committed Nov 12, 2015
1 parent 6c41756 commit aa29ea5
Show file tree
Hide file tree
Showing 10 changed files with 201 additions and 159 deletions.
11 changes: 5 additions & 6 deletions src/container/RelayContainer.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
import type {ConcreteFragment} from 'ConcreteQuery';
var ErrorUtils = require('ErrorUtils');
var GraphQLFragmentPointer = require('GraphQLFragmentPointer');
var GraphQLStoreChangeEmitter = require('GraphQLStoreChangeEmitter');
var GraphQLStoreDataHandler = require('GraphQLStoreDataHandler');
var GraphQLStoreQueryResolver = require('GraphQLStoreQueryResolver');
var React = require('React');
Expand Down Expand Up @@ -74,17 +73,17 @@ export type RootQueries = {
[queryName: string]: RelayQLQueryBuilder;
};

GraphQLStoreChangeEmitter.injectBatchingStrategy(
ReactDOM.unstable_batchedUpdates
);

var containerContextTypes = {
route: RelayPropTypes.QueryConfig.isRequired,
};
var nextContainerID = 0;

var storeData = RelayStoreData.getDefaultInstance();

storeData.getChangeEmitter().injectBatchingStrategy(
ReactDOM.unstable_batchedUpdates
);

/**
* @public
*
Expand Down Expand Up @@ -551,7 +550,7 @@ function createContainerComponent(
}
} else if (!queryResolver) {
queryResolver = new GraphQLStoreQueryResolver(
storeData.getQueuedStore(),
storeData,
fragmentPointer,
this._handleFragmentDataUpdate.bind(this)
);
Expand Down
128 changes: 73 additions & 55 deletions src/legacy/store/GraphQLStoreChangeEmitter.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,17 @@ var GraphQLStoreRangeUtils = require('GraphQLStoreRangeUtils');

var resolveImmediate = require('resolveImmediate');

type BatchStrategy = (callback: Function) => void;
type SubscriptionCallback = () => void;

export type ChangeSubscription = {
remove: () => void;
remove: SubscriptionCallback;
};

var batchUpdate = callback => callback();
var subscribers = [];

var executingIDs = {};
var scheduledIDs = null;
type Subscriber = {
callback: SubscriptionCallback,
subscribedIDs: Array<string>,
};

/**
* Asynchronous change emitter for nodes stored in the Relay cache.
Expand All @@ -37,73 +39,89 @@ var scheduledIDs = null;
*
* @internal
*/
var GraphQLStoreChangeEmitter = {
class GraphQLStoreChangeEmitter {
_batchUpdate: BatchStrategy;
_subscribers: Array<Subscriber>;

addListenerForIDs: function(
_executingIDs: Object;
_scheduledIDs: ?Object;

constructor() {
this._batchUpdate = callback => callback();
this._subscribers = [];

this._executingIDs = {};
this._scheduledIDs = null;
}

addListenerForIDs(
ids: Array<string>,
callback: () => void
callback: SubscriptionCallback
): ChangeSubscription {
var subscribedIDs = ids.map(getBroadcastID);
var index = subscribers.length;
subscribers.push({subscribedIDs, callback});
var index = this._subscribers.length;
this._subscribers.push({subscribedIDs, callback});
return {
remove: function() {
delete subscribers[index];
}
remove: () => {
delete this._subscribers[index];
},
};
},
}

broadcastChangeForID: function(id: string): void {
if (scheduledIDs === null) {
resolveImmediate(processBroadcasts);
scheduledIDs = {};
broadcastChangeForID(id: string): void {
var scheduledIDs = this._scheduledIDs;
if (scheduledIDs == null) {
resolveImmediate(() => this._processBroadcasts());
scheduledIDs = this._scheduledIDs = {};
}
// Record index of the last subscriber so we do not later unintentionally
// invoke callbacks that were subscribed after this broadcast.
scheduledIDs[getBroadcastID(id)] = subscribers.length - 1;
},
scheduledIDs[getBroadcastID(id)] = this._subscribers.length - 1;
}

injectBatchingStrategy: function(batchStrategy: Function): void {
batchUpdate = batchStrategy;
},
injectBatchingStrategy(batchStrategy: BatchStrategy): void {
this._batchUpdate = batchStrategy;
}

_processBroadcasts(): void {
if (this._scheduledIDs) {
this._executingIDs = this._scheduledIDs;
this._scheduledIDs = null;
this._batchUpdate(() => this._processSubscribers());
}
}

/**
* Exposed for profiling reasons.
* @private
*/
_processSubscribers: processSubscribers

};

function processBroadcasts(): void {
if (scheduledIDs) {
executingIDs = scheduledIDs;
scheduledIDs = null;
batchUpdate(processSubscribers);
_processSubscribers(): void {
this._subscribers.forEach((subscriber, subscriberIndex) =>
this._processSubscriber(subscriber, subscriberIndex)
);
}
}

function processSubscribers(): void {
subscribers.forEach(processSubscriber);
}

function processSubscriber({subscribedIDs, callback}, subscriberIndex): void {
for (var broadcastID in executingIDs) {
if (executingIDs.hasOwnProperty(broadcastID)) {
var broadcastIndex = executingIDs[broadcastID];
if (broadcastIndex < subscriberIndex) {
// Callback was subscribed after this particular broadcast.
break;
}
if (subscribedIDs.indexOf(broadcastID) >= 0) {
ErrorUtils.applyWithGuard(
callback,
null,
null,
null,
'GraphQLStoreChangeEmitter'
);
break;
_processSubscriber(
{subscribedIDs, callback}: Subscriber,
subscriberIndex: number
): void {
for (var broadcastID in this._executingIDs) {
if (this._executingIDs.hasOwnProperty(broadcastID)) {
var broadcastIndex = this._executingIDs[broadcastID];
if (broadcastIndex < subscriberIndex) {
// Callback was subscribed after this particular broadcast.
break;
}
if (subscribedIDs.indexOf(broadcastID) >= 0) {
ErrorUtils.applyWithGuard(
callback,
null,
null,
null,
'GraphQLStoreChangeEmitter'
);
break;
}
}
}
}
Expand Down
57 changes: 28 additions & 29 deletions src/legacy/store/GraphQLStoreQueryResolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,13 @@

import type {ChangeSubscription} from 'GraphQLStoreChangeEmitter';
import type GraphQLFragmentPointer from 'GraphQLFragmentPointer';
var GraphQLStoreChangeEmitter = require('GraphQLStoreChangeEmitter');
var GraphQLStoreRangeUtils = require('GraphQLStoreRangeUtils');
import type RelayStoreGarbageCollector from 'RelayStoreGarbageCollector';
import type {DataID} from 'RelayInternalTypes';
var RelayProfiler = require('RelayProfiler');
import type RelayQuery from 'RelayQuery';
import type RelayRecordStore from 'RelayRecordStore';
var RelayStoreData = require('RelayStoreData');
import type RelayStoreData from 'RelayStoreData';
import type {StoreReaderData} from 'RelayTypes';

var filterExclusiveKeys = require('filterExclusiveKeys');
Expand All @@ -46,18 +45,18 @@ class GraphQLStoreQueryResolver {
GraphQLStorePluralQueryResolver |
GraphQLStoreSingleQueryResolver
);
_store: RelayRecordStore;
_storeData: RelayStoreData;

constructor(
store: RelayRecordStore,
storeData: RelayStoreData,
fragmentPointer: GraphQLFragmentPointer,
callback: Function
) {
this.reset();
this._callback = callback;
this._fragmentPointer = fragmentPointer;
this._resolver = null;
this._store = store;
this._storeData = storeData;
}

/**
Expand All @@ -76,8 +75,8 @@ class GraphQLStoreQueryResolver {
var resolver = this._resolver;
if (!resolver) {
resolver = this._fragmentPointer.getFragment().isPlural() ?
new GraphQLStorePluralQueryResolver(this._store, this._callback) :
new GraphQLStoreSingleQueryResolver(this._store, this._callback);
new GraphQLStorePluralQueryResolver(this._storeData, this._callback) :
new GraphQLStoreSingleQueryResolver(this._storeData, this._callback);
this._resolver = resolver;
}
return resolver.resolve(fragmentPointer);
Expand All @@ -91,12 +90,12 @@ class GraphQLStorePluralQueryResolver {
_callback: Function;
_resolvers: Array<GraphQLStoreSingleQueryResolver>;
_results: Array<?StoreReaderData>;
_store: RelayRecordStore;
_storeData: RelayStoreData;

constructor(store: RelayRecordStore, callback: Function) {
constructor(storeData: RelayStoreData, callback: Function) {
this.reset();
this._callback = callback;
this._store = store;
this._storeData = storeData;
}

reset(): void {
Expand Down Expand Up @@ -126,7 +125,7 @@ class GraphQLStorePluralQueryResolver {
// Ensure that we have exactly `nextLength` resolvers.
while (resolvers.length < nextLength) {
resolvers.push(
new GraphQLStoreSingleQueryResolver(this._store, this._callback)
new GraphQLStoreSingleQueryResolver(this._storeData, this._callback)
);
}
while (resolvers.length > nextLength) {
Expand Down Expand Up @@ -162,16 +161,15 @@ class GraphQLStoreSingleQueryResolver {
_hasDataChanged: boolean;
_result: ?StoreReaderData;
_resultID: ?DataID;
_store: RelayRecordStore;
_storeData: RelayStoreData;
_subscribedIDs: DataIDSet;
_subscription: ?ChangeSubscription;

constructor(store: RelayRecordStore, callback: Function) {
constructor(storeData: RelayStoreData, callback: Function) {
this.reset();
this._callback = callback;
this._garbageCollector =
RelayStoreData.getDefaultInstance().getGarbageCollector();
this._store = store;
this._garbageCollector = storeData.getGarbageCollector();
this._storeData = storeData;
this._subscribedIDs = {};
}

Expand Down Expand Up @@ -209,7 +207,7 @@ class GraphQLStoreSingleQueryResolver {
if (
prevFragment != null &&
prevID != null &&
getCanonicalID(prevID) === getCanonicalID(nextID)
this._getCanonicalID(prevID) === this._getCanonicalID(nextID)
) {
if (
prevID !== nextID ||
Expand All @@ -219,7 +217,7 @@ class GraphQLStoreSingleQueryResolver {
// same canonical ID,
// but the data, call(s), route, and/or variables have changed
[nextResult, subscribedIDs] = resolveFragment(
this._store,
this._storeData.getQueuedStore(),
nextFragment,
nextID
);
Expand All @@ -231,7 +229,7 @@ class GraphQLStoreSingleQueryResolver {
} else {
// Pointer has a different ID or is/was fake data.
[nextResult, subscribedIDs] = resolveFragment(
this._store,
this._storeData.getQueuedStore(),
nextFragment,
nextID
);
Expand All @@ -246,7 +244,8 @@ class GraphQLStoreSingleQueryResolver {
if (subscribedIDs) {
// always subscribe to the root ID
subscribedIDs[nextID] = true;
this._subscription = GraphQLStoreChangeEmitter.addListenerForIDs(
var changeEmitter = this._storeData.getChangeEmitter();
this._subscription = changeEmitter.addListenerForIDs(
Object.keys(subscribedIDs),
this._handleChange.bind(this)
);
Expand All @@ -263,6 +262,15 @@ class GraphQLStoreSingleQueryResolver {
return this._result;
}

/**
* Ranges publish events for the entire range, not the specific view of that
* range. For example, if "client:1" is a range, the event is on "client:1",
* not "client:1_first(5)".
*/
_getCanonicalID(id: DataID): DataID {
return GraphQLStoreRangeUtils.getCanonicalClientID(id);
}

_handleChange(): void {
if (!this._hasDataChanged) {
this._hasDataChanged = true;
Expand Down Expand Up @@ -297,15 +305,6 @@ function resolveFragment(
return [data, dataIDs];
}

/**
* Ranges publish events for the entire range, not the specific view of that
* range. For example, if "client:1" is a range, the event is on "client:1",
* not "client:1_first(5)".
*/
function getCanonicalID(id: DataID): DataID {
return GraphQLStoreRangeUtils.getCanonicalClientID(id);
}

RelayProfiler.instrumentMethods(GraphQLStoreQueryResolver.prototype, {
resolve: 'GraphQLStoreQueryResolver.resolve'
});
Expand Down
18 changes: 11 additions & 7 deletions src/legacy/store/__mocks__/GraphQLStoreChangeEmitter.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,17 @@

var GraphQLStoreChangeEmitter = jest.genMockFromModule('GraphQLStoreChangeEmitter');

GraphQLStoreChangeEmitter.addListenerForIDs.mock.remove = [];
GraphQLStoreChangeEmitter.addListenerForIDs.mockImplementation(() => {
var returnValue = {remove: jest.genMockFunction()};
GraphQLStoreChangeEmitter.addListenerForIDs.mock.remove.push(
returnValue.remove
);
return returnValue;
GraphQLStoreChangeEmitter.mockImplementation(function() {
this.addListenerForIDs.mock.remove = [];
this.addListenerForIDs.mockImplementation(() => {
var returnValue = {remove: jest.genMockFunction()};
this.addListenerForIDs.mock.remove.push(
returnValue.remove
);
return returnValue;
});

return this;
});

module.exports = GraphQLStoreChangeEmitter;
Loading

0 comments on commit aa29ea5

Please sign in to comment.