2
\$\begingroup\$

I'm trying to build a reusable object cache for Angular. The goal is to:

  1. Cache objects by type
  2. Prevent collisions and conflicts
  3. Find objects by like property
  4. Ensure caches are separated by the logged in user

I am using Satellizer to determine if the user is logged in. Each object's service will act as a wrapper to this service and pass in its typeName property (probably set as a constant in the calling service).

(function () {

    'use strict';

    angular
        .module('blocks.object-cache');

    objectCache.$inject = ['CacheFactory', '$auth'];

    function objectCache(CacheFactory, $auth) {

        var _options = {
            maxAge : (60 * 60 * 1000),
            deleteOnExpire : 'passive',
            storageMode : 'localStorage'
        };

        var service = {
            setOptions          : setOptions,
            getCache            : getCache,

            clear               : clear,

            getAll              : getAll,
            getItem             : getItem,
            getItems            : getItems,

            putItem             : putItem,
            putItems            : putItems,

            getItemsByKey       : getItemsByKey,
            getItemByKeyFirst   : getItemByKeyFirst,
            getItemByKeySingle  : getItemByKeySingle,


            removeItemsByKey    : removeItemsByKey,
            removeItemByKey     : removeItemByKey,

            putItemsByKey       : putItemsByKey,
            putItemByKey        : putItemByKey
        };

        return service;
        ////////////////////////////////////////////////////////////////////////////////

        function setOptions (options) {
            options = options || {};
            options.maxAge = options.maxAge = _options.maxAge;
            options.deleteOnExpire = options.deleteOnExpire = _options.deleteOnExpire;
            options.storageMode = options.storageMode = _options.storageMode;
            _options = options;
        }
        function getCache(typeName) {
            var cacheName = [getUserId(), normalizeTypeName(typeName || 'objects')].join('_');
            var cache = CacheFactory(cacheName);
            if (cache) { return cache; }
            cache = CacheFactory(cacheName, _options);
            return cache;
        }

        function clear (typeName) {
            var cache = getCache(typeName);
            cache.removeAll();
            return (!cache.keys() || (cache.keys().length < 1));
        }

        function getAll (typeName) {
            var cache = getCache(typeName);
            var result = [];
            (cache.keys() || []).forEach(function(key){
                result.push(cache(key));
            });
            return result;
        }
        function getItem(typeName, id) {
            if (typeof id == 'undefined' || !id.trim) { return null; }
            var cache = getCache(typeName);
            return cache.get(id);
        }
        function getItems(typeName, ids) {
            var cache = getCache(typeName),
                result = [],
                _ids   = [];
            (ids || []).forEach(function(id){
                if (_ids.indexOf(id) < 0) {
                    _ids.push(id);
                    var item = cache.get(id);
                    if (item) { result.push(item); }
                }
            });
            return result;
        }

        function putItem(typeName, item, id, refresh) {
            if (typeof item == 'undefined') { return false; }
            if (typeof id == 'undefined' || !id.trim) { return false; }
            var cache = getCache(typeName);
            var existing = cache.get(id);
            if (existing && !refresh) { return true; }
            if (existing) { cache.remove(id); }
            cache.put(item, id);
            return (!!cache.get(id));
        }
        function putItems(typeName, items, idField, refresh) {
            var cache = getCache(typeName);
            (items || []).forEach(function(item){
                var id = item[idField];
                if (typeof id != 'undefined') {
                    var existing = cache.get(id);
                    if (existing && !!refresh) { cache.remove(id); }
                    if (!existing || !!refresh) { cache.put(item, id); }
                    if (!cache.get(id)) { return false; }
                }
            });
            return true;
        }

        function getItemsByKey(typeName, key, value, isCaseSensitive) {
            var result = [];
            (getAll(typeName) || []).forEach(function(item){
                var itemValue = item[key];
                if (typeof itemValue != 'undefined') {
                    if ((typeof value == 'string') && (typeof itemValue == 'string') && (!isCaseSensitive || value.toLowerCase() == itemValue.toLowerCase())) {
                        result.push(item);
                    } else if (((typeof value) == (typeof itemValue)) && (value == itemValue)) {
                        result.push(item);
                    } else {
                        // Other scenarios?
                    }
                }
            });
            return result;
        }
        function getItemByKeyFirst(typeName, key, value, isCaseSensitive) {
            var items = getItemsByKey(typeName, key, value, isCaseSensitive) || [];
            return (items.length > 0) ? items[0] : null;
        }
        function getItemByKeySingle(typeName, key, value, isCaseSensitive) {
            var items = getItemsByKey(typeName, key, value, isCaseSensitive) || [];
            return (items.length === 0) ? items[0] : null;
        }

        function removeItemsByKey (typeName, keyField, values, isCaseSensitive) {
            var cache = getCache(typeName),
                keysToRemove = [];
            (cache.keys() || []).forEach(function(key){
                var item        = cache.get[key],
                    itemValue   = item[keyField];
                if (typeof itemValue != 'undefined') {
                    for (var v = 0; v < (values || []).length; v += 1) {
                        if ((typeof values[v] == 'string') && (typeof itemValue == 'string') && (!isCaseSensitive || values[v].toLowerCase() == itemValue.toLowerCase())) {
                            keysToRemove.push(key);
                            break;
                        } else if (((typeof values[v]) == (typeof itemValue)) && (values[v] == itemValue)) {
                            keysToRemove.push(key);
                            break;
                        } else {
                            // Other scenarios?
                        }
                    }
                }
            });
            var success = true;
            keysToRemove.forEach(function(key){
                cache.remove(key);
                if (cache.get(key)) { success = false; }
            });
            return success;
        }
        function removeItemByKey (typeName, keyField, value, isCaseSensitive) {
            return removeItemsByKey(typeName, keyField, [value], isCaseSensitive);
        }

        function putItemsByKey(typeName, items, keyField, refresh, isCaseSensitive) {
            if (!!refresh) {
                var values  = _.map((items || []), keyField);
                if (!removeItemsByKey(typeName, keyField, values, isCaseSensitive)) { return false; }
            }
            var cache = getCache(typeName);
            (items || []).forEach(function(item){
                var id = item[keyField];
                if (typeof value != 'undefined') { cache.put(item, id); }
                if (!cache.get(id)) { return false; }
            });
            return true;
        }
        function putItemByKey(typeName, item, keyField, refresh, isCaseSensitive) {
            return putItemsByKey(typeName, [item], keyField, refresh, isCaseSensitive);
        }

        function getUserId () {
            return $auth.isAuthenticated() ? ($auth.getPayload().sub || 'unknown') : 'public';
        }
        function normalizeTypeName (typeName) {
            return typeName.split('.').join('-');
        }
    }

})();
\$\endgroup\$

2 Answers 2

1
\$\begingroup\$

Found your question and link on Stack Exchange. You were asking to move to a Factory pattern. I'm pasting my answer here for consistency.

Edit #1: Factory-like pattern.
Of course, you could use an actual constructor and factory. However, my first pass was the reorientate the syntax to accomplish the goal of passing in the variables... which is why the JSON object syntax was continued. I'd probably change this to a more formal structure using functions.

Edit #2: Remove the putBy* methods.
The putBy* methods seem to conflict the logic. You seem to be trying to prevent duplicate items by the ID of the object. If you are ensuring uniqueness by the ID then the specified key only makes sense for finding or removing the objects. That is why I've removed them here.

Edit #3: Passing in ID and type name.
In Stack Overflow you wanted to pass in the type name to your factory. After playing with the code, it seems to make sense to also pass in the ID field.

(function () {

    'use strict';

    angular
        .module('blocks.object-cache')
        .service('ObjectCache', ObjectCache);

    ObjectCache.$inject = ['CacheFactory', '$auth'];

    function ObjectCache(CacheFactory, $auth) {

        var _options = {
            maxAge : (60 * 60 * 1000),
            deleteOnExpire : 'passive',
            storageMode : 'localStorage'
        };

        var factory = {
            getCache  : getCache
        };

        return factory;
        ////////////////////////////////////////////////////////////////////////////////

        function getCache(typeName, options, idField) {

            options = options || {};
            options.maxAge = options.maxAge = _options.maxAge;
            options.deleteOnExpire = options.deleteOnExpire = _options.deleteOnExpire;
            options.storageMode = options.storageMode = _options.storageMode;

            typeName    = normalizeTypeName(typeName || 'objects');
            var userId  = getUserId() || 'public';
            var name    = userId + '_' + typeName;

            var service = {
                type    : typeName,
                user    : userId,
                name    : name,
                options : options,
                idField : idField || 'id',
                cache   : CacheFactory(name) || CacheFactory.createCache(name, options),

                clear   : function () {
                    this.cache.removeAll();
                },

                getAll  : function () {
                    var result = [];
                    var keys = this.cache.keys() || [];
                    for (var i = 0; i < keys.length; i += 1) {
                        result.push(this.cache(keys[i]));
                    }
                    return result;
                },
                getItems : function (ids) {
                    var result = [],
                        _ids   = [];
                    for (var i = 0; i < (ids || []).length; i += 1) {
                        var id = ids[i];
                        if (_ids.indexOf(id) < 0) {
                            _ids.push(id);
                            var item = this.cache.get(id);
                            if (item) { result.push(item); }
                        }
                    }
                    return result;
                },
                getItem  : function (id) {
                    var items = this.getItems([id]);
                    return (items.length > 0) ? items[0] : null;
                },

                putItem  : function (item, refresh) {
                    if (!item) { return false; }
                    var id = item[this.idField];
                    if (typeof id == 'undefined') { return false; }
                    var existing = this.cache.get(id);
                    if (existing && !refresh) { return true; }
                    if (existing) { this.cache.remove(id); }
                    this.cache.put(item, id);
                    return (!!this.cache.get(id));
                },
                putItems : function (items, refresh) {
                    var success = true;
                    for (var i = 0; i < (items || []).length; i += 1) {
                        if (!this.putItem(item, refresh)) { success = false; }
                    }
                    return success;
                },

                getItemsByKey : function (key, value, isCaseSensitive) {
                    return (this.getAll() || []).filter(function(item){
                        var itemValue = item[key];
                        return ((value === itemValue) ||
                                (typeof value == 'string' && 
                                 typeof itemValue == 'string' && 
                                 !isCaseSensitive && 
                                 value.toLowerCase() == itemValue.toLowerCase()));
                    });
                },
                getItemByKeyFirst : function (key, value, isCaseSensitive) {
                    var items = this.getItemsByKey(key, value, isCaseSensitive) || [];
                    return (items.length > 0) ? items[0] : null;
                },
                getItemByKeySingle : function (key, value, isCaseSensitive) {
                    var items = this.getItemsByKey(key, value, isCaseSensitive) || [];
                    return (items.length === 0) ? items[0] : null;
                },
                removeItemsByKey : function (keyField, values, isCaseSensitive) {
                    var keysToRemove = [],
                        keys = this.cache.keys() || [];
                    for (var k = 0; k < keys.length; k += 1) {
                        var key       = keys[k];
                        var item      = this.cache.get(key);
                        var itemValue = item[keyField];
                        for (var v = 0; v < (values || []).length; v += 1) {
                            var value = values[v];
                            if ((value === itemValue) ||
                                (typeof value == 'string' && 
                                 typeof itemValue == 'string' && 
                                 !isCaseSensitive && 
                                 value.toLowerCase() == itemValue.toLowerCase())) {
                                keysToRemove.push(key);
                                break;
                            }
                        }
                    }
                    var success = true;
                    for (var r = 0; r < keysToRemove.length; r += 1) {
                        this.cache.remove(keysToRemove[r]);
                        if (this.cache.get(keysToRemove[r])) { success = false; }
                    }
                    return success;
                },
                removeItemByKey : function (keyField, value, isCaseSensitive) {
                    return this.removeItemsByKey(keyField, [value], isCaseSensitive);
                }
            };

            return service;
        }


        function getUserId () {
            return $auth.isAuthenticated() ? ($auth.getPayload().sub || 'unknown') : null;
        }
        function normalizeTypeName (typeName) {
            return typeName.split('.').join('-');
        }
    }

})();
\$\endgroup\$
0
\$\begingroup\$

Your code is good, but there's a few things you should fix:


whitespace

storageMode : 'localStorage'

You have an extraneous whitespace after the keys in your objects. In your service object, you have a weird indentation style, I would recommend not indenting it that much, or just not all

> ////////////////////////////////////////////////////

Not only is that ugly as hell, it's fundamentally pointless. If you're declaring config items or whatever, do it in another file, or built inline into the page.

In getCache, you define a variable, only to return it immediately after.
Just return it directly.


ES6

If you want to code to ES6, you can use default parameters in the parameter block, like so:

function setItems(options = {}){

You can also use the let keyword to make scope-levelled variables


// Other scenarios? / comments

You should probably consider those. And this belongs in an issue tracker, not in your code.

As a general rule of thumb, it's best to write your code to explain itself, helpful (but not oversaturating) comments would be good though.


var

Try to declare your variables each with the var keyword on their own line. Otherwise it's one comma away from floating to become a global.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.