From 3446716997adebf480171ca52052db5312bafc1d Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Fri, 6 Jun 2025 14:38:21 +0200 Subject: [PATCH 1/4] impr: store custom text in indexedDB (@fehmer) --- frontend/package.json | 1 + frontend/src/ts/modals/save-custom-text.ts | 24 ++- frontend/src/ts/modals/saved-texts.ts | 22 +- frontend/src/ts/modals/simple-modals.ts | 14 +- frontend/src/ts/test/custom-text.ts | 225 +++++++++++---------- frontend/src/ts/test/test-logic.ts | 11 +- pnpm-lock.yaml | 150 +++----------- 7 files changed, 193 insertions(+), 254 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 1057f5e9a67f..82437935b82b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -99,6 +99,7 @@ "hangul-js": "0.2.6", "howler": "2.2.3", "html2canvas": "1.4.1", + "idb": "8.0.3", "jquery": "3.7.1", "jquery-color": "2.2.0", "jquery.easing": "1.4.1", diff --git a/frontend/src/ts/modals/save-custom-text.ts b/frontend/src/ts/modals/save-custom-text.ts index d63e77f946f0..678b4cdbfc22 100644 --- a/frontend/src/ts/modals/save-custom-text.ts +++ b/frontend/src/ts/modals/save-custom-text.ts @@ -32,7 +32,7 @@ function hide(): void { void modal.hide(); } -function save(): boolean { +async function save(): Promise { const name = $("#saveCustomTextModal .textName").val() as string; const checkbox = $("#saveCustomTextModal .isLongText").prop( "checked" @@ -48,7 +48,11 @@ function save(): boolean { return false; } - const saved = CustomText.setCustomText(name, state.textToSave, checkbox); + const saved = await CustomText.setCustomText( + name, + state.textToSave, + checkbox + ); if (saved) { CustomTextState.setCustomTextName(name, checkbox); Notifications.add("Custom text saved", 1); @@ -59,7 +63,7 @@ function save(): boolean { } } -function updateIndicatorAndButton(): void { +async function updateIndicatorAndButton(): Promise { const val = $("#saveCustomTextModal .textName").val() as string; const checkbox = $("#saveCustomTextModal .isLongText").prop( "checked" @@ -69,7 +73,7 @@ function updateIndicatorAndButton(): void { indicator?.hide(); $("#saveCustomTextModal button.save").prop("disabled", true); } else { - const names = CustomText.getCustomTextNames(checkbox); + const names = await CustomText.getCustomTextNames(checkbox); if (names.includes(val)) { indicator?.show("unavailable"); $("#saveCustomTextModal button.save").prop("disabled", true); @@ -98,22 +102,22 @@ async function setup(modalEl: HTMLElement): Promise { level: 0, }, }); - modalEl.addEventListener("submit", (e) => { + modalEl.addEventListener("submit", async (e) => { e.preventDefault(); - if (save()) hide(); + if (await save()) hide(); }); - modalEl.querySelector(".textName")?.addEventListener("input", (e) => { + modalEl.querySelector(".textName")?.addEventListener("input", async (e) => { const val = (e.target as HTMLInputElement).value; if (val.length > 0) { indicator?.show("loading"); - updateInputAndButtonDebounced(); + await updateInputAndButtonDebounced(); } }); - modalEl.querySelector(".isLongText")?.addEventListener("input", (e) => { + modalEl.querySelector(".isLongText")?.addEventListener("input", async (e) => { const val = (e.target as HTMLInputElement).value; if (val.length > 0) { indicator?.show("loading"); - updateInputAndButtonDebounced(); + await updateInputAndButtonDebounced(); } }); } diff --git a/frontend/src/ts/modals/saved-texts.ts b/frontend/src/ts/modals/saved-texts.ts index 4fb77bbc6089..6649a036f36f 100644 --- a/frontend/src/ts/modals/saved-texts.ts +++ b/frontend/src/ts/modals/saved-texts.ts @@ -9,7 +9,7 @@ import { showPopup } from "./simple-modals"; import * as Notifications from "../elements/notifications"; async function fill(): Promise { - const names = CustomText.getCustomTextNames(); + const names = await CustomText.getCustomTextNames(); const listEl = $(`#savedTextsModal .list`).empty(); let list = ""; if (names.length === 0) { @@ -26,7 +26,7 @@ async function fill(): Promise { } listEl.html(list); - const longNames = CustomText.getCustomTextNames(true); + const longNames = await CustomText.getCustomTextNames(true); const longListEl = $(`#savedTextsModal .listLong`).empty(); let longList = ""; if (longNames.length === 0) { @@ -36,7 +36,9 @@ async function fill(): Promise { longList += `
${escapeHTML(name)}
reset
@@ -91,19 +93,19 @@ async function fill(): Promise { } ); - $("#savedTextsModal .list .savedText .button.name").on("click", (e) => { + $("#savedTextsModal .list .savedText .button.name").on("click", async (e) => { const name = $(e.target).text(); CustomTextState.setCustomTextName(name, false); - const text = getSavedText(name, false); + const text = await getSavedText(name, false); hide({ modalChainData: { text, long: false } }); }); $("#savedTextsModal .listLong .savedLongText .button.name").on( "click", - (e) => { + async (e) => { const name = $(e.target).text(); CustomTextState.setCustomTextName(name, true); - const text = getSavedText(name, true); + const text = await getSavedText(name, true); hide({ modalChainData: { text, long: true } }); } ); @@ -124,10 +126,10 @@ function hide(hideOptions?: HideOptions): void { }); } -function getSavedText(name: string, long: boolean): string { - let text = CustomText.getCustomText(name, long); +async function getSavedText(name: string, long: boolean): Promise { + let text = await CustomText.getCustomText(name, long); if (long) { - text = text.slice(CustomText.getCustomTextLongProgress(name)); + text = text.slice(await CustomText.getCustomTextLongProgress(name)); } return text.join(" "); } diff --git a/frontend/src/ts/modals/simple-modals.ts b/frontend/src/ts/modals/simple-modals.ts index 5d714462c6dc..ab475e78fcb6 100644 --- a/frontend/src/ts/modals/simple-modals.ts +++ b/frontend/src/ts/modals/simple-modals.ts @@ -1035,7 +1035,10 @@ list.deleteCustomText = new SimpleModal({ text: "Are you sure?", buttonText: "delete", execFn: async (_thisPopup): Promise => { - CustomText.deleteCustomText(_thisPopup.parameters[0] as string, false); + await CustomText.deleteCustomText( + _thisPopup.parameters[0] as string, + false + ); CustomTextState.setCustomTextName("", undefined); return { @@ -1054,7 +1057,7 @@ list.deleteCustomTextLong = new SimpleModal({ text: "Are you sure?", buttonText: "delete", execFn: async (_thisPopup): Promise => { - CustomText.deleteCustomText(_thisPopup.parameters[0] as string, true); + await CustomText.deleteCustomText(_thisPopup.parameters[0] as string, true); CustomTextState.setCustomTextName("", undefined); return { @@ -1073,8 +1076,11 @@ list.resetProgressCustomTextLong = new SimpleModal({ text: "Are you sure?", buttonText: "reset", execFn: async (_thisPopup): Promise => { - CustomText.setCustomTextLongProgress(_thisPopup.parameters[0] as string, 0); - const text = CustomText.getCustomText( + await CustomText.setCustomTextLongProgress( + _thisPopup.parameters[0] as string, + 0 + ); + const text = await CustomText.getCustomText( _thisPopup.parameters[0] as string, true ); diff --git a/frontend/src/ts/test/custom-text.ts b/frontend/src/ts/test/custom-text.ts index 908a949733f8..a69155a4e7dd 100644 --- a/frontend/src/ts/test/custom-text.ts +++ b/frontend/src/ts/test/custom-text.ts @@ -6,27 +6,7 @@ import { LocalStorageWithSchema } from "../utils/local-storage-with-schema"; import { z } from "zod"; import { CompletedEventCustomTextSchema } from "@monkeytype/contracts/schemas/results"; import { deepClone } from "../utils/misc"; - -const CustomTextObjectSchema = z.record(z.string(), z.string()); -type CustomTextObject = z.infer; - -const CustomTextLongObjectSchema = z.record( - z.string(), - z.object({ text: z.string(), progress: z.number() }) -); -type CustomTextLongObject = z.infer; - -const customTextLS = new LocalStorageWithSchema({ - key: "customText", - schema: CustomTextObjectSchema, - fallback: {}, -}); -//todo maybe add migrations here? -const customTextLongLS = new LocalStorageWithSchema({ - key: "customTextLong", - schema: CustomTextLongObjectSchema, - fallback: {}, -}); +import { DBSchema, IDBPDatabase, openDB } from "idb"; export const CustomTextSettingsSchema = CompletedEventCustomTextSchema.omit({ textLen: true, @@ -65,6 +45,86 @@ const customTextSettings = new LocalStorageWithSchema({ return migratedData; }, }); +type CustomTextDB = DBSchema & { + customTexts: { + key: string; + value: { + text: string; + }; + }; + customLongTexts: { + key: string; + value: { + text: string; + progress: number; + }; + }; +}; + +export async function getDB(): Promise> { + return await openDB("customTexts", 1, { + async upgrade(db, oldVersion, _newVersion, tx, _event) { + if (oldVersion === 0) { + console.debug("Initialize indexedDB for customTexts from localStorage"); + + //Legacy storage + const CustomTextObjectSchema = z.record(z.string(), z.string()); + const CustomTextLongObjectSchema = z.record( + z.string(), + z.object({ text: z.string(), progress: z.number() }) + ); + const customTextLS = new LocalStorageWithSchema({ + key: "customText", + schema: CustomTextObjectSchema, + fallback: {}, + }); + + //todo maybe add migrations here? + const customTextLongLS = new LocalStorageWithSchema({ + key: "customTextLong", + schema: CustomTextLongObjectSchema, + fallback: {}, + }); + + //create objectStores + await db.createObjectStore("customTexts"); + await db.createObjectStore("customLongTexts"); + //await db.createObjectStore("currentSettings"); + + const ctStore = tx.objectStore("customTexts"); + const longCtStore = tx.objectStore("customLongTexts"); + //const currentSettingsStore = tx.objectStore("currentSettings"); + + //copy from old localStorage + await Promise.all([ + ...Object.entries(customTextLS.get()).map(async ([key, value]) => + ctStore.add({ text: value }, key) + ), + ...Object.entries(customTextLongLS.get()).map(async ([key, value]) => + longCtStore.add({ text: value.text, progress: value.progress }, key) + ), + /* currentSettingsStore.put( + customTextSettings.get(), + "_currentSettings_" + ),*/ + tx.done, + ]); + + console.debug("Remove localStorage after migration"); + //TODO: + //customTextLS.destroy(); + //customTextLongLS.destroy(); + //customTextSettings.destroy(); + } + }, + }); +} + +window.globalThis["db"] = { + get: getDB, + getText: getCustomText, + setText: setCustomText, +}; export function getText(): string[] { return customTextSettings.get().text; @@ -140,111 +200,72 @@ export function getData(): CustomTextSettings { return customTextSettings.get(); } -export function getCustomText(name: string, long = false): string[] { - if (long) { - const customTextLong = getLocalStorageLong(); - const customText = customTextLong[name]; - if (customText === undefined) - throw new Error(`Custom text ${name} not found`); - return customText.text.split(/ +/); - } else { - const customText = getLocalStorage()[name]; - if (customText === undefined) - throw new Error(`Custom text ${name} not found`); - return customText.split(/ +/); - } +export async function getCustomText( + name: string, + long = false +): Promise { + const db = await getDB(); + const customText = await db.get( + long ? "customLongTexts" : "customTexts", + name + ); + if (customText === undefined) + throw new Error(`Custom text ${name} not found`); + + return customText.text.split(/ +/); } -export function setCustomText( +export async function setCustomText( name: string, text: string | string[], long = false -): boolean { - if (long) { - const customText = getLocalStorageLong(); - - customText[name] = { - text: "", - progress: 0, - }; - - const textByName = customText[name]; - if (textByName === undefined) { - throw new Error("Custom text not found"); - } - - if (typeof text === "string") { - textByName.text = text; - } else { - textByName.text = text.join(" "); - } - - return setLocalStorageLong(customText); - } else { - const customText = getLocalStorage(); - - if (typeof text === "string") { - customText[name] = text; +): Promise { + const db = await getDB(); + const textToStore = typeof text === "string" ? text : text.join(" "); + try { + if (long) { + await db.put("customLongTexts", { text: textToStore, progress: 0 }, name); } else { - customText[name] = text.join(" "); + await db.put("customTexts", { text: textToStore }, name); } - - return setLocalStorage(customText); + return true; + } catch (e) { + console.debug("Storing to indexedDb failed: ", e); + return false; } } -export function deleteCustomText(name: string, long: boolean): void { - const customText = long ? getLocalStorageLong() : getLocalStorage(); - - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete customText[name]; +export async function deleteCustomText( + name: string, + long: boolean +): Promise { + const db = await getDB(); - if (long) { - setLocalStorageLong(customText as CustomTextLongObject); - } else { - setLocalStorage(customText as CustomTextObject); - } + await db.delete(long ? "customLongTexts" : "customTexts", name); } -export function getCustomTextLongProgress(name: string): number { - const customText = getLocalStorageLong()[name]; +export async function getCustomTextLongProgress(name: string): Promise { + const db = await getDB(); + const customText = await db.get("customLongTexts", name); if (customText === undefined) throw new Error("Custom text not found"); return customText.progress ?? 0; } -export function setCustomTextLongProgress( +export async function setCustomTextLongProgress( name: string, progress: number -): void { - const customTexts = getLocalStorageLong(); - const customText = customTexts[name]; +): Promise { + const db = await getDB(); + const customText = await db.get("customLongTexts", name); if (customText === undefined) throw new Error("Custom text not found"); customText.progress = progress; - setLocalStorageLong(customTexts); + await db.put("customLongTexts", customText, name); } -function getLocalStorage(): CustomTextObject { - return customTextLS.get(); -} +export async function getCustomTextNames(long = false): Promise { + const db = getDB(); -function getLocalStorageLong(): CustomTextLongObject { - return customTextLongLS.get(); -} - -function setLocalStorage(data: CustomTextObject): boolean { - return customTextLS.set(data); -} - -function setLocalStorageLong(data: CustomTextLongObject): boolean { - return customTextLongLS.set(data); -} - -export function getCustomTextNames(long = false): string[] { - if (long) { - return Object.keys(getLocalStorageLong()); - } else { - return Object.keys(getLocalStorage()); - } + return (await db).getAllKeys(long ? "customLongTexts" : "customTexts"); } diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts index e128c3b4f69e..43842d054507 100644 --- a/frontend/src/ts/test/test-logic.ts +++ b/frontend/src/ts/test/test-logic.ts @@ -1076,20 +1076,21 @@ export async function finish(difficultyFailed = false): Promise { const historyLength = TestInput.input.getHistory()?.length; const newProgress = - CustomText.getCustomTextLongProgress(customTextName) + historyLength; - CustomText.setCustomTextLongProgress(customTextName, newProgress); + (await CustomText.getCustomTextLongProgress(customTextName)) + + historyLength; + await CustomText.setCustomTextLongProgress(customTextName, newProgress); Notifications.add("Long custom text progress saved", 1, { duration: 5, important: true, }); - let newText = CustomText.getCustomText(customTextName, true); + let newText = await CustomText.getCustomText(customTextName, true); newText = newText.slice(newProgress); CustomText.setText(newText); } else { // They finished the test - CustomText.setCustomTextLongProgress(customTextName, 0); - const text = CustomText.getCustomText(customTextName, true); + await CustomText.setCustomTextLongProgress(customTextName, 0); + const text = await CustomText.getCustomText(customTextName, true); CustomText.setText(text); Notifications.add("Long custom text completed", 1, { duration: 5, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 772d5f07b1db..aecce338b4e6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,7 +19,7 @@ importers: version: link:packages/release '@vitest/coverage-v8': specifier: 2.1.9 - version: 2.1.9(vitest@2.1.9(@types/node@20.5.1)(happy-dom@15.10.2)(sass@1.70.0)(terser@5.39.0)) + version: 2.1.9(vitest@2.1.9(@types/node@20.5.1)(happy-dom@15.10.2)(sass@1.70.0)(terser@5.40.0)) conventional-changelog: specifier: 6.0.0 version: 6.0.0(conventional-commits-filter@5.0.0) @@ -49,7 +49,7 @@ importers: version: 2.3.3 vitest: specifier: 2.1.9 - version: 2.1.9(@types/node@20.5.1)(happy-dom@15.10.2)(sass@1.70.0)(terser@5.39.0) + version: 2.1.9(@types/node@20.5.1)(happy-dom@15.10.2)(sass@1.70.0)(terser@5.40.0) backend: dependencies: @@ -239,7 +239,7 @@ importers: version: 10.0.0 '@vitest/coverage-v8': specifier: 2.1.9 - version: 2.1.9(vitest@2.1.9(@types/node@20.14.11)(happy-dom@15.10.2)(sass@1.70.0)(terser@5.39.0)) + version: 2.1.9(vitest@2.1.9(@types/node@20.14.11)(happy-dom@15.10.2)(sass@1.70.0)(terser@5.40.0)) concurrently: specifier: 8.2.2 version: 8.2.2 @@ -272,7 +272,7 @@ importers: version: 5.5.4 vitest: specifier: 2.1.9 - version: 2.1.9(@types/node@20.14.11)(happy-dom@15.10.2)(sass@1.70.0)(terser@5.39.0) + version: 2.1.9(@types/node@20.14.11)(happy-dom@15.10.2)(sass@1.70.0)(terser@5.40.0) vitest-mongodb: specifier: 1.0.0 version: 1.0.0 @@ -339,6 +339,9 @@ importers: html2canvas: specifier: 1.4.1 version: 1.4.1 + idb: + specifier: 8.0.3 + version: 8.0.3 jquery: specifier: 3.7.1 version: 3.7.1 @@ -544,7 +547,7 @@ importers: version: 5.5.4 vitest: specifier: 2.1.9 - version: 2.1.9(@types/node@20.14.11)(happy-dom@15.10.2)(sass@1.70.0)(terser@5.39.0) + version: 2.1.9(@types/node@20.14.11)(happy-dom@15.10.2)(sass@1.70.0)(terser@5.40.0) packages/eslint-config: devDependencies: @@ -611,7 +614,7 @@ importers: version: 5.5.4 vitest: specifier: 2.1.9 - version: 2.1.9(@types/node@20.14.11)(happy-dom@15.10.2)(sass@1.70.0)(terser@5.39.0) + version: 2.1.9(@types/node@20.14.11)(happy-dom@15.10.2)(sass@1.70.0)(terser@5.40.0) packages/oxlint-config: {} @@ -692,7 +695,7 @@ importers: version: 5.5.4 vitest: specifier: 2.1.9 - version: 2.1.9(@types/node@20.14.11)(happy-dom@15.10.2)(sass@1.70.0)(terser@5.39.0) + version: 2.1.9(@types/node@20.14.11)(happy-dom@15.10.2)(sass@1.70.0)(terser@5.40.0) zod: specifier: 3.23.8 version: 3.23.8 @@ -5809,6 +5812,9 @@ packages: idb@7.1.1: resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==} + idb@8.0.3: + resolution: {integrity: sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==} + identity-function@1.0.0: resolution: {integrity: sha512-kNrgUK0qI+9qLTBidsH85HjDLpZfrrS0ElquKKe/fJFdB3D7VeKdXXEvOPDUHSHOzdZKCAAaQIWWyp0l2yq6pw==} @@ -9163,11 +9169,6 @@ packages: engines: {node: '>=10'} hasBin: true - terser@5.39.0: - resolution: {integrity: sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==} - engines: {node: '>=10'} - hasBin: true - terser@5.40.0: resolution: {integrity: sha512-cfeKl/jjwSR5ar7d0FGmave9hFGJT8obyo0z+CrQOylLDbk7X81nPU6vq9VORa5jU30SkDnT2FXjLbR8HLP+xA==} engines: {node: '>=10'} @@ -13053,24 +13054,6 @@ snapshots: '@ungap/structured-clone@1.2.0': {} - '@vitest/coverage-v8@2.1.9(vitest@2.1.9(@types/node@20.14.11)(happy-dom@15.10.2)(sass@1.70.0)(terser@5.39.0))': - dependencies: - '@ampproject/remapping': 2.3.0 - '@bcoe/v8-coverage': 0.2.3 - debug: 4.4.0 - istanbul-lib-coverage: 3.2.2 - istanbul-lib-report: 3.0.1 - istanbul-lib-source-maps: 5.0.6 - istanbul-reports: 3.1.7 - magic-string: 0.30.17 - magicast: 0.3.5 - std-env: 3.8.0 - test-exclude: 7.0.1 - tinyrainbow: 1.2.0 - vitest: 2.1.9(@types/node@20.14.11)(happy-dom@15.10.2)(sass@1.70.0)(terser@5.39.0) - transitivePeerDependencies: - - supports-color - '@vitest/coverage-v8@2.1.9(vitest@2.1.9(@types/node@20.14.11)(happy-dom@15.10.2)(sass@1.70.0)(terser@5.40.0))': dependencies: '@ampproject/remapping': 2.3.0 @@ -13089,7 +13072,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@2.1.9(vitest@2.1.9(@types/node@20.5.1)(happy-dom@15.10.2)(sass@1.70.0)(terser@5.39.0))': + '@vitest/coverage-v8@2.1.9(vitest@2.1.9(@types/node@20.5.1)(happy-dom@15.10.2)(sass@1.70.0)(terser@5.40.0))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 0.2.3 @@ -13103,7 +13086,7 @@ snapshots: std-env: 3.8.0 test-exclude: 7.0.1 tinyrainbow: 1.2.0 - vitest: 2.1.9(@types/node@20.5.1)(happy-dom@15.10.2)(sass@1.70.0)(terser@5.39.0) + vitest: 2.1.9(@types/node@20.5.1)(happy-dom@15.10.2)(sass@1.70.0)(terser@5.40.0) transitivePeerDependencies: - supports-color @@ -13114,14 +13097,6 @@ snapshots: chai: 5.1.2 tinyrainbow: 1.2.0 - '@vitest/mocker@2.1.9(vite@5.4.17(@types/node@20.14.11)(sass@1.70.0)(terser@5.39.0))': - dependencies: - '@vitest/spy': 2.1.9 - estree-walker: 3.0.3 - magic-string: 0.30.17 - optionalDependencies: - vite: 5.4.17(@types/node@20.14.11)(sass@1.70.0)(terser@5.39.0) - '@vitest/mocker@2.1.9(vite@5.4.17(@types/node@20.14.11)(sass@1.70.0)(terser@5.40.0))': dependencies: '@vitest/spy': 2.1.9 @@ -13130,13 +13105,13 @@ snapshots: optionalDependencies: vite: 5.4.17(@types/node@20.14.11)(sass@1.70.0)(terser@5.40.0) - '@vitest/mocker@2.1.9(vite@5.4.17(@types/node@20.5.1)(sass@1.70.0)(terser@5.39.0))': + '@vitest/mocker@2.1.9(vite@5.4.17(@types/node@20.5.1)(sass@1.70.0)(terser@5.40.0))': dependencies: '@vitest/spy': 2.1.9 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 5.4.17(@types/node@20.5.1)(sass@1.70.0)(terser@5.39.0) + vite: 5.4.17(@types/node@20.5.1)(sass@1.70.0)(terser@5.40.0) '@vitest/pretty-format@2.1.9': dependencies: @@ -16672,6 +16647,8 @@ snapshots: idb@7.1.1: {} + idb@8.0.3: {} + identity-function@1.0.0: {} ieee754@1.2.1: {} @@ -20569,14 +20546,6 @@ snapshots: commander: 2.20.3 source-map-support: 0.5.21 - terser@5.39.0: - dependencies: - '@jridgewell/source-map': 0.3.6 - acorn: 8.14.1 - commander: 2.20.3 - source-map-support: 0.5.21 - optional: true - terser@5.40.0: dependencies: '@jridgewell/source-map': 0.3.6 @@ -21280,24 +21249,6 @@ snapshots: dependencies: vite: 6.3.4(@types/node@20.14.11)(sass@1.70.0)(terser@5.40.0)(tsx@4.16.2)(yaml@2.5.0) - vite-node@2.1.9(@types/node@20.14.11)(sass@1.70.0)(terser@5.39.0): - dependencies: - cac: 6.7.14 - debug: 4.4.0 - es-module-lexer: 1.6.0 - pathe: 1.1.2 - vite: 5.4.17(@types/node@20.14.11)(sass@1.70.0)(terser@5.39.0) - transitivePeerDependencies: - - '@types/node' - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - vite-node@2.1.9(@types/node@20.14.11)(sass@1.70.0)(terser@5.40.0): dependencies: cac: 6.7.14 @@ -21316,13 +21267,13 @@ snapshots: - supports-color - terser - vite-node@2.1.9(@types/node@20.5.1)(sass@1.70.0)(terser@5.39.0): + vite-node@2.1.9(@types/node@20.5.1)(sass@1.70.0)(terser@5.40.0): dependencies: cac: 6.7.14 debug: 4.4.0 es-module-lexer: 1.6.0 pathe: 1.1.2 - vite: 5.4.17(@types/node@20.5.1)(sass@1.70.0)(terser@5.39.0) + vite: 5.4.17(@types/node@20.5.1)(sass@1.70.0)(terser@5.40.0) transitivePeerDependencies: - '@types/node' - less @@ -21400,17 +21351,6 @@ snapshots: transitivePeerDependencies: - supports-color - vite@5.4.17(@types/node@20.14.11)(sass@1.70.0)(terser@5.39.0): - dependencies: - esbuild: 0.21.5 - postcss: 8.5.1 - rollup: 4.34.8 - optionalDependencies: - '@types/node': 20.14.11 - fsevents: 2.3.3 - sass: 1.70.0 - terser: 5.39.0 - vite@5.4.17(@types/node@20.14.11)(sass@1.70.0)(terser@5.40.0): dependencies: esbuild: 0.21.5 @@ -21422,7 +21362,7 @@ snapshots: sass: 1.70.0 terser: 5.40.0 - vite@5.4.17(@types/node@20.5.1)(sass@1.70.0)(terser@5.39.0): + vite@5.4.17(@types/node@20.5.1)(sass@1.70.0)(terser@5.40.0): dependencies: esbuild: 0.21.5 postcss: 8.5.1 @@ -21431,7 +21371,7 @@ snapshots: '@types/node': 20.5.1 fsevents: 2.3.3 sass: 1.70.0 - terser: 5.39.0 + terser: 5.40.0 vite@6.3.4(@types/node@20.14.11)(sass@1.70.0)(terser@5.40.0)(tsx@4.16.2)(yaml@2.5.0): dependencies: @@ -21461,42 +21401,6 @@ snapshots: - snappy - supports-color - vitest@2.1.9(@types/node@20.14.11)(happy-dom@15.10.2)(sass@1.70.0)(terser@5.39.0): - dependencies: - '@vitest/expect': 2.1.9 - '@vitest/mocker': 2.1.9(vite@5.4.17(@types/node@20.14.11)(sass@1.70.0)(terser@5.39.0)) - '@vitest/pretty-format': 2.1.9 - '@vitest/runner': 2.1.9 - '@vitest/snapshot': 2.1.9 - '@vitest/spy': 2.1.9 - '@vitest/utils': 2.1.9 - chai: 5.1.2 - debug: 4.4.0 - expect-type: 1.1.0 - magic-string: 0.30.17 - pathe: 1.1.2 - std-env: 3.8.0 - tinybench: 2.9.0 - tinyexec: 0.3.2 - tinypool: 1.0.2 - tinyrainbow: 1.2.0 - vite: 5.4.17(@types/node@20.14.11)(sass@1.70.0)(terser@5.39.0) - vite-node: 2.1.9(@types/node@20.14.11)(sass@1.70.0)(terser@5.39.0) - why-is-node-running: 2.3.0 - optionalDependencies: - '@types/node': 20.14.11 - happy-dom: 15.10.2 - transitivePeerDependencies: - - less - - lightningcss - - msw - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - vitest@2.1.9(@types/node@20.14.11)(happy-dom@15.10.2)(sass@1.70.0)(terser@5.40.0): dependencies: '@vitest/expect': 2.1.9 @@ -21533,10 +21437,10 @@ snapshots: - supports-color - terser - vitest@2.1.9(@types/node@20.5.1)(happy-dom@15.10.2)(sass@1.70.0)(terser@5.39.0): + vitest@2.1.9(@types/node@20.5.1)(happy-dom@15.10.2)(sass@1.70.0)(terser@5.40.0): dependencies: '@vitest/expect': 2.1.9 - '@vitest/mocker': 2.1.9(vite@5.4.17(@types/node@20.5.1)(sass@1.70.0)(terser@5.39.0)) + '@vitest/mocker': 2.1.9(vite@5.4.17(@types/node@20.5.1)(sass@1.70.0)(terser@5.40.0)) '@vitest/pretty-format': 2.1.9 '@vitest/runner': 2.1.9 '@vitest/snapshot': 2.1.9 @@ -21552,8 +21456,8 @@ snapshots: tinyexec: 0.3.2 tinypool: 1.0.2 tinyrainbow: 1.2.0 - vite: 5.4.17(@types/node@20.5.1)(sass@1.70.0)(terser@5.39.0) - vite-node: 2.1.9(@types/node@20.5.1)(sass@1.70.0)(terser@5.39.0) + vite: 5.4.17(@types/node@20.5.1)(sass@1.70.0)(terser@5.40.0) + vite-node: 2.1.9(@types/node@20.5.1)(sass@1.70.0)(terser@5.40.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 20.5.1 From 060f99c4db79177dc5f09ae29ab1fd7ccab89462 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Fri, 6 Jun 2025 14:51:22 +0200 Subject: [PATCH 2/4] current text in indexedDb --- frontend/src/ts/ready.ts | 3 +++ frontend/src/ts/test/custom-text.ts | 38 ++++++++++++++++++----------- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/frontend/src/ts/ready.ts b/frontend/src/ts/ready.ts index 13c32a150d69..35bb8aeb253d 100644 --- a/frontend/src/ts/ready.ts +++ b/frontend/src/ts/ready.ts @@ -8,6 +8,7 @@ import Konami from "konami"; import * as ServerConfiguration from "./ape/server-configuration"; import { getActiveFunboxesWithFunction } from "./test/funbox/list"; import { loadPromise } from "./config"; +import { initCurrentText } from "./test/custom-text"; $(async (): Promise => { await loadPromise; @@ -67,4 +68,6 @@ $(async (): Promise => { }); } } + + await initCurrentText(); }); diff --git a/frontend/src/ts/test/custom-text.ts b/frontend/src/ts/test/custom-text.ts index a69155a4e7dd..4cd2eb605398 100644 --- a/frontend/src/ts/test/custom-text.ts +++ b/frontend/src/ts/test/custom-text.ts @@ -25,6 +25,14 @@ const defaultCustomTextSettings: CustomTextSettings = { pipeDelimiter: false, }; +let cachedCurrentText = defaultCustomTextSettings.text; +export async function initCurrentText(): Promise { + const db = await getDB(); + cachedCurrentText = + (await db.get("currentText", "_currentText_")) ?? + defaultCustomTextSettings.text; +} + const customTextSettings = new LocalStorageWithSchema({ key: "customTextSettings", schema: CustomTextSettingsSchema, @@ -59,6 +67,7 @@ type CustomTextDB = DBSchema & { progress: number; }; }; + currentText: { key: "_currentText_"; value: string[] }; }; export async function getDB(): Promise> { @@ -89,11 +98,11 @@ export async function getDB(): Promise> { //create objectStores await db.createObjectStore("customTexts"); await db.createObjectStore("customLongTexts"); - //await db.createObjectStore("currentSettings"); + await db.createObjectStore("currentText"); const ctStore = tx.objectStore("customTexts"); const longCtStore = tx.objectStore("customLongTexts"); - //const currentSettingsStore = tx.objectStore("currentSettings"); + const currentTextStore = tx.objectStore("currentText"); //copy from old localStorage await Promise.all([ @@ -103,10 +112,7 @@ export async function getDB(): Promise> { ...Object.entries(customTextLongLS.get()).map(async ([key, value]) => longCtStore.add({ text: value.text, progress: value.progress }, key) ), - /* currentSettingsStore.put( - customTextSettings.get(), - "_currentSettings_" - ),*/ + currentTextStore.put(customTextSettings.get().text, "_currentText_"), tx.done, ]); @@ -115,6 +121,10 @@ export async function getDB(): Promise> { //customTextLS.destroy(); //customTextLongLS.destroy(); //customTextSettings.destroy(); + customTextSettings.set({ + ...customTextSettings.get(), + text: ["outdated"], + }); } }, }); @@ -124,19 +134,19 @@ window.globalThis["db"] = { get: getDB, getText: getCustomText, setText: setCustomText, + getData: getData, }; export function getText(): string[] { - return customTextSettings.get().text; + return cachedCurrentText; } export function setText(txt: string[]): void { - const currentSettings = customTextSettings.get(); - customTextSettings.set({ - ...currentSettings, - text: txt, - limit: { value: txt.length, mode: currentSettings.limit.mode }, - }); + cachedCurrentText = txt; + setTimeout(async () => { + const db = await getDB(); + await db.put("currentText", cachedCurrentText, "_currentText_"); + }, 0); } export function getMode(): CustomTextMode { @@ -197,7 +207,7 @@ export function setPipeDelimiter(val: boolean): void { } export function getData(): CustomTextSettings { - return customTextSettings.get(); + return { ...customTextSettings.get(), text: cachedCurrentText }; } export async function getCustomText( From 065b8174fc1acfc6f0f6310190c1e1863a2f1c7f Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Fri, 6 Jun 2025 15:18:54 +0200 Subject: [PATCH 3/4] fix test --- frontend/__tests__/utils/url-handler.spec.ts | 7 +++++-- frontend/src/ts/test/custom-text.ts | 1 - 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/frontend/__tests__/utils/url-handler.spec.ts b/frontend/__tests__/utils/url-handler.spec.ts index 6321d8131032..22169a284d49 100644 --- a/frontend/__tests__/utils/url-handler.spec.ts +++ b/frontend/__tests__/utils/url-handler.spec.ts @@ -3,7 +3,7 @@ import { Difficulty, Mode, Mode2 } from "@monkeytype/contracts/schemas/shared"; import { compressToURI } from "lz-ts"; import * as UpdateConfig from "../../src/ts/config"; import * as Notifications from "../../src/ts/elements/notifications"; -import { CustomTextSettings } from "../../src/ts/test/custom-text"; +import * as CustomText from "../../src/ts/test/custom-text"; import * as TestLogic from "../../src/ts/test/test-logic"; import * as TestState from "../../src/ts/test/test-state"; import * as Misc from "../../src/ts/utils/misc"; @@ -33,6 +33,8 @@ describe("url-handler", () => { const restartTestMock = vi.spyOn(TestLogic, "restart"); const addNotificationMock = vi.spyOn(Notifications, "add"); + const setCustomTextMock = vi.spyOn(CustomText, "setText"); + beforeEach(() => { [ findGetParameterMock, @@ -48,6 +50,7 @@ describe("url-handler", () => { setFunboxMock, restartTestMock, addNotificationMock, + setCustomTextMock, ].forEach((it) => it.mockReset()); findGetParameterMock.mockImplementation((override) => override); @@ -262,7 +265,7 @@ const urlData = ( data: Partial<{ mode: Mode | undefined; mode2: Mode2 | number; - customText: CustomTextSettings; + customText: CustomText.CustomTextSettings; punctuation: boolean; numbers: boolean; language: string; diff --git a/frontend/src/ts/test/custom-text.ts b/frontend/src/ts/test/custom-text.ts index 4cd2eb605398..6d0f7412db26 100644 --- a/frontend/src/ts/test/custom-text.ts +++ b/frontend/src/ts/test/custom-text.ts @@ -120,7 +120,6 @@ export async function getDB(): Promise> { //TODO: //customTextLS.destroy(); //customTextLongLS.destroy(); - //customTextSettings.destroy(); customTextSettings.set({ ...customTextSettings.get(), text: ["outdated"], From bc5f8a217e31e2c2bee967c18bb7af7cd7fbac11 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Fri, 6 Jun 2025 16:33:10 +0200 Subject: [PATCH 4/4] remove localstorage after migration --- frontend/src/ts/test/custom-text.ts | 13 +++---------- frontend/src/ts/utils/local-storage-with-schema.ts | 3 +++ 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/frontend/src/ts/test/custom-text.ts b/frontend/src/ts/test/custom-text.ts index 6d0f7412db26..dce029f4d120 100644 --- a/frontend/src/ts/test/custom-text.ts +++ b/frontend/src/ts/test/custom-text.ts @@ -117,9 +117,9 @@ export async function getDB(): Promise> { ]); console.debug("Remove localStorage after migration"); - //TODO: - //customTextLS.destroy(); - //customTextLongLS.destroy(); + customTextLS.destroy(); + customTextLongLS.destroy(); + customTextSettings.set({ ...customTextSettings.get(), text: ["outdated"], @@ -129,13 +129,6 @@ export async function getDB(): Promise> { }); } -window.globalThis["db"] = { - get: getDB, - getText: getCustomText, - setText: setCustomText, - getData: getData, -}; - export function getText(): string[] { return cachedCurrentText; } diff --git a/frontend/src/ts/utils/local-storage-with-schema.ts b/frontend/src/ts/utils/local-storage-with-schema.ts index 64637f182973..ddd893116701 100644 --- a/frontend/src/ts/utils/local-storage-with-schema.ts +++ b/frontend/src/ts/utils/local-storage-with-schema.ts @@ -118,4 +118,7 @@ export class LocalStorageWithSchema { return false; } } + public destroy(): void { + window.localStorage.removeItem(this.key); + } }