Home Reference Source Repository

src/store/RelayQueryResultObservable.js

/**
 * Copyright (c) 2013-present, 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 RelayQueryResultObservable
 * @flow
 */

'use strict';

const GraphQLStoreQueryResolver = require('GraphQLStoreQueryResolver');
import type {DataID} from 'RelayInternalTypes';
import type RelayQuery from 'RelayQuery';
import type RelayStoreData from 'RelayStoreData';
import type {
  StoreReaderData,
  Subscription,
  SubscriptionCallbacks,
} from 'RelayTypes';

const invariant = require('invariant');

/**
 * An Rx Observable representing the results of a fragment in the local cache.
 * Subscribers are notified as follows:
 *
 * `onNext`: Called with the latest results of a fragment. Results may be `null`
 * if the data was marked as deleted or `undefined` if the fragment was either
 * not fetched or evicted from the cache. Note that required fields may be
 * missing if the fragment was not fetched with `Relay.Store.primeCache` or
 * `Relay.Store.forceFetch` before creating a subscription.
 * - Called synchronously on `subscribe()`.
 * - Called whenever the results of the fragment change.
 *
 * `onError`: Currently not called. In the future this may be used to indicate
 * that required data for the fragment has not been fetched or was evicted
 * from the cache.
 *
 * `onCompleted`: Not called.
 *
 * @see http://reactivex.io/documentation/observable.html
 */
class RelayQueryResultObservable {
  _data: ?StoreReaderData;
  _dataID: DataID;
  _fragment: RelayQuery.Fragment;
  _fragmentResolver: ?GraphQLStoreQueryResolver;
  _storeData: RelayStoreData;
  _subscriptionCallbacks: Array<SubscriptionCallbacks<?StoreReaderData>>;
  _subscriptionCount: number;

  constructor(
    storeData: RelayStoreData,
    fragment: RelayQuery.Fragment,
    dataID: DataID
  ) {
    this._data = undefined;
    this._dataID = dataID;
    this._fragment = fragment;
    this._fragmentResolver = null;
    this._storeData = storeData;
    this._subscriptionCallbacks = [];
    this._subscriptionCount = 0;
  }

  subscribe(callbacks: SubscriptionCallbacks<?StoreReaderData>): Subscription {
    this._subscriptionCount++;
    const subscriptionIndex = this._subscriptionCallbacks.length;
    const subscription = {
      dispose: () => {
        invariant(
          this._subscriptionCallbacks[subscriptionIndex],
          'RelayQueryResultObservable: Subscriptions may only be disposed once.'
        );
        delete this._subscriptionCallbacks[subscriptionIndex];
        this._subscriptionCount--;
        if (this._subscriptionCount === 0) {
          this._unobserve();
        }
      },
    };
    this._subscriptionCallbacks.push(callbacks);

    if (this._subscriptionCount === 1) {
      this._resolveData(this._observe());
    }
    this._fire(callbacks);

    return subscription;
  }

  _observe(): GraphQLStoreQueryResolver {
    invariant(
      !this._fragmentResolver,
      'RelayQueryResultObservable: Initialized twice.'
    );
    const fragmentResolver = new GraphQLStoreQueryResolver(
      this._storeData,
      this._fragment,
      () => this._onUpdate(fragmentResolver)
    );
    this._fragmentResolver = fragmentResolver;
    return fragmentResolver;
  }

  _unobserve(): void {
    if (this._fragmentResolver) {
      this._data = undefined;
      this._fragmentResolver.dispose();
      this._fragmentResolver = null;
    }
  }

  _onUpdate(fragmentResolver: GraphQLStoreQueryResolver): void {
    this._resolveData(fragmentResolver);
    this._subscriptionCallbacks.forEach(callbacks => this._fire(callbacks));
  }

  _fire(callbacks: SubscriptionCallbacks<?StoreReaderData>): void {
    callbacks.onNext && callbacks.onNext(this._data);
  }

  _resolveData(fragmentResolver: GraphQLStoreQueryResolver): void {
    const data = fragmentResolver.resolve(this._fragment, this._dataID);
    invariant(
      !Array.isArray(data),
      'RelayQueryResultObservable: Plural fragments are not supported.'
    );
    this._data = data;
  }
}

module.exports = RelayQueryResultObservable;