feat(db): complete async call-site conversion — Phase 3 done, validated on SQLite

The full sync→async conversion is complete and green on the SQLite backend. Every
DB call across the app now awaits the async adapter, so the identical code runs on
Postgres at cutover.

Converted (this commit finishes Phase 3):
- session.js: currentUser/apiKeyFromReq async → 63 route awaits + WS + static.
- routes.js: all ~250 R.* awaited; DTO helpers (namesFor, avatarsFor, buildMsgDTO,
  buildPollDTO, reactionsForMessage, postSystemMessage, pushGroupUpdate,
  issueRefreshToken, provisionFromBizgaze) made async; every `.map(x=>buildDTO(x))`
  restructured to `await Promise.all(...map(async...))` preserving order; `.filter`
  predicates that hit the DB moved to an `asyncFilter` helper; chained
  `R.x.y(...).length/.map/.filter` wrapped as `(await R.x.y(...)).method`; stream
  upload handlers (recording/transcript/attachment) made async.
- calls.js / signaling.js: all call/meeting fns async; leaveMeeting AWAITS
  persistCallHistory + finalizeTranscript BEFORE endCallByRoom (ordering matters —
  fire-and-forget would race the map teardown); WS handle()/cleanup() async with
  .catch guards.
- static.js: authAttachment(Raw) async (the .some carrier check became a loop),
  handleGet async; server.js dispatch catches handler rejections → 500 not a hang.
- media.js backfill, push.js, reminders.js, webhooks.js await their repo calls.

Validation on DB_BACKEND=sqlite: db-smoke 22/22; legacy e2e 80 checks pass with zero
FAILs (throws only at a PRE-EXISTING WS lobby-drift assertion, unrelated). Every
server file `node --check` clean.

Still on the branch — master untouched. Next: Phase 5 (pg backend + ~7 dialect
queries + data migration + Docker Postgres + cutover), then merge.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-24 22:06:27 +05:30
parent 2460c0f9eb
commit 3250530596
8 changed files with 158 additions and 146 deletions
+18 -13
View File
@@ -12,27 +12,32 @@ const MIME = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css
// Authorize an attachment id: the uploader, a member of the group using it as an avatar, or a participant of
// ANY message carrying it (the "any" covers forwarded attachments, which reuse the same id). Returns the row.
function authAttachmentRaw(id, u) {
async function authAttachmentRaw(id, u) {
const a = await R.attachments.byId(id);
if (!a || a.team_id !== u.team_id) return null;
const avatarGroup = await R.conversations.byAvatar(id);
const carriers = await R.messages.allByAttachment(id);
const ok = a.uploader_id === u.id
|| (avatarGroup && await R.conversations.isMember(avatarGroup.id, u.id))
|| carriers.some((msg) => msg.conversation_id
? await R.conversations.isMember(msg.conversation_id, u.id)
: (msg.sender_id === u.id || msg.recipient_id === u.id));
let ok = a.uploader_id === u.id || (avatarGroup && await R.conversations.isMember(avatarGroup.id, u.id));
if (!ok) {
// A .some() predicate can't await, so walk the carriers explicitly.
for (const msg of carriers) {
const carried = msg.conversation_id
? await R.conversations.isMember(msg.conversation_id, u.id)
: (msg.sender_id === u.id || msg.recipient_id === u.id);
if (carried) { ok = true; break; }
}
}
return ok ? a : null;
}
// Video playback fires MANY /files Range requests, and authAttachmentRaw scans messages by attachment_id
// (un-indexed) each time — that per-chunk scan is what made playback stutter ("buffers and plays…"). Cache the
// each time — that per-chunk scan is what made playback stutter ("buffers and plays…"). Cache the resolved
// decision per user+attachment for 60s so range requests after the first are ~free. Bounded to keep memory flat.
const _attAuth = new Map();
function authAttachment(id, u) {
async function authAttachment(id, u) {
const key = u.id + ':' + id, now = Date.now();
const hit = _attAuth.get(key);
if (hit && hit.exp > now) return hit.a;
const a = authAttachmentRaw(id, u);
const a = await authAttachmentRaw(id, u);
if (_attAuth.size > 4000) _attAuth.clear();
_attAuth.set(key, { a, exp: now + 60000 });
return a;
@@ -100,7 +105,7 @@ function serveStatic(req, res) {
}
// GET fallback: authenticated transcript/recording downloads, else static files.
function handleGet(req, res) {
async function handleGet(req, res) {
const pathOnly = req.url.split('?')[0];
// Stable "latest Windows installer" link (used by the site's Download button). Reads the
// electron-updater manifest and redirects to the current versioned .exe.
@@ -211,7 +216,7 @@ function handleGet(req, res) {
const u = await currentUser(req);
if (!u) return json(res, 401, { error: 'unauthorized' });
const id = path.basename(decodeURIComponent(pathOnly));
const a = authAttachment(id, u);
const a = await authAttachment(id, u);
if (!a) return json(res, 404, { error: 'not found' });
if (!/^video\//.test(a.mime || '')) return json(res, 404, { error: 'not a video' });
const src = path.join(UPLOADS_DIR, id);
@@ -239,7 +244,7 @@ function handleGet(req, res) {
const u = await currentUser(req);
if (!u) return json(res, 401, { error: 'unauthorized' });
const id = path.basename(decodeURIComponent(pathOnly));
const a = authAttachment(id, u);
const a = await authAttachment(id, u);
if (!a) return json(res, 404, { error: 'not found' });
media.ensureWebRendition(id, a.mime); // idempotent — also backfills pre-existing uploads
const ready = media.hasWebRendition(id);
@@ -256,7 +261,7 @@ function handleGet(req, res) {
const u = await currentUser(req);
if (!u) return json(res, 401, { error: 'unauthorized' });
const id = path.basename(decodeURIComponent(pathOnly));
const a = authAttachment(id, u);
const a = await authAttachment(id, u);
if (!a) return json(res, 404, { error: 'not found' });
const fp = path.join(UPLOADS_DIR, id);
if (!fp.startsWith(UPLOADS_DIR)) return json(res, 403, { error: 'forbidden' });