Skip to main content

baaba_realtime_client 3.0.1 copy "baaba_realtime_client: ^3.0.1" to clipboard
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 pins equatable to 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.uri is now Uri?. Code that reads config.uri needs a ! or a null check; constructing a config is unchanged.
  • RealtimeClient gained reconnectedStream, quality, qualityStream, sendReliable and deliveryStream. A custom implementation of RealtimeClient must add them; FakeRealtimeClient already has.
  • RealtimeTransportType gained signalrClassic; an exhaustive switch over it needs a case.
  • connect() while already connected now returns Right(unit) instead of reopening the socket.
  • HeartbeatMonitor's onPing returns bool? (internal, but tests constructing it directly need a return).

Added

  • Classic SignalR 2.x — RealtimeTransportType.signalrClassic with ClassicSignalROptions, backed by the new ClassicSignalRTransport: negotiate / connect / start, invoke correlated by invocation id (resolves with the hub's return value, fails with serverError when it throws), several hubs on one socket (hub.Method routes to a hub; events carry metadata['hub']), an optional hub method as the heartbeat ping, session resumption through /reconnect with the last message id and groups token (the server replays what was missed and keeps the connection id and groups), and /abort on disconnect(). The heartbeat waits out the server's own KeepAliveTimeout, 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; handshake headers reach the socket upgrade on Android, iOS and desktop. A hub error whose error data is 1 is 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 RealtimeTransport may implement InvokingTransport, KeepAliveTransport and SessionTransport to own request/response, keep-alive and a server session. A transport's stream may also emit RealtimeMessages directly. A KeepAliveTransport reports its server's keepAliveTimeout, which an unanswered ping waits out.
  • RealtimeConfig.connectionProvider — resolves a RealtimeEndpoint (uri, token, headers) for every attempt, for servers that hand out the hub address with a short-lived token. While it is set, auth.getToken is not consulted, including by SignalR's per-request token factory.
  • reconnectedStream — a RealtimeReconnection (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, as RealtimeClientLeases; the socket closes with the last release().
  • 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 an ackEvent on raw sockets. Configured by ReliableDeliveryPolicy (RealtimeConfig.reliableDelivery).
  • quality / qualityStream — smoothed round-trip latency from answered invocations and heartbeat pings, as good / degraded / poor, per ConnectionQualityPolicy.
  • MessagePack — SignalROptions(protocol: SignalRHubProtocol.messagePack). Binary records are parsed one at a time, so a record signalr_netcore cannot decode is reported as a parseError instead 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, simulateDelivery and recordedReliableSends.

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 an int. String rooms need no change.

  • If you relied on sends being queued while offline, add offlineQueue: const OfflineQueuePolicy() to your RealtimeConfig.

  • A custom RealtimeClient implementation must change joinRoom / leaveRoom to take Object roomName. FakeRealtimeClient already does.

  • Changed: joinRoom / leaveRoom take Object room, sent to the hub as given. Rooms were String-only, so joinRoom('42', joinMethod: 'JoinChatGroup') — the README's own example — sent "42" to a hub method declared JoinChatGroup(int chatId), which System.Text.Json does not bind. Pass 42 now; the same value is replayed on every rejoin. activeRooms stays Set<String>, keyed by the room's string form. Source-compatible for callers; anything implementing RealtimeClient must widen the two parameters.

  • Changed: the offline queue is off by default. RealtimeConfig.offlineQueue defaults to OfflineQueuePolicy.disabled(), so send / invoke while disconnected return RealtimeFailure.disconnected() immediately instead of quietly buffering. Pass const 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 catch that 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 manual connect(), and indistinguishable from a server outage. Every consumer on default config was affected.
  • Fixed: rooms rejoin through the method they were joined with. rejoinAll hardcoded JoinRoom, so joinRoom(r, joinMethod: 'JoinChatGroup') worked once and then silently resubscribed to nothing after the first reconnect. RoomManager now stores room → joinMethod. Rejoin failures reach errorStream as well: invoke returns a Left rather than throwing, so the surrounding try never saw one.
  • Fixed: ReconnectionPolicy.retryIf is actually called. It was declared, documented and copyWith-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. RealtimeNetworkInfo now exposes onStatusChange; 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 with RealtimeConfig.connectivityStream.
  • Changed: an attempt that never reached the wire no longer spends the maxAttempts budget. Offline attempts also use a flat 5 s poll rather than exponential backoff. maxAttempts is for a server that keeps refusing, not for expiring while the radio is down.
  • Added: app-lifecycle awareness (RealtimeConfig.lifecycleAware, default on, plus notifyAppResumed() / notifyAppPaused() for manual control). On resume the client retires a connected state the transport knows is dead, and runs a reconnect whose timer the OS suspended. Both previously resolved only after the server timeout — up to pingInterval + pongTimeout of 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. accessTokenFactory received a string captured at connect time, pinning a long-lived connection to a possibly short-lived JWT. It now calls auth.getToken() each time signalr_netcore asks, 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, and TransportSession.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. TransportSession carries a generation counter; a transport whose connect() 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: pendingInvocationCount and abandonedInvocationCount. signalr_netcore has no cancel for a non-streaming invocation, so a timed-out invoke stays registered in its HubConnection until 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 only connect() can trigger. A message dropped or expired after the fact is reported on errorStream.
  • Fixed: a queued send is flushed fire-and-forget, not as an RPC invocation. Previously the flush inside connect() 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 (null when there is none). It previously yielded payload ?? arguments, silently handing back a List when the event had no arguments or a leading null.
  • Added: onArgs(name) returning the full argument list, for multi-parameter hub methods. on(name) would only ever have given you the first.
  • Added: errorStream and lastFailure. 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 by RealtimeClient.reset(). Closes every stream the client owns.
  • Fixed: domain streams are cached. chatStream and friends built a new controller and subscription on every read, so reading one in a build() leaked per frame.
  • Fixed: typingStream accepts positional arguments. A hub sending SendAsync("UserTyping", room, isTyping) produced nothing at all before.
  • FakeRealtimeClient gains simulateError and dispose, and simulateEvent now treats a List payload as the argument list.
  • Written a real README.
  • Internal: realtime_client.dart split into five files. It was ~1200 lines holding the contract and the implementation together. Now lib/src/realtime_client.dart is the abstract contract alone, and lib/src/client/ holds RealtimeClientImplementation plus the three collaborators it delegates to: TransportSession (the wire, where RealtimeTransportType stops mattering), RealtimeEventHub (event routing and domain streams), RealtimeDiagnostics (errors, logs, observer) and ConnectionTarget (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 raw message instead of being silently dropped. SignalR is unaffected.

1.1.0 #

  • SignalR is now backed by signalr_netcore instead of a hand-rolled JSON Hub Protocol implementation. SignalRTransport keeps 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: /negotiate support, 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 /negotiate fails, 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: SignalROptions on RealtimeConfig.signalR — skipNegotiation, negotiationFallback, transport, negotiateHeaders, logMessageContent, negotiateTimeout, plus SignalROptions.webSocketsOnly().
  • Transport defaults to WebSockets rather than negotiated auto. An SSE fallback failure in sse_channel escapes every try/catch and crashes the app instead of returning a Left; auto is opt-in. Negotiation still runs, so Azure SignalR Service is unaffected.
  • The /negotiate POST now carries X-Requested-With: XMLHttpRequest and a JSON content-type instead of signalr_netcore's FlutterHttpClient / text/plain, which edge filters drop.
  • negotiateTimeout defaults 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 /negotiate POST as well as the socket, and is no longer also appended to the query string (duplicate access_token values are comma-joined by ASP.NET Core and authenticate nothing).
  • Keep-alive on the SignalR path is now driven by HubConnection from the same HeartbeatConfig; HeartbeatMonitor no longer starts there, so a dead socket schedules one reconnect, not two. SignalRTransport.sendPing() is a no-op on this path.
  • connectionId is 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 injectable RealtimeTransport seam.

0.0.1 #

  • TODO: Describe initial release.
0
likes
140
points
405
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

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.

Topics

#realtime #websocket #signalr #chat #webrtc

License

MIT (license)

Dependencies

equatable, flutter, fpdart, http, internet_connection_checker_plus, logging, signalr_netcore, web_socket_channel

More

Packages that depend on baaba_realtime_client