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, PeerConnectionKind, PeerInfo, SessionStats,
18 SettingsDelta, TorrentConfig, 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 /// M253/ER2: enable or disable first/last-pieces-first ordering.
447 SetPrioritizeFirstLastPieces {
448 enabled: bool,
449 reply: oneshot::Sender<()>,
450 },
451 /// M253/ER2: query whether first/last-pieces-first ordering is enabled.
452 IsPrioritizeFirstLastPieces {
453 reply: oneshot::Sender<bool>,
454 },
455 /// Enable or disable BEP 16 super seeding mode.
456 SetSuperSeeding {
457 enabled: bool,
458 reply: oneshot::Sender<()>,
459 },
460 /// Query whether super seeding mode is enabled.
461 IsSuperSeeding {
462 reply: oneshot::Sender<bool>,
463 },
464 /// Enable or disable user-requested seed-only mode (M159).
465 ///
466 /// When enabled, the torrent stops scheduling new block requests and
467 /// cancels all in-flight requests, but continues to serve uploads to
468 /// interested peers. Mirrors libtorrent's `seed_mode` flag.
469 SetSeedMode {
470 enabled: bool,
471 reply: oneshot::Sender<()>,
472 },
473 /// Override the per-torrent seed ratio limit (`None` = use session default).
474 SetSeedRatioLimit {
475 limit: Option<f64>,
476 reply: oneshot::Sender<()>,
477 },
478 /// Add a new tracker URL (fire-and-forget at torrent level).
479 AddTracker {
480 url: String,
481 },
482 /// Replace all tracker URLs with a new set.
483 ReplaceTrackers {
484 urls: Vec<String>,
485 reply: oneshot::Sender<()>,
486 },
487 /// Trigger a full piece verification (force recheck).
488 ForceRecheck {
489 reply: oneshot::Sender<crate::Result<()>>,
490 },
491 /// Rename a file within the torrent on disk.
492 RenameFile {
493 file_index: usize,
494 new_name: String,
495 reply: oneshot::Sender<crate::Result<()>>,
496 },
497 /// Set the per-torrent maximum number of connections (0 = use global default).
498 SetMaxConnections {
499 limit: usize,
500 reply: oneshot::Sender<()>,
501 },
502 /// Get the current per-torrent maximum connection limit.
503 MaxConnections {
504 reply: oneshot::Sender<usize>,
505 },
506 /// Set the per-torrent maximum number of unchoke slots (upload slots).
507 SetMaxUploads {
508 limit: usize,
509 reply: oneshot::Sender<()>,
510 },
511 /// Get the current per-torrent maximum unchoke slots (upload slots).
512 MaxUploads {
513 reply: oneshot::Sender<usize>,
514 },
515 /// Get per-peer details for all connected peers.
516 GetPeerInfo {
517 reply: oneshot::Sender<Vec<PeerInfo>>,
518 },
519 /// Get in-flight piece download status (the download queue).
520 GetDownloadQueue {
521 reply: oneshot::Sender<Vec<PartialPieceInfo>>,
522 },
523 /// Check whether a specific piece has been downloaded.
524 HavePiece {
525 index: u32,
526 reply: oneshot::Sender<bool>,
527 },
528 /// Get per-piece availability counts from connected peers.
529 PieceAvailability {
530 reply: oneshot::Sender<Vec<u32>>,
531 },
532 /// Get per-file bytes-downloaded progress.
533 FileProgress {
534 reply: oneshot::Sender<Vec<u64>>,
535 },
536 /// Get the torrent's identity hashes (v1 and/or v2).
537 InfoHashes {
538 reply: oneshot::Sender<irontide_core::InfoHashes>,
539 },
540 /// Get the full v1 metainfo (None for magnet links before metadata received).
541 TorrentFile {
542 reply: oneshot::Sender<Option<irontide_core::TorrentMetaV1>>,
543 },
544 /// Get the full v2 metainfo (None if not a v2/hybrid torrent or before metadata received).
545 TorrentFileV2 {
546 reply: oneshot::Sender<Option<irontide_core::TorrentMetaV2>>,
547 },
548 /// Force an immediate DHT announce (fire-and-forget at torrent level).
549 ForceDhtAnnounce,
550 /// Read all data for a specific piece from disk.
551 ReadPiece {
552 index: u32,
553 reply: oneshot::Sender<crate::Result<bytes::Bytes>>,
554 },
555 /// Flush the disk write cache for this torrent.
556 FlushCache {
557 reply: oneshot::Sender<crate::Result<()>>,
558 },
559 /// Clear the error state and resume if the torrent was paused due to error.
560 ClearError,
561 /// Get per-file open/mode status based on torrent state.
562 FileStatus {
563 reply: oneshot::Sender<Vec<crate::types::FileStatus>>,
564 },
565 /// Read the current torrent flags as a bitflag set.
566 Flags {
567 reply: oneshot::Sender<TorrentFlags>,
568 },
569 /// Set (enable) the specified torrent flags.
570 SetFlags {
571 flags: TorrentFlags,
572 reply: oneshot::Sender<()>,
573 },
574 /// Unset (disable) the specified torrent flags.
575 UnsetFlags {
576 flags: TorrentFlags,
577 reply: oneshot::Sender<()>,
578 },
579 /// Immediately initiate a peer connection to the given address.
580 ConnectPeer {
581 addr: SocketAddr,
582 },
583 /// Clear the `need_save_resume` dirty flag after a successful file save (M161).
584 ClearSaveResumeFlag,
585 /// M245 F1 — re-arm `need_save_resume` after a failed resume WRITE.
586 /// [`TakeResumeIfDirty`](Self::TakeResumeIfDirty) clears the flag on
587 /// capture; without this the captured-but-unwritten state would never be
588 /// retried on a later save cycle.
589 MarkResumeDirty,
590 /// Restore a piece bitmap from resume data (M161 Phase 4).
591 ///
592 /// Replaces the chunk tracker's bitfield with the provided raw piece bytes.
593 /// The handler validates the bitfield length before applying.
594 RestoreResumeBitmap {
595 /// Raw piece bitfield bytes from resume data.
596 pieces: Vec<u8>,
597 /// Reply with `Ok(())` on success or an error if validation fails.
598 reply: oneshot::Sender<crate::Result<()>>,
599 },
600 /// M178: Restore the per-URL web-seed stats map from resume data.
601 ///
602 /// Used by the post-add resume-restore path so that downloaded-byte
603 /// counters and last-error / consecutive-failure state survive app
604 /// restart (Tension-1 fast-resume persistence).
605 RestoreWebSeedStats {
606 /// Map of URL → stats from `FastResumeData::web_seed_stats`.
607 stats: HashMap<String, irontide_core::WebSeedStats>,
608 /// Reply with `Ok(())` on success.
609 reply: oneshot::Sender<crate::Result<()>>,
610 },
611 /// M178 (Lane B3 / TODO-2): cumulative `(pex, lsd)` unique-peer counts
612 /// for the GUI Trackers tab + qBt v2 trackers pseudo-tracker rows.
613 GetPeerSourceCounts {
614 /// Reply with `(pex_peer_count, lsd_peer_count)`.
615 reply: oneshot::Sender<(usize, usize)>,
616 },
617 /// Per-peer cumulative unchoke duration over the torrent's lifetime.
618 /// Keyed by `SocketAddr`; merges live `PeerState` accumulators with
619 /// the durable per-torrent map so reconnects preserve history.
620 /// Used by libtorrent-mirror perf scenarios that gate on
621 /// optimistic-unchoke fairness.
622 QueryUnchokeDurations {
623 /// Reply with one entry per peer ever unchoked by us.
624 reply: oneshot::Sender<HashMap<SocketAddr, std::time::Duration>>,
625 },
626 /// M178 (Lane C): snapshot of per-URL web-seed stats for the qBt v2
627 /// `/api/v2/torrents/webseeds` endpoint and the GUI HTTP Sources tab.
628 GetWebSeedStats {
629 /// Reply with one entry per URL with active stats.
630 reply: oneshot::Sender<Vec<irontide_core::WebSeedStats>>,
631 },
632 /// M147: Pre-resolved metadata from the background `MetadataResolver`.
633 ///
634 /// Sent by `SessionActor::spawn_metadata_resolver()` when the background
635 /// resolver successfully obtains torrent metadata before the `TorrentActor`'s
636 /// own `FetchingMetadata` phase completes. This is a race: first to resolve
637 /// wins; the other path's result is silently discarded.
638 PreResolvedMetadata {
639 /// Raw bencoded info dictionary bytes.
640 info_bytes: Vec<u8>,
641 /// Peers that were successfully connected during metadata resolution
642 /// (for pre-seeding the peer pipeline).
643 peers: Vec<SocketAddr>,
644 },
645 /// v0.173.1: single source of truth for torrent metadata.
646 ///
647 /// Returns `Some(meta.clone())` if the actor has assembled metadata (via
648 /// its own `ut_metadata` fetch or a `PreResolvedMetadata` push), else
649 /// `None`. Replaces `SessionActor.TorrentEntry.meta` as the authoritative
650 /// source — see class-A archaeology in the v0.173.1 plan file at
651 /// `docs/plans/2026-04-22-irontide-v0.173.1-qbt-v2-bug-sweep.md`.
652 GetMeta {
653 /// Reply with `Some(meta)` when available, `None` for a magnet that
654 /// hasn't resolved metadata yet.
655 reply: oneshot::Sender<Option<irontide_core::TorrentMetaV1>>,
656 },
657 /// **TEST-ONLY (v0.173.2).** Synchronously inject a fully-assembled info-dict
658 /// payload via the same internal handler as the M147 `PreResolvedMetadata`
659 /// path, but with backpressure + completion-ack so tests can rely on the
660 /// metadata being processed when the future resolves. The M147 fast-path
661 /// uses `try_send` and is fire-and-forget by design (resolver shouldn't
662 /// block); this variant is the synchronous-test counterpart.
663 #[cfg(feature = "test-util")]
664 TestInjectMetadata {
665 /// Raw bencoded info dictionary bytes.
666 info_bytes: Vec<u8>,
667 /// Completion ack — fired after `handle_pre_resolved_metadata` returns.
668 reply: oneshot::Sender<()>,
669 },
670 /// v0.187.1: broadcast changed session-level settings to a running torrent.
671 ///
672 /// Patches `self.config` fields so that settings changes made via
673 /// Preferences → Apply take effect on existing torrents, not just
674 /// newly-added ones.
675 ///
676 /// Boxed since M255: the delta is ~60 `Option` fields and rarely sent —
677 /// boxing keeps every `TorrentCommand` channel slot small
678 /// (`clippy::large_enum_variant` tripped when the delta grew).
679 UpdateSettings(Box<SettingsDelta>),
680}