diff --git a/frontend/__tests__/utils/local-storage-with-schema.spec.ts b/frontend/__tests__/utils/local-storage-with-schema.spec.ts index fa74cf0407d9..d722e579e1c5 100644 --- a/frontend/__tests__/utils/local-storage-with-schema.spec.ts +++ b/frontend/__tests__/utils/local-storage-with-schema.spec.ts @@ -15,7 +15,7 @@ describe("local-storage-with-schema.ts", () => { fontSize: 16, }; - const ls = new LocalStorageWithSchema({ + let ls = new LocalStorageWithSchema({ key: "config", schema: objectSchema, fallback: defaultObject, @@ -37,144 +37,204 @@ describe("local-storage-with-schema.ts", () => { removeItemMock.mockReset(); }); - it("should save to localStorage if schema is correct and return true", () => { - const res = ls.set(defaultObject); - - expect(localStorage.setItem).toHaveBeenCalledWith( - "config", - JSON.stringify(defaultObject) - ); - expect(res).toBe(true); + beforeEach(() => { + ls = new LocalStorageWithSchema({ + key: "config", + schema: objectSchema, + fallback: defaultObject, + }); }); - it("should fail to save to localStorage if schema is incorrect and return false", () => { - const obj = { - hi: "hello", - }; + describe("set", () => { + it("should save to localStorage if schema is correct and return true", () => { + const res = ls.set(defaultObject); - const res = ls.set(obj as any); + expect(localStorage.setItem).toHaveBeenCalledWith( + "config", + JSON.stringify(defaultObject) + ); + expect(res).toBe(true); + }); - expect(localStorage.setItem).not.toHaveBeenCalled(); - expect(res).toBe(false); - }); + it("should fail to save to localStorage if schema is incorrect and return false", () => { + const obj = { + hi: "hello", + }; - it("should revert to the fallback value if localstorage is null", () => { - getItemMock.mockReturnValue(null); + const res = ls.set(obj as any); - const res = ls.get(); + expect(localStorage.setItem).not.toHaveBeenCalled(); + expect(res).toBe(false); + }); - expect(localStorage.getItem).toHaveBeenCalledWith("config"); - expect(localStorage.setItem).not.toHaveBeenCalled(); - expect(res).toEqual(defaultObject); - }); + it("should update cache on set", () => { + ls.set(defaultObject); - it("should revert to the fallback value if localstorage json is malformed", () => { - getItemMock.mockReturnValue("badjson"); + expect(ls.get()).toStrictEqual(defaultObject); - const res = ls.get(); + const update = { ...defaultObject, fontSize: 5 }; + ls.set(update); - expect(localStorage.getItem).toHaveBeenCalledWith("config"); - expect(localStorage.setItem).toHaveBeenCalledWith( - "config", - JSON.stringify(defaultObject) - ); - expect(res).toEqual(defaultObject); - }); + expect(ls.get()).toStrictEqual(update); - it("should get from localStorage", () => { - getItemMock.mockReturnValue(JSON.stringify(defaultObject)); + expect(getItemMock).not.toHaveBeenCalled(); + }); - const res = ls.get(); + it("should get last valid value if schema is incorrect", () => { + ls.set(defaultObject); - expect(localStorage.getItem).toHaveBeenCalledWith("config"); - expect(localStorage.setItem).not.toHaveBeenCalled(); - expect(res).toEqual(defaultObject); - }); + ls.set({ hi: "hello" } as any); - it("should revert to fallback value if no migrate function and schema failed", () => { - getItemMock.mockReturnValue(JSON.stringify({ hi: "hello" })); - const ls = new LocalStorageWithSchema({ - key: "config", - schema: objectSchema, - fallback: defaultObject, + expect(ls.get()).toEqual(defaultObject); + + expect(setItemMock).toHaveBeenCalledOnce(); + expect(getItemMock).not.toHaveBeenCalled(); }); + }); - const res = ls.get(); + describe("get", () => { + it("should revert to the fallback value if localstorage is null", () => { + getItemMock.mockReturnValue(null); - expect(localStorage.getItem).toHaveBeenCalledWith("config"); - expect(localStorage.setItem).toHaveBeenCalledWith( - "config", - JSON.stringify(defaultObject) - ); - expect(res).toEqual(defaultObject); - }); + const res = ls.get(); + + expect(getItemMock).toHaveBeenCalledWith("config"); + expect(setItemMock).not.toHaveBeenCalled(); + expect(res).toEqual(defaultObject); - it("should migrate (when function is provided) if schema failed", () => { - const existingValue = { hi: "hello" }; + //cache used + expect(ls.get()).toEqual(res); + expect(getItemMock).toHaveBeenCalledOnce(); + }); - getItemMock.mockReturnValue(JSON.stringify(existingValue)); + it("should revert to the fallback value if localstorage json is malformed", () => { + getItemMock.mockReturnValue("badjson"); - const migrated = { - punctuation: false, - mode: "time", - fontSize: 1, - }; + const res = ls.get(); - const migrateFnMock = vi.fn(() => migrated as any); + expect(getItemMock).toHaveBeenCalledWith("config"); + expect(setItemMock).toHaveBeenCalledWith( + "config", + JSON.stringify(defaultObject) + ); + expect(res).toEqual(defaultObject); - const ls = new LocalStorageWithSchema({ - key: "config", - schema: objectSchema, - fallback: defaultObject, - migrate: migrateFnMock, + //cache used + expect(ls.get()).toEqual(defaultObject); + expect(getItemMock).toHaveBeenCalledOnce(); }); - const res = ls.get(); - - expect(localStorage.getItem).toHaveBeenCalledWith("config"); - expect(migrateFnMock).toHaveBeenCalledWith( - existingValue, - expect.any(Array) - ); - expect(localStorage.setItem).toHaveBeenCalledWith( - "config", - JSON.stringify(migrated) - ); - expect(res).toEqual(migrated); - }); + it("should get from localStorage", () => { + getItemMock.mockReturnValue(JSON.stringify(defaultObject)); - it("should revert to fallback if migration ran but schema still failed", () => { - const existingValue = { hi: "hello" }; + const res = ls.get(); - getItemMock.mockReturnValue(JSON.stringify(existingValue)); + expect(getItemMock).toHaveBeenCalledWith("config"); + expect(setItemMock).not.toHaveBeenCalled(); + expect(res).toEqual(defaultObject); - const invalidMigrated = { - punctuation: 1, - mode: "time", - fontSize: 1, - }; + //cache used + expect(ls.get()).toEqual(res); + expect(getItemMock).toHaveBeenCalledOnce(); + }); - const migrateFnMock = vi.fn(() => invalidMigrated as any); + it("should revert to fallback value if no migrate function and schema failed", () => { + getItemMock.mockReturnValue(JSON.stringify({ hi: "hello" })); + const ls = new LocalStorageWithSchema({ + key: "config", + schema: objectSchema, + fallback: defaultObject, + }); + + const res = ls.get(); + + expect(getItemMock).toHaveBeenCalledWith("config"); + expect(setItemMock).toHaveBeenCalledWith( + "config", + JSON.stringify(defaultObject) + ); + expect(res).toEqual(defaultObject); + + //cache used + expect(ls.get()).toEqual(defaultObject); + expect(getItemMock).toHaveBeenCalledOnce(); + }); - const ls = new LocalStorageWithSchema({ - key: "config", - schema: objectSchema, - fallback: defaultObject, - migrate: migrateFnMock, + it("should migrate (when function is provided) if schema failed", () => { + const existingValue = { hi: "hello" }; + + getItemMock.mockReturnValue(JSON.stringify(existingValue)); + + const migrated = { + punctuation: false, + mode: "time", + fontSize: 1, + }; + + const migrateFnMock = vi.fn(() => migrated as any); + + const ls = new LocalStorageWithSchema({ + key: "config", + schema: objectSchema, + fallback: defaultObject, + migrate: migrateFnMock, + }); + + const res = ls.get(); + + expect(getItemMock).toHaveBeenCalledWith("config"); + expect(migrateFnMock).toHaveBeenCalledWith( + existingValue, + expect.any(Array) + ); + expect(setItemMock).toHaveBeenCalledWith( + "config", + JSON.stringify(migrated) + ); + expect(res).toEqual(migrated); + + //cache used + expect(ls.get()).toEqual(migrated); + expect(getItemMock).toHaveBeenCalledOnce(); }); - const res = ls.get(); - - expect(localStorage.getItem).toHaveBeenCalledWith("config"); - expect(migrateFnMock).toHaveBeenCalledWith( - existingValue, - expect.any(Array) - ); - expect(localStorage.setItem).toHaveBeenCalledWith( - "config", - JSON.stringify(defaultObject) - ); - expect(res).toEqual(defaultObject); + it("should revert to fallback if migration ran but schema still failed", () => { + const existingValue = { hi: "hello" }; + + getItemMock.mockReturnValue(JSON.stringify(existingValue)); + + const invalidMigrated = { + punctuation: 1, + mode: "time", + fontSize: 1, + }; + + const migrateFnMock = vi.fn(() => invalidMigrated as any); + + const ls = new LocalStorageWithSchema({ + key: "config", + schema: objectSchema, + fallback: defaultObject, + migrate: migrateFnMock, + }); + + const res = ls.get(); + + expect(getItemMock).toHaveBeenCalledWith("config"); + expect(migrateFnMock).toHaveBeenCalledWith( + existingValue, + expect.any(Array) + ); + expect(setItemMock).toHaveBeenCalledWith( + "config", + JSON.stringify(defaultObject) + ); + expect(res).toEqual(defaultObject); + + //cache used + expect(ls.get()).toEqual(defaultObject); + expect(getItemMock).toHaveBeenCalledOnce(); + }); }); }); }); diff --git a/frontend/src/ts/utils/local-storage-with-schema.ts b/frontend/src/ts/utils/local-storage-with-schema.ts index 7d2477334cb0..64637f182973 100644 --- a/frontend/src/ts/utils/local-storage-with-schema.ts +++ b/frontend/src/ts/utils/local-storage-with-schema.ts @@ -12,6 +12,7 @@ export class LocalStorageWithSchema { value: Record | unknown[], zodIssues?: ZodIssue[] ) => T; + private cache?: T; constructor(options: { key: string; @@ -29,13 +30,18 @@ export class LocalStorageWithSchema { } public get(): T { - console.debug(`LS ${this.key} Getting value from localStorage`); + if (this.cache !== undefined) { + console.debug(`LS ${this.key} Got cached value:`, this.cache); + return this.cache; + } + console.debug(`LS ${this.key} Getting value from localStorage`); const value = window.localStorage.getItem(this.key); if (value === null) { console.debug(`LS ${this.key} No value found, returning fallback`); - return this.fallback; + this.cache = this.fallback; + return this.cache; } let migrated = false; @@ -49,12 +55,14 @@ export class LocalStorageWithSchema { console.debug( `LS ${this.key} Migrating from old format to new format` ); - return this.migrate(oldData, zodIssues); + this.cache = this.migrate(oldData, zodIssues); + return this.cache; } else { console.debug( `LS ${this.key} No migration function provided, returning fallback` ); - return this.fallback; + this.cache = this.fallback; + return this.cache; } }, }) @@ -65,7 +73,8 @@ export class LocalStorageWithSchema { `LS ${this.key} Failed to parse from localStorage: ${error.message}` ); window.localStorage.setItem(this.key, JSON.stringify(this.fallback)); - return this.fallback; + this.cache = this.fallback; + return this.cache; } if (migrated || parsed === this.fallback) { @@ -74,7 +83,8 @@ export class LocalStorageWithSchema { } console.debug(`LS ${this.key} Got value:`, parsed); - return parsed; + this.cache = parsed; + return this.cache; } public set(data: T): boolean { @@ -83,6 +93,7 @@ export class LocalStorageWithSchema { const parsed = this.schema.parse(data); console.debug(`LS ${this.key} Setting in localStorage`); window.localStorage.setItem(this.key, JSON.stringify(parsed)); + this.cache = parsed; return true; } catch (e) { let message = "Unknown error occurred";