From fe07c60dd43f4b801e22cc95dc8c2a48baa153d8 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Wed, 3 Jun 2026 10:22:41 +0300 Subject: [PATCH] refactor: adopt createSingleton across the remaining stores Replace the hand-rolled let _x / getX / __resetX boilerplate with the createSingleton helper in all nine remaining singleton stores. Exposed accessor names (getX, __resetX) are unchanged, so consumers and specs are unaffected. Teardown wired to each stores destroy() where it has one (fontCatalog, fontLifecycle, typography, availableFilter, theme, layout, scrollBreadcrumbs); sort and appliedFilter have no teardown. Also merges layoutStores duplicate $shared/lib imports. --- .../fontCatalogStore.svelte.ts | 15 +++++++------ .../fontLifecycleManager.svelte.ts | 17 +++++++-------- .../typographySettingsStore.svelte.ts | 20 +++++++----------- .../store/scrollBreadcrumbsStore.svelte.ts | 18 ++++++++-------- .../store/ThemeManager/ThemeManager.svelte.ts | 18 +++++++--------- .../appliedFilterStore.svelte.ts | 21 ++++++++++--------- .../availableFilterStore.svelte.ts | 17 +++++++-------- .../model/store/sortStore/sortStore.svelte.ts | 14 ++++++------- .../stores/layoutStore/layoutStore.svelte.ts | 19 ++++++++--------- 9 files changed, 74 insertions(+), 85 deletions(-) diff --git a/src/entities/Font/model/store/fontCatalogStore/fontCatalogStore.svelte.ts b/src/entities/Font/model/store/fontCatalogStore/fontCatalogStore.svelte.ts index ceff630..4d2b96c 100644 --- a/src/entities/Font/model/store/fontCatalogStore/fontCatalogStore.svelte.ts +++ b/src/entities/Font/model/store/fontCatalogStore/fontCatalogStore.svelte.ts @@ -3,6 +3,7 @@ import { DEFAULT_QUERY_STALE_TIME_MS, getQueryClient, } from '$shared/api/queryClient'; +import { createSingleton } from '$shared/lib/helpers/createSingleton/createSingleton'; import { type InfiniteData, InfiniteQueryObserver, @@ -483,14 +484,12 @@ export class FontCatalogStore { } } -let _catalog: FontCatalogStore | undefined; +const catalog = createSingleton( + () => new FontCatalogStore({ limit: 50 }), + instance => instance.destroy(), +); -export function getFontCatalog(): FontCatalogStore { - return (_catalog ??= new FontCatalogStore({ limit: 50 })); -} +export const getFontCatalog = catalog.get; // test-only reset, so specs don't share a live observer -export function __resetFontCatalog() { - _catalog?.destroy(); - _catalog = undefined; -} +export const __resetFontCatalog = catalog.reset; diff --git a/src/entities/Font/model/store/fontLifecycleManager/fontLifecycleManager.svelte.ts b/src/entities/Font/model/store/fontLifecycleManager/fontLifecycleManager.svelte.ts index f159a82..3a19af5 100644 --- a/src/entities/Font/model/store/fontLifecycleManager/fontLifecycleManager.svelte.ts +++ b/src/entities/Font/model/store/fontLifecycleManager/fontLifecycleManager.svelte.ts @@ -1,3 +1,4 @@ +import { createSingleton } from '$shared/lib/helpers/createSingleton/createSingleton'; import { SvelteMap } from 'svelte/reactivity'; import { type FontLoadRequestConfig, @@ -419,18 +420,16 @@ export class FontLifecycleManager { } } -let _fontLifecycleManager: FontLifecycleManager | undefined; - /** * App-wide font lifecycle manager, created on first access. Lazy so its * AbortController / FontFace bookkeeping isn't set up at module load. */ -export function getFontLifecycleManager(): FontLifecycleManager { - return (_fontLifecycleManager ??= new FontLifecycleManager()); -} +const fontLifecycleManager = createSingleton( + () => new FontLifecycleManager(), + instance => instance.destroy(), +); + +export const getFontLifecycleManager = fontLifecycleManager.get; // test-only reset, so specs don't share loaded-font/eviction state -export function __resetFontLifecycleManager() { - _fontLifecycleManager?.destroy(); - _fontLifecycleManager = undefined; -} +export const __resetFontLifecycleManager = fontLifecycleManager.reset; diff --git a/src/features/AdjustTypography/model/store/typographySettingsStore/typographySettingsStore.svelte.ts b/src/features/AdjustTypography/model/store/typographySettingsStore/typographySettingsStore.svelte.ts index 9960c9f..dff832c 100644 --- a/src/features/AdjustTypography/model/store/typographySettingsStore/typographySettingsStore.svelte.ts +++ b/src/features/AdjustTypography/model/store/typographySettingsStore/typographySettingsStore.svelte.ts @@ -19,6 +19,7 @@ import { import { type PersistentStore, createPersistentStore, + createSingleton, } from '$shared/lib'; import type { NumericControl } from '$shared/ui'; import { SvelteMap } from 'svelte/reactivity'; @@ -349,22 +350,17 @@ export function createTypographySettingsStore( export type TypographySettingsStoreInstance = ReturnType; -let _typographySettingsStore: TypographySettingsStoreInstance | undefined; - /** * App-wide typography settings store, keyed for the comparison view. * Created on first access so its persistent-store sync effects aren't set up * at module load. */ -export function getTypographySettingsStore(): TypographySettingsStoreInstance { - return (_typographySettingsStore ??= createTypographySettingsStore( - DEFAULT_TYPOGRAPHY_CONTROLS_DATA, - COMPARISON_STORAGE_KEY, - )); -} +const typographySettingsStore = createSingleton( + () => createTypographySettingsStore(DEFAULT_TYPOGRAPHY_CONTROLS_DATA, COMPARISON_STORAGE_KEY), + instance => instance.destroy(), +); + +export const getTypographySettingsStore = typographySettingsStore.get; // test-only reset, so specs don't share persisted typography state or leak effects -export function __resetTypographySettingsStore() { - _typographySettingsStore?.destroy(); - _typographySettingsStore = undefined; -} +export const __resetTypographySettingsStore = typographySettingsStore.reset; diff --git a/src/features/Breadcrumb/model/store/scrollBreadcrumbsStore.svelte.ts b/src/features/Breadcrumb/model/store/scrollBreadcrumbsStore.svelte.ts index 4183428..a44906e 100644 --- a/src/features/Breadcrumb/model/store/scrollBreadcrumbsStore.svelte.ts +++ b/src/features/Breadcrumb/model/store/scrollBreadcrumbsStore.svelte.ts @@ -1,3 +1,5 @@ +import { createSingleton } from '$shared/lib/helpers/createSingleton/createSingleton'; + /** * Scroll-based breadcrumb tracking store * @@ -279,17 +281,15 @@ export function createScrollBreadcrumbsStore(): ScrollBreadcrumbsStore { return new ScrollBreadcrumbsStore(); } -let _scrollBreadcrumbsStore: ScrollBreadcrumbsStore | undefined; - /** * App-wide scroll breadcrumbs store, created on first access. */ -export function getScrollBreadcrumbsStore(): ScrollBreadcrumbsStore { - return (_scrollBreadcrumbsStore ??= createScrollBreadcrumbsStore()); -} +const scrollBreadcrumbsStore = createSingleton( + () => createScrollBreadcrumbsStore(), + instance => instance.destroy(), +); + +export const getScrollBreadcrumbsStore = scrollBreadcrumbsStore.get; // test-only reset, so specs don't share observer/scroll state -export function __resetScrollBreadcrumbsStore() { - _scrollBreadcrumbsStore?.destroy(); - _scrollBreadcrumbsStore = undefined; -} +export const __resetScrollBreadcrumbsStore = scrollBreadcrumbsStore.reset; diff --git a/src/features/ChangeAppTheme/model/store/ThemeManager/ThemeManager.svelte.ts b/src/features/ChangeAppTheme/model/store/ThemeManager/ThemeManager.svelte.ts index 78ba77d..d3f6584 100644 --- a/src/features/ChangeAppTheme/model/store/ThemeManager/ThemeManager.svelte.ts +++ b/src/features/ChangeAppTheme/model/store/ThemeManager/ThemeManager.svelte.ts @@ -28,7 +28,10 @@ * ``` */ -import { createPersistentStore } from '$shared/lib'; +import { + createPersistentStore, + createSingleton, +} from '$shared/lib'; export const STORAGE_KEY = 'glyphdiff:theme'; @@ -195,23 +198,18 @@ class ThemeManager { } } -let _themeManager: ThemeManager | undefined; - /** * App-wide theme manager, created on first access. * * Lazy so its persistent-store subscription isn't set up at module load. * Call init() on mount and destroy() on unmount (see Layout). */ -export function getThemeManager(): ThemeManager { - return (_themeManager ??= new ThemeManager()); -} +const themeManager = createSingleton(() => new ThemeManager(), instance => instance.destroy()); + +export const getThemeManager = themeManager.get; // test-only reset, so specs don't share persisted theme state -export function __resetThemeManager() { - _themeManager?.destroy(); - _themeManager = undefined; -} +export const __resetThemeManager = themeManager.reset; /** * ThemeManager class exported for testing purposes diff --git a/src/features/FilterAndSortFonts/model/store/appliedFilterStore/appliedFilterStore.svelte.ts b/src/features/FilterAndSortFonts/model/store/appliedFilterStore/appliedFilterStore.svelte.ts index 522dd3d..c0bee23 100644 --- a/src/features/FilterAndSortFonts/model/store/appliedFilterStore/appliedFilterStore.svelte.ts +++ b/src/features/FilterAndSortFonts/model/store/appliedFilterStore/appliedFilterStore.svelte.ts @@ -23,7 +23,10 @@ * ``` */ -import { createFilter } from '$shared/lib'; +import { + createFilter, + createSingleton, +} from '$shared/lib'; import { createDebouncedState } from '$shared/lib/helpers'; import type { FilterConfig, @@ -129,8 +132,6 @@ export function createAppliedFilterStore(config: FilterCo export type AppliedFilterStore = ReturnType; -let _appliedFilterStore: AppliedFilterStore | undefined; - /** * App-wide filter manager, created on first access. * @@ -138,14 +139,14 @@ let _appliedFilterStore: AppliedFilterStore | undefined; * lives in `./bindings.svelte` and populates groups once backend filter * metadata arrives. */ -export function getAppliedFilterStore(): AppliedFilterStore { - return (_appliedFilterStore ??= createAppliedFilterStore({ +const appliedFilterStore = createSingleton(() => + createAppliedFilterStore({ queryValue: '', groups: [], - })); -} + }) +); + +export const getAppliedFilterStore = appliedFilterStore.get; // test-only reset, so specs don't share filter/selection state -export function __resetAppliedFilterStore() { - _appliedFilterStore = undefined; -} +export const __resetAppliedFilterStore = appliedFilterStore.reset; diff --git a/src/features/FilterAndSortFonts/model/store/availableFilterStore/availableFilterStore.svelte.ts b/src/features/FilterAndSortFonts/model/store/availableFilterStore/availableFilterStore.svelte.ts index 02d2c3f..f11b276 100644 --- a/src/features/FilterAndSortFonts/model/store/availableFilterStore/availableFilterStore.svelte.ts +++ b/src/features/FilterAndSortFonts/model/store/availableFilterStore/availableFilterStore.svelte.ts @@ -22,6 +22,7 @@ import { DEFAULT_QUERY_STALE_TIME_MS, getQueryClient, } from '$shared/api/queryClient'; +import { createSingleton } from '$shared/lib/helpers/createSingleton/createSingleton'; import { type QueryKey, QueryObserver, @@ -126,18 +127,16 @@ export class AvailableFilterStore { } } -let _availableFilterStore: AvailableFilterStore | undefined; - /** * App-wide filter-metadata store, created on first access. Lazy so the * QueryObserver isn't constructed at module load. */ -export function getAvailableFilterStore(): AvailableFilterStore { - return (_availableFilterStore ??= new AvailableFilterStore()); -} +const availableFilterStore = createSingleton( + () => new AvailableFilterStore(), + instance => instance.destroy(), +); + +export const getAvailableFilterStore = availableFilterStore.get; // test-only reset, so specs don't share a live observer -export function __resetAvailableFilterStore() { - _availableFilterStore?.destroy(); - _availableFilterStore = undefined; -} +export const __resetAvailableFilterStore = availableFilterStore.reset; diff --git a/src/features/FilterAndSortFonts/model/store/sortStore/sortStore.svelte.ts b/src/features/FilterAndSortFonts/model/store/sortStore/sortStore.svelte.ts index 5d2a8ca..b748471 100644 --- a/src/features/FilterAndSortFonts/model/store/sortStore/sortStore.svelte.ts +++ b/src/features/FilterAndSortFonts/model/store/sortStore/sortStore.svelte.ts @@ -1,3 +1,5 @@ +import { createSingleton } from '$shared/lib/helpers/createSingleton/createSingleton'; + /** * Sort store — manages the current sort option for font listings. * @@ -46,16 +48,12 @@ export function createSortStore(initial: SortOption = 'Popularity') { export type SortStore = ReturnType; -let _sortStore: SortStore | undefined; - /** * App-wide sort store, created on first access. */ -export function getSortStore(): SortStore { - return (_sortStore ??= createSortStore()); -} +const sortStore = createSingleton(() => createSortStore()); + +export const getSortStore = sortStore.get; // test-only reset, so specs don't share selection state -export function __resetSortStore() { - _sortStore = undefined; -} +export const __resetSortStore = sortStore.reset; diff --git a/src/widgets/SampleList/model/stores/layoutStore/layoutStore.svelte.ts b/src/widgets/SampleList/model/stores/layoutStore/layoutStore.svelte.ts index f9c8189..ea25a68 100644 --- a/src/widgets/SampleList/model/stores/layoutStore/layoutStore.svelte.ts +++ b/src/widgets/SampleList/model/stores/layoutStore/layoutStore.svelte.ts @@ -16,8 +16,11 @@ * - Desktop Large (>= 1280px): 4 columns */ -import { createPersistentStore } from '$shared/lib'; -import { responsiveManager } from '$shared/lib'; +import { + createPersistentStore, + createSingleton, + responsiveManager, +} from '$shared/lib'; export type LayoutMode = 'list' | 'grid'; @@ -153,20 +156,16 @@ class LayoutManager { } } -let _layoutManager: LayoutManager | undefined; - /** * App-wide layout manager, created on first access. Lazy so its persisted * layout preference isn't read at module load. */ -export function getLayoutManager(): LayoutManager { - return (_layoutManager ??= new LayoutManager()); -} +const layoutManager = createSingleton(() => new LayoutManager(), instance => instance.destroy()); + +export const getLayoutManager = layoutManager.get; // test-only reset, so specs don't share persisted layout state -export function __resetLayoutManager() { - _layoutManager = undefined; -} +export const __resetLayoutManager = layoutManager.reset; // Export class for testing purposes export { LayoutManager };