/** * 收件箱兜底轮询:SSE 的**冗余**通道。 * * ## 为什么需要 * * SSE 只在**真的断开**时触发 `onerror`。有一类故障它看不见: * **连接还在、TCP 没断、却不再收数据**(代理静默丢包、NAT 超时、 * 中间设备挂死长连接)。两端都认为连接正常 ⇒ 不重连 ⇒ 页面停留就再也不同步。 * * 症状与用户报的一致:「页面停留不动、新邮件不自动同步」, * 而手动刷新能看到(刷新走新请求,与那条挂死的连接无关)。 * * ## 为什么只比 `total` * * 每 N 秒全量重拉会让接口与渲染无谓地抖。`getInbox('all', 1)` 只要计数, * 变了才真的拉列表。 */ import { test } from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; const HERE = dirname(fileURLToPath(import.meta.url)); const pollSrc = readFileSync(join(HERE, '..', 'src', 'lib', 'inboxFallbackPoll.ts'), 'utf8'); const appSrc = readFileSync(join(HERE, '..', 'src', 'App.tsx'), 'utf8'); test('★ 探针只取计数(limit=1),变了才重拉', () => { // 全量重拉会让接口与渲染无谓抖动;探针要廉价 assert.ok( /getInbox\('all',\s*1\)/.test(pollSrc), '探针必须 limit=1 只要 total,不要正文'); assert.ok( /total !== lastTotal/.test(pollSrc), '必须比 total —— 变了才刷新'); }); test('★ 首轮只建基线不触发刷新(否则刚打开页面就白拉一次)', () => { assert.ok( /lastTotal === null/.test(pollSrc) && /return false; \/\/ 首轮建立基线/.test(pollSrc), '首轮必须只记录基线,不触发刷新'); }); test('★ 探针失败不得重置基线', () => { // 网络抖动不该让下一轮误判成「有变化」而触发全量重拉 const m = pollSrc.match(/} catch \{[\s\S]*?\n \}/); assert.ok(m, '未找到 catch 分支'); assert.ok( !/lastTotal = null/.test(m[0]), '★ catch 里不得重置基线 —— 那会把一次抖动变成「下一轮假装有变化」'); }); test('★ inFlight 去重(上一轮未回就不叠请求)', () => { assert.ok( /if \(inFlight\) return false;/.test(pollSrc), '必须有 inFlight 去重,否则慢网络下请求会叠加'); }); test('★ 页面隐藏时暂停 + 恢复可见立刻探一次', () => { // 用户往往就是「切回来发现没更新」才报的 —— 那一下正好补上 assert.ok( /visibilitychange/.test(pollSrc) && /pollOnce\(fetchInbox\)/.test(pollSrc), '恢复可见时应立刻探一次,不等下一个周期'); assert.ok(/paused = document\.visibilityState !== 'visible'/.test(pollSrc), '初始就应按可见性决定是否暂停'); }); test('★ App 里两条通道的 cleanup 都要调(漏一个 = 旧账号定时器残留)', () => { assert.ok(/stopPoll\(\)/.test(appSrc), 'cleanup 必须停轮询定时器'); assert.ok(/disconnect\(\)/.test(appSrc), 'cleanup 必须断 SSE'); assert.ok( /resetPollBaseline\(\)/.test(appSrc), '切号必须丢弃基线:新账号的 total 与旧账号无关,' + '不丢会误判成「有变化」而白拉一次'); }); test('★ 轮询与 SSE 共用同一个 effect 依赖(凭证变化时一起重建)', () => { // 轮询若挂在另一个 effect 上,切号时它不会重建 ⇒ 拿新凭证查旧账号的收件箱 assert.ok( /const credentialSig = useCredentialChange\(\);/.test(appSrc), '凭证身份必须存在'); assert.ok( /}, \[phase, credentialSig\]\);/.test(appSrc), 'effect 依赖必须含 credentialSig —— 轮询与 SSE 都在这个 effect 里'); });