fix(shared): give createPersistentStore a destroy() to dispose its effect.root

The store created an $effect.root for the save-on-change sync but returned no
disposer, so the effect leaked for the life of the process — contradicting
the rule that $effect.root owners must expose destroy(). Capture and expose
the disposer.

- add destroy() to the returned store; covered by tests (flushSync proves the
  save effect runs before destroy and stops after)
- trim the bloated header (two near-duplicate @example blocks) to one concise
  JSDoc — no fluff
- update typographySettings test mocks to satisfy the now-required destroy()

Consumers (LayoutManager, ThemeManager, typographySettings, comparisonStore)
do not yet call it — threading + the createSingleton migration follow.
This commit is contained in:
Ilia Mashkov
2026-06-03 10:00:21 +03:00
parent f0736f4d35
commit ded9606c30
3 changed files with 62 additions and 61 deletions
@@ -1,6 +1,7 @@
/**
* @vitest-environment jsdom
*/
import { flushSync } from 'svelte';
import {
afterEach,
beforeEach,
@@ -376,4 +377,39 @@ describe('createPersistentStore', () => {
expect(store.value[0].name).toBe('First');
});
});
describe('Lifecycle', () => {
it('persists value changes via the sync effect', () => {
const store = createPersistentStore(testKey, 'a');
const spy = vi.spyOn(mockLocalStorage, 'setItem');
store.value = 'b';
flushSync();
expect(spy).toHaveBeenCalledWith(testKey, JSON.stringify('b'));
});
it('stops persisting after destroy()', () => {
const store = createPersistentStore(testKey, 'a');
flushSync();
store.destroy();
const spy = vi.spyOn(mockLocalStorage, 'setItem');
store.value = 'c';
flushSync();
expect(spy).not.toHaveBeenCalled();
// reading still works after disposal
expect(store.value).toBe('c');
});
it('destroy() is safe to call repeatedly', () => {
const store = createPersistentStore(testKey, 'a');
expect(() => {
store.destroy();
store.destroy();
}).not.toThrow();
});
});
});