fix: глубокое ревью онлайн-урока — 14 исправлений (P0-P3)
P0 — краши: - CREATE TABLE classroom_hands в migrate.js (отсутствовала) - emit→emitToUser для allowDraw/revokeDraw/mutePeer (WS доставка) - deleteHistorySession обёрнут в db.transaction() + добавлена очистка hands/invites P1 — гонки и безопасность: - deletePage: 4 SQL в транзакции (race при параллельной записи) - postStrokes: MAX(seq) внутрь транзакции (дубли seq) - duplicatePage: добавлен seq в INSERT (NOT NULL crash) - hasAccess для lowerHand/getHands/reactToMessage (утечка данных) - loadTemplate: проверка owner шаблона - attachment_url: только /uploads/* (XSS через javascript:/data: URI) - wbFlushBatch: backoff при ошибке (было 12.5 req/s retry) - pagehide leave: keepalive fetch для гарантированной доставки - _wbOwnIds: cap 2000 (утечка памяти на длинных уроках) P2-P3: - simState: лимит 64KB (предотвращает OOM broadcast) - ws-server кеши: cleanup drawCache при invalidateSession Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+20
-2
@@ -3180,6 +3180,7 @@
|
||||
let _wbInitializing = false; // true while loading initial strokes from server
|
||||
let _wbPendingSSE = []; // SSE strokes buffered during initialization
|
||||
const _wbOwnIds = new Set(); // server-assigned IDs of our own sent strokes (to skip in SSE)
|
||||
const _WB_OWN_IDS_MAX = 2000; // cap to prevent unbounded growth on long lessons
|
||||
|
||||
/* ── WebSocket (low-latency cursor + preview) ── */
|
||||
let _crWs = null; // WebSocket instance
|
||||
@@ -4388,25 +4389,33 @@
|
||||
}
|
||||
}
|
||||
|
||||
let _wbFlushFails = 0;
|
||||
async function wbFlushBatch() {
|
||||
if (!_sessionId || _wbBatch.length === 0) return;
|
||||
// Backoff: skip ticks after consecutive failures (max ~5s pause)
|
||||
if (_wbFlushFails > 0) {
|
||||
const skipTicks = Math.min(60, Math.pow(2, _wbFlushFails)); // 2,4,8,16...60
|
||||
if (Math.random() > 1 / skipTicks) return;
|
||||
}
|
||||
const toSend = _wbBatch.splice(0, _wbBatch.length); // drain queue
|
||||
try {
|
||||
const res = await LS.post(`/api/classroom/${_sessionId}/strokes`, {
|
||||
page_num: _wbCurrentPage,
|
||||
strokes: toSend.map(s => ({ tool: s.tool, data: s.data })),
|
||||
});
|
||||
_wbFlushFails = 0;
|
||||
// update local (negative) ids to server-assigned ids; track own ids to skip in SSE
|
||||
if (res.strokes && _wb) {
|
||||
res.strokes.forEach((saved, i) => {
|
||||
if (toSend[i]) {
|
||||
_wbOwnIds.add(saved.id); // register BEFORE confirmStroke calls render
|
||||
if (_wbOwnIds.size > _WB_OWN_IDS_MAX) { const it = _wbOwnIds.values().next().value; _wbOwnIds.delete(it); }
|
||||
_wb.confirmStroke(toSend[i].id, saved.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// put back on failure
|
||||
_wbFlushFails++;
|
||||
_wbBatch.unshift(...toSend);
|
||||
}
|
||||
}
|
||||
@@ -7485,7 +7494,16 @@
|
||||
stopPolling();
|
||||
if (_timerHandle) { clearInterval(_timerHandle); _timerHandle = null; }
|
||||
if (_rtc) { _rtc.destroy(); _rtc = null; }
|
||||
if (_sessionId) LS.post(`/api/classroom/${_sessionId}/leave`).catch(() => {});
|
||||
if (_sessionId) {
|
||||
const url = `/api/classroom/${_sessionId}/leave`;
|
||||
const token = localStorage.getItem('ls_token');
|
||||
// keepalive: true ensures the request completes even if the page is unloading
|
||||
fetch(url, {
|
||||
method: 'POST', keepalive: true,
|
||||
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
|
||||
body: '{}',
|
||||
}).catch(() => {});
|
||||
}
|
||||
});
|
||||
|
||||
/* ── run ── */
|
||||
|
||||
Reference in New Issue
Block a user