Home Reference Source

src/directory/DirectorySubject.js

import * as R from 'ramda';

import * as TypeChecker from '../functions/typeChecker';
import DirectoryMutation from './DirectoryMutation';
import Rx from 'rxjs/Rx';
import extendArray from '../functions/extendArray';
import createReactiveInterface from '../functions/createReactiveInterface';

/**
 * @class {DirectorySubject}
 */
export default class DirectorySubject {
    /**
     * Create DirectorySubject
     * @param {Directory} directory 
     * @param {EntityStore} store 
     */
    constructor(directory, store) {
        /**
         * Attached {@link Directory}
         * @member {Directory}
         */
        this.directory = directory;
        /**
         * Attached {@link EntityStore}
         * @member {EntityStore}
         */
        this._store = store;
        /**
         * [model name][entity id] like structure of subscriptions
         * @member {Object}
         */
        this._subscriptions = {};
        /**
         * Observable, updated each time directory state is changed
         * @member {Rx.Subject}
         */
        this.observable = new Rx.Subject();

        Object.entries(store.models).forEach(([key, value]) => {
            this._subscriptions[key] = {};
        });

        this.observable.subscribe(_ => {
            this.updateInterface();
        })

        /**
         * Reactive interface which contains mutations, computed values and evaluated state
         * @member {Object}
         */
        this.interface = null;
        this.updateInterface();
    }

    /**
     * Force update interface
     */
    updateInterface() {
        this.interface = this._createReactiveInterface();
    }

    /**
     * Mutate directory state
     * @param {Object} mutation - payload
     * @param {string} message - commit message
     */
    mutate(mutation, message = "") {
        new DirectoryMutation(this, mutation).commit(message)
    }

    /**
     * Writes evaluated state into item from structure
     * @param {Object} structure - structure
     * @param {Object} item - object to write to
     * @param {number} deepness - how deep to resolve relationships
     */
    _asObject(structure, item, deepness) {
        const json = {};
        Object.entries(structure).forEach(([key, value]) => {
            if (TypeChecker.isAttribute(structure[key])) {
                json[key] = item[key];
            } else if (TypeChecker.isAttributeArray(structure[key])) {
                json[key] = item[key];
            } else if (TypeChecker.isReference(structure[key])) {
                if (deepness > 0 && item[key])
                    json[key] = item[key].$subject.asObject(deepness - 1);
                else
                    json[key] = null;
            } else if (TypeChecker.isReferenceArray(structure[key])) {
                if (deepness > 0)
                    json[key] = item[key].map(inter => {
                        return inter.$subject.asObject(deepness - 1);
                    })
                else
                    json[key] = [];
            } else if (TypeChecker.isIdentifier(structure[key])) {
                json[key] = item[key];
            } else {
                //object
                json[key] = this._asObject(structure[key], item[key], deepness);
            }
        })

        return json;
    }

    /**
     * @returns {Object} - evaluated javascript object
     * @param {number} relationDeepness - how deep to resolve relationships
     */
    asObject(relationDeepness) {
        if (this.interface) {
            const result = this._asObject(this.directory.structure, this.interface, relationDeepness)
            this._applyComputed(result);
            return result;
        } else
            return null;
    }

    /**
     * Force update observable
     */
    update() {
        this.observable.next(this.state);
    }


    /**
     * @returns {Object} - reactive interface
     */
    _createReactiveInterface() {
        const get = (key) => {
            return this.directory.state[key];
        }
        const set = (key, value) => {
            this.mutate({
                [key]: value
            }, `Modify ${key} with value ${value}`)
        }
        const reactiveInterface = {};
        this._mapObserverToSource(reactiveInterface, this.directory.structure, {
            get,
            set
        });
        this._applyComputed(reactiveInterface);
        this._applyMutations(reactiveInterface);
        return reactiveInterface;
    }


    /**
     * Insert computed values into item(reactive interface or evaluated state)
     * @param {Object} item 
     */
    _applyComputed(item) {
        Object.entries(this.directory.computed).forEach(([key, value]) => {
            item[key] = value.call(item)
        });
    }

    /**
     * Insert mutation functions into reactiveInteface
     * @param {Object} reactiveInterface 
     */
    _applyMutations(reactiveInterface) {
        Object.entries(this.directory.mutations).forEach(([key, value]) => {
            reactiveInterface[key] = value.bind(reactiveInterface)
        })
    }


    /**
     * Define reactive getters and setters
     * @param {Object} item - object to write to
     * @param {Object} structure - structure
     * @param {Object} stateProvider - get by key and set by key and value helper
     */
    _mapObserverToSource(item = {}, structure, stateProvider) {
        const store = this._store;
        const directorySubject = this;
        createReactiveInterface({
            item: item,
            stateProvider: stateProvider,
            store: this._store,
            updateState(source) {
                directorySubject.update();
            },
            structure: this.directory.structure,
            subscribe(name, id) {
                if (!directorySubject.isSubscriberOf(name, id)) {
                    directorySubject.subscribe(name, id)(({ payload, source }) => {
                        directorySubject.update();
                    });
                }
            },
            unsubscribe(name, id) {
                directorySubject.unsubscribe(name, id)
            }
        });
    }

    /**
     * @returns {Boolean} - is this subject is subscriber of passed entity
     * @param {string} name - model name
     * @param {*} id - entity id
     */
    isSubscriberOf(name, id) {
        return this._subscriptions[name][id];
    }

    /**
     * Prepares to subscribe
     * Will rewrite previous subscription
     * @returns {Function} - real subscribe function
     * @param {stirng} name - model name
     * @param {*} id - entity id
     */
    subscribe(name, id) {
        return (a, b, c) => {
            this._subscriptions[name][id] = this._store.getOrCreateEntitySubject(name, id).observable
                .filter(({ payload, source }) => source !== this)
                .subscribe(a, b, c);
        }
    }

    /**
     * Unsubscribe from entity updates and delete subscription
     * @param {string} name - model name
     * @param {*} id - entity id 
     */
    unsubscribe(name, id) {
        if (this._subscriptions[name][id]) {
            this._subscriptions[name][id].unsubscribe();
        }
        delete this._subscriptions[name][id];
    }
}