Skip to content

fix(realtime): process every key in a presence diff's leaves - #1631

Open
hsusul wants to merge 1 commit into
supabase:mainfrom
hsusul:fix/realtime-presence-leaves
Open

hsusul wants to merge 1 commit into
supabase:mainfrom
hsusul:fix/realtime-presence-leaves

Conversation

@hsusul

@hsusul hsusul commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

What kind of change does this PR introduce?

Bug fix.

What is the current behavior?

Fixes #1630

AsyncRealtimePresence._sync_diff uses break where it means continue:

for key, left_presences in leaves.items():
    current_presences = self.state.get(key, [])

    if len(current_presences) == 0:
        break

When a presence_diff carries leaves for a key the client isn't tracking, the loop aborts and every remaining key in that diff is dropped. Those presences stay in presence.state indefinitely and on_presence_leave never fires for them — a ghost user in the UI until the next full presence_state resync.

leaves comes straight off the wire in _on_diff_event, so the server controls both the contents and the key order; a diff listing an untracked key before a tracked one is enough. A client sees untracked keys routinely — it joined after that user left, or already removed the key in an earlier diff.

What is the new behavior?

continue, so only the untracked key is skipped. This matches Phoenix's presence.js, which this file is a port of and which returns from the per-key callback there.

_sync/presence.py is a stub (SyncRealtimePresence has no state handling), so there is no sync counterpart to update.

Tests

test_sync_diff_processes_every_leave_key in src/realtime/tests/test_presence.py — feeds a diff whose leaves list an untracked key before a tracked one, and asserts the tracked key is removed from state and its on_leave callback fires. It fails on main (bob survives the diff, no callback) and passes here.

Validation

  • uv run --package realtime pytest src/realtime/tests/test_presence.py — 9 passed.
  • uv run --package realtime pytest src/realtime/tests — 25 passed, 2 failed. The two failures are test_connection.py::test_postgrest_changes and ::test_postgrest_changes_on_different_tables; they fail identically on main without this change in my environment, and are unrelated to presence.
  • uv run --package realtime mypy src/realtime/src/realtime src/realtime/tests — success, 20 source files.
  • uv run ruff check / uv run ruff format --check — clean.
  • git diff --check — clean.

The realtime suite runs against the local Supabase stack (make realtime.start-infra, which needs Docker and the Supabase CLI); I had it running, hence the live tests above passing.

Additional context

None.

_sync_diff broke out of the leaves loop when a key was not present in the
local state, so every remaining key in that diff was skipped: those
presences stayed in presence.state forever and on_presence_leave never
fired for them. Since leaves comes straight off the wire, a server diff
listing an untracked key before a tracked one is enough to trigger it.

Skip only the untracked key, matching Phoenix's presence.js.

Fixes supabase#1630
@hsusul
hsusul requested review from a team and o-santi as code owners September 10, 2026 16:13
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.

Presence diff stops processing leaves after an untracked key

1 participant