feat(communication): server-pushed chat events via SSE #44
No reviewers
Labels
No labels
prio_critical
prio_low
type_bug
type_contact
type_issue
type_lead
type_question
type_story
type_task
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lhumina_code/hero_osis!44
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "development_messaging_sse_subscribe"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Server-pushed chat events over Server-Sent Events. Recipients now see new messages live without reloading the page — closing the messaging archipelago's pull-only data flow.
Wires the per-domain extension hook from hero_rpc PR #39 (now merged on
development) to mount aweb_events.sockon thecommunicationdomain. hero_router auto-discovers it via the standardweb_<name>.sockconvention, exposing/hero_osis_communication/eventsto browsers.Related Issue
Closes (UI half — see hero_archipelagos PR for the consumer): lhumina_code/hero_archipelagos#182
Changes
crates/hero_osis_server/src/communication/server/sse.rs(new, ~480 lines incl. 11 tests)ChatEventenum: tagged JSON{type: "chatmessage.new" | "conversation.updated", data: {...}}wire format.CommunicationBroadcaster: process-widetokio::sync::broadcast(capacity 256) withLagged→resyncSSE-frame conversion.(conversation_sid → participant_keys)cache, primed byconversation_trigger_save_postand_get_post.chatmessage_trigger_save_postreads the cache to embed participants in the published event so the SSE delivery filter is O(participants) at delivery, no storage round-trip on the hot path.set_conversation_lookupoverride hook for consumer crates that need a different resolution strategy.events_handlerreadsX-Public-Keyheader (or?public_key=…query fallback forweb_sys::EventSourcewhich can't set headers), filters per-subscriber, wraps asaxum::response::sse::Ssewith 15 s keepalive.TODOto flip back to fail-closed once real authentication lands. Rationale: the messaging archipelago still runs on theSYSTEM_KEY = "system"placeholder; without the carve-out the QA repro for #182 silently drops everything at the SSE boundary.crates/hero_osis_server/src/communication/server/{mod.rs, rpc.rs}mod.rs: declarespub mod sse;and re-exports the public surface (ChatEvent,CommunicationBroadcaster,publish,subscribe,sse_router, etc.).rpc.rs: trigger functions emit events (conversation_trigger_save_post,conversation_trigger_get_post,chatmessage_trigger_save_post). The_get_postcache priming closes the cold-start case where conversations created before process boot would otherwise have emptyparticipant_keys.crates/hero_osis_server/build.rsOschemaBuildConfig::with_domain_router_ext("communication", "hero_osis_server::communication::server::sse_router")so the generator emitsextension: Some(sse_router())for the communication domain. Other domains keepextension: Noneand behave unchanged.crates/hero_osis_server/src/bin/hero_osis.rsextension:field on theDomainServiceliteral for each domain.crates/hero_osis_sdk/src/communication/{events.rs, mod.rs}(sdk/events.rs is new)ChatEvent: SDK-side mirror of the server enum.SubscribeErrorenum:Resync(u64)/Disconnected/SseError(String)(sibling enum becauseClientErrorlives in the upstreamhero_rpc_clientcrate).CommunicationClientis now a thin wrapper struct around the generated client withDerefforwarding — all existing RPC methods keep working unchanged via deref coercion. Addssubscribe(public_key)with cfg-gated implementations:text/event-streamconsumer with a line-oriented SSE parser.web_sys::EventSource): query-string fallback for context + public_key (EventSource can't set custom headers),Dropguard closes the underlying connection on stream drop.subscribe()ends withErr(SubscribeError::Disconnected)when the connection drops, no hidden retry surprises.Cargo deps
hero_osis_server: addstokio-stream(forBroadcastStream) andfutures(forStream/StreamExt).hero_osis_sdk: cfg-gated additions —reqweststreamfeature +bytes(native),wasm-bindgen-futures+js-sys+web-sysEventSourcefeature (wasm32).futurescommon.Test plan
cargo test -p hero_osis_server -p hero_osis_sdk --lib— all 124 server tests + 5 SDK tests pass.communication::server::sse::unit tests cover: broadcaster mechanics, JSON tag shape, anonymous/participant filter, trigger pipeline (save_post + get_post cache priming), cache-miss safe-fail,resolve_caller_pubkeyprecedence (header > query).cargo check -p hero_osis_sdk --target wasm32-unknown-unknown— clean.Notes
Generator regen
build.rs regenerates
*_generated.rsfiles in every domain crate when invoked, picking up the newextension:emission from hero_rpc PR #39's generator. Those regenerated files are intentionally not included in this PR — the diff would be ~30k lines of mechanical churn (whitespace +extension: Nonefield additions for every non-communication domain) that buries the actual SSE work.A separate follow-up PR can land the regen snapshots if they prove necessary for CI tree-dirty checks.
Pre-existing local-dev pin
This PR doesn't touch the
[patch."https://forge.ourworld.tf/lhumina_code/hero_lib.git"]block in workspaceCargo.toml(still pinned to/home/omar/hero/code0/...). That's a pre-existing local-dev artefact tracked separately.Race fix in messaging archipelago
The companion hero_archipelagos PR includes a small race fix in
archipelago.rs::handle_send— when the SSE event echo of the user's own send arrives before the optimistic-insert, the existing entry is now upgraded (is_own = true,sender_name = "Me") instead of being dedup'd out. Without that, the sender briefly saw a gray bubble for their own message because SSE won the race andchatmessage_to_messagedatadefaultsis_own = falseunder the placeholder identity scheme.Sequencing
This is PR 2 of 3 for issue #182: