Skip to main content

max / makenotwork

2.5 KB · 83 lines History Blame Raw
1 use super::{PgPool, Uuid};
2
3 /// Track a thread (upsert).
4 #[tracing::instrument(skip_all)]
5 pub async fn track_thread(
6 pool: &PgPool,
7 user_id: Uuid,
8 thread_id: Uuid,
9 ) -> Result<(), sqlx::Error> {
10 sqlx::query!(
11 "INSERT INTO tracked_threads (user_id, thread_id)
12 VALUES ($1, $2)
13 ON CONFLICT (user_id, thread_id) DO NOTHING",
14 user_id,
15 thread_id,
16 )
17 .execute(pool)
18 .await?;
19 Ok(())
20 }
21
22 /// Untrack a thread.
23 #[tracing::instrument(skip_all)]
24 pub async fn untrack_thread(
25 pool: &PgPool,
26 user_id: Uuid,
27 thread_id: Uuid,
28 ) -> Result<(), sqlx::Error> {
29 sqlx::query!(
30 "DELETE FROM tracked_threads WHERE user_id = $1 AND thread_id = $2",
31 user_id,
32 thread_id,
33 )
34 .execute(pool)
35 .await?;
36 Ok(())
37 }
38
39 /// Stop tracking all threads for a user.
40 #[tracing::instrument(skip_all)]
41 pub async fn untrack_all(pool: &PgPool, user_id: Uuid) -> Result<(), sqlx::Error> {
42 sqlx::query!("DELETE FROM tracked_threads WHERE user_id = $1", user_id)
43 .execute(pool)
44 .await?;
45 Ok(())
46 }
47
48 /// Advance the read position for a tracked thread to `last_post_id`.
49 ///
50 /// Monotonic: the position only ever moves forward in thread order. The caller
51 /// (`routes::forum::thread`) bumps to the last post *on the page being viewed*,
52 /// so an unguarded write would drag the position backward whenever a reader
53 /// revisits an earlier page, and `list_tracked_threads` would then re-count
54 /// already-read posts as unread. The two writes are also spawned off the
55 /// response path, so concurrent views of different pages race; keeping the
56 /// comparison in SQL means Postgres re-evaluates it against the winning row
57 /// under read-committed rather than letting the last writer win.
58 ///
59 /// A `last_post_id` that names no row leaves the position untouched: the
60 /// scalar subquery yields NULL, the comparison is not true, and the guard
61 /// fails closed.
62 #[tracing::instrument(skip_all)]
63 pub async fn update_read_position(
64 pool: &PgPool,
65 user_id: Uuid,
66 thread_id: Uuid,
67 last_post_id: Uuid,
68 ) -> Result<(), sqlx::Error> {
69 sqlx::query!(
70 "UPDATE tracked_threads SET last_read_post_id = $3
71 WHERE user_id = $1 AND thread_id = $2
72 AND (last_read_post_id IS NULL
73 OR (SELECT np.created_at FROM posts np WHERE np.id = $3)
74 > (SELECT cp.created_at FROM posts cp WHERE cp.id = last_read_post_id))",
75 user_id,
76 thread_id,
77 last_post_id,
78 )
79 .execute(pool)
80 .await?;
81 Ok(())
82 }
83