Skip to content

Commit 3b0cf16

Browse files
authored
perf: use cache in local-storage-with-schema (@fehmer) (monkeytypegame#6596)
1 parent b26e1d2 commit 3b0cf16

File tree

2 files changed

+185
-114
lines changed

2 files changed

+185
-114
lines changed

frontend/__tests__/utils/local-storage-with-schema.spec.ts

Lines changed: 168 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ describe("local-storage-with-schema.ts", () => {
1515
fontSize: 16,
1616
};
1717

18-
const ls = new LocalStorageWithSchema({
18+
let ls = new LocalStorageWithSchema({
1919
key: "config",
2020
schema: objectSchema,
2121
fallback: defaultObject,
@@ -37,144 +37,204 @@ describe("local-storage-with-schema.ts", () => {
3737
removeItemMock.mockReset();
3838
});
3939

40-
it("should save to localStorage if schema is correct and return true", () => {
41-
const res = ls.set(defaultObject);
42-
43-
expect(localStorage.setItem).toHaveBeenCalledWith(
44-
"config",
45-
JSON.stringify(defaultObject)
46-
);
47-
expect(res).toBe(true);
40+
beforeEach(() => {
41+
ls = new LocalStorageWithSchema({
42+
key: "config",
43+
schema: objectSchema,
44+
fallback: defaultObject,
45+
});
4846
});
4947

50-
it("should fail to save to localStorage if schema is incorrect and return false", () => {
51-
const obj = {
52-
hi: "hello",
53-
};
48+
describe("set", () => {
49+
it("should save to localStorage if schema is correct and return true", () => {
50+
const res = ls.set(defaultObject);
5451

55-
const res = ls.set(obj as any);
52+
expect(localStorage.setItem).toHaveBeenCalledWith(
53+
"config",
54+
JSON.stringify(defaultObject)
55+
);
56+
expect(res).toBe(true);
57+
});
5658

57-
expect(localStorage.setItem).not.toHaveBeenCalled();
58-
expect(res).toBe(false);
59-
});
59+
it("should fail to save to localStorage if schema is incorrect and return false", () => {
60+
const obj = {
61+
hi: "hello",
62+
};
6063

61-
it("should revert to the fallback value if localstorage is null", () => {
62-
getItemMock.mockReturnValue(null);
64+
const res = ls.set(obj as any);
6365

64-
const res = ls.get();
66+
expect(localStorage.setItem).not.toHaveBeenCalled();
67+
expect(res).toBe(false);
68+
});
6569

66-
expect(localStorage.getItem).toHaveBeenCalledWith("config");
67-
expect(localStorage.setItem).not.toHaveBeenCalled();
68-
expect(res).toEqual(defaultObject);
69-
});
70+
it("should update cache on set", () => {
71+
ls.set(defaultObject);
7072

71-
it("should revert to the fallback value if localstorage json is malformed", () => {
72-
getItemMock.mockReturnValue("badjson");
73+
expect(ls.get()).toStrictEqual(defaultObject);
7374

74-
const res = ls.get();
75+
const update = { ...defaultObject, fontSize: 5 };
76+
ls.set(update);
7577

76-
expect(localStorage.getItem).toHaveBeenCalledWith("config");
77-
expect(localStorage.setItem).toHaveBeenCalledWith(
78-
"config",
79-
JSON.stringify(defaultObject)
80-
);
81-
expect(res).toEqual(defaultObject);
82-
});
78+
expect(ls.get()).toStrictEqual(update);
8379

84-
it("should get from localStorage", () => {
85-
getItemMock.mockReturnValue(JSON.stringify(defaultObject));
80+
expect(getItemMock).not.toHaveBeenCalled();
81+
});
8682

87-
const res = ls.get();
83+
it("should get last valid value if schema is incorrect", () => {
84+
ls.set(defaultObject);
8885

89-
expect(localStorage.getItem).toHaveBeenCalledWith("config");
90-
expect(localStorage.setItem).not.toHaveBeenCalled();
91-
expect(res).toEqual(defaultObject);
92-
});
86+
ls.set({ hi: "hello" } as any);
9387

94-
it("should revert to fallback value if no migrate function and schema failed", () => {
95-
getItemMock.mockReturnValue(JSON.stringify({ hi: "hello" }));
96-
const ls = new LocalStorageWithSchema({
97-
key: "config",
98-
schema: objectSchema,
99-
fallback: defaultObject,
88+
expect(ls.get()).toEqual(defaultObject);
89+
90+
expect(setItemMock).toHaveBeenCalledOnce();
91+
expect(getItemMock).not.toHaveBeenCalled();
10092
});
93+
});
10194

102-
const res = ls.get();
95+
describe("get", () => {
96+
it("should revert to the fallback value if localstorage is null", () => {
97+
getItemMock.mockReturnValue(null);
10398

104-
expect(localStorage.getItem).toHaveBeenCalledWith("config");
105-
expect(localStorage.setItem).toHaveBeenCalledWith(
106-
"config",
107-
JSON.stringify(defaultObject)
108-
);
109-
expect(res).toEqual(defaultObject);
110-
});
99+
const res = ls.get();
100+
101+
expect(getItemMock).toHaveBeenCalledWith("config");
102+
expect(setItemMock).not.toHaveBeenCalled();
103+
expect(res).toEqual(defaultObject);
111104

112-
it("should migrate (when function is provided) if schema failed", () => {
113-
const existingValue = { hi: "hello" };
105+
//cache used
106+
expect(ls.get()).toEqual(res);
107+
expect(getItemMock).toHaveBeenCalledOnce();
108+
});
114109

115-
getItemMock.mockReturnValue(JSON.stringify(existingValue));
110+
it("should revert to the fallback value if localstorage json is malformed", () => {
111+
getItemMock.mockReturnValue("badjson");
116112

117-
const migrated = {
118-
punctuation: false,
119-
mode: "time",
120-
fontSize: 1,
121-
};
113+
const res = ls.get();
122114

123-
const migrateFnMock = vi.fn(() => migrated as any);
115+
expect(getItemMock).toHaveBeenCalledWith("config");
116+
expect(setItemMock).toHaveBeenCalledWith(
117+
"config",
118+
JSON.stringify(defaultObject)
119+
);
120+
expect(res).toEqual(defaultObject);
124121

125-
const ls = new LocalStorageWithSchema({
126-
key: "config",
127-
schema: objectSchema,
128-
fallback: defaultObject,
129-
migrate: migrateFnMock,
122+
//cache used
123+
expect(ls.get()).toEqual(defaultObject);
124+
expect(getItemMock).toHaveBeenCalledOnce();
130125
});
131126

132-
const res = ls.get();
133-
134-
expect(localStorage.getItem).toHaveBeenCalledWith("config");
135-
expect(migrateFnMock).toHaveBeenCalledWith(
136-
existingValue,
137-
expect.any(Array)
138-
);
139-
expect(localStorage.setItem).toHaveBeenCalledWith(
140-
"config",
141-
JSON.stringify(migrated)
142-
);
143-
expect(res).toEqual(migrated);
144-
});
127+
it("should get from localStorage", () => {
128+
getItemMock.mockReturnValue(JSON.stringify(defaultObject));
145129

146-
it("should revert to fallback if migration ran but schema still failed", () => {
147-
const existingValue = { hi: "hello" };
130+
const res = ls.get();
148131

149-
getItemMock.mockReturnValue(JSON.stringify(existingValue));
132+
expect(getItemMock).toHaveBeenCalledWith("config");
133+
expect(setItemMock).not.toHaveBeenCalled();
134+
expect(res).toEqual(defaultObject);
150135

151-
const invalidMigrated = {
152-
punctuation: 1,
153-
mode: "time",
154-
fontSize: 1,
155-
};
136+
//cache used
137+
expect(ls.get()).toEqual(res);
138+
expect(getItemMock).toHaveBeenCalledOnce();
139+
});
156140

157-
const migrateFnMock = vi.fn(() => invalidMigrated as any);
141+
it("should revert to fallback value if no migrate function and schema failed", () => {
142+
getItemMock.mockReturnValue(JSON.stringify({ hi: "hello" }));
143+
const ls = new LocalStorageWithSchema({
144+
key: "config",
145+
schema: objectSchema,
146+
fallback: defaultObject,
147+
});
148+
149+
const res = ls.get();
150+
151+
expect(getItemMock).toHaveBeenCalledWith("config");
152+
expect(setItemMock).toHaveBeenCalledWith(
153+
"config",
154+
JSON.stringify(defaultObject)
155+
);
156+
expect(res).toEqual(defaultObject);
157+
158+
//cache used
159+
expect(ls.get()).toEqual(defaultObject);
160+
expect(getItemMock).toHaveBeenCalledOnce();
161+
});
158162

159-
const ls = new LocalStorageWithSchema({
160-
key: "config",
161-
schema: objectSchema,
162-
fallback: defaultObject,
163-
migrate: migrateFnMock,
163+
it("should migrate (when function is provided) if schema failed", () => {
164+
const existingValue = { hi: "hello" };
165+
166+
getItemMock.mockReturnValue(JSON.stringify(existingValue));
167+
168+
const migrated = {
169+
punctuation: false,
170+
mode: "time",
171+
fontSize: 1,
172+
};
173+
174+
const migrateFnMock = vi.fn(() => migrated as any);
175+
176+
const ls = new LocalStorageWithSchema({
177+
key: "config",
178+
schema: objectSchema,
179+
fallback: defaultObject,
180+
migrate: migrateFnMock,
181+
});
182+
183+
const res = ls.get();
184+
185+
expect(getItemMock).toHaveBeenCalledWith("config");
186+
expect(migrateFnMock).toHaveBeenCalledWith(
187+
existingValue,
188+
expect.any(Array)
189+
);
190+
expect(setItemMock).toHaveBeenCalledWith(
191+
"config",
192+
JSON.stringify(migrated)
193+
);
194+
expect(res).toEqual(migrated);
195+
196+
//cache used
197+
expect(ls.get()).toEqual(migrated);
198+
expect(getItemMock).toHaveBeenCalledOnce();
164199
});
165200

166-
const res = ls.get();
167-
168-
expect(localStorage.getItem).toHaveBeenCalledWith("config");
169-
expect(migrateFnMock).toHaveBeenCalledWith(
170-
existingValue,
171-
expect.any(Array)
172-
);
173-
expect(localStorage.setItem).toHaveBeenCalledWith(
174-
"config",
175-
JSON.stringify(defaultObject)
176-
);
177-
expect(res).toEqual(defaultObject);
201+
it("should revert to fallback if migration ran but schema still failed", () => {
202+
const existingValue = { hi: "hello" };
203+
204+
getItemMock.mockReturnValue(JSON.stringify(existingValue));
205+
206+
const invalidMigrated = {
207+
punctuation: 1,
208+
mode: "time",
209+
fontSize: 1,
210+
};
211+
212+
const migrateFnMock = vi.fn(() => invalidMigrated as any);
213+
214+
const ls = new LocalStorageWithSchema({
215+
key: "config",
216+
schema: objectSchema,
217+
fallback: defaultObject,
218+
migrate: migrateFnMock,
219+
});
220+
221+
const res = ls.get();
222+
223+
expect(getItemMock).toHaveBeenCalledWith("config");
224+
expect(migrateFnMock).toHaveBeenCalledWith(
225+
existingValue,
226+
expect.any(Array)
227+
);
228+
expect(setItemMock).toHaveBeenCalledWith(
229+
"config",
230+
JSON.stringify(defaultObject)
231+
);
232+
expect(res).toEqual(defaultObject);
233+
234+
//cache used
235+
expect(ls.get()).toEqual(defaultObject);
236+
expect(getItemMock).toHaveBeenCalledOnce();
237+
});
178238
});
179239
});
180240
});

frontend/src/ts/utils/local-storage-with-schema.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export class LocalStorageWithSchema<T> {
1212
value: Record<string, unknown> | unknown[],
1313
zodIssues?: ZodIssue[]
1414
) => T;
15+
private cache?: T;
1516

1617
constructor(options: {
1718
key: string;
@@ -29,13 +30,18 @@ export class LocalStorageWithSchema<T> {
2930
}
3031

3132
public get(): T {
32-
console.debug(`LS ${this.key} Getting value from localStorage`);
33+
if (this.cache !== undefined) {
34+
console.debug(`LS ${this.key} Got cached value:`, this.cache);
35+
return this.cache;
36+
}
3337

38+
console.debug(`LS ${this.key} Getting value from localStorage`);
3439
const value = window.localStorage.getItem(this.key);
3540

3641
if (value === null) {
3742
console.debug(`LS ${this.key} No value found, returning fallback`);
38-
return this.fallback;
43+
this.cache = this.fallback;
44+
return this.cache;
3945
}
4046

4147
let migrated = false;
@@ -49,12 +55,14 @@ export class LocalStorageWithSchema<T> {
4955
console.debug(
5056
`LS ${this.key} Migrating from old format to new format`
5157
);
52-
return this.migrate(oldData, zodIssues);
58+
this.cache = this.migrate(oldData, zodIssues);
59+
return this.cache;
5360
} else {
5461
console.debug(
5562
`LS ${this.key} No migration function provided, returning fallback`
5663
);
57-
return this.fallback;
64+
this.cache = this.fallback;
65+
return this.cache;
5866
}
5967
},
6068
})
@@ -65,7 +73,8 @@ export class LocalStorageWithSchema<T> {
6573
`LS ${this.key} Failed to parse from localStorage: ${error.message}`
6674
);
6775
window.localStorage.setItem(this.key, JSON.stringify(this.fallback));
68-
return this.fallback;
76+
this.cache = this.fallback;
77+
return this.cache;
6978
}
7079

7180
if (migrated || parsed === this.fallback) {
@@ -74,7 +83,8 @@ export class LocalStorageWithSchema<T> {
7483
}
7584

7685
console.debug(`LS ${this.key} Got value:`, parsed);
77-
return parsed;
86+
this.cache = parsed;
87+
return this.cache;
7888
}
7989

8090
public set(data: T): boolean {
@@ -83,6 +93,7 @@ export class LocalStorageWithSchema<T> {
8393
const parsed = this.schema.parse(data);
8494
console.debug(`LS ${this.key} Setting in localStorage`);
8595
window.localStorage.setItem(this.key, JSON.stringify(parsed));
96+
this.cache = parsed;
8697
return true;
8798
} catch (e) {
8899
let message = "Unknown error occurred";

0 commit comments

Comments
 (0)