Skip to content

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 reconciliation

Invalid 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 at MAX_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_PREFERENCES with 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-sets profileDisplayPreference to the linked provider
  • unlinkSocialAccount() resets profileDisplayPreference to "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