Repository navigation
Conversation
A channel is SUBSCRIBED as soon as it joins, which can be before the server
has set up its postgres_changes subscription, or refused it. Like
realtime-js's postgres_changes_options, postgresChangesOptions { wait = true }
asks the server (Realtime with supabase/realtime#2109) to hold the join until
the subscription is set up, so SUBSCRIBED means changes are being received.
If it can't be set up, the server rejects the join and the channel retries
it like any other channel error. Off by default; servers without support
ignore it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Like realtime-js's CHANNEL_ERROR, errorFlow() emits a RealtimeChannelError for a rejected join, with the server's reason (e.g. a postgres changes subscription it refused for a channel that waits for it), and for a phx_error. The reason used to reach only the logs. A failed reply to an ordinary push isn't a channel error and isn't emitted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A join the server never answered waited forever. Like realtime-js, each join attempt now times out after Realtime.Config.timeout (10 s by default): the channel emits a TIMED_OUT error on errorFlow() and rejoins, which counts toward maxAttempts. For a channel that waits for its postgres changes subscription, the timeout outlasts the server's wait, as in realtime-js. Rejected joins, channel errors and timeouts now share one path that reports the error, then rejoins or gives up. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
subscribe(blockUntilSubscribed = true) waited for SUBSCRIBED with no other way out, so it never returned once the channel gave up or was closed. It also hung if the channel subscribed and closed before the wait started, since the wait then missed SUBSCRIBED. It now waits on the status together with why the channel stopped joining, both state, so a late wait still sees the outcome. When the channel gives up or is closed, it throws RealtimeChannelJoinException, with the error it gave up after (null for a close). A lost connection doesn't end the wait, since the channel rejoins when the client reconnects. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Four commits, each building and passing the realtime tests on its own, so they can be reviewed one at a time.
What changed
SUBSCRIBEDas soon as the server accepts the join, which can be before the server has set up itspostgres_changessubscription, or refused it. Like realtime-js'spostgres_changes_options,postgresChangesOptions { wait = true }in the channel builder asks the server (supabase/realtime#2109) to hold the join until the subscription is set up. With it,SUBSCRIBEDmeans changes are being received. If the subscription can't be set up, the server rejects the join and the channel retries like after any other channel error.timeoutsets how long the server waits. The option is off by default, and servers without support ignore it.errorFlow()reports channel errors, like realtime-js'sCHANNEL_ERROR. ARealtimeChannelErroris emitted for a rejected join, with the server's reason (for exampleRealtimeDisabledForConfiguration: ...for a refused subscription), and for aphx_error. Before, the reason only reached the logs. A failed reply to an ordinary push isn't a channel error and isn't emitted.TIMED_OUT. A join the server never answered used to wait forever. Each join attempt now times out after the newRealtime.Config.timeout(10 s, realtime-js's default). A timeout emits aTIMED_OUTerror onerrorFlow()and rejoins, and the rejoin counts towardmaxAttempts. When the channel has postgres changes and waits for them, the timeout is extended past the server's wait, as in realtime-js:max(timeout, (options.timeout ?: 15 s) + 10 s). Rejected joins, channel errors and timeouts now share one path that reports the error, then rejoins or gives up.subscribe(blockUntilSubscribed = true)ends when the channel stops joining. It waited forSUBSCRIBEDwith no other way out, so it never returned once the channel gave up or was closed. It also hung if the channel subscribed and then closed before the wait started, because the wait missedSUBSCRIBED. Now it waits on the status together with why the channel stopped joining. Both are state, so a wait that starts late still sees the outcome. When the channel gives up or is closed, the call throws the newRealtimeChannelJoinException, which carries the error the channel gave up after (null for a close). A lost connection doesn't end the wait, because the channel rejoins when the client reconnects.Behavior changes
maxAttempts(5 by default). Before, it waited forever.subscribe(blockUntilSubscribed = true)throwsRealtimeChannelJoinExceptionin the cases where it used to hang.errorFlow()is a new member of theRealtimeChannelinterface, so classes outside the library that implementRealtimeChannelneed to add it.Differences from realtime-js
subscribe(callback, timeout).rejoinDelayandmaxAttempts. realtime-js (through Phoenix) backs off and retries forever.Why
We hit the failure that
waitis for in production. An app release subscribed to a table that wasn't in thesupabase_realtimepublication yet. The channel reportedSUBSCRIBED, and the refusal arrived later as asystemmessage that the app didn't check. Withwait, the channel doesn't reportSUBSCRIBEDuntil the subscription is set up, and the reason for a refusal comes througherrorFlow(). The timeout and the blocking subscribe fix close the cases where a channel would otherwise wait forever.Testing
RealtimeChannelTest: the options are sent with the join (wait, andtimeoutin milliseconds), and nothing is sent by default.RealtimeChannelRejoinTest, againstScriptedWebsocket:phx_errorreacherrorFlow(), with the reason for the rejected join. The error that makes the channel give up is emitted too. A failed reply to an ordinary push isn't emitted.maxAttempts. An answered join doesn't time out. A rejected join doesn't also time out while its rejoin is pending.waitand a servertimeout.CallbackManagerTest: error callbacks.UNSUBSCRIBED, and a close that isn't recorded../gradlew :realtime-kt:jvmTest :realtime-kt:detektpass. The JS, Wasm, LinuxX64 and common metadata targets compile.Not changed here
maxAttemptsisUNSUBSCRIBED, but it doesn't send a leave and isn't removed fromsubscriptions.🤖 Generated with Claude Code