Migrate window-bounds persistence off the electron-window-state library into a WindowManager built-in `rememberBounds` capability, backed by the main-process persist cache (`window.bounds` key — its first real consumer). - New `windowBoundsTracker` free-function module: validates the stored record (including displayBounds), restores onto the display the window was last on (clamping into its work area, never resetting to primary), and snapshots at teardown via getNormalBounds + isMaximized. - Singleton-only gate (dev warning for non-singleton types). Runtime toggle `wm.setRememberBounds` (orthogonal to the registry flag; OFF drops only that type's slot) plus `wm.peekWindowBounds`. - Persist at three teardown exits: native close (singletons), before window.destroy() in destroyWindow (programmatic destroys), and a new onStop so shutdown writes land before CacheService flushes its persist map. - Wire Main + QuickAssistant. Main re-applies maximize consumer-side on its own show schedule (tray-on-launch defers to first show); remove electron-window-state and its orphaned keepers/constants/comments. Fullscreen is not persisted and old *-state.json is not migrated (one-time reset, loseable). Adds tracker/integration/persist tests, extends the main CacheService mock with persist methods, and documents the capability plus a breaking-change note.
10 KiB
Cache System Overview
Three-tier cache for regenerable data. In-process memory, cross-window shared state, and localStorage-backed persistence.
Scope
Use Cache for data that:
- Can be regenerated or lost without user impact
- Needs no backup or cross-device sync
- Has lifecycle tied to a component, window, or app session
For user settings use Preference; for business data use DataApi.
Tiers
| Tier | Scope | Survives restart | Authority | Use for |
|---|---|---|---|---|
| Memory | Per-process | No | Local to each process | Computed results, API responses |
| Shared | All renderer windows + Main | No | Main (relays + conflict sink) | Cross-window UI state |
| Persist (Renderer) | All renderer windows | Yes (localStorage) | Each renderer | Recent items, non-critical UI state |
| Persist (Main) | Main process only | Yes (JSON file) | Main | Loseable main-process state |
Persist has two independent stores. Each renderer persists to its own localStorage; Main persists to its own JSON file ({userData}/cache.json) exposed as getPersist / setPersist / hasPersist / deletePersist (plus subscribePersistChange) on the Main CacheService. The two never share data — Main cannot read renderer persist and vice versa. Separately, Main still relays renderer-origin CacheSyncMessage { type: 'persist' } between windows (it forwards them; it does not store the renderer's persist).
Reach for the Main persist tier last. It was the last tier added, for a deliberately narrow need: small, loseable, main-process-authoritative state that genuinely belongs nowhere else. Before choosing it, rule out the better-fitting systems first — a user setting belongs in Preference; cross-window or renderer-owned UI state belongs in Shared / renderer Persist; business data belongs in DataApi. In the vast majority of cases one of those is the right answer, so use Main persist only when the state is owned by the main process, regenerable, and has no home in any other system. See System Selection for the full decision guide.
Key Types
| Type | Example schema | Call site | Tiers |
|---|---|---|---|
| Fixed | 'app.user.avatar': string |
get('app.user.avatar') |
Memory / Shared / Persist |
| Template | 'scroll.position.${topicId}': number |
get('scroll.position.t42') |
Memory / Shared |
| Casual | (none — type argument only) | getCasual<T>('my.dynamic.key') |
Memory only |
Template keys share one default value across all instances — all web_search.provider.last_used_key.* fall back to ''. Casual keys are blocked at compile time from matching any schema pattern (UseCacheCasualKey in src/shared/data/cache/cacheSchemas.ts:393).
Design Invariants
Non-obvious rules the code enforces; assume them when designing consumers.
- Same-value write is a no-op. Equality via
lodash.isEqual. No broadcast, no subscriber fire, no hook re-render. (src/main/data/CacheService.tsisEqualguards beforebroadcastSync/ notifier) - TTL-only refresh does not fire subscribers. Updating
expireAton the same value is silent. - Subscribers fire only on explicit writes. Lazy TTL cleanup, the 10-min GC sweep, and
onStopdo not fire. - Hooks + TTL is discouraged.
useCache/useSharedCachelog a warn when the key has TTL (src/renderer/data/hooks/useCache.ts:186-192,289-295) — values can expire between renders. - Hooks pin cache entries.
registerHook/unregisterHookrefcount keys;delete/deleteSharedreturnfalsewhile any hook is active. - Persist presence means "overridden", not "stored". Both persist tiers (Main JSON + renderer localStorage) have no absent state —
getPersistalways returns the stored override or the schema default (never undefined).hasPersistreports whether the effective value differs from the default (i.e. has been overridden), anddeletePersistresets a key to its default rather than removing it. Keys are fixed by schema. Change subscription differs by process in API shape only: Main exposes a dedicatedsubscribePersistChange(main-local, same model assubscribeChange; never relayed to renderers), while the renderer routes persist changes through its unifiedsubscribe(key, cb). - TTL uses absolute
expireAt(Unix ms). Every process expires the same entry at the same instant, regardless of clock skew in IPC delivery. - Main-wins convergence. All cross-window shared writes are serialized through Main; on window init, Main-priority override applies to conflicts with the renderer's pre-sync copy.
- Re-entrant callbacks are safe. Subscribers may write back into the same key; the
isEqualshort-circuit terminates loops once the value stabilizes. Callback errors are caught and logged without skipping other subscribers. - Template placeholders are runtime-anonymous.
${providerId}and${foo}match identical concrete keys. Dynamic segments match[\w\-]+only — dots, colons, and non-ASCII are rejected (src/shared/data/cache/templateKey.ts:35-46).
Architecture
┌─────────────────────── Renderer Process ──────────────────────┐
│ useCache / useSharedCache / usePersistCache │
│ │ │
│ ▼ │
│ CacheService (Renderer) │
│ - Memory cache (local) │
│ - Shared cache (local copy; init-synced from Main) │
│ - Persist cache (localStorage, authoritative) │
└──────────────────────────┬────────────────────────────────────┘
│ IPC: Cache_Sync / Cache_GetAllShared
┌──────────────────────────▼────────────────────────────────────┐
│ CacheService (Main) │
│ - Internal cache (Main-only) │
│ - Shared cache (authoritative; relays to all windows) │
│ - Persist: own JSON store + relays renderer persist │
│ - subscribeChange / subscribeSharedChange for Main services │
└───────────────────────────────────────────────────────────────┘
Process Responsibilities
| Concern | Main | Renderer |
|---|---|---|
| Internal memory cache | Yes (services' own scratch space) | Yes (window-local) |
| Shared cache authority | Yes | Local copy; writes broadcast via IPC to Main |
| Persist cache storage | Yes (own JSON file, debounced 350ms, flush on stop); also relays renderer persist sync | Yes (localStorage, debounced 350ms, flush on unload) |
| Init sync for new windows | Serves getAllShared() |
Calls getAllShared() on startup |
subscribeChange / subscribeSharedChange |
Main-only API; template-aware | — |
| Hook refcounting | — | registerHook / unregisterHook |
| GC (10-min sweep of expired) | Yes | — |
API Reference
Renderer
| Method | Tier | Key type |
|---|---|---|
useCache / get / set / has / delete / hasTTL |
Memory | Fixed + Template |
getCasual / setCasual / hasCasual / deleteCasual / hasTTLCasual |
Memory | Dynamic only (schema keys blocked) |
useSharedCache / getShared / setShared / hasShared / deleteShared / hasSharedTTL |
Shared | Fixed + Template |
usePersistCache / getPersist / setPersist / hasPersist / deletePersist |
Persist | Fixed only |
isSharedCacheReady / onSharedCacheReady |
Shared | — |
getStats(includeDetails?: boolean) |
All | — |
Main
| Method | Tier | Key type |
|---|---|---|
get / set / has / delete |
Internal | Free-form string |
getShared / setShared / hasShared / deleteShared |
Shared | Fixed + Template |
getPersist / setPersist / hasPersist / deletePersist |
Persist (Main) | Fixed only |
subscribeChange<T>(key, cb) |
Internal | Exact key |
subscribeSharedChange<K>(key, cb) |
Shared | Fixed + Template (fires for every matching concrete instance) |
subscribePersistChange<K>(key, cb) |
Persist (Main) | Exact key (main-local) |
See Also
- Cache Usage — React hooks, direct API, patterns
- Cache Schema Guide — Adding fixed and template keys
- Source:
src/main/data/CacheService.ts,src/renderer/data/CacheService.ts,src/renderer/data/hooks/useCache.ts,src/shared/data/cache/