jonocairns/angular-client-cache

View on GitHub
src/scripts/angular-client-cache.ts

Summary

Maintainability
A
1 hr
Test Coverage
/* tslint:disable:no-bitwise */
'use strict';

module ClientCache {
    export interface IClientCacheService {
        set(key: string, value: any): void;
        get<T>(key: string): T;
        tryGetSet<T>(key: string, apiCall: Function, objectBuilder?: Function): ng.IPromise<T>;
        configure(options: IStorageOptions): void;
        remove(key: string): void;
        removeAll(): void;
    }

    export interface IStorageOptions {
        storagePrefix?: string;
        useCompression?: boolean;
    }

    export class ClientCacheService implements IClientCacheService {

        private options: IStorageOptions;
        private sessionCache: ng.ICacheObject;
        constructor(private $q: ng.IQService, private $cacheFactory: ng.ICacheFactoryService) {
          this.options = {
              storagePrefix: 'client-cache',
              useCompression: false
          };
          this.sessionCache = $cacheFactory(this.options.storagePrefix);
        }

        public set(key: string, value: any): void {
            if (angular.isUndefined(key) || key === null) {
              throw new Error('Argument null exception. Parameter name: key. Function called: set');
            }
            if (angular.isUndefined(value) || value === null) {
              throw new Error('Argument null exception. Parameter name: value. Function called: set');
            }

            key = this.prefix(key);

            var stringValue = value;

            // Only stringify if json.
            if (angular.isObject(stringValue) || angular.isArray(stringValue) || angular.isNumber(+stringValue || stringValue)) {
                stringValue = angular.toJson(value);
            }

            this.sessionCache.put(key, stringValue);

            var shouldOverride = this.overrideCacheItem(key, stringValue);

            if (shouldOverride) {
                if (this.options.useCompression) {
                    stringValue = LZString.compressToUTF16(stringValue);
                }
                localStorage.setItem(key, stringValue);
            }
        }

        public get<T>(key: string): T {
            if (angular.isUndefined(key) || key === null) {
              throw new Error('Argument null exception. Parameter name: key. Function called: get');
            }

            key = this.prefix(key);
            var stringValue = this.sessionCache.get(key);

            if (!angular.isUndefined(stringValue) && stringValue !== null) {
                return this.parse(stringValue);
            } else {
                stringValue = localStorage.getItem(key);
            }

            if (angular.isUndefined(stringValue) || stringValue === null) {
                return undefined;
            }

            if (this.options.useCompression) {
              stringValue = LZString.decompressFromUTF16(stringValue);
            }

            return this.parse(stringValue);
        }

        public tryGetSet<T>(key: string, apiCall: Function, objectBuilder?: Function): ng.IPromise<T> {
            var deferred = this.$q.defer();
            key = this.prefix(key);
            var value = this.sessionCache.get(key);

            if(!angular.isUndefined(value) && value !== null) {
                this.set(key, value);
                value = this.parse(value);
                if(!angular.isUndefined(objectBuilder) && objectBuilder !== null) {
                  value = objectBuilder(value);
                }
                deferred.resolve(value);
                return deferred.promise;
            }
            /* istanbul ignore next */
            return apiCall().then((response: T) => {
                this.set(key, response);
                if(!angular.isUndefined(objectBuilder) && objectBuilder !== null) {
                  response = objectBuilder(response);
                }
                return response;
            }, () => {
                // fallback to localStorage if API call fails
                var value = this.get(key);
                if(!angular.isUndefined(objectBuilder) && objectBuilder !== null) {
                  value = objectBuilder(value);
                }
                return value;
            });
        }

        public configure(options: IStorageOptions) {
            angular.extend(this.options, options);
            if(angular.isDefined(options.storagePrefix) && options.storagePrefix !== null) {
                this.sessionCache.destroy();
                this.sessionCache = this.$cacheFactory(options.storagePrefix);
            }
        }

        public removeAll(): void {
            localStorage.clear();
            this.sessionCache.removeAll();
        }

        public remove(key: string): void {
            key = this.prefix(key);
            localStorage.removeItem(key);
            this.sessionCache.remove(key);
        }

        private parse(stringValue: string): any {
          var value;
          try {
              value = angular.fromJson(stringValue);
          } catch (e) {
              value = stringValue;
          }
          return value;
        }

        private overrideCacheItem(key: string, stringValue: string) {
            var itemExist = localStorage.getItem(this.prefix(key));
            if (angular.isUndefined(itemExist) || itemExist === null) {
              return true;
            }
            if (this.options.useCompression) {
                itemExist = LZString.decompressFromUTF16(itemExist);
            }
            var originalHash = this.hash(itemExist);
            var newHash = this.hash(stringValue);
            return originalHash !== newHash;
        }

        private prefix(key: string) {
            return this.options.storagePrefix + '.' + key;
        }

        private hash(value: string) {
            var hash = 0, i, chr, len;
            if (value.length === 0) return hash;
            for (i = 0, len = value.length; i < len; i++) {
                chr = value.charCodeAt(i);
                hash = ((hash << 5) - hash) + chr;
                hash |= 0;
            }
            return hash;
        }
    }

    function factory($q: ng.IQService, $cacheFactory: ng.ICacheFactoryService): IClientCacheService {
        return new ClientCacheService($q, $cacheFactory);
    }
    factory.$inject = [
        '$q',
        '$cacheFactory'
    ];

    angular.module('ClientCache', []);

    angular
        .module('ClientCache')
        .factory('ClientCacheService', factory);
}