"use strict" ; import { MapLike } from "map-like" ; import { Payload } from "../payload/Payload" ; import { DispatcherPayloadMeta , DispatcherPayloadMetaImpl } from "../DispatcherPayloadMeta" ; import { isWillExecutedPayload } from "../payload/WillExecutedPayload" ; import { isCompletedPayload } from "../payload/CompletedPayload" ; import { shallowEqual } from "shallow-equal-object" ; import { Dispatcher } from "../Dispatcher" ; import { StateMap , StoreMap } from "./StoreGroupTypes" ; import { createStoreStateMap , StoreStateMap } from "./StoreStateMap" ; import { Store } from "../Store" ; import { StoreGroupEmitChangeChecker } from "./StoreGroupEmitChangeChecker" ; import { shouldStateUpdate } from "./StoreGroupUtils" ; import { Commitment } from "../UnitOfWork/UnitOfWork" ; import { InitializedPayload } from "../payload/InitializedPayload" ; import { StoreGroupChangingStoreStrictChecker } from "./StoreGroupChangingStoreStrictChecker" ; import { StoreGroupLike , StoreGroupReasonForChange } from "./StoreGroupLike" ; import { isDidExecutedPayload } from "../payload/DidExecutedPayload" ; import { isErrorPayload } from "../payload/ErrorPayload" ; import { isTransactionBeganPayload } from "../payload/TransactionBeganPayload" ; import { isTransactionEndedPayload } from "../payload/TransactionEndedPayload" ; import AlminInstruments from "../instrument/AlminInstruments" ; import { DebugId } from "../instrument/AlminAbstractPerfMarker" ; import { generateNewId } from "./StoreGroupIdGenerator" ; import { assertOK } from "../util/assert" ; import { Events } from "../Events" ; export interface StoreGroupState { [ key ] : any ; } /**
 * assert: check arguments of constructor.
 */ const assertConstructorArguments = ( arg ) : void => { const message = `Should initialize this StoreGroup with a stateName-store mapping object.
const aStore = new AStore();
const bStore = new BStore();
// A arguments is stateName-store mapping object like { stateName: store }
const storeGroup = new StoreGroup({
    a: aStore,
    b: bStore
});
console.log(storeGroup.getState());
// { a: "a value", b: "b value" }
` ; assertOK ( typeof arg === "object" && arg !== null && ! Array . isArray ( arg ) , message ) ; const keys = Object . keys ( arg ) ; for ( let i = 0 ; i < keys . length ; i ++ ) { const key = keys [ i ] ; const value = arg [ key ] ; assertOK ( Store . isStore ( value ) , `value should be instance of Store: ${ key }: ${ value }` + "\n" + message ) ; } } ; type StoreGroupChangeEvent < T > = { stores : Array < Store < T > > ; details : StoreGroupReasonForChange | undefined ; } ; /**
 * StoreGroup is a parts of read-model.
 *
 * StoreGroup has separated two phase in a life-cycle.
 * These are called Write phase and Read phase.
 *
 * StoreGroup often does write phase and, then read phase.
 *
 * ## Write phase
 *
 * StoreGroup notify update timing for each stores.
 *
 * It means that call each `Store#receivePayload()`.
 *
 * ### When
 *
 * - Initialize StoreGroup
 * - A parts of life-cycle during execute UseCase
 * - Force update StoreGroup
 *
 * ### What does store?
 *
 * - Store update own state if needed
 *
 * ### What does not store?
 *
 * - Store should not directly assign to state instead of using `Store#setState`
 *
 * ## Read phase
 *
 * StoreGroup read the state from each stores.
 *
 * It means that call each `Store#getState()`.
 *
 * ### When
 *
 * - Initialize StoreGroup
 * - A parts of life-cycle during execute UseCase
 * - Force update StoreGroup
 * - Some store call `Store#emitChange`
 *
 * ### What does store?
 *
 * - Store return own state
 *
 * ### What does not store?
 *
 * - Does not update own state
 * - Please update own state in write phase
 *
 * ### Notes
 *
 * #### Pull-based: Recompute every time value is needed
 *
 * Pull-based Store has only getState.
 * Just create the state and return it when `getState` is called.
 *
 * #### Push-based: Recompute when a source value changes
 *
 * Push-based Store have to create the state and save it.
 * Just return the state when `getState` is called.
 * It is similar with cache system.
 *
 */ export class StoreGroup < T > extends Dispatcher implements StoreGroupLike { static displayName ? : string ; public stores : Array < Store < T > > ; public state : StateMap < T > ; public name : string ; public id : string ; private _changingStores : Array < Store < T > > = [ ] ; private _releaseHandlers : Array < Function > = [ ] ; private _workingUseCaseMap : MapLike < string , boolean > ; private _finishedUseCaseMap : MapLike < string , boolean > ; private _stateCacheMap : MapLike < Store < T > , any > ; private _storeStateMap : StoreStateMap ; private isStrictMode = false ; private isTransactionWorking = false ; private storeGroupEmitChangeChecker = new StoreGroupEmitChangeChecker ( ) ; private storeGroupChangingStoreStrictChecker = new StoreGroupChangingStoreStrictChecker ( ) ; private storeGroupChangeEvent = new Events < StoreGroupChangeEvent < T > > ( ) ; /**
     * Initialize this StoreGroup with a stateName-store mapping object.
     *
     * The rule of initializing StoreGroup is that "define the state name of the store".
     *
     * ## Example
     *
     * Initialize with store-state mapping object.
     *
     * ```js
     * class AStore extends Store {
     *     getState() {
     *         return "a value";
     *     }
     * }
     * class BStore extends Store {
     *     getState() {
     *         return "b value";
     *     }
     * }
     * const aStore = new AStore();
     * const bStore = new BStore();
     * const storeGroup = new StoreGroup({
     *     a: aStore, // stateName: store
     *     b: bStore
     * });
     * console.log(storeGroup.getState());
     * // { a: "a value", b: "b value" }
     * ```
     */ constructor ( public stateStoreMapping ) { super ( ) ; if ( process . env . NODE_ENV !== "production" ) { assertConstructorArguments ( stateStoreMapping ) ; } const own = this . constructor as typeof StoreGroup ; type { string } this . name = own . displayName || own . name || "StoreGroup" ; this . id = generateNewId ( ) ; this . _storeStateMap = createStoreStateMap ( stateStoreMapping ) ; this . stores = this . _storeStateMap . stores ; this . _workingUseCaseMap = new MapLike < string , boolean > ( ) ; this . _finishedUseCaseMap = new MapLike < string , boolean > ( ) ; this . _stateCacheMap = new MapLike < Store < T > , any > ( ) ; this . stores . forEach ( ( store ) => { const unRegisterHandler = this . _registerStore ( store ) ; this . _releaseHandlers . push ( unRegisterHandler ) ; } ) ; this . state = this . initializeGroupState ( this . stores ) ; } /**
     * If exist working UseCase, return true
     */ private get existWorkingUseCase ( ) { return this . _workingUseCaseMap . size > 0 ; } /**
     * Return the state object that merge each stores's state
     */ getState ( ) { return this . state ; } private initializeGroupState ( stores ) { const payload = new InitializedPayload ( ) ; const meta = new DispatcherPayloadMetaImpl ( { isTrusted : true } ) ; this . writePhaseInRead ( stores , payload , meta ) ; return this . readPhaseInRead ( stores ) ; } private writePhaseInRead ( stores , payload , meta , debugId ? ) { if ( process . env . NODE_ENV !== "production" && AlminInstruments . debugTool ) { if ( debugId ) { AlminInstruments . debugTool . beforeStoreGroupWritePhase ( debugId , this ) ; } } for ( let i = 0 ; i < stores . length ; i ++ ) { const store = stores [ i ] ; if ( process . env . NODE_ENV !== "production" ) { this . storeGroupChangingStoreStrictChecker . mark ( store ) ; } const hasReceivePayload = typeof store . receivePayload === "function" ; if ( process . env . NODE_ENV !== "production" && AlminInstruments . debugTool ) { if ( hasReceivePayload && debugId ) { AlminInstruments . debugTool . beforeStoreReceivePayload ( debugId , store ) ; } } if ( ! meta . isTrusted ) { store . dispatch ( payload , meta ) ; } if ( hasReceivePayload ) { store . receivePayload ! ( payload ) ; } if ( process . env . NODE_ENV !== "production" && AlminInstruments . debugTool ) { if ( hasReceivePayload && debugId ) { AlminInstruments . debugTool . afterStoreReceivePayload ( debugId , store ) ; } } if ( process . env . NODE_ENV !== "production" ) { this . storeGroupChangingStoreStrictChecker . unMark ( store ) ; } } if ( process . env . NODE_ENV !== "production" && AlminInstruments . debugTool ) { if ( debugId ) { AlminInstruments . debugTool . afterStoreGroupWritePhase ( debugId , this ) ; } } } private readPhaseInRead ( stores ) { const debugTool = AlminInstruments . debugTool ; if ( process . env . NODE_ENV !== "production" && debugTool ) { debugTool . beforeStoreGroupReadPhase ( this . id , this ) ; } const groupState = { } ; for ( let i = 0 ; i < stores . length ; i ++ ) { const store = stores [ i ] ; const prevState = this . _stateCacheMap . get ( store ) ; if ( process . env . NODE_ENV !== "production" && debugTool ) { debugTool . beforeStoreGetState ( this . id , this ) ; } const nextState = store . getState ( ) ; if ( process . env . NODE_ENV !== "production" && debugTool ) { debugTool . afterStoreGetState ( this . id , this ) ; } const stateName = this . _storeStateMap . get ( store ) ; if ( process . env . NODE_ENV !== "production" ) { assertOK ( stateName !== undefined , `Store:${ store . name } is not registered in constructor.
But, ${ store . name }#getState() was called.` ) ; this . storeGroupEmitChangeChecker . warningIfStatePropertyIsModifiedDirectly ( store , prevState , nextState ) ; this . storeGroupEmitChangeChecker . unMark ( store ) ; } if ( ! shouldStateUpdate ( store , prevState , nextState ) ) { groupState [ stateName ! ] = prevState ; continue ; } this . _stateCacheMap . set ( store , nextState ) ; this . _addChangingStateOfStores ( store ) ; groupState [ stateName ! ] = nextState ; } if ( process . env . NODE_ENV !== "production" && debugTool ) { debugTool . afterStoreGroupReadPhase ( this . id , this ) ; } return groupState as StateMap < T > ; } /**
     * Use `shouldStateUpdate()` to let StoreGroup know if a event is not affected.
     * The default behavior is to emitChange on every life-cycle change,
     * and in the vast majority of cases you should rely on the default behavior.
     * Default behavior is shallow-equal prev/next state.
     *
     * ## Example
     *
     * If you want to use `Object.is` to equal states, overwrite following.
     *
     * ```js
     * shouldStateUpdate(prevState, nextState) {
     *    return !Object.is(prevState, nextState)
     * }
     * ```
     */ shouldStateUpdate ( prevState , nextState ) { return ! shallowEqual ( prevState , nextState ) ; } /**
     * Emit change if the state is changed.
     * If call with no-arguments, use ChangedPayload by default.
     */ emitChange ( ) { this . emitChangeIfStateIsChange ( ) ; } private tryToUpdateState ( payload , meta , debugId ? ) { this . writePhaseInRead ( this . stores , payload , meta , debugId ) ; if ( ! this . isTransactionWorking ) { this . emitChangeIfStateIsChange ( payload , meta ) ; } } /**
     * **Internal**
     *
     * ## Implementation Notes:
     *
     * StoreGroup should be push-model.
     * Almin can control all transaction to StoreGroup.
     *
     * It means that StoreGroup doesn't use `this.onDispatch`.
     * It use `commit` insteadof receive data via `this.onDispatch`.
     */ commit ( commitment ) { const payload = commitment . payload ; const meta = commitment . meta ; const debugId = commitment . debugId ; if ( isTransactionBeganPayload ( payload ) ) { this . isTransactionWorking = true ; } else if ( ! meta . isTrusted ) { this . tryToUpdateState ( payload , meta , debugId ) ; } else if ( isErrorPayload ( payload ) ) { this . tryToUpdateState ( payload , meta , debugId ) ; } else if ( isWillExecutedPayload ( payload ) && meta . useCase ) { this . _workingUseCaseMap . set ( meta . useCase . id , true ) ; } else if ( isDidExecutedPayload ( payload ) && meta . useCase ) { if ( meta . isUseCaseFinished ) { this . _finishedUseCaseMap . set ( meta . useCase . id , true ) ; } this . tryToUpdateState ( payload , meta , debugId ) ; } else if ( isCompletedPayload ( payload ) && meta . useCase && meta . isUseCaseFinished ) { if ( this . _finishedUseCaseMap . has ( meta . useCase . id ) ) { this . _finishedUseCaseMap . delete ( meta . useCase . id ) ; this . _workingUseCaseMap . delete ( meta . useCase . id ) ; return ; } this . tryToUpdateState ( payload , meta , debugId ) ; this . _workingUseCaseMap . delete ( meta . useCase . id ) ; } else if ( isTransactionEndedPayload ( payload ) ) { this . isTransactionWorking = false ; this . emitChangeIfStateIsChange ( payload , meta ) ; } } useStrict ( ) { this . isStrictMode = true ; } private emitChangeIfStateIsChange ( payload ? , meta ? ) { this . _pruneChangingStateOfStores ( ) ; const nextState = this . readPhaseInRead ( this . stores ) ; if ( ! this . shouldStateUpdate ( this . state , nextState ) ) { return ; } this . state = nextState ; const changingStores = this . _changingStores . slice ( ) ; const details = payload && meta ? { payload , meta } : undefined ; this . storeGroupChangeEvent . emit ( { stores : changingStores , details } ) ; } /**
     * Observe changes of the store group.
     *
     * For example, the user can change store using `Store#setState` manually.
     * In this case, the `details` is defined and report.
     *
     * Contrast, almin try to update store in a lifecycle(didUseCase, completeUseCase etc...).
     * In this case, the `details` is not defined and report.
     *
     * StoreGroup#onChange workflow: https://code2flow.com/mHFviS
     */ onChange ( handler ) { const releaseHandler = this . storeGroupChangeEvent . addEventListener ( ( event ) => handler ( event . stores , event . details ) ) ; this . _releaseHandlers . push ( releaseHandler ) ; return releaseHandler ; } /**
     * Release all events handler.
     * You can call this when no more call event handler
     */ release ( ) { this . _releaseHandlers . forEach ( ( releaseHandler ) => releaseHandler ( ) ) ; this . _releaseHandlers . length = 0 ; this . _pruneChangingStateOfStores ( ) ; } /**
     * register store and listen onChange.
     * If you release store, and do call `release` method.
     */ private _registerStore ( store ) { const onChangeHandler = ( ) => { if ( process . env . NODE_ENV !== "production" ) { const prevState = this . _stateCacheMap . get ( store ) ; const nextState = store . getState ( ) ; this . storeGroupEmitChangeChecker . mark ( store , prevState , nextState ) ; if ( this . isStrictMode ) { this . storeGroupChangingStoreStrictChecker . warningIfStoreIsNotAllowedUpdate ( store ) ; } } if ( ! this . existWorkingUseCase ) { this . emitChangeIfStateIsChange ( ) ; } } ; if ( process . env . NODE_ENV !== "production" ) { ( onChangeHandler as any ) . displayName = `${ store . name }#onChange->handler` ; } return store . onChange ( onChangeHandler ) ; } private _addChangingStateOfStores ( store ) { if ( this . _changingStores . indexOf ( store ) === - 1 ) { this . _changingStores . push ( store ) ; } } private _pruneChangingStateOfStores ( ) { this . _changingStores = [ ] ; } }