Skip to main content

qubit_fs/write/
rejected_async_writer.rs

1// =============================================================================
2//    Copyright (c) 2026 Haixing Hu.
3//
4//    SPDX-License-Identifier: Apache-2.0
5// =============================================================================
6//! Explicit cleanup authority for a rejected write session.
7
8use std::fmt::Debug;
9use std::fmt::Formatter;
10use std::fmt::Result as FmtResult;
11use std::pin::Pin;
12
13use crate::error::FsError;
14use crate::error::FsErrorKind;
15use crate::error::FsOperation;
16use crate::error::FsResult;
17use crate::error::RecoveryCleanupState;
18use crate::facade::internal::RecoveryCleanupGuard;
19use crate::path::Path;
20use crate::spi::AsyncFileWriteSession;
21use crate::spi::SpiFuture;
22use crate::write::WriteAbortOutcome;
23
24/// Isolated cleanup ownership after a provider returned an invalid identity.
25///
26/// This handle cannot write or publish. Cleanup acts only on resources owned by
27/// its session, never on a diagnostic path. Drop performs no explicit cleanup
28/// or cancellation hook; retain this handle until cleanup is confirmed.
29///
30/// ```compile_fail
31/// use qubit_fs::write::RejectedAsyncWriter;
32/// fn cannot_publish(mut recovery: RejectedAsyncWriter) {
33///     recovery.commit();
34/// }
35/// ```
36///
37/// # Examples
38///
39/// ```rust
40/// use qubit_fs::error::RecoveryCleanupState;
41/// use qubit_fs::write::RejectedAsyncWriter;
42///
43/// assert!(std::any::type_name::<RejectedAsyncWriter>().contains("RejectedAsyncWriter"));
44/// assert_eq!(RecoveryCleanupState::Pending, RecoveryCleanupState::Pending);
45/// ```
46#[must_use = "explicitly clean or retain the isolated recovery session"]
47pub struct RejectedAsyncWriter {
48    /// The actual provider session, retained independently of cleanup futures.
49    session: Pin<Box<dyn AsyncFileWriteSession>>,
50    /// Last observed cleanup state, never a publication snapshot.
51    state: RecoveryCleanupState,
52    /// Trusted configured provider identifier.
53    provider: Box<str>,
54    /// Requested path, never the unvalidated provider identity.
55    path: Option<Path>,
56}
57impl RejectedAsyncWriter {
58    /// Takes ownership of a rejected session and trusted request context.
59    pub(crate) fn new(session: Box<dyn AsyncFileWriteSession>, provider: &str, path: Option<Path>) -> Self {
60        Self {
61            session: Box::into_pin(session),
62            state: RecoveryCleanupState::Pending,
63            provider: provider.into(),
64            path,
65        }
66    }
67    /// Returns cleanup progress; errors and cancellation do not release
68    /// ownership.
69    pub const fn cleanup_state(&self) -> RecoveryCleanupState {
70        self.state
71    }
72
73    /// Explicitly cleans resources actually owned by this isolated session.
74    ///
75    /// A failed attempt retains the session for explicit retry or
76    /// reconciliation. No work occurs until polled. Dropping a polled,
77    /// unfinished future records Indeterminate; dropping an unpolled future
78    /// leaves the state unchanged.
79    ///
80    /// # Errors
81    /// Returns InvalidState after confirmed cleanup, or the contextual provider
82    /// error without replacing the original opening failure.
83    pub fn abort_async(&mut self) -> SpiFuture<'_, FsResult<WriteAbortOutcome>> {
84        Box::pin(async move {
85            if self.state == RecoveryCleanupState::Completed {
86                return Err(self.contextual_error(FsError::new(
87                    FsErrorKind::InvalidState,
88                    FsOperation::AbortWriter,
89                    "isolated session cleanup already completed",
90                )));
91            }
92            let mut guard = RecoveryCleanupGuard::start(&mut self.state);
93            let result = self.session.as_mut().abort_async().await;
94            guard.finish(matches!(
95                &result,
96                Ok(WriteAbortOutcome::NotPublished | WriteAbortOutcome::Published)
97            ));
98            drop(guard);
99            result.map_err(|error| self.contextual_error(error))
100        })
101    }
102    /// Adds only trusted request context to a cleanup error.
103    fn contextual_error(&self, error: FsError) -> FsError {
104        error.with_trusted_cleanup_context(FsOperation::AbortWriter, self.path.as_ref(), &self.provider)
105    }
106}
107impl Debug for RejectedAsyncWriter {
108    /// Omits the session and unvalidated provider identity.
109    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
110        f.debug_struct("RejectedAsyncWriter")
111            .field("cleanup_state", &self.state)
112            .finish_non_exhaustive()
113    }
114}