fix up pg replication example
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
container_name: livevue_postgres
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: livevue
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
- ./pg_hba.conf:/etc/postgresql/pg_hba.conf
|
||||
command:
|
||||
- postgres
|
||||
- -c
|
||||
- wal_level=logical
|
||||
- -c
|
||||
- max_replication_slots=10
|
||||
- -c
|
||||
- max_wal_senders=10
|
||||
- -c
|
||||
- hba_file=/etc/postgresql/pg_hba.conf
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres -d livevue"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
volumes:
|
||||
postgres_data:
|
||||
|
||||
@@ -37,13 +37,12 @@ use axum::{
|
||||
Router,
|
||||
};
|
||||
use maud::{html, Markup, DOCTYPE};
|
||||
use serde::Deserialize;
|
||||
use sqlx::FromRow;
|
||||
use uuid::Uuid;
|
||||
|
||||
use livevue_rs::{
|
||||
pg_replication::{pg_table_key, PgReplicationConfig, PgReplicationListener},
|
||||
server::{ingest_signals, ActionParams, AppState, SharedState},
|
||||
server::{ingest_signals, patch_signal, AppState, SharedState},
|
||||
spawn_fanout, RenderContext, Store,
|
||||
};
|
||||
|
||||
@@ -112,6 +111,7 @@ async fn render_page(cx: &mut RenderContext, rs: &RenderState) -> anyhow::Result
|
||||
.await?;
|
||||
|
||||
let new_msg = cx.signal("newMsg", livevue_rs::global());
|
||||
let _conn_id_signal = cx.signal("connId", livevue_rs::global());
|
||||
|
||||
Ok(html! {
|
||||
div #app {
|
||||
@@ -127,11 +127,10 @@ async fn render_page(cx: &mut RenderContext, rs: &RenderState) -> anyhow::Result
|
||||
type="text"
|
||||
placeholder="Type a message…"
|
||||
data-bind=(new_msg.name)
|
||||
data-on:keydown="{if (event.key === 'Enter') $post('/action/post_message')}"
|
||||
data-on:keydown="{if (event.key === 'Enter') @post('/action/post_message')}"
|
||||
;
|
||||
button
|
||||
data-on:click="$post('/action/post_message')"
|
||||
data-attr=(format!("{{disabled: {}.trim() === ''}}", new_msg.val))
|
||||
data-on:click="@post('/action/post_message')"
|
||||
{ "Send" }
|
||||
}
|
||||
|
||||
@@ -179,16 +178,14 @@ fn page_shell(conn_id: Uuid, signals_json: &str, inner: &Markup) -> String {
|
||||
// DataStar SDK (loaded from CDN for the example)
|
||||
script
|
||||
type="module"
|
||||
src="https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.0-beta.11/bundles/datastar.js"
|
||||
src="https://cdn.jsdelivr.net/gh/starfederation/datastar@1.0.0-RC.8/bundles/datastar.js"
|
||||
{}
|
||||
}
|
||||
body {
|
||||
div
|
||||
data-signals=(signals_json)
|
||||
data-on:load=(format!("@get('/sse?conn={conn_id}')"))
|
||||
{
|
||||
(inner)
|
||||
}
|
||||
body
|
||||
data-signals=(signals_json)
|
||||
data-init=(format!("@get('/sse?conn={conn_id}')"))
|
||||
{
|
||||
(inner)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -202,7 +199,7 @@ fn page_shell(conn_id: Uuid, signals_json: &str, inner: &Markup) -> String {
|
||||
async fn index_handler(State(state): State<(SharedState, RenderState)>) -> impl IntoResponse {
|
||||
let (app_state, rs) = &state;
|
||||
let conn_id = Uuid::new_v4();
|
||||
let signals_json = r#"{"newMsg":""}"#;
|
||||
let signals_json = &format!(r#"{{"newMsg":"","connId":"{}"}}"#, conn_id);
|
||||
app_state.connections.insert(conn_id, {
|
||||
// We need a sender for the connection, but the SSE handler will replace
|
||||
// it. Use a dummy channel that immediately drops.
|
||||
@@ -214,7 +211,7 @@ async fn index_handler(State(state): State<(SharedState, RenderState)>) -> impl
|
||||
.update_signals(&conn_id, signals_json.to_string());
|
||||
|
||||
let mut cx =
|
||||
RenderContext::new(conn_id, livevue_rs::SignalStore::from_json(serde_json::json!({"newMsg": ""})), Default::default(), app_state.store.clone());
|
||||
RenderContext::new(conn_id, livevue_rs::SignalStore::from_json(serde_json::json!({"newMsg": "", "connId": conn_id.to_string()})), Default::default(), app_state.store.clone());
|
||||
|
||||
let inner = render_page(&mut cx, rs).await.unwrap_or_else(|e| {
|
||||
html! { p { "Render error: " (e) } }
|
||||
@@ -232,18 +229,17 @@ async fn sse_adapter(
|
||||
livevue_rs::sse_handler(State(state.0), query).await
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct PostMessageBody {
|
||||
#[serde(rename = "newMsg")]
|
||||
new_msg: Option<String>,
|
||||
}
|
||||
|
||||
async fn post_message_handler(
|
||||
State(state): State<(SharedState, RenderState)>,
|
||||
axum::extract::Query(params): axum::extract::Query<ActionParams>,
|
||||
Json(body): Json<serde_json::Value>,
|
||||
) -> impl IntoResponse {
|
||||
let signals = ingest_signals(&state.0, params.conn, body);
|
||||
let conn_id = body
|
||||
.get("connId")
|
||||
.and_then(|v| v.as_str())
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or_else(Uuid::new_v4);
|
||||
|
||||
let signals = ingest_signals(&state.0, conn_id, body);
|
||||
let msg: String = signals
|
||||
.get("newMsg")
|
||||
.and_then(|v| v.as_str().map(|s| s.trim().to_string()))
|
||||
@@ -262,6 +258,9 @@ async fn post_message_handler(
|
||||
return StatusCode::INTERNAL_SERVER_ERROR;
|
||||
}
|
||||
|
||||
// Update the stored signals to reset the input field
|
||||
patch_signal(&state.0, &conn_id, "newMsg", serde_json::json!(""));
|
||||
|
||||
// Note: we do NOT call store.publish() here — the PgReplicationListener
|
||||
// will detect the INSERT through the WAL stream and fire the invalidation
|
||||
// automatically. This demonstrates the key value of the module: mutations
|
||||
@@ -384,4 +383,4 @@ async fn main() -> anyhow::Result<()> {
|
||||
axum::serve(listener_tcp, app).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
# docker/pg_hba.conf
|
||||
local all all trust
|
||||
host all all 0.0.0.0/0 trust
|
||||
host replication all 0.0.0.0/0 trust
|
||||
Reference in New Issue
Block a user