Improve IndexedDB module rollback handling
All checks were successful
Deploy Sokko G / deploy (push) Successful in 16s

This commit is contained in:
Shinuwa 2026-08-25 08:55:11 +02:00
parent fe91d18624
commit 43a9e791c5
4 changed files with 162 additions and 19 deletions

View file

@ -0,0 +1,56 @@
// Rôle : fournit les helpers purs de versioning et rollback optimiste des données modules.
function hasOwnValue(data, key) {
return Object.prototype.hasOwnProperty.call(data, key);
}
function uniqueKeys(keys) {
return [...new Set((Array.isArray(keys) ? keys : []).map((key) => String(key || "")).filter(Boolean))];
}
export function createModuleDataCheckpoint(moduleData, versionsByKey, key) {
const version = (versionsByKey[key] || 0) + 1;
return {
key,
version,
hadValue: hasOwnValue(moduleData, key),
value: moduleData[key]
};
}
export function createModuleDataCheckpoints(moduleData, versionsByKey, keys) {
return uniqueKeys(keys).map((key) => createModuleDataCheckpoint(moduleData, versionsByKey, key));
}
export function commitModuleDataCheckpoints(versionsByKey, checkpoints) {
const nextVersions = { ...versionsByKey };
checkpoints.forEach((checkpoint) => {
nextVersions[checkpoint.key] = checkpoint.version;
});
return nextVersions;
}
export function applyModuleDataValue(moduleData, key, value) {
const nextModuleData = { ...moduleData };
if (value == null) delete nextModuleData[key];
else nextModuleData[key] = value;
return nextModuleData;
}
export function removeModuleDataKeys(moduleData, keys) {
const nextModuleData = { ...moduleData };
uniqueKeys(keys).forEach((key) => {
delete nextModuleData[key];
});
return nextModuleData;
}
export function rollbackModuleDataCheckpoints(moduleData, versionsByKey, checkpoints) {
let nextModuleData = moduleData;
checkpoints.forEach((checkpoint) => {
if (versionsByKey[checkpoint.key] !== checkpoint.version) return;
if (nextModuleData === moduleData) nextModuleData = { ...moduleData };
if (checkpoint.hadValue) nextModuleData[checkpoint.key] = checkpoint.value;
else delete nextModuleData[checkpoint.key];
});
return nextModuleData;
}

View file

@ -19,6 +19,14 @@ import {
normalizeToolbox,
prepareModuleDataForStorage
} from "./toolboxStorage.js";
import {
applyModuleDataValue,
commitModuleDataCheckpoints,
createModuleDataCheckpoint,
createModuleDataCheckpoints,
removeModuleDataKeys,
rollbackModuleDataCheckpoints
} from "./optimisticModuleData.js";
export function useIndexedToolboxes(onError) {
const [ready, setReady] = useState(false);
@ -34,7 +42,7 @@ export function useIndexedToolboxes(onError) {
const writeVersionsRef = useRef({
toolboxes: 0,
links: 0,
moduleData: 0,
moduleDataByKey: {},
toastPosition: 0
});
@ -136,15 +144,13 @@ export function useIndexedToolboxes(onError) {
function updateModuleData(toolboxId, moduleId, value, moduleType = "") {
const compact = prepareModuleDataForStorage(toolboxesRef.current, toolboxId, moduleId, value, moduleType);
const key = moduleStorageKey(toolboxId, moduleId);
const previousModuleData = moduleDataRef.current;
const version = ++writeVersionsRef.current.moduleData;
const nextModuleData = { ...previousModuleData };
if (compact == null) delete nextModuleData[key];
else nextModuleData[key] = compact;
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) => {
if (writeVersionsRef.current.moduleData === version) commitModuleData(previousModuleData);
commitModuleData(rollbackModuleDataCheckpoints(moduleDataRef.current, writeVersionsRef.current.moduleDataByKey, [checkpoint]));
reportStorageError(error);
});
return true;
@ -152,13 +158,11 @@ export function useIndexedToolboxes(onError) {
function removeModuleData(toolboxId, moduleId) {
const key = moduleStorageKey(toolboxId, moduleId);
const previousModuleData = moduleDataRef.current;
const version = ++writeVersionsRef.current.moduleData;
const nextModuleData = { ...previousModuleData };
delete nextModuleData[key];
commitModuleData(nextModuleData);
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) => {
if (writeVersionsRef.current.moduleData === version) commitModuleData(previousModuleData);
commitModuleData(rollbackModuleDataCheckpoints(moduleDataRef.current, writeVersionsRef.current.moduleDataByKey, [checkpoint]));
reportStorageError(error);
});
}
@ -166,13 +170,11 @@ export function useIndexedToolboxes(onError) {
function removeToolboxModuleData(toolbox) {
const keys = (toolbox?.modules || []).map((module) => moduleStorageKey(toolbox.id, module.id));
if (!keys.length) return;
const previousModuleData = moduleDataRef.current;
const version = ++writeVersionsRef.current.moduleData;
const nextModuleData = { ...previousModuleData };
keys.forEach((key) => delete nextModuleData[key]);
commitModuleData(nextModuleData);
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) => {
if (writeVersionsRef.current.moduleData === version) commitModuleData(previousModuleData);
commitModuleData(rollbackModuleDataCheckpoints(moduleDataRef.current, writeVersionsRef.current.moduleDataByKey, checkpoints));
reportStorageError(error);
});
}