Skip to content

🔨 persist configuration through Jetpack DataStore with per-key updates - #5174

Open
guiyanakuang wants to merge 4 commits into
mainfrom
feature/datastore-config-persistence
Open

guiyanakuang wants to merge 4 commits into
mainfrom
feature/datastore-config-persistence

Conversation

@guiyanakuang

@guiyanakuang guiyanakuang commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

Summary

Closes #5100.

Replaces the single-document appConfig.json (DesktopConfigManager over OneFilePersist) with androidx.datastore:datastore-preferences-core while keeping the ConfigManager API every caller uses, so none of the ~80 updateConfig call sites change.

Persistence (app/src/commonMain/.../config)

  • ConfigKey / ConfigSchema: typed, named keys (booleanConfigKey("port"), ...). The shared AppConfig properties live in AppConfigKeys, desktop-only ones in DesktopConfigKeys.
  • ConfigStore: one DataStore preferences file. Its corruption handler quarantines an unparseable file to <name>.corrupt before falling back to empty preferences; if even that move fails the read error is surfaced and nothing is written, so a file that can be neither read nor preserved is never replaced.
  • ConfigRepository: routes each key to the store owning it, one transaction per store.
  • LegacyJsonConfigImporter: on first launch imports every known key of appConfig.json and renames it to appConfig.json.migrated; an unparseable document is quarantined to .corrupt and ignored.
  • DataStoreConfigManager: the common manager base. Each update is a DataStore transaction touching only the given keys and becomes visible in the StateFlow once it persisted; a failed write leaves file and snapshot unchanged and is reported (desktop: the existing failed_to_save_config notification). The saveBlocked flag is gone: a store that cannot be read only costs the session its values, every later write re-reads the file and can never overwrite it with defaults.

Separation of settings and runtime state

User settings go to appConfig.preferences_pb; lastPasteboardChangeCount, lastSeenChangelogVersion, networkBlockingDismissedFingerprint and linuxRemoteDesktopRestoreToken go to appState.preferences_pb (ConfigScope.RUNTIME_STATE), so exit-time bookkeeping no longer rewrites the user's settings file.

Desktop

  • DesktopAppConfig.copy(key, value) resolves keys through the schema instead of the 150-line hand-written when chain; an unknown key now throws instead of being silently ignored. DesktopAppConfig.fromPreferences is the single mapping from stored values to the snapshot.
  • DesktopConfigManager(configDir, localeUtils) builds both stores and the legacy importer; CrossPaste.bootstrap() passes the user directory.
  • The appInstanceId migration also reads the archived legacy document, so the identity fallback for a corrupt .metadata keeps working after the import.
  • ProGuard keep rule for DataStore's bundled protobuf-lite (mirrors the library's consumer rules).

CLI

The CLI located cli-endpoint.json through appConfig.json, which is now archived after the import. CliConfigReader decodes appConfig.preferences_pb instead: the file is DataStore's PreferenceMap proto, mirrored with kotlinx-serialization-protobuf exactly as DataStore's own non-JVM serializer does, so the CLI needs no DataStore dependency (which does not ship mingwX64). The legacy JSON stays as the fallback while an older app is installed.

Deviation from the issue

The issue places the code in shared/commonMain. DataStore publishes no mingwX64 artifact and shared compiles its commonMain for the Windows CLI target, so the implementation lives in app/src/commonMain (the directory shared with the mobile apps per CLAUDE.md) with the platform factory in desktopMain.

Test plan

  • ./gradlew app:desktopTest — DesktopConfigManagerTest (defaults, per-key and batch updates, persistence across instances, settings/state file separation, corrupt store quarantine, unreadable store never overwritten, legacy JSON import and archive, corrupt legacy JSON, wrong-typed legacy values, durable update and write-failure semantics), DesktopConfigKeysTest (data class, keys and fromPreferences stay in sync; runtime-state scope), DesktopAppMetadataMigrationTest (archived legacy fallback), updated DesktopAppConfigTest.
  • ./gradlew :cli:linuxX64Test (or the host target) — CliConfigReaderTest decodes a hand-encoded PreferenceMap, falls back to the legacy JSON, and defaults on an unreadable file.
  • Upgrade path on a real install: appConfig.json is imported on first launch, archived as appConfig.json.migrated, settings survive a restart, and crosspaste status still finds the endpoint of an app with a custom storage path.

claude and others added 3 commits October 8, 2026 20:35
Replace the single-document appConfig.json (DesktopConfigManager over
OneFilePersist) with androidx.datastore preferences, keeping the
ConfigManager API every caller uses:

- app/commonMain: ConfigKey/ConfigSchema (typed, named keys shared with
  mobile), ConfigStore (one DataStore file with a corruption handler that
  quarantines the unreadable file to <name>.corrupt before falling back to
  defaults), ConfigRepository (routes each key to the store owning it),
  LegacyJsonConfigImporter (ingests appConfig.json on first launch and
  archives it as appConfig.json.migrated) and DataStoreConfigManager, the
  common manager base class.
- Settings and runtime state (pasteboard change count, dismissed prompts,
  restore token, last seen changelog) live in separate files, so exit-time
  bookkeeping no longer rewrites the user's settings.
- Every update is a DataStore transaction touching only the given keys; a
  write that fails leaves file and snapshot unchanged and is reported. The
  saveBlocked flag is gone: an unreadable file only costs the session its
  values, later writes re-read it and never overwrite it with defaults.
- DesktopAppConfig.copy(key, value) now resolves keys through the schema
  instead of a hand-written when chain; DesktopConfigKeysTest keeps the
  data class, the keys and fromPreferences in sync.
- The appInstanceId migration also reads the archived legacy config.

Closes #5100
The CLI locates cli-endpoint.json through the app's storage path, which it
used to read from appConfig.json. That document is now imported into
DataStore and archived on the first launch, so CliConfigReader decodes
appConfig.preferences_pb instead: the file is DataStore's PreferenceMap
proto, mirrored with kotlinx-serialization-protobuf exactly as DataStore's
own non-JVM serializer does, so the CLI needs no DataStore dependency. The
legacy JSON document remains the fallback while an older app is installed.
…fig-persistence

# Conflicts:
#	app/src/desktopMain/kotlin/com/crosspaste/config/DesktopAppConfig.kt
#	app/src/desktopTest/kotlin/com/crosspaste/i18n/GlobalCopywriterTest.kt

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactor configuration persistence to multiplatform Jetpack DataStore with atomic per-key updates

2 participants