cross_tab 0.2.1
cross_tab: ^0.2.1 copied to clipboard
Coordinate the browser tabs of a Flutter Web app: send typed messages between them, know how many are open, and elect exactly one to do the work.
0.2.1 #
No code changes. This release exists because the package was almost impossible to install.
sdk: ^3.13.0 meant Dart 3.13, which only ships with Flutter 3.47 —
released three weeks ago. Anyone on an older SDK hit a version-solve failure
before they could try it. The floor is now ^3.5.0 / Flutter 3.24, and it is
tested: the library resolves and type-checks against Flutter 3.16.9 / Dart
3.2.6, so the declared range is narrower than the verified one.
0.2.0 #
An audit of every package in this repo found five defects here, including two that produced the one thing leader election exists to prevent: two leaders.
Fixed #
- A backgrounded tab was pruned as dead while it still believed it led.
Browsers throttle a hidden tab's timers to about a minute, so it stopped
beating, everyone else dropped it after three missed beats and elected a new
leader — while the hidden tab, running its own throttled clock, still thought
it was in charge. Two tabs then did the leader's work. The channel now beats
on
visibilitychangeand skips one prune round after becoming visible, since everyone's clocks were throttled together. - A brand new tab crowned itself before hearing from anyone.
_electedLeader()had no path that returned null despite the nullable return type, so a tab reportedisLeadertrue on its first presence event — before it could know whether it was joining a crowd. Leadership is nownull("electing") for one heartbeat unless a peer speaks up first, and a tab that really is alone picks it up a beat later. presencecould drop a listener's own demotion. It was anasync*generator whoseyield currentprelude suspended before anything subscribed to the controller, so events in that window went nowhere. It subscribes eagerly now and replays the current state throughonListen— which also makes it genuinely broadcast.- The payload contract was documented backwards.
TabMessage.dataclaimed to be "exactly as the sender passed it"; the envelope is JSON-encoded, so aDateTimenever survives and a nested map arrives asMap<String, dynamic>. The docs now say so, andsendrefuses a non-encodable value with anArgumentErrornaming the type instead of failing deep inside the encoder. sendafterclosethrew on web and did nothing off-web. The stub now matches, so the mistake surfaces in a VM test rather than only in a browser.
0.1.3 #
- Screenshots on pub.dev, generated by
tool/shoot.shrather than captured by hand, so they can be regenerated when the demo changes. - The example now bundles Material icons. It never had
uses-material-design: true, so every icon in it rendered as an empty box — which nothing warns about at build time; the giveaway is an emptyFontManifest.json. - Fixed a key placement in the example: the
ValueKeywas on the panel rather than on theExpandedwrapping it, so closing one tab tore down and rebuilt all the others with new ids. Flutter matches children by position and key together, and an unkeyedExpandedthat shifts takes its subtree with it.
0.1.2 #
Actually fixes the README's opening example.
0.1.1 said it had fixed this and had not. The edit was made in the working tree and never staged, so that release changed only the version and the changelog — the broken snippet went out again, now with a note claiming it was gone. Verifying that a fix landed is part of the fix, and skipping it turned a small error into a false claim.
The opening example read if (p.isLeader) startPolling() else stopPolling();,
which is missing the semicolon before else and does not compile.
0.1.1 #
- Fixes the README's opening example, which did not compile. It read
if (p.isLeader) startPolling() else stopPolling();— missing the semicolon beforeelse— so the first thing anyone copied out of this package was a syntax error. The corrected snippet was checked through the analyser rather than by eye.
0.1.0 #
Initial release: cross-tab coordination for Flutter Web.
CrossTab.open(name)joins every tab of your app sharing that name.- Leader election. Exactly one tab is elected to do work that must happen once — polling, a socket, a notification — and leadership moves when that tab closes. The oldest tab leads, with the id as a tiebreak so tabs opened in the same microsecond still reach the same answer.
- Presence.
count,tabs,leader,isLeader, emitted on change and on subscription. A closed tab says goodbye so the others react immediately; a crashed one is dropped after three missed heartbeats. - Messages with a sender id, carrying structure rather than strings. A tab never hears its own.
- Off the web the package still compiles and behaves as the single instance it
is: one tab, always the leader. That is the truthful answer, and it means
leader logic needs no
if (kIsWeb)around it.
Verified in a real browser: flutter test --platform chrome exercises four
tabs agreeing on one leader, the oldest winning, and leadership passing on
close — using two BroadcastChannel objects in one document, which see each
other exactly as separate tabs do.
