diff --git a/lib/main.test.ts b/lib/main.test.ts index 79d75ea..3dfd696 100644 --- a/lib/main.test.ts +++ b/lib/main.test.ts @@ -43,6 +43,7 @@ describe("index exports", () => { // session manager "MemoryStorage", "ChromeStore", + "LocalStorage", "storageSettings", "ExpoSecureStore", diff --git a/lib/sessionManager/index.ts b/lib/sessionManager/index.ts index 4af815b..90c7384 100644 --- a/lib/sessionManager/index.ts +++ b/lib/sessionManager/index.ts @@ -13,7 +13,8 @@ export const storageSettings: StorageSettingsType = { maxLength: 2000, }; -export { MemoryStorage } from "./stores/memory.js"; -export { ChromeStore } from "./stores/chromeStore.js"; -export { ExpoSecureStore } from "./stores/expoSecureStore.js"; +export { MemoryStorage } from "./stores/memory.ts"; +export { ChromeStore } from "./stores/chromeStore.ts"; +export { ExpoSecureStore } from "./stores/expoSecureStore.ts"; +export { LocalStorage } from "./stores/localStorage.ts"; export * from "./types.ts"; diff --git a/lib/sessionManager/stores/localStorage.test.ts b/lib/sessionManager/stores/localStorage.test.ts new file mode 100644 index 0000000..c7d91c8 --- /dev/null +++ b/lib/sessionManager/stores/localStorage.test.ts @@ -0,0 +1,143 @@ +import { describe, it, expect, beforeEach, vi } from "vitest"; +import { LocalStorage } from "./localStorage"; +import { StorageKeys } from "../types"; + +enum ExtraKeys { + testKey = "testKey2", +} + +const localStorageMock = (function () { + let store: { [key: string]: string } = {}; + + return { + getItem(key: string) { + return store[key] || null; + }, + setItem(key: string, value: string) { + store[key] = String(value); + }, + removeItem(key: string) { + delete store[key]; + }, + clear() { + store = {}; + }, + }; +})(); +vi.stubGlobal("localStorage", localStorageMock); + +describe("LocalStorage standard keys", () => { + let sessionManager: LocalStorage; + + beforeEach(() => { + sessionManager = new LocalStorage(); + }); + + it("should set and get an item in session storage", async () => { + console.log("here"); + await sessionManager.setSessionItem(StorageKeys.accessToken, "testValue"); + expect(await sessionManager.getSessionItem(StorageKeys.accessToken)).toBe( + "testValue", + ); + }); + + it("should remove an item from session storage", async () => { + await sessionManager.setSessionItem(StorageKeys.accessToken, "testValue"); + expect(await sessionManager.getSessionItem(StorageKeys.accessToken)).toBe( + "testValue", + ); + + await sessionManager.removeSessionItem(StorageKeys.accessToken); + expect( + await sessionManager.getSessionItem(StorageKeys.accessToken), + ).toBeNull(); + }); + + it("should clear all items from session storage", async () => { + await sessionManager.setSessionItem(StorageKeys.accessToken, "testValue"); + expect(await sessionManager.getSessionItem(StorageKeys.accessToken)).toBe( + "testValue", + ); + sessionManager.destroySession(); + expect( + await sessionManager.getSessionItem(StorageKeys.accessToken), + ).toBeNull(); + }); + + it("should clear all items from session storage", async () => { + await sessionManager.setSessionItem(StorageKeys.accessToken, true); + expect(await sessionManager.getSessionItem(StorageKeys.accessToken)).toBe( + "true", + ); + sessionManager.destroySession(); + expect( + await sessionManager.getSessionItem(StorageKeys.accessToken), + ).toBeNull(); + }); +}); + +describe("LocalStorage keys: storageKeys", () => { + let sessionManager: LocalStorage; + + beforeEach(() => { + sessionManager = new LocalStorage(); + }); + + it("should set and get an item in storage: StorageKeys", async () => { + await sessionManager.setSessionItem(StorageKeys.accessToken, "testValue"); + expect(await sessionManager.getSessionItem(StorageKeys.accessToken)).toBe( + "testValue", + ); + }); + + it("should remove an item from storage: StorageKeys", async () => { + await sessionManager.setSessionItem(StorageKeys.accessToken, "testValue"); + expect(await sessionManager.getSessionItem(StorageKeys.accessToken)).toBe( + "testValue", + ); + + await sessionManager.removeSessionItem(StorageKeys.accessToken); + expect( + await sessionManager.getSessionItem(StorageKeys.accessToken), + ).toBeNull(); + }); + + it("should clear all items from storage: StorageKeys", async () => { + await sessionManager.setSessionItem(StorageKeys.accessToken, "testValue"); + expect(await sessionManager.getSessionItem(StorageKeys.accessToken)).toBe( + "testValue", + ); + + sessionManager.destroySession(); + expect( + await sessionManager.getSessionItem(StorageKeys.accessToken), + ).toBeNull(); + }); + + it("should set and get an item in extra storage", async () => { + await sessionManager.setSessionItem(ExtraKeys.testKey, "testValue"); + expect(await sessionManager.getSessionItem(ExtraKeys.testKey)).toBe( + "testValue", + ); + }); + + it("should remove an item from extra storage", async () => { + await sessionManager.setSessionItem(ExtraKeys.testKey, "testValue"); + expect(await sessionManager.getSessionItem(ExtraKeys.testKey)).toBe( + "testValue", + ); + + sessionManager.removeSessionItem(ExtraKeys.testKey); + expect(await sessionManager.getSessionItem(ExtraKeys.testKey)).toBeNull(); + }); + + it("should clear all items from extra storage", async () => { + await sessionManager.setSessionItem(ExtraKeys.testKey, "testValue"); + expect(await sessionManager.getSessionItem(ExtraKeys.testKey)).toBe( + "testValue", + ); + + sessionManager.destroySession(); + expect(await sessionManager.getSessionItem(ExtraKeys.testKey)).toBeNull(); + }); +}); diff --git a/lib/sessionManager/stores/localStorage.ts b/lib/sessionManager/stores/localStorage.ts new file mode 100644 index 0000000..3b93fae --- /dev/null +++ b/lib/sessionManager/stores/localStorage.ts @@ -0,0 +1,102 @@ +import { storageSettings } from "../index.js"; +import { StorageKeys, type SessionManager } from "../types.js"; +import { splitString } from "../utils.js"; + +/** + * Provides a localStorage based session manager implementation for the browser. + * @class LocalStorage + */ +export class LocalStorage implements SessionManager { + constructor() { + console.warn("LocalStorage store should not be used in production"); + } + + setItems: Set = new Set(); + + /** + * Clears all items from session store. + * @returns {void} + */ + async destroySession(): Promise { + this.setItems.forEach((key) => { + this.removeSessionItem(key); + }); + } + + /** + * Sets the provided key-value store to the localStorage cache. + * @param {V} itemKey + * @param {unknown} itemValue + * @returns {void} + */ + async setSessionItem( + itemKey: V | StorageKeys, + itemValue: unknown, + ): Promise { + // clear items first + await this.removeSessionItem(itemKey); + this.setItems.add(itemKey); + + if (typeof itemValue === "string") { + splitString(itemValue, storageSettings.maxLength).forEach( + (splitValue, index) => { + localStorage.setItem( + `${storageSettings.keyPrefix}${itemKey}${index}`, + splitValue, + ); + }, + ); + return; + } + localStorage.setItem( + `${storageSettings.keyPrefix}${itemKey}0`, + itemValue as string, + ); + } + + /** + * Gets the item for the provided key from the localStorage cache. + * @param {string} itemKey + * @returns {unknown | null} + */ + async getSessionItem(itemKey: V | StorageKeys): Promise { + if ( + localStorage.getItem(`${storageSettings.keyPrefix}${itemKey}0`) === null + ) { + return null; + } + + let itemValue = ""; + let index = 0; + let key = `${storageSettings.keyPrefix}${String(itemKey)}${index}`; + while (localStorage.getItem(key) !== null) { + itemValue += localStorage.getItem(key); + index++; + key = `${storageSettings.keyPrefix}${String(itemKey)}${index}`; + } + + return itemValue; + } + + /** + * Removes the item for the provided key from the localStorage cache. + * @param {V} itemKey + * @returns {void} + */ + async removeSessionItem(itemKey: V | StorageKeys): Promise { + // Remove all items with the key prefix + let index = 0; + while ( + localStorage.getItem( + `${storageSettings.keyPrefix}${String(itemKey)}${index}`, + ) !== null + ) { + localStorage.removeItem( + `${storageSettings.keyPrefix}${String(itemKey)}${index}`, + ); + + index++; + } + this.setItems.delete(itemKey); + } +}