Skip to content

fix(relay): refresh NIP-29 discovery snapshot on already-member join (#3796) - #4277

Open
iroiro147 wants to merge 1 commit into
block:mainfrom
iroiro147:fix/3796-join-selfheal
Open

fix(relay): refresh NIP-29 discovery snapshot on already-member join (#3796)#4277
iroiro147 wants to merge 1 commit into
block:mainfrom
iroiro147:fix/3796-join-selfheal

Conversation

@iroiro147

Copy link
Copy Markdown

What

When a kind:9021 join request arrives from an actor that is already an active member of the channel, the relay previously returned Ok(()) after logging kind:9021 join — already a member, skipping. That is correct for membership state (no duplicate join notifications, no redundant add_member write) but gives the requester no repair path when the relay-side kind:39002 discovery snapshot is missing or stale.

Issue #3796 is exactly that: the reporter's channel_members rows are correct (role=owner, removed_at IS NULL, hidden_at IS NULL), yet after a Desktop relaunch the sidebar hides the starter #general channel and shows "Join to participate". Clicking Join reaches the relay, logs the skip line, and the Desktop state never converges — because Desktop's get_channels Step 1 derives isMember from kind:39002 events with #p: [my_pubkey], and nothing on the wire refreshes that snapshot in response to the redundant join.

Fix

Keep the membership fast-path (no duplicate notification, no extra add_member write) but add a best-effort refresh of emit_group_discovery_events inside the already-member branch.

  • replace_addressable_event is a no-op write when the member list is unchanged, so the refresh is idempotent for healthy relays.
  • It self-heals any relay whose kind:39002 was lost, replaced without the requester's p-tag, or never emitted for this channel (e.g. manual DB surgery, a prior emission that raced and failed, restores from backup).
  • The requesting client converges on the very next fetch: Desktop re-queries get_channels after join_channel succeeds (AppShell.tsx:482-486 invalidates channelsQueryKey), and in-flight subscribers receive the addressable replace via the existing dispatch_persistent_event fan-out.

What does NOT change

  • The non-member path is untouched: add_memberinvalidate_membershipemit_system_message(member_joined)emit_group_discovery_eventsemit_membership_notification runs exactly as before.
  • Failed is_member_cached lookups still fail closed (the ? operator propagates the error before the skip branch is entered).
  • No API, schema, or client-visible contract change — only an extra relay-signed addressable event write per redundant join click.

Cost

One extra kind:39002 addressable replace per duplicate-join click. The rate of such clicks is bounded by user behaviour (the button is only visible when Desktop thinks the user is a non-member, so this only fires in exactly the broken state being repaired). For healthy relays the button is invisible, so the extra write never triggers in normal usage.

Reporter's expectation

"If a redundant kind-9021 join is sent, the relay's 'already a member' success/no-op response should reconcile Desktop to the joined state rather than leaving it as a non-member."

This commit is that reconciliation.

Tests

  • cargo check -p buzz-relay --lib — clean
  • cargo clippy -p buzz-relay --lib -- -D warnings — clean
  • cargo test -p buzz-relay --lib side_effects — 5/5 passing (unchanged pure-function tests; full DB-backed integration is requires Postgres gated separately)

Refs #3796

Signed-off-by: Sarthak Singh sarthak.singh@juspay.in

…lock#3796)

When a kind:9021 join request arrives from an actor that is already an
active member of the channel, the relay previously returned Ok(()) without
doing anything. That is correct for membership state — the row is already
present — but it gives the requester no repair path if the relay-side
kind:39002 discovery snapshot is missing or stale.

The reporter in block#3796 hit exactly that: their `channel_members` rows are
correct (role=owner, removed_at IS NULL, hidden_at IS NULL), yet after a
Desktop relaunch the sidebar hid the starter `#general` channel and showed
"Join to participate". Clicking Join reached the relay, got the
"already a member, skipping" log line, and the Desktop state never
converged — because Desktop's `get_channels` Step 1 derives `isMember`
from `kind:39002` events with `#p: [my_pubkey]`, and nothing refreshed
that snapshot in response to the redundant join.

This commit keeps the membership fast-path (no duplicate join
notifications, no redundant `add_member` write) but adds a best-effort
refresh of `emit_group_discovery_events` inside the already-member branch.
`replace_addressable_event` is a no-op write when the member list is
unchanged, so the refresh is idempotent for healthy relays and self-heals
any relay whose kind:39002 was lost, replaced without the requester's
p-tag, or never emitted for this channel.

The requesting client has no other signal to converge on:

- Desktop re-queries `get_channels` after `join_channel` succeeds, so the
  new snapshot is picked up on the very next fetch.
- In-flight subscribers for the channel receive the addressable replace
  via `dispatch_persistent_event`, so the change lands live too.

No behavioural change for the non-member path (the existing
`add_member` + `emit_group_discovery_events` sequence is unchanged), and
no change for failed `is_member_cached` lookups (still fail closed).

Refs block#3796

Signed-off-by: Sarthak Singh <sarthak.singh@juspay.in>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant