refactor(filters): mount-scope store bindings and fix effect-update loop

Replace the side-effect-on-import $effect.root in bindings with an explicit
startFilterBindings() started from an AppBindings provider in onMount, so the
filters/sort -> font-catalog bridge has a lifecycle tied to the app tree and a
returned cleanup. bindings now consumes getFontCatalog().

Fix the effect-update loop this surfaced: setGroups populated the reactive
groups array in place via `groups.length = 0; groups.push(...)`. push reads the
array's length signal, so the populating effect both read and wrote
groups.length each run and re-triggered itself forever
(effect_update_depth_exceeded). setGroups now reassigns the array (groups is
`let`), which does not read length.

Extract mapFilterMetadataToGroups to own the metadata -> group-config mapping,
including sorting a copy of options (the source is TanStack-cached data; an
in-place sort corrupts the cache and writes into the effect's read dependency).
This commit is contained in:
Ilia Mashkov
2026-06-01 17:25:26 +03:00
parent 1ad015aed6
commit 9780ff9358
9 changed files with 191 additions and 58 deletions
+9 -4
View File
@@ -16,12 +16,17 @@
*/
import '$routes/router';
import { Router } from 'sv-router';
import { QueryProvider } from './providers';
import {
AppBindingsProvider,
QueryProvider,
} from './providers';
import Layout from './ui/Layout.svelte';
</script>
<QueryProvider>
<Layout>
<Router />
</Layout>
<AppBindingsProvider>
<Layout>
<Router />
</Layout>
</AppBindingsProvider>
</QueryProvider>
+24
View File
@@ -0,0 +1,24 @@
<!--
Component: AppBindings
Provider that starts app-wide store bindings (filters → sort → font catalog)
for its subtree. Mount-scoped so the bindings' lifetime tracks the app tree.
-->
<script lang="ts">
import { startFilterBindings } from '$features/FilterAndSortFonts';
import { onMount } from 'svelte';
import type { Snippet } from 'svelte';
interface Props {
/**
* Content snippet
*/
children?: Snippet;
}
let { children }: Props = $props();
// startFilterBindings returns its $effect.root cleanup; onMount runs it on unmount.
onMount(() => startFilterBindings());
</script>
{@render children?.()}
+1
View File
@@ -1 +1,2 @@
export { default as AppBindingsProvider } from './AppBindings.svelte';
export { default as QueryProvider } from './QueryProvider.svelte';