- 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