rangoo94/easen-tools

View on GitHub
packages/core/src/internal/createActionExecutor.js

Summary

Maintainability
C
1 day
Test Coverage
A
100%
const ServiceActionNotFoundError = require('../ServiceError/ServiceActionNotFoundError')
const ActionExecutorFunctionBuilder = require('./ActionExecutorFunctionBuilder')

// Set up default options for action executor

const defaultOptions = {
  Promise: Promise,
  includeContext: true,
  ensurePromiseImplementation: true,
  onUncaughtError: null,
  onActionStateChange: null,
  createContext: null,
  isActionSupported: null,
  process: null,
  preExecute: null,
  execute: null,
  finalizeContext: null,
  processResult: null
}

// Set up internal function names

const InternalFnNames = {
  emit: '$emit',
  isActionSupported: '$supports',
  createContext: '$create',
  processAction: '$process',
  preExecuteAction: '$preExecute',
  executeAction: '$execute',
  processResult: '$processResult',
  finalizeContext: '$finalize'
}

/**
 * Validate options passed to action executor.
 *
 * @param {object} options
 * @param {function} options.createContext
 * @param {function} options.execute
 */
function validateActionExecutorOptions (options) {
  if (!options.createContext) {
    throw new Error('You need to pass action context creator')
  }

  if (!options.execute) {
    throw new Error('You need to pass action executor')
  }
}

/**
 * Create action executor.
 *
 * @param {object} [options]
 * @param {boolean} [options.includeContext]
 * @param {boolean} [options.ensurePromiseImplementation]
 * @param {function|{ reject: function }} [options.Promise]
 * @param {function} [options.onActionStateChange]
 * @param {function(string): boolean} [options.isActionSupported]
 * @param {function(string, object, object): object} [options.createContext]
 * @param {function(object)} [options.process]
 * @param {function(object)} [options.preExecute]
 * @param {function(object): *} [options.execute]
 * @param {function(object, object)} [options.finalizeContext]
 * @param {function(object, object)} [options.processResult]
 * @param {function} [options.onUncaughtError]
 * @returns {function(string, [object], [object]): Promise<*,*>|{ context: object }}
 */
function createActionExecutor (options) {
  // Set up options
  options = Object.assign({}, defaultOptions, options)

  // Validate passed data
  validateActionExecutorOptions(options)

  // Extract basic stuff
  const Promise = options.Promise
  const onActionStateChange = options.onActionStateChange

  // Extract procedure steps
  const includeContext = !!options.includeContext
  const ensurePromiseImplementation = !!options.ensurePromiseImplementation
  const isActionSupported = options.isActionSupported
  const createContext = options.createContext
  const processAction = options.process
  const preExecuteAction = options.preExecute
  const executeAction = options.execute
  const finalizeContext = options.finalizeContext
  const processResult = options.processResult
  const onUncaughtError = typeof options.onUncaughtError === 'function'
    ? options.onUncaughtError
    : console.warn.bind(console)

  // Initialize side effects

  const emitChange = onActionStateChange ? (state, context, value) => {
    try {
      onActionStateChange(state, context, value)
    } catch (error) {
      onUncaughtError(error)
    }
  } : null

  // Initialize internal functions map

  const internalFunctionsMap = {
    [InternalFnNames.emit]: emitChange,
    [InternalFnNames.isActionSupported]: isActionSupported,
    [InternalFnNames.createContext]: createContext,
    [InternalFnNames.processAction]: processAction,
    [InternalFnNames.preExecuteAction]: preExecuteAction,
    [InternalFnNames.executeAction]: executeAction,
    [InternalFnNames.finalizeContext]: finalizeContext,
    [InternalFnNames.processResult]: processResult
  }

  // Initialize abstract function with context set
  const fnAbstract = new ActionExecutorFunctionBuilder()
    .setContext('Promise', Promise)
    .setContext('ServiceActionNotFoundError', ServiceActionNotFoundError)

  // Set up values in context
  for (let fnName in internalFunctionsMap) {
    if (internalFunctionsMap[fnName]) {
      fnAbstract.setContext(fnName, internalFunctionsMap[fnName])
    }
  }

  // Create error handler

  const fnHandleError = fnAbstract.clone()
    .setArguments('$ctx', '$error')

    /* eslint-disable indent */
    // Handle ImmediateResult
    .conditional('$error && $error.$immediateResult')
    .open()
      // Pass ImmediateResult down
      .declare('$v', '$error.value')
      .callAvailable(null, InternalFnNames.finalizeContext, [ '$ctx', '$v' ])
      .callAvailable('$v', InternalFnNames.processResult, [ '$v', '$ctx' ])

      // Emit "success" event
      .callAvailable(null, InternalFnNames.emit, [ '"success"', '$ctx', '$v' ])

      .finish('$v')
    .close()
    /* eslint-enable indent */

    // Handle real error
    .callAvailable(null, InternalFnNames.finalizeContext, [ '$ctx', 'undefined', '$error' ])

    // Emit "error" event
    .callAvailable(null, InternalFnNames.emit, [ '"error"', '$ctx', '$error' ])

    .append('throw $error')

  // Create callback for asynchronous action execution

  const fnExecuteCallback = fnAbstract.clone()
    .setName('$dispatchAction$executeCallback')
    .setArguments('$ctx', '$v')

    // Execute callback
    .callAvailable(null, InternalFnNames.finalizeContext, [ '$ctx', '$v' ])
    .callAvailable('$v', InternalFnNames.processResult, [ '$v', '$ctx' ])

    // Emit "success" event
    .callAvailable(null, InternalFnNames.emit, [ '"success"', '$ctx', '$v' ])

    .finish('$v')

  // Create callback for asynchronous action pre-execution

  const fnPreExecuteCallback = fnAbstract.clone()
    .setName('$dispatchAction$preExecuteCallback')
    .setArguments('$ctx')
    .setContext('$exCb', fnExecuteCallback.build())

    // Pre-execute callback
    // Emit "execution" event
    .callAvailable(null, InternalFnNames.emit, [ '"execution"', '$ctx' ])

    .whenAvailable(
      InternalFnNames.executeAction,
      x => x
        .append(`var $v = ${InternalFnNames.executeAction}($ctx);`)
        .handleAsyncContinuation('$v', '$exCb.bind(undefined, $ctx)')
    )

    // Execute callback
    .callAvailable(null, InternalFnNames.finalizeContext, [ '$ctx', '$v' ])
    .callAvailable('$v', InternalFnNames.processResult, [ '$v', '$ctx' ])

    // Emit "success" event
    .callAvailable(null, InternalFnNames.emit, [ '"success"', '$ctx', '$v' ])

    .finish('$v')

  // Create callback for asynchronous action processing

  const fnProcessCallback = fnAbstract.clone()
    .setName('$dispatchAction$processCallback')
    .setArguments('$ctx')
    .setContext('$exCb', fnExecuteCallback.build())
    .setContext('$preCb', fnPreExecuteCallback.build())

    // Processing callback
    // Emit "ready" event
    .callAvailable(null, InternalFnNames.emit, [ '"ready"', '$ctx' ])

    .whenAvailable(
      InternalFnNames.preExecuteAction,
      x => x
        .append(`var $cc = ${InternalFnNames.preExecuteAction}($ctx);`)
        .handleAsyncContinuation('$cc', '$preCb.bind(undefined, $ctx)')
    )

    // Pre-execute callback
    // Emit "execution" event
    .callAvailable(null, InternalFnNames.emit, [ '"execution"', '$ctx' ])

    .whenAvailable(
      InternalFnNames.executeAction,
      x => x
        .append(`var $v = ${InternalFnNames.executeAction}($ctx);`)
        .handleAsyncContinuation('$v', '$exCb.bind(undefined, $ctx)')
    )

    // Execute callback
    .callAvailable(null, InternalFnNames.finalizeContext, [ '$ctx', '$v' ])
    .callAvailable('$v', InternalFnNames.processResult, [ '$v', '$ctx' ])

    // Emit "success" event
    .callAvailable(null, InternalFnNames.emit, [ '"success"', '$ctx', '$v' ])

    .finish('$v')

  // Create callback for action context dispatching

  const fnDispatch = fnAbstract.clone()
    .setName('$dispatchAction')
    .setArguments('$ctx')
    .setContext('$exCb', fnExecuteCallback.build())
    .setContext('$preCb', fnPreExecuteCallback.build())
    .setContext('$proCb', fnProcessCallback.build())
    .setContext('$errCb', fnHandleError.build())
    .setErrorHandlerCode('return $errCb($ctx, $error);')

    .setErrorHandlerCode(includeContext ? `try {
      var $$promise = Promise.resolve($errCb($ctx, $error));
      $$promise.context = $ctx;
      return $$promise;
    } catch ($error) {
      var $$promise = Promise.reject($error);
      $$promise.context = $ctx;
      return $$promise;
    }` : `try {
      return Promise.resolve($errCb($ctx, $error));
    } catch ($error) {
      return Promise.reject($error);
    }`)

    // Processing
    .whenAvailable(
      InternalFnNames.processAction,
      x => x
        .append(`var $cc = ${InternalFnNames.processAction}($ctx);`)
        .handleAsyncContinuation(
          '$cc',
          '$proCb.bind(undefined, $ctx)',
          '$errCb.bind(undefined, $ctx)',
          ensurePromiseImplementation
        )
    )

    // Processing callback
    // Emit "ready" event
    .callAvailable(null, InternalFnNames.emit, [ '"ready"', '$ctx' ])

    .whenAvailable(
      InternalFnNames.preExecuteAction,
      x => x
        .append(`var $dd = ${InternalFnNames.preExecuteAction}($ctx);`)
        .handleAsyncContinuation(
          '$dd',
          '$preCb.bind(undefined, $ctx)',
          '$errCb.bind(undefined, $ctx)',
          ensurePromiseImplementation
        )
    )

    // Pre-execute callback
    // Emit "execution" event
    .callAvailable(null, InternalFnNames.emit, [ '"execution"', '$ctx' ])

    .whenAvailable(
      InternalFnNames.executeAction,
      x => x
        .append(`var $v = ${InternalFnNames.executeAction}($ctx);`)
        .handleAsyncContinuation(
          '$v',
          '$exCb.bind(undefined, $ctx)',
          '$errCb.bind(undefined, $ctx)',
          ensurePromiseImplementation
        )
    )

    // Execute callback
    .callAvailable(null, InternalFnNames.finalizeContext, [ '$ctx', '$v' ])
    .callAvailable('$v', InternalFnNames.processResult, [ '$v', '$ctx' ])

    // Emit "success" event
    .callAvailable(null, InternalFnNames.emit, [ '"success"', '$ctx', '$v' ])

    .finish('Promise.resolve($v)')

  // Create "input" function

  const fnInit = fnAbstract.clone()
    .setName('$initAction')
    .setArguments('name', 'params', 'metaData')
    .setContext('$dispatch', fnDispatch.build())
    .setErrorHandlerCode(`return Promise.reject($error);`)

    .declare('$ctx', `${InternalFnNames.createContext}(name, params, metaData)`)

    // Emit "created" event
    .callAvailable(null, InternalFnNames.emit, [ '"created"', '$ctx' ])

    .whenAvailable(InternalFnNames.isActionSupported, x => x
      .conditional(`!${InternalFnNames.isActionSupported}(name)`)
      /* eslint-disable indent */
      .open()
        .declare('$errorUnknown', 'Promise.reject(new ServiceActionNotFoundError())')
        .callAvailable(null, InternalFnNames.finalizeContext, [ '$ctx', 'undefined', '$errorUnknown' ])

        // Emit "unknown" event
        .callAvailable(null, InternalFnNames.emit, [ '"unknown"', '$ctx' ])

        .when(
          includeContext,
          x => x.finishWithContext('$errorUnknown', '$ctx'),
          x => x.finish('$errorUnknown')
        )
      .close()
      /* eslint-enable indent */
    )
    .when(
      includeContext,
      x => x.finishWithContext('$dispatch($ctx)', '$ctx'),
      x => x.finish('$dispatch($ctx)')
    )

  // Return back "input" function
  return fnInit.build()
}

module.exports = createActionExecutor