fix(user-flow): close offline-queue, export, session, ban & realtime gaps
Comprehensive user-flow review across guest/host/admin roles, then fixes with
e2e regression guards. Highlights:
- Offline upload queue auto-resumes on reconnect: network errors keep items
pending (not error), 4xx are terminal (no infinite retry), quota exhaustion
returns a distinct 413; queue cap + dedup.
- Export lifecycle: release atomically locks uploads; reopen invalidates and
re-release regenerates the keepsake; workers claim their job atomically so a
reopen->re-release can't corrupt the ZIP; startup re-spawns interrupted
exports.
- Sessions slide on activity (no 30-day cliff); JWT expiry deferred to the
revocable session row; sign-out-everywhere + revoke-on-PIN-reset.
- Ban always hides content (v_feed / find_visible_media / export filter
is_banned) but stays a read-only ban per USER_JOURNEYS §10 — sessions are
not revoked, read access + keepsake download preserved.
- Realtime: server-clock SSE delta cursor; event-closed/opened drive the UI
live; feed_delta rate-limited; like returns {liked, like_count} to fix
multi-device drift; lightbox live comments; diashow delta backfill.
- Forgotten-PIN in-app request flow; simultaneous same-name join returns 409;
quota increment is transactional; operator floor.
Adds e2e/specs/10-flow-review/ (offline resume, export integrity, deterministic
anti-race guard) and updates existing specs for the new contracts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -186,6 +186,11 @@ pub struct DeltaResponse {
|
||||
/// rather than merging (the older missed uploads are absent and unrecoverable
|
||||
/// via a later delta, which advances `since` past them).
|
||||
pub truncated: bool,
|
||||
/// The server's clock at the moment this delta was computed. The client advances its
|
||||
/// reconnect cursor from THIS, never `new Date()` — a browser clock even seconds fast
|
||||
/// would otherwise silently skip uploads whose server `created_at` falls in the skew
|
||||
/// window (they'd never reappear without a hard refresh).
|
||||
pub server_time: DateTime<Utc>,
|
||||
}
|
||||
|
||||
pub async fn feed_delta(
|
||||
@@ -193,6 +198,33 @@ pub async fn feed_delta(
|
||||
auth: AuthUser,
|
||||
Query(q): Query<DeltaQuery>,
|
||||
) -> Result<Json<DeltaResponse>, AppError> {
|
||||
// Rate-limit the delta the same way as the paginated feed. Without this, ~100 clients
|
||||
// reconnecting at once (post-outage, or a flapping network) each fire an unbounded
|
||||
// delta fetch — a reconnect stampede. Keyed per-user so one client can't starve others
|
||||
// behind a shared NAT.
|
||||
let rate_limits_on = config::get_bool(&state.config_cache, "rate_limits_enabled", true).await;
|
||||
let feed_rate_on = config::get_bool(&state.config_cache, "feed_rate_enabled", true).await;
|
||||
if rate_limits_on && feed_rate_on {
|
||||
let rate_limit = config::get_usize(&state.config_cache, "feed_rate_per_min", 60).await;
|
||||
if !state.rate_limiter.check(
|
||||
format!("feed_delta:{}", auth.user_id),
|
||||
rate_limit,
|
||||
Duration::from_secs(60),
|
||||
) {
|
||||
return Err(AppError::TooManyRequests(
|
||||
"Zu viele Anfragen. Bitte warte kurz und versuche es erneut.".into(),
|
||||
None,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Anchor the next cursor to the DB clock, captured *before* the queries so an upload
|
||||
// committed during this handler is re-fetched next time rather than skipped (a
|
||||
// duplicate id merges idempotently on the client; a miss is unrecoverable).
|
||||
let server_time: DateTime<Utc> = sqlx::query_scalar("SELECT NOW()")
|
||||
.fetch_one(&state.pool)
|
||||
.await?;
|
||||
|
||||
// Bounded like the paginated feed: a stale `since` could otherwise pull the
|
||||
// entire event's uploads in one response. If a client hits the cap it should
|
||||
// fall back to a full feed refresh rather than another delta.
|
||||
@@ -254,6 +286,7 @@ pub async fn feed_delta(
|
||||
uploads,
|
||||
deleted_ids: deleted_ids.into_iter().map(|r| r.0).collect(),
|
||||
truncated,
|
||||
server_time,
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user