Skip to content

Commit

Permalink
feat: add ignore and fallback storage options
Browse files Browse the repository at this point in the history
  • Loading branch information
gcascio committed May 10, 2022
1 parent 52fce07 commit 901af36
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 71 deletions.
3 changes: 3 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
"plugin:@typescript-eslint/recommended-requiring-type-checking",
"prettier/@typescript-eslint"
],
"rules": {
"import/prefer-default-export": "off"
},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "./tsconfig.json",
Expand Down
4 changes: 2 additions & 2 deletions __tests__/interface.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import TypedStorage from '../src/index';
const typedStorage = new TypedStorage<{}>();

describe('TypedLocalStorage', () => {
it('should expose a length method', () => {
expect(typedStorage.length).toBeInstanceOf(Function);
it('should expose a length getter', () => {
expect(Object.getOwnPropertyDescriptor(Object.getPrototypeOf(typedStorage), 'length')).toBeTruthy();
});

it('should expose a key method', () => {
Expand Down
59 changes: 56 additions & 3 deletions __tests__/storage.test.ts → __tests__/typedStorage.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import TypedStorage from '../src/index';
import TypedStorage, { MemoryStorage } from '../src/index';

/**
* Initialize test data
Expand Down Expand Up @@ -222,7 +222,7 @@ describe('TypedStorage set some values and check length', () => {
typedStorage.setItem('stringValue', testObject.stringValue)
typedStorage.setItem('objectValue', testObject.objectValue)

expect(typedStorage.length()).toEqual(2);
expect(typedStorage.length).toEqual(2);
});

it('should clear storage', () => {
Expand Down Expand Up @@ -269,12 +269,65 @@ describe('TypedStorage set and retrieve invalid json in safe retrieval mode', ()
});
});

describe('Initialize TypedStorage with localStorage missing on window', () => {
it('should use global localStorage if not present on window', () => {
const { localStorage, ...windowWithoutLocalStorage } = { ...window };
const globalSpy = jest.spyOn(global, 'localStorage', 'get');
const windowSpy = jest.spyOn(global, 'window', 'get');
windowSpy.mockImplementation(() => windowWithoutLocalStorage as Window & typeof globalThis);
globalSpy.mockImplementation(() => localStorage as Storage);

const storage = new TypedStorage<Schema>()

expect(() => storage.length).not.toThrow();
expect(() =>storage.key(0)).not.toThrow();
expect(() =>storage.getItem('stringValue')).not.toThrow();
expect(() =>storage.setItem('stringValue', '')).not.toThrow();
expect(() =>storage.removeItem('stringValue')).not.toThrow();
expect(() =>storage.clear()).not.toThrow();
});
});

describe('Initialize TypedStorage with Web Storage API missing', () => {
it('should throw exception if Web Storage API is not present', () => {
const { localStorage } = { ...window };
const globalSpy = jest.spyOn(global, 'localStorage', 'get');
const windowSpy = jest.spyOn(global, 'window', 'get');
windowSpy.mockImplementation(() => null as unknown as Window & typeof globalThis);
globalSpy.mockImplementation(() => null as unknown as Storage);

expect(() => new TypedStorage<Schema>()).toThrowError('Web Storage API not found');
});

it('should use fallback storage if Web Storage API is not present', () => {
const memoryStorage = new MemoryStorage()
const { localStorage, ...windowWithoutLocalStorage } = { ...window };
const globalSpy = jest.spyOn(global, 'localStorage', 'get');
const windowSpy = jest.spyOn(global, 'window', 'get');
windowSpy.mockImplementation(() => windowWithoutLocalStorage as Window & typeof globalThis);
globalSpy.mockImplementation(() => null as unknown as Storage);

expect(() => new TypedStorage<Schema>()).toThrowError('Web Storage API not found');
const storage = new TypedStorage<Schema>({ fallbackStorage: memoryStorage })

expect(storage).toBeTruthy();
});
});

describe('Ignore initialization errors', () => {
it('should ignore initialization error when Web Storage API is not present', () => {
const { localStorage, ...windowWithoutLocalStorage } = { ...window };
const globalSpy = jest.spyOn(global, 'localStorage', 'get');
const windowSpy = jest.spyOn(global, 'window', 'get');
windowSpy.mockImplementation(() => windowWithoutLocalStorage as Window & typeof globalThis);
globalSpy.mockImplementation(() => null as unknown as Storage);

const storage = new TypedStorage<Schema>({ ignoreMissingStorage: true })

expect(() => storage.length).not.toThrow();
expect(() =>storage.key(0)).not.toThrow();
expect(() =>storage.getItem('stringValue')).not.toThrow();
expect(() =>storage.setItem('stringValue', '')).not.toThrow();
expect(() =>storage.removeItem('stringValue')).not.toThrow();
expect(() =>storage.clear()).not.toThrow();
});
});
69 changes: 3 additions & 66 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,67 +1,4 @@
export type RetrievalMode = 'fail' | 'raw' | 'safe';
export * from './memoryStorage';
export * from './types';

export interface ITypedStorage<T> {
length(): number;
key<U extends keyof T>(index: number): U;
setItem<U extends keyof T>(key: U, value: T[U]): void;
getItem<U extends keyof T>(key: U): T[U] | null;
removeItem<U extends keyof T>(key: U, retrievalMode: RetrievalMode): void;
clear(): void;
}

export interface TypedStorageOptions {
storage: 'localStorage' | 'sessionStorage';
}

export default class TypedStorage<T> implements ITypedStorage<T> {
private readonly storage: Storage;

constructor(options: TypedStorageOptions = { storage: 'localStorage' }) {
this.storage = typeof window !== 'undefined' ? window[options.storage] : global[options.storage];

if (!this.storage) {
throw Error('Web Storage API not found.');
}
}

public length(): number {
return this.storage?.length;
}

public key<U extends keyof T>(index: number): U {
return this.storage?.key(index) as U;
}

public getItem<U extends keyof T>(key: U, retrievalMode: RetrievalMode = 'fail'): T[U] | null {
const item = this.storage?.getItem(key.toString());

if (item == null) {
return item;
}

try {
return JSON.parse(item) as T[U];
} catch (error) {
switch (retrievalMode) {
case 'safe':
return null;
case 'raw':
return (item as unknown) as T[U];
default:
throw error;
}
}
}

public setItem<U extends keyof T>(key: U, value: T[U]): void {
this.storage?.setItem(key.toString(), JSON.stringify(value));
}

public removeItem<U extends keyof T>(key: U): void {
this.storage?.removeItem(key.toString());
}

public clear(): void {
this.storage?.clear();
}
}
export { default } from './typedStorage';
58 changes: 58 additions & 0 deletions src/typedStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { RetrievalMode, ITypedStorage, TypedStorageOptions } from './types';

export default class TypedStorage<T> implements ITypedStorage<T> {
private readonly storage: Storage;

constructor({
storage = 'localStorage',
ignoreMissingStorage = false,
fallbackStorage = undefined,
}: TypedStorageOptions = {}) {
this.storage = window?.[storage] || global[storage] || fallbackStorage;

if (!this.storage && !ignoreMissingStorage) {
throw Error('Web Storage API not found.');
}
}

public get length(): number {
return this.storage?.length;
}

public key<U extends keyof T>(index: number): U {
return this.storage?.key(index) as U;
}

public getItem<U extends keyof T>(key: U, retrievalMode: RetrievalMode = 'fail'): T[U] | null {
const item = this.storage?.getItem(key.toString());

if (item == null) {
return item;
}

try {
return JSON.parse(item) as T[U];
} catch (error) {
switch (retrievalMode) {
case 'safe':
return null;
case 'raw':
return (item as unknown) as T[U];
default:
throw error;
}
}
}

public setItem<U extends keyof T>(key: U, value: T[U]): void {
this.storage?.setItem(key.toString(), JSON.stringify(value));
}

public removeItem<U extends keyof T>(key: U): void {
this.storage?.removeItem(key.toString());
}

public clear(): void {
this.storage?.clear();
}
}
16 changes: 16 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export type RetrievalMode = 'fail' | 'raw' | 'safe';

export interface ITypedStorage<T> {
length: number;
key<U extends keyof T>(index: number): U;
setItem<U extends keyof T>(key: U, value: T[U]): void;
getItem<U extends keyof T>(key: U): T[U] | null;
removeItem<U extends keyof T>(key: U, retrievalMode: RetrievalMode): void;
clear(): void;
}

export interface TypedStorageOptions {
storage?: 'localStorage' | 'sessionStorage';
fallbackStorage?: Storage;
ignoreMissingStorage?: boolean;
}

0 comments on commit 901af36

Please sign in to comment.