fix(feed): width-change measurement invalidation + best-effort SSE counts

Post-commit review follow-ups for the batch-3 feed work.

Frontend — VirtualFeed: the guarded setOptions keyed only on (count, scrollMargin),
so a rotation (or the first-paint 0->real container width) changed colWidth / card
heights without invalidating the cached measurements, leaving getTotalSize and the
scrollbar stale until each row scrolled back through the window. Track appliedWidth
and call virtualizer.measure() on a width delta so heights re-estimate at the new
width (rendered rows re-measure immediately via their ResizeObserver). Covers list +
grid and the first-paint 120px-estimate case.

Backend — social.rs: the fresh like/comment count SELECT used `.await?`, so a
transient DB failure would 500 the request after the like/comment had already
committed. Make the count + broadcast best-effort (if let Ok { ... }); the mutation
succeeds regardless and the next event / pull-to-refresh reconciles the count.

Docs — FOLLOWUPS: record the review findings left as-is (grid-prepend tile reflow,
filtered-grid auto-load which is pre-existing, comment-delete stale count, and the
last-write-wins count ordering note).

Verified: svelte-check 0 errors, cargo build clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
fabi
2026-06-30 19:32:32 +02:00
parent e79e020566
commit 0d7938aff5
3 changed files with 79 additions and 27 deletions

View File

@@ -44,18 +44,22 @@ pub async fn toggle_like(
}
// Fresh count so feed clients can patch the single card in place instead of
// refetching page 1 (mirrors v_feed.like_count = COUNT(DISTINCT user_id)).
let like_count: i64 =
sqlx::query_scalar("SELECT COUNT(DISTINCT user_id) FROM \"like\" WHERE upload_id = $1")
.bind(upload_id)
.fetch_one(&state.pool)
.await?;
// Broadcast SSE
let _ = state.sse_tx.send(crate::state::SseEvent {
event_type: "like-update".to_string(),
data: serde_json::json!({ "upload_id": upload_id, "like_count": like_count }).to_string(),
});
// refetching page 1 (mirrors v_feed.like_count = COUNT(DISTINCT user_id)). The
// count + broadcast are a UI optimisation — the like itself is already committed,
// so a failure here must not fail the request. Swallow the error and skip the
// broadcast; the next event or a pull-to-refresh reconciles the count.
if let Ok(like_count) = sqlx::query_scalar::<_, i64>(
"SELECT COUNT(DISTINCT user_id) FROM \"like\" WHERE upload_id = $1",
)
.bind(upload_id)
.fetch_one(&state.pool)
.await
{
let _ = state.sse_tx.send(crate::state::SseEvent {
event_type: "like-update".to_string(),
data: serde_json::json!({ "upload_id": upload_id, "like_count": like_count }).to_string(),
});
}
Ok(StatusCode::NO_CONTENT)
}
@@ -123,20 +127,23 @@ pub async fn add_comment(
}
// Fresh count so feed clients can patch the single card in place instead of
// refetching page 1 (mirrors v_feed.comment_count = COUNT(DISTINCT c.id)).
let comment_count: i64 = sqlx::query_scalar(
// refetching page 1 (mirrors v_feed.comment_count = COUNT(DISTINCT c.id); COUNT(*)
// over the same deleted_at filter is identical since comment.id is the PK). The
// count + broadcast are a UI optimisation — the comment is already committed, so a
// failure here must not fail the request. Swallow the error and skip the broadcast.
if let Ok(comment_count) = sqlx::query_scalar::<_, i64>(
"SELECT COUNT(*) FROM comment WHERE upload_id = $1 AND deleted_at IS NULL",
)
.bind(upload_id)
.fetch_one(&state.pool)
.await?;
// Broadcast SSE
let _ = state.sse_tx.send(crate::state::SseEvent {
event_type: "new-comment".to_string(),
data: serde_json::json!({ "upload_id": upload_id, "comment_count": comment_count })
.to_string(),
});
.await
{
let _ = state.sse_tx.send(crate::state::SseEvent {
event_type: "new-comment".to_string(),
data: serde_json::json!({ "upload_id": upload_id, "comment_count": comment_count })
.to_string(),
});
}
let dto = CommentDto {
id: comment.id,