From 86d2a36731e8ecc3dead4d46cdfb92c1b73f2f00 Mon Sep 17 00:00:00 2001 From: undefined Date: Tue, 25 Aug 2026 05:39:05 +0000 Subject: [PATCH] ui: migrate scratchpad drafts to IndexedDB --- .../components/scratchpad/reducers/editor.ts | 25 ++++------------ .../ui-default/pages/problem_detail.page.tsx | 29 +++++++++++++++++++ packages/ui-default/utils/db.ts | 24 ++++++++++++++- 3 files changed, 57 insertions(+), 21 deletions(-) diff --git a/packages/ui-default/components/scratchpad/reducers/editor.ts b/packages/ui-default/components/scratchpad/reducers/editor.ts index bc033bb74..bee53cd62 100644 --- a/packages/ui-default/components/scratchpad/reducers/editor.ts +++ b/packages/ui-default/components/scratchpad/reducers/editor.ts @@ -1,24 +1,9 @@ -let cacheKey = `${UserContext._id}/${UiContext.pdoc.domainId}/${UiContext.pdoc.docId}`; -if (UiContext.tdoc?._id) cacheKey += `@${UiContext.tdoc._id}`; - -// TODO switch to indexeddb export default function reducer(state = { - lang: localStorage.getItem(`${cacheKey}#lang`) || UiContext.codeLang, - code: localStorage.getItem(cacheKey) || UiContext.codeTemplate, + lang: UiContext.codeLang, + code: UiContext.codeTemplate, }, action: any = {}) { - if (action.type === 'SCRATCHPAD_EDITOR_UPDATE_CODE') { - localStorage.setItem(cacheKey, action.payload); - return { - ...state, - code: action.payload, - }; - } - if (action.type === 'SCRATCHPAD_EDITOR_SET_LANG') { - localStorage.setItem(`${cacheKey}#lang`, action.payload); - return { - ...state, - lang: action.payload, - }; - } + if (action.type === 'SCRATCHPAD_EDITOR_HYDRATE') return action.payload; + if (action.type === 'SCRATCHPAD_EDITOR_UPDATE_CODE') return { ...state, code: action.payload }; + if (action.type === 'SCRATCHPAD_EDITOR_SET_LANG') return { ...state, lang: action.payload }; return state; } diff --git a/packages/ui-default/pages/problem_detail.page.tsx b/packages/ui-default/pages/problem_detail.page.tsx index 00507ec11..35d4ecf9b 100644 --- a/packages/ui-default/pages/problem_detail.page.tsx +++ b/packages/ui-default/pages/problem_detail.page.tsx @@ -1,5 +1,6 @@ import $ from 'jquery'; import * as yaml from 'js-yaml'; +import _ from 'lodash'; import React from 'react'; import { createRoot } from 'react-dom/client'; import { confirm } from 'vj/components/dialog'; @@ -144,6 +145,34 @@ const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homewor const { default: ScratchpadApp } = await import('../components/scratchpad'); const { default: ScratchpadReducer } = await import('../components/scratchpad/reducers'); const { Provider, store } = await loadReactRedux(ScratchpadReducer); + let cacheKey = `${UserContext._id}/${UiContext.pdoc.domainId}/${UiContext.pdoc.docId}`; + if (UiContext.tdoc?._id) cacheKey += `@${UiContext.tdoc._id}`; + try { + const draft = await (await openDB).get('scratchpad-drafts', cacheKey); + if (draft) { + store.dispatch({ + type: 'SCRATCHPAD_EDITOR_HYDRATE', + payload: { + code: draft.code ?? UiContext.codeTemplate, + lang: draft.lang ?? UiContext.codeLang, + }, + }); + } + } catch { } + + let previousEditorState = store.getState().editor; + let write = Promise.resolve(); + const saveDraft = _.debounce((code: string, lang: string) => { + write = write + .then(() => openDB.then((db) => db.put('scratchpad-drafts', { id: cacheKey, code, lang }))) + .catch(() => { }); + }, 400); + store.subscribe(() => { + const editor = store.getState().editor; + if (editor === previousEditorState) return; + previousEditorState = editor; + saveDraft(editor.code, editor.lang); + }); // @ts-ignore window.store = store; diff --git a/packages/ui-default/utils/db.ts b/packages/ui-default/utils/db.ts index 0f2e853bd..bee69f419 100644 --- a/packages/ui-default/utils/db.ts +++ b/packages/ui-default/utils/db.ts @@ -1,7 +1,9 @@ import { openDB as _open } from 'idb'; import { alert } from 'vj/components/dialog'; -export const openDB = _open('hydro', 2, { +const scratchpadCleanup: string[] = []; + +export const openDB = _open('hydro', 3, { upgrade(db, oldVersion) { if (oldVersion < 1) { const solutionStore = db.createObjectStore('solutions', { keyPath: 'id' }); @@ -18,6 +20,23 @@ export const openDB = _open('hydro', 2, { } } if (oldVersion < 2) db.createObjectStore('domain-info', { keyPath: 'id' }); + if (oldVersion < 3) { + const scratchpadStore = db.createObjectStore('scratchpad-drafts', { keyPath: 'id' }); + const drafts = new Map(); + const scratchpadKey = /^(\d+\/[A-Za-z]\w{3,31}\/\d+(?:@[\dA-Fa-f]{24})?)(#lang)?$/; + for (const key of Object.keys(localStorage)) { + const match = key.match(scratchpadKey); + if (!match) continue; + const value = localStorage.getItem(key); + if (value === null) continue; + const draft = drafts.get(match[1]) || { id: match[1] }; + if (match[2]) draft.lang = value; + else draft.code = value; + drafts.set(draft.id, draft); + scratchpadCleanup.push(key); + } + for (const draft of drafts.values()) scratchpadStore.put(draft); + } }, blocked(currentVersion, blockedVersion) { console.error('IDB Blocked by version', blockedVersion, 'want', currentVersion); @@ -30,4 +49,7 @@ export const openDB = _open('hydro', 2, { terminated() { console.error('IDB Terminated'); }, +}).then((db) => { + for (const key of scratchpadCleanup) localStorage.removeItem(key); + return db; });