feat(realtime)!: support asynchronous encode/decode codecs - #1684
Draft
spydon wants to merge 2 commits into
Draft
Conversation
Contributor
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
The codec overrides were synchronous and worked on raw maps, so the JSON work always ran on the main isolate and a large payload could block the event loop. RealtimeEncode and RealtimeDecode now take and return a RealtimeMessage and return a Future, so serialization can run on a background isolate. RealtimeMessage carries joinRef, ref, topic, event and payload and converts to and from the shape a protocol version puts on the wire, so a codec only has to turn that shape into bytes and back. Encoding starts as soon as a message is pushed, but the write to the sink is chained onto the previous pending write, so frames reach the socket in push order even when a later encode completes first. Incoming frames chain the same way, so messages are dispatched in receive order. A failed encode or decode drops only its own message. encode and decode are null unless one is passed, and the built-in codec is used while they are. That codec is synchronous, so a client that does not override it writes and dispatches without a microtask hop. Closes #1401
Without this an isolate codec is only reachable by constructing a RealtimeClient by hand, which a supabase_flutter user never does.
spydon
force-pushed
the
realtime-async-codec
branch
from
August 12, 2026 13:01
9009144 to
3973bbd
Compare
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.
Closes #1401
What
The realtime codec overrides (
RealtimeEncode/RealtimeDecode, added in #1397) were synchronous and worked on rawMap<String, dynamic>s, sojsonEncode/jsonDecodeof every message ran on the main isolate. A large payload blocks the event loop, and a background-isolate codec could not be plugged in at all.Both typedefs now work on a
RealtimeMessageand return aFuture:Object Function(Map<String, dynamic>)Future<Object> Function(RealtimeMessage)Map<String, dynamic> Function(Object)Future<RealtimeMessage> Function(Object)RealtimeClientOptionscarries the two callbacks throughSupabaseClientandSupabase.initialize, so asupabase_flutteruser never has to construct aRealtimeClientby hand.RealtimeMessageA new exported class carrying
joinRef,ref,topic,eventandpayload, withtoJson([version])andRealtimeMessage.fromJson(json, [version])converting to and from the shape each protocol version puts on the wire (positional array for2.0.0, object for1.0.0, both defaulting to2.0.0). A codec only has to turn that shape into bytes and back rather than hand-building the positional array.It replaces the internal
Messageclass. An@internalRealtimeMessage.outgoingbuilds one from aChannelEventand replacesBindings in the payload with their serializable shape, which matters more now: aBindingholds a callback and could never be sent to an isolate.Ordering
Introducing awaits into the message pipeline is the risky part, so both directions are chained:
Fast path
encodeanddecodearenullunless one is passed, and the built-in codec is used while they are. That codec is synchronous, so a client that does not override it writes and dispatches without a microtask hop, and noFutureOris needed anywhere.Tests
async_codec_test.dart: completion-order scrambling in both directions, an immediate codec queued behind a slow one, failure isolation, buffered flush ordering, and the no-hop fast path for the built-in codec.realtime_message_test.dart: event naming, binding stripping, andtoJson/fromJsonround trips per protocol version.An integration test runs a real
Isolate.runcodec against the local Realtime server, delaying each broadcast in reverse order so the encodes and decodes complete backwards. It fails without the chaining and passes with it.serializer_test.dart,socket_test.dartandheartbeat_test.dartported to the new types.client_test.dartcovers theRealtimeClientOptionspassthrough reaching the realtime client, and the default stayingnull.sdk-compliance.yamlregisters the newRealtimeMessageandRealtimeClientOptionssymbols; the symbol and drift checks pass locally.Performance
The message class costs nothing to introduce. Benchmarked against the old map-based path (best of 7 rounds per variant), a
RealtimeMessagereplaces aLinkedHashMapallocation in each direction and turns hash lookups plusascasts in the dispatch into field reads:AOT numbers track the same way. The 1 MB case is flat because
jsonEncode/jsonDecodeis essentially all of it, which is the case the async codec exists to move off the event loop.Breaking
Documented in
MIGRATION.md.