[{"data":1,"prerenderedAt":243},["ShallowReactive",2],{"navigation_docs":3,"-docs-architecture-receiver":59,"-docs-architecture-receiver-surround":238},[4,25,40],{"title":5,"path":6,"stem":7,"children":8,"page":24},"Getting Started","\u002Fdocs\u002Fgetting-started","docs\u002F1.getting-started",[9,14,19],{"title":10,"path":11,"stem":12,"icon":13},"Introduction","\u002Fdocs\u002Fgetting-started\u002Fintroduction","docs\u002F1.getting-started\u002F1.introduction","i-lucide-house",{"title":15,"path":16,"stem":17,"icon":18},"Installation","\u002Fdocs\u002Fgetting-started\u002Finstallation","docs\u002F1.getting-started\u002F2.installation","i-lucide-download",{"title":20,"path":21,"stem":22,"icon":23},"Using Porter","\u002Fdocs\u002Fgetting-started\u002Fusing-porter","docs\u002F1.getting-started\u002F3.using-porter","i-lucide-terminal",false,{"title":26,"path":27,"stem":28,"children":29,"page":24},"Architecture","\u002Fdocs\u002Farchitecture","docs\u002F2.architecture",[30,35],{"title":31,"path":32,"stem":33,"icon":34},"Overview","\u002Fdocs\u002Farchitecture\u002Foverview","docs\u002F2.architecture\u002F1.overview","i-lucide-layers",{"title":36,"path":37,"stem":38,"icon":39},"Receiver internals","\u002Fdocs\u002Farchitecture\u002Freceiver","docs\u002F2.architecture\u002F2.receiver","i-lucide-smartphone",{"title":41,"path":42,"stem":43,"children":44,"page":24},"Reference","\u002Fdocs\u002Freference","docs\u002F3.reference",[45,49,54],{"title":46,"path":47,"stem":48,"icon":23},"Commands","\u002Fdocs\u002Freference\u002Fcommands","docs\u002F3.reference\u002F1.commands",{"title":50,"path":51,"stem":52,"icon":53},"Wire format","\u002Fdocs\u002Freference\u002Fwire-format","docs\u002F3.reference\u002F2.wire-format","i-lucide-binary",{"title":55,"path":56,"stem":57,"icon":58},"Decisions","\u002Fdocs\u002Freference\u002Fdecisions","docs\u002F3.reference\u002F3.decisions","i-lucide-file-text",{"id":60,"title":36,"body":61,"description":231,"extension":232,"links":233,"meta":234,"navigation":235,"path":37,"seo":236,"stem":38,"__hash__":237},"docs\u002Fdocs\u002F2.architecture\u002F2.receiver.md",{"type":62,"value":63,"toc":218},"minimark",[64,73,78,90,97,107,118,122,132,142,149,153,164,167,172,181,191,195,198,201,205,215],[65,66,67,68,72],"p",{},"The receiver has one architectural decision worth understanding: ",[69,70,71],"strong",{},"decoding does\nnot happen on the UI thread",".",[74,75,77],"h2",{"id":76},"why","Why",[65,79,80,81,85,86,89],{},"Decoding a scanned chunk means fountain peeling, GF(2) elimination, gzip\ndecompression, SHA-256, and per-chunk disk I\u002FO. That ran synchronously inside\n",[82,83,84],"code",{},"Assembler.ingest"," on the UI isolate, and a real 109 MB \u002F ~26,000-chunk transfer\nexposed it: the UI froze during decode bursts, and rewriting ",[82,87,88],{},"metadata.json"," on\nevery scan added blocking disk I\u002FO to each frame.",[74,91,93,94],{"id":92},"why-not-compute","Why not ",[82,95,96],{},"compute()",[65,98,99,100,102,103,106],{},"The obvious Flutter answer is ",[82,101,96],{}," or ",[82,104,105],{},"Isolate.run()",", which spins up a\nfresh isolate per call. That is the wrong shape here.",[65,108,109,110,113,114,117],{},"The ",[82,111,112],{},"Assembler"," and ",[82,115,116],{},"FountainDecoder"," hold state that must persist across\nthousands of calls — recovered blocks, pending fountain symbols, per-transfer\nmaps. Re-creating that per QR scan would mean serialising the entire decode\nstate and shipping it across the isolate boundary on every frame, which costs\nmore than the problem it solves.",[74,119,121],{"id":120},"the-shape","The shape",[65,123,124,125,128,129,131],{},"One long-lived worker isolate, spawned once via ",[82,126,127],{},"AssemblerWorker.spawn",", owns\nthe real ",[82,130,112],{}," for the app's lifetime.",[133,134,140],"pre",{"className":135,"code":137,"language":138,"meta":139},[136],"language-text","UI isolate                    Worker isolate\n  AssemblerWorker  ──event──►   Assembler\n   (thin handle)                FountainDecoder\n                                ChunkStorage\n                 ◄─snapshot──   (all disk I\u002FO)\n","text","",[82,141,137],{"__ignoreMap":139},[65,143,144,145,148],{},"State lives entirely in the worker. Only small ",[82,146,147],{},"ProgressSnapshot"," values cross\nback — enough to draw a progress bar, and nothing else.",[74,150,152],{"id":151},"disk-hydration","Disk hydration",[65,154,155,156,159,160,163],{},"Chunks are written to disk as they resolve, one ",[82,157,158],{},"chunk_NNNNNN.bin"," per recovered\nblock, under ",[82,161,162],{},"\u003CoutputDirectory>\u002F\u003Ctransfer.id>\u002Fchunks\u002F",". A cold start therefore\nhas enough on disk to resume without re-scanning.",[65,165,166],{},"The question is what to trust when rebuilding in-memory state.",[168,169,171],"h3",{"id":170},"filenames-not-metadata","Filenames, not metadata",[65,173,174,176,177,180],{},[82,175,88],{}," is cheap to read but ",[69,178,179],{},"can lag reality",": writes are debounced to\nat most once every five seconds, for the same UI-blocking reason above. A crash\nbetween a chunk write and the next metadata flush under-reports what is\ngenuinely on disk.",[65,182,183,184,187,188,190],{},"Chunk filenames cannot be stale in that way. Writes are single-shot\n",[82,185,186],{},"writeAsBytes",", not incremental, so a file that exists was fully written.\nHydration reads the filenames as ground truth and consults ",[82,189,88],{}," only\nfor fields it cannot derive from them.",[168,192,194],{"id":193},"not-eagerly","Not eagerly",[65,196,197],{},"The first implementation read every hydrated chunk's bytes into memory during\nthe scan. That crashed the receiver outright against a real multi-thousand-chunk\ntransfer — two in-progress transfers were enough to exhaust memory before the\napp finished starting.",[65,199,200],{},"Hydration now records what exists without loading it.",[74,202,204],{"id":203},"known-gap","Known gap",[65,206,207,210,211,214],{},[82,208,209],{},"flutter\u002Fthird_party\u002Fmobile_scanner"," is a vendored fork pinned at 7.2.0 via\n",[82,212,213],{},"dependency_overrides",", carrying a local patch that adds macOS external-camera\nenumeration and selection — something upstream does not support.",[65,216,217],{},"Upstream has moved to 7.4.0. Re-diffing the patch is real effort with real risk\nto camera selection, which is the entire reason the fork exists, for a changelog\nthat is mostly camera lifecycle and orientation fixes. It is recorded as a gap\nrather than silently carried.",{"title":139,"searchDepth":219,"depth":219,"links":220},2,[221,222,224,225,230],{"id":76,"depth":219,"text":77},{"id":92,"depth":219,"text":223},"Why not compute()",{"id":120,"depth":219,"text":121},{"id":151,"depth":219,"text":152,"children":226},[227,229],{"id":170,"depth":228,"text":171},3,{"id":193,"depth":228,"text":194},{"id":203,"depth":219,"text":204},"The worker isolate that owns decoding, and the disk hydration that makes a transfer resumable.","md",null,{},{"icon":39},{"title":36,"description":231},"G8yuuVbYuexRZ4mRv9k4hRurJcj8vgIS4LjEACNrllA",[239,241],{"title":31,"path":32,"stem":33,"description":240,"icon":34,"children":-1},"The two programs, the one-way link between them, and where each stage of a transfer happens.",{"title":46,"path":47,"stem":48,"description":242,"icon":23,"children":-1},"Every flag, subcommand and keybinding the sender accepts.",1786373778112]