refactor(fontCatalogStore): single source of truth for query params

On initial load, two separate $effects in bindings.svelte.ts — one for
filters, one for sort — each issued its own setOptions with a different
queryKey on the first flush, producing an orphaned
`/fonts?limit=50&offset=0` request immediately followed by the real
`/fonts?limit=50&sort=popularity&offset=0`. Hardcoding the default sort
on the singleton would have papered over the symptom while leaving the
sortStore default and the catalog-store default coupled by hand.

Make bindings the sole emitter of query params:

- features/.../bindings: merge filter + sort effects into one. The effect
  reads both stores, builds the merged param object, and issues a single
  setParams. No more interleaved setOptions on mount.
- entities/.../fontCatalogStore: gate the observer with `enabled: false`
  on construction. The first setParams flips `#enabled` on and triggers
  exactly one fetch with the correct queryKey. Removes the need for a
  hardcoded default sort on the singleton.
- isEmpty is also gated on `#enabled` so the brief pre-config window
  doesn't render "no results" before bindings configures the query.
- The constructor seeds #result from observer.getCurrentResult() because
  subscribe may not fire synchronously when the observer is disabled.
This commit is contained in:
Ilia Mashkov
2026-05-28 21:38:17 +03:00
parent 7f20f36d0a
commit 5d72bb7a4c
3 changed files with 31 additions and 16 deletions
@@ -42,20 +42,19 @@ $effect.root(() => {
});
/**
* Mirror filter selections + debounced search query into fontCatalogStore params.
* Mirror filter selections + debounced search query + sort into fontCatalogStore params.
*
* Filters and sort are merged into one setParams call to avoid a startup race:
* two separate effects each issued setOptions with a different queryKey on the
* first flush, producing an orphaned `?limit=50&offset=0` fetch immediately
* followed by the real `?limit=50&sort=popularity&offset=0` fetch.
*
* untrack the write so fontCatalogStore's internal $state reads don't feed back
* into this effect's dependency graph.
*/
$effect(() => {
const params = mapAppliedFiltersToParams(appliedFilterStore);
untrack(() => fontCatalogStore.setParams(params));
});
/**
* Mirror sort selection into fontCatalogStore.
*/
$effect(() => {
const apiSort = sortStore.apiValue;
untrack(() => fontCatalogStore.setSort(apiSort));
const sort = sortStore.apiValue;
untrack(() => fontCatalogStore.setParams({ ...params, sort }));
});
});