- Add RenderTrigger enum to connection.rs (avoids circular imports)
- Replace ConnectionState.sse_sender with trigger_tx + pipe_tx channels
- Add get_trigger_sender and get_pipe_sender to ConnectionManager
- Extract render_and_send helper to eliminate duplicated render/SSE logic
- Implement connection_task: long-lived per-connection coroutine that owns
the SSE pipe, debounces render triggers (2ms), and survives disconnects for
a 30s grace period to allow clean reconnection
- Update sse_handler: spawns connection_task on fresh connect, hands new SSE
pipe to existing task on reconnect; removes cleanup-on-disconnect spawn
- Update spawn_fanout: now a pure router — sends RenderTrigger::Invalidated
to each subscribed connection's task; no render logic remains in fanout
- Bump broadcast channel capacity from 256 to 16384 in Store::new
- Merge brotli compression branch (config hot-reload, BrotliBody middleware)
https://claude.ai/code/session_01QiTaFtwXETxJDk79XcgfrN
Key changes:
- **src/config.rs** – `LiveVueConfig` (server host/port, brotli quality +
window size), loaded from `livevue.toml`. A notify-based file watcher
reloads the config on any file change; a SIGHUP handler does the same on
demand. Falls back to compiled-in defaults when the file is absent.
- **src/brotli_layer.rs** – `BrotliBody`, a custom `http_body::Body` wrapper
that streams brotli-compressed data through a single persistent
`brotli::CompressorWriter`. The encoder survives across SSE event flushes,
so its sliding window accumulates context from all prior events —
progressively better compression as the stream grows. Both quality (0–11)
and window size (lgwin 10–24, i.e. 1 KB – 16 MB) are taken from config.
`brotli_compression` is an axum `from_fn_with_state` middleware that
activates only when the client sends `Accept-Encoding: br`.
- **src/server.rs** – `AppState` gains a `config: SharedConfig` field.
`AppState::new` takes the config; `AppState::new_default` is a zero-config
convenience constructor.
- **livevue.toml** – documented example config with inline comments
explaining each field and the brotli window-size tradeoff table.
- **examples/todo** – loads config via `load_and_watch_config`, derives the
bind address from `config.server`, and applies `brotli_compression` as a
router layer.
https://claude.ai/code/session_01UnQQkkwts64FPUsSzFfdQb
- Add `publish_key()` default method to `Query` trait (returns `None`)
- Add `mutation_exec!` macro for SQLite INSERT/UPDATE/DELETE ops; overrides
`publish_key()` so the mutation carries its own invalidation key
- Add `Store::exec()` which runs a mutation and auto-publishes its key —
action handlers no longer need a separate `store.publish()` call
- Add `PgQuery` trait + `pg_query_one!` / `pg_query_all!` macros
(feature-gated on `pg_replication`) mirroring the SQLite Query API
- Add `cx.run_pg(pool, query)` to `RenderContext` which executes a PgQuery
and auto-subscribes its cache key — no more manual `cx.subscribe()` calls
- Update todo example: replace plain mutation fns with `mutation_exec!` structs
and `store.exec()` calls; remove `action_then_publish` helper
- Update pg_replication example: add `ListMessages` via `pg_query_all!` and
replace manual subscribe + raw query with `cx.run_pg()`
https://claude.ai/code/session_01HXe8rAZPweU9j2piBo9MdG
Implements a PostgreSQL logical replication module that streams WAL
changes, buffers them per XID for transaction atomicity, and publishes
CacheKey invalidation events to the existing framework broadcast channel.
Key design choices:
- Transaction atomicity: changes are buffered until COMMIT so the fanout
always receives a consistent view. A 1000-row bulk-insert emits one
table-level CacheKey, not 1000 row events.
- Native protocol: uses a self-contained raw TCP + postgres-protocol
implementation for the replication connection (tokio-postgres 0.7 does
not expose copy_both_simple publicly).
- Corrected pgoutput v1 parser: original maybe_sql_integration code
incorrectly read XID from DML messages; DML messages carry no XID in
proto v1 — only Begin does.
- Clean integration: publishes CacheKey::Channel events to store.events
so the existing fanout task picks them up with zero changes to the
fanout loop.
- New cx.subscribe(key) API on RenderContext for manually registering
subscription keys (needed when queries go through PgPool, not cx.run).
New files:
src/pg_replication/mod.rs — PgReplicationListener, key helpers
src/pg_replication/wal_parser.rs — pgoutput v1 binary protocol parser
src/pg_replication/emitter.rs — WalEmitter (tx-buffering + CacheKey emit)
src/pg_replication/proto.rs — raw TCP PG wire-protocol connection
examples/pg_replication/main.rs — live message board example
docker-compose.yml — PG 16 container with wal_level=logical
Run the example:
docker compose up -d
cargo run --example pg_replication --features pg_replication
https://claude.ai/code/session_01SLGgXeqKV2o7KTmCaQZZPg
Three bugs preventing any SSE events from reaching clients:
1. SubscriptionRegistry::clone() deep-cloned the DashMaps instead of sharing
them. The sse_handler spawned a task with a disconnected clone, so
subscriptions.update() wrote into a throwaway copy — the fanout always saw
an empty registry and found zero connections to push to. Fixed by wrapping
both DashMaps in Arc so clone() is a cheap pointer copy sharing live state.
2. The SSE event data payload used .join("\ndata: ") to pre-embed the `data:`
prefix into the string, then handed it to axum's Event::data() which does
its own \n-splitting to add `data:` prefixes. This produced double-prefixed
lines (`data: data: elements ...`) that DataStar couldn't parse. Fixed by
using .join("\n") and letting axum format the data lines correctly.
3. The fanout skipped connections with None signals (new connections that had
not yet received a client action). This broke clock-style server-push events
on fresh connections. Fixed by falling back to "{}" instead of continue-ing.
https://claude.ai/code/session_0168cLRd1wr6LK9FjBsjA37W
Two bugs that caused signals to not work on the frontend:
1. data-on-signal-change → data-on-signal-patch
The data-on-signal-change attribute was removed in RC8.
The replacement is data-on-signal-patch.
2. data-bind-newTodo="" → data-bind="newTodo"
HTML normalises attribute names to lowercase at parse time, so
data-bind-newTodo becomes data-bind-newtodo in the DOM. DataStar
would then bind to signal $newtodo instead of $newTodo, breaking
the two-way binding. The value syntax data-bind="newTodo" preserves
the camelCase name correctly.
https://claude.ai/code/session_01JThiQbp3Bn9J1VhBpRuz4u