feat: 准实时同步(变更 3 秒推送 + SSE 秒级拉取)并修复云端认证
This commit is contained in:
+44
-1
@@ -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 = {}
|
||||
|
||||
Reference in New Issue
Block a user