Home Reference Source Repository

lib/middleware.js

import invariant from 'invariant'
import createContext from './context'
import { IS_DEV, IDLESTATUS_ACTIVE, ROOT_STATE_KEY, NEXT_IDLE_STATUS_BLUEPRINT, LAST_IDLE_STATUS_BLUEPRINT, START_BLUEPRINT, STOP_BLUEPRINT, ACTIVITY_BLUEPRINT } from './constants'
import { bisectStore } from 'redux-mux'
import { publicBlueprints, nextIdleStatusBlueprint, lastIdleStatusBlueprint } from './blueprints'
import { createDetection } from './actions'
import { getNextIdleStatusIn } from './states'

/** When context has already been created, it can be shared to middleware component. */
export const createMiddleware = context => {
  const { log, activeStatusAction, idleStatusAction, translateBlueprintTypes, translateBlueprints, IDLE_STATUSES, idleStatusDelay, thresholds } = context
  const { start, stop } = translateBlueprints(publicBlueprints)
  const { nextIdleStatusAction
        , lastIdleStatusAction
        } = translateBlueprints({ nextIdleStatusAction: nextIdleStatusBlueprint
                                , lastIdleStatusAction: lastIdleStatusBlueprint
                                })


  const { START
        , STOP
        , NEXT_IDLE_STATUS
        , LAST_IDLE_STATUS
        , ACTIVITY
        } = translateBlueprintTypes({ START: START_BLUEPRINT
                                    , STOP: STOP_BLUEPRINT
                                    , NEXT_IDLE_STATUS: NEXT_IDLE_STATUS_BLUEPRINT
                                    , LAST_IDLE_STATUS: LAST_IDLE_STATUS_BLUEPRINT
                                    , ACTIVITY: ACTIVITY_BLUEPRINT
                                    })


  const idleStatuses = [ IDLESTATUS_ACTIVE, ...IDLE_STATUSES ]
  const getNextIdleStatus = getNextIdleStatusIn(idleStatuses)
  const IDLESTATUS_FIRST = getNextIdleStatus(IDLESTATUS_ACTIVE)
  const IDLESTATUS_LAST = IDLE_STATUSES.slice(-1)[0]

  let nextTimeoutID = null
  let startDetectionID = null
  let isStarted = false
  return store => {
    const idleStore = bisectStore(ROOT_STATE_KEY)(store)
    const { startActivityDetection, stopActivityDetection, localSync } = createDetection(context)(store)
    if(IS_DEV) {
      invariant(startActivityDetection, 'createDetection should return startActivityDetection')
      invariant(stopActivityDetection, 'createDetection should return stopActivityDetection')
      invariant(localSync, 'localSync should exist')
    }
    return next => action => {
      const { dispatch, getState } = store

      if(!action.type)
        return next(action)
      const { type, payload } = action

      const scheduleTransition = idleStatus => {
        clearTimeout(nextTimeoutID)
        let delay = dispatch(idleStatusDelay(idleStatus))
        invariant(delay, `must return an idle status delay for idleStatus === '${idleStatus}'`)
        invariant(typeof delay === 'number', `idle status delay must be a number type for idleStatus === '${idleStatus}'`)

        let lastActive = new Date().toTimeString()
        let nextMessage = `${NEXT_IDLE_STATUS} action continuing after ${delay} MS delay, lastActive: ${new Date().toTimeString()}`
        let nextCancelMessage = cancelledAt => `${NEXT_IDLE_STATUS} action cancelled before ${delay} MS delay by dispatcher, lastActive: ${new Date().toTimeString()}, cancelledAt: ${cancelledAt}`
        let nextIdleStatus = getNextIdleStatus(idleStatus)
        nextTimeoutID = setTimeout(() => {
          next(action)
          dispatch(idleStatusAction(idleStatus))
          if(nextIdleStatus) {
            dispatch(nextIdleStatusAction(nextIdleStatus))
          } else {
            localSync.trigger(false)
          }
        }, delay)
        return function cancel() {
          clearTimeout(nextTimeoutID)
        }
      }

      if(type === START) {
        if(!isStarted) {
          startActivityDetection()
          localSync.start()
          isStarted = true
        }
        let result = next(action)
        dispatch(nextIdleStatusAction(IDLESTATUS_FIRST))
        return result
      }

      if(type === STOP) {
        clearTimeout(nextTimeoutID)
        clearTimeout(startDetectionID)
        if(isStarted) {
          localSync.stop()
          stopActivityDetection()
          isStarted = false
        }
      }

      if(type === NEXT_IDLE_STATUS) {
        return scheduleTransition(payload.nextIdleStatus)
      }

      if(type === LAST_IDLE_STATUS) {
        clearTimeout(nextTimeoutID)
        dispatch(idleStatusAction(IDLESTATUS_LAST))
      }

      if(type === ACTIVITY) {
        if(thresholds.phaseOffMS) {
          localSync.stop()
          stopActivityDetection()
          startDetectionID = setTimeout(() => {
            startActivityDetection()
            localSync.start()
          }, thresholds.phaseOffMS)
        }

        let result = next(action)
        if(payload.type !== 'local') {
          log.info('Setting local tab to active')
          localSync.trigger(true)
        }
        if(payload.isTransition) {
          dispatch(activeStatusAction)
        }
        dispatch(nextIdleStatusAction(IDLESTATUS_FIRST))
        return result
      }
      return next(action)
    }
  }
}

/** Creates middleware from opts including validation in development */
export default function configureMiddleware(opts) { return createMiddleware(createContext(opts)) }