import { push } from "redux-first-history" ; import { Location } from "history" ; import { SagaGenerator } from "../typed-saga" ; import { put } from "redux-saga/effects" ; import { produce , enablePatches , enableES5 } from "immer" ; import { app } from "../app" ; import { Logger } from "../Logger" ; import { TickIntervalDecoratorFlag } from "../module" ; import { setStateAction , State } from "../reducer" ; import { allowNavigation , blockNavigation } from "../util/browser-util" ; enableES5 ( ) ; if ( process . env . NODE_ENV === "development" ) { enablePatches ( ) ; } export interface ModuleHistoryLocation < State extends object > extends Omit < Location , "state" > { state : Readonly < State > | undefined ; } export interface ModuleLifecycleListener < RouteParam extends object = object , HistoryState extends object = object > { onEnter : ( entryComponentProps ? : any ) => SagaGenerator ; onDestroy : ( ) => SagaGenerator ; onLocationMatched : ( routeParameters : RouteParam , location : ModuleHistoryLocation < HistoryState > ) => SagaGenerator ; onTick : ( ( ) => SagaGenerator ) & TickIntervalDecoratorFlag ; } export class Module < RootState extends State , ModuleName extends keyof RootState [ "app" ] & string , RouteParam extends object = object , HistoryState extends object = object > implements ModuleLifecycleListener < RouteParam , HistoryState > { constructor ( readonly name , readonly initialState ) { } * onEnter ( entryComponentProps ) { } * onDestroy ( ) { } * onLocationMatched ( routeParam , location ) { } * onTick ( ) { } get state ( ) : Readonly < RootState [ "app" ] [ ModuleName ] > { return this . rootState . app [ this . name ] ; } get rootState ( ) : Readonly < RootState > { return app . store . getState ( ) as Readonly < RootState > ; } get logger ( ) : Logger { return app . logger ; } setNavigationPrevented ( isPrevented ) { if ( isPrevented ) { blockNavigation ( ) ; } else { allowNavigation ( ) ; } } setState < K extends keyof RootState [ "app" ] [ ModuleName ] > ( stateOrUpdater ) { if ( typeof stateOrUpdater === "function" ) { const originalState = this . state ; const updater = stateOrUpdater as ( state : RootState [ "app" ] [ ModuleName ] ) => void ; let patchDescriptions ; const newState = produce < Readonly < RootState [ "app" ] [ ModuleName ] > , RootState [ "app" ] [ ModuleName ] > ( originalState , ( draftState ) => { updater ( draftState ) ; } , process . env . NODE_ENV === "development" ? ( patches ) => { patchDescriptions = patches . map ( ( _ ) => _ . path . join ( "." ) ) ; } : undefined ) ; if ( newState !== originalState ) { const description = `@@${ this . name }/setState${ patchDescriptions ? `[${ patchDescriptions . join ( "/" ) }]` : `` }` ; app . store . dispatch ( setStateAction ( this . name , newState , description ) ) ; } } else { const partialState = stateOrUpdater as object ; this . setState ( ( state ) => Object . assign ( state , partialState ) ) ; } } /**
     * CAVEAT:
     * (1)
     * Calling this.pushHistory to other module should cancel the following logic.
     * Using store.dispatch here will lead to error while cancelling in lifecycle.
     *
     * Because the whole process is in sync mode:
     * dispatch push action -> location change -> router component will un-mount -> lifecycle saga cancel
     *
     * Cancelling the current sync-running saga will throw "TypeError: Generator is already executing".
     *
     * (2)
     * Adding yield cancel() in pushHistory is also incorrect.
     * If this.pushHistory is only to change state rather than URL, it will lead to the whole lifecycle saga cancelled.
     *
     * https://github.com/react-boilerplate/react-boilerplate/issues/1281
     */ pushHistory ( url ) ; pushHistory ( url , stateMode ) ; pushHistory < T extends object > ( url , state ) ; pushHistory ( state ) ; * pushHistory ( urlOrState , state ? ) { if ( typeof urlOrState === "string" ) { const url = urlOrState ; if ( state ) { yield put ( push ( url , state === "keep-state" ? app . browserHistory . location . state : state ) ) ; } else { yield put ( push ( url ) ) ; } } else { const currentURL = location . pathname + location . search ; const state = urlOrState ; yield put ( push ( currentURL , state ) ) ; } } }