fix(translate): replace fromStore+ with +subscribe to prevent reactive loop, add runComplete flag, show error_message, collapsible run results
- TranslationRunGlobalIndicator: replace fromStore + 6x with (() => store.subscribe()) to prevent accumulating render_effect instances via createSubscriber that caused infinite Svelte reactive flush - translate/[id]/+page.svelte: add runComplete flag to hide progress bar after completion without touching the store; add collapsible run detail cards with status badge, counts, and error_message; filter invalid runs - translationRun.js: remove stale guard (no longer needed) - Sidebar.svelte: fix mobile overlay close on route change - docs/adr/ADR-0007-rejected-fromStore-derived.md: document REJECTED pattern
This commit is contained in:
61
docs/adr/ADR-0007-rejected-fromStore-derived.md
Normal file
61
docs/adr/ADR-0007-rejected-fromStore-derived.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# ADR-0007: REJECTED — `fromStore` + multiple `$derived` in Svelte 5
|
||||
|
||||
## Status
|
||||
REJECTED — pattern forbidden in ss-tools frontend.
|
||||
|
||||
## Context
|
||||
`fromStore(store)` returns a reactive object whose `.current` property
|
||||
reflects the underlying Svelte writable store. Using it with multiple
|
||||
`$derived(runState.current?.xxx)` values creates persistent `render_effect`
|
||||
instances via Svelte's `createSubscriber` on every reactive re-evaluation.
|
||||
|
||||
### Observed failure (ss-tools, May 2026)
|
||||
`TranslationRunGlobalIndicator` used 6 `$derived` reading `runState.current`.
|
||||
Each `$derived` re-evaluation (triggered by store update) called `subscribe()`
|
||||
which called `render_effect()` inside `createSubscriber`. The `render_effect`
|
||||
teardown queues a microtask that decrements the subscriber count. When all
|
||||
teardowns execute, subscriber count drops to zero, which triggers `increment(version)`,
|
||||
which marks all `$derived` as dirty again — creating an **infinite reactive flush loop**.
|
||||
|
||||
### Symptoms
|
||||
- After ~300 store poll cycles, the browser tab reaches 100% CPU on 2 cores.
|
||||
- Svelte runtime (`flush` → `Ve` → `yn` → `fn`) throws `Uncaught` repeatedly.
|
||||
- `CTFFznDe.js` chunk enters infinite loop.
|
||||
- Page becomes completely unresponsive.
|
||||
|
||||
## Decision
|
||||
`fromStore` MUST NOT be used with `$derived` when the `.current` object is
|
||||
read by more than one `$derived` in the same component.
|
||||
|
||||
Use `$effect(() => store.subscribe(s => state = s))` instead — this creates
|
||||
exactly ONE subscription per component lifecycle, with no accumulating
|
||||
`render_effect` instances.
|
||||
|
||||
## Consequences
|
||||
### Positive
|
||||
- Store subscription is created once per component mount and cleaned up on destroy.
|
||||
- No accumulating `render_effect` instances → no infinite reactive loop.
|
||||
- Works correctly in Svelte 5.43.x.
|
||||
|
||||
### Negative
|
||||
- Requires explicit `$state` initial value and `$effect` subscription.
|
||||
- Slightly more verbose than `fromStore`.
|
||||
|
||||
## Migration
|
||||
Replace:
|
||||
```javascript
|
||||
const state = fromStore(store);
|
||||
let foo = $derived(state.current?.foo || null);
|
||||
let bar = $derived(state.current?.bar || 0);
|
||||
```
|
||||
|
||||
With:
|
||||
```javascript
|
||||
let _state = $state(initialValue);
|
||||
$effect(() => {
|
||||
const unsub = store.subscribe(s => { _state = s; });
|
||||
return () => unsub();
|
||||
});
|
||||
let foo = $derived(_state?.foo || null);
|
||||
let bar = $derived(_state?.bar || 0);
|
||||
```
|
||||
Reference in New Issue
Block a user