sokko-g/website/src/features/toolboxes/storage/useIndexedToolboxes.js
Shinuwa 43a9e791c5
All checks were successful
Deploy Sokko G / deploy (push) Successful in 16s
Improve IndexedDB module rollback handling
2026-08-25 08:55:11 +02:00

211 lines
7.9 KiB
JavaScript

// Rôle : synchronise les toolboxes, données d'outils, liens de jeux et quota via IndexedDB.
import { useCallback, useEffect, useRef, useState } from "react";
import {
getAllModuleData as dbGetAllModuleData,
getLinks as dbGetLinks,
getSetting as dbGetSetting,
getStorageEstimate,
getToolboxes as dbGetToolboxes,
removeModuleData as dbRemoveModuleData,
removeModuleDataKeys as dbRemoveModuleDataKeys,
setLinks as dbSetLinks,
setModuleData as dbSetModuleData,
setSetting as dbSetSetting,
setToolboxes as dbSetToolboxes
} from "../../../utils/indexedDbStorage.js";
import {
compactToolboxesForStorage,
moduleStorageKey,
normalizeToolbox,
prepareModuleDataForStorage
} from "./toolboxStorage.js";
import {
applyModuleDataValue,
commitModuleDataCheckpoints,
createModuleDataCheckpoint,
createModuleDataCheckpoints,
removeModuleDataKeys,
rollbackModuleDataCheckpoints
} from "./optimisticModuleData.js";
export function useIndexedToolboxes(onError) {
const [ready, setReady] = useState(false);
const [toolboxes, setToolboxesState] = useState([]);
const [links, setLinksState] = useState({});
const [moduleData, setModuleDataState] = useState({});
const [toastPosition, setToastPositionState] = useState("right");
const [storageUsage, setStorageUsage] = useState({ used: 0, limit: 0, ratio: 0 });
const toolboxesRef = useRef(toolboxes);
const linksRef = useRef(links);
const moduleDataRef = useRef(moduleData);
const toastPositionRef = useRef(toastPosition);
const writeVersionsRef = useRef({
toolboxes: 0,
links: 0,
moduleDataByKey: {},
toastPosition: 0
});
const reportStorageError = useCallback((error) => {
onError(error?.message || "Erreur de stockage IndexedDB.");
}, [onError]);
function commitToolboxes(nextToolboxes) {
toolboxesRef.current = nextToolboxes;
setToolboxesState(nextToolboxes);
}
function commitLinks(nextLinks) {
linksRef.current = nextLinks;
setLinksState(nextLinks);
}
function commitModuleData(nextModuleData) {
moduleDataRef.current = nextModuleData;
setModuleDataState(nextModuleData);
}
function commitToastPosition(nextToastPosition) {
toastPositionRef.current = nextToastPosition;
setToastPositionState(nextToastPosition);
}
const refreshQuota = useCallback(async () => {
try {
const estimate = await getStorageEstimate();
const used = estimate.usage || 0;
const limit = estimate.quota || 0;
setStorageUsage({ used, limit, ratio: limit ? Math.min(1, used / limit) : 0 });
} catch {
setStorageUsage({ used: 0, limit: 0, ratio: 0 });
}
}, []);
useEffect(() => {
let cancelled = false;
async function loadStore() {
try {
const [storedToolboxes, storedLinks, storedModules, storedToastPosition] = await Promise.all([
dbGetToolboxes(),
dbGetLinks(),
dbGetAllModuleData(),
dbGetSetting("toastPosition", "right")
]);
if (cancelled) return;
commitToolboxes((Array.isArray(storedToolboxes) ? storedToolboxes : []).map(normalizeToolbox).filter(Boolean));
commitLinks(storedLinks && typeof storedLinks === "object" ? storedLinks : {});
commitModuleData(Object.fromEntries((storedModules || []).map((entry) => [entry.key, entry.data])));
commitToastPosition(storedToastPosition === "left" ? "left" : "right");
setReady(true);
refreshQuota();
} catch (error) {
if (!cancelled) {
setReady(true);
reportStorageError(error);
}
}
}
loadStore();
return () => { cancelled = true; };
}, [refreshQuota, reportStorageError]);
function persistToolboxes(nextToolboxes) {
const normalized = compactToolboxesForStorage(nextToolboxes).map(normalizeToolbox).filter(Boolean);
const previousToolboxes = toolboxesRef.current;
const version = ++writeVersionsRef.current.toolboxes;
commitToolboxes(normalized);
dbSetToolboxes(compactToolboxesForStorage(normalized))
.then(refreshQuota)
.catch((error) => {
if (writeVersionsRef.current.toolboxes === version) commitToolboxes(previousToolboxes);
reportStorageError(error);
});
return true;
}
function persistLinks(nextLinks) {
const previousLinks = linksRef.current;
const version = ++writeVersionsRef.current.links;
commitLinks(nextLinks);
dbSetLinks(nextLinks)
.then(refreshQuota)
.catch((error) => {
if (writeVersionsRef.current.links === version) commitLinks(previousLinks);
reportStorageError(error);
});
return true;
}
function getModuleData(toolboxId, moduleId, fallback) {
const value = moduleDataRef.current[moduleStorageKey(toolboxId, moduleId)];
return value == null ? fallback : value;
}
function updateModuleData(toolboxId, moduleId, value, moduleType = "") {
const compact = prepareModuleDataForStorage(toolboxesRef.current, toolboxId, moduleId, value, moduleType);
const key = moduleStorageKey(toolboxId, moduleId);
const checkpoint = createModuleDataCheckpoint(moduleDataRef.current, writeVersionsRef.current.moduleDataByKey, key);
writeVersionsRef.current.moduleDataByKey = commitModuleDataCheckpoints(writeVersionsRef.current.moduleDataByKey, [checkpoint]);
const nextModuleData = applyModuleDataValue(moduleDataRef.current, key, compact);
commitModuleData(nextModuleData);
const operation = compact == null ? dbRemoveModuleData(key) : dbSetModuleData(key, compact);
operation.then(refreshQuota).catch((error) => {
commitModuleData(rollbackModuleDataCheckpoints(moduleDataRef.current, writeVersionsRef.current.moduleDataByKey, [checkpoint]));
reportStorageError(error);
});
return true;
}
function removeModuleData(toolboxId, moduleId) {
const key = moduleStorageKey(toolboxId, moduleId);
const checkpoint = createModuleDataCheckpoint(moduleDataRef.current, writeVersionsRef.current.moduleDataByKey, key);
writeVersionsRef.current.moduleDataByKey = commitModuleDataCheckpoints(writeVersionsRef.current.moduleDataByKey, [checkpoint]);
commitModuleData(removeModuleDataKeys(moduleDataRef.current, [key]));
dbRemoveModuleData(key).then(refreshQuota).catch((error) => {
commitModuleData(rollbackModuleDataCheckpoints(moduleDataRef.current, writeVersionsRef.current.moduleDataByKey, [checkpoint]));
reportStorageError(error);
});
}
function removeToolboxModuleData(toolbox) {
const keys = (toolbox?.modules || []).map((module) => moduleStorageKey(toolbox.id, module.id));
if (!keys.length) return;
const checkpoints = createModuleDataCheckpoints(moduleDataRef.current, writeVersionsRef.current.moduleDataByKey, keys);
writeVersionsRef.current.moduleDataByKey = commitModuleDataCheckpoints(writeVersionsRef.current.moduleDataByKey, checkpoints);
commitModuleData(removeModuleDataKeys(moduleDataRef.current, keys));
dbRemoveModuleDataKeys(keys).then(refreshQuota).catch((error) => {
commitModuleData(rollbackModuleDataCheckpoints(moduleDataRef.current, writeVersionsRef.current.moduleDataByKey, checkpoints));
reportStorageError(error);
});
}
function setToastPosition(position) {
const normalized = position === "left" ? "left" : "right";
const previousToastPosition = toastPositionRef.current;
const version = ++writeVersionsRef.current.toastPosition;
commitToastPosition(normalized);
dbSetSetting("toastPosition", normalized)
.then(refreshQuota)
.catch((error) => {
if (writeVersionsRef.current.toastPosition === version) commitToastPosition(previousToastPosition);
reportStorageError(error);
});
}
return {
ready,
toolboxes,
links,
moduleData,
toastPosition,
storageUsage,
setToolboxes: persistToolboxes,
setLinks: persistLinks,
getModuleData,
updateModuleData,
removeModuleData,
removeToolboxModuleData,
setToastPosition,
refreshQuota
};
}