Pinia Store Architecture in RomM v2: Modular State Management for High-Performance Galleries
RomM v2 uses four specialized Pinia stores—v2GalleryRoms, v2GallerySelection, v2ScrollRestoration, and v2FocusRestoration—to manage sparse window-based ROM caching, multi-selection state, and UI restoration in a modular, high-performance architecture.
The frontend rewrite in RomM v2 (available at rommapp/romm) replaces the monolithic state management of v1 with a targeted Pinia store architecture. These stores handle distinct concerns—gallery data, selection, scroll position, and focus—enabling efficient virtualized grids and seamless navigation. This architecture leverages Vue 3's reactivity while maintaining strict separation from legacy v1 stores.
Core Pinia Stores in RomM v2
v2GalleryRoms – Sparse ROM Cache and Gallery Context
Located in frontend/src/v2/stores/galleryRoms.ts, this store maintains a sparse, window-based cache of ROMs rather than loading entire collections. It tracks the current gallery context (platform, collection, virtual collection, smart collection, or search) and manages pagination via charIndex and romIdIndex.
Core state includes:
currentPlatform,currentCollection,currentVirtualCollection,currentSmartCollection,currentSearchbyPosition: AMap<number, SimpleRom>linking positions to ROM dataloadedWindows,pendingWindows,failedWindows: Sets tracking fetch statusorderBy,orderDir: Sorting parameters
Key actions:
setCurrentPlatform(),setCurrentCollection(),setOrderBy(): Context switchers that triggerresetGallery()andinvalidateWindows()fetchInitialMetadata(): Retrieves total counts and filter indicesfetchWindowAt(position),fetchRomAt(position): On-demand data loading withWINDOW_SIZE = 72cancelFetchAt(): UsesAbortControllerinstances stored ininFlightControllersto cancel stale requests
v2GallerySelection – Multi-Select State Management
Defined in frontend/src/v2/stores/gallerySelection.ts, this store handles multi-selection independently from the ROM cache. It stores full SimpleRom objects in a Map<id, SimpleRom> to avoid re-lookup after window eviction.
State:
selected: Map of ROM IDs to objectslastSelectedPosition: Anchor for shift-click range selections
Actions:
toggle(rom, position): Single selection toggletoggleRange(position, getRomAt): Shift-click range selection using the anchorselectAllLoaded(),clear(),removeIds()
Getters:
enabled,count,roms,ids
v2ScrollRestoration – Route-Aware Scroll Persistence
This store (frontend/src/v2/stores/scrollRestoration.ts) persists scroll positions for virtual scrollers across route changes.
State:
positions: Map<string, number>(routeFullPath → scrollTop)
Actions:
save(routeFullPath, scrollTop): Stores position before navigationrestore(routeFullPath): Returns saved position or nullclear(routeFullPath): Removes specific entry
v2FocusRestoration – Keyboard Navigation State
Found in frontend/src/v2/stores/focusRestoration.ts, this enables gamepad and keyboard navigation to resume after route changes.
State:
keys: Map<string, string>(routeFullPath → focusKey)
Actions:
save(routeFullPath, focusKey): Stores last focused tilerestore(routeFullPath): Retrieves focus key for re-focus
Store Interactions and Data Flow
The stores communicate through well-defined actions rather than direct state mutation:
-
Gallery Context Changes: When users switch platforms or collections,
v2GalleryRomsactions likesetCurrentPlatform()reset the cache viaresetGallery()and fetch initial metadata throughfetchInitialMetadata(). -
Virtual Scrolling: Components call
getRomAt(position)fromv2GalleryRoms. If data is missing, they triggerfetchRomAt(position)orfetchWindowAt(position), which updates thebyPositionMap reactively. -
Selection Handling: Tile components invoke
v2GallerySelection.toggle(rom, position)on click. For range selections,toggleRange(position, galleryRoms.getRomAt)uses the ROM store's getter to fetch boundary items. -
Navigation Restoration: Before route leave,
scrollRestoration.save()andfocusRestoration.save()cache UI state. On mount,restore()methods reinitialize the scroller and focus target. -
Request Deduplication:
v2GalleryRomsmaintains aMap<string, AbortController>to track in-flight requests, ensuring rapid filter changes cancel outdated fetches viacancelFetchAt().
Implementation Details and Performance Optimizations
Windowed Caching Strategy: Instead of loading thousands of ROMs, v2GalleryRoms fetches windows of 72 items (matching the backend pagination limit). This reduces initial load time and memory pressure significantly.
Reactive Map Usage: Both byPosition and selected use native JavaScript Map objects. Vue 3's reactive Map support ensures component updates trigger only for specific key changes, crucial for virtualized grids rendering hundreds of cells.
Abort Controller Management: The store centralizes network cancellation through inFlightControllers. When fetchWindowAt() initiates a request, it stores an AbortController; subsequent calls to cancelFetchAt() or new fetches abort stale requests, preventing race conditions.
Immutable Store Boundaries: Selection stores retain full ROM objects to survive window eviction, while gallery stores maintain sparse references. This separation prevents selection loss during aggressive cache trimming.
Practical Code Examples
Accessing the ROM store and initializing gallery data:
import { useStore as useGalleryRoms } from '@/stores/roms'
const galleryRoms = useGalleryRoms()
onMounted(() => {
// Load first window and metadata (total count, filters)
galleryRoms.fetchInitialMetadata()
})
Lazy-loading ROM data in virtual scroller components:
function getRom(position: number) {
const rom = galleryRoms.getRomAt(position)
if (!rom) {
// Triggers fetch; store handles deduplication
galleryRoms.fetchRomAt(position)
}
return rom
}
Handling selection toggles in tile components:
import useGallerySelection from '@/stores/gallerySelection'
const selection = useGallerySelection()
function onTileClick(rom: SimpleRom, position: number) {
selection.toggle(rom, position)
}
Implementing shift-click range selection:
function onTileShiftClick(position: number) {
// galleryRoms.getRomAt passed as resolver
selection.toggleRange(position, (p) => galleryRoms.getRomAt(p))
}
Saving scroll position before route navigation:
import useScrollRestoration from '@/stores/scrollRestoration'
function onBeforeRouteLeave(to, from, next) {
const container = document.querySelector('.gallery-scroller') as HTMLElement
useScrollRestoration().save(from.fullPath, container.scrollTop)
next()
}
onMounted(() => {
const saved = useScrollRestoration().restore(route.fullPath)
if (saved !== null) {
const container = document.querySelector('.gallery-scroller') as HTMLElement
container.scrollTop = saved
}
})
Persisting and restoring focus for keyboard navigation:
import useFocusRestoration from '@/stores/focusRestoration'
function onTileFocus(e: FocusEvent) {
const focusKey = (e.target as HTMLElement).dataset.focusKey
if (focusKey) useFocusRestoration().save(route.fullPath, focusKey)
}
onMounted(() => {
const key = useFocusRestoration().restore(route.fullPath)
if (key) {
const el = document.querySelector(`[data-focus-key="${key}"]`) as HTMLElement
el?.focus()
}
})
Summary
- Four specialized stores (
v2GalleryRoms,v2GallerySelection,v2ScrollRestoration,v2FocusRestoration) replace the monolithic v1 architecture inrommapp/romm - Sparse window-based caching in
v2GalleryRomsloads only 72-item windows on demand, minimizing memory usage - Independent selection state stores full ROM objects to survive cache eviction and enable shift-click range selection
- Navigation restoration persists scroll positions and focus keys per route using simple Map-based stores
- AbortController integration prevents race conditions during rapid gallery context switches
- Vue 3 Map reactivity ensures precise component updates without full re-renders in virtualized grids
Frequently Asked Questions
How does v2GalleryRoms handle pagination without loading all ROMs?
The store implements a sparse cache using a Map<number, SimpleRom> called byPosition. It fetches data in 72-item windows via fetchWindowAt() when the virtual scroller requests specific positions. This windowed approach, defined in frontend/src/v2/stores/galleryRoms.ts, keeps memory usage constant regardless of collection size.
Why does v2GallerySelection store full ROM objects instead of just IDs?
Storing complete SimpleRom objects in the selected Map ensures selections persist even when v2GalleryRoms evicts windows from its sparse cache. This decoupling prevents selection loss during aggressive memory management and eliminates the need to re-fetch ROM metadata for the selection bar.
How does RomM v2 cancel outdated API requests when switching galleries quickly?
The v2GalleryRoms store maintains an inFlightControllers Map that tracks AbortController instances for each pending fetch. When contexts change rapidly, cancelFetchAt() or new fetch actions abort previous requests, preventing race conditions and reducing server load.
What is the difference between v2 stores and the legacy v1 stores?
Stores prefixed with v2 (located in frontend/src/v2/stores/) belong to the rewrite and follow a modular, domain-specific architecture. Legacy v1 stores remain frozen in frontend/src/stores/ for backward compatibility. The v2 naming convention allows both implementations to coexist during migration, with v2 stores specifically optimized for virtualized galleries and windowed data fetching.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →