state-manager.ts 5.57 KB
/**
 * @fileoverview
 * State handler.
 * 
 * @author mebjas <minhazav@gmail.com>
 */

/** Different states of scanner */
export enum Html5QrcodeScannerState {
    // Invalid internal state, do not set to this state.
    UNKNOWN = 0,
    // Indicates the scanning is not running or user is using file based
    // scanning.
    NOT_STARTED = 1,
    // Camera scan is running.
    SCANNING,
    // Camera scan is paused but camera is running.
    PAUSED,
}

/** Transaction for state transition. */
export interface StateManagerTransaction {
    /**
     * Executes the current transaction.
     */
    execute(): void;

    /**
     * Cancels the current transaction.
     */
    cancel(): void;
}

/** Manager class for states. */
export interface StateManager {
    /**
     * Start a transition to a new state. No other transitions will be allowed
     * till this one is executed.
     * 
     * @param newState new state to transition to.
     * 
     * @returns transaction of type {@interface StateManagerTransaction}.
     * 
     * @throws error if the new state is not a valid transition from current
     * state.
     */
    startTransition(newState: Html5QrcodeScannerState): StateManagerTransaction;

    /**
     * Directly execute a transition.
     * 
     * @param newState new state to transition to.
     * 
     * @throws error if the new state is not a valid transition from current
     * state.
     */
    directTransition(newState: Html5QrcodeScannerState): void;

    /**
     * Get current state.
     */
    getState(): Html5QrcodeScannerState;
}

/** 
 * Implementation of {@interface StateManager} and 
 * {@interface StateManagerTransaction}.
 */
class StateManagerImpl implements StateManager, StateManagerTransaction {

    private state: Html5QrcodeScannerState = Html5QrcodeScannerState.NOT_STARTED;

    private onGoingTransactionNewState: Html5QrcodeScannerState
        = Html5QrcodeScannerState.UNKNOWN;

    public directTransition(newState: Html5QrcodeScannerState) {
        this.failIfTransitionOngoing();
        this.validateTransition(newState);
        this.state = newState;
    }

    public startTransition(newState: Html5QrcodeScannerState): StateManagerTransaction {
        this.failIfTransitionOngoing();
        this.validateTransition(newState);

        this.onGoingTransactionNewState = newState;
        return this;
    }

    public execute() {
        if (this.onGoingTransactionNewState 
                === Html5QrcodeScannerState.UNKNOWN) {
            throw "Transaction is already cancelled, cannot execute().";
        }

        const tempNewState = this.onGoingTransactionNewState;
        this.onGoingTransactionNewState = Html5QrcodeScannerState.UNKNOWN;
        this.directTransition(tempNewState);        
    }

    public cancel() {
        if (this.onGoingTransactionNewState 
                === Html5QrcodeScannerState.UNKNOWN) {
            throw "Transaction is already cancelled, cannot cancel().";
        }

        this.onGoingTransactionNewState = Html5QrcodeScannerState.UNKNOWN;
    }

    public getState(): Html5QrcodeScannerState {
        return this.state;
    }

    //#region private methods
    private failIfTransitionOngoing() {
        if (this.onGoingTransactionNewState 
            !== Html5QrcodeScannerState.UNKNOWN) {
            throw "Cannot transition to a new state, already under transition"; 
         }
    }

    private validateTransition(newState: Html5QrcodeScannerState) {
        switch(this.state) {
            case Html5QrcodeScannerState.UNKNOWN:
                throw "Transition from unknown is not allowed";
            case Html5QrcodeScannerState.NOT_STARTED:
                this.failIfNewStateIs(newState, [Html5QrcodeScannerState.PAUSED]);
                break;
            case Html5QrcodeScannerState.SCANNING:
                // Both state transitions legal from here.
                break;
            case Html5QrcodeScannerState.PAUSED:
                // Both state transitions legal from here.
                break;
        }
    }

    private failIfNewStateIs(
        newState: Html5QrcodeScannerState,
        disallowedStatesToTransition: Array<Html5QrcodeScannerState>) {
        for (const disallowedState of disallowedStatesToTransition) {
            if (newState === disallowedState) {
                throw `Cannot transition from ${this.state} to ${newState}`;
            }
        }
    }
    //#endregion
}

export class StateManagerProxy {
    private stateManager: StateManager;

    constructor(stateManager: StateManager) {
        this.stateManager = stateManager;
    }

    startTransition(newState: Html5QrcodeScannerState): StateManagerTransaction {
        return this.stateManager.startTransition(newState);
    }

    directTransition(newState: Html5QrcodeScannerState) {
        this.stateManager.directTransition(newState);
    }

    getState(): Html5QrcodeScannerState {
        return this.stateManager.getState();
    }

    canScanFile(): boolean {
        return this.stateManager.getState() === Html5QrcodeScannerState.NOT_STARTED;
    }

    isScanning(): boolean {
        return this.stateManager.getState() !== Html5QrcodeScannerState.NOT_STARTED;
    }

    isStrictlyScanning(): boolean {
        return this.stateManager.getState() === Html5QrcodeScannerState.SCANNING;
    }

    isPaused(): boolean {
        return this.stateManager.getState() === Html5QrcodeScannerState.PAUSED;
    }
}

/**
 * Factory for creating instance of {@class StateManagerProxy}.
 */
 export class StateManagerFactory {
    public static create(): StateManagerProxy {
        return new StateManagerProxy(new StateManagerImpl());
    }
}