Repository navigation
refactor(opds): sharing as an explicit state machine with Local/All network modes - #160
Merged
Merged
Conversation
libcalibre Test Coverage ReportOverall coverage: 80.16% Coverage breakdown available in the artifacts. |
…etwork modes The service becomes a small enum state machine - Stopped / Starting / Running / Waiting / Failed - whose variants own their resources, with transitions as the only place state changes and a generation counter that dismisses stale events (a death watcher firing after stop, a retry landing after stop). The mutex is never held across an await: begin_start bumps the generation, the bind happens unlocked, and started(gen) reconciles. The monitor/reconcile loop, failure ledger, and backoff ladder are gone: interfaces are enumerated once per attempt, Waiting retries on a small poll that exists only while Waiting, and a dead listener fails the service with honest copy (turn sharing back on). Bind targets become LocalNetworks (classified LAN, private/ULA, policy- filtered) and AllInterfaces (0.0.0.0 + :: with V6ONLY set, the future auth- gated mode). The per-interface picker target is deleted along with the interfaces query - the UI will offer two choices, not sixty-five.
phildenhoff
force-pushed
the
rework-opds-sharing
branch
from
September 22, 2026 03:35
67e0a28 to
6f4b4c4
Compare
Member
Author
|
Plato's implementation review flagged three real issues, all fixed in this push:
Plus two smaller items: |
libcalibre Test Coverage ReportOverall coverage: 80.16% Coverage breakdown available in the artifacts. |
This was referenced Sep 22, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The sharing service grew a reconcile loop: every 2 seconds it re-enumerated interfaces, diffed the plan against the running listeners, restarted failures with backoff, and tracked a ledger of per-address failures. That loop was the most complex code in the feature, and it existed to handle a case that barely matters for v1 (the network changing underneath a running share). Separately, the interface picker exposed ~65 entries on a developer machine (Docker bridges, Colima, Tailscale, AWDL...) — unusable noise, and the wrong abstraction.
What this does
The service is now an explicit state machine — one private enum, variants owning their resources:
Stopped→ start →Starting→Running {listeners, urls}/Waiting {reason}/Failed {error}begin_start,started(gen),waiting(gen),listener_died(gen),failed(gen),stop. Illegal transitions don't exist because no code can express them.started(gen)sees the stale generation and drains the fresh listeners).From<&SharingState>), so state and reported status cannot drift.The loop is gone. Interfaces are enumerated once per attempt.
Waitingruns a small poll that exists only while waiting (sharing auto-starts when a network appears; everything else is turn-it-off-and-on). A dead listener fails the service with honest copy — "Sharing stopped unexpectedly. Turn it back on." Stop is awaited (5s budget); stale events can't resurrect anything.Two bind modes instead of a picker:
localNetworks(default): classified LAN interfaces, private/ULA addresses, exact binds, never Docker bridges/Tailscale/global addresses.allInterfaces: wildcard0.0.0.0+[::]withIPV6_V6ONLYset explicitly (Linux dual-stack defaults differ), no enumeration at all. Intended to be gated on username/password once auth lands (feat(opds): HTTP Basic auth and credential management #149); theBindPolicyhook (allow_global) is already threaded for that.The per-interface target and the interfaces query are deleted (bindings regenerated). Globally-routable addresses are excluded from local-network binds by default.
Testing
40 tests. The state machine's transitions are table-tested purely (no sockets): stale generations rejected, stop-from-any-state, derived status. Service tests cover: conflict on double-start, waiting→poll→running, stop cancelling the poll, dead listener → failed → restart, permanent bind failure, AllInterfaces binding without enumeration, and a real dual-wildcard bind on one port (the socket2 path).
Notes
BindPolicycarried forward; the backoff/ledger machinery it added dies with the loop).allInterfaces(feat(opds): HTTP Basic auth and credential management #149), settings UI with the two-mode toggle (feat(opds): add headless server on a Tauri-independent runtime #151).