Skip to main content

text_document/
operation.rs

1//! Typed long operation handle.
2
3use std::sync::Arc;
4use std::time::{Duration, Instant};
5
6use crate::Result;
7
8use common::long_operation::{OperationCompletion, OperationStatus};
9use frontend::AppContext;
10
11/// Backstop for a completion signal that never arrives.
12///
13/// This is **not** a polling interval: a wait is woken by the manager's condvar
14/// the instant its operation publishes completion, so in normal operation this
15/// timeout is never reached. It exists only so that a signal lost to an
16/// abnormally-terminated worker — one killed between storing its result and
17/// publishing — degrades into a slow re-check instead of a permanent hang.
18const COMPLETION_BACKSTOP: Duration = Duration::from_secs(1);
19
20/// Function that reads the long-operation manager for a result.
21type ResultFn<T> = Box<dyn Fn(&AppContext, &str) -> Option<Result<T>> + Send>;
22
23/// Shared state for a single long operation.
24pub(crate) struct OperationState {
25    ctx: AppContext,
26}
27
28impl OperationState {
29    pub fn new(ctx: &AppContext) -> Self {
30        Self { ctx: ctx.clone() }
31    }
32}
33
34/// A handle to a running long operation (Markdown/HTML import, DOCX export).
35///
36/// Provides typed access to progress, cancellation, and the result.
37/// Progress events are also emitted via [`DocumentEvent::LongOperationProgress`](crate::DocumentEvent::LongOperationProgress)
38/// and [`DocumentEvent::LongOperationFinished`](crate::DocumentEvent::LongOperationFinished)
39/// for the callback/polling path.
40///
41/// Retrieve the result via [`wait()`](Self::wait) (blocking, consumes the handle)
42/// or [`try_result()`](Self::try_result) (non-blocking, can be called repeatedly).
43pub struct Operation<T> {
44    id: String,
45    state: OperationState,
46    result_fn: ResultFn<T>,
47}
48
49impl<T> Operation<T> {
50    pub(crate) fn new(id: String, ctx: &AppContext, result_fn: ResultFn<T>) -> Self {
51        Self {
52            id,
53            state: OperationState::new(ctx),
54            result_fn,
55        }
56    }
57
58    /// The operation ID (for matching with [`DocumentEvent`](crate::DocumentEvent) variants).
59    pub fn id(&self) -> &str {
60        &self.id
61    }
62
63    /// Get the current progress, if available.
64    /// Returns `(percent, message)` where percent is 0.0–100.0.
65    pub fn progress(&self) -> Option<(f64, String)> {
66        let mgr = self.state.ctx.long_operation_manager.lock();
67        mgr.get_operation_progress(&self.id)
68            .map(|p| (p.percentage as f64, p.message.unwrap_or_default()))
69    }
70
71    /// Returns `true` if the operation has finished (success or failure).
72    ///
73    /// Reads the completion signal, not just the result slot: a cancelled or
74    /// failed operation stores no result but is very much finished.
75    pub fn is_done(&self) -> bool {
76        self.completion().is_finished(&self.id)
77    }
78
79    /// This operation's completion signal, with the manager lock **released**.
80    ///
81    /// Never block while holding `long_operation_manager`: a wait lasts as long
82    /// as the operation, and every other operation query would queue behind it.
83    fn completion(&self) -> Arc<OperationCompletion> {
84        self.state
85            .ctx
86            .long_operation_manager
87            .lock()
88            .completion_signal()
89    }
90
91    /// The error for an operation that finished without producing a result —
92    /// which means it was cancelled, or it failed.
93    fn terminal_error(&self) -> crate::DocumentError {
94        let status = self
95            .state
96            .ctx
97            .long_operation_manager
98            .lock()
99            .get_operation_status(&self.id);
100        match status {
101            Some(OperationStatus::Failed(err)) => anyhow::anyhow!("{err}").into(),
102            Some(OperationStatus::Cancelled) => anyhow::anyhow!("operation was cancelled").into(),
103            // Finished, no result, no live handle: it was cleaned up out from
104            // under us, so the outcome is no longer knowable.
105            _ => anyhow::anyhow!("operation finished without producing a result").into(),
106        }
107    }
108
109    /// Cancel the operation. No-op if already finished.
110    pub fn cancel(&self) {
111        self.state
112            .ctx
113            .long_operation_manager
114            .lock()
115            .cancel_operation(&self.id);
116    }
117
118    /// Block the calling thread until the operation completes and return
119    /// the typed result. Consumes the handle.
120    ///
121    /// The wait is **signal-driven**: the manager wakes it the moment the
122    /// operation publishes completion, so a short operation costs only its own
123    /// runtime. It previously re-checked on a 50 ms timer, which put a ~50 ms
124    /// floor under *every* call however trivial the work — invisible for one
125    /// import, but seconds of dead time for a caller loading documents in a loop.
126    ///
127    /// A cancelled or failed operation now returns `Err` rather than blocking
128    /// forever: neither ever stores a result, so the old "loop until a result
129    /// appears" never terminated for them.
130    pub fn wait(self) -> Result<T> {
131        let completion = self.completion();
132        loop {
133            // Read completion *before* polling. The worker stores its result
134            // before publishing, so if the operation had already finished at this
135            // point, the read below is authoritative — a miss then means no
136            // result was ever produced, not that one has yet to land.
137            let finished = completion.is_finished(&self.id);
138            if let Some(result) = (self.result_fn)(&self.state.ctx, &self.id) {
139                return result;
140            }
141            if finished {
142                return Err(self.terminal_error());
143            }
144            completion.wait_for(&self.id, Some(COMPLETION_BACKSTOP));
145        }
146    }
147
148    /// Block until the operation completes or the timeout expires.
149    /// Returns `None` if the timeout elapsed before the operation finished.
150    ///
151    /// Like [`wait`](Self::wait), this blocks on the completion signal rather
152    /// than a timer, so it returns as soon as the operation ends instead of on
153    /// the next 50 ms boundary.
154    pub fn wait_timeout(self, timeout: Duration) -> Option<Result<T>> {
155        let completion = self.completion();
156        let deadline = Instant::now() + timeout;
157        loop {
158            // Same ordering rule as `wait`: completion first, then the result.
159            let finished = completion.is_finished(&self.id);
160            if let Some(result) = (self.result_fn)(&self.state.ctx, &self.id) {
161                return Some(result);
162            }
163            if finished {
164                return Some(Err(self.terminal_error()));
165            }
166            let remaining = deadline.saturating_duration_since(Instant::now());
167            if remaining.is_zero() {
168                return None;
169            }
170            // Wait out the real remaining time in one blocking call — capped by
171            // the backstop so a lost signal still re-checks — rather than in
172            // 50 ms slices.
173            completion.wait_for(&self.id, Some(remaining.min(COMPLETION_BACKSTOP)));
174        }
175    }
176
177    /// Non-blocking: returns the result if the operation has completed,
178    /// `None` if still running. Can be called repeatedly.
179    pub fn try_result(&mut self) -> Option<Result<T>> {
180        (self.result_fn)(&self.state.ctx, &self.id)
181    }
182}
183
184// ── Result types ────────────────────────────────────────────────
185
186/// Result of a Markdown import (`set_markdown`).
187#[derive(Debug, Clone)]
188pub struct MarkdownImportResult {
189    pub block_count: usize,
190}
191
192/// Result of an HTML import (`set_html`).
193#[derive(Debug, Clone)]
194pub struct HtmlImportResult {
195    pub block_count: usize,
196}
197
198/// Result of a djot import (`set_djot`).
199#[derive(Debug, Clone)]
200pub struct DjotImportResult {
201    pub block_count: usize,
202}
203
204/// Result of a DOCX export (`to_docx`).
205#[derive(Debug, Clone)]
206pub struct DocxExportResult {
207    pub file_path: String,
208    pub paragraph_count: usize,
209}
210
211/// Result of an EPUB export (`to_epub`).
212#[derive(Debug, Clone)]
213pub struct EpubExportResult {
214    pub file_path: String,
215    pub chapter_count: usize,
216}
217
218/// Result of a PDF export (`to_pdf`).
219#[derive(Debug, Clone)]
220pub struct PdfExportResult {
221    pub file_path: String,
222    pub page_count: usize,
223}