Appearance
PreferencesManager
Manages user preferences storage and retrieval with validation and caching.
High-Level Design
User-Scoped, Not Account-Scoped
Preferences are stored in a dedicated user_preferences table (single-row JSON blob) that is not cleared on account reset. This means chart settings, onboarding progress, and pinned symbols survive challenge resets — they belong to the user, not to a trading account.
Partial Update with Silent Filtering
The core update pattern is designed for frontend convenience:
Client sends Partial<UserPreferences>
│
▼
validateUpdates() ── filters out invalid values silently
│ (wrong types, unknown symbols, bad intervals)
▼
Merge valid fields into current preferences via spread
│
▼
Return full updated preferences to client for reconciliationInvalid fields are silently dropped rather than causing errors. If every field in an update is invalid, the current preferences are returned unchanged. This makes the API tolerant of version skew between client and server.
In-Memory Cache
A cachedPreferences field avoids repeated SQLite reads. The cache is populated on first access and invalidated only via clearCache(). Since each DO instance serves a single user, there is no cache coherency concern.
Validation Rules
Each preference field has its own validation:
- pinnedSymbols: Must be valid
SymbolIdentifiers(checked against a Set), deduplicated, capped atMAX_PINNED_SYMBOLS(10) - Chart intervals: Must match one of the 8 valid intervals (
1m,5m,15m,30m,1h,4h,1d,1w), deduplicated, capped at 10 - chartType: Strict enum —
"candlestick"or"line" - Boolean fields:
typeof === "boolean"check (rejects truthy/falsy values) - profileDisplayPreference: Must be one of
"wallet","twitter","discord","telegram" - Malformed JSON in SQLite falls back to
DEFAULT_USER_PREFERENCESwith an error log
Social Preferences and Leaderboard Propagation
When showSocialInLeaderboard changes, the manager triggers challengeManager.sendLeaderboardUpdate() to propagate the visibility change to the global leaderboard. This is the only preference that has a side effect beyond local storage.
The parent DO also drives social preference changes externally:
completeSocialLinking()auto-setsprofileDisplayPreferenceto the linked providerunlinkSocialAccount()resetsprofileDisplayPreferenceto"wallet"
getSocialAccountPreferences() exposes only the social-related subset (profileDisplayPreference, showSocialInLeaderboard) for the ChallengeManager to build leaderboard entries.
Defaults
All preferences have sensible defaults (e.g., BTC/ETH/SOL/XRP/HYPE as pinned symbols, 5m chart interval, all display toggles on, all onboarding flags off). resetPreferences() restores the full default set.
See Also
- UserPaperTradePortfolio - Parent DO