diff --git a/.eslintrc b/.eslintrc index 2352498..8c22a17 100644 --- a/.eslintrc +++ b/.eslintrc @@ -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", diff --git a/__tests__/interface.test.ts b/__tests__/interface.test.ts index ee97357..0299e0b 100644 --- a/__tests__/interface.test.ts +++ b/__tests__/interface.test.ts @@ -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', () => { diff --git a/__tests__/storage.test.ts b/__tests__/typedStorage.test.ts similarity index 76% rename from __tests__/storage.test.ts rename to __tests__/typedStorage.test.ts index 3255b4c..cffa70d 100644 --- a/__tests__/storage.test.ts +++ b/__tests__/typedStorage.test.ts @@ -1,4 +1,4 @@ -import TypedStorage from '../src/index'; +import TypedStorage, { MemoryStorage } from '../src/index'; /** * Initialize test data @@ -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', () => { @@ -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() + + 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()).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()).toThrowError('Web Storage API not found'); + const storage = new TypedStorage({ 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({ 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(); }); }); diff --git a/src/index.ts b/src/index.ts index 17b5975..6f91873 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,67 +1,4 @@ -export type RetrievalMode = 'fail' | 'raw' | 'safe'; +export * from './memoryStorage'; +export * from './types'; -export interface ITypedStorage { - length(): number; - key(index: number): U; - setItem(key: U, value: T[U]): void; - getItem(key: U): T[U] | null; - removeItem(key: U, retrievalMode: RetrievalMode): void; - clear(): void; -} - -export interface TypedStorageOptions { - storage: 'localStorage' | 'sessionStorage'; -} - -export default class TypedStorage implements ITypedStorage { - 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(index: number): U { - return this.storage?.key(index) as U; - } - - public getItem(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(key: U, value: T[U]): void { - this.storage?.setItem(key.toString(), JSON.stringify(value)); - } - - public removeItem(key: U): void { - this.storage?.removeItem(key.toString()); - } - - public clear(): void { - this.storage?.clear(); - } -} +export { default } from './typedStorage'; diff --git a/src/typedStorage.ts b/src/typedStorage.ts new file mode 100644 index 0000000..56ba301 --- /dev/null +++ b/src/typedStorage.ts @@ -0,0 +1,58 @@ +import { RetrievalMode, ITypedStorage, TypedStorageOptions } from './types'; + +export default class TypedStorage implements ITypedStorage { + 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(index: number): U { + return this.storage?.key(index) as U; + } + + public getItem(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(key: U, value: T[U]): void { + this.storage?.setItem(key.toString(), JSON.stringify(value)); + } + + public removeItem(key: U): void { + this.storage?.removeItem(key.toString()); + } + + public clear(): void { + this.storage?.clear(); + } +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..f58f1a6 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,16 @@ +export type RetrievalMode = 'fail' | 'raw' | 'safe'; + +export interface ITypedStorage { + length: number; + key(index: number): U; + setItem(key: U, value: T[U]): void; + getItem(key: U): T[U] | null; + removeItem(key: U, retrievalMode: RetrievalMode): void; + clear(): void; +} + +export interface TypedStorageOptions { + storage?: 'localStorage' | 'sessionStorage'; + fallbackStorage?: Storage; + ignoreMissingStorage?: boolean; +}