RolandJansen/intermix.js

View on GitHub
src/registry/MasterRegistry.ts

Summary

Maintainability
B
4 hrs
Test Coverage
C
77%
import { Reducer, ReducersMapObject, ActionCreatorsMapObject } from "redux";
import { store } from "../store/store";
import combineReducersWithRoot from "./combineReducersWithRoot";
import SeqPartRegistry from "./SeqPartRegistry";
import SeqPart from "../seqpart/SeqPart";
import PluginRegistry from "./PluginRegistry";
import { AudioEndpoint } from "../interfaces/interfaces";
import { IPlugin, IPluginConstructor } from "../interfaces/IRegistryItems";
import rootReducer from "../store/rootReducer";
import { addPlugin, addPart, removePlugin, removePart, connectAudioNodes } from "../store/rootActions";

// this file has a method also called "addPlugin"
// so we'll rename it to avoid confusion.
const addPluginAction = addPlugin;
const removePluginAction = removePlugin;

/**
 * Calls the appropriate sub-registries
 * and adds new items to the state and reducers map
 *
 * This is not derived from AbstractRegistry,
 * it's more like an orchestrator class.
 */
export default class MasterRegistry {
    private plugins: PluginRegistry;
    private seqParts: SeqPartRegistry;

    public constructor(private ac: AudioContext) {
        this.plugins = new PluginRegistry(ac);
        this.seqParts = new SeqPartRegistry();
    }

    public addPlugin(pluginClass: IPluginConstructor): string {
        const newPlugin: IPlugin = this.plugins.add(pluginClass);

        // build a new root reducer and replace the current one
        this.replaceReducer();

        store.dispatch(addPluginAction(newPlugin.uid));

        // make the new item observe the store
        newPlugin.unsubscribe = this.plugins.observeStore(store, newPlugin);
        return newPlugin.uid;
    }

    public removePlugin(itemId: string): void {
        this.plugins.remove(itemId);
        this.replaceReducer();
        store.dispatch(removePluginAction(itemId));
    }

    public addSeqPart(lengthInStepsPerBar?: number): string {
        let newPart: SeqPart;

        // add new item to the seqPart Registry
        if (lengthInStepsPerBar) {
            newPart = this.seqParts.add(lengthInStepsPerBar);
        } else {
            newPart = this.seqParts.add();
        }

        // build a new root reducer and replace the current one
        this.replaceReducer();

        store.dispatch(addPart(newPart.uid));

        // make the new item observe the store
        newPart.unsubscribe = this.seqParts.observeStore(store, newPart);

        return newPart.uid;
    }

    public removeSeqPart(itemId: string): void {
        this.seqParts.remove(itemId);
        this.replaceReducer();
        store.dispatch(removePart(itemId));
    }

    public getActionCreators(itemId: string, bound?: string): ActionCreatorsMapObject {
        const pluginKeys = this.plugins.getUidList();
        const seqPartKeys = this.seqParts.getUidList();
        const actionCreatorsType = bound === "unbound" ? "unboundActionCreators" : "actionCreators";
        let actionCreators: ActionCreatorsMapObject = {};

        if (pluginKeys.includes(itemId)) {
            const item = this.plugins.itemList.get(itemId);
            if (item) {
                actionCreators = Object.assign({}, item[actionCreatorsType]);
            }
        } else if (seqPartKeys.includes(itemId)) {
            const item = this.seqParts.itemList.get(itemId);
            if (item) {
                actionCreators = Object.assign({}, item[actionCreatorsType]);
            }
        }

        return actionCreators;
    }

    /**
     * Connects two audio endpoints and dispatches the new state.
     * If the id of the input plugin is not valid, it connects to the soundcard input.
     * If the id of the output plugin is not valid, it cancels the operation.
     * @param connection Audio endpoints to be connected
     */
    public connectAudioNodes(connection: [AudioEndpoint, AudioEndpoint]): void {
        const output = connection[0];
        const input = connection[1];

        const pluginOut = this.plugins.itemList.get(output[0]);
        const pluginIn = this.plugins.itemList.get(input[0]);
        if (typeof pluginOut === "undefined") {
            return;
        }

        const audioNodeOut = pluginOut.outputs[output[1]];
        const audioNodeIn = pluginIn ? pluginIn.inputs[input[1]] : this.ac.destination;

        if (audioNodeIn.numberOfOutputs === 0) {
            input[0] = "destination"; // enshure that no wrong id will be dispatched
        }
        audioNodeOut.disconnect();
        audioNodeOut.connect(audioNodeIn);
        store.dispatch(connectAudioNodes(connection));
    }

    /**
     * Combines all sub reducers with the root reducer
     * and replaces the current reducer
     */
    private replaceReducer(): void {
        const pluginReducers: ReducersMapObject = this.plugins.getAllSubReducers();
        const seqPartReducers: ReducersMapObject = this.seqParts.getAllSubReducers();

        const subReducers: ReducersMapObject = this.getSubReducer(pluginReducers, seqPartReducers);
        const reducerTree: Reducer = this.getCompleteReducer(rootReducer, subReducers);

        store.replaceReducer(reducerTree);
    }

    private getSubReducer(...subReducers: ReducersMapObject[]): ReducersMapObject {
        return Object.assign({}, ...subReducers);
    }

    private getCompleteReducer(rootReducer: Reducer, subReducers: ReducersMapObject): Reducer {
        return combineReducersWithRoot(subReducers, rootReducer);
    }
}