Skip to content

Commit

Permalink
Merge pull request #14 from kinde-oss/feat/localstorage
Browse files Browse the repository at this point in the history
feat: added localStorage store
  • Loading branch information
DanielRivers authored Oct 24, 2024
2 parents e6ad37e + 8f0139b commit f9a3f00
Show file tree
Hide file tree
Showing 4 changed files with 250 additions and 3 deletions.
1 change: 1 addition & 0 deletions lib/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ describe("index exports", () => {
// session manager
"MemoryStorage",
"ChromeStore",
"LocalStorage",
"storageSettings",
"ExpoSecureStore",

Expand Down
7 changes: 4 additions & 3 deletions lib/sessionManager/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
143 changes: 143 additions & 0 deletions lib/sessionManager/stores/localStorage.test.ts
Original file line number Diff line number Diff line change
@@ -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<ExtraKeys>;

beforeEach(() => {
sessionManager = new LocalStorage<ExtraKeys>();
});

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();
});
});
102 changes: 102 additions & 0 deletions lib/sessionManager/stores/localStorage.ts
Original file line number Diff line number Diff line change
@@ -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<V = StorageKeys> implements SessionManager<V> {
constructor() {
console.warn("LocalStorage store should not be used in production");
}

setItems: Set<V | StorageKeys> = new Set<V>();

/**
* Clears all items from session store.
* @returns {void}
*/
async destroySession(): Promise<void> {
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<void> {
// 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<unknown | null> {
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<void> {
// 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);
}
}

0 comments on commit f9a3f00

Please sign in to comment.