fix(account): ein vor dem Umbenennen gestartetes Lesen meldet den alten Namen nicht mehr

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-05 07:46:47 +00:00
1 parent f46a69df68
commit 7c55912ebe
2 files changed
+67 -3

No files matched your search

+56
View File
@@ -512,6 +512,62 @@ describe('loadMyUsername', () => {
mocks.getRow.mockRejectedValue(offline);
await expect(profile.loadMyUsername()).rejects.toBe(offline);
});
it('eine Antwort, die vor dem Umbenennen abgeschickt wurde, meldet den alten Namen nicht noch einmal: der neuere Name gilt', async () => {
mocks.userId = 'u1';
let answer!: (row: unknown) => void;
mocks.getRow.mockReturnValue(new Promise((resolve) => (answer = resolve)));
const listener = vi.fn();
profile.onUsernameChange(listener);
const loading = profile.loadMyUsername(); // das Lesen läuft (Stand vor dem Umbenennen)
mocks.apiPost.mockResolvedValue({ username: 'Neu' });
await profile.saveUsername('Neu'); // währenddessen umbenannt
answer({ $id: 'u1', username: 'Alt' }); // die Antwort ist älter als das Umbenennen
await expect(loading).resolves.toBe('Neu');
expect(profile.myUsername()).toBe('Neu');
expect(listener).toHaveBeenCalledExactlyOnceWith('u1', 'Neu'); // „Alt“ wurde nie gemeldet
expect([...(await profile.usernamesFor(['u1']))]).toEqual([['u1', 'Neu']]);
});
it('dasselbe gilt für eine Antwort „kein Profil“, die vor dem Speichern abgeschickt wurde (Registrierung)', async () => {
mocks.userId = 'u1';
let answer!: (error: unknown) => void;
mocks.getRow.mockReturnValue(new Promise((_, reject) => (answer = reject)));
const listener = vi.fn();
profile.onUsernameChange(listener);
const loading = profile.loadMyUsername();
mocks.apiPost.mockResolvedValue({ username: 'Kenso' });
await profile.saveUsername('Kenso');
answer(notFound());
await expect(loading).resolves.toBe('Kenso');
expect(listener).toHaveBeenCalledExactlyOnceWith('u1', 'Kenso');
});
it('ein Lesen, das erst nach dem Umbenennen beginnt, gilt wieder (der Name kann auf einem anderen Gerät geändert worden sein)', async () => {
mocks.userId = 'u1';
mocks.apiPost.mockResolvedValue({ username: 'Neu' });
await profile.saveUsername('Neu');
mocks.getRow.mockResolvedValue({ $id: 'u1', username: 'Anderswo' });
const listener = vi.fn();
profile.onUsernameChange(listener);
await expect(profile.loadMyUsername()).resolves.toBe('Anderswo');
expect(profile.myUsername()).toBe('Anderswo');
expect(listener).toHaveBeenCalledExactlyOnceWith('u1', 'Anderswo');
});
it('das Umbenennen eines anderen Kontos macht ein laufendes Lesen nicht hinfällig', async () => {
mocks.userId = 'u1';
let answer!: (row: unknown) => void;
mocks.getRow.mockReturnValue(new Promise((resolve) => (answer = resolve)));
const loading = profile.loadMyUsername();
mocks.userId = 'u2'; // der Aufruf gehört einem anderen Konto
mocks.apiPost.mockResolvedValue({ username: 'Mia' });
await profile.saveUsername('Mia');
mocks.userId = 'u1';
answer({ $id: 'u1', username: 'Kenso' });
await expect(loading).resolves.toBe('Kenso');
expect(profile.myUsername()).toBe('Kenso');
});
});
describe('Merker „in dieser Sitzung schon gefragt“', () => {
+11 -3
View File
@@ -79,6 +79,9 @@ export function validateUsername(name: string): string | null {
/** Konto-ID → Benutzername, nur gefundene (ein Konto ohne Profil kann sich später einen Namen wählen, also wird es wieder gefragt). */
const names = new Map<string, string>();
/** Zählt die Namen, die diese Seite gespeichert hat; `savedAt` merkt je Konto den Stand beim letzten Speichern (siehe `loadMyUsername`). */
let saves = 0;
const savedAt = new Map<string, number>();
const listeners = new Set<(userId: string, username: string) => void>();
/** Merkt den Namen eines Kontos, dessen Stand diese Seite gerade selbst erfahren hat (Speichern, Lesen des eigenen Profils), und meldet ihn. */
@@ -188,9 +191,11 @@ export async function isUsernameTaken(name: string): Promise<boolean> {
export async function loadMyUsername(): Promise<string | null> {
const userId = currentUserId();
if (!userId) return null;
const started = saves;
const name = await getUsername(userId);
// Kein Profil: war es inzwischen gespeichert (Registrierung: die Abfrage war schneller als das Speichern), gilt der gemerkte Name.
if (name === null) return names.get(userId) ?? null;
// Wurde der Name währenddessen gespeichert (Umbenennen, Registrierung: die Abfrage war schneller als das Speichern), ist die Antwort
// älter als der gemerkte Name: er gilt, und der alte wird nicht noch einmal gemeldet. Kein Profil heißt sonst ebenfalls „der gemerkte“.
if (name === null || (savedAt.get(userId) ?? 0) > started) return names.get(userId) ?? null;
announce(userId, name);
return name;
}
@@ -208,7 +213,10 @@ export async function saveUsername(name: string): Promise<string> {
const saved = res.username;
// Eine Antwort ohne Namen ist keine Antwort des Dienstes (Proxy, Fehlseite): der Ausgang ist offen.
if (typeof saved !== 'string' || saved === '') throw unavailableError();
if (userId) announce(userId, saved);
if (userId) {
savedAt.set(userId, ++saves);
announce(userId, saved);
}
return saved;
}