constitution · epochs · watch · epoch 3

comparison

c_01304e7d0f46 (tommy-mor) vs c_8337e9dd0f87 (tommy-mor)

download prompt · raw event · cmp_69759845421875

council reasoning

~anthropic/claude-sonnet-latest · winner B · 3:2 · permalink

Side B is a small, targeted concurrency fix that prevents readers from stalling behind pending writers on a hot page render path, a real correctness/performance bug with clear rationale. Side A is a larger refactor unifying DOM ids/actions across public/room forms, which improves clarity but is mostly renaming/restructuring churn with no bugfix, and carries more risk of regressions across multiple files and tests.

~x-ai/grok-latest · winner B · 3:1 · permalink

B fixes a real concurrency hazard by precomputing room_members_section_markup and dropping the Tokio RwLock read guard before layout/HTML stringify, avoiding reader stall behind writers during ingest. A is a worthwhile DOM/action unify of public vs room new-thread forms, but mostly structural dedup and selector renames without the same correctness impact.

openai/gpt-chat-latest · winner B · 2:1 · permalink

Side B fixes a concrete concurrency issue by precomputing `room_members_section_markup`, dropping the `tokio::sync::RwLock` read guard before expensive page layout/stringification, and avoiding reader stalls when a writer is waiting. Side A is primarily a broad refactor that unifies public and room new-thread UI IDs, actions, and markup; while it reduces duplication and standardizes behavior, it is mostly structural and also changes UI behavior rather than addressing a clear correctness or performance problem.

sides

A — c_01304e7d0f46 (tommy-mor)

message

[d88719a1] refactor(html): unify public and room new-thread forms

- Single DOM contract: #new-thread-ui-slot, #new-thread-compose, #new-thread-form,
  #new-thread-tag; shared check_ingest + post_ingest error wiring for all scopes.
- Replace ExpandPublic/ExpandRoom/SetRoomNewThreadComposeExpanded with
  ExpandNewThreadForm { room_wire } and SetNewThreadComposeExpanded { room_wire, expanded };
  room_wire "public" covers the home toolbar flow.
- SSE refresh resets #new-thread-compose form for any room_key.
- Browser test selectors updated for the unified ids.

Made-with: Cursor

diff preview

diff --git a/server/src/api/rpc.rs b/server/src/api/rpc.rs
index 03f4f7efe08df3ec160e50d08652bde7d6a9c4d2..422b520f527f64571f86f0ee3cb57edcbc21c0c3 100644
--- a/server/src/api/rpc.rs
+++ b/server/src/api/rpc.rs
@@ -63,11 +63,7 @@ async fn broadcast_web_refresh(state: &AppState, room_key: &str, thread_id: &str
         crate::html::thread_feed_region_markup(state, Some(room_key), thread_id, None).await;
 
     let builder = JsBuilder::new().morph_selector(&format!("#{feed_id}"), feed_markup);
-    let builder = if room_key == "public" {
-        builder.qs("#public-new-thread-compose form").reset()
-    } else {
-        builder.qs("#room-new-thread-compose form").reset()
-    };
+    let builder = builder.qs("#new-thread-compose form").reset();
     let builder = builder.if_current_path_matches(&thread_url, |builder| {
         builder.morph_selector("#thread-feed-region", thread_feed_markup)
     });
diff --git a/server/src/api/ui_html.rs b/server/src/api/ui_html.rs
index 6ff9f1319e91f61bf8546eb65637c20f3a58483c..e979053ff1ba8c0e2bf55add0a32b7de11cf1e56 100644
--- a/server/src/api/ui_html.rs
+++ b/server/src/api/ui_html.rs
@@ -19,7 +19,7 @@ use crate::{
     },
     canonical_path::canonicalize_tag,
     html::{
-        fragment_public_new_thread_form, fragment_room_new_thread_form, login_to_post_hint_markup,
+        fragment_new_thread_slot, login_to_post_hint_markup,
         parse_html_ui_from_form, room_members_section_markup, thread_feed_html,
         thread_feed_html_for_room, thread_feed_region_markup, thread_ui_collapse_redacted_post,
         thread_ui_expand_post_full, thread_ui_expand_redacted_post, ui_js_warn, user_can_post_room,
@@ -139,24 +139,24 @@ async fn dispatch_ui_action(
                 }
             }
         }
-        HtmlUiAction::ExpandPublicNewThreadForm => {
-            let reduced = state.reduced.read().await;
-            let user = session.map(|s| s.username.as_str());
-            drop(reduced);
-            let markup = if user.is_some() {
-                fragment_public_new_thread_form(true)
-            } else {
-                login_to_post_hint_markup()
-            };
-            JsBuilder::new()
-                .morph_inner_selector("#public-new-thread-ui-slot", markup)
-                .into_response()
-        }
-        HtmlUiAction::ExpandRoomNewThreadForm { room_wire } => {
+        HtmlUiAction::ExpandNewThreadForm { room_wire } => {
             let room_wire = room_wire.trim().to_string();
             if room_wire.is_empty() {
                 return ui_js_warn("missing room").into_response();
             }
+            if room_wire == "public" {
+                let reduced = state.reduced.read().await;
+                let user = session.map(|s| s.username.as_str());
+                drop(reduced);
+                let markup = if user.is_some() {
+                    fragment_new_thread_slot(&ThreadNav::public(), true, false)
+                } else {
+                    login_to_post_hint_markup()
+                };
+                return JsBuilder::new()
+                    .morph_inner_selector("#new-thread-ui-slot", markup)
+                    .into_response();
+            }
             let reduced = state.reduced.read().await;
             let user = session.map(|s| s.username.as_str());
             if !reduced.rooms.contains(&room_wire) {
@@ -176,12 +176,12 @@ async fn dispatch_ui_action(
                 return ui_js_warn("bad room").into_response();
             };
             let markup = if can_post {
-                fragment_room_new_thread_form(&nav, true, false)
+                fragment_new_thread_slot(&nav, true, false)
             } else {
                 login_to_post_hint_markup()
             };
             JsBuilder::new()
-                .morph_inner_selector("#room-new-thread-ui-slot", markup)
+                .morph_inner_selector("#new-thread-ui-slot", markup)
                 .into_response()
         }
         HtmlUiAction::SetRoomMembersExpanded { room_wire, expanded } => {
@@ -205,11 +205,28 @@ async fn dispatch_ui_action(
                 .morph_selector("#room-members-section", markup)
                 .into_response()
         }
-        HtmlUiAction::SetRoomNewThreadComposeExpanded { room_wire, expanded } => {
+        HtmlUiAction::SetNewThreadComposeExpanded { room_wire, expanded } => {
             let room_wire = room_wire.trim().to_string();
             if room_wire.is_empty() {
                 return ui_js_warn("missing room").into_response();
             }
+            if room_wire == "public" {
+                let reduced = state.reduced.read().await;
+                let _user = session.map(|s| s.username.as_str());
+                drop(reduced);
+                let can_post = session.is_some();
+                let nav = ThreadNav::public();
+                let markup = if can_post {
+                    fragment_new_thread_slot(&nav, true, expanded)
+                } else {
+                    login_to_post_hint_markup()
+                };
+                let mut b = JsBuilder::new().morph_inner_selector("#new-thread-ui-slot", markup);
+                if expanded && can_post {
+                    b = b.focus_selector("#new-thread-tag");
+                }
+                return b.into_response();
+            }
             let reduced = state.reduced.read().await;
             let user = session.map(|s| s.username.as_str());
             if !reduced.rooms.contains(&room_wire) {
@@ -229,13 +246,13 @@ async fn dispatch_ui_action(
                 return ui_js_warn("bad room").into_response();
             };
             let markup = if can_post {
-                fragment_room_new_thread_form(&nav, true, expanded)
+                fragment_new_thread_slot(&nav, true, expanded)
             } else {
                 login_to_post_hint_markup()
             };
-            let mut b = JsBuilder::new().morph_inner_selector("#room-new-thread-ui-slot", markup);
+            let mut b = JsBuilder::new().morph_inner_selector("#new-thread-ui-slot", markup);
             if expanded && can_post {
-                b = b.focus_selector("#room-new-tag");
+                b = b.focus_selector("#new-thread-tag");
             }
             b.into_response()
         }
diff --git a/server/src/html/forum/feed.rs b/server/src/html/forum/feed.rs
index 30b95ff6e85245a82199c96e41484d03c24989dd..1b4ae7baa3ad4757b74172d7b67f3f2b33d1075d 100644
--- a/server/src/html/forum/feed.rs
+++ b/server/src/html/forum/feed.rs
@@ -245,11 +245,13 @@ pub async fn home(
             p class="muted" { "dark = time-ordered · light = vote-ranked" }
             div class="thread-feed-toolbar" {
                 form method="POST" action="/ui" {
-                    input type="hidden" name=(UI_RPC_FIELD) value=(template_json_compact(&HtmlUiAction::ExpandPublicNewThreadForm).expect("static json"));
+                    input type="hidden" name=(UI_RPC_FIELD) value=(template_json_compact(&HtmlUiAction::ExpandNewThreadForm {
+                        room_wire: "public".into(),
+                    }).expect("static json"));
                     button type="submit" class="section-add-btn" { "+" }
                 }
             }
-            div id="public-new-thread-ui-slot" {}
+            div id="new-thread-ui-slot" {}
             (render_thread_feed(Some(&nav), "thread-feed", &public_rows, now))
             (cli_panel(&["npx slugsocial public forum list"]))
         },
diff --git a/server/src/html/forum/mod.rs b/server/src/html/forum/mod.rs
index 8fbf3b71f41fafb474999ebb3d622e6bd5b5f313..bd60703924eee5b0e5a7f69c0a2dc526442512a2 100644
--- a/server/src/html/forum/mod.rs
+++ b/server/src/html/forum/mod.rs
@@ -20,9 +20,7 @@ pub use profile::user_profile_page;
 pub use views::{room_page, room_thread_view, thread_view};
 
 pub(crate) use access::{user_can_post_room, user_can_view_room};
-pub(crate) use new_thread::{
-    fragment_public_new_thread_form, fragment_room_new_thread_form, login_to_post_hint_markup,
-};
+pub(crate) use new_thread::{fragment_new_thread_slot, login_to_post_hint_markup};
 pub(crate) use room_members::room_members_section_markup;
 pub(crate) use thread_morph::{
     thread_ui_collapse_redacted_post, thread_ui_expand_post_full, thread_ui_expand_redacted_post,
diff --git a/server/src/html/forum/new_thread.rs b/server/src/html/forum/new_thread.rs
index b2d24b8ab3ec7eee0edd16bed8ee6605e49bf408..4dd2654f953a7db6ee9b25c348f99906da93a94a 100644
--- a/server/src/html/forum/new_thread.rs
+++ b/server/src/html/forum/new_thread.rs
@@ -5,120 +5,48 @@ use serde_json::json;
 
 use super::nav::ThreadNav;
 
-struct NewThreadIds {
-    compose_section_id: &'static str,
-    errors_id: &'static str,
-    form_id: &'static str,
-    tag_input_id: &'static str,
-    text_input_id: Option<&'static str>,
-}
-
-const PUBLIC_IDS: NewThreadIds = NewThreadIds {
-    compose_section_id: "public-new-thread-compose",
-    errors_id: "public-new-thread-errors",
-    form_id: "public-new-thread-form",
-    tag_input_id: "new-thread-tag",
-    text_input_id: Some("new-thread-text"),
-};
-
-const ROOM_IDS: NewThreadIds = NewThreadIds {
-    compose_section_id: "room-new-thread-compose",
-    errors_id: "room-new-thread-errors",
-    form_id: "room-new-thread-form",
-    tag_input_id: "room-new-tag",
-    text_input_id: None,
-};
-
-#[derive(Clone, Copy)]
-enum NewThreadComposeKind {
-    /// Home page: no client-side check RPC; post template omits `error_target` / `form_id`.
-    Public,
-    /// Room page: `check_ingest` + error targets on post (matches thread compose).
-    Room,
-}
-
-/// Shared `<section class="compose">` for creating a thread + first post.
-fn new_thread_compose_section(room_wire: &str, ids: &NewThreadIds, kind: NewThreadComposeKind) -> Markup {
-    let client_check = matches!(kind, NewThreadComposeKind::Room);
-    let (tag_placeholder, text_placeholder, submit_label) = match kind {
-        NewThreadComposeKind::Public => (
-            "thread-title-slug-here",
-            "Hello threadgoers!! Behold my new thread!",
-            "create thread / make first post",
-        ),
-        NewThreadComposeKind::Room => (
-            "thread-topic-slug-here",
-            "First post body…",
-            "post",
-        ),
-    };
+/// Stable ids shared by public home and private room “new thread” UI (`#new-thread-ui-slot`).
+const COMPOSE_SECTION_ID: &str = "new-thread-compose";
+const ERRORS_ID: &str = "new-thread-errors";
+const FORM_ID: &str = "new-thread-form";
+const TAG_INPUT_ID: &str = "new-thread-tag";
 
+/// Shared `<section class="compose">`: check + post with `error_target` / `form_id` (same as thread reply compose).
+fn new_thread_compose_section(room_wire: &str) -> Markup {
     html! {
-        section class="compose" id=(ids.compose_section_id) {
-            div id=(ids.errors_id) {}
-            @if client_check {
-                form id=(ids.form_id) method="POST" action="/ui" data-check-action="/ui" data-check-rpc=(template_json_compact(&json!({
-                    "action": "check_ingest",
+        section class="compose" id=(COMPOSE_SECTION_ID) {
+            div id=(ERRORS_ID) {}
+            form id=(FORM_ID) method="POST" action="/ui" data-check-action="/ui" data-check-rpc=(template_json_compact(&json!({
+                "action": "check_ingest",
+                "room": room_wire,
+                "thread_tag": {"$form": "thread_tag"},
+                "text": {"$form": "text"},
+                "error_target": ERRORS_ID,
+                "form_id": FORM_ID,
+            })).unwrap()) {
+                input type="hidden" name=(UI_RPC_FIELD) value=(template_json_compact(&json!({
+                    "action": "post_ingest",
                     "room": room_wire,
                     "thread_tag": {"$form": "thread_tag"},
                     "text": {"$form": "text"},
-                    "error_target": ids.errors_id,
-             

… preview truncated; 13,556 characters omitted

download full diff A

B — c_8337e9dd0f87 (tommy-mor)

message

[d91e9431] fix(server): drop reduced read lock before room page layout

Precompute room_members_section_markup under the guard, then release
before layout/html stringify. Tokio RwLock queues new readers behind a
waiting writer; holding the read guard across rendering could stall
navigations when ingests take the write lock.

Made-with: Cursor

diff preview

diff --git a/server/src/html/forum.rs b/server/src/html/forum.rs
index f6e45b05bb92126966e304619f80ef1d45be7a4b..a2d36c0645cd5cdd97da78151a2fd574175e67f8 100644
--- a/server/src/html/forum.rs
+++ b/server/src/html/forum.rs
@@ -965,18 +965,23 @@ pub async fn room_page(
         drop(reduced);
         return (StatusCode::NOT_FOUND, "room not found").into_response();
     };
-    let slug_display = room_slug.as_str();
+    // Precompute markup that needs `&ReducerState`, then release the lock before `layout`.
+    // `tokio::sync::RwLock` queues new readers behind a waiting writer; holding the read
+    // guard across HTML/stringify could stall other navigations when an ingest needs a write lock.
+    let members_markup = room_members_section_markup(&reduced, &room_id, false);
     let forum_cli = format!("npx slugsocial private {room_id} forum list");
     let garden_cli = format!("npx slugsocial private {room_id} garden tree");
     let audit_cli = format!("npx slugsocial private {room_id} audit");
+    drop(reduced);
 
+    let slug_display = room_slug.as_str();
     let page = layout(
         &format!("room {slug_display} — slug.social"),
         "view-thread",
         html! {
             (strip)
             nav class="breadcrumb" { (bc_room(&nav, slug_display, None)) }
-            (room_members_section_markup(&reduced, &room_id, false))
+            (members_markup)
             h3 { "threads" }
             (render_thread_feed(Some(&nav), "room-thread-feed", &rows, now))
             @if show_new {
@@ -992,7 +997,6 @@ pub async fn room_page(
         theme_from_jar(&jar),
         &theme_next_from_uri(&uri),
     );
-    drop(reduced);
     Html(page.into_string()).into_response()
 }
 

download full diff B

Hardlinks — judgments / attempts / prompt

prompt download

judgments

attempts

Prompt text is loaded only by the download route.