Receiver internals
The receiver has one architectural decision worth understanding: decoding does not happen on the UI thread.
Why
Decoding a scanned chunk means fountain peeling, GF(2) elimination, gzip
decompression, SHA-256, and per-chunk disk I/O. That ran synchronously inside
Assembler.ingest on the UI isolate, and a real 109 MB / ~26,000-chunk transfer
exposed it: the UI froze during decode bursts, and rewriting metadata.json on
every scan added blocking disk I/O to each frame.
Why not compute()
The obvious Flutter answer is compute() or Isolate.run(), which spins up a
fresh isolate per call. That is the wrong shape here.
The Assembler and FountainDecoder hold state that must persist across
thousands of calls — recovered blocks, pending fountain symbols, per-transfer
maps. Re-creating that per QR scan would mean serialising the entire decode
state and shipping it across the isolate boundary on every frame, which costs
more than the problem it solves.
The shape
One long-lived worker isolate, spawned once via AssemblerWorker.spawn, owns
the real Assembler for the app's lifetime.
UI isolate Worker isolate
AssemblerWorker ──event──► Assembler
(thin handle) FountainDecoder
ChunkStorage
◄─snapshot── (all disk I/O)
State lives entirely in the worker. Only small ProgressSnapshot values cross
back — enough to draw a progress bar, and nothing else.
Disk hydration
Chunks are written to disk as they resolve, one chunk_NNNNNN.bin per recovered
block, under <outputDirectory>/<transfer.id>/chunks/. A cold start therefore
has enough on disk to resume without re-scanning.
The question is what to trust when rebuilding in-memory state.
Filenames, not metadata
metadata.json is cheap to read but can lag reality: writes are debounced to
at most once every five seconds, for the same UI-blocking reason above. A crash
between a chunk write and the next metadata flush under-reports what is
genuinely on disk.
Chunk filenames cannot be stale in that way. Writes are single-shot
writeAsBytes, not incremental, so a file that exists was fully written.
Hydration reads the filenames as ground truth and consults metadata.json only
for fields it cannot derive from them.
Not eagerly
The first implementation read every hydrated chunk's bytes into memory during the scan. That crashed the receiver outright against a real multi-thousand-chunk transfer — two in-progress transfers were enough to exhaust memory before the app finished starting.
Hydration now records what exists without loading it.
Known gap
flutter/third_party/mobile_scanner is a vendored fork pinned at 7.2.0 via
dependency_overrides, carrying a local patch that adds macOS external-camera
enumeration and selection — something upstream does not support.
Upstream has moved to 7.4.0. Re-diffing the patch is real effort with real risk to camera selection, which is the entire reason the fork exists, for a changelog that is mostly camera lifecycle and orientation fixes. It is recorded as a gap rather than silently carried.