SuperStore.js

import Vue from 'vue'
import Store from './Store.js'

const SuperStoreError = {
  STORE_NULL: 'Store must exist',
  INVALID_STORE: 'Store must extends Store',
  STORE_ALREADY_ADDED: (name) => `Store ${name} was already added`
}
/**
 * "*One Store to rule them all*"
 * 
 * This class is the top-level store. It manages all the state and stores in order to dispatch actions.
 * It can be seen as a container for all the stores. Flue is just a SuperStore instance.
 */
class SuperStore extends Store {
  constructor() {
    super()
    this.middlewares = []
    this.actions = {}
    this.providers = {}
    this.stores = []
    this.refs = {}
    this.initialize()
  }

  _copyActionsRefs(actions) {
    for (let key in actions) {
      this.actions[key] = actions[key]
    }
  }

  /**
   * This function takes an object of actions and to add them
   * to the global store making them available from anywhere.
   * Example:
   * 
   * ```javascript
   * import { flue } from 'flue-vue'
   * const actions = {
   *  test() {
   *    flue.dispatch(new Action("TEST_ACTION"))
   *   }
   * }
   * 
   * flue.addActions(actions)
   * flue.actions.test()
   * 
   * ```
   * @param {Function} actionProvider
   */
  addActions(actions) {
    this._copyActionsRefs(actions)
  }
  /**
   * Copy all the store's action, if any. 
   * This is done automatically when a new store is added 
   * using flye.addStore(Store)
   * @param {Store} store 
   */
  addActionsFromStore(store) {
    if (typeof store.actions == 'function'){
      const actions = store.actions(store)
      this._copyActionsRefs(actions)
    }
  }

  _registerStoreToDispatcher(store) {
    store._dispachToken = this._dispatcher.register(store.reduce.bind(store))
  }

  // use Vue vm to make it reactive
  _addState(state){
    for (let key in state) {
      Vue.set(this.state, key, state[key])
    }
  }

  /**
   * Add multiple stores at the same tme
   * @param {Array<Store>} arrayOfStores 
   */
  addStores(arrayOfStores) {
    arrayOfStores.forEach(store => this.addStore(store))
  }

  /**
  * This function add Store to the global flue instance,
  * under the hood it override the state pointer of each store with the
  * global one
  * @param {Store} store A Store instance
  */
  addStore(store) {
    if (!store || store === undefined)
      throw new Error(SuperStoreError.STORE_NULL)

    if (!(store instanceof Store))
      throw new Error(SuperStoreError.INVALID_STORE)

    this._registerStoreToDispatcher(store)
    // assign global dispatcher
    store._dispatcher = this._dispatcher
    // fetch all the store's state and put in the superStore
    this._addState(store.state)
    // override state pointer of the store with the global one -> make the store stateless
    store.state = this.state
    // take all the actions from the store and pass to them the dispatcher and the store itself as a context
    this.addActionsFromStore(store)
    // make a ref to the superStore
    store.flue = this  
    store.sStore = this    
    store.$store = this    
    // also directly store providers to fast access
    store.providers = this.providers
    // save the store
    this.stores.push(store)
    this.refs[store.name] = store
  }

  /**
   * Add a raw reducer function, a unnamed store
   * will be created and its reduce function overrided
   * Example:
   * 
   * var reducer = (action) => { console.log(action.type) }
   * flue.addReducer(reducer)
   * 
   * @param {Function} reducer 
   */
  addReducer(reducer) {
    // spawn a new store
    const unknowStore = new Store()
   // override its reduce function
    unknowStore.reduce = reducer
    
    this.addStore(unknowStore)
  }

  /**
   * This function applies the given middlewares
   * to a single store
   * [Redux middlewares](http://redux.js.org/docs/advanced/Middleware.html) can be used
   * @param {Store} stores A Store instance
   * @param {Array} middlewares 
   */
  applyMiddlewareToStore(store, middlewares) {
    middlewares.forEach(middleware => {
      let dispatch = this.dispatch.bind(this)
      // state cannot be modified from the middleware
      const middlewareAPI = {
        getState: this.getState.bind(this),
        dispatch: (action) => dispatch(action)
      }
      
      store.dispatch = middleware(middlewareAPI)(dispatch)
    })
  }

  /**
   * This function applies the given middlewares
   * to multiple stores
   * [Redux middlewares](http://redux.js.org/docs/advanced/Middleware.html) can be used
   * @param {Array} stores An Array of Stores
   * @param {Array} middlewares 
   */
  applyMiddlewareToStores(stores, middlewares) {
    stores.forEach(store => this.applyMiddlewareToStore(store, middlewares))
  }

  /**
   * This function applies the given middlewares 
   * to each store.
   * [Redux middlewares](http://redux.js.org/docs/advanced/Middleware.html) can be used
   * @param {Array} middlewares 
   */
  applyGlobalMiddleware(middlewares) {
    // middlewares = middlewares.slice()
    // middlewares.reverse()
    this.applyMiddlewareToStores(this.stores, middlewares)
  }

  /**
   * It may be convinient to add some external package
   * directly into flue in order to make it
   * available from all the stores and components.
   * Example:
   * 
   * ```javascript
   * import axios from 'axios'
   * 
   * flue.addProvider({ key: 'client', source: axios })
   * 
   * flue.providers.client.get(...)
   * ```
   * 
   * @param {Object} provider A provider.
   */
  addProvider(provider){
    this.providers[provider.key] = provider.source
  }
  
}

const flue = new SuperStore()

export default flue