Snippets

Matt Meisberger rxCacher

Created by Matt Meisberger
import { Observable } from 'rxjs/Observable';


// example usage
class MyService {
    @rxRemember(1000, CacheLevel.PERSIST, 'data-is-not-sensitive')
    getMeStuff(id: string, take: number, page: number) {
        // return this.http.put()
    }
}


enum CacheLevel { PERSIST = 'persisten', SESSION = 'session', MEMORY = 'memory' }

/**
 * Helper for observables that you want to cache results
 * Note: You should expect your function to return an observable that resolves once. Subsequent resolves are ignore
 */
export const rxRemember = (timeoutMs: number, level: CacheLevel = CacheLevel.PERSIST, resultsAreNotSensitive: 'data-is-not-sensitive'|false = false) => (target, key, descriptor: PropertyDescriptor) => {
    const engine = {
        [CacheLevel.MEMORY]: MemoryStorage,
        [CacheLevel.PERSIST]: localStorage,
        [CacheLevel.SESSION]: sessionStorage,
    }[level];

    if (level !== CacheLevel.MEMORY && resultsAreNotSensitive) {
        throw Error('You cannot store sensitive information in local/session storage');
    }

    const cache = makeTimedStore(toObjStore(engine));

    // save a reference to the original method this way we keep the values currently in the
    // descriptor and don't overwrite what another decorator might have done to the descriptor.
    // copy pasta from example
    if(descriptor === undefined) {
        descriptor = Object.getOwnPropertyDescriptor(target, key);
    }

    // copy original in case cache is missing
    const originalMethod = descriptor.value;

    // new wrapper
    descriptor.value = function (...args) {
        const cacheKey = JSON.stringify([key,args])
        const fromCache = cache.getItem(cacheKey, false);

        return fromCache
            ? Observable.of(fromCache).do(() => console.log('hit'))
            : originalMethod.bind(this)(...args)
                .do(() => console.log('miss:', cacheKey))
                .do(v => cache.setItem(cacheKey, v, timeoutMs))
                .take(1)
                .share();
    };

    // return edited descriptor as opposed to overwriting the descriptor
    return descriptor;
}

interface StorageDefault {
    readonly length: number;
    clear(): void;
    getItem(key: string, defaultVal: any): string | null | any;
    key(index: number): string | null;
    removeItem(key: string): void;
    setItem(key: string, value: string): void;
}

// in memory version of localStorage
const cache = {}
const MemoryStorage = {
    getItem: (key: string) => cache[key],
    setItem: (key: string, val: string) => cache[key] = `${val}`,
    removeItem: (key: string) => { delete cache[key]; },
    clear: () => Object.keys(cache).forEach(k => {
        delete cache[k];
    }),
    length: Object.keys(cache).length,
    key: () => ''
} as Storage

// storage but serialize / unserialize in flight
const toObjStore = (store: Storage) => ({
    ...store,
    getItem: <T>(key: string, defaultVal: any) => {
        if (!store.getItem(key)) return defaultVal;
        return JSON.parse(store.getItem(key)) as T;
    },
    setItem: (key: string, val: any) => console.log('setting', key) as any || store.setItem(key, JSON.stringify(val)),
    removeItem: store.removeItem,
    clear: store.clear,
}) as StorageDefault

// storage but store expires
const makeTimedStore = (store: Storage|StorageDefault) => {
    const getItem = (...args) => {
        // clear cache if expired
        const cacheExpiry = (store.getItem as any)(`${args[0]}:expiry`, 'not-found');
        if (cacheExpiry !== 'not-found' && Number(cacheExpiry) > new Date().getTime()) {
            console.log('clearing cache for time', new Date().getTime(), (store.getItem as any)(`${args[0]}:expiry`, false))
            store.removeItem(`${args[0]}:expiry`);
            store.removeItem(args[0]);
        }

        return (store.getItem as any)(...args);
    }
    return {
        ...store,
        getItem,
        setItem: (key: string, val: any, ms: number) => {
            store.setItem(key, val);
            store.setItem(`${key}:expiry`, `${new Date().getTime() + ms}`)
        },
    }
}

Comments (0)

HTTPS SSH

You can clone a snippet to your computer for local editing. Learn more.