Skip to content

feat: rejoin backoff, duplicate binding dedup and aligned binding ids - #46

Merged
zoedsoupe merged 3 commits into
mainfrom
feat/rt-rejoin-dedup
Oct 16, 2026
Merged

zoedsoupe merged 3 commits into
mainfrom
feat/rt-rejoin-dedup

Conversation

@zoedsoupe

Copy link
Copy Markdown
Member

Problem

Three gaps in the realtime client:

  • A channel whose join fails while the socket stays up stays :errored until the next socket reconnect. Phoenix retries a failed join on its own with backoff; the client did not.
  • Adding an identical postgres_changes binding is silently accepted, but the server collapses identical filters into one subscription, which desyncs the client's positional binding ids.
  • While investigating that, we found the ids were already misaligned with two or more distinct bindings: the join payload was emitted newest-first while the reply handling reverse-zipped as if it were oldest-first, so each stored binding could be assigned another binding's id.

Solution

  • Add Supabase.Realtime.Rejoin, a pure module holding the phoenix backoff sequence (1s, 2s, 5s, then 10s), and a per-channel rejoin timer owned by the Registry. A failed join (error/timeout reply, phx_error, failed leave) arms a retry that re-joins with a fresh join_ref and token. The timer is cleared on :open/:closed, on a successful join, on unsubscribe and on close. A :rejoin_after_ms start option overrides the sequence.
  • Reject a duplicate postgres_changes binding at the on/3 boundary with {:error, :duplicate_binding}, comparing event, schema, table, filter and select.
  • Emit the join payload's postgres_changes entries oldest-first, matching realtime-js, so the server's positional ids line up with the stored bindings.
  • Fix the Realtime.send/3 docs, which showed broadcasting through the raw send path.

Rationale

The timer lives in the Registry because every join and cancel trigger already lands there; putting it in the Connection would need new cross-process messages. Dedup returns a tuple rather than silently ignoring the duplicate (as realtime-js does) to match the boundary contract already set by :invalid_select. The payload-order fix makes the existing reverse-zip correct without touching it, and matches the reference client.

@zoedsoupe
zoedsoupe force-pushed the feat/rt-postgres-observability branch from 9d32623 to 880a3af Compare October 16, 2026 22:17
@zoedsoupe
zoedsoupe force-pushed the feat/rt-rejoin-dedup branch from 2310ad1 to 0023ea7 Compare October 16, 2026 22:17
@zoedsoupe
zoedsoupe force-pushed the feat/rt-postgres-observability branch 2 times, most recently from 6b555b9 to 9560012 Compare October 16, 2026 22:24
Base automatically changed from feat/rt-postgres-observability to main October 16, 2026 22:24
@zoedsoupe
zoedsoupe force-pushed the feat/rt-rejoin-dedup branch from 0023ea7 to 7170489 Compare October 16, 2026 22:26
@zoedsoupe
zoedsoupe merged commit ce458d9 into main Oct 16, 2026
24 checks passed
@zoedsoupe
zoedsoupe deleted the feat/rt-rejoin-dedup branch October 16, 2026 22:36
@zoedsoupe zoedsoupe mentioned this pull request Oct 16, 2026
zoedsoupe added a commit that referenced this pull request Oct 19, 2026
🚀 Want to release this?
---


##
[0.6.0](v0.5.0...v0.6.0)
(2026-10-16)


### Features

* filter builder, select, heartbeat telemetry and presence fixes
([#44](#44))
([6fa0dae](6fa0dae))
* rejoin backoff, duplicate binding dedup and aligned binding ids
([#46](#46))
([ce458d9](ce458d9))
* serializer behaviour and vsn 2.0.0 binary framing
([#43](#43))
([3aec1d6](3aec1d6))


### Bug Fixes

* realtime protocol correctness (join, ack, heartbeat, tls)
([#42](#42))
([73548ac](73548ac))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).
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