Home Reference Source Repository

lib/context.js

import invariant from 'invariant'
import  { ROOT_STATE_KEY, ACTION_PREFIX, IDLESTATUS_ACTIVE } from './constants'
import { getActiveEvents, getUseFastState, getUseLocalState, getUseWebRTCState, getUseWebSocketsState, getThresholds, getLevel } from './defaults'
import { translateBlueprintsWith, translateBlueprintTypesWith } from 'redux-blueprint'

const validateContext = (libContext, appContext) => {
  invariant(libContext, 'must pass opts to validate')
  invariant(appContext, 'must pass opts to validate')

  const { libName, appName } = libContext
  const { activeEvents, thresholds } = appContext

  invariant(libName, 'libName must exist')
  invariant(typeof libName === 'string', 'libName option must be a string')
  invariant(libName.length > 0, 'libName option must not be empty')
  invariant(appName, 'appName must exist')
  invariant(typeof appName === 'string', 'appName option must be a string')
  invariant(appName.length > 0, 'appName option must not be empty')
  invariant(activeEvents, 'active events must exist')
  invariant(thresholds, 'thresholds must exist')
  invariant(thresholds.mouse, 'thresholds.mouse must exist')
  invariant(typeof thresholds.mouse === 'number', 'thresholds.mouse must be a number corresponding to pixels')
  invariant(typeof thresholds.phaseOffMS === 'number', 'thresholds.phaseOffMS must be a number corresponding to minimum milliseconds between updates to redux')
  invariant(typeof thresholds.phaseOnMS === 'number', 'thresholds.phaseOnMS must be a number corresponding to minimum milliseconds between updates to redux')
}

const configureInitialState = libContext => appContext => {
  return  { idleStatus: IDLESTATUS_ACTIVE
          , isRunning: false
          , isDetectionRunning: false
          , isIdle: false
          , lastActive: +new Date()
          , lastEvent: { x: -1, y: -1, type: null }
          }
}

export default function createContext({ appName
                                      , IDLE_STATUSES
                                      , idleStatusDelay
                                      , activeStatusAction
                                      , idleStatusAction
                                      , activeEvents = getActiveEvents()
                                      , useFastStore = getUseFastState()
                                      , useLocalStore = getUseLocalState()
                                      , useWebRTCState = getUseWebRTCState()
                                      , useWebSocketsState = getUseWebSocketsState()
                                      , thresholds = getThresholds()
                                      , level = getLevel()
                                      } = {}) {
  const libName = ROOT_STATE_KEY
  const libOpts = { libName, validateContext, configureAppContext: libContext => appOpts => appOpts, configureInitialState }
  const appOpts = { appName, IDLE_STATUSES, idleStatusDelay, activeStatusAction, idleStatusAction, activeEvents, useFastStore, useLocalStore, useWebRTCState, useWebSocketsState, thresholds, level }
  return configureContext(libOpts)(appOpts)
}

const cleanActionName = name => name.toUpperCase().replace(/-+\s+/, '_')

/** Validates library creators options */
const validateLibOpts = libOptsRaw => {
  invariant(libOptsRaw, 'libOpts definition is required')
  const { libName, validateContext, configureAppContext, configureInitialState } = libOptsRaw
  invariant(typeof libName === 'string', 'libName must be a string')
  invariant(libName.length > 0, 'libName must not be empty')

  invariant(validateContext, 'validateContext must exist')
  invariant(typeof validateContext === 'function', 'validateContext must be a function')

  invariant(configureAppContext, 'configureAppContext must exist')
  invariant(typeof configureAppContext === 'function', 'configureAppContext must be a function')

  invariant(configureInitialState, 'configureInitialState must exist')
  invariant(typeof configureInitialState === 'function', 'configureInitialState must be a function')
}

/** Validates library consumers options */
const validateAppOpts = appOptsRaw => {
  invariant(appOptsRaw, 'appOpts are required')
  const { appName } = appOptsRaw

  invariant(typeof appName === 'string', 'appName opt must be a string')
  invariant(appName.length > 0, 'appName opt must not be empty')
}

function configureContext(libOpts) {
  const isDev = process.env.NODE_ENV !== 'production'
  if(isDev) validateLibOpts(libOpts)
  const { libName, validateContext, configureAppContext, configureInitialState } = libOpts
  return appOpts => {
    if(isDev) validateAppOpts(appOpts)
    const { appName, level } = appOpts

    const translateBlueprintType =  blueprintType => `${cleanActionName(libName)}_${cleanActionName(appName)}_${cleanActionName(blueprintType)}`
    const translateBlueprintTypes = translateBlueprintTypesWith(translateBlueprintType)
    const translateBlueprints = translateBlueprintsWith(translateBlueprintType)

    const libContext =  { log: createLogger({ libName, level })
                        , libName
                        , appName
                        , translateBlueprintTypes
                        , translateBlueprints
                        }

    const appContext = configureAppContext(libContext)(appOpts)
    if(isDev) validateContext(libContext, appContext)

    return Object.assign( appContext, libContext, { get initialState() { return configureInitialState(libContext)(appContext) }
                                                  })
  }
}


const noop = () => {}

function createLogger ({ libName, level }) {
  const _formatMessage = ({ level, message, obj }) => {
    if(!message && typeof obj === 'string') {
      message = obj
      obj = noop()
    }
    return _formatLog(obj ? `${level}: '${message}' => ${JSON.stringify(obj)}`  : `${level}: '${message}'`)
  }

  const _formatLog = message =>`${libName} | ${message}`

  return process.env.NODE_ENV !== 'production' ? (
    { trace: (obj, message) => level === 'trace' ? console.trace(_formatMessage({ level: 'trace', message, obj })): noop()
    , debug: (obj, message) => [ 'trace', 'debug' ].includes(level) ? console.log(_formatMessage({ level: 'debug', message, obj })) : noop()
    , info: (obj, message) => [ 'trace', 'debug', 'info' ].includes(level) ? console.info(_formatMessage({ level: 'info', message, obj })) : noop()
    , warn: (obj, message) => [ 'trace', 'debug', 'info', 'warn' ].includes(level) ? console.warn(_formatMessage({ level: 'warn', message, obj })) : noop()
    , error: (obj, message) => [ 'trace', 'debug', 'info', 'warn', 'error' ].includes(level) ? console.error(_formatMessage({ level: 'error', message, obj })) : noop()
    }
  ) : ({ trace: noop, debug: noop, info: noop, warn: noop, error: noop })
}