Skip to content

Make multi-file phone→laptop shares prompt and self-reporting - #13

Open
X-Ryl669 wants to merge 1 commit into
zoir-dev:mainfrom
X-Ryl669:fix/multi-file-pull
Open

Make multi-file phone→laptop shares prompt and self-reporting#13
X-Ryl669 wants to merge 1 commit into
zoir-dev:mainfrom
X-Ryl669:fix/multi-file-pull

Conversation

@X-Ryl669

Copy link
Copy Markdown

Selecting more than two files to send from the phone took minutes, while a single file was instant. A test shown that a five-file share sat 2m34s before its first byte moved, then completed in 8 s once a heartbeat round finally ran. Everything below came out of that one report; each part is a distinct defect on the same path, and the path only works once they are all fixed.

This PR implements a faster (single and multi) file transfer with user feedback on the smartphone (via toasts). It's also able to resume a failed transfer (by emptying the queue of a failed item that the current code doesn't do and get struck infinitely). This is authored by Claude.

The pull is piggybacked on the LAN heartbeat, ONE file per round, so the heartbeat's cadence IS the transfer rate:

  • the cadence knew nothing about the queue. Four failed rounds and it fell to the idle tick — 240 s while BLE is live — parking a half-received batch for minutes. It now stays at 2 s while files are queued AND the queue is moving (file_pull_active()), easing to 12 s, never 240, once it stalls: a queue that can never drain must not spin a TCP+IK every 2 s for the session;
  • the cached-IP probe got a single 2 s connect. resolve_peer_addr already retries 3× because a dozing Wi-Fi radio answers a cold connect late, and excused the heartbeat on the grounds that it "ticks again in a few seconds" — untrue for a round carrying a file. It now retries when work is queued;
  • the phone was often unreachable at the cached address at all. While BLE is up it releases the multicast lock and answers no mDNS, so a DHCP renew left the laptop dialing a dead lease with no way to learn better (observed: 2m34s spent on some address while the phone sat on another). LanServer.keepLanHot() now holds the throughput Wi-Fi lock AND the multicast lock for 60 s around a pull, re-announces over mDNS, and the first offer of a batch pushes AppState over BLE — that carries the live wifi_ip, which repoints the cache with no mDNS involved. The throughput lock was also per-blob, so the radio parked between rounds (one file pulled at 0.2 MB/s where warm rounds do 3.5-7.8);
  • a token the phone had evicted wedged the queue for ever. It answers "nomatch" in the bulk-sync done frame, which exchange_bulk logged and discarded, so the laptop re-requested a dead token every round, blocking every file behind it. LanReconnectOutcome.bulk_status now carries that map and the pull arm drops the entry. Silence is never treated as an answer, and the pop is guarded on the queue front still being the requested token.

The offer that starts it all was fire-and-forget, and both ways it could fail were invisible from the phone:

  • sealAndNotify returning false (no BLE session — app restarted, out of range, laptop re-handshaking) was discarded and logged as success. Observed: three files shared 31 s before a BLE reconnect completed went nowhere while the phone logged "offered to laptop" and toasted "Sending 3 files…". Offers are now retried every 3 s for ~1 min, and immediately when the link returns;
  • returning TRUE only means the local stack queued the notify. Three offers 4-9 ms apart cost one outright — the laptop logged resynced past dropped BLE frame(s) dropped=1 and queued 2 of 3 files. Offers are now paced 60 ms (the only bulk BLE path here that had no pacing at all), the LAN warming is deferred 250 ms past the burst so its own mDNS re-announce and STATE notify stop cutting in line, and a sent-but-unfetched offer is re-announced. The laptop dedups by content token, so a re-announce of one that DID arrive can't queue a second pull, write a spurious "name (1).ext", or re-prompt for consent.

A dropped offer no longer waits out the 30 s resend timer either: the laptop's pull queue is FIFO in arrival order, so it fetching a LATER offer while an earlier one is outstanding PROVES the earlier one never arrived (offersPresumedDropped()), and that one is re-announced within ~1 s.

Finally, feedback, because a share could sit a minute with nothing on screen after the share sheet's "Sending…": the phone reports "Laptop unreachable — keeping the file(s) queued" once per outage, "File sent: " as each file is FETCHED (the only moment this device can honestly call success), and names what was lost if an offer ends up nowhere.

Verified on the live pair (Plasma 6 laptop + phone over BLE/Wi-Fi): a 3-file share that took 2m34s+ before now completes in ~6 s, with each file's "File sent:" toast; a force-stopped phone app recovers by itself — offers held through a 29 s BLE reconnect, delivered on retry, and a notify dropped in flight re-announced and fetched — where before all three files vanished silently. Tests: 141 daemon + 37 app (Rust), 88 Android unit tests, no new clippy warnings.

…ting

Selecting more than two files to send from the phone took minutes, while a
single file was instant. Measured on 2026-08-20: a five-file share sat 2m34s
before its first byte moved, then completed in 8 s once a heartbeat round
finally ran. Everything below came out of that one report; each part is a
distinct defect on the same path, and the path only works once they are all
fixed.

The pull is piggybacked on the LAN heartbeat, ONE file per round, so the
heartbeat's cadence IS the transfer rate:

* the cadence knew nothing about the queue. Four failed rounds and it fell to
  the idle tick — 240 s while BLE is live — parking a half-received batch for
  minutes. It now stays at 2 s while files are queued AND the queue is moving
  (`file_pull_active()`), easing to 12 s, never 240, once it stalls: a queue
  that can never drain must not spin a TCP+IK every 2 s for the session;
* the cached-IP probe got a single 2 s connect. `resolve_peer_addr` already
  retries 3× because a dozing Wi-Fi radio answers a cold connect late, and
  excused the heartbeat on the grounds that it "ticks again in a few seconds"
  — untrue for a round carrying a file. It now retries when work is queued;
* the phone was often unreachable at the cached address at all. While BLE is
  up it releases the multicast lock and answers no mDNS, so a DHCP renew left
  the laptop dialing a dead lease with no way to learn better (observed: 2m34s
  spent on 192.168.0.15 while the phone sat on .125). `LanServer.keepLanHot()`
  now holds the throughput Wi-Fi lock AND the multicast lock for 60 s around a
  pull, re-announces over mDNS, and the first offer of a batch pushes AppState
  over BLE — that carries the live `wifi_ip`, which repoints the cache with no
  mDNS involved. The throughput lock was also per-blob, so the radio parked
  between rounds (one file pulled at 0.2 MB/s where warm rounds do 3.5-7.8);
* a token the phone had evicted wedged the queue for ever. It answers
  `"nomatch"` in the bulk-sync done frame, which `exchange_bulk` logged and
  discarded, so the laptop re-requested a dead token every round, blocking
  every file behind it. `LanReconnectOutcome.bulk_status` now carries that map
  and the pull arm drops the entry. Silence is never treated as an answer, and
  the pop is guarded on the queue front still being the requested token.

The offer that starts it all was fire-and-forget, and both ways it could fail
were invisible from the phone:

* `sealAndNotify` returning false (no BLE session — app restarted, out of
  range, laptop re-handshaking) was discarded and logged as success. Observed:
  three files shared 31 s before a BLE reconnect completed went nowhere while
  the phone logged "offered to laptop" and toasted "Sending 3 files…". Offers
  are now retried every 3 s for ~1 min, and immediately when the link returns;
* returning TRUE only means the local stack queued the notify. Three offers
  4-9 ms apart cost one outright — the laptop logged `resynced past dropped
  BLE frame(s) dropped=1` and queued 2 of 3 files. Offers are now paced 60 ms
  (the only bulk BLE path here that had no pacing at all), the LAN warming is
  deferred 250 ms past the burst so its own mDNS re-announce and STATE notify
  stop cutting in line, and a sent-but-unfetched offer is re-announced. The
  laptop dedups by content token, so a re-announce of one that DID arrive
  can't queue a second pull, write a spurious "name (1).ext", or re-prompt for
  consent.

A dropped offer no longer waits out the 30 s resend timer either: the
laptop's pull queue is FIFO in arrival order, so it fetching a LATER offer
while an earlier one is outstanding PROVES the earlier one never arrived
(`offersPresumedDropped()`), and that one is re-announced within ~1 s.

Finally, feedback, because a share could sit a minute with nothing on screen
after the share sheet's "Sending…": the phone reports "Laptop unreachable —
keeping the file(s) queued" once per outage, "File sent: <name>" as each file
is FETCHED (the only moment this device can honestly call success), and names
what was lost if an offer ends up nowhere.

Verified on the live pair (Plasma 6 laptop + phone over BLE/Wi-Fi): a 3-file
share that took 2m34s+ before now completes in ~6 s, with each file's
"File sent:" toast; a force-stopped phone app recovers by itself — offers held
through a 29 s BLE reconnect, delivered on retry, and a notify dropped in
flight re-announced and fetched — where before all three files vanished
silently. Tests: 141 daemon + 37 app (Rust), 88 Android unit tests, no new
clippy warnings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant