Protect toolbox data after IndexedDB startup failures
All checks were successful
Deploy Sokko G / deploy (push) Successful in 18s

This commit is contained in:
Shinuwa 2026-09-12 23:05:16 +02:00
parent b53c7c17c8
commit 09a2d4b71a
13 changed files with 242 additions and 5 deletions

View file

@ -30,6 +30,9 @@ function normalizeToolboxes(toolboxes) {
export function useIndexedToolboxes(onError) {
const [ready, setReady] = useState(false);
const [loadError, setLoadError] = useState("");
const [loadAttempt, setLoadAttempt] = useState(0);
const writableRef = useRef(false);
const [state, setState] = useState({ toolboxes: [], links: {}, moduleData: {}, toastPosition: "right" });
const [queue] = useState(() => createPersistenceQueue(state, setState));
const [storageUsage, setStorageUsage] = useState({ used: 0, limit: 0, ratio: 0 });
@ -71,26 +74,34 @@ export function useIndexedToolboxes(onError) {
moduleData: Object.fromEntries((modules || []).map((entry) => [entry.key, entry.data])),
toastPosition: toastPosition === "left" ? "left" : "right"
});
writableRef.current = true;
setReady(true);
refreshQuota();
} catch (error) {
if (!cancelled) {
setReady(true);
reportStorageError(error);
setLoadError(error?.message || "Erreur de stockage IndexedDB.");
}
}
}
loadStore();
return () => { cancelled = true; };
}, [queue, refreshQuota, reportStorageError]);
return () => { cancelled = true; writableRef.current = false; };
}, [queue, refreshQuota, loadAttempt]);
function retryLoad() {
if (ready || !loadError) return;
setLoadError("");
setLoadAttempt((attempt) => attempt + 1);
}
function enqueue(apply, persist) {
if (!writableRef.current) return Promise.resolve(false);
const result = queue.enqueue(apply, persist, reportStorageError);
void result.then((success) => { if (success) refreshQuota(); });
return result;
}
function persistToolboxes(value) {
if (!writableRef.current) return false;
const apply = typeof value === "function" ? value : () => value;
enqueue(
(current) => ({ ...current, toolboxes: normalizeToolboxes(apply(current.toolboxes)) }),
@ -100,6 +111,7 @@ export function useIndexedToolboxes(onError) {
}
function persistLinks(value) {
if (!writableRef.current) return false;
enqueue(
(current) => ({ ...current, links: typeof value === "function" ? value(current.links) : value }),
(next) => dbSetLinks(next.links)
@ -112,6 +124,7 @@ export function useIndexedToolboxes(onError) {
}
function updateModuleData(toolboxId, moduleId, value, moduleType = "") {
if (!writableRef.current) return false;
const compact = prepareModuleDataForStorage(queue.getCurrent().toolboxes, toolboxId, moduleId, value, moduleType);
const key = moduleStorageKey(toolboxId, moduleId);
const hasModule = (current) => current.toolboxes.some((toolbox) => toolbox.id === toolboxId && toolbox.modules.some((module) => module.id === moduleId));
@ -134,6 +147,7 @@ export function useIndexedToolboxes(onError) {
}
function removeToolboxModuleData(toolbox) {
if (!writableRef.current) return Promise.resolve(false);
const keys = (toolbox?.modules || []).map((module) => moduleStorageKey(toolbox.id, module.id));
if (!keys.length) return Promise.resolve(true);
return enqueue(
@ -150,6 +164,7 @@ export function useIndexedToolboxes(onError) {
}
function setToastPosition(position) {
if (!writableRef.current) return false;
const toastPosition = position === "left" ? "left" : "right";
enqueue(
(current) => ({ ...current, toastPosition }),
@ -160,6 +175,8 @@ export function useIndexedToolboxes(onError) {
return {
...state,
ready,
loadError,
retryLoad,
storageUsage,
setToolboxes: persistToolboxes,
setLinks: persistLinks,

View file

@ -27,6 +27,7 @@ function getLibraryUrl() {
export function useToolboxActions({ store, notify, creationTemplate, setConfirmModal, setCreateModal, setLinkModalGameId, setDrawerGameId, setImage }) {
function createToolbox(name, gameId = "", options = {}) {
if (!store.ready) return null;
const now = new Date().toISOString();
const memosModuleId = uid("mod");
const linksModuleId = uid("mod");
@ -241,6 +242,7 @@ export function useToolboxActions({ store, notify, creationTemplate, setConfirmM
store.setToolboxes((toolboxes) => [...toolboxes].sort((a, b) => (order.get(a.id) ?? 9999) - (order.get(b.id) ?? 9999)));
},
importToolbox: async (file, gameId = "") => {
if (!store.ready) return null;
try {
const toolbox = await importToolboxPayload(file, gameId);
if (toolbox) notify("Toolbox importée.");
@ -257,6 +259,7 @@ export function useToolboxActions({ store, notify, creationTemplate, setConfirmM
}
},
importAllToolboxes: async (file) => {
if (!store.ready) return false;
try {
await importAllToolboxesPayload(file);
notify("Import global terminé.");
@ -273,12 +276,14 @@ export function useToolboxActions({ store, notify, creationTemplate, setConfirmM
}
},
exportToolbox: (id) => {
if (!store.ready) return false;
const toolbox = normalizeToolbox(store.toolboxes.find((item) => item.id === id));
if (!toolbox) return;
downloadJson(createToolboxExportPayload(toolbox, store.moduleData), `${toolbox.name.toLowerCase().replace(/[^a-z0-9]+/gi, "-")}.sokkog.json`);
notify("Export de la toolbox lancé.");
},
exportAllToolboxes: () => {
if (!store.ready) return false;
downloadJson(createGlobalExportPayload(store.toolboxes, store.links, store.moduleData), `sokkog-global-${new Date().toISOString().slice(0, 10)}.json`);
notify("Export global lancé.");
}