Problem
join_group/join_group_with_key are idempotent per user_topic via the client's handle registry, but the registry entry is inserted only after the store-open .await completes. Two overlapping, un-awaited first joins of the same topic on the single-threaded wasm executor therefore both pass the registry-miss fast-path, both derive/build a WebGroupInit (each with its own broadcast channels), and both send Command::JoinGroup.
The engine's groups.contains_key guard correctly keeps the FIRST init and makes the second a no-op ack — group state is never clobbered, and writes from either handle work (they route by topic through the command channel). But the second caller's WebGroupHandle holds inbound_tx/resolved_tx senders the engine never publishes to: its subscribe()/subscribe_resolved() streams are silently dead. The registry ends up holding whichever handle inserted last, so even client.group(topic) can return the dead-channel handle.
Not reachable with typical sequential/awaited usage (the Dioxus flow awaits each join); requires two un-awaited joins of the same brand-new topic in one tick. Auto-rejoined groups are unaffected (built in one synchronous pass).
Fix sketch
After the engine ack in join_group_inner, re-check the registry and return the existing entry if one appeared, instead of inserting unconditionally. Alternative: have the JoinGroup ack carry back the channels of the group the engine actually kept, and build the returned handle from those.
Found during the #93 review; see web_engine.rs join_group_inner (registry insert after the awaits) and the engine's JoinGroup handler (contains_key guard).
Problem
join_group/join_group_with_keyare idempotent peruser_topicvia the client's handle registry, but the registry entry is inserted only after the store-open.awaitcompletes. Two overlapping, un-awaited first joins of the same topic on the single-threaded wasm executor therefore both pass the registry-miss fast-path, both derive/build aWebGroupInit(each with its own broadcast channels), and both sendCommand::JoinGroup.The engine's
groups.contains_keyguard correctly keeps the FIRST init and makes the second a no-op ack — group state is never clobbered, and writes from either handle work (they route by topic through the command channel). But the second caller'sWebGroupHandleholdsinbound_tx/resolved_txsenders the engine never publishes to: itssubscribe()/subscribe_resolved()streams are silently dead. The registry ends up holding whichever handle inserted last, so evenclient.group(topic)can return the dead-channel handle.Not reachable with typical sequential/awaited usage (the Dioxus flow awaits each join); requires two un-awaited joins of the same brand-new topic in one tick. Auto-rejoined groups are unaffected (built in one synchronous pass).
Fix sketch
After the engine ack in
join_group_inner, re-check the registry and return the existing entry if one appeared, instead of inserting unconditionally. Alternative: have the JoinGroup ack carry back the channels of the group the engine actually kept, and build the returned handle from those.Found during the #93 review; see
web_engine.rsjoin_group_inner(registry insert after the awaits) and the engine's JoinGroup handler (contains_keyguard).