Files
ss-tools/docs/adr/ADR-0007-rejected-fromStore-derived.md
busya ec6421de35 rename ss-tools to superset-tools across the entire project
- Replace all occurrences of 'ss-tools' with 'superset-tools' in 104 files
- Rename git bundle file ss-tools.bundle → superset-tools.bundle
- Update .gitignore pattern accordingly
- Preserve variable names (hasSsTools etc.) and code identifiers
2026-06-16 11:15:19 +03:00

2.9 KiB

[DEF:ADR-0007:ADR]

@STATUS REJECTED

@PURPOSE Document the decision to forbid fromStore + multiple $derived in Svelte 5 components due to infinite reactive flush loop.

@RELATION DEPENDS_ON -> [ADR-0006:ADR]

@RATIONALE fromStore(store) returns a reactive object with .current property. Using it with multiple $derived creates persistent render_effect instances on every reactive re-evaluation, leading to an infinite flush loop and 100% CPU usage after ~300 poll cycles.

@REJECTED fromStore with multiple $derived in the same component — causes infinite reactive flush loop in Svelte 5 runtime.

ADR-0007: REJECTED — fromStore + multiple $derived in Svelte 5

Status

REJECTED — pattern forbidden in superset-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 (superset-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 (flushVeynfn) 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:

const state = fromStore(store);
let foo = $derived(state.current?.foo || null);
let bar = $derived(state.current?.bar || 0);

With:

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);

[/DEF:ADR-0007:ADR]