Files
daywork/intern-log/repro/engine-realtime.ts
T

113 lines
5.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 实时链路引擎级验证:本地写入 -> 3s 防抖自动推送;外部端写入 -> SSE -> 自动拉取
import 'fake-indexeddb/auto'
const BASE = 'http://127.0.0.1:3780'
let pass = 0, fail = 0
function check(name: string, ok: boolean, detail?: unknown): void {
if (ok) { pass++; console.log(' PASS ' + name) }
else { fail++; console.log(' FAIL ' + name + (detail !== undefined ? ' -> ' + String(JSON.stringify(detail)).slice(0, 200) : '')) }
}
// Node 没有 localStorage:先垫片再导入 auth/engine
const store = new Map<string, string>()
;(globalThis as any).localStorage = {
getItem: (k: string) => (store.has(k) ? (store.get(k) as string) : null),
setItem: (k: string, v: string) => { store.set(k, String(v)) },
removeItem: (k: string) => { store.delete(k) }
}
store.set('dsh_cloud_creds', JSON.stringify({ username: 'twodev', password: 'dev123456' }))
store.set('dsh_session_user', JSON.stringify({ userId: 'local_rt', username: 'twodev' }))
// Node 24 自带 EventSource;老环境则垫一个基于 fetch 流的最小实现
if (typeof (globalThis as any).EventSource === 'undefined') {
class MiniES {
onmessage: ((e: { data: string }) => void) | null = null
onerror: (() => void) | null = null
private ab = new AbortController()
constructor(url: string) {
fetch(url, { signal: this.ab.signal }).then(async r => {
const dec = new TextDecoder()
let buf = ''
const reader = (r.body as any).getReader()
while (true) {
const { done, value } = await reader.read()
if (done) break
buf += dec.decode(value)
while (buf.includes('event: change')) {
buf = buf.slice(buf.indexOf('event: change') + 'event: change'.length)
const cb = this.onmessage
if (cb) cb({ data: '{}' })
}
}
}).catch(() => { const cb = this.onerror; if (cb) cb() })
}
close(): void { this.ab.abort() }
}
;(globalThis as any).EventSource = MiniES
}
async function main() {
const schema = await import('../src/db/schema')
const bindDatabase = schema.bindDatabase, WorklogDB = schema.WorklogDB
const engine = await import('../src/sync/engine')
const repo = await import('../src/db/repo')
check('Node 有 EventSource(原生或垫片)', typeof (globalThis as any).EventSource !== 'undefined')
bindDatabase(new WorklogDB('rt_a'))
store.set('dsh_sync_base', BASE) // 测试环境同步基址走本地覆盖
engine.enableRealtimeSync()
// ---- 场景1:本地写入 -> 防抖 3s -> 自动推送 ----
await schema.db.project.add({ name: '实时推送项目', archived: 0, created_at: '2026-01-01T00:00:00' })
const t0 = performance.now()
const login = await fetch(BASE + '/api/auth/login', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: 'twodev', password: 'dev123456' })
})
const tok = (await login.json()).token as string
async function serverSnapshot(): Promise<Record<string, any>> {
const r = await fetch(BASE + '/api/sync', {
method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + tok },
body: JSON.stringify({ tables: {} })
})
return (await r.json()).tables
}
let pushedAt = -1
for (let i = 0; i < 30; i++) {
await new Promise(r => globalThis.setTimeout(r, 300))
const snap = await serverSnapshot()
if ((snap.project || []).some((p: any) => p.name === '实时推送项目')) { pushedAt = performance.now() - t0; break }
}
check('本地写入 4.5 秒内自动推送上云', pushedAt > 0, 'syncLastError=' + engine.syncLastError.value)
if (pushedAt > 0) console.log(' INFO 到云端可见耗时: ' + Math.round(pushedAt) + ' ms')
check('自动推送未留下错误', engine.syncLastError.value === '', engine.syncLastError.value)
// ---- 场景2:外部端写入 -> SSE 通知 -> 本端自动拉取 ----
await fetch(BASE + '/api/sync', {
method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + tok },
body: JSON.stringify({ tables: { memo: [{ uuid: 'ext-memo-1', title: '来自另一端', content: 'sse', updated_at: '2026-02-03T00:00:00' }] } })
})
const t1 = performance.now()
let pulledAt = -1
for (let i = 0; i < 25; i++) {
await new Promise(r => globalThis.setTimeout(r, 250))
const got = await schema.db.memo.toArray()
if (got.some(m => (m as any).uuid === 'ext-memo-1')) { pulledAt = performance.now() - t1; break }
}
check('外部端写入后 6 秒内自动拉取到本地', pulledAt > 0, 'syncLastError=' + engine.syncLastError.value)
if (pulledAt > 0) console.log(' INFO SSE 通知到本地落库耗时: ' + Math.round(pulledAt) + ' ms')
check('自动拉取未留下错误', engine.syncLastError.value === '', engine.syncLastError.value)
// ---- 场景3:autoSyncNow 手动失败可见性(断開服务)----
engine.stopRealtime()
store.set('dsh_sync_base', 'http://127.0.0.1:9') // 服务不可达
await engine.autoSyncNow()
check('服务不可达时 syncLastError 被记录', engine.syncLastError.value !== '', engine.syncLastError.value)
store.set('dsh_sync_base', BASE)
console.log('==== 实时链路审计: PASS ' + pass + ' / FAIL ' + fail + ' ====')
process.exit(fail > 0 ? 1 : 0)
}
main().catch(e => { console.error('FAILED:', (e && (e.stack || e.message)) || e); process.exit(1) })