perf(classroom): инкрементальный поллинг доски, картинки в файлы, ретеншн

#1 Студенческий поллинг: вместо полной перезагрузки доски каждые 2с —
   лёгкая сигнатура страницы (?meta=1 → maxSeq+count). Если доска совпадает
   с сервером (обычный случай при живом WS) — ничего не грузим. Полная
   перезагрузка только при расхождении. Счёт подтверждённых штрихов — по
   положительным id (без bookkeeping).
#2 Картинки-штрихи выносятся в файлы /uploads/classroom (вместо base64 в БД):
   меньше БД и payload поллинга. Имя с префиксом sessionId.
#5 Ретеншн: classroom-cleanup удаляет штрихи+файлы завершённых сессий старше
   N дней (app_settings.classroom_retention_days, по умолч. 30; 0 = выкл),
   историю/чат/посещаемость не трогает. Планировщик в server.js.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-12 13:02:26 +03:00
parent ddc260e114
commit 5d3db90b5d
4 changed files with 128 additions and 8 deletions
+16 -5
View File
@@ -4550,8 +4550,17 @@
}
}
/* Student-side full-sync poll: replaces board state every 2s.
Handles missed SSE events for strokes AND clears — no since_seq tricks needed. */
/* Кол-во подтверждённых сервером штрихов на доске: у них положительный id
(локальные, ещё не отправленные — отрицательный). */
function _wbLoadedServerCount() {
try { return (_wb && _wb._strokes) ? _wb._strokes.filter(s => s.id > 0).length : 0; }
catch (e) { return 0; }
}
/* Student-side poll (2s, страховка к WS/SSE). Сначала тянет лёгкую сигнатуру
страницы (maxSeq + count); если доска уже совпадает с сервером (обычный случай
при живом WS) — НИЧЕГО не грузит. Полная перезагрузка — только при расхождении
(пропущенные при обрыве события, клиры, удаления). */
function wbStartPoll() {
wbStopPoll();
_strokePollTimer = setInterval(async () => {
@@ -4559,14 +4568,16 @@
const page = _wbCurrentPage;
const gen = _wbClearGen;
try {
const sig = await LS.get(`/api/classroom/${_sessionId}/strokes?page_num=${page}&meta=1`);
if (_wbClearGen !== gen || _wbCurrentPage !== page) return;
// Доска идентична серверу → пропускаем тяжёлую перезагрузку
if (sig && sig.maxSeq === _wbMaxSeq && sig.count === _wbLoadedServerCount()) return;
// Расхождение → полная перезагрузка (проверенный путь: корректно отражает клиры/undo/пропуски)
const res = await LS.get(`/api/classroom/${_sessionId}/strokes?page_num=${page}`);
// Discard if board was cleared or page switched while fetching
if (_wbClearGen !== gen || _wbCurrentPage !== page) return;
const strokes = res.strokes || [];
// Update seq cursor so SSE dedup still works
_wbMaxSeq = 0;
_wbUpdateMaxSeq(strokes);
// Full replace — correctly reflects clears, undos, and any missed events
_wb.loadStrokes(strokes);
if (res.template) _wb.setTemplate(res.template);
wbUpdateThumbnail(page);