Source

managers/BaseManager.js

const {INFO_PATH, PARTICIPANT_MOUNT_PATH, IDENTITY_MOUNT_PATH, DATABASE_MOUNT_PATH} = require('../constants');
const { getResolver ,getKeySSISpace,  _err} = require('../services/utils');
const relevantMounts = [PARTICIPANT_MOUNT_PATH, DATABASE_MOUNT_PATH];
const {getMessageManager, Message} = require('./MessageManager');
const DBLock = require('./DBLock');

/**
 * Base Manager Class
 *
 * Manager Classes in this context should do the bridge between the controllers
 * and the services exposing only the necessary api to the controllers while encapsulating <strong>all</strong> business logic.
 *
 * All Manager Classes should be singletons.
 *
 * This complete separation of concerts is very beneficial for 2 reasons:
 * <ul>
 *     <li>Allows for testing since there's no browser dependent code (i think) since the DSUStorage can be 'mocked'</li>
 *     <li>Allows for different controllers access different business logic when necessary (while benefiting from the singleton behaviour)</li>
 * </ul>
 *
 * This Base Manager Class is designed to integrate with the pdm-trust-loader and a init.file configuration of
 *
 * <pre>
 *      define $ID$ fromvar -$Identity-
 *      define $ENV$ fromvar -$Environment-
 *
 *      with cmd createdsu seed traceability specificstring
 *          define $SEED$ fromcmd getidentifier true
 *          createfile info $ID$
 *      endwith
 *      createfile environment.json $ENV$
 *      mount $SEED$ /id
 *
 *      with var $SEED$
 *          define $READ$ fromcmd derive true
 *      endwith
 *
 *      define $SECRETS$ fromcmd objtoarray $ID$
 *
 *      with cmd createdsu const traceability $SECRETS$
 *          mount $READ$ /id
 *          define $CONST$ fromcmd getidentifier true
 *      endwith
 *
 *      mount $CONST$ /participant
 *
 *      with cmd createdsu seed traceability fordb
 *          define $DB$ fromcmd getidentifier true
 *      endwith
 *
 *      mount $DB$ /db
 * </pre>
 *
 * As well as the SSApp Architecture {@link ../drawing.png here}
 *
 * it also integrates with the {@link DSUStorage} to provide direct access to the Base DSU by default.
 *
 * All other Managers in this architecture can inherit from this to get access to the getIdentity && getEnvironment API from the credentials set in the pdm-loader
 *
 *
 * @memberOf Managers
 * @class BaseManager
 * @abstract
 */
class BaseManager {
    /**
     * @param {DSUStorage} dsuStorage the controllers dsu storage
     * @param {function(err, BaseManager)} [callback] optional callback. called after initialization. mostly for testing
     * @constructor
     */
    constructor(dsuStorage, callback) {
        this.DSUStorage = dsuStorage;
        this.rootDSU = undefined;
        this.db = undefined;
        this.dbLock = undefined;
        this.participantConstSSI = undefined;
        this.did = undefined;
        this.messenger = undefined;
        this.identity = undefined;
        this.managerCache = {};
        this.controller = undefined;
        this._getResolver = getResolver;
        this._getKeySSISpace = getKeySSISpace;
        this._err = _err;

        const self = this;
        const initializer = function(){
            self._initialize((err) => {
                if (err){
                    console.log(`Could not initialize base manager ${err}`);
                    if(callback)
                        return callback(err);
                }
                console.log(`base manager initialized`);
                if (callback)
                    callback(undefined, self);
            });
        }

        if (!self.controller)
            return initializer();

        // For ui flow reasons
        setTimeout(() => {
            initializer();
        }, 100)

    };

    /**
     * Caches every other manager to enforce a singleton behaviour
     * @param {Manager} manager
     */
    cacheManager(manager){
        const name = manager.constructor.name;
        if (name in this.managerCache)
            throw new Error("Duplicate managers " + name);

        this.managerCache[name] = manager;
    }

    /**
     * Returns a cached {@link Manager}
     * @param {class | string} manager the class ex: 'getManager(SomethingManager)'
     * @throws error when the requested manager is not cached
     */
    getManager(manager){
        const name = typeof manager === 'string' ? manager : manager.name;
        if (!(name in this.managerCache))
            throw new Error("No manager cached " + name);
        return this.managerCache[name];
    }

    /**
     * Sends a message to a DID via the {@link MessageManager}
     * @param {string | Wc3DID} did
     * @param {string} api
     * @param {{}} message
     * @param {function(err)} callback
     */
    sendMessage(did, api, message, callback){
        const msg = new Message(api, message)
        this.messenger.sendMessage(did, msg, callback);
    }

    /**
     * Registers a {@link Manager} with the {@link MessageManager} of the provided api
     * so it'll be updated automatically
     * @param {string} api the tableName typically
     * @param {function(Message)} listener
     * @return {*}
     */
    registerMessageListener(api, listener){
        const self = this;
        if (this.messenger) { // initialization done
            return this.messenger.registerListeners(api, listener);
        } else {
            console.log("Waiting for participant initialization");
            setTimeout(() => { self.registerMessageListener.call(self, api, listener); },
                100);
        }
    }

    /**
     * See {@link MessageManager#deleteMessage}.
     */
    deleteMessage(message, callback){
        this.messenger.deleteMessage(message, callback);
    }

    /**
     * See {@link MessageManager#getMessages}.
     */
    getMessages(api, callback){
        this.messenger.getMessages(api, callback);
    }

    /**
     * Stops the message service listener
     */
    shutdownMessenger(){
        this.messenger.shutdown();
    }

    /**
     * giver the manager a reference to the controller so it can refresh the UI
     * @param {LocalizedController} controller
     */
    setController(controller){
        this.controller = controller;
    }

    /**
     * Retrieves the RootDSU syncronasly to the SSAPP, where all the other DSU's are mounted/referenced
     * @return {Archive}
     * @private
     * @throws error if the DSU is not cached
     */
    _getRootDSU(){
        if (!this.rootDSU)
            throw new Error("ParticipantDSU not cached");
        return this.rootDSU;
    };

    /**
     * Initializes the Base Manager
     * Also loads and caches the 'Public identity from the loader credentials'
     * @param {function(err)} callback
     * @private
     */
    _initialize(callback){
        if (this.rootDSU)
            return callback();
        let self = this;

        const enableDirectAccess = function(callback){
            self.DSUStorage.enableDirectAccess((err) => {
                self.rootDSU = self.DSUStorage;
                callback(err);
            });
        }

        const getIdentity = function(callback){
            self.getIdentity((err, identity) => err
                ? self._err(`Could not get Identity`, err, callback)
                : callback(err, identity));
        }

        if (!self.controller)
            return enableDirectAccess(err => err
                ? callback(err)
                : getIdentity((err, identity) => err
                    ? self._err(`Could not get Identity`, err, callback)
                    : self._cacheRelevant(callback, identity)));

        // For UI Responsiveness
        setTimeout(() => {
            enableDirectAccess(err => err
                ? callback(err)
                : setTimeout(() => {
                    getIdentity((err, identity) => err
                        ? self._err(`Could not get Identity`, err, callback)
                        : setTimeout(() => {
                            self._cacheRelevant(callback, identity);
                        }), 250);
                }), 100);
        }, 100);
    };

    /**
     * Veryfied that all the DSU's necessary to the SSAPP Architecture are available
     * @param {{}} mounts
     * @private
     */
    _verifyRelevantMounts(mounts){
        return this._cleanPath(DATABASE_MOUNT_PATH) in mounts && this._cleanPath(PARTICIPANT_MOUNT_PATH) in mounts;
    }

    /**
     * Util method to handle mount paths
     * @param {string} path
     * @return {string}
     * @private
     */
    _cleanPath(path){
        return path[0] === '/' ? path.substring(1) : path;
    }

    /**
     * Caches relevant objects to be able to provide synchronous access to other managers
     * @param {function(err, Participant)} callback
     * @param identity
     * @private
     */
    _cacheRelevant(callback, identity){
        let self = this;
        this.rootDSU.listMountedDSUs('/', (err, mounts) => {
            if (err)
                return self._err(`Could not list mounts in root DSU`, err, callback);
            const relevant = {};
            mounts.forEach(m => {
                if (relevantMounts.indexOf('/' + m.path) !== -1)
                    relevant[m.path] = m.identifier;
            });
            if (!self._verifyRelevantMounts(relevant))
                return callback(`Loader Initialization failed`);
            let dbSSI = getKeySSISpace().parse(relevant[self._cleanPath(DATABASE_MOUNT_PATH)]);
            if (!dbSSI)
                return callback(`Could not retrieve db ssi`);
            dbSSI = dbSSI.derive();

            const loadDB = function(callback){
                try{
                    self.db = require('opendsu').loadApi('db').getWalletDB(dbSSI, 'mydb');
                    self.db.on('initialised', () => {
                        console.log(`Database Cached`);
                        self.dbLock = new DBLock(self.db);
                        callback();
                    });
                } catch (e) {
                    return self._err(`Error Loading Database`, e, callback);
                }
            }

            const loadMessenger = function(callback){
                self.participantConstSSI = relevant[self._cleanPath(PARTICIPANT_MOUNT_PATH)];
                self._getDIDString(identity, self.participantConstSSI, (err, didString) => {
                    if (err)
                        return callback(err);
                    console.log(`DID String is ${didString}`);
                    getMessageManager(self, didString, (err, messageManager) => {
                        if (err)
                            return callback(err);
                        self.messenger = messageManager;
                        callback(undefined, self);
                    });
                });
            }

            if (!self.controller)
                return loadDB((err) => err
                    ? callback(err)
                    : loadMessenger(callback));

            // For UI Responsiveness
            setTimeout(() => {
                loadDB(err => err
                    ? callback(err)
                    : setTimeout(() => loadMessenger(callback), 20));
            }, 20);
        });
    }

    /**
     * @param {string|KeySSI} keySSI
     * @param {function(err, Archive)} callback
     * @private
     */
    _loadDSU(keySSI, callback){
        let self = this;
        if (typeof keySSI === 'string'){
            try {
                keySSI = self._getKeySSISpace().parse(keySSI);
            } catch (e) {
                return self._err(`Could not parse SSI ${keySSI}`, e, callback);
            }
            return self._loadDSU(keySSI, callback);
        }
        this._getResolver().loadDSU(keySSI, callback);
    };

    /**
     * reads the participant information (if exists)
     * @param {function(err, object)} [callback] only required if the identity is not cached
     * @returns {Participant} identity (if cached and no callback is provided)
     */
    getIdentity(callback){
        if (this.identity){
            if (callback)
                return callback(undefined, this.identity);
            return this.identity;
        }

        let self = this;
        self.DSUStorage.getObject(`${PARTICIPANT_MOUNT_PATH}${IDENTITY_MOUNT_PATH}${INFO_PATH}`, (err, participant) => {
            if (err)
                return self._err(`Could not get identity`, err, callback);
            self.identity = participant;
            callback(undefined, participant)
        });
    };

    /**
     * Must return the string to be used to generate the DID
     * @param {object} identity
     * @param {string} participantConstSSI
     * @param {function(err, string)}callback
     * @protected
     */
    _getDIDString(identity, participantConstSSI, callback){
        throw new Error(`Subclasses must implement this`);
    }

    /**
     * Edits/Overwrites the Participant details. Should this be allowed??
     * @param {Participant} participant
     * @param {function(err)} callback
     */
    editIdentity(participant, callback) {
        let self = this;
        this._initialize(err => {
            if (err)
                return self._err(`Could not initialize`, err, callback);
            self.DSUStorage.setObject(`${PARTICIPANT_MOUNT_PATH}${INFO_PATH}`, JSON.stringify(participant), (err) => {
                if (err)
                    return callback(err);
                console.log(`Participant updated`);
                this.identity = participant;
                callback(undefined, participant);
            });
        });
    };
}

module.exports = BaseManager;