All files / core action.ts

100% Statements 50/50
100% Branches 22/22
100% Functions 17/17
100% Lines 47/47
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170                                                                                2x       2x 2x                   2x 1x           2x 22x                 2x 74x 74x 13x 13x   74x                   2x 19x 10x   19x 19x                 26x   26x 26x 26x 3x     23x       61x 61x 53x 53x 53x     8x         60x 54x         60x   2x 58x   21x         58x       22x 18x   22x           23x   23x       2x 1x   2x 20x     2x  
import 'rxjs/add/observable/empty'
import 'rxjs/add/observable/from'
import 'rxjs/add/operator/catch'
import 'rxjs/add/operator/map'
import 'rxjs/add/operator/mergeMap'
import 'rxjs/add/operator/share'
import 'rxjs/add/operator/skipWhile'
 
import Immutable from './immutable'
 
import { ActionObserver } from './observers'
import { Observable } from 'rxjs/Observable'
import { Observer } from 'rxjs/Observer'
import { ReplaceableState } from './replaceable-state'
import { State } from './state'
 
/**
 * Defines an action which an be extended to implement custom actions for a statex application
 *
 * @example
 *
 * // Create your own action class
 * class PageSwitchAction extends Action {
 *   constructor(public pageId: string) { super() }
 * }
 *
 * // Subscribe to your action
 * new PageSwitchAction(undefined).subscribe((state: State, action: PageSwitchAction): Observable<State> => {
 *   return Observable.create((observer: Observer<State>) => {
 *     observer.next(updatedState)
 *       observer.complete()
 *   }).share()
 * }, this)
 *
 * // Dispatch your action
 * new PageSwitchAction('page1').dispatch()
 *
 * @export
 * @class Action
 */
export class Action {
 
  private static _lastAction: Action
  private static _showError: boolean
  private static identities: any[] = []
  private static subscriptions: any[] = []
 
  /**
   * The last action occurred
   *
   * @readonly
   * @static
   *
   * @memberOf Action
   */
  public static get lastAction() {
    return Action._lastAction
  }
 
  /**
   * Set show error flag, if set to true, this module must show errors on Action rejections
   */
  public static set showError(showError: boolean) {
    Action._showError = showError
  }
 
  /**
   * Returns identity of this class
   *
   * @readonly
   * @type {string}
   */
  get identity(): string {
    let id = Action.identities.indexOf(this.constructor)
    if (id < 0) {
      Action.identities.push(this.constructor)
      id = Action.identities.indexOf(this.constructor)
    }
    return `c${id}`
  }
 
  /**
   * Subscribe to this action. actionObserver will be called when 'dispatch()' is invoked
   *
   * @param {ActionObserver} actionObserver The function that process the action
   * @param {*} context Context binding
   * @returns {Action}
   */
  public subscribe(actionObserver: ActionObserver, context: any): Action {
    if (!Action.subscriptions[this.identity]) {
      Action.subscriptions[this.identity] = []
    }
    Action.subscriptions[this.identity].push(actionObserver.bind(context))
    return this
  }
 
  /**
   * Dispatch this action. Returns an observable which will be completed when all action subscribers
   * complete it's processing
   *
   * @returns {Observable<S>}
   */
  dispatch(): Promise<any> {
 
    Action._lastAction = this
    let subscriptions: ActionObserver[] = Action.subscriptions[this.identity]
    if (subscriptions == undefined || subscriptions.length === 0) {
      return new Promise(resolve => resolve())
    }
 
    let observable: Observable<any> = Observable.from(subscriptions)
 
      // convert 'Observable' returned by action subscribers to state
      .flatMap((actionObserver: ActionObserver): Observable<any> => {
        const result = actionObserver(State.current, this)
        if (!(result instanceof Observable || result instanceof Promise)) {
          return Observable.create((observer: Observer<any>) => {
            observer.next(result)
            observer.complete()
          })
        }
        return result as Observable<any>
      })
 
      // if reducer returns function call that function to resolve state
      .map((state: any) => {
        if (typeof state === 'function') return state(State.current)
        return state
      })
 
      // merge or replace state
      .map((state: any) => {
        if (state instanceof ReplaceableState) {
          // replace the state with the new one if not 'undefined'
          return Immutable.from((state as ReplaceableState).state)
        } else if (state != undefined) {
          // merge the state with existing state
          return State.current.merge(state, { deep: true })
        }
      })
 
      // wait until all the subscripts have completed processing
      .skipWhile((state: any, i: number) => i + 1 < subscriptions.length)
 
      // push 'next' state to 'stateStream' if there has been a change to the state
      .map((state: any) => {
        if (state != undefined) {
          State.next(state)
        }
        return state
      })
 
      // make this sharable (to avoid multiple copies of this observable being created)
      .share()
 
    return new Promise((resolve, reject) => {
      // to trigger observable
      observable.subscribe(() => {
        // empty function
      }, (error) => {
        // show error
        if (Action._showError) {
          console.error(error)
        }
        reject(error)
      }, () => resolve(State.current))
    })
  }
}