socket_hub 1.1.0
socket_hub: ^1.1.0 copied to clipboard
One WebSocket, many channels — ref-counted subscriptions derived from stream listeners, batched frames, typed payload routing and resubscribe on reconnect.
Changelog #
1.1.0 #
SubscriptionKey.parsereads a key back from the canonicalid, so an id that was logged or persisted in one run is usable in the next. The round trip is exact while no channel or argument name contains|or=; argument values may contain=freely.whenReadytakes atimeout. It throws aTimeoutExceptionand leaves the hub alone — still connecting, still reconnecting — rather than making a caller who wants to give up waiting tear the hub down.retainLatestno longer loses a null payload. The replay tested the cached value for null rather than the cache for the key, so with a nullable payload type a retained null was silently dropped and a late listener sat empty.hasLatest(key)now distinguishes "nothing retained" from "null was retained", whichlatest(key)alone cannot.- The idle watchdog measures elapsed time on a
Stopwatch. It compared wall-clockDateTimes, so a clock stepped backwards (an NTP correction, a user changing the time) held a dead socket open, and a step forwards killed a live one. - The SDK floor drops from Dart 3.13.0 to 3.3.0 — Flutter 3.19.0 — which
is where
web_socket_channel3.0.3 starts and below which nothing here can go. The old bound was the SDK the package happened to be written on rather than anything the code needed, and it kept the package out of every app not yet on the newest Flutter. The dev dependencies are ranged rather than pinned to their newest major so the floor resolves. CI builds on 3.3.0 as well as stable. JsonSocketCodecgainscontrolOpsanderrorReader.controlOpsnames theopvalues that mean control, so a server that stamps anopon its data frames too no longer has them swallowed as control;errorReaderreplaces theerrorFieldlookup for a server reporting failure some other way, such as{"success": false, "msg": …}. Both default to the previous behaviour.
1.0.1 #
- Point the
repositoryandissue_trackerlinks, and the README's GitHub link, at the renamedsocket_hubrepository. No code changes.
1.0.0 #
Initial release, extracted from three near-identical WebSocket data sources in an internal trading app and generalised on the way out. Several things the originals got wrong are fixed here rather than carried over.
SocketChannelHub— one socket, a stream per subscription. Reference counts come from stream listeners, so listening subscribes and cancelling the last listener unsubscribes; there is nosubscribe/unsubscribepair for a caller to keep balanced.- Subscriptions are batched per microtask. A screen opening four streams
sends one frame, and a symbol switch that closes four and opens four sends
two. The originals sent one frame per channel, and had a separate
subscribeTickerListmethod purely to batch a list of symbols by hand — that method has no successor because it is no longer needed. - A subscribe and an unsubscribe of the same key in one turn cancel out. Reconciling from the difference between what callers want and what the server has been told, rather than from a queue of deltas, means a stream opened and closed in the same turn touches the wire not at all.
- Reconnection, which the originals had none of. They called
listen(cancelOnError: true)and closed every stream controller on the first socket error, so one dropped frame ended every subscription in the app and left the sockets shut until something happened to rebuild them. Here a drop is a gap in events, streams stay open, and every live subscription is re-sent on the new socket.ReconnectPolicycovers the backoff, with jitter so a fleet dropped by one outage does not return in lockstep. - A keepalive.
heartbeatIntervalsends the codec's ping frame, andidleTimeouttreats a socket that has delivered nothing for that long as dead and reopens it. Neither existed before, which is the other half of why a silently dropped connection went unnoticed. The two run on separate timers, so a protocol needing no ping can still have its dead sockets noticed. - The payload cache is bounded. The originals created a stream controller for every key an inbound frame mentioned, whether or not anyone had asked for it, so a server pushing hundreds of symbols grew the map for the life of the session. Nothing is now created or retained for a key no caller holds: the controller is made when the first listener arrives and dropped when the last one leaves, so a session cycling through symbols keeps a map the size of what is on screen rather than of everything ever looked at.
SocketCodec— the whole protocol behind four methods, withhandshakerunning after the socket opens and before any subscription frame, so a login gating private channels is ordered by construction rather than by a_pendingPrivateSubscribeflag.JsonSocketCodec— a ready-made codec for the{"op": "subscribe", "args": [...]}convention, reading a frame's key fields from the top level, from a nestedargsmap or from insidedata. All three of the originals' wire shapes go through it with configuration alone.SubscriptionKey— a channel plus arguments, with a canonical id, so argument order cannot split one subscription into two. Null arguments are dropped, which removes the conditionals the originals built keys with.- Fan-out is declarative: a codec returns more than one key and the payload is
routed to each. The originals hard-coded a
'::ALL:'key prefix and a per-channelisAccountcheck in the middle of the message handler. SocketTransport— the socket behind an interface, withWebSocketTransportoverpackage:web_socket_channel. The originals reached forWebSocketChannel.connectdirectly and so could not be tested at all.socket_hub_testing.dart—FakeTransport, a socket with nothing behind it, andMockSocketServer, which reads the frames a hub sends and answers them. Mock mode becomes a different far end rather than a parallel set of data sources.ReconnectPolicycomputes its backoff in doubles.pow(2, attempt)on two ints does integer arithmetic, which wraps to zero past 2⁶³ — so a long outage would have produced a zero delay and a reconnect storm. Caught by a test asserting the delay stops growing atmaxDelay.disconnect()clears the failure flag it left behind. That flag stops one dying socket reporting itself three times from scheduling three reconnects. Left set by a disconnect that landed mid-backoff, it also swallowed the failure of the next attempt — so an app backgrounded during a retry and resumed into a still-unreachable server sat inconnectingwith nothing on a timer, needing a restart to recover.- Dartdoc across the public API, a runnable
example/that needs no network, and 66 tests covering reference counting, batching, routing, fan-out, reconnection, the handshake, the keepalive and the lifecycle. CI runs them, the formatter, the analyzer and a publish dry run against both the minimum supported SDK and current stable.