diff --git a/src/container/RelayContainer.js b/src/container/RelayContainer.js index 08c5209ced5f9..dc3629dbb9f55 100644 --- a/src/container/RelayContainer.js +++ b/src/container/RelayContainer.js @@ -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'); @@ -74,10 +73,6 @@ export type RootQueries = { [queryName: string]: RelayQLQueryBuilder; }; -GraphQLStoreChangeEmitter.injectBatchingStrategy( - ReactDOM.unstable_batchedUpdates -); - var containerContextTypes = { route: RelayPropTypes.QueryConfig.isRequired, }; @@ -85,6 +80,10 @@ var nextContainerID = 0; var storeData = RelayStoreData.getDefaultInstance(); +storeData.getChangeEmitter().injectBatchingStrategy( + ReactDOM.unstable_batchedUpdates +); + /** * @public * @@ -551,7 +550,7 @@ function createContainerComponent( } } else if (!queryResolver) { queryResolver = new GraphQLStoreQueryResolver( - storeData.getQueuedStore(), + storeData, fragmentPointer, this._handleFragmentDataUpdate.bind(this) ); diff --git a/src/legacy/store/GraphQLStoreChangeEmitter.js b/src/legacy/store/GraphQLStoreChangeEmitter.js index 8270186075130..a8acc9b50cd2c 100644 --- a/src/legacy/store/GraphQLStoreChangeEmitter.js +++ b/src/legacy/store/GraphQLStoreChangeEmitter.js @@ -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, +}; /** * Asynchronous change emitter for nodes stored in the Relay cache. @@ -37,73 +39,89 @@ var scheduledIDs = null; * * @internal */ -var GraphQLStoreChangeEmitter = { +class GraphQLStoreChangeEmitter { + _batchUpdate: BatchStrategy; + _subscribers: Array; - addListenerForIDs: function( + _executingIDs: Object; + _scheduledIDs: ?Object; + + constructor() { + this._batchUpdate = callback => callback(); + this._subscribers = []; + + this._executingIDs = {}; + this._scheduledIDs = null; + } + + addListenerForIDs( ids: Array, - 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; + } } } } diff --git a/src/legacy/store/GraphQLStoreQueryResolver.js b/src/legacy/store/GraphQLStoreQueryResolver.js index e5012038616d4..e879e5a2dbdae 100644 --- a/src/legacy/store/GraphQLStoreQueryResolver.js +++ b/src/legacy/store/GraphQLStoreQueryResolver.js @@ -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'); @@ -46,10 +45,10 @@ class GraphQLStoreQueryResolver { GraphQLStorePluralQueryResolver | GraphQLStoreSingleQueryResolver ); - _store: RelayRecordStore; + _storeData: RelayStoreData; constructor( - store: RelayRecordStore, + storeData: RelayStoreData, fragmentPointer: GraphQLFragmentPointer, callback: Function ) { @@ -57,7 +56,7 @@ class GraphQLStoreQueryResolver { this._callback = callback; this._fragmentPointer = fragmentPointer; this._resolver = null; - this._store = store; + this._storeData = storeData; } /** @@ -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); @@ -91,12 +90,12 @@ class GraphQLStorePluralQueryResolver { _callback: Function; _resolvers: Array; _results: Array; - _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 { @@ -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) { @@ -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 = {}; } @@ -209,7 +207,7 @@ class GraphQLStoreSingleQueryResolver { if ( prevFragment != null && prevID != null && - getCanonicalID(prevID) === getCanonicalID(nextID) + this._getCanonicalID(prevID) === this._getCanonicalID(nextID) ) { if ( prevID !== nextID || @@ -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 ); @@ -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 ); @@ -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) ); @@ -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; @@ -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' }); diff --git a/src/legacy/store/__mocks__/GraphQLStoreChangeEmitter.js b/src/legacy/store/__mocks__/GraphQLStoreChangeEmitter.js index 2d7a72a878385..0130036f09319 100644 --- a/src/legacy/store/__mocks__/GraphQLStoreChangeEmitter.js +++ b/src/legacy/store/__mocks__/GraphQLStoreChangeEmitter.js @@ -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; diff --git a/src/legacy/store/__tests__/GraphQLStoreChangeEmitter-test.js b/src/legacy/store/__tests__/GraphQLStoreChangeEmitter-test.js index 049ee72295446..17a2412d76b2f 100644 --- a/src/legacy/store/__tests__/GraphQLStoreChangeEmitter-test.js +++ b/src/legacy/store/__tests__/GraphQLStoreChangeEmitter-test.js @@ -18,11 +18,14 @@ var GraphQLStoreChangeEmitter = require('GraphQLStoreChangeEmitter'); var GraphQLStoreRangeUtils = require('GraphQLStoreRangeUtils'); describe('GraphQLStoreChangeEmitter', () => { + var changeEmitter; var mockCallback; beforeEach(() => { jest.resetModuleRegistry(); + changeEmitter = new GraphQLStoreChangeEmitter(); + GraphQLStoreRangeUtils.getCanonicalClientID.mockImplementation(id => id); ErrorUtils.applyWithGuard.mockImplementation(callback => { @@ -34,8 +37,8 @@ describe('GraphQLStoreChangeEmitter', () => { }); it('should broadcast changes asynchronously', () => { - GraphQLStoreChangeEmitter.addListenerForIDs(['foo'], mockCallback); - GraphQLStoreChangeEmitter.broadcastChangeForID('foo'); + changeEmitter.addListenerForIDs(['foo'], mockCallback); + changeEmitter.broadcastChangeForID('foo'); expect(mockCallback).not.toBeCalled(); jest.runAllTimers(); @@ -43,8 +46,8 @@ describe('GraphQLStoreChangeEmitter', () => { }); it('should broadcast exclusively to subscribed IDs', () => { - GraphQLStoreChangeEmitter.addListenerForIDs(['foo'], mockCallback); - GraphQLStoreChangeEmitter.broadcastChangeForID('bar'); + changeEmitter.addListenerForIDs(['foo'], mockCallback); + changeEmitter.broadcastChangeForID('bar'); jest.runAllTimers(); @@ -52,8 +55,8 @@ describe('GraphQLStoreChangeEmitter', () => { }); it('should not broadcast to removed callbacks', () => { - GraphQLStoreChangeEmitter.addListenerForIDs(['foo'], mockCallback).remove(); - GraphQLStoreChangeEmitter.broadcastChangeForID('foo'); + changeEmitter.addListenerForIDs(['foo'], mockCallback).remove(); + changeEmitter.broadcastChangeForID('foo'); jest.runAllTimers(); @@ -61,8 +64,8 @@ describe('GraphQLStoreChangeEmitter', () => { }); it('should only invoke callbacks subscribed at the time of broadcast', () => { - GraphQLStoreChangeEmitter.broadcastChangeForID('foo'); - GraphQLStoreChangeEmitter.addListenerForIDs(['foo'], mockCallback); + changeEmitter.broadcastChangeForID('foo'); + changeEmitter.addListenerForIDs(['foo'], mockCallback); jest.runAllTimers(); @@ -70,16 +73,16 @@ describe('GraphQLStoreChangeEmitter', () => { }); it('should only broadcast once per execution loop', () => { - GraphQLStoreChangeEmitter.addListenerForIDs(['foo', 'bar'], mockCallback); - GraphQLStoreChangeEmitter.broadcastChangeForID('foo'); - GraphQLStoreChangeEmitter.broadcastChangeForID('bar'); + changeEmitter.addListenerForIDs(['foo', 'bar'], mockCallback); + changeEmitter.broadcastChangeForID('foo'); + changeEmitter.broadcastChangeForID('bar'); jest.runAllTimers(); expect(mockCallback.mock.calls.length).toBe(1); - GraphQLStoreChangeEmitter.broadcastChangeForID('bar'); - GraphQLStoreChangeEmitter.broadcastChangeForID('foo'); + changeEmitter.broadcastChangeForID('bar'); + changeEmitter.broadcastChangeForID('foo'); jest.runAllTimers(); @@ -91,8 +94,8 @@ describe('GraphQLStoreChangeEmitter', () => { id => id === 'baz_first(5)' ? 'baz' : id ); - GraphQLStoreChangeEmitter.addListenerForIDs(['baz_first(5)'], mockCallback); - GraphQLStoreChangeEmitter.broadcastChangeForID('baz'); + changeEmitter.addListenerForIDs(['baz_first(5)'], mockCallback); + changeEmitter.broadcastChangeForID('baz'); jest.runAllTimers(); @@ -104,9 +107,9 @@ describe('GraphQLStoreChangeEmitter', () => { throw new Error(); }); - GraphQLStoreChangeEmitter.addListenerForIDs(['foo'], mockThrowingCallback); - GraphQLStoreChangeEmitter.addListenerForIDs(['foo'], mockCallback); - GraphQLStoreChangeEmitter.broadcastChangeForID('foo'); + changeEmitter.addListenerForIDs(['foo'], mockThrowingCallback); + changeEmitter.addListenerForIDs(['foo'], mockCallback); + changeEmitter.broadcastChangeForID('foo'); expect(() => { jest.runAllTimers(); @@ -125,14 +128,14 @@ describe('GraphQLStoreChangeEmitter', () => { mockBatching = false; } ); - GraphQLStoreChangeEmitter.injectBatchingStrategy(mockBatchingStrategy); + changeEmitter.injectBatchingStrategy(mockBatchingStrategy); mockCallback.mockImplementation(() => { expect(mockBatching).toBe(true); }); - GraphQLStoreChangeEmitter.addListenerForIDs(['foo'], mockCallback); - GraphQLStoreChangeEmitter.broadcastChangeForID('foo'); + changeEmitter.addListenerForIDs(['foo'], mockCallback); + changeEmitter.broadcastChangeForID('foo'); expect(mockBatchingStrategy.mock.calls.length).toBe(0); jest.runAllTimers(); @@ -143,13 +146,13 @@ describe('GraphQLStoreChangeEmitter', () => { var mockBatchingStrategy = jest.genMockFunction().mockImplementation( callback => callback() ); - GraphQLStoreChangeEmitter.injectBatchingStrategy(mockBatchingStrategy); + changeEmitter.injectBatchingStrategy(mockBatchingStrategy); - GraphQLStoreChangeEmitter.addListenerForIDs(['foo'], () => { - GraphQLStoreChangeEmitter.broadcastChangeForID('bar'); + changeEmitter.addListenerForIDs(['foo'], () => { + changeEmitter.broadcastChangeForID('bar'); }); - GraphQLStoreChangeEmitter.addListenerForIDs(['bar'], mockCallback); - GraphQLStoreChangeEmitter.broadcastChangeForID('foo'); + changeEmitter.addListenerForIDs(['bar'], mockCallback); + changeEmitter.broadcastChangeForID('foo'); jest.runAllTimers(); diff --git a/src/legacy/store/__tests__/GraphQLStoreQueryResolver-test.js b/src/legacy/store/__tests__/GraphQLStoreQueryResolver-test.js index e877ec3566118..665c46a74b9b4 100644 --- a/src/legacy/store/__tests__/GraphQLStoreQueryResolver-test.js +++ b/src/legacy/store/__tests__/GraphQLStoreQueryResolver-test.js @@ -18,13 +18,15 @@ jest.dontMock('GraphQLStoreQueryResolver'); var Relay = require('Relay'); var GraphQLFragmentPointer = require('GraphQLFragmentPointer'); -var GraphQLStoreChangeEmitter = require('GraphQLStoreChangeEmitter'); var GraphQLStoreQueryResolver = require('GraphQLStoreQueryResolver'); var readRelayQueryData = require('readRelayQueryData'); var RelayStoreData = require('RelayStoreData'); var RelayStoreGarbageCollector = require('RelayStoreGarbageCollector'); describe('GraphQLStoreQueryResolver', () => { + var changeEmitter; + var storeData; + var mockCallback; var mockQueryFragment; var mockPluralQueryFragment; @@ -43,6 +45,9 @@ describe('GraphQLStoreQueryResolver', () => { beforeEach(() => { jest.resetModuleRegistry(); + storeData = new RelayStoreData(); + changeEmitter = storeData.getChangeEmitter(); + mockCallback = jest.genMockFunction(); mockQueryFragment = getNode(Relay.QL`fragment on Node{id,name}`); mockPluralQueryFragment = getNode(Relay.QL` @@ -64,7 +69,7 @@ describe('GraphQLStoreQueryResolver', () => { readRelayQueryData.mockReturnValue({data: mockResult}); var resolver = new GraphQLStoreQueryResolver( - RelayStoreData.getDefaultInstance().getQueuedStore(), + storeData, fragmentPointer, mockCallback ); @@ -90,13 +95,13 @@ describe('GraphQLStoreQueryResolver', () => { mockReader(mockResult); var resolver = new GraphQLStoreQueryResolver( - RelayStoreData.getDefaultInstance().getQueuedStore(), + storeData, fragmentPointer, mockCallback ); resolver.resolve(fragmentPointer); - var addListenersForIDs = GraphQLStoreChangeEmitter.addListenerForIDs; + var addListenersForIDs = changeEmitter.addListenerForIDs; expect(addListenersForIDs).toBeCalled(); expect(addListenersForIDs.mock.calls[0][0]).toEqual(['1038750002']); }); @@ -110,7 +115,7 @@ describe('GraphQLStoreQueryResolver', () => { var mockResultB = {__dataID__: '1038750002', id: '1038750002', name: 'Tim'}; var resolver = new GraphQLStoreQueryResolver( - RelayStoreData.getDefaultInstance().getQueuedStore(), + storeData, fragmentPointer, mockCallback ); @@ -134,7 +139,7 @@ describe('GraphQLStoreQueryResolver', () => { var mockResultB = {__dataID__: '1038750002', id: '1038750002', name: 'Tee'}; var resolver = new GraphQLStoreQueryResolver( - RelayStoreData.getDefaultInstance().getQueuedStore(), + storeData, fragmentPointer, mockCallback ); @@ -144,7 +149,7 @@ describe('GraphQLStoreQueryResolver', () => { }); var resolvedA = resolver.resolve(fragmentPointer); - var callback = GraphQLStoreChangeEmitter.addListenerForIDs.mock.calls[0][1]; + var callback = changeEmitter.addListenerForIDs.mock.calls[0][1]; callback(['1038750002']); mockReader({ @@ -168,7 +173,7 @@ describe('GraphQLStoreQueryResolver', () => { ); var resolver = new GraphQLStoreQueryResolver( - RelayStoreData.getDefaultInstance().getQueuedStore(), + storeData, fragmentPointerA, mockCallback ); @@ -193,7 +198,7 @@ describe('GraphQLStoreQueryResolver', () => { }; var resolver = new GraphQLStoreQueryResolver( - RelayStoreData.getDefaultInstance().getQueuedStore(), + storeData, fragmentPointer, mockCallback ); @@ -201,7 +206,7 @@ describe('GraphQLStoreQueryResolver', () => { mockReader(mockResult); resolver.resolve(fragmentPointer); - var callback = GraphQLStoreChangeEmitter.addListenerForIDs.mock.calls[0][1]; + var callback = changeEmitter.addListenerForIDs.mock.calls[0][1]; callback(['1038750002']); expect(mockCallback).toBeCalled(); @@ -219,7 +224,7 @@ describe('GraphQLStoreQueryResolver', () => { mockReader(mockResults); var resolver = new GraphQLStoreQueryResolver( - RelayStoreData.getDefaultInstance().getQueuedStore(), + storeData, fragmentPointer, mockCallback ); @@ -249,7 +254,7 @@ describe('GraphQLStoreQueryResolver', () => { mockReader(mockResults); var resolver = new GraphQLStoreQueryResolver( - RelayStoreData.getDefaultInstance().getQueuedStore(), + storeData, fragmentPointer, mockCallback ); @@ -272,7 +277,7 @@ describe('GraphQLStoreQueryResolver', () => { mockReader(mockResults); var resolver = new GraphQLStoreQueryResolver( - RelayStoreData.getDefaultInstance().getQueuedStore(), + storeData, fragmentPointer, mockCallback ); @@ -280,7 +285,7 @@ describe('GraphQLStoreQueryResolver', () => { var resolvedA = resolver.resolve(fragmentPointer); mockResults['1'] = {__dataID__: '1', name: 'Won'}; - var callback = GraphQLStoreChangeEmitter.addListenerForIDs.mock.calls[0][1]; + var callback = changeEmitter.addListenerForIDs.mock.calls[0][1]; callback(['1']); var resolvedB = resolver.resolve(fragmentPointer); @@ -311,7 +316,7 @@ describe('GraphQLStoreQueryResolver', () => { mockReader(mockResults); var resolver = new GraphQLStoreQueryResolver( - RelayStoreData.getDefaultInstance().getQueuedStore(), + storeData, fragmentPointer, mockCallback ); @@ -386,7 +391,7 @@ describe('GraphQLStoreQueryResolver', () => { }); var resolver = new GraphQLStoreQueryResolver( - RelayStoreData.getDefaultInstance().getQueuedStore(), + storeData, fragmentPointer, mockCallback ); @@ -394,7 +399,7 @@ describe('GraphQLStoreQueryResolver', () => { // Resolve the fragment pointer with the mocked data resolver.resolve(fragmentPointer); var callback = - GraphQLStoreChangeEmitter.addListenerForIDs.mock.calls[0][1]; + changeEmitter.addListenerForIDs.mock.calls[0][1]; // On first resolve we get data for all added ids expect(getIncreaseSubscriptionsParameters(2)).toEqual([ 'address', 'chris' diff --git a/src/store/RelayQueryResultObservable.js b/src/store/RelayQueryResultObservable.js index a98505b7f871f..135164feab5e1 100644 --- a/src/store/RelayQueryResultObservable.js +++ b/src/store/RelayQueryResultObservable.js @@ -15,7 +15,7 @@ var GraphQLFragmentPointer = require('GraphQLFragmentPointer'); var GraphQLStoreQueryResolver = require('GraphQLStoreQueryResolver'); -import type RelayRecordStore from 'RelayRecordStore'; +import type RelayStoreData from 'RelayStoreData'; import type { StoreReaderData, Subscription, @@ -48,18 +48,18 @@ class RelayQueryResultObservable { _data: ?StoreReaderData; _fragmentPointer: GraphQLFragmentPointer; _queryResolver: ?GraphQLStoreQueryResolver; - _store: RelayRecordStore; + _storeData: RelayStoreData; _subscriptionCallbacks: Array>; _subscriptionCount: number; constructor( - store: RelayRecordStore, + storeData: RelayStoreData, fragmentPointer: GraphQLFragmentPointer ) { this._data = undefined; this._fragmentPointer = fragmentPointer; this._queryResolver = null; - this._store = store; + this._storeData = storeData; this._subscriptionCallbacks = []; this._subscriptionCount = 0; } @@ -96,7 +96,7 @@ class RelayQueryResultObservable { 'RelayQueryResultObservable: Initialized twice.' ); var queryResolver = new GraphQLStoreQueryResolver( - this._store, + this._storeData, this._fragmentPointer, () => this._onUpdate(queryResolver) ); diff --git a/src/store/RelayStore.js b/src/store/RelayStore.js index 3fb7b04094a60..edf06d35d4fbc 100644 --- a/src/store/RelayStore.js +++ b/src/store/RelayStore.js @@ -158,7 +158,7 @@ var RelayStore = { fragment.isPlural()? [dataID] : dataID, fragment ); - return new RelayQueryResultObservable(queuedStore, fragmentPointer); + return new RelayQueryResultObservable(storeData, fragmentPointer); }, update( diff --git a/src/store/RelayStoreData.js b/src/store/RelayStoreData.js index 6e59c5ebd1eee..704e914197efb 100644 --- a/src/store/RelayStoreData.js +++ b/src/store/RelayStoreData.js @@ -65,6 +65,7 @@ class RelayStoreData { _cachePopulated: boolean; _cachedRecords: Records; _cachedRootCalls: RootCallMap; + _changeEmitter: GraphQLStoreChangeEmitter; _deferredQueryTracker: GraphQLDeferredQueryTracker; _garbageCollector: ?RelayStoreGarbageCollector; _mutationQueue: RelayMutationQueue; @@ -109,6 +110,7 @@ class RelayStoreData { this._cachePopulated = true; this._cachedRecords = cachedRecords; this._cachedRootCalls = cachedRootCallMap; + this._changeEmitter = new GraphQLStoreChangeEmitter(); this._deferredQueryTracker = new GraphQLDeferredQueryTracker(recordStore); this._mutationQueue = new RelayMutationQueue(this); this._nodeRangeMap = nodeRangeMap; @@ -348,7 +350,7 @@ class RelayStoreData { clearQueuedData(): void { forEachObject(this._queuedRecords, (_, key) => { delete this._queuedRecords[key]; - GraphQLStoreChangeEmitter.broadcastChangeForID(key); + this._changeEmitter.broadcastChangeForID(key); }); } @@ -390,6 +392,10 @@ class RelayStoreData { return this._deferredQueryTracker; } + getChangeEmitter(): GraphQLStoreChangeEmitter { + return this._changeEmitter; + } + /** * @deprecated * @@ -414,9 +420,7 @@ class RelayStoreData { */ _handleChangedAndNewDataIDs(changeSet: ChangeSet): void { var updatedDataIDs = Object.keys(changeSet.updated); - updatedDataIDs.forEach( - GraphQLStoreChangeEmitter.broadcastChangeForID - ); + updatedDataIDs.forEach(id => this._changeEmitter.broadcastChangeForID(id)); if (this._garbageCollector) { var createdDataIDs = Object.keys(changeSet.created); var garbageCollector = this._garbageCollector; diff --git a/src/store/__tests__/RelayQueryResultObservable-test.js b/src/store/__tests__/RelayQueryResultObservable-test.js index 7b47d30b8e7d5..30f5613477575 100644 --- a/src/store/__tests__/RelayQueryResultObservable-test.js +++ b/src/store/__tests__/RelayQueryResultObservable-test.js @@ -12,6 +12,7 @@ 'use strict'; jest + .dontMock('RelayStoreData') .dontMock('GraphQLStoreChangeEmitter') .dontMock('GraphQLStoreQueryResolver'); @@ -19,14 +20,17 @@ var RelayTestUtils = require('RelayTestUtils'); RelayTestUtils.unmockRelay(); var GraphQLFragmentPointer = require('GraphQLFragmentPointer'); -var GraphQLStoreChangeEmitter = require('GraphQLStoreChangeEmitter'); var Relay = require('Relay'); var RelayQueryResultObservable = require('RelayQueryResultObservable'); var RelayRecordStore = require('RelayRecordStore'); +var RelayStoreData = require('RelayStoreData'); var readRelayQueryData = require('readRelayQueryData'); describe('RelayQueryResultObservable', () => { + var storeData; + var changeEmitter; + var query; var records; var results; @@ -55,7 +59,7 @@ describe('RelayQueryResultObservable', () => { dataID, query ); - return new RelayQueryResultObservable(store, fragmentPointer); + return new RelayQueryResultObservable(storeData, fragmentPointer); } beforeEach(() => { @@ -76,7 +80,13 @@ describe('RelayQueryResultObservable', () => { name: 'Joe', }; store = new RelayRecordStore({records}); + storeData = new RelayStoreData(); + + storeData.getQueuedStore = jest.genMockFunction().mockImplementation(() => { + return store; + }); + changeEmitter = storeData.getChangeEmitter(); jest.addMatchers(RelayTestUtils.matchers); }); @@ -137,7 +147,7 @@ describe('RelayQueryResultObservable', () => { store.putField('123', 'name', 'Joseph'); results.name = 'Joseph'; - GraphQLStoreChangeEmitter.broadcastChangeForID('123'); + changeEmitter.broadcastChangeForID('123'); jest.runAllTimers(); subscribers.forEach(subscriber => { @@ -156,7 +166,7 @@ describe('RelayQueryResultObservable', () => { store.putField('123', 'name', 'Joseph'); results.name = 'Joseph'; - GraphQLStoreChangeEmitter.broadcastChangeForID('123'); + changeEmitter.broadcastChangeForID('123'); jest.runAllTimers(); expect(subscriber.onCompleted).not.toBeCalled(); @@ -176,7 +186,7 @@ describe('RelayQueryResultObservable', () => { // fetching the record calls onNext store.putRecord('oops'); - GraphQLStoreChangeEmitter.broadcastChangeForID('oops'); + changeEmitter.broadcastChangeForID('oops'); jest.runAllTimers(); expect(subscriber.onCompleted).not.toBeCalled(); expect(subscriber.onError).not.toBeCalled(); @@ -193,7 +203,7 @@ describe('RelayQueryResultObservable', () => { // deleting the record calls onNext store.deleteRecord('123'); - GraphQLStoreChangeEmitter.broadcastChangeForID('123'); + changeEmitter.broadcastChangeForID('123'); jest.runAllTimers(); expect(subscriber.onCompleted).not.toBeCalled(); expect(subscriber.onError).not.toBeCalled(); @@ -202,7 +212,7 @@ describe('RelayQueryResultObservable', () => { // restoring the record calls onNext store.putRecord('123'); - GraphQLStoreChangeEmitter.broadcastChangeForID('123'); + changeEmitter.broadcastChangeForID('123'); jest.runAllTimers(); expect(subscriber.onCompleted).not.toBeCalled(); expect(subscriber.onError).not.toBeCalled(); @@ -219,7 +229,7 @@ describe('RelayQueryResultObservable', () => { // evicting the record calls onNext store.removeRecord('123'); - GraphQLStoreChangeEmitter.broadcastChangeForID('123'); + changeEmitter.broadcastChangeForID('123'); jest.runAllTimers(); expect(subscriber.onCompleted).not.toBeCalled(); expect(subscriber.onError).not.toBeCalled(); @@ -228,7 +238,7 @@ describe('RelayQueryResultObservable', () => { // restoring the record calls onNext store.putRecord('123'); - GraphQLStoreChangeEmitter.broadcastChangeForID('123'); + changeEmitter.broadcastChangeForID('123'); jest.runAllTimers(); expect(subscriber.onCompleted).not.toBeCalled(); expect(subscriber.onError).not.toBeCalled();