betajs/betajs-scoped

View on GitHub
src/main/namespace.js

Summary

Maintainability
F
3 days
Test Coverage
function newNamespace (opts/* : {tree ?: boolean, global ?: boolean, root ?: Object} */) {

    var options/* : {
        tree: boolean,
        global: boolean,
        root: Object
    } */ = {
        tree: typeof opts.tree === "boolean" ? opts.tree : false,
        global: typeof opts.global === "boolean" ? opts.global : false,
        root: typeof opts.root === "object" ? opts.root : {}
    };

    /*::
    type Node = {
        route: ?string,
        parent: ?Node,
        children: any,
        watchers: any,
        data: any,
        ready: boolean,
        lazy: any
    };
    */

    function initNode(options)/* : Node */ {
        return {
            route: typeof options.route === "string" ? options.route : null,
            parent: typeof options.parent === "object" ? options.parent : null,
            ready: typeof options.ready === "boolean" ? options.ready : false,
            children: {},
            watchers: [],
            data: {},
            lazy: []
        };
    }
    
    var nsRoot = initNode({ready: true});
    
    if (options.tree) {
        if (options.global) {
            try {
                if (window)
                    nsRoot.data = window;
            } catch (e) { }
            try {
                if (global)
                    nsRoot.data = global;
            } catch (e) { }
            try {
                if (self)
                    nsRoot.data = self;
            } catch (e) { }
        } else
            nsRoot.data = options.root;
    }
    
    function nodeDigest(node/* : Node */) {
        if (node.ready)
            return;
        if (node.parent && !node.parent.ready) {
            nodeDigest(node.parent);
            return;
        }
        if (node.route && node.parent && (node.route in node.parent.data)) {
            node.data = node.parent.data[node.route];
            node.ready = true;
            for (var i = 0; i < node.watchers.length; ++i)
                node.watchers[i].callback.call(node.watchers[i].context || this, node.data);
            node.watchers = [];
            for (var key in node.children)
                nodeDigest(node.children[key]);
        }
    }
    
    function nodeEnforce(node/* : Node */) {
        if (node.ready)
            return;
        if (node.parent && !node.parent.ready)
            nodeEnforce(node.parent);
        node.ready = true;
        if (node.parent) {
            if (options.tree && typeof node.parent.data == "object")
                node.parent.data[node.route] = node.data;
        }
        for (var i = 0; i < node.watchers.length; ++i)
            node.watchers[i].callback.call(node.watchers[i].context || this, node.data);
        node.watchers = [];
    }
    
    function nodeSetData(node/* : Node */, value) {
        if (typeof value == "object" && node.ready) {
            for (var key in value)
                node.data[key] = value[key];
        } else
            node.data = value;
        if (typeof value == "object") {
            for (var ckey in value) {
                if (node.children[ckey])
                    node.children[ckey].data = value[ckey];
            }
        }
        nodeEnforce(node);
        for (var k in node.children)
            nodeDigest(node.children[k]);
    }
    
    function nodeClearData(node/* : Node */) {
        if (node.ready && node.data) {
            for (var key in node.data)
                delete node.data[key];
        }
    }
    
    function nodeNavigate(path/* : ?String */) {
        if (!path)
            return nsRoot;
        var routes = path.split(".");
        var current = nsRoot;
        for (var i = 0; i < routes.length; ++i) {
            if (routes[i] in current.children)
                current = current.children[routes[i]];
            else {
                current.children[routes[i]] = initNode({
                    parent: current,
                    route: routes[i]
                });
                current = current.children[routes[i]];
                nodeDigest(current);
            }
        }
        return current;
    }
    
    function nodeAddWatcher(node/* : Node */, callback, context) {
        if (node.ready)
            callback.call(context || this, node.data);
        else {
            node.watchers.push({
                callback: callback,
                context: context
            });
            if (node.lazy.length > 0) {
                var f = function (node) {
                    if (node.lazy.length > 0) {
                        var lazy = node.lazy.shift();
                        lazy.callback.call(lazy.context || this, node.data);
                        f(node);
                    }
                };
                f(node);
            }
        }
    }
    
    function nodeUnresolvedWatchers(node/* : Node */, base, result) {
        node = node || nsRoot;
        result = result || [];
        if (!node.ready && node.lazy.length === 0 && node.watchers.length > 0)
            result.push(base);
        for (var k in node.children) {
            var c = node.children[k];
            var r = (base ? base + "." : "") + c.route;
            result = nodeUnresolvedWatchers(c, r, result);
        }
        return result;
    }

    /** 
     * The namespace module manages a namespace in the Scoped system.
     * 
     * @module Namespace
     * @access public
     */
    return {
        
        /**
         * Extend a node in the namespace by an object.
         * 
         * @param {string} path path to the node in the namespace
         * @param {object} value object that should be used for extend the namespace node
         */
        extend: function (path, value) {
            nodeSetData(nodeNavigate(path), value);
        },
        
        /**
         * Set the object value of a node in the namespace.
         * 
         * @param {string} path path to the node in the namespace
         * @param {object} value object that should be used as value for the namespace node
         */
        set: function (path, value) {
            var node = nodeNavigate(path);
            if (node.data)
                nodeClearData(node);
            nodeSetData(node, value);
        },
        
        /**
         * Read the object value of a node in the namespace.
         * 
         * @param {string} path path to the node in the namespace
         * @return {object} object value of the node or null if undefined
         */
        get: function (path) {
            var node = nodeNavigate(path);
            return node.ready ? node.data : null;
        },
        
        /**
         * Lazily navigate to a node in the namespace.
         * Will asynchronously call the callback as soon as the node is being touched.
         *
         * @param {string} path path to the node in the namespace
         * @param {function} callback callback function accepting the node's object value
         * @param {context} context optional callback context
         */
        lazy: function (path, callback, context) {
            var node = nodeNavigate(path);
            if (node.ready)
                callback(context || this, node.data);
            else {
                node.lazy.push({
                    callback: callback,
                    context: context
                });
            }
        },
        
        /**
         * Digest a node path, checking whether it has been defined by an external system.
         * 
         * @param {string} path path to the node in the namespace
         */
        digest: function (path) {
            nodeDigest(nodeNavigate(path));
        },
        
        /**
         * Asynchronously access a node in the namespace.
         * Will asynchronously call the callback as soon as the node is being defined.
         *
         * @param {string} path path to the node in the namespace
         * @param {function} callback callback function accepting the node's object value
         * @param {context} context optional callback context
         */
        obtain: function (path, callback, context) {
            nodeAddWatcher(nodeNavigate(path), callback, context);
        },
        
        /**
         * Returns all unresolved watchers under a certain path.
         * 
         * @param {string} path path to the node in the namespace
         * @return {array} list of all unresolved watchers 
         */
        unresolvedWatchers: function (path) {
            return nodeUnresolvedWatchers(nodeNavigate(path), path);
        },
        
        __export: function () {
            return {
                options: options,
                nsRoot: nsRoot
            };
        },
        
        __import: function (data) {
            options = data.options;
            nsRoot = data.nsRoot;
        }
        
    };
    
}