Keep a settings change made before the first read, and wait for it #162

Closed
opened 2026-08-31 22:09:27 +00:00 by jcoffey-dev · 0 comments
Owner

Two defects on the path that decides what language the app starts in. Both were found chasing "the toast is still English", and one of them answers "do we need to tick this is my own device?" — no, and unticked is the worse case.

loadJson opens with if (!trusted) return fallback. With the box unticked the settings cache is never read or written, so the app boots on the defaults — uiLanguage: "en"every time. Ticking it only helps on a return visit; the first sign-in after a deploy has no cache either, because sign-out clears it and every deploy signs everyone out.

A change made before the settings file came back was thrown away

queueSettingsPush returned early while unarmed, dropping the value instead of holding it:

- if (!armed || !settingsSyncAvailable()) return;
- pending = synced;
+ if (!settingsSyncAvailable()) return;
+ pending = synced;
+ if (!armed) return;   // kept; armSettingsSync writes whatever is waiting

So a language picked in the second or so after a page load was never written up: it survived until the next reload and no further. That is a better account of "sometimes it takes several clicks" than the remount race fixed in #160 — the click that stuck was one made after the read had finished.

Keeping the value is safe precisely because #160 taught hydrate to refuse to overwrite a key that is still queued, so the newer local change wins over the older file rather than racing it.

Evidence it was real: before this change no ihasmail folder was ever created in the account's files, because even the seed write never fired. After it, the folder appears.

Without a cached settings object the tree painted too early

The first frame was the defaults, and the defaults are English. Anything computed in that window is computed in the wrong language. The interface recovers — it is rebuilt when the catalogue lands — but a string emitted once does not, which is exactly what a toast is:

boot (no cache) → uiLanguage "en" → render
                → MailView effect fires → toast, in English
                → settings file lands → "es" → tree rebuilds
                → everything Spanish except the toast already on screen

So without a cache the authenticated tree now waits for the account's settings and their catalogue. That costs nothing: there was nothing worth painting yet. With a cache it does not wait, and the first frame is as quick as it was — PAINTED_FROM_CACHE decides, and it is computed once at module load.

What I could not verify, and how to check it

The mock server does not persist the account settings file — it creates the ihasmail folder but never the settings.json inside it, and a file I wrote there by hand was not read back either. So locally the app always boots on defaults and stays there, which makes this whole class of bug invisible to local testing. (Worth fixing in the mock separately; it is why this took so long to pin down.)

Production does persist it — verified by John across sign-out/sign-in, with the box both ticked and unticked — so the fix needs checking there:

  1. set the interface language to something other than English
  2. sign out and back in
  3. open a URL for a folder that no longer exists

The toast should now be in that language. If it is still English, the cause is not this.

npx tsc --noEmit, npm run i18n:check, 109 node tests, 467 web tests, npm run build.

Merged 2026-08-31 as coffey-labs/ihasmail@2380ed282e

Rebuilt from: git history, session transcript.

Two defects on the path that decides what language the app starts in. Both were found chasing "the toast is still English", and one of them answers "do we need to tick *this is my own device*?" — **no, and unticked is the worse case**. `loadJson` opens with `if (!trusted) return fallback`. With the box unticked the settings cache is never read *or* written, so the app boots on the defaults — `uiLanguage: "en"` — **every time**. Ticking it only helps on a return visit; the first sign-in after a deploy has no cache either, because sign-out clears it and every deploy signs everyone out. ## A change made before the settings file came back was thrown away `queueSettingsPush` returned early while unarmed, dropping the value instead of holding it: ```js - if (!armed || !settingsSyncAvailable()) return; - pending = synced; + if (!settingsSyncAvailable()) return; + pending = synced; + if (!armed) return; // kept; armSettingsSync writes whatever is waiting ``` So a language picked in the second or so after a page load was never written up: it survived until the next reload and no further. That is a better account of *"sometimes it takes several clicks"* than the remount race fixed in #160 — the click that stuck was one made after the read had finished. Keeping the value is safe precisely because #160 taught `hydrate` to refuse to overwrite a key that is still queued, so the newer local change wins over the older file rather than racing it. **Evidence it was real:** before this change no `ihasmail` folder was ever created in the account's files, because even the seed write never fired. After it, the folder appears. ## Without a cached settings object the tree painted too early The first frame was the defaults, and the defaults are English. Anything computed in that window is computed in the wrong language. The *interface* recovers — it is rebuilt when the catalogue lands — but a string emitted once does not, which is exactly what a toast is: ``` boot (no cache) → uiLanguage "en" → render → MailView effect fires → toast, in English → settings file lands → "es" → tree rebuilds → everything Spanish except the toast already on screen ``` So without a cache the authenticated tree now waits for the account's settings and their catalogue. That costs nothing: there was nothing worth painting yet. With a cache it does not wait, and the first frame is as quick as it was — `PAINTED_FROM_CACHE` decides, and it is computed once at module load. ## What I could not verify, and how to check it The mock server does not persist the account settings file — it creates the `ihasmail` folder but never the `settings.json` inside it, and a file I wrote there by hand was not read back either. So locally the app always boots on defaults and stays there, which makes this whole class of bug invisible to local testing. (Worth fixing in the mock separately; it is why this took so long to pin down.) Production does persist it — verified by John across sign-out/sign-in, with the box both ticked and unticked — so the fix needs checking there: 1. set the interface language to something other than English 2. sign out and back in 3. open a URL for a folder that no longer exists The toast should now be in that language. If it is still English, the cause is not this. `npx tsc --noEmit`, `npm run i18n:check`, 109 node tests, 467 web tests, `npm run build`. **Merged** 2026-08-31 as coffey-labs/ihasmail@2380ed282e8e <sub>Rebuilt from: git history, session transcript.</sub>
This repo is archived. You cannot comment on issues.