fix(ui): hide clock warning until first sync settles

读取到校准缓存时 status 会先变成 cached,start() 随即渲染出警告条,
几百毫秒后校准成功又隐藏,表现为每次刷新闪现一次。

新增 hasSettledSync 标记,首次校准有结果前不渲染任何警告。原先只有
local 分支有 syncPromise 守卫,cached 和 stale 分支漏了同样的处理。
This commit is contained in:
wuzf
2026-08-09 21:43:33 +08:00
parent e3c229b2fe
commit 6d3b90af2b
2 changed files with 63 additions and 1 deletions

View File

@@ -37,6 +37,7 @@ export function getTimeCode() {
this.syncAttemptId = 0;
this.activeSampleControllers = new Set();
this.started = false;
this.hasSettledSync = false;
this.loadCachedOffset();
}
@@ -248,6 +249,7 @@ export function getTimeCode() {
this.localWallAtSyncMs = Date.now();
this.monotonicEpochAtSyncMs = this.monotonicEpochNow();
this.status = 'synced';
this.hasSettledSync = true;
this.generation += 1;
this.persistOffset();
this.renderStatus();
@@ -256,6 +258,7 @@ export function getTimeCode() {
}
markSyncFailure() {
this.hasSettledSync = true;
this.status = Number.isFinite(this.offsetMs) ? 'cached' : 'local';
this.renderStatus();
}
@@ -352,7 +355,11 @@ export function getTimeCode() {
const isStale = Number.isFinite(ageMs) && ageMs > CLOCK_SYNC_STALE_MS;
let message = '';
if (this.status === 'local' && !this.syncPromise) {
// 首次校准落地前不提示:页面刚加载时缓存偏移已生效但校准仍在进行,
// 此时提示会在校准成功后立即消失,表现为刷新时的一次闪烁。
if (!this.hasSettledSync) {
message = '';
} else if (this.status === 'local') {
message = '无法校准服务器时间OTP 正在使用设备时间,可能不正确。';
} else if (this.status === 'cached') {
const ageText = this.formatAge(ageMs);

View File

@@ -548,6 +548,61 @@ describe('trusted browser clock', () => {
await expect(backgroundSync).resolves.toBe(true);
});
it('keeps the warning hidden on load while the first sync is still in flight', async () => {
const elements = createWarningElements();
const harness = createHarness({
elements,
storageValues: {
[STORAGE_KEY]: createCachedClock({
offsetMs: 12_000,
localWallAtSyncMs: SERVER_BASE_MS - 60 * 60 * 1000,
}),
},
});
expect(harness.api.trustedClock.status).toBe('cached');
harness.queueSamples([20, 40, 60].map((rttMs) => ({ rttMs, offsetMs: 15_000 })));
harness.api.trustedClock.start();
const initialSync = harness.api.trustedClock.syncPromise;
expect(elements.clockWarning.hidden).toBe(true);
expect(elements.clockWarningText.textContent).toBe('');
expect(elements.clockWarning.classList.toggle).not.toHaveBeenCalledWith('show', true);
await harness.completePendingRequests();
await expect(initialSync).resolves.toBe(true);
expect(harness.api.trustedClock.status).toBe('synced');
expect(elements.clockWarning.hidden).toBe(true);
expect(elements.clockWarning.classList.toggle).not.toHaveBeenCalledWith('show', true);
});
it('shows the cached warning once the first sync fails instead of before it settles', async () => {
const elements = createWarningElements();
const harness = createHarness({
elements,
storageValues: {
[STORAGE_KEY]: createCachedClock({
offsetMs: 12_000,
localWallAtSyncMs: SERVER_BASE_MS - 60 * 60 * 1000,
}),
},
});
harness.queueSamples([20, 40, 60].map((rttMs) => ({ reject: true, rttMs })));
harness.api.trustedClock.start();
const initialSync = harness.api.trustedClock.syncPromise;
expect(elements.clockWarning.hidden).toBe(true);
await harness.completePendingRequests();
await expect(initialSync).resolves.toBe(false);
expect(harness.api.trustedClock.status).toBe('cached');
expect(elements.clockWarning.hidden).toBe(false);
expect(elements.clockWarning.classList.toggle).toHaveBeenLastCalledWith('show', true);
expect(elements.clockWarningText.textContent).not.toBe('');
});
it('shows a stale warning after 24 hours during periodic and offline status refreshes', async () => {
const elements = createWarningElements();
const harness = createHarness({ elements });