mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 01:01:09 +02:00
fix(mana/web): three runtime regressions from sprint 1-3 data layer rewrite
1. database.ts — defer pending-change writes out of the user transaction. `taskTable.add()` opens an implicit Dexie transaction scoped only to `tasks`. The creating-hook then tried to write to `_pendingChanges`, which is not in scope, throwing `NotFoundError: object store not in scope` and breaking every create across todo/calendar/contacts/etc. `queueMicrotask` is not enough — Dexie binds the active transaction to the current zone via Promise scheduling and treats microtasks as "still inside". `setTimeout(0)` breaks out cleanly so the deferred add() spawns its own implicit transaction. 2. workbench/AppPage.svelte — guard ListView reload by appId. The list-loader $effect read `app` (a $derived of getApp(appId)) and on every reactive churn cleared `ListComponent = null`, making the whole carousel flash a spinner. After a task create, liveQuery churn propagated up enough to retrigger this effect, which looked exactly like a full page reload to the user. Now we only reload when appId itself changes, with a stale-load guard for out-of-order awaits. 3. zitare/ListView.svelte — pull `quotesStore.initialize()` out of $effect. The effect called initialize() (which writes `currentQuote` $state) and then read `currentQuote` back, creating a classic write-then-read loop that hit `effect_update_depth_exceeded`. Initialize now runs in onMount; the effect is read-only. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ae648650ea
commit
85fda7b5df
3 changed files with 49 additions and 13 deletions
|
|
@ -61,14 +61,30 @@
|
|||
let ListComponent = $state<Component | null>(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;
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<string, unknown>): 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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
Supports tag drag-and-drop onto the current quote.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { liveQuery } from 'dexie';
|
||||
import { db } from '$lib/data/database';
|
||||
import { quotesStore } from '$lib/modules/zitare/stores/quotes.svelte';
|
||||
|
|
@ -22,11 +23,18 @@
|
|||
let favorites = $state<LocalFavorite[]>([]);
|
||||
let quote = $state<Quote | null>(null);
|
||||
|
||||
$effect(() => {
|
||||
// Initialize once on mount (writes to store state — keep out of $effect
|
||||
// to avoid the read/write loop where reading currentQuote retriggers
|
||||
// the effect after initialize() updates it).
|
||||
onMount(() => {
|
||||
quotesStore.initialize();
|
||||
quote = quotesStore.currentQuote;
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
quote = quotesStore.currentQuote;
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
const sub = liveQuery(async () => {
|
||||
return db
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue