Account settings stop syncing after AuthedApp remount triggered by language loading #23

Closed
opened 2026-10-03 13:47:12 +00:00 by githubrepo · 1 comment

Summary

Account settings can stop being written to Stalwart after a language-catalog update remounts AuthedApp during initial settings loading. Reading settings.json succeeds, but subsequent changes are lost after reload or sign-out/sign-in.

Reproduction

  1. Use an account whose saved settings select a non-English language.
  2. Sign in without a cached language catalog/settings, so remote settings hydration triggers language loading.
  3. After the interface loads, change a synced setting, such as the theme.
  4. Wait beyond the settings-write debounce, then hard-refresh or sign out and back in.

Actual: the change can revert because settings writes were never armed.
Expected: changes are written to the account's settings file and survive reload and sign-in.

The trigger depends on the language-loading lifecycle; the component regression test deterministically covers the already-loaded remount path.

Environment and prior validation

  • Originally reproduced on ihasmail 2026.9.18+gc118184.
  • Self-hosted Stalwart 0.16.22; Docker deployment; Firefox.
  • Fresh Stalwart instance: not separately tested.
  • The patched deployment preserved language, theme and other settings after hard refresh and sign-out/sign-in. Its Docker build succeeded.

Root cause

settingsAlreadyLoadedFor(accountId) claims the account on the first mount. Remote settings hydration can load a language catalog, changing the key of the subtree containing AuthedApp. The first effect is canceled before reaching armSettingsSync(). The replacement mount takes the already-loaded early-return, which sets readiness but does not arm writes.

This early-return is still present on official upstream main at 42e1f4ca60 (checked 2026-10-03).

Proposed fix

Call armSettingsSync() immediately after setReady(true) in the settingsAlreadyLoadedFor(accountId) early-return in web/src/App.tsx. Keep the existing guard against reloading stale remote settings.

Add a component regression test asserting that this path arms writes without calling loadRemoteSettings() again; export AuthedApp to enable the test.

On previously tested upstream revision 23557a72a2 plus this change, the regression test passed. Removing only the added call made it fail with zero calls to armSettingsSync(); restoring the call made it pass again. Tests have not yet been rerun on the latest upstream revision.

## Summary Account settings can stop being written to Stalwart after a language-catalog update remounts AuthedApp during initial settings loading. Reading settings.json succeeds, but subsequent changes are lost after reload or sign-out/sign-in. ## Reproduction 1. Use an account whose saved settings select a non-English language. 2. Sign in without a cached language catalog/settings, so remote settings hydration triggers language loading. 3. After the interface loads, change a synced setting, such as the theme. 4. Wait beyond the settings-write debounce, then hard-refresh or sign out and back in. Actual: the change can revert because settings writes were never armed. Expected: changes are written to the account's settings file and survive reload and sign-in. The trigger depends on the language-loading lifecycle; the component regression test deterministically covers the already-loaded remount path. ## Environment and prior validation - Originally reproduced on ihasmail 2026.9.18+gc118184. - Self-hosted Stalwart 0.16.22; Docker deployment; Firefox. - Fresh Stalwart instance: not separately tested. - The patched deployment preserved language, theme and other settings after hard refresh and sign-out/sign-in. Its Docker build succeeded. ## Root cause settingsAlreadyLoadedFor(accountId) claims the account on the first mount. Remote settings hydration can load a language catalog, changing the key of the subtree containing AuthedApp. The first effect is canceled before reaching armSettingsSync(). The replacement mount takes the already-loaded early-return, which sets readiness but does not arm writes. This early-return is still present on official upstream main at 42e1f4ca60e1cdfda28206c4a68e7d5a355c0d4c (checked 2026-10-03). ## Proposed fix Call armSettingsSync() immediately after setReady(true) in the settingsAlreadyLoadedFor(accountId) early-return in web/src/App.tsx. Keep the existing guard against reloading stale remote settings. Add a component regression test asserting that this path arms writes without calling loadRemoteSettings() again; export AuthedApp to enable the test. On previously tested upstream revision 23557a72a2a72088081792f8ea0cabedea4e7bcb plus this change, the regression test passed. Removing only the added call made it fail with zero calls to armSettingsSync(); restoring the call made it pass again. Tests have not yet been rerun on the latest upstream revision.
jcoffey-dev self-assigned this 2026-10-03 22:18:46 +00:00
jcoffey-dev added the bug
priority
p1
labels 2026-10-05 03:10:46 +00:00
Owner

Thank you for the detailed report, @githubrepo. The root-cause analysis and the regression test made this one quick to track down.

Fixed in #29. I went one step further than the proposed one-liner: arming writes in the early-return could push the browser's settings over the account's file when the remount comes before the file has been read. So the settings load now belongs to the account rather than the mount. It runs once, every mount waits on it, and it arms writes when it finishes. In dev, React's StrictMode remount was hitting the same path, so there the file was never applied at all.

It will be in the next release. Thanks again!

Thank you for the detailed report, @githubrepo. The root-cause analysis and the regression test made this one quick to track down. Fixed in #29. I went one step further than the proposed one-liner: arming writes in the early-return could push the browser's settings over the account's file when the remount comes before the file has been read. So the settings load now belongs to the account rather than the mount. It runs once, every mount waits on it, and it arms writes when it finishes. In dev, React's StrictMode remount was hitting the same path, so there the file was never applied at all. It will be in the next release. Thanks again!
Sign in to join this conversation.
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: coffey-labs/ihasmail#23