socket_hub 1.1.0 copy "socket_hub: ^1.1.0" to clipboard
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.parse reads a key back from the canonical id, 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.
  • whenReady takes a timeout. It throws a TimeoutException and leaves the hub alone — still connecting, still reconnecting — rather than making a caller who wants to give up waiting tear the hub down.
  • retainLatest no 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", which latest(key) alone cannot.
  • The idle watchdog measures elapsed time on a Stopwatch. It compared wall-clock DateTimes, 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_channel 3.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.
  • JsonSocketCodec gains controlOps and errorReader. controlOps names the op values that mean control, so a server that stamps an op on its data frames too no longer has them swallowed as control; errorReader replaces the errorField lookup for a server reporting failure some other way, such as {"success": false, "msg": …}. Both default to the previous behaviour.

1.0.1 #

  • Point the repository and issue_tracker links, and the README's GitHub link, at the renamed socket_hub repository. 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 no subscribe/unsubscribe pair 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 subscribeTickerList method 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. ReconnectPolicy covers the backoff, with jitter so a fleet dropped by one outage does not return in lockstep.
  • A keepalive. heartbeatInterval sends the codec's ping frame, and idleTimeout treats 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, with handshake running after the socket opens and before any subscription frame, so a login gating private channels is ordered by construction rather than by a _pendingPrivateSubscribe flag.
  • JsonSocketCodec — a ready-made codec for the {"op": "subscribe", "args": [...]} convention, reading a frame's key fields from the top level, from a nested args map or from inside data. 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-channel isAccount check in the middle of the message handler.
  • SocketTransport — the socket behind an interface, with WebSocketTransport over package:web_socket_channel. The originals reached for WebSocketChannel.connect directly and so could not be tested at all.
  • socket_hub_testing.dartFakeTransport, a socket with nothing behind it, and MockSocketServer, 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.
  • ReconnectPolicy computes 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 at maxDelay.
  • 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 in connecting with 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.
1
likes
160
points
141
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

One WebSocket, many channels — ref-counted subscriptions derived from stream listeners, batched frames, typed payload routing and resubscribe on reconnect.

Homepage
Repository (GitHub)
View/report issues

Topics

#websocket #streams #subscriptions #reconnect #realtime

License

MIT (license)

Dependencies

web_socket_channel

More

Packages that depend on socket_hub