From 369e2c8b5aeac76f702623ef400705092ee27bca Mon Sep 17 00:00:00 2001 From: Afonsop Date: Fri, 18 Sep 2026 17:08:27 +0100 Subject: [PATCH] fix: close the review findings on the restore-all/deep-links/PWA wave MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two reviewers, no blockers, three majors — all fixed. - The remembered-tab focus after a Restore-all batch fired at the message boundary, but the LAST restore's tab inserts one async round later (its OPFS recent-cache write precedes the insertion) and the insertion's own activation overwrote the selection. The focus flag now also consumes at the insertion point, so the batch focus holds in the common all-success case. - The service worker cached ANY successful in-scope navigation under the shell key — a direct navigation to a font or JSON would be served as the app shell for every offline navigation until the next online visit. Only the scope root caches as the shell now. - Local macOS builds broke: sha256sum does not exist there (shasum does) and BSD sed requires an explicit -i suffix. The wrapper picks the available hasher and uses a portable in-place edit. Also: '/app/' gets its own web app manifest (the previous commit's 'installable' claim was true only of the marketing site, whose start_url sits OUTSIDE the service worker's scope — an installed app opened offline showed an error page). The README now states the honest offline timing (assets captured on the second visit — the first visit's preloads outrun the installing worker) and the long-lived-window update behavior. --- README.md | 2 +- scripts/build-web.sh | 7 +++++-- src/app/update/file.rs | 19 +++++++++++++++---- src/app/update/mod.rs | 2 +- web-app.html | 3 +++ web/app.webmanifest | 18 ++++++++++++++++++ web/logo.svg | 21 +++++++++++++++++++++ web/sw.js | 6 +++++- 8 files changed, 69 insertions(+), 9 deletions(-) create mode 100644 web/app.webmanifest create mode 100644 web/logo.svg diff --git a/README.md b/README.md index 222038bb4..faa116882 100644 --- a/README.md +++ b/README.md @@ -183,7 +183,7 @@ never go missing from the bundle): sh scripts/build-web.sh ``` -The bundle ships a service worker (`web/sw.js`, deployed at `/app/sw.js`) that caches the app shell: after one online visit the app starts fully offline and the ~55 MB wasm bundle is served from the browser cache instead of re-downloaded. `build-web.sh` stamps a hash of the finished bundle into `sw.js` as the cache version, so every deploy ships a fresh cache and stale ones are cleaned up automatically. +The bundle ships a service worker (`web/sw.js`, deployed at `/app/sw.js`) that caches the app shell: after a first online visit registers it, the assets are captured on the following visit and the app then starts fully offline, the ~55 MB wasm bundle served from the browser cache instead of re-downloaded. A long-lived installed window keeps running its current version until all its tabs close (no mid-session disruption); the next launch picks up the update. `build-web.sh` stamps a hash of the finished bundle into `sw.js` as the cache version, so every deploy ships a fresh cache and stale ones are cleaned up automatically. `/app/` carries its own web app manifest, so the editor itself can be installed as a standalone app. ## Automation diff --git a/scripts/build-web.sh b/scripts/build-web.sh index 0f4ac8588..8cd192a3d 100644 --- a/scripts/build-web.sh +++ b/scripts/build-web.sh @@ -62,8 +62,11 @@ done echo "build-web.sh: dist/app/sw.js missing — is web/sw.js still listed as a copy-file link in web-app.html?" >&2 exit 1 } -bundle_hash=$(find dist/app -type f ! -name sw.js -print0 | sort -z | xargs -0 sha256sum | sha256sum | cut -c 1-12) -sed -i "s/^const CACHE_VERSION = \"[^\"]*\";/const CACHE_VERSION = \"$bundle_hash\";/" dist/app/sw.js +# Portable hashing: macOS ships shasum (no sha256sum) and BSD sed needs an +# explicit -i suffix — both handled so local builds work everywhere. +if command -v sha256sum >/dev/null 2>&1; then H="sha256sum"; else H="shasum -a 256"; fi +bundle_hash=$(find dist/app -type f ! -name sw.js -print0 | LC_ALL=C sort -z | xargs -0 $H | $H | cut -c 1-12) +sed -i.bak "s/^const CACHE_VERSION = \"[^\"]*\";/const CACHE_VERSION = \"$bundle_hash\";/" dist/app/sw.js && rm -f dist/app/sw.js.bak grep -q "^const CACHE_VERSION = \"$bundle_hash\";" dist/app/sw.js || { echo "error: failed to stamp the cache version into dist/app/sw.js" >&2 exit 1 diff --git a/src/app/update/file.rs b/src/app/update/file.rs index df8bd46e8..eab4d1755 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -1501,6 +1501,15 @@ impl OpenCADStudio { self.apply_display_defaults(idx); idx }; + // A Restore-all batch's LAST restore inserts its tab here — one + // async round AFTER its settle message. The focus flag therefore + // also fires here: the message-boundary hook alone could run + // before this insertion and be overwritten by it. + #[cfg(target_arch = "wasm32")] + if self.web_session_focus_pending && self.web_autosave_restore_all_remaining == 0 { + self.web_session_focus_pending = false; + self.select_remembered_session_tab(); + } let mut recovery_report = recovery_needed.then(|| { crate::io::recovery::RecoveryReport::recovered( @@ -3556,19 +3565,19 @@ impl OpenCADStudio { /// from the after-message hook (`web_session_focus_pending`), so the /// whole settle batch — including the last tab insertion — is done. #[cfg(target_arch = "wasm32")] - pub(super) fn select_remembered_session_tab(&mut self) { + pub(super) fn select_remembered_session_tab(&mut self) -> Task { let Some(session) = self.web_session.as_ref() else { - return; + return Task::none(); }; let Some(remembered) = session.tabs.get(session.active) else { - return; + return Task::none(); }; let recovered = crate::io::web_autosave::recovered_name(remembered); let Some(index) = self.tabs.iter().position(|tab| { let name = tab.tab_display_name(); name == remembered.as_str() || name == recovered }) else { - return; + return Task::none(); }; self.active_tab = index; // The active tab changed under the ribbon; re-sync its chips like @@ -3576,6 +3585,8 @@ impl OpenCADStudio { self.sync_ribbon_layers(); self.sync_ribbon_styles(); self.sync_ribbon_from_selection(); + + Task::none() } /// Remember the open-tab list (names + active index) in localStorage — diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 8a43e31c7..9862c5975 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -297,7 +297,7 @@ impl OpenCADStudio { #[cfg(target_arch = "wasm32")] if self.web_session_focus_pending { self.web_session_focus_pending = false; - self.select_remembered_session_tab(); + return self.select_remembered_session_tab(); } self.sync_open_command_history(); // Close the document-level first-touch transaction started by diff --git a/web-app.html b/web-app.html index b66bb39f0..02009a16a 100644 --- a/web-app.html +++ b/web-app.html @@ -1,6 +1,7 @@ + @@ -46,6 +47,8 @@ + +
diff --git a/web/app.webmanifest b/web/app.webmanifest new file mode 100644 index 000000000..0e79b2c53 --- /dev/null +++ b/web/app.webmanifest @@ -0,0 +1,18 @@ +{ + "name": "Open CAD Studio", + "short_name": "OCS", + "description": "A free CAD editor for DWG and DXF drawings.", + "start_url": "./", + "scope": "./", + "display": "standalone", + "background_color": "#1e1e1e", + "theme_color": "#1e1e1e", + "icons": [ + { + "src": "logo.svg", + "sizes": "any", + "type": "image/svg+xml", + "purpose": "any" + } + ] +} diff --git a/web/logo.svg b/web/logo.svg new file mode 100644 index 000000000..7b4fdc6b5 --- /dev/null +++ b/web/logo.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/web/sw.js b/web/sw.js index b74d2c8ef..13483a675 100644 --- a/web/sw.js +++ b/web/sw.js @@ -89,9 +89,13 @@ self.addEventListener("fetch", (event) => { event.respondWith( (async () => { const cache = await caches.open(CACHE_NAME); + // Only the scope root is the app shell: caching any other in-scope + // navigation here would store e.g. a font or JSON body under the + // shell key and serve it for every offline navigation. + const isShell = new URL(request.url).pathname === SHELL_URL.pathname; try { const fresh = await fetch(request); - await put(cache, SHELL_URL, fresh); + if (isShell) await put(cache, SHELL_URL, fresh); return fresh; } catch { const cached = await cache.match(SHELL_URL);