diff --git a/apps/mana/apps/web/src/lib/components/workbench/AppPage.svelte b/apps/mana/apps/web/src/lib/components/workbench/AppPage.svelte index 72fe92a1c..454266622 100644 --- a/apps/mana/apps/web/src/lib/components/workbench/AppPage.svelte +++ b/apps/mana/apps/web/src/lib/components/workbench/AppPage.svelte @@ -61,14 +61,30 @@ let ListComponent = $state(null); let loadError = $state(false); + // Track the last appId we loaded for so we only reload when it actually + // changes — not on every reactive churn from the data layer below us. + // Without this guard, any liveQuery update inside a child ListView (e.g. + // creating a task) can re-run this effect, which clears ListComponent and + // makes the whole carousel flash like a page reload. + let lastLoadedAppId: string | undefined = undefined; + $effect(() => { + if (appId === lastLoadedAppId) return; + lastLoadedAppId = appId; + ListComponent = null; loadError = false; - if (app) { - const loader = app.views.list.load; - loader().then( - (mod) => (ListComponent = mod.default), - () => (loadError = true) + const currentApp = app; + if (currentApp) { + currentApp.views.list.load().then( + (mod) => { + // Guard against an out-of-order load if appId changed again + // while we were awaiting. + if (lastLoadedAppId === appId) ListComponent = mod.default; + }, + () => { + if (lastLoadedAppId === appId) loadError = true; + } ); } }); diff --git a/apps/mana/apps/web/src/lib/data/database.ts b/apps/mana/apps/web/src/lib/data/database.ts index cb3ec6bef..c96e6e8fe 100644 --- a/apps/mana/apps/web/src/lib/data/database.ts +++ b/apps/mana/apps/web/src/lib/data/database.ts @@ -651,15 +651,27 @@ const pendingChangesTable = db.table('_pendingChanges'); * The Dexie creating/updating hook itself is synchronous and cannot await * a recovery, so we just dispatch the event and let the UI / sync engine * decide what to do (e.g. surface a toast, run cleanupTombstones). + * + * IMPORTANT: Dexie hooks fire inside the calling write's implicit transaction + * which only includes the user-facing table (e.g. `tasks`). Writing to + * `_pendingChanges` from there hits `NotFoundError: object store not in + * scope`. We defer the write to a microtask so it runs in a fresh + * transaction after the user's commit lands. */ function trackPendingChange(table: string, change: Record): void { - pendingChangesTable.add(change).catch((err: unknown) => { - if (isQuotaError(err)) { - notifyQuotaExceeded({ table, op: 'pending-change', cleaned: 0, recovered: false }); - } else { - console.error('[mana-sync] failed to record pending change:', err); - } - }); + // setTimeout (not queueMicrotask) is required: Dexie binds the active + // transaction to the current Zone via Promise scheduling, and a microtask + // is still considered "inside" the transaction. setTimeout(0) breaks out + // completely so the new add() spawns its own implicit transaction. + setTimeout(() => { + pendingChangesTable.add(change).catch((err: unknown) => { + if (isQuotaError(err)) { + notifyQuotaExceeded({ table, op: 'pending-change', cleaned: 0, recovered: false }); + } else { + console.error('[mana-sync] failed to record pending change:', err); + } + }); + }, 0); } /** diff --git a/apps/mana/apps/web/src/lib/modules/zitare/ListView.svelte b/apps/mana/apps/web/src/lib/modules/zitare/ListView.svelte index 33e52b848..41efa2496 100644 --- a/apps/mana/apps/web/src/lib/modules/zitare/ListView.svelte +++ b/apps/mana/apps/web/src/lib/modules/zitare/ListView.svelte @@ -4,6 +4,7 @@ Supports tag drag-and-drop onto the current quote. -->