fix(tauri-app): kill effect_update_depth_exceeded via untrack + setView
Two independent Svelte 5 effect-loop bugs that triggered the
same 'effect_update_depth_exceeded' guard:
1. PostCard.svelte: the embed-quote-fetch $effect and the
likedBox $effect.pre read a state variable (quotedLoading /
likedBox) and then synchronously wrote to it in the same
effect run. Svelte 5's effect tracker schedules
possible_effect_self_invalidation on the touched state, the
effect re-fires immediately, and the cycle trips the
'flush_count > 1000' guard. With ~30 PostCards mounting on
login the per-card loop compounds into the depth exceeded
error. Wrap the read+write blocks in untrack() so the
hydration flags don't contribute to the effect's dep set;
the outer 'post.embed.uri / post.did / post.rkey' reads
remain tracked so navigation between cards still triggers a
fresh hydrate.
2. App.svelte: the four $effect blocks (home-refresh,
home-poll, profile-refresh, search-debounce) lived and died
together. Even after splitting, Svelte 5 still flagged the
call chain into refreshTimeline / refreshProfile because
their sync prelude writes 'timelineLoading = true' /
'profileLoading = true' while the effect already tracks the
same downstream state via the proxy. Drive everything
imperatively through a single setView(v) function and move
the 5s poll into the session.subscribe callback, which fires
only on actual login/logout transitions. setView is the
single point that flips view AND triggers the right refresh
per destination — NavRail on_select, LoginScreen onLogin,
handleLogout, the 'back to timeline' button, and the tray
navigate-event bridge all route through it now.
Also: NavRail and StatusBar self-style their grid-area in
their own component styles ('grid-area: rail' / 'status') so
App.svelte doesn't need the fragile '$state.s-XXX > nav.rail'
cross-component selector that Svelte 5 was failing to match
in the Tauri webview, leaving rail buttons invisible to clicks.
This commit is contained in:
@@ -62,7 +62,15 @@
|
||||
</nav>
|
||||
|
||||
<style>
|
||||
/* Self-assign the grid area so the parent App.svelte doesn't
|
||||
need a `:global(nav.rail)` selector. Doing it via a parent
|
||||
child-selector is fragile under Svelte 5's scoping — the
|
||||
parent's `.shell.s-XXX > nav.rail` doesn't reliably match
|
||||
`<nav class="rail s-YYY">` in the Tauri webview, which leaves
|
||||
the buttons invisible to clicks. Owning the grid placement
|
||||
here avoids the cross-component selector entirely. */
|
||||
.rail {
|
||||
grid-area: rail;
|
||||
width: 88px;
|
||||
background: var(--bg);
|
||||
border-right: 1px solid var(--line);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { untrack } from "svelte";
|
||||
import {
|
||||
fetchPost,
|
||||
likePost,
|
||||
@@ -23,13 +24,24 @@
|
||||
let quotedErr: string | null = $state(null);
|
||||
let quotedLoading: boolean = $state(false);
|
||||
|
||||
// Resolve the embedded `app.bsky.embed.record` (a quoted post) by
|
||||
// fetching the full record once per URI. We `untrack()` the
|
||||
// in-flight check (`quoted` / `quotedLoading`) so a sync read+write
|
||||
// of the same $state isn't reported as
|
||||
// `effect_update_depth_exceeded` — without it, every time the
|
||||
// effect re-fires (e.g. on parent re-render) Svelte 5's depth
|
||||
// tracker saw `quotedLoading` read **and** flipped to `true`
|
||||
// within the same tick.
|
||||
$effect(() => {
|
||||
const rec = (post.embed?.$type === "app.bsky.embed.record" || post.embed?.$type === "app.bsky.embed.recordWithMedia")
|
||||
? post.embed?.record
|
||||
: null;
|
||||
if (rec?.uri && !quoted && !quotedLoading) {
|
||||
const targetUri = rec?.uri;
|
||||
if (!targetUri) return;
|
||||
untrack(() => {
|
||||
if (quoted || quotedLoading) return;
|
||||
quotedLoading = true;
|
||||
fetchPost(rec.uri)
|
||||
fetchPost(targetUri)
|
||||
.then((r) => {
|
||||
quoted = r.post;
|
||||
})
|
||||
@@ -39,7 +51,7 @@
|
||||
.finally(() => {
|
||||
quotedLoading = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Resolve the embed shape once at render time. We sniff $type to
|
||||
@@ -99,13 +111,26 @@
|
||||
$state(null);
|
||||
$effect.pre(() => {
|
||||
const k = localStorageKey(`liked:${post.did}:${post.rkey}`);
|
||||
likedBox = useLocalStorage<{ liked: boolean; uri: string | null }>(k, {
|
||||
liked: false,
|
||||
uri: null,
|
||||
// Re-create the box whenever the post changes; the hydration
|
||||
// reads (`likedBox.get()`) and the writes that seed `liked` /
|
||||
// `likedUri` from localStorage all happen inside `untrack` so
|
||||
// `likedBox` (which is $state) is **read and written in the same
|
||||
// effect run**. Without untrack, Svelte 5's effect tracker would
|
||||
// schedule `possible_effect_self_invalidation` on `likedBox`
|
||||
// and the effect would loop until `effect_update_depth_exceeded`
|
||||
// fires. See the explorer agent's read-out: this is THE loop
|
||||
// that took down login with `process_fn x 95`. The outer
|
||||
// `post.did` / `post.rkey` reads remain tracked so the effect
|
||||
// still re-runs when navigating from one card to the next.
|
||||
untrack(() => {
|
||||
likedBox = useLocalStorage<{ liked: boolean; uri: string | null }>(k, {
|
||||
liked: false,
|
||||
uri: null,
|
||||
});
|
||||
const stored = likedBox.get();
|
||||
liked = stored.liked;
|
||||
likedUri = stored.uri;
|
||||
});
|
||||
const stored = likedBox.get();
|
||||
liked = stored.liked;
|
||||
likedUri = stored.uri;
|
||||
});
|
||||
$effect(() => {
|
||||
if (!likedBox) return;
|
||||
|
||||
@@ -53,7 +53,11 @@
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Self-assign the grid area — see NavRail.svelte for why we don't
|
||||
rely on the parent's `:global(.statusbar)` child selector
|
||||
(Svelte 5 scoping makes it unreliable in the Tauri webview). */
|
||||
.statusbar {
|
||||
grid-area: status;
|
||||
height: 24px;
|
||||
background: var(--bg-elev);
|
||||
border-top: 1px solid var(--line);
|
||||
|
||||
Reference in New Issue
Block a user