refactor(helpers): modernize reactive helpers and add tests

This commit is contained in:
Ilia Mashkov
2026-03-02 22:18:59 +03:00
parent 594af924c7
commit ac73fd5044
12 changed files with 1117 additions and 185 deletions

View File

@@ -1,10 +1,66 @@
/**
* Reusable persistent storage utility using Svelte 5 runes
* Persistent localStorage-backed reactive state
*
* Automatically syncs state with localStorage.
* Creates reactive state that automatically syncs with localStorage.
* Values persist across browser sessions and are restored on page load.
*
* Handles edge cases:
* - SSR safety (no localStorage on server)
* - JSON parse errors (falls back to default)
* - Storage quota errors (logs warning, doesn't crash)
*
* @example
* ```ts
* // Store user preferences
* const preferences = createPersistentStore('user-prefs', {
* theme: 'dark',
* fontSize: 16,
* sidebarOpen: true
* });
*
* // Access reactive state
* $: currentTheme = preferences.value.theme;
*
* // Update (auto-saves to localStorage)
* preferences.value.theme = 'light';
*
* // Clear stored value
* preferences.clear();
* ```
*/
/**
* Creates a reactive store backed by localStorage
*
* The value is loaded from localStorage on initialization and automatically
* saved whenever it changes. Uses Svelte 5's $effect for reactive sync.
*
* @param key - localStorage key for storing the value
* @param defaultValue - Default value if no stored value exists
* @returns Persistent store with getter/setter and clear method
*
* @example
* ```ts
* // Simple value
* const counter = createPersistentStore('counter', 0);
* counter.value++;
*
* // Complex object
* interface Settings {
* theme: 'light' | 'dark';
* fontSize: number;
* }
* const settings = createPersistentStore<Settings>('app-settings', {
* theme: 'light',
* fontSize: 16
* });
* ```
*/
export function createPersistentStore<T>(key: string, defaultValue: T) {
// Initialize from storage or default
/**
* Load value from localStorage or return default
* Safely handles missing keys, parse errors, and SSR
*/
const loadFromStorage = (): T => {
if (typeof window === 'undefined') {
return defaultValue;
@@ -21,6 +77,7 @@ export function createPersistentStore<T>(key: string, defaultValue: T) {
let value = $state<T>(loadFromStorage());
// Sync to storage whenever value changes
// Wrapped in $effect.root to prevent memory leaks
$effect.root(() => {
$effect(() => {
if (typeof window === 'undefined') {
@@ -29,18 +86,27 @@ export function createPersistentStore<T>(key: string, defaultValue: T) {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
// Quota exceeded or privacy mode - log but don't crash
console.warn(`[createPersistentStore] Error saving ${key}:`, error);
}
});
});
return {
/**
* Current value (getter/setter)
* Changes automatically persist to localStorage
*/
get value() {
return value;
},
set value(v: T) {
value = v;
},
/**
* Remove value from localStorage and reset to default
*/
clear() {
if (typeof window !== 'undefined') {
localStorage.removeItem(key);
@@ -50,4 +116,7 @@ export function createPersistentStore<T>(key: string, defaultValue: T) {
};
}
/**
* Type representing a persistent store instance
*/
export type PersistentStore<T> = ReturnType<typeof createPersistentStore<T>>;