Skip to content

TypeScript:Example:Storage

LocalStorage와 SessionStorage를 적용할 수 있는 TypeScript 구현체 샘플.

index.ts

Vue플러그인으로 구현한다.

import VueInterface from 'vue';
import { PluginObject } from 'vue/types/plugin';
import PersistRecc from '@/persists/PersistRecc';
import PersistOptions from '@/persists/PersistOptions';


class PersistPlugin implements PluginObject<any> {
    install(Vue: typeof VueInterface, options?: PersistOptions): void {
        Vue.prototype.$persist = new PersistRecc(options);
    }
}

const VuePersist = new PersistPlugin()
export default VuePersist;

이걸 Vue 플러그인으로 적용할 때:

import VuePersist from "@/persists";
Vue.use(VuePersist)

MemoryStorage.ts

export default class MemoryStorage implements Storage {

    private _dict: Map<string, string>;

    constructor() {
        this._dict = new Map<string, string>();
    }

    get length(): number {
        return this._dict.size;
    }

    clear(): void {
        this._dict.clear();
    }

    getItem(key: string): string | null {
        const result = this._dict.get(key);
        if (result === undefined) {
            return null;
        }
        return result;
    }

    key(index: number): string | null {
        if (0 <= index && index < this._dict.size) {
            const keys = this._dict.keys();
            while (true) {
                if (index) {
                    keys.next();
                    --index;
                } else {
                    return keys.next().value;
                }
            }
        } else {
            return null;
        }
    }

    removeItem(key: string): void {
        this._dict.delete(key);
    }

    setItem(key: string, value: string): void {
        this._dict.set(key, value);
    }
}

PersistBase.ts

import MemoryStorage from '@/persists/MemoryStorage';
import PersistOptions from '@/persists/PersistOptions';

export interface ListenerData {
    key: string;
    oldValue: any;
    newValue: any;
    storage: Storage;
    url: string;
}

export type Listener = (ListenerData) => void;

export const STORAGE_TYPE_LOCAL = 'local';
export const STORAGE_TYPE_SESSION = 'session';
export const STORAGE_TYPE_MEMORY = 'memory';

export const DEFAULT_STORAGE_TYPE = STORAGE_TYPE_LOCAL;
export const DEFAULT_PREFIX = 'persist.';

export default class PersistBase {

    private readonly _storage: Storage;
    private readonly _prefix: string;
    private _listeners: Map<string, Set<Listener>>;

    constructor(options?: PersistOptions) {
        const type = (options && options.type) ? options.type : DEFAULT_STORAGE_TYPE;
        const prefix = (options && options.prefix) ? options.prefix : DEFAULT_PREFIX;

        if (type == STORAGE_TYPE_LOCAL) {
            this._storage = window.localStorage;
        } else if (type == STORAGE_TYPE_SESSION) {
            this._storage = window.sessionStorage;
        } else if (type == STORAGE_TYPE_MEMORY) {
            this._storage = new MemoryStorage();
        } else {
            throw new Error(`Unknown storage type: ${type}`);
        }

        this._prefix = prefix;
        this._listeners = new Map<string, Set<Listener>>();

        if (type == STORAGE_TYPE_LOCAL || type == STORAGE_TYPE_SESSION) {
            if (window) {
                if (window.addEventListener) {
                    window.addEventListener('storage', this.onStorage, false);
                } else {
                    window['onstorage'] = this.onStorage;
                }
            } else {
                console.warn('The `window` object does not exist.')
            }
        }
    }

    private onStorage(event: StorageEvent) {
        if (!event || !event.key) {
            return;
        }

        if (!event.key.startsWith(this._prefix)) {
            return;
        }

        const localKey = event.key.substr(this._prefix.length);
        const callbacks = this._listeners.get(localKey) as Set<Listener>;
        if (!callbacks || callbacks.size == 0) {
            return;
        }

        const _value = (data: string | null) => {
            if (!data) {
                return null;
            }
            try {
                return JSON.parse(data).value;
            } catch (error) {
                return null;
            }
        };

        callbacks.forEach((listener: Listener) => {
            let data = {} as ListenerData;
            data.key = localKey;
            data.oldValue = _value(event.oldValue);
            data.newValue = _value(event.newValue);
            data.storage = this._storage;
            data.url = event.url;
            listener(data);
        });
    }

    get length(): number {
        return this._storage.length;
    }

    size(): number {
        return this.length;
    }

    addListener(key: string, listener: Listener): void {
        if (!this._listeners.has(key)) {
            this._listeners.set(key, new Set<Listener>());
        }
        const callbacks = this._listeners.get(key) as Set<Listener>;
        callbacks.add(listener);
    }

    removeListener(key: string, listener: Listener): void {
        if (!this._listeners.has(key)) {
            return;
        }
        const callbacks = this._listeners.get(key) as Set<Listener>;
        callbacks.delete(listener);
    }

    set(key: string, value: any): void {
        const stringifyValue = JSON.stringify({
            value: value,
            datetime: new Date().getTime(),
        });
        this._storage.setItem(this._prefix + key, stringifyValue);
    }

    get(key: string, defaultValue: any = undefined): any {
        const value = this._storage.getItem(this._prefix + key);
        if (value === null) {
            return defaultValue;
        }

        try {
            const data = JSON.parse(value);
            return data.value;
        } catch (error) {
            return defaultValue;
        }
    }

    remove(key: string): void {
        this._storage.removeItem(this._prefix + key);
    }

    clear(): void {
        if (!this.length) {
            return;
        }
        for (const key of this.keys(true)) {
            this._storage.removeItem(key);
        }
    }

    keys(original = false): Array<string> {
        let result = new Array<string>();
        for (let i = 0; i < this.length; i++) {
            const key = this._storage.key(i);
            if (key !== null && key.startsWith(this._prefix)) {
                if (original) {
                    result.push(key);
                } else {
                    result.push(key.substr(this._prefix.length));
                }
            }
        }
        return result;
    }

    has(key: string): boolean {
        const realKey = this._prefix + key;
        for (let i = 0; i < this.length; i++) {
            if (realKey == this._storage.key(i)) {
                return true;
            }
        }
        return false;
    }
}

PersistOptions.ts

export default interface PersistOptions {
    type?: string;
    prefix?: string;
}

PersistRecc.ts

import PersistBase, { STORAGE_TYPE_LOCAL } from '@/persists/PersistBase';
import PersistOptions from '@/persists/PersistOptions';

export const DEFAULT_STORAGE_TYPE = STORAGE_TYPE_LOCAL;
export const DEFAULT_PREFIX = 'recc.';

const KEY_API_ORIGIN = "api.origin"

export default class PersistRecc extends PersistBase {

    constructor(options?: PersistOptions) {
        let o = {} as PersistOptions;
        o.type = (options && options.type) ? options.type : DEFAULT_STORAGE_TYPE;
        o.prefix = (options && options.prefix) ? options.prefix : DEFAULT_PREFIX;
        super(options);
    }

    get apiOrigin(): string {
        return super.get(KEY_API_ORIGIN, document.location.origin);
    }

    set apiOrigin(val: string) {
        super.set(KEY_API_ORIGIN, val);
    }
}

Jest Test Unit

MemoryStorage.spec.ts

import MemoryStorage from '@/persists/MemoryStorage';

describe('MemoryStorage', () => {
    let storage = new MemoryStorage();

    test('default', () => {
        const key0 = 'key0';
        const val0 = 'val0';

        const key1 = 'key1';
        const val1 = 'val1';

        expect(storage.length).toEqual(0);

        storage.setItem(key0, val0);
        expect(storage.length).toEqual(1);
        expect(storage.getItem(key0)).toEqual(val0);
        expect(storage.key(0)).toEqual(key0)

        storage.removeItem(key0);
        expect(storage.length).toEqual(0);

        storage.setItem(key1, val1);
        expect(storage.length).toEqual(1);

        storage.clear();
        expect(storage.length).toEqual(0);
    });

    test('errors', () => {
        const key0 = 'key0';
        const val0 = 'val0';
        const val1 = 'val1';

        storage.setItem(key0, val0);
        storage.setItem(key0, val1);

        expect(storage.length).toEqual(1);
        expect(storage.getItem(key0)).toEqual(val1);

        storage.removeItem(key0);
        expect(storage.length).toEqual(0);
        storage.removeItem(key0);
        expect(storage.length).toEqual(0);

        expect(storage.getItem(key0)).toBeNull();
        expect(storage.key(0)).toBeNull();
    });
});

PersistBase.spec.ts

import PersistBase, { ListenerData } from '@/persists/PersistBase';

describe.each(['memory', 'local', 'session'])('PersistBase', (type) => {
    let storage = new PersistBase({type: type, prefix: `test.${type}`});

    test(`${type}-default`, () => {
        const key0 = 'key0';
        const val0 = 'val0';

        const key1 = 'key1';
        const val1 = 'val1';

        expect(storage.length).toEqual(0);

        storage.set(key0, val0);
        expect(storage.length).toEqual(1);
        expect(storage.get(key0)).toEqual(val0);
        expect(storage.keys().length).toEqual(1)
        expect(storage.keys()[0]).toEqual(key0)

        storage.remove(key0);
        expect(storage.length).toEqual(0);

        storage.set(key1, val1);
        expect(storage.length).toEqual(1);

        storage.clear();
        expect(storage.length).toEqual(0);
    });
});

See also