From 10bdd64efba3662044531e362ae0af193020c6cc Mon Sep 17 00:00:00 2001 From: Till JS Date: Fri, 17 Apr 2026 15:35:33 +0200 Subject: [PATCH] refactor(workbench): consolidate deep-link handler, reset store on dispose MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Merge the onMount + $effect deep-link handlers into one $effect gated on `workbenchScenesStore.initialized`. On cold load the effect fires early (store not ready), bounces, and re-fires once init completes. Removes the duplicated logic and eliminates the race between the two paths. onMount now only kicks off initialize(). - `dispose()` now resets `initializedState` and `subscribeRetryCount` so a navigate-away → back cycle re-runs `initialize()` with a fresh subscription and a clean retry budget. scenesState is left intact to avoid an empty-workbench flash while the new liveQuery re-emits. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/lib/stores/workbench-scenes.svelte.ts | 7 ++++ .../apps/web/src/routes/(app)/+page.svelte | 40 +++++++------------ 2 files changed, 22 insertions(+), 25 deletions(-) diff --git a/apps/mana/apps/web/src/lib/stores/workbench-scenes.svelte.ts b/apps/mana/apps/web/src/lib/stores/workbench-scenes.svelte.ts index cd266b560..fec45b2b8 100644 --- a/apps/mana/apps/web/src/lib/stores/workbench-scenes.svelte.ts +++ b/apps/mana/apps/web/src/lib/stores/workbench-scenes.svelte.ts @@ -230,6 +230,13 @@ export const workbenchScenesStore = { dispose() { subscription?.unsubscribe(); subscription = null; + subscribeRetryCount = 0; + // Reset the init flag so a subsequent mount (navigate-away → back) + // re-runs `initialize()` with a fresh subscription. Leave + // `scenesState` / `activeSceneIdState` untouched — re-mount keeps + // the in-memory snapshot so the UI doesn't flash empty while + // Dexie's liveQuery re-emits. + initializedState = false; }, // ── Scene CRUD ─────────────────────────────────────────── diff --git a/apps/mana/apps/web/src/routes/(app)/+page.svelte b/apps/mana/apps/web/src/routes/(app)/+page.svelte index 30e0730d5..a3b02e2e6 100644 --- a/apps/mana/apps/web/src/routes/(app)/+page.svelte +++ b/apps/mana/apps/web/src/routes/(app)/+page.svelte @@ -63,45 +63,35 @@ }); // ── Scene store wiring ────────────────────────────────── - onMount(async () => { - await workbenchScenesStore.initialize(); - // Deep-link: `/?app=settings` (or any registered appId) opens the - // app in the active scene (or focuses it if already open) and - // scrolls it into view. Used by command menu, pill-nav settings - // link, onboarding CTAs, sync-status banner — anywhere we used - // to navigate to `/settings` before the route was removed. - const target = $page.url.searchParams.get('app'); - if (target && getApp(target)) { - const already = workbenchScenesStore.openApps.find((a) => a.appId === target); - if (!already) await workbenchScenesStore.addApp(target); - await tick(); - scrollToPage(target); - // Clean the query out of the URL so refresh doesn't re-trigger. - const clean = new URL($page.url); - clean.searchParams.delete('app'); - history.replaceState({}, '', clean); - } + onMount(() => { + workbenchScenesStore.initialize(); }); - // Reactive deep-link: handles `/?app=…` navigations that happen - // while the page is already mounted (e.g. clicking a link inside - // the companion chat). onMount only fires once; this $effect - // re-runs whenever the URL search params change. + + // Deep-link handler — runs on initial mount AND on post-mount URL + // changes (e.g. a link inside the companion chat). Gated on + // `initialized` so the addApp() call always hits a seeded store — + // on cold load the effect fires before initialize() completes and + // comes back in when the store is ready. Used by command menu, + // pill-nav settings link, onboarding CTAs, sync-status banner — + // anywhere we used to navigate to `/settings` before the route + // was removed. $effect(() => { + if (!workbenchScenesStore.initialized) return; const target = $page.url.searchParams.get('app'); if (!target || !getApp(target)) return; const hash = $page.url.hash?.slice(1) || ''; - // Use queueMicrotask so we don't mutate state during the effect's first run + // Use queueMicrotask so we don't mutate state during the effect's first run. queueMicrotask(async () => { const already = workbenchScenesStore.openApps.find((a) => a.appId === target); if (!already) await workbenchScenesStore.addApp(target); await tick(); scrollToPage(target); - // Clean the ?app= param but preserve the hash for the target panel + // Clean the ?app= param but preserve the hash for the target panel. const clean = new URL($page.url); clean.searchParams.delete('app'); history.replaceState({}, '', clean); // Notify the target panel about the hash anchor (e.g. settings - // needs to switch to the right tab and scroll to the section) + // needs to switch to the right tab and scroll to the section). if (hash) { await tick(); window.dispatchEvent(