From 70842599a937d63d1a49daf1ec1ce29195bb8f18 Mon Sep 17 00:00:00 2001 From: Jack Date: Fri, 13 Sep 2024 13:24:56 +0200 Subject: [PATCH] feat(dev): add deepclone util function (@miodec) (#5882) !nuf --- frontend/__tests__/test/misc.spec.ts | 53 ++++++++++++++++++- frontend/src/ts/constants/default-config.ts | 3 +- .../ts/constants/default-result-filters.ts | 3 +- .../src/ts/elements/account/result-filters.ts | 8 +-- frontend/src/ts/test/result.ts | 2 +- frontend/src/ts/test/test-logic.ts | 8 ++- frontend/src/ts/utils/misc.ts | 26 +++++++++ frontend/src/ts/utils/results.ts | 2 +- 8 files changed, 89 insertions(+), 16 deletions(-) diff --git a/frontend/__tests__/test/misc.spec.ts b/frontend/__tests__/test/misc.spec.ts index 9239d3f35..50938f05b 100644 --- a/frontend/__tests__/test/misc.spec.ts +++ b/frontend/__tests__/test/misc.spec.ts @@ -1,4 +1,4 @@ -import { isObject } from "../../src/ts/utils/misc"; +import { deepClone, isObject } from "../../src/ts/utils/misc"; import { getLanguageDisplayString, removeLanguageSize, @@ -118,4 +118,55 @@ describe("misc.ts", () => { }); }); }); + describe("deepClone", () => { + it("should correctly clone objects", () => { + const tests = [ + { + input: {}, + expected: {}, + }, + { + input: { a: 1 }, + expected: { a: 1 }, + }, + { + input: { a: { b: 2 } }, + expected: { a: { b: 2 } }, + }, + { + input: { a: { b: 2 }, c: [1, 2, 3] }, + expected: { a: { b: 2 }, c: [1, 2, 3] }, + }, + { + input: [], + expected: [], + }, + { + input: [1, 2, 3], + expected: [1, 2, 3], + }, + { + input: "string", + expected: "string", + }, + { + input: 1, + expected: 1, + }, + { + input: null, + expected: null, + }, + { + input: undefined, + expected: undefined, + }, + ]; + + tests.forEach((test) => { + const result = deepClone(test.input); + expect(result).toStrictEqual(test.expected); + }); + }); + }); }); diff --git a/frontend/src/ts/constants/default-config.ts b/frontend/src/ts/constants/default-config.ts index a43c0577a..893c19749 100644 --- a/frontend/src/ts/constants/default-config.ts +++ b/frontend/src/ts/constants/default-config.ts @@ -2,6 +2,7 @@ import { Config, CustomThemeColors, } from "@monkeytype/contracts/schemas/configs"; +import { deepClone } from "../utils/misc"; const obj = { theme: "serika_dark", @@ -102,4 +103,4 @@ const obj = { maxLineWidth: 0, } as Config; -export default JSON.parse(JSON.stringify(obj)) as Config; +export default deepClone(obj); diff --git a/frontend/src/ts/constants/default-result-filters.ts b/frontend/src/ts/constants/default-result-filters.ts index 071061d13..63560d5de 100644 --- a/frontend/src/ts/constants/default-result-filters.ts +++ b/frontend/src/ts/constants/default-result-filters.ts @@ -1,4 +1,5 @@ import { ResultFilters } from "@monkeytype/contracts/schemas/users"; +import { deepClone } from "../utils/misc"; const object: ResultFilters = { _id: "default", @@ -63,4 +64,4 @@ const object: ResultFilters = { }, }; -export default JSON.parse(JSON.stringify(object)) as ResultFilters; +export default deepClone(object); diff --git a/frontend/src/ts/elements/account/result-filters.ts b/frontend/src/ts/elements/account/result-filters.ts index c77797aef..14c651db1 100644 --- a/frontend/src/ts/elements/account/result-filters.ts +++ b/frontend/src/ts/elements/account/result-filters.ts @@ -166,16 +166,12 @@ export async function setFilterPreset(id: string): Promise { ).addClass("active"); } -function deepCopyFilter(filter: ResultFilters): ResultFilters { - return JSON.parse(JSON.stringify(filter)) as ResultFilters; -} - function addFilterPresetToSnapshot(filter: ResultFilters): void { const snapshot = DB.getSnapshot(); if (!snapshot) return; DB.setSnapshot({ ...snapshot, - filterPresets: [...snapshot.filterPresets, deepCopyFilter(filter)], + filterPresets: [...snapshot.filterPresets, Misc.deepClone(filter)], }); } @@ -963,7 +959,7 @@ $(".group.presetFilterButtons .filterBtns").on( ); function verifyResultFiltersStructure(filterIn: ResultFilters): ResultFilters { - const filter = deepCopyFilter(filterIn); + const filter = Misc.deepClone(filterIn); Object.entries(defaultResultFilters).forEach((entry) => { const key = entry[0] as ResultFiltersGroup; const value = entry[1]; diff --git a/frontend/src/ts/test/result.ts b/frontend/src/ts/test/result.ts index a28271dd5..0cddfe238 100644 --- a/frontend/src/ts/test/result.ts +++ b/frontend/src/ts/test/result.ts @@ -839,7 +839,7 @@ export async function update( dontSave: boolean ): Promise { resultAnnotation = []; - result = Object.assign({}, res); + result = Misc.deepClone(res); hideCrown(); $("#resultWordsHistory .words").empty(); $("#result #resultWordsHistory").addClass("hidden"); diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts index cd5be47cc..85d75070c 100644 --- a/frontend/src/ts/test/test-logic.ts +++ b/frontend/src/ts/test/test-logic.ts @@ -74,7 +74,7 @@ export function clearNotSignedInResult(): void { notSignedInLastResult = null; } -export function setNotSignedInUid(uid: string): void { +export function setNotSignedInUidAndHash(uid: string): void { if (notSignedInLastResult === null) return; notSignedInLastResult.uid = uid; //@ts-expect-error @@ -897,7 +897,7 @@ export async function finish(difficultyFailed = false): Promise { dontSave = true; } - const completedEvent = JSON.parse(JSON.stringify(ce)) as CompletedEvent; + const completedEvent = Misc.deepClone(ce) as CompletedEvent; ///////// completed event ready @@ -1054,9 +1054,7 @@ export async function finish(difficultyFailed = false): Promise { $("#result .stats .dailyLeaderboard").addClass("hidden"); - TestStats.setLastResult( - JSON.parse(JSON.stringify(completedEvent)) as CompletedEvent - ); + TestStats.setLastResult(Misc.deepClone(completedEvent)); if (!ConnectionState.get()) { ConnectionState.showOfflineBanner(); diff --git a/frontend/src/ts/utils/misc.ts b/frontend/src/ts/utils/misc.ts index 28e8828da..28ec93e2b 100644 --- a/frontend/src/ts/utils/misc.ts +++ b/frontend/src/ts/utils/misc.ts @@ -705,4 +705,30 @@ export function parseJsonWithSchema(input: string, schema: ZodSchema): T { } } +export function deepClone(obj: T[]): T[]; +export function deepClone(obj: T): T; +export function deepClone(obj: T): T; +export function deepClone(obj: T | T[]): T | T[] { + // Check if the value is a primitive (not an object or array) + if (obj === null || typeof obj !== "object") { + return obj; + } + + // Handle arrays + if (Array.isArray(obj)) { + return obj.map((item) => deepClone(item)); + } + + // Handle objects + const clonedObj = {} as { [K in keyof T]: T[K] }; + + for (const key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + clonedObj[key] = deepClone((obj as { [K in keyof T]: T[K] })[key]); + } + } + + return clonedObj; +} + // DO NOT ALTER GLOBAL OBJECTSONSTRUCTOR, IT WILL BREAK RESULT HASHES diff --git a/frontend/src/ts/utils/results.ts b/frontend/src/ts/utils/results.ts index 29b135383..ab04f2e58 100644 --- a/frontend/src/ts/utils/results.ts +++ b/frontend/src/ts/utils/results.ts @@ -6,7 +6,7 @@ import * as TestLogic from "../test/test-logic"; export async function syncNotSignedInLastResult(uid: string): Promise { const notSignedInLastResult = TestLogic.notSignedInLastResult; if (notSignedInLastResult === null) return; - TestLogic.setNotSignedInUid(uid); + TestLogic.setNotSignedInUidAndHash(uid); const response = await Ape.results.add({ body: { result: notSignedInLastResult },