MiguelMadero/ember-view-state

View on GitHub
addon/services/view-state-repository.js

Summary

Maintainability
A
1 hr
Test Coverage
import Ember from 'ember';
import storage from 'ember-view-state/utils/local-storage';

/***
 * The ViewStateRepository manages the data access to persist
 * properties for the ViewState Mixin and Service
 * We don't return promises to avoid async and delaying, UX
 * but we can expect that data to be "eventually consistent".
 */
export default Ember.Service.extend({
  /***
   * Flushes in memory data to localStorage so it's persisted across browser sessions
   * and enqueues a server side save (if implemented)
   */
  flush: function() {
    let data = {
      viewState: this.get('_state'),
      lastUpdatedAt: new Date()
    };
    data = JSON.stringify(data);
    storage.setItem(this._getNamespace(), data);
    // We persist in both places, but delay the server side save
    this._enqueueServerSideSave();
  },

  /***
   * Loads data from from localStorage and the server and leaves it in memory
   */
  load: function() {
    if (Ember.isEmpty(this.get('_state'))) {
      let localState = storage.getItem(this._getNamespace());
      localState = localState ? JSON.parse(localState) : {};
      localState.viewState = localState.viewState || {};
      this.set('_state', localState.viewState);

      return this._loadServerViewState().then(serverState=>
        this._mergeViewState(localState, serverState)
      );
    }
  },

  getViewStateFor: function(key) {
    this.load();
    let state = this.get('_state');
    const viewStateForKey = state[key] || {};
    state[key] = viewStateForKey;
    return viewStateForKey;
  },

  _getNamespace: function() {
    // NOTE: document that we can allow the user to override this so each user
    // can have a different namespace without overriding each other.
    // return 'ViewState.' + session.get('userGUID');
    return 'ViewState';
  },

  _serverSideSaveDelay: 5*60*1000, // 5 minutes
  _loadServerViewState: function() {
    // TODO: let consumers implement this add an example using firebase
    // return ajax(url);
    return Ember.RSVP.resolve();
  },

  /**
   * Merges the ViewState stored locally with the server side ViewState
   * giving ViewState to the newer ones
   * @param  {Object} localViewState
   * @param  {Object} serverViewState
   */
  _mergeViewState: function(localViewState, serverViewState) {
    if (!serverViewState) {
      return;
    }
    const newLocalState = !localViewState.lastUpdatedAt;
    if (localViewState.lastUpdatedAt === serverViewState.lastUpdatedAt) {
      // They're the same, nothing to do here.
      return;
    }
    localViewState.viewState = localViewState.viewState || {};
    serverViewState.viewState = serverViewState.viewState || {};
    let local = localViewState.viewState;
    let server = serverViewState.viewState;

    let allKeys = Object.keys(local).concat(Object.keys(server));
    allKeys.forEach(function(key) {
      const localValue = local[key];
      const serverValue = server[key];
      const serverLastUpdatedAt = Ember.getWithDefault(serverValue || {}, 'lastUpdatedAt', serverViewState.lastUpdatedAt);
      const localLastUpdatedAt = Ember.getWithDefault(localValue || {}, 'lastUpdatedAt', localViewState.lastUpdatedAt);

      if (!localValue || serverLastUpdatedAt > localLastUpdatedAt) {
        if (serverValue) {
          Ember.set(local, key, serverValue);
        }
      }
    });

    if (newLocalState || serverViewState.lastUpdatedAt > localViewState.lastUpdatedAt) {
      localViewState.lastUpdatedAt = serverViewState.lastUpdatedAt;
    }
  },

  _enqueueServerSideSave: function() {
    const immediate = false; // trigger on the tail of the wait time interval as opposed as the default of leading
    Ember.run.throttle(this, ()=>{
      const data = storage.getItem(this._getNamespace());
      this._persistViewStateServerSide(data);
    }, this._serverSideSaveDelay, immediate);
  },

  _persistViewStateServerSide: function (/*data*/) {
    // return ajax(url, 'POST', data);
    return Ember.RSVP.resolve();
  }
});