packages/hmr-plugin/src/internal/hmr-lifecycle.ts

Summary

Maintainability
A
0 mins
Test Coverage
import { ɵNgxsAppBootstrappedState } from '@ngxs/store/internals';
import { Observable, Subscription } from 'rxjs';
import { StateContext } from '@ngxs/store';

import { HmrOptionBuilder } from './hmr-options-builder';
import { HmrCallback, NgxsHmrLifeCycle } from '../symbols';
import { HmrStateContextFactory } from './hmr-state-context-factory';
import { HmrBeforeDestroyAction } from '../actions/hmr-before-destroy.action';
import { HmrStorage } from './hmr-storage';

export class HmrLifecycle<T extends Partial<NgxsHmrLifeCycle<S>>, S> {
  constructor(
    private ngAppModule: T,
    private appBootstrappedState: ɵNgxsAppBootstrappedState,
    private storage: HmrStorage<S>,
    private context: HmrStateContextFactory<T, S>,
    private options: HmrOptionBuilder
  ) {}

  public hmrNgxsStoreOnInit(hmrAfterOnInit: HmrCallback<S>) {
    let moduleHmrInit: HmrCallback<S> = this.getModuleHmrInitCallback();
    moduleHmrInit = moduleHmrInit.bind(this.ngAppModule);
    this.detectIvyWithJIT();
    this.stateEventLoop((ctx, state) => {
      moduleHmrInit(ctx, state);
      hmrAfterOnInit(ctx, state);
    });
  }

  private getModuleHmrInitCallback(): HmrCallback<S> {
    if (typeof this.ngAppModule.hmrNgxsStoreOnInit === 'function') {
      return this.ngAppModule.hmrNgxsStoreOnInit;
    }
    return function defaultModuleHmrInit(ctx, state) {
      ctx.patchState(state);
    };
  }

  public hmrNgxsStoreBeforeOnDestroy(): Partial<S> {
    let state: Partial<S> = {};
    const ctx: StateContext<S> = this.context.createStateContext();
    if (typeof this.ngAppModule.hmrNgxsStoreBeforeOnDestroy === 'function') {
      state = this.ngAppModule.hmrNgxsStoreBeforeOnDestroy(ctx);
    } else {
      state = ctx.getState();
    }

    ctx.dispatch(new HmrBeforeDestroyAction(state));
    return state;
  }

  private stateEventLoop(callback: HmrCallback<S>): void {
    if (!this.storage.hasData()) return;

    const state$: Observable<any> = this.context.store.select(state => state);

    this.appBootstrappedState.subscribe(() => {
      let eventId: number;
      const storeEventId: Subscription = state$.subscribe(() => {
        // setTimeout used for zone detection after set hmr state
        clearInterval(eventId);
        eventId = window.setTimeout(() => {
          // close check on the message queue
          storeEventId.unsubscribe();
          // if events are no longer running on the call stack,
          // then we can update the state
          callback(this.context.createStateContext(), this.storage.snapshot as Partial<S>);
        }, this.options.deferTime);
      });
    });
  }

  private detectIvyWithJIT(): void {
    const jit: boolean = this.ngAppModule.constructor.hasOwnProperty('__annotations__');
    const ivy: boolean = this.ngAppModule.constructor.hasOwnProperty('ɵmod');
    if (jit && ivy) {
      throw new Error(
        `@ngxs/hmr-plugin doesn't work with JIT mode in Angular Ivy. Please use AOT mode.`
      );
    }
  }
}