feat: 准实时同步(变更 3 秒推送 + SSE 秒级拉取)并修复云端认证

This commit is contained in:
root
2026-10-16 11:45:20 +08:00
parent 052158d96d
commit ba07899dbe
4 changed files with 168 additions and 38 deletions
+44 -1
View File
@@ -76,6 +76,24 @@ function base64url(s) {
return Buffer.from(s).toString('base64url')
}
// ---------- SSE 长连接:按用户分组,数据变更时实时通知 ----------
const sseClients = new Map()
function broadcast(userId, event) {
const set = sseClients.get(userId)
if (!set) return
for (const res of set) {
try { res.write('event: ' + event + '\ndata: {}\n\n') } catch { /* 连接已断开 */ }
}
}
function addSse(userId, res) {
if (!sseClients.has(userId)) sseClients.set(userId, new Set())
sseClients.get(userId).add(res)
}
function removeSse(userId, res) {
const set = sseClients.get(userId)
if (set) { set.delete(res); if (!set.size) sseClients.delete(userId) }
}
// ---------- HTTP 基础 ----------
function json(res, status, data) {
const body = JSON.stringify(data)
@@ -113,6 +131,25 @@ async function handle(req, res) {
if (req.method === 'GET' && route === '/api/health') { json(res, 200, { ok: true }); return }
// SSE 事件流:token 经查询参数传递(EventSource 无法自定义请求头)
if (req.method === 'GET' && route === '/api/events') {
const auth = verifyToken(url.searchParams.get('token'))
if (!auth) { json(res, 401, { error: '未认证' }); return }
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
'Access-Control-Allow-Origin': '*'
})
res.write('event: hello\ndata: {}\n\n')
addSse(auth.userId, res)
const ka = setInterval(() => {
try { res.write(': keepalive\n\n') } catch { /* 忽略 */ }
}, 25000)
req.on('close', () => { clearInterval(ka); removeSse(auth.userId, res) })
return
}
if (req.method === 'POST' && route === '/api/auth/register') {
const body = await readBody(req)
const username = String(body.username || '').trim()
@@ -149,6 +186,7 @@ async function handle(req, res) {
// 应用推送的变更(新者胜)
const incomingTables = body.tables || {}
let changed = 0
for (const t of TABLES) {
const rows = incomingTables[t] || []
userData.tables[t] = userData.tables[t] || {}
@@ -159,6 +197,7 @@ async function handle(req, res) {
const existing = userData.tables[t][row.uuid]
if (!existing || String(clean.updated_at || '') >= String(existing.updated_at || '')) {
userData.tables[t][row.uuid] = clean
changed++
}
}
}
@@ -168,7 +207,10 @@ async function handle(req, res) {
if (!TABLES.includes(tb.table) || !tb.uuid) continue
userData.tombstones[tb.table] = userData.tombstones[tb.table] || {}
const prev = userData.tombstones[tb.table][tb.uuid]
if (!prev || tb.deleted_at > prev) userData.tombstones[tb.table][tb.uuid] = tb.deleted_at
if (!prev || tb.deleted_at > prev) {
userData.tombstones[tb.table][tb.uuid] = tb.deleted_at
changed++
}
}
// 墓碑生效:清除被删数据
for (const t of TABLES) {
@@ -179,6 +221,7 @@ async function handle(req, res) {
}
}
saveUserData(auth.userId, userData)
if (changed) broadcast(auth.userId, 'change')
// 返回权威全量
const outTables = {}