irontide_engine/types.rs
1use std::collections::HashMap;
2use std::net::SocketAddr;
3use std::path::PathBuf;
4
5use bytes::Bytes;
6use tokio::sync::oneshot;
7
8use irontide_storage::Bitfield;
9use irontide_wire::ExtHandshake;
10
11// Payload + vocabulary types relocated to `irontide-session-types` at M244a
12// (types.rs god-module decomposition). Re-exported here so `crate::types::*`
13// paths resolve unchanged for internal callers and the `irontide_session`
14// public facade; the actor message types below (`PeerEvent` / `PeerCommand` /
15// `TorrentCommand` / `BlockEntry`) stay until the M244b/M244c actor split.
16pub use irontide_session_types::{
17 FileMode, FileStatus, PartialPieceInfo, PeerInfo, SessionStats, SettingsDelta, TorrentConfig,
18 TorrentFlags, TorrentState, TorrentStats,
19};
20// `TorrentSummary` is referenced only by engine's inline unit tests via
21// `crate::types::…`; production engine code uses `irontide_session_types::…`
22// directly. Gate the re-export to `#[cfg(test)]` so neither the lib nor the
23// test build flags it unused under `-D warnings`. (The other session-types
24// payloads have no engine-internal `crate::types::` consumer post-M244b split.)
25#[cfg(test)]
26pub use irontide_session_types::TorrentSummary;
27
28/// Lightweight record of a single block write completion,
29/// carried in `PeerEvent::PieceBlocksBatch`.
30#[derive(Debug, Clone)]
31pub(crate) struct BlockEntry {
32 pub index: u32, // piece index
33 pub begin: u32, // byte offset within piece
34 pub length: u32, // block size (usually 16384)
35}
36
37/// Events sent from a `PeerTask` back to the `TorrentActor`.
38#[derive(Debug)]
39#[allow(dead_code)] // consumed by peer/torrent modules (not yet implemented)
40pub(crate) enum PeerEvent {
41 Bitfield {
42 peer_addr: SocketAddr,
43 bitfield: Bitfield,
44 },
45 Have {
46 peer_addr: SocketAddr,
47 index: u32,
48 },
49 /// BEP 54: Peer no longer has a piece (`lt_donthave` extension).
50 DontHave {
51 peer_addr: SocketAddr,
52 index: u32,
53 },
54 PieceData {
55 peer_addr: SocketAddr,
56 index: u32,
57 begin: u32,
58 data: Bytes,
59 },
60 /// Block completion from a peer task's direct disk write.
61 /// Sent immediately on each block write for real-time `TorrentActor` visibility.
62 PieceBlocksBatch {
63 peer_addr: SocketAddr,
64 blocks: Vec<BlockEntry>,
65 },
66 PeerChoking {
67 peer_addr: SocketAddr,
68 choking: bool,
69 },
70 PeerInterested {
71 peer_addr: SocketAddr,
72 interested: bool,
73 },
74 ExtHandshake {
75 peer_addr: SocketAddr,
76 handshake: ExtHandshake,
77 },
78 MetadataPiece {
79 peer_addr: SocketAddr,
80 piece: u32,
81 data: Bytes,
82 total_size: u64,
83 },
84 MetadataReject {
85 peer_addr: SocketAddr,
86 piece: u32,
87 },
88 PexPeers {
89 new_peers: Vec<SocketAddr>,
90 },
91 TrackersReceived {
92 tracker_urls: Vec<String>,
93 },
94 IncomingRequest {
95 peer_addr: SocketAddr,
96 index: u32,
97 begin: u32,
98 length: u32,
99 },
100 RejectRequest {
101 peer_addr: SocketAddr,
102 index: u32,
103 begin: u32,
104 length: u32,
105 },
106 AllowedFast {
107 peer_addr: SocketAddr,
108 index: u32,
109 },
110 SuggestPiece {
111 peer_addr: SocketAddr,
112 index: u32,
113 },
114 /// Peer successfully connected with a specific transport.
115 TransportIdentified {
116 peer_addr: SocketAddr,
117 transport: crate::rate_limiter::PeerTransport,
118 },
119 /// M140: BT handshake completed successfully — peer is now truly live.
120 /// Sent from `run_peer` after BT protocol handshake exchange validates
121 /// `info_hash` and `peer_id`. Triggers `mark_live()` in the actor.
122 HandshakeComplete {
123 peer_addr: SocketAddr,
124 /// M174: Whether MSE/PE negotiated RC4 encryption for this connection.
125 is_encrypted: bool,
126 },
127 Disconnected {
128 peer_addr: SocketAddr,
129 reason: Option<String>,
130 },
131 WebSeedPieceData {
132 url: String,
133 index: u32,
134 data: Bytes,
135 },
136 WebSeedError {
137 url: String,
138 piece: u32,
139 message: String,
140 },
141 /// M178: Periodic per-URL progress update from `WebSeedTask`. Coalesced
142 /// by the task's 250 ms throttle (configurable via
143 /// `Settings::web_seed_progress_throttle_ms`); the actor accumulates
144 /// `WebSeedStats` from these. `error == Some(_)` records a transition
145 /// into the errored state; the field is reset to `None` on recovery
146 /// emissions but the accumulated `last_error` on `WebSeedStats`
147 /// persists per Issue 2.2.
148 WebSeedProgress {
149 url: String,
150 bytes: u64,
151 rate_bps: u64,
152 error: Option<String>,
153 },
154 /// M186: Web seed completed backoff and is ready for new piece assignments.
155 WebSeedRetryReady {
156 url: String,
157 },
158 /// M186: Web seed permanently failed after max consecutive failures.
159 WebSeedPermanentFailure {
160 url: String,
161 },
162 /// BEP 52: Received hash response from peer.
163 HashesReceived {
164 peer_addr: SocketAddr,
165 request: irontide_core::HashRequest,
166 hashes: Vec<irontide_core::Id32>,
167 },
168 /// BEP 52: Peer rejected our hash request.
169 HashRequestRejected {
170 peer_addr: SocketAddr,
171 request: irontide_core::HashRequest,
172 },
173 /// BEP 52: Peer sent a hash request to us.
174 IncomingHashRequest {
175 peer_addr: SocketAddr,
176 request: irontide_core::HashRequest,
177 },
178 /// BEP 55: Received a Rendezvous request (we are the relay).
179 HolepunchRendezvous {
180 peer_addr: SocketAddr,
181 target: SocketAddr,
182 },
183 /// BEP 55: Received a Connect message (we should initiate simultaneous connect).
184 HolepunchConnect {
185 peer_addr: SocketAddr,
186 target: SocketAddr,
187 },
188 /// BEP 55: Received an Error message from the relay.
189 HolepunchError {
190 peer_addr: SocketAddr,
191 target: SocketAddr,
192 error_code: u32,
193 },
194 /// MSE handshake failed — peer is being retried with a different encryption mode.
195 /// Carries the new command channel sender so the `TorrentActor` can
196 /// update its `PeerState`.
197 MseRetry {
198 peer_addr: SocketAddr,
199 cmd_tx: tokio::sync::mpsc::Sender<PeerCommand>,
200 },
201 /// Peer released a piece it was downloading (choke, error, disconnect).
202 PieceReleased {
203 peer_addr: SocketAddr,
204 piece: u32,
205 },
206 /// M187: Requester asks the actor to acquire a piece via `PieceTracker`.
207 /// Actor responds with the piece index via the oneshot, or `NoneAvailable`
208 /// if the peer has no dispatchable pieces.
209 AcquirePiece {
210 peer_addr: SocketAddr,
211 response_tx: tokio::sync::oneshot::Sender<crate::piece_reservation::AcquireResponse>,
212 },
213}
214
215/// Commands sent from the `TorrentActor` to a `PeerTask`.
216#[derive(Debug)]
217#[allow(dead_code)] // consumed by peer/torrent modules (not yet implemented)
218pub(crate) enum PeerCommand {
219 Request {
220 index: u32,
221 begin: u32,
222 length: u32,
223 },
224 Cancel {
225 index: u32,
226 begin: u32,
227 length: u32,
228 },
229 SetChoking(bool),
230 SetInterested(bool),
231 Have(u32),
232 RequestMetadata {
233 piece: u32,
234 },
235 RejectRequest {
236 index: u32,
237 begin: u32,
238 length: u32,
239 },
240 AllowedFast(u32),
241 SendPiece {
242 index: u32,
243 begin: u32,
244 data: Bytes,
245 },
246 /// Send an updated extension handshake (e.g. BEP 21 upload-only).
247 SendExtHandshake(irontide_wire::ExtHandshake),
248 /// BEP 6: Suggest a piece to the peer.
249 SuggestPiece(u32),
250 /// BEP 52: Send a hash request to the peer.
251 SendHashRequest(irontide_core::HashRequest),
252 /// BEP 52: Send hashes in response to a peer's request.
253 SendHashes {
254 request: irontide_core::HashRequest,
255 hashes: Vec<irontide_core::Id32>,
256 },
257 /// BEP 52: Reject a peer's hash request.
258 SendHashReject(irontide_core::HashRequest),
259 /// BEP 11: Send a PEX message to this peer.
260 SendPex {
261 message: crate::pex::PexMessage,
262 },
263 /// BEP 55: Send a holepunch message to this peer.
264 SendHolepunch(irontide_wire::HolepunchMessage),
265 /// Update the piece count after BEP 9 metadata assembly.
266 UpdateNumPieces(u32),
267 /// M159: Tell the peer task to stop dispatching block requests.
268 ///
269 /// Translated to `DispatchCommand::Stop` by the reader loop. The requester
270 /// transitions back to the idle state waiting for a fresh `StartRequesting`.
271 /// Uploads are unaffected.
272 StopRequesting,
273 /// M75: Actor sends reservation state to peer task for integrated dispatch.
274 /// Sent after metadata download (magnet) or at peer connection (non-magnet).
275 StartRequesting {
276 piece_notify: std::sync::Arc<tokio::sync::Notify>,
277 disk_handle: Option<crate::disk::DiskHandle>,
278 write_error_tx: tokio::sync::mpsc::Sender<crate::disk::DiskWriteError>,
279 lengths: irontide_core::Lengths,
280 },
281 Shutdown,
282}
283
284/// Helper trait combining [`AsyncRead`] + [`AsyncWrite`] for trait-object erasure.
285///
286/// Rust doesn't allow `dyn AsyncRead + AsyncWrite` directly, so this trait
287/// combines both into a single trait that can be used as a trait object.
288pub trait AsyncReadWrite: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send {}
289
290impl<T> AsyncReadWrite for T where T: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send {}
291
292/// Boxed async stream (`AsyncRead` + `AsyncWrite` + Unpin + Send) with a Debug impl.
293///
294/// Used for incoming SSL peer connections where the concrete TLS type is erased.
295pub struct BoxedAsyncStream(pub Box<dyn AsyncReadWrite>);
296
297impl std::fmt::Debug for BoxedAsyncStream {
298 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
299 f.write_str("BoxedAsyncStream(..)")
300 }
301}
302
303/// Commands sent from a `TorrentHandle` to the `TorrentActor`.
304///
305/// `pub` at the M244b engine split: session-core (`irontide-session`) sends a
306/// subset of these (`ForceResume` / `SetSeedRatioLimit` / `UpdateSettings`) across
307/// the crate boundary, re-exported there as `crate::types::TorrentCommand`.
308#[derive(Debug)]
309#[allow(dead_code)] // most variants are consumed only within the engine crate
310pub enum TorrentCommand {
311 AddPeers {
312 peers: Vec<SocketAddr>,
313 source: crate::peer_state::PeerSource,
314 },
315 Stats {
316 reply: oneshot::Sender<TorrentStats>,
317 },
318 Pause,
319 Queue,
320 Resume,
321 /// Resume the torrent bypassing queue limits (force-start).
322 ForceResume,
323 Shutdown,
324 /// M170: update the category label recorded on this torrent. `None`
325 /// clears the label (uncategorised).
326 SetCategory {
327 category: Option<String>,
328 reply: oneshot::Sender<()>,
329 },
330 /// M171: replace the torrent's tag set wholesale (qBt-compat).
331 ///
332 /// Mirrors qBt's `addTags` / `removeTags` wire behaviour at the API
333 /// layer — always a wholesale replacement at the engine layer.
334 SetTags {
335 tags: Vec<String>,
336 reply: oneshot::Sender<()>,
337 },
338 /// M171 Lane B: snapshot the list of configured web seed URLs
339 /// (BEP 19 `url-list` + BEP 17 `httpseeds`).
340 ///
341 /// Returns an empty vec when metadata hasn't resolved yet.
342 GetWebSeeds {
343 reply: oneshot::Sender<Vec<String>>,
344 },
345 /// M171 Lane B: snapshot the per-piece state array as qBt codes.
346 ///
347 /// Each element is one of {0: not downloaded, 1: downloading,
348 /// 2: downloaded + checked}. Returns an empty vec when metadata
349 /// hasn't resolved yet (piece count unknown).
350 GetPieceStates {
351 reply: oneshot::Sender<Vec<u8>>,
352 },
353 /// M171 Lane B: return a paginated slice of per-piece hashes.
354 ///
355 /// M245 L3: the reply carries the RAW hash bytes for the requested window
356 /// (v1 / hybrid: 20-byte SHA-1; v2-only: 32-byte SHA-256) — one `Vec<u8>`
357 /// per piece. The `hex::encode` is done by the caller
358 /// ([`TorrentHandle::get_piece_hashes`]) OFF the recv loop, so the actor no
359 /// longer encodes (and discards) every hash on its hot path. Returns an
360 /// empty vec when metadata hasn't resolved yet, or when `offset` is past the
361 /// end of the hash list.
362 GetPieceHashes {
363 offset: u32,
364 limit: u32,
365 reply: oneshot::Sender<Vec<Vec<u8>>>,
366 },
367 SaveResumeData {
368 reply: oneshot::Sender<crate::Result<irontide_core::FastResumeData>>,
369 },
370 /// M245 F1 — atomically take resume data IFF the torrent is dirty,
371 /// clearing `need_save_resume` in the SAME actor turn (no `.await`
372 /// between the read and the clear). Replaces the racy `SaveResumeData` +
373 /// `ClearSaveResumeFlag` two-step, where a `need_save_resume` set between
374 /// the build and the clear was silently lost. `Ok(None)` = not dirty.
375 TakeResumeIfDirty {
376 reply: oneshot::Sender<crate::Result<Option<irontide_core::FastResumeData>>>,
377 },
378 SetFilePriority {
379 index: usize,
380 priority: irontide_core::FilePriority,
381 reply: oneshot::Sender<crate::Result<()>>,
382 },
383 FilePriorities {
384 reply: oneshot::Sender<Vec<irontide_core::FilePriority>>,
385 },
386 ForceReannounce,
387 TrackerList {
388 reply: oneshot::Sender<Vec<crate::tracker_manager::TrackerInfo>>,
389 },
390 Scrape {
391 reply: oneshot::Sender<Option<(String, irontide_tracker::ScrapeInfo)>>,
392 },
393 /// Incoming peer routed from the session-level accept loop (TCP or uTP).
394 IncomingPeer {
395 stream: crate::transport::BoxedStream,
396 addr: SocketAddr,
397 },
398 /// Open a streaming reader for a file within the torrent.
399 OpenFile {
400 file_index: usize,
401 reply: oneshot::Sender<crate::Result<crate::streaming::FileStreamHandle>>,
402 },
403 /// Update the external IP for BEP 40 peer priority calculation.
404 UpdateExternalIp {
405 ip: std::net::IpAddr,
406 },
407 /// Move torrent data files to a new directory.
408 MoveStorage {
409 new_path: PathBuf,
410 reply: oneshot::Sender<crate::Result<()>>,
411 },
412 /// Incoming SSL peer routed from the session-level SSL listener (M42).
413 ///
414 /// The TLS handshake has already been completed by the session actor.
415 SpawnSslPeer {
416 addr: SocketAddr,
417 stream: BoxedAsyncStream,
418 },
419 /// Set the per-torrent download rate limit (bytes/sec, 0 = unlimited).
420 SetDownloadLimit {
421 bytes_per_sec: u64,
422 reply: oneshot::Sender<()>,
423 },
424 /// Set the per-torrent upload rate limit (bytes/sec, 0 = unlimited).
425 SetUploadLimit {
426 bytes_per_sec: u64,
427 reply: oneshot::Sender<()>,
428 },
429 /// Get the current per-torrent download rate limit (bytes/sec, 0 = unlimited).
430 DownloadLimit {
431 reply: oneshot::Sender<u64>,
432 },
433 /// Get the current per-torrent upload rate limit (bytes/sec, 0 = unlimited).
434 UploadLimit {
435 reply: oneshot::Sender<u64>,
436 },
437 /// Enable or disable sequential (in-order) piece downloading.
438 SetSequentialDownload {
439 enabled: bool,
440 reply: oneshot::Sender<()>,
441 },
442 /// Query whether sequential downloading is enabled.
443 IsSequentialDownload {
444 reply: oneshot::Sender<bool>,
445 },
446 /// Enable or disable BEP 16 super seeding mode.
447 SetSuperSeeding {
448 enabled: bool,
449 reply: oneshot::Sender<()>,
450 },
451 /// Query whether super seeding mode is enabled.
452 IsSuperSeeding {
453 reply: oneshot::Sender<bool>,
454 },
455 /// Enable or disable user-requested seed-only mode (M159).
456 ///
457 /// When enabled, the torrent stops scheduling new block requests and
458 /// cancels all in-flight requests, but continues to serve uploads to
459 /// interested peers. Mirrors libtorrent's `seed_mode` flag.
460 SetSeedMode {
461 enabled: bool,
462 reply: oneshot::Sender<()>,
463 },
464 /// Override the per-torrent seed ratio limit (`None` = use session default).
465 SetSeedRatioLimit {
466 limit: Option<f64>,
467 reply: oneshot::Sender<()>,
468 },
469 /// Add a new tracker URL (fire-and-forget at torrent level).
470 AddTracker {
471 url: String,
472 },
473 /// Replace all tracker URLs with a new set.
474 ReplaceTrackers {
475 urls: Vec<String>,
476 reply: oneshot::Sender<()>,
477 },
478 /// Trigger a full piece verification (force recheck).
479 ForceRecheck {
480 reply: oneshot::Sender<crate::Result<()>>,
481 },
482 /// Rename a file within the torrent on disk.
483 RenameFile {
484 file_index: usize,
485 new_name: String,
486 reply: oneshot::Sender<crate::Result<()>>,
487 },
488 /// Set the per-torrent maximum number of connections (0 = use global default).
489 SetMaxConnections {
490 limit: usize,
491 reply: oneshot::Sender<()>,
492 },
493 /// Get the current per-torrent maximum connection limit.
494 MaxConnections {
495 reply: oneshot::Sender<usize>,
496 },
497 /// Set the per-torrent maximum number of unchoke slots (upload slots).
498 SetMaxUploads {
499 limit: usize,
500 reply: oneshot::Sender<()>,
501 },
502 /// Get the current per-torrent maximum unchoke slots (upload slots).
503 MaxUploads {
504 reply: oneshot::Sender<usize>,
505 },
506 /// Get per-peer details for all connected peers.
507 GetPeerInfo {
508 reply: oneshot::Sender<Vec<PeerInfo>>,
509 },
510 /// Get in-flight piece download status (the download queue).
511 GetDownloadQueue {
512 reply: oneshot::Sender<Vec<PartialPieceInfo>>,
513 },
514 /// Check whether a specific piece has been downloaded.
515 HavePiece {
516 index: u32,
517 reply: oneshot::Sender<bool>,
518 },
519 /// Get per-piece availability counts from connected peers.
520 PieceAvailability {
521 reply: oneshot::Sender<Vec<u32>>,
522 },
523 /// Get per-file bytes-downloaded progress.
524 FileProgress {
525 reply: oneshot::Sender<Vec<u64>>,
526 },
527 /// Get the torrent's identity hashes (v1 and/or v2).
528 InfoHashes {
529 reply: oneshot::Sender<irontide_core::InfoHashes>,
530 },
531 /// Get the full v1 metainfo (None for magnet links before metadata received).
532 TorrentFile {
533 reply: oneshot::Sender<Option<irontide_core::TorrentMetaV1>>,
534 },
535 /// Get the full v2 metainfo (None if not a v2/hybrid torrent or before metadata received).
536 TorrentFileV2 {
537 reply: oneshot::Sender<Option<irontide_core::TorrentMetaV2>>,
538 },
539 /// Force an immediate DHT announce (fire-and-forget at torrent level).
540 ForceDhtAnnounce,
541 /// Read all data for a specific piece from disk.
542 ReadPiece {
543 index: u32,
544 reply: oneshot::Sender<crate::Result<bytes::Bytes>>,
545 },
546 /// Flush the disk write cache for this torrent.
547 FlushCache {
548 reply: oneshot::Sender<crate::Result<()>>,
549 },
550 /// Clear the error state and resume if the torrent was paused due to error.
551 ClearError,
552 /// Get per-file open/mode status based on torrent state.
553 FileStatus {
554 reply: oneshot::Sender<Vec<crate::types::FileStatus>>,
555 },
556 /// Read the current torrent flags as a bitflag set.
557 Flags {
558 reply: oneshot::Sender<TorrentFlags>,
559 },
560 /// Set (enable) the specified torrent flags.
561 SetFlags {
562 flags: TorrentFlags,
563 reply: oneshot::Sender<()>,
564 },
565 /// Unset (disable) the specified torrent flags.
566 UnsetFlags {
567 flags: TorrentFlags,
568 reply: oneshot::Sender<()>,
569 },
570 /// Immediately initiate a peer connection to the given address.
571 ConnectPeer {
572 addr: SocketAddr,
573 },
574 /// Clear the `need_save_resume` dirty flag after a successful file save (M161).
575 ClearSaveResumeFlag,
576 /// M245 F1 — re-arm `need_save_resume` after a failed resume WRITE.
577 /// [`TakeResumeIfDirty`](Self::TakeResumeIfDirty) clears the flag on
578 /// capture; without this the captured-but-unwritten state would never be
579 /// retried on a later save cycle.
580 MarkResumeDirty,
581 /// Restore a piece bitmap from resume data (M161 Phase 4).
582 ///
583 /// Replaces the chunk tracker's bitfield with the provided raw piece bytes.
584 /// The handler validates the bitfield length before applying.
585 RestoreResumeBitmap {
586 /// Raw piece bitfield bytes from resume data.
587 pieces: Vec<u8>,
588 /// Reply with `Ok(())` on success or an error if validation fails.
589 reply: oneshot::Sender<crate::Result<()>>,
590 },
591 /// M178: Restore the per-URL web-seed stats map from resume data.
592 ///
593 /// Used by the post-add resume-restore path so that downloaded-byte
594 /// counters and last-error / consecutive-failure state survive app
595 /// restart (Tension-1 fast-resume persistence).
596 RestoreWebSeedStats {
597 /// Map of URL → stats from `FastResumeData::web_seed_stats`.
598 stats: HashMap<String, irontide_core::WebSeedStats>,
599 /// Reply with `Ok(())` on success.
600 reply: oneshot::Sender<crate::Result<()>>,
601 },
602 /// M178 (Lane B3 / TODO-2): cumulative `(pex, lsd)` unique-peer counts
603 /// for the GUI Trackers tab + qBt v2 trackers pseudo-tracker rows.
604 GetPeerSourceCounts {
605 /// Reply with `(pex_peer_count, lsd_peer_count)`.
606 reply: oneshot::Sender<(usize, usize)>,
607 },
608 /// Per-peer cumulative unchoke duration over the torrent's lifetime.
609 /// Keyed by `SocketAddr`; merges live `PeerState` accumulators with
610 /// the durable per-torrent map so reconnects preserve history.
611 /// Used by libtorrent-mirror perf scenarios that gate on
612 /// optimistic-unchoke fairness.
613 QueryUnchokeDurations {
614 /// Reply with one entry per peer ever unchoked by us.
615 reply: oneshot::Sender<HashMap<SocketAddr, std::time::Duration>>,
616 },
617 /// M178 (Lane C): snapshot of per-URL web-seed stats for the qBt v2
618 /// `/api/v2/torrents/webseeds` endpoint and the GUI HTTP Sources tab.
619 GetWebSeedStats {
620 /// Reply with one entry per URL with active stats.
621 reply: oneshot::Sender<Vec<irontide_core::WebSeedStats>>,
622 },
623 /// M147: Pre-resolved metadata from the background `MetadataResolver`.
624 ///
625 /// Sent by `SessionActor::spawn_metadata_resolver()` when the background
626 /// resolver successfully obtains torrent metadata before the `TorrentActor`'s
627 /// own `FetchingMetadata` phase completes. This is a race: first to resolve
628 /// wins; the other path's result is silently discarded.
629 PreResolvedMetadata {
630 /// Raw bencoded info dictionary bytes.
631 info_bytes: Vec<u8>,
632 /// Peers that were successfully connected during metadata resolution
633 /// (for pre-seeding the peer pipeline).
634 peers: Vec<SocketAddr>,
635 },
636 /// v0.173.1: single source of truth for torrent metadata.
637 ///
638 /// Returns `Some(meta.clone())` if the actor has assembled metadata (via
639 /// its own `ut_metadata` fetch or a `PreResolvedMetadata` push), else
640 /// `None`. Replaces `SessionActor.TorrentEntry.meta` as the authoritative
641 /// source — see class-A archaeology in the v0.173.1 plan file at
642 /// `docs/plans/2026-04-22-irontide-v0.173.1-qbt-v2-bug-sweep.md`.
643 GetMeta {
644 /// Reply with `Some(meta)` when available, `None` for a magnet that
645 /// hasn't resolved metadata yet.
646 reply: oneshot::Sender<Option<irontide_core::TorrentMetaV1>>,
647 },
648 /// **TEST-ONLY (v0.173.2).** Synchronously inject a fully-assembled info-dict
649 /// payload via the same internal handler as the M147 `PreResolvedMetadata`
650 /// path, but with backpressure + completion-ack so tests can rely on the
651 /// metadata being processed when the future resolves. The M147 fast-path
652 /// uses `try_send` and is fire-and-forget by design (resolver shouldn't
653 /// block); this variant is the synchronous-test counterpart.
654 #[cfg(feature = "test-util")]
655 TestInjectMetadata {
656 /// Raw bencoded info dictionary bytes.
657 info_bytes: Vec<u8>,
658 /// Completion ack — fired after `handle_pre_resolved_metadata` returns.
659 reply: oneshot::Sender<()>,
660 },
661 /// v0.187.1: broadcast changed session-level settings to a running torrent.
662 ///
663 /// Patches `self.config` fields so that settings changes made via
664 /// Preferences → Apply take effect on existing torrents, not just
665 /// newly-added ones.
666 UpdateSettings(SettingsDelta),
667}