From ffe57ab0ec2b07467c5f511bb650bb75fa08d624 Mon Sep 17 00:00:00 2001 From: Nico de Haen Date: Mon, 7 Sep 2026 22:00:49 +0200 Subject: [PATCH] fix: syncing failures --- src/App.tsx | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index d52c7c7..4d3e0f8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -14,6 +14,10 @@ import "@ironcalc/workbook/style.css"; import { Webxdc } from "@webxdc/types"; import { encode as base64Encode, decode as base64Decode } from "base64-arraybuffer"; +// Concurrent WASM initialization can replace the instance backing existing +// models. Share the pending initialization across StrictMode effect runs. +let initialization: ReturnType | undefined; + declare global { interface Window { webxdc: Webxdc<{ data: string, sender: string }>; @@ -53,8 +57,8 @@ function App() { // sure we don't actually let cancelled = false; async function start() { - // initialize ironcalc. This is idempotent so can be called multiple times in StrictMode. - await init(); + initialization ??= init(); + await initialization; // the first time around in StrictMode, we have been cancelled so // we don't attempt to set the model. if (cancelled) { @@ -62,7 +66,15 @@ function App() { } // the second time in StrictMode (or the first time outside of StrictMode) is the real one // so we load the model from storage or create a new one and set it in state. - const newModel = loadSelectedModelFromStorage() ?? createNewModel(); + const storedModel = loadSelectedModelFromStorage(); + if (!storedModel) { + // A replay cursor is only valid together with the workbook it describes. + localStorage.removeItem("last_serial"); + // With no cached local edits, historical updates from this device + // must also be replayed instead of being mistaken for live echoes. + localStorage.removeItem("uuid"); + } + const newModel = storedModel ?? createNewModel(); setModel(newModel); } // we start the async here, not waiting for its completion @@ -107,19 +119,21 @@ function App() { // Incoming: apply remote diffs to the model. useEffect(() => { - if (listenerRegistered.current) { + // Register only after WASM and the workbook are ready. + // Delta Chat retains updates and replays them when we register. + if (!model || listenerRegistered.current) { return } listenerRegistered.current = true window.webxdc.setUpdateListener((update) => { const model = modelRef.current; const payload = update.payload; - localStorage.setItem("last_serial", update.serial.toString()); if (!model) { console.warn("Received external diffs but model is not initialized yet"); return } if (payload.sender === uuid) { + localStorage.setItem("last_serial", update.serial.toString()); return } // Decode base64 back to binary and convert to Uint8Array @@ -127,11 +141,13 @@ function App() { const diff = new Uint8Array(diffBuffer); model.applyExternalDiffs(diff); saveSelectedModelInStorage(model); + // Never acknowledge an update before its changes have been saved. + localStorage.setItem("last_serial", update.serial.toString()); // The model is mutated in place; bump the revision so IronCalc repaints // the canvas without disturbing an in-progress edit. setExternalRevision((revision) => revision + 1); }, get_last_serial()) - }, [uuid]) + }, [model, uuid]) if (!model) {