Skip to main content

text_document/
operation.rs

1//! Typed long operation handle.
2
3use std::thread;
4use std::time::Duration;
5
6use anyhow::Result;
7
8use frontend::AppContext;
9
10/// Shared state for a single long operation.
11pub(crate) struct OperationState {
12    ctx: AppContext,
13}
14
15impl OperationState {
16    pub fn new(ctx: &AppContext) -> Self {
17        Self { ctx: ctx.clone() }
18    }
19}
20
21/// A handle to a running long operation (Markdown/HTML import, DOCX export).
22///
23/// Provides typed access to progress, cancellation, and the result.
24/// Progress events are also emitted via [`DocumentEvent::LongOperationProgress`](crate::DocumentEvent::LongOperationProgress)
25/// and [`DocumentEvent::LongOperationFinished`](crate::DocumentEvent::LongOperationFinished)
26/// for the callback/polling path.
27///
28/// Retrieve the result via [`wait()`](Self::wait) (blocking, consumes the handle)
29/// or [`try_result()`](Self::try_result) (non-blocking, can be called repeatedly).
30pub struct Operation<T> {
31    id: String,
32    state: OperationState,
33    result_fn: Box<dyn Fn(&AppContext, &str) -> Option<Result<T>> + Send>,
34}
35
36impl<T> Operation<T> {
37    pub(crate) fn new(
38        id: String,
39        ctx: &AppContext,
40        result_fn: Box<dyn Fn(&AppContext, &str) -> Option<Result<T>> + Send>,
41    ) -> Self {
42        Self {
43            id,
44            state: OperationState::new(ctx),
45            result_fn,
46        }
47    }
48
49    /// The operation ID (for matching with [`DocumentEvent`](crate::DocumentEvent) variants).
50    pub fn id(&self) -> &str {
51        &self.id
52    }
53
54    /// Get the current progress, if available.
55    /// Returns `(percent, message)` where percent is 0.0–100.0.
56    pub fn progress(&self) -> Option<(f64, String)> {
57        let mgr = self
58            .state
59            .ctx
60            .long_operation_manager
61            .lock()
62            .ok()
63            .or_else(|| {
64                // Mutex poisoned — recover by re-locking through the poison.
65                match self.state.ctx.long_operation_manager.lock() {
66                    Ok(g) => Some(g),
67                    Err(e) => Some(e.into_inner()),
68                }
69            })?;
70        mgr.get_operation_progress(&self.id)
71            .map(|p| (p.percentage as f64, p.message.unwrap_or_default()))
72    }
73
74    /// Returns `true` if the operation has finished (success or failure).
75    pub fn is_done(&self) -> bool {
76        (self.result_fn)(&self.state.ctx, &self.id).is_some()
77    }
78
79    /// Cancel the operation. No-op if already finished.
80    pub fn cancel(&self) {
81        if let Ok(mgr) = self.state.ctx.long_operation_manager.lock() {
82            mgr.cancel_operation(&self.id);
83        }
84    }
85
86    /// Block the calling thread until the operation completes and return
87    /// the typed result. Consumes the handle.
88    pub fn wait(self) -> Result<T> {
89        loop {
90            if let Some(result) = (self.result_fn)(&self.state.ctx, &self.id) {
91                return result;
92            }
93            thread::sleep(Duration::from_millis(50));
94        }
95    }
96
97    /// Block until the operation completes or the timeout expires.
98    /// Returns `None` if the timeout elapsed before the operation finished.
99    pub fn wait_timeout(self, timeout: Duration) -> Option<Result<T>> {
100        let deadline = std::time::Instant::now() + timeout;
101        loop {
102            if let Some(result) = (self.result_fn)(&self.state.ctx, &self.id) {
103                return Some(result);
104            }
105            if std::time::Instant::now() >= deadline {
106                return None;
107            }
108            let remaining = deadline.saturating_duration_since(std::time::Instant::now());
109            thread::sleep(remaining.min(Duration::from_millis(50)));
110        }
111    }
112
113    /// Non-blocking: returns the result if the operation has completed,
114    /// `None` if still running. Can be called repeatedly.
115    pub fn try_result(&mut self) -> Option<Result<T>> {
116        (self.result_fn)(&self.state.ctx, &self.id)
117    }
118}
119
120// ── Result types ────────────────────────────────────────────────
121
122/// Result of a Markdown import (`set_markdown`).
123#[derive(Debug, Clone)]
124pub struct MarkdownImportResult {
125    pub block_count: usize,
126}
127
128/// Result of an HTML import (`set_html`).
129#[derive(Debug, Clone)]
130pub struct HtmlImportResult {
131    pub block_count: usize,
132}
133
134/// Result of a DOCX export (`to_docx`).
135#[derive(Debug, Clone)]
136pub struct DocxExportResult {
137    pub file_path: String,
138    pub paragraph_count: usize,
139}