baaba_realtime_client 3.0.1
baaba_realtime_client: ^3.0.1 copied to clipboard
Real-time client for Flutter: ASP.NET Core SignalR, classic SignalR 2.x and WebSockets. Either-based errors, auto-reconnect, reliable sends, offline queue, test doubles.
3.0.1 #
Dependency constraints raised. No API or behaviour changes.
equatable^2.1.0→^3.0.0. An app that pinsequatableto 2.x must move to 3.x to resolve this version.http^1.1.0→^1.6.0,logging^1.2.0→^1.3.0,web_socket_channel^3.0.2→^3.0.3,internet_connection_checker_plus^3.1.1→^3.1.2.
3.0.0 #
Classic ASP.NET SignalR 2.x support, and the pieces an app otherwise builds around the client itself: catching up after a reconnect, sharing one socket, reliable sends, connection quality, an endpoint resolved per attempt, MessagePack, and a way for custom transports to own what they know better than the client does.
Migrating from 2.x
RealtimeConfig.uriis nowUri?. Code that readsconfig.urineeds a!or a null check; constructing a config is unchanged.RealtimeClientgainedreconnectedStream,quality,qualityStream,sendReliableanddeliveryStream. A custom implementation ofRealtimeClientmust add them;FakeRealtimeClientalready has.RealtimeTransportTypegainedsignalrClassic; an exhaustiveswitchover it needs a case.connect()while already connected now returnsRight(unit)instead of reopening the socket.HeartbeatMonitor'sonPingreturnsbool?(internal, but tests constructing it directly need areturn).
Added
- Classic SignalR 2.x —
RealtimeTransportType.signalrClassicwithClassicSignalROptions, backed by the newClassicSignalRTransport: negotiate / connect / start,invokecorrelated by invocation id (resolves with the hub's return value, fails withserverErrorwhen it throws), several hubs on one socket (hub.Methodroutes to a hub; events carrymetadata['hub']), an optional hub method as the heartbeat ping, session resumption through/reconnectwith the last message id and groups token (the server replays what was missed and keeps the connection id and groups), and/abortondisconnect(). The heartbeat waits out the server's ownKeepAliveTimeout, so a late server keep-alive does not end a healthy connection; a resume that fails because the server is unreachable keeps the session for the next attempt; handshakeheadersreach the socket upgrade on Android, iOS and desktop. A hub error whose error data is1is a failed call, not the server's disconnect command.ClassicSignalRTransport.dispose()releases its streams and HTTP client;RealtimeClient.dispose()calls it for the transport it created. - Transport capability interfaces — a
RealtimeTransportmay implementInvokingTransport,KeepAliveTransportandSessionTransportto own request/response, keep-alive and a server session. A transport'sstreammay also emitRealtimeMessages directly. AKeepAliveTransportreports its server'skeepAliveTimeout, which an unanswered ping waits out. RealtimeConfig.connectionProvider— resolves aRealtimeEndpoint(uri, token, headers) for every attempt, for servers that hand out the hub address with a short-lived token. While it is set,auth.getTokenis not consulted, including by SignalR's per-request token factory.reconnectedStream— aRealtimeReconnection(connection id, downtime, attempts, resumed) after every connection but the first, once rooms are rejoined and the queue flushed.RealtimeClient.acquire(key, …)— reference-counted shared clients, asRealtimeClientLeases; the socket closes with the lastrelease().sendReliable/deliveryStream— retries until acknowledged with one idempotency key, waits out disconnects without spending attempts, publishes pending / sent / acknowledged / failed. Acknowledged by the invocation's answer, or by anackEventon raw sockets. Configured byReliableDeliveryPolicy(RealtimeConfig.reliableDelivery).quality/qualityStream— smoothed round-trip latency from answered invocations and heartbeat pings, asgood/degraded/poor, perConnectionQualityPolicy.- MessagePack —
SignalROptions(protocol: SignalRHubProtocol.messagePack). Binary records are parsed one at a time, so a recordsignalr_netcorecannot decode is reported as aparseErrorinstead of taking its batch down with an unhandled error. HeartbeatConfig.pingFrame— the raw WebSocket ping is no longer hard-coded.- New dependency:
http, for the classic SignalR handshake. RealtimeClient.sharedHolders(key);FakeRealtimeClient.simulateReconnected,simulateQuality,simulateDeliveryandrecordedReliableSends.
Not added: stateful reconnect on ASP.NET Core SignalR. It needs protocol support in
signalr_netcore (ack and sequence messages, handshake v2), which it does not have, and this
package does not reimplement that protocol. Use reconnectedStream to refetch.
2.0.0 #
Two breaking changes that make the defaults match how the package is actually used: rooms keyed by a hub's real parameter type, and no silent buffering while disconnected.
Migrating from 1.x
-
joinRoom('42', joinMethod: 'JoinChatGroup')→joinRoom(42, joinMethod: 'JoinChatGroup')when the hub parameter is anint. String rooms need no change. -
If you relied on sends being queued while offline, add
offlineQueue: const OfflineQueuePolicy()to yourRealtimeConfig. -
A custom
RealtimeClientimplementation must changejoinRoom/leaveRoomto takeObject roomName.FakeRealtimeClientalready does. -
Changed:
joinRoom/leaveRoomtakeObject room, sent to the hub as given. Rooms wereString-only, sojoinRoom('42', joinMethod: 'JoinChatGroup')— the README's own example — sent"42"to a hub method declaredJoinChatGroup(int chatId), which System.Text.Json does not bind. Pass42now; the same value is replayed on every rejoin.activeRoomsstaysSet<String>, keyed by the room's string form. Source-compatible for callers; anything implementingRealtimeClientmust widen the two parameters. -
Changed: the offline queue is off by default.
RealtimeConfig.offlineQueuedefaults toOfflineQueuePolicy.disabled(), sosend/invokewhile disconnected returnRealtimeFailure.disconnected()immediately instead of quietly buffering. Passconst OfflineQueuePolicy()to keep the previous behaviour.
1.3.0 #
Resilience pass, aimed at what actually goes wrong on a phone: networks that come and go, an OS that suspends timers, and sockets that die while the app is in the background.
- Fixed: reconnection no longer ends for good when an attempt lands while the device is
offline. The connectivity pre-flight returned before the
catchthat schedules the next attempt, so a single reconnect firing during a tunnel, a lift or a Wi-Fi/LTE handover left the client permanently disconnected — no timer pending, recoverable only by a manualconnect(), and indistinguishable from a server outage. Every consumer on default config was affected. - Fixed: rooms rejoin through the method they were joined with.
rejoinAllhardcodedJoinRoom, sojoinRoom(r, joinMethod: 'JoinChatGroup')worked once and then silently resubscribed to nothing after the first reconnect.RoomManagernow storesroom → joinMethod. Rejoin failures reacherrorStreamas well:invokereturns aLeftrather than throwing, so the surroundingtrynever saw one. - Fixed:
ReconnectionPolicy.retryIfis actually called. It was declared, documented andcopyWith-able, but no code path consulted it — configuring it was a silent no-op. A predicate that throws is treated as no veto rather than as a stop. - Added: connectivity is a push signal, not just a pre-flight probe.
RealtimeNetworkInfonow exposesonStatusChange; the client subscribes while a connection is wanted and runs a pending reconnect the moment the network returns, instead of waiting out the rest of the backoff. Override the source withRealtimeConfig.connectivityStream. - Changed: an attempt that never reached the wire no longer spends the
maxAttemptsbudget. Offline attempts also use a flat 5 s poll rather than exponential backoff.maxAttemptsis for a server that keeps refusing, not for expiring while the radio is down. - Added: app-lifecycle awareness (
RealtimeConfig.lifecycleAware, default on, plusnotifyAppResumed()/notifyAppPaused()for manual control). On resume the client retires aconnectedstate the transport knows is dead, and runs a reconnect whose timer the OS suspended. Both previously resolved only after the server timeout — up topingInterval + pongTimeoutof a UI reporting "connected" over sends that could not land. Inert where no Flutter binding exists, never fatal. - Changed: the SignalR bearer token is re-resolved per request.
accessTokenFactoryreceived a string captured at connect time, pinning a long-lived connection to a possibly short-lived JWT. It now callsauth.getToken()each timesignalr_netcoreasks, falling back to the connect-time token if that store is momentarily unreadable or empty — so a refresh elsewhere in the app is picked up without a reconnect. - Fixed: concurrent connection attempts no longer strand a socket.
connect(), the backoff timer, the connectivity listener and an app resume can all ask for a connection; two landing together started two attempts, andTransportSession.open()tears down whatever it finds before overwriting its transport field — so the loser's socket stayed open, unobserved and with no reference left to close it. A request now joins the attempt already in flight. - Fixed: a
disconnect()during a handshake tears down the socket that arrives afterwards.TransportSessioncarries a generation counter; a transport whoseconnect()resolves after the session dropped it is closed instead of installed. Previously it re-opened itself into a session that no longer referenced it. - Added:
pendingInvocationCountandabandonedInvocationCount.signalr_netcorehas no cancel for a non-streaming invocation, so a timed-outinvokestays registered in itsHubConnectionuntil the connection drops. That cannot be reclaimed from here, so it is made visible: a count that climbs and never returns to zero is a hub method that is not completing. Both are 0 on the raw WebSocket path.
1.2.0 #
Usability pass. The API surface is smaller to reason about and harder to misuse.
- Fixed:
send()no longer deadlocks when called while disconnected with the offline queue enabled. It acknowledges the enqueue rather than awaiting a flush that onlyconnect()can trigger. A message dropped or expired after the fact is reported onerrorStream. - Fixed: a queued
sendis flushed fire-and-forget, not as an RPC invocation. Previously the flush insideconnect()waited for a hub completion the server was never asked to produce, hanging the connect indefinitely. - Changed: one event-delivery rule.
on(name)now always yields the first argument (nullwhen there is none). It previously yieldedpayload ?? arguments, silently handing back aListwhen the event had no arguments or a leadingnull. - Added:
onArgs(name)returning the full argument list, for multi-parameter hub methods.on(name)would only ever have given you the first. - Added:
errorStreamandlastFailure. One place to wire crash reporting; it also carries failures no call site could see — background reconnect failures, dropped sockets, parse errors on typed streams, offline-queue drops. - Added:
dispose(), called byRealtimeClient.reset(). Closes every stream the client owns. - Fixed: domain streams are cached.
chatStreamand friends built a new controller and subscription on every read, so reading one in abuild()leaked per frame. - Fixed:
typingStreamaccepts positional arguments. A hub sendingSendAsync("UserTyping", room, isTyping)produced nothing at all before. FakeRealtimeClientgainssimulateErroranddispose, andsimulateEventnow treats aListpayload as the argument list.- Written a real README.
- Internal:
realtime_client.dartsplit into five files. It was ~1200 lines holding the contract and the implementation together. Nowlib/src/realtime_client.dartis the abstract contract alone, andlib/src/client/holdsRealtimeClientImplementationplus the three collaborators it delegates to:TransportSession(the wire, whereRealtimeTransportTypestops mattering),RealtimeEventHub(event routing and domain streams),RealtimeDiagnostics(errors, logs, observer) andConnectionTarget(pure uri/header resolution). No public API change. - On the raw WebSocket path, a frame that parses as valid JSON but is not an object (an array or
a scalar) is now delivered as a
rawmessage instead of being silently dropped. SignalR is unaffected.
1.1.0 #
- SignalR is now backed by
signalr_netcoreinstead of a hand-rolled JSON Hub Protocol implementation.SignalRTransportkeeps its API and its wire behaviour — the existing protocol tests pass unchanged — but framing, handshake, RPC correlation and keep-alive now belong to the library. - Added:
/negotiatesupport, enabled by default. This is what Azure SignalR Service and any hub behind a negotiate redirect require, and the previous implementation could not do it. - Added: automatic handshake fallback. If
/negotiatefails, the connection retries with it skipped and remembers which variant worked, so a hub behind an edge that resets the negotiate POST (errno 104) but allows the WebSocket upgrade now connects. Authentication failures skip the fallback so token refresh still fires. - Added:
SignalROptionsonRealtimeConfig.signalR—skipNegotiation,negotiationFallback,transport,negotiateHeaders,logMessageContent,negotiateTimeout, plusSignalROptions.webSocketsOnly(). - Transport defaults to WebSockets rather than negotiated
auto. An SSE fallback failure insse_channelescapes everytry/catchand crashes the app instead of returning aLeft;autois opt-in. Negotiation still runs, so Azure SignalR Service is unaffected. - The
/negotiatePOST now carriesX-Requested-With: XMLHttpRequestand a JSON content-type instead ofsignalr_netcore'sFlutterHttpClient/text/plain, which edge filters drop. negotiateTimeoutdefaults to 15 s;signalr_netcore's own 2 s default is missable on mobile.- Added:
RealtimeConfig.invokeTimeout, defaulting to 30 s. Neither SignalR nor a raw socket imposes one, so an unanswered invocation previously stayed pending for the life of the app. - On the SignalR path the bearer token is delivered via
accessTokenFactory, so it reaches the/negotiatePOST as well as the socket, and is no longer also appended to the query string (duplicateaccess_tokenvalues are comma-joined by ASP.NET Core and authenticate nothing). - Keep-alive on the SignalR path is now driven by
HubConnectionfrom the sameHeartbeatConfig;HeartbeatMonitorno longer starts there, so a dead socket schedules one reconnect, not two.SignalRTransport.sendPing()is a no-op on this path. connectionIdis now the id assigned during negotiation when one is available, instead of a synthetic timestamp.- Unchanged:
Either-based public API,ReconnectionPolicy, offline queue, room rejoin, interceptors, observers, and the injectableRealtimeTransportseam.
0.0.1 #
- TODO: Describe initial release.