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