Serialize toolbox writes and preserve concurrent edits
Some checks failed
Deploy Sokko G / deploy (push) Failing after 2s

This commit is contained in:
Shinuwa 2026-09-10 19:09:48 +02:00
parent 7521d50626
commit a4cf633ab4
12 changed files with 381 additions and 320 deletions

View file

@ -46,41 +46,53 @@ export function useToolboxActions({ store, notify, creationTemplate, setConfirmM
]
};
if (options.tutorial === true) toolbox.tutorial = true;
if (!store.setToolboxes([toolbox, ...store.toolboxes])) return null;
store.updateModuleData(toolbox.id, memosModuleId, { items: [{ id: uid("memo"), text: welcomeText, color: "#f6c453", updatedAt: now }] }, "memos");
store.updateModuleData(toolbox.id, linksModuleId, { links: [libraryLink] }, "links");
if (gameId) store.setLinks({ ...store.links, [gameId]: toolbox.id });
const memos = compactModuleDataForStorage("memos", { items: [{ id: uid("memo"), text: welcomeText, color: "#f6c453", updatedAt: now }] });
const links = compactModuleDataForStorage("links", { links: [libraryLink] });
void store.persistToolboxSnapshot((current) => ({
toolboxes: [toolbox, ...current.toolboxes],
links: gameId ? { ...current.links, [gameId]: toolbox.id } : current.links,
moduleData: {
...current.moduleData,
[globalModuleKey(toolbox.id, memosModuleId)]: memos,
[globalModuleKey(toolbox.id, linksModuleId)]: links
}
}));
return toolbox;
}
function updateToolbox(nextToolbox) {
store.setToolboxes(store.toolboxes.map((toolbox) => toolbox.id === nextToolbox.id
? { ...nextToolbox, updatedAt: new Date().toISOString() }
const updatedAt = new Date().toISOString();
store.setToolboxes((toolboxes) => toolboxes.map((toolbox) => toolbox.id === nextToolbox.id
? { ...nextToolbox, updatedAt }
: toolbox));
}
function deleteToolbox(id) {
const toolbox = store.toolboxes.find((item) => item.id === id);
const nextToolboxes = store.toolboxes.filter((item) => item.id !== id);
const nextModuleData = { ...store.moduleData };
(toolbox?.modules || []).forEach((module) => delete nextModuleData[`${id}:${module.id}`]);
const nextLinks = { ...store.links };
Object.entries(nextLinks).forEach(([gameId, toolboxId]) => {
if (toolboxId === id) delete nextLinks[gameId];
return store.persistToolboxSnapshot((current) => {
const toolbox = current.toolboxes.find((item) => item.id === id);
const moduleData = { ...current.moduleData };
(toolbox?.modules || []).forEach((module) => delete moduleData[`${id}:${module.id}`]);
return {
toolboxes: current.toolboxes.filter((item) => item.id !== id),
moduleData,
links: Object.fromEntries(Object.entries(current.links).filter(([, toolboxId]) => toolboxId !== id))
};
});
return store.persistToolboxSnapshot(nextToolboxes, nextLinks, nextModuleData);
}
function linkToolboxToGame(gameId, toolboxId) {
const previousToolboxId = store.links[gameId] || "";
const nextLinks = { ...store.links };
if (toolboxId) nextLinks[gameId] = toolboxId;
else delete nextLinks[gameId];
store.setLinks(nextLinks);
const touched = new Set([previousToolboxId, toolboxId].filter(Boolean));
store.setToolboxes(store.toolboxes.map((toolbox) => touched.has(toolbox.id)
? { ...toolbox, updatedAt: new Date().toISOString() }
: toolbox));
const updatedAt = new Date().toISOString();
return store.persistToolboxSnapshot((current) => {
const links = { ...current.links };
const touched = new Set([links[gameId], toolboxId].filter(Boolean));
if (toolboxId) links[gameId] = toolboxId;
else delete links[gameId];
return {
...current,
links,
toolboxes: current.toolboxes.map((toolbox) => touched.has(toolbox.id) ? { ...toolbox, updatedAt } : toolbox)
};
});
}
async function addImageFiles(toolboxId, moduleId, files) {
@ -125,8 +137,9 @@ export function useToolboxActions({ store, notify, creationTemplate, setConfirmM
confirmLabel: "Créer",
onResolve: (confirmed) => {
if (!confirmed) return;
store.setToolboxes(store.toolboxes.map((item) => item.id === toolboxId
? { ...item, modules: [...item.modules, { id: moduleId, type: "checklist", title: list.title || "Checklist" }], updatedAt: new Date().toISOString() }
const updatedAt = new Date().toISOString();
store.setToolboxes((toolboxes) => toolboxes.map((item) => item.id === toolboxId
? { ...item, modules: [...item.modules, { id: moduleId, type: "checklist", title: list.title || "Checklist" }], updatedAt }
: item));
store.updateModuleData(toolboxId, moduleId, { sections }, "checklist");
notify("Checklist créée dans la toolbox associée.");
@ -147,7 +160,7 @@ export function useToolboxActions({ store, notify, creationTemplate, setConfirmM
});
imported.moduleOrder = remapModuleOrderIds(imported.moduleOrder, moduleIdMap);
const normalizedImport = normalizeToolbox(imported);
const nextModuleData = { ...store.moduleData };
const nextModuleData = {};
Object.entries(payload.modules || {}).forEach(([oldId, data]) => {
const nextId = moduleIdMap.get(oldId);
const module = normalizedImport.modules.find((item) => item.id === nextId);
@ -156,9 +169,11 @@ export function useToolboxActions({ store, notify, creationTemplate, setConfirmM
if (compact) nextModuleData[`${normalizedImport.id}:${nextId}`] = compact;
}
});
const nextLinks = { ...store.links };
if (gameId) nextLinks[gameId] = normalizedImport.id;
const saved = await store.persistToolboxSnapshot([normalizedImport, ...store.toolboxes], nextLinks, nextModuleData);
const saved = await store.persistToolboxSnapshot((current) => ({
toolboxes: [normalizedImport, ...current.toolboxes],
links: gameId ? { ...current.links, [gameId]: normalizedImport.id } : current.links,
moduleData: { ...current.moduleData, ...nextModuleData }
}));
if (!saved) return null;
return normalizedImport;
}
@ -183,12 +198,12 @@ export function useToolboxActions({ store, notify, creationTemplate, setConfirmM
const moduleOrder = remapModuleOrderIds(toolbox.moduleOrder, localModuleIdMap);
return normalizeToolbox({ ...toolbox, id: nextToolboxId, name: `${toolbox.name || "Toolbox"} (import)`, modules, moduleOrder, updatedAt: new Date().toISOString() });
});
const nextLinks = { ...store.links };
const nextLinks = {};
Object.entries(payload.links || {}).forEach(([gameId, oldToolboxId]) => {
const nextToolboxId = toolboxIdMap.get(oldToolboxId);
if (nextToolboxId) nextLinks[gameId] = nextToolboxId;
});
const nextModuleData = { ...store.moduleData };
const nextModuleData = {};
Object.entries(payload.modules).forEach(([key, data]) => {
const [oldToolboxId] = key.split(":");
const nextToolboxId = toolboxIdMap.get(oldToolboxId);
@ -198,7 +213,11 @@ export function useToolboxActions({ store, notify, creationTemplate, setConfirmM
const compact = compactModuleDataForStorage(module?.type, data);
if (nextToolboxId && nextModuleId && compact) nextModuleData[`${nextToolboxId}:${nextModuleId}`] = compact;
});
const saved = await store.persistToolboxSnapshot([...importedToolboxes, ...store.toolboxes], nextLinks, nextModuleData);
const saved = await store.persistToolboxSnapshot((current) => ({
toolboxes: [...importedToolboxes, ...current.toolboxes],
links: { ...current.links, ...nextLinks },
moduleData: { ...current.moduleData, ...nextModuleData }
}));
if (!saved) throw new Error("Impossible d'enregistrer l'import global.");
}
@ -219,7 +238,7 @@ export function useToolboxActions({ store, notify, creationTemplate, setConfirmM
updateModuleData: store.updateModuleData,
updateToolboxOrder: (orderedIds) => {
const order = new Map(orderedIds.map((id, index) => [id, index]));
store.setToolboxes([...store.toolboxes].sort((a, b) => (order.get(a.id) ?? 9999) - (order.get(b.id) ?? 9999)));
store.setToolboxes((toolboxes) => [...toolboxes].sort((a, b) => (order.get(a.id) ?? 9999) - (order.get(b.id) ?? 9999)));
},
importToolbox: async (file, gameId = "") => {
try {