Librechat-Mobile/core/data/CLAUDE.md

4.4 KiB

core:data

Room database, DataStore preferences, EncryptedSharedPreferences for tokens, and all repository implementations. This is the coordination layer between network and local storage.

What This Module Provides

  • Room database (db/LibreChatDatabase.kt): 6 entities, 6 DAOs, version 1, exportSchema = true.
  • Entities: ConversationEntity, MessageEntity, FileEntity, AgentEntity, PresetEntity, ConversationTagEntity. Complex fields (lists, nested objects) stored as JSON strings via Converters.
  • DAOs: ConversationDao, MessageDao, FileDao, AgentDao, PresetDao, ConversationTagDao. Read methods return Flow<T> for reactive observation.
  • DataStore: ServerDataStore (server URL prefs), SettingsDataStore (user preferences).
  • Token storage (datastore/TokenDataStore.kt): Implements TokenManager from :core:network using EncryptedSharedPreferences with AES-256. Uses Mutex to ensure only one refresh runs at a time when multiple 401s arrive concurrently.
  • Repositories (repository/): Interface + Impl for each domain: AuthRepository, ConversationRepository, MessageRepository, ChatRepository, FileRepository, AgentRepository, PresetRepository, PromptRepository, TagRepository, ShareRepository, ConfigRepository, UserRepository, SettingsRepository.
  • Entity mappers (db/mapper/EntityMapper.kt): Convert between Room entities and domain models.
  • Sync manager (sync/ConversationSyncManager.kt): Orchestrates cache invalidation.

Key Patterns

Repository: Interface + Impl

interface ConversationRepository {
    fun observeConversations(...): Flow<Result<List<Conversation>>>
    suspend fun updateTitle(id: String, title: String): Result<Conversation>
}

All impls take constructor parameters (api, dao, mapper, dispatcher) wired via Koin named qualifiers.

Read-Through Cache

  1. Emit cached data from Room immediately (first page).
  2. Fetch fresh data from network via API service.
  3. Upsert into Room. The Room Flow auto-emits the updated list.
  4. On network error, the cached data remains visible; error is surfaced separately.

Token Refresh with Mutex

private val refreshMutex = Mutex()
override suspend fun refreshAccessToken(): Boolean = refreshMutex.withLock {
    // Single refresh attempt, all concurrent callers wait on this lock
}

Wrap EncryptedSharedPreferences in try/catch -- some OEM devices have broken Keystore implementations. On KeyStoreException, clear tokens and force re-login.

DataModule Bindings

DataModule.kt defines a Koin module { } that binds repository interfaces to their implementations via singleOf(::Impl) bind Interface::class and provides the Room database, DAOs, and DataStore instances.

Room TypeConverters

Converters.kt handles JSON serialization for complex Room fields: List<String>, MessageContentPart lists, Feedback, FileReference lists, etc. Uses the same Json instance from the Koin graph.

Rules

  • Dependencies: :core:network, :core:model, :core:common, Room, DataStore, security-crypto, Ktor, kotlinx-serialization, Timber, Koin.
  • Convention plugins: librechat.mobile.library + librechat.mobile.koin + librechat.mobile.room + librechat.kotlin.serialization.
  • Repositories must use safeApiCall from :core:common for all network calls.
  • Entities are internal to this module -- feature modules work with domain models from :core:model.
  • All DAO read methods that the UI observes should return Flow, not suspend functions.

New Repositories (Round 2)

  • MemoryRepository / MemoryRepositoryImpl — wraps MemoriesApi for memory CRUD + preferences
  • McpRepository / McpRepositoryImpl — wraps McpApi for server CRUD, tools, connection status, reinitialize
  • Both bound in DataModule.kt via singleOf
  • Both use safeApiCall — no local caching (server is sole source of truth for memories and MCP)
  • Gotcha: Memory delete/update use key as identifier (not a separate ID field)
  • Gotcha: MCP server operations use serverName as identifier

New Repositories (Round 3)

  • BannerRepository / BannerRepositoryImpl — wraps BannerApi for fetching server banners
  • AgentRepository.getAgentsPaginated() — server-side paginated agent fetch, maps response to PaginatedAgents domain model
  • Both bound in DataModule.kt via singleOf, both use safeApiCall, no local caching