feat: blobDownload facade + hand-maintained blob FFI bindings#20
Conversation
…013)
Adds the blob-transfer surface to peat_ffi.dart (hand-maintained per
README/justfile — the uniffi-bindgen-dart generator isn't available to
this team, so new methods are added by hand following existing
argBuf/returnBuf patterns, not by running a wholesale regen): the
BlobFetchStatus sealed class, BlobFetchHandle, and PeatNode methods
enableBlobTransfer/blobAddPeer/blobAddPeerId/blobPut/blobExistsLocally/
blobEndpointId/blobBoundAddr/blobFetchStart.
Adds PeatFlutterNode.blobDownload(hash, size, {peerIdHex}), a broadcast
Stream<BlobFetchStatus> modeled on the existing subscribeChanges poll
pattern — peerIdHex omitted runs the mesh-sync path, provided runs a
direct peer-targeted pull, closing the stream on a terminal status and
disposing the underlying fetch on early cancellation. Thin passthrough
methods expose the rest of the blob surface on the facade.
Verified end-to-end against a real compiled libpeat_ffi via a new Dart
smoke test (tool/smoke_blob.dart) that exercises every new method,
including a real two-node direct P2P transfer and a mesh-sync fetch of
an already-local blob — this caught three broken methods (blob_put,
blob_endpoint_id, blob_bound_addr) and one silent memory-corruption bug
(a missing 4-byte length prefix on the Vec<u8> arg to blob_put) that
flutter analyze/test alone did not.
Depends on peat#1017 (peat-ffi blob_fetch_start), itself blocked on
peat-mesh#274 — Cargo.toml carries a temporary [patch.crates-io] path
override for local development per peat/SKILL.md's cross-repo gotcha.
…ter demo Bump peat-ffi pin from =0.2.9 to =0.2.10 (blob surface now published) and drop the LOCAL-DEV-ONLY [patch.crates-io] path overrides that broke the arm64 CI build. Add an Attachments card to the peat-water example app exercising the blob transfer API: Store Sample (blobPut), Fetch from peer (blobDownload stream), download progress, local blob list with copy-to-clipboard hash, and blob endpoint advertisement in CRDT presence so peers auto-discover each other.
peat-bot
left a comment
There was a problem hiding this comment.
Peat QA Review (SHA: 14ef43c)
Scope reviewed: rust/Cargo.toml (peat-ffi pin 0.2.9 → 0.2.10, drop [patch.crates-io]), lib/src/generated/peat_ffi.dart (hand-added blob surface — BlobFetchStatus, BlobFetchHandle, and eight PeatNode blob methods), lib/src/peat_node.dart (blobDownload broadcast stream + passthroughs), lib/peat_flutter.dart (re-exports), example/lib/main.dart (Attachments card wiring), tool/smoke_blob.dart (headless two-node smoke test), rust/Cargo.lock.
Verified positives:
- Native artifact identity in
rust/Cargo.tomlremainsname = "peat_ffi"(libpeat_ffi.{so,dylib,a}) — UniFFI namespace intact. bindingsContractVersion = 30retained; per-symbol checksums for all eight new blob methods +BlobFetchHandle.{dispose,status}are hard-coded in_ensureApiIntegrity, so any drift vs. peat-ffi 0.2.10 fails at first load rather than silently.- Hand-added
blob_putcorrectly prepends the 4-byte BE length prefix on theVec<u8>arg (matches the workingingestInboundFramepattern; this was the memory-corruption bug the smoke test caught). Option<String>args (blob_fetch_start.peerIdHex,enable_blob_transfer.bindAddr) lowered via the standard i8-tag + string writer path.BlobFetchHandlegets its ownFinalizer+ free/clone symbols +close()/_ensureOpen()mirroringPeatNode/SubscriptionHandle— no leak or use-after-free on the Dart side.PeatFlutterNode.blobDownload's dispose/close ordering matches the existingsubscribeChanges/startOutboundFramesidiom: closure-captureshandle, guards with!handle.isClosed, closes on terminalCompleted/Failed, dispose+close on new-call and onPeatFlutterNode.dispose.ChangeOrigin.local()/.remote()convenience constructors +isLocal/peerIdfields preserved (the "known manual fix" flagged in the PR body).- No cross-platform native-build script changes required —
rust/Cargo.tomlis the single source, per-platform wrappers (ios/build-rust.sh,macos/build-rust.sh,android/build.gradleviacargo-ndk,src/CMakeLists.txt) all pick up the new peat-ffi 0.2.10 pin automatically. - No security-primitive changes; no schema/proto stub changes.
Findings:
[WARNING] tool/smoke_blob.dart has not been re-run against a libpeat_ffi built from the published peat-ffi 0.2.10 crate (the checkbox "Re-verify against the real published peat-ffi once peat#1017 ships" in the PR test plan is still unchecked, and the second commit's message only mentions the arm64 CI build passing, which only exercises linking — not runtime symbol resolution or checksum verification). This is the exact verification the PR body committed to before merge, and the smoke test was itself the mechanism that caught three broken FFI stub methods (blob_endpoint_id, blob_bound_addr, blob_put) and one memory-corruption bug (missing length prefix) that neither flutter analyze nor flutter test surfaced. The hand-maintained checksums in _ensureApiIntegrity (peat_ffi.dart:3961+) were computed against the dev branch — if anything shifted between the pre-release local branch and the published 0.2.10 crate (added arg, reordered method, minor signature edit), the bindings will throw StateError at first createNode() on-device. Action: rebuild rust/ against published peat-ffi 0.2.10 (with [patch.crates-io] removed, as this commit does) and run dart run tool/smoke_blob.dart <libpath> to confirm SMOKE OK, then check the box in the PR body.
peat-bot
left a comment
There was a problem hiding this comment.
Peat QA Review (SHA: 14ef43c)
Incremental review since 14ef43c.
No new commits since the prior review (git diff 14ef43c..14ef43c empty) — this re-review was triggered by an edit to the PR title/description. Re-evaluating the single prior finding against the current PR body.
Prior findings:
- Resolved: [WARNING]
tool/smoke_blob.darthad not been re-run against alibpeat_ffibuilt from publishedpeat-ffi 0.2.10. The current PR test plan now shows- [x] Re-verified against published peat-ffi 0.2.10 — SMOKE OK, matching the exact verification the prior review requested. All previously verified positives (native artifact identitypeat_ffi,bindingsContractVersion = 30retained, per-symbol checksums for new blob methods hard-coded in_ensureApiIntegrity,blob_put4-byte BE length prefix onVec<u8>,Option<String>lowering via i8-tag + string writer,BlobFetchHandleFinalizer + free/clone symbols,blobDownloaddispose/close ordering, preservedChangeOriginconvenience fields, no cross-platform native-build script changes, no security-primitive or schema changes) remain intact — nothing in the delta could have regressed them.
No findings.
Summary
Adds blob-transfer support to the Dart facade (peat#1013):
PeatFlutterNode.blobDownload(hashHex, sizeBytes, {peerIdHex})— abroadcast
Stream<BlobFetchStatus>modeled on the existingsubscribeChanges/startOutboundFramespoll pattern. OmitpeerIdHexfor mesh-sync mode (automatic candidate-peer selection); pass it for
direct P2P mode (pull straight from that peer, no fallback). The stream
closes itself on a terminal
Completed/Failedstatus; cancelling earlydisposes the underlying fetch.
enableBlobTransfer,blobAddPeer,blobAddPeerId,blobPut,blobExistsLocally,blobEndpointId,blobBoundAddr.peat-ffi 0.2.10(no path overrides).blob API: Store Sample (blobPut), Fetch from peer (blobDownload stream),
download progress, local blob list with copy-to-clipboard hash, and blob
endpoint advertisement in CRDT presence so peers auto-discover each other.
Important: how the bindings were added
lib/src/generated/peat_ffi.dart's own header claims "Generated byuniffi-bindgen-dart" — that's stale. Per
README.md("Bindings aremaintained by hand — the
uniffi-bindgen-dartgenerator is not currentlypublicly available") and the
justfile'scheck-bindingstask (whichdocuments known manual fixes), this file is genuinely hand-maintained.
I initially ran a copy of
uniffi-bindgen-dartagainst the compiledlibrary expecting a safe regen. It was not: a full regen would have (a)
silently broken
createNode's return-handle unwrapping (a documented"known manual fix" the tool doesn't reproduce), (b) replaced a working
peatNodeInvokeAllPeerTransportStateswith athrow UnsupportedError(...)stub — the tool can't generate real code for that ABI shape and never
told anyone, and (c) dropped the
ChangeOrigin.isLocal/.peerIdconvenience fields that a previous session hand-added. I reverted the full
regen and instead hand-added only the new blob symbols, extracted via a
diff between two regens (clean vs. with-blob) to isolate exactly the
additive changes, then inserted them into the real hand-maintained file
following existing patterns. Two more instances of the same
generator-stub bug turned up in the diff itself (
blob_endpoint_id,blob_bound_addr,blob_putall came through asthrow UnsupportedError(...)) and needed hand-written real implementations,modeled on other working methods with the same signature shape
(
endpointSocketAddr,nodeId).peat-ffi/src/dart_ffi.rs(this repo's Rust-side hand-written ABI shim —#[uniffi::export]alone doesn't wire up the custom flat-array"ffibuffer" calling convention this repo's Dart bindings use) needed
matching new entry points too; see peat#1017.
Verification
Rather than trust
flutter analyze/flutter testalone (they can'tcatch a wrong-but-well-typed FFI buffer layout), I wrote
tool/smoke_blob.dart— a headless smoke test in this repo's existingstyle (
tool/smoke_reconnect.dart) that drives two real nodes against theactual compiled
libpeat_ffi.dylib: enable transfer, put a blob, confirmexistence, register a peer explicitly, direct-mode fetch across two real
nodes, dispose, id-only peer registration, and a mesh-sync fetch of an
already-local blob. This is what caught the three broken stub methods
above, plus a fourth real bug:
blob_put'sVec<u8>argument was missingUniFFI's required 4-byte big-endian length prefix (present on other
Vec<u8>-arg methods likeingestInboundFramebut absent from the tool'soutput for
blob_put), which caused a Rust-side panic on every call untilfixed.
Test plan
flutter analyze— no issues inlib/flutter test— all existing tests pass (incl.ChangeOriginJSONround-trip, confirming the hand-added bindings didn't disturb the
existing hand-maintained surface)
dart run tool/smoke_blob.dart <path-to-libpeat_ffi.dylib>—SMOKE OKpeat-ffi 0.2.10—SMOKE OK