<reponame>kirillDanshin/cerebral<filename>packages/node_modules/cerebral/src/Controller.js<gh_stars>0
import DependencyStore from './DependencyStore'
import BaseController from './BaseController'
import Model from './Model'
import { Reaction } from './Reaction'
import { Tag } from 'function-tree'
import { ensureStrictPath, extractModuleProp, isComputedValue } from './utils'
import { Compute } from './Compute'

/*
  The controller is where everything is attached. The devtools
  is attached directly. Also a top level module is created.
  The controller creates the function tree that will run all sequences,
  based on top level providers and providers defined in modules
*/
class Controller extends BaseController {
  constructor(rootModule, options) {
    super(
      rootModule,
      Object.assign(
        {
          Model,
        },
        options
      )
    )

    this.dependencyStore = new DependencyStore()
    this.flush = this.flush.bind(this)

    this.on('asyncFunction', (execution, funcDetails) => {
      if (!funcDetails.isParallel) {
        this.flush()
      }
    })
    this.on('parallelStart', () => this.flush())
    this.on(
      'parallelProgress',
      (execution, currentPayload, functionsResolving) => {
        if (functionsResolving === 1) {
          this.flush()
        }
      }
    )
    this.on('mutation', (mutation) => this.updateComputed(mutation))
    this.on('end', () => this.flush())

    extractModuleProp(this.module, 'reactions', (reactions, currentModule) => {
      if (reactions) {
        Object.keys(reactions)
          .filter((key) => reactions[key] instanceof Reaction)
          .forEach((key) => reactions[key].initialize())
      }

      return reactions
    })

    this.getState = this.getState.bind(this)
    this.getSequence = this.getSequence.bind(this)
    this.getSequences = this.getSequences.bind(this)
  }
  /*
    Whenever components needs to be updated, this method
    can be called
  */
  flush(force) {
    const changes = this.model.flush()

    if (!force && !changes.length) {
      return
    }

    this.updateWatchers(changes, force)
    this.emit('flush', changes, Boolean(force))
  }
  /*
    Flags computed as dirty related to mutations
  */
  updateComputed(change) {
    const entities = this.dependencyStore.getUniqueEntities([change])

    entities.forEach((entity) => {
      if (entity instanceof Compute) {
        entity.isDirty = true
      }
    })
  }
  updateWatchers(changes, force) {
    let watchToUpdate = []

    if (force) {
      watchToUpdate = this.dependencyStore.getAllUniqueEntities()
    } else {
      watchToUpdate = this.dependencyStore.getUniqueEntities(changes)
    }

    const startDateTime = Date.now()
    const start =
      typeof performance === 'undefined' ? Date.now() : performance.now()
    watchToUpdate.forEach((watch) => {
      // We skip computed as they have already been flagged
      if (watch instanceof Compute) {
        return
      }

      watch.onUpdate(changes, force)
    })
    const end =
      typeof performance === 'undefined' ? Date.now() : performance.now()

    if (this.devtools && watchToUpdate.length) {
      this.devtools.sendWatchMap(
        watchToUpdate,
        changes,
        startDateTime,
        end - start
      )
    }
  }
  /*
    Returns stuff based on tags/proxy
  */
  get(tag, overrideProps = {}) {
    const context = this.createContext(overrideProps)
    const value = tag.getValue(context)

    if (isComputedValue(value)) {
      return value.getValue(overrideProps)
    }

    return value
  }
  /*
    Create an object used to grab data from tags
  */
  createContext(props = {}, modulePath = []) {
    const modulePathString = modulePath.length ? modulePath.join('.') + '.' : ''

    return {
      props,
      controller: this,
      execution: {
        name: modulePathString,
      },
    }
  }
  /*
    Creates a dependency map to be used with the store
  */
  createDependencyMap(dependencies, props, modulePath) {
    const getters = this.createContext(props, modulePath)

    return dependencies.reduce((currentDepsMap, dependency) => {
      if (dependency instanceof Tag) {
        return dependency
          .getTags(getters)
          .reduce((updatedCurrentDepsMap, tag) => {
            if (tag.type === 'state' || tag.type === 'moduleState') {
              const value = tag.getValue(getters)

              if (isComputedValue(value)) {
                // We have to trigger the value to get the dependencyMap
                value.getValue(props)
                return Object.assign(
                  updatedCurrentDepsMap,
                  value.getDependencyMap()
                )
              }

              const path = tag.getPath(getters)
              const strictPath = ensureStrictPath(path, this.getState(path))

              updatedCurrentDepsMap[strictPath] = true
            }
            return updatedCurrentDepsMap
          }, currentDepsMap)
      }
      return currentDepsMap
    }, {})
  }
}

export default Controller
