rusty_cat/meow_client.rs
1use std::sync::atomic::{AtomicBool, Ordering};
2use std::sync::{Arc, OnceLock, RwLock};
3
4use crate::dflt::default_http_transfer::{default_breakpoint_arcs, DefaultHttpTransfer};
5use crate::error::{InnerErrorCode, MeowError};
6use crate::file_transfer_record::FileTransferRecord;
7use crate::ids::{GlobalProgressListenerId, TaskId};
8use crate::inner::executor::Executor;
9use crate::inner::inner_task::InnerTask;
10use crate::inner::task_callbacks::{CompleteCb, ProgressCb, TaskCallbacks};
11use crate::log::{set_debug_log_listener, DebugLogListener, DebugLogListenerError};
12use crate::meow_config::MeowConfig;
13use crate::pounce_task::PounceTask;
14use crate::transfer_snapshot::TransferSnapshot;
15
16/// Callback type for globally observing task progress events.
17///
18/// The callback is invoked from runtime worker context. Keep callback logic
19/// fast and non-blocking to avoid delaying event processing.
20pub type GlobalProgressListener = ProgressCb;
21
22/// Main entry point of the `rusty-cat` SDK.
23///
24/// `MeowClient` owns runtime state and provides high-level operations:
25/// enqueue, pause, resume, cancel, snapshot, and close.
26///
27/// # Usage pattern
28///
29/// 1. Create [`MeowConfig`].
30/// 2. Construct `MeowClient::new(config)`.
31/// 3. Build tasks with upload/download builders.
32/// 4. Call [`Self::enqueue`] and store returned [`TaskId`].
33/// 5. Control task lifecycle with pause/resume/cancel.
34/// 6. Call [`Self::close`] during shutdown.
35///
36/// # Lifecycle contract: you **must** call [`Self::close`]
37///
38/// The background scheduler runs on a dedicated [`std::thread`] that drives
39/// its own Tokio runtime. That thread is **detached**: there is no handle
40/// stored anywhere to `join()` it, and the only clean shutdown protocol is
41/// an explicit `close().await` command which:
42///
43/// - cancels in-flight transfers,
44/// - flushes `Paused` status events to user callbacks for every known group,
45/// - breaks the worker loop and lets the runtime drop.
46///
47/// Forgetting to call `close` leaves the scheduler thread alive until all
48/// command senders are dropped (which does happen when `MeowClient` is
49/// dropped, but only as a fallback). When that fallback path runs, the
50/// guarantees above do **not** hold: callers may miss terminal status
51/// events, in-flight HTTP transfers are aborted abruptly, and for long-lived
52/// SDK hosts (servers, mobile runtimes, etc.) the misuse is nearly
53/// impossible to debug from the outside.
54///
55/// To help surface this misuse the internal executor implements a
56/// **best-effort [`Drop`]** that, when `close` was never called:
57///
58/// - emits a `Warn`-level log via the debug log listener (tag
59/// `"executor_drop"`),
60/// - performs a non-blocking `try_send` of a final `Close` command so the
61/// worker still has a chance to drain its state,
62/// - then drops the command sender, causing the worker loop to exit on its
63/// own.
64///
65/// This is a safety net, **not** a substitute for calling `close`. Treat
66/// `close().await` as a mandatory step in your shutdown sequence.
67///
68/// # Sharing across tasks / threads
69///
70/// `MeowClient` **intentionally does not implement [`Clone`]**.
71///
72/// The client owns a lazily-initialized [`Executor`] (a single background
73/// worker loop plus its task table, scheduler state and shutdown flag). A
74/// naive field-by-field `Clone` would copy the `OnceLock<Executor>` *before*
75/// it was initialized, letting different clones each spin up their **own**
76/// executor on first use. The result would be:
77///
78/// - multiple independent task tables (tasks enqueued via one clone are
79/// invisible to `pause` / `resume` / `cancel` / `snapshot` on another);
80/// - concurrency limits ([`MeowConfig::max_upload_concurrency`] /
81/// [`MeowConfig::max_download_concurrency`]) silently multiplied by the
82/// number of clones;
83/// - [`Self::close`] only shutting down one of the worker loops, leaking the
84/// rest.
85///
86/// To share a client across tasks or threads, wrap it in [`std::sync::Arc`]
87/// and clone the `Arc` instead:
88///
89/// ```no_run
90/// use std::sync::Arc;
91/// use rusty_cat::api::{MeowClient, MeowConfig};
92///
93/// let client = Arc::new(MeowClient::new(MeowConfig::default()));
94/// let client_for_task = Arc::clone(&client);
95/// tokio::spawn(async move {
96/// let _ = client_for_task; // use the shared client here
97/// });
98/// ```
99pub struct MeowClient {
100 /// Lazily initialized task executor.
101 ///
102 /// Deliberately **not** wrapped in `Arc`: `MeowClient` is not `Clone`, so
103 /// there is exactly one owner of this `OnceLock`. Share the whole client
104 /// via `Arc<MeowClient>` when multi-owner access is needed.
105 executor: OnceLock<Executor>,
106 /// Immutable runtime configuration.
107 config: MeowConfig,
108 /// Global listeners receiving progress records for all tasks.
109 global_progress_listener: Arc<RwLock<Vec<(GlobalProgressListenerId, GlobalProgressListener)>>>,
110 /// Global closed flag. Once set to `true`, task control APIs reject calls.
111 closed: Arc<AtomicBool>,
112}
113
114impl std::fmt::Debug for MeowClient {
115 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116 f.debug_struct("MeowClient")
117 .field("config", &self.config)
118 .field("global_progress_listener", &"..")
119 .finish()
120 }
121}
122
123impl MeowClient {
124 /// Creates a new client with the provided configuration.
125 ///
126 /// The internal executor is initialized lazily on first task operation.
127 ///
128 /// # Examples
129 ///
130 /// ```no_run
131 /// use rusty_cat::api::{MeowClient, MeowConfig};
132 ///
133 /// let config = MeowConfig::default();
134 /// let client = MeowClient::new(config);
135 /// let _ = client;
136 /// ```
137 pub fn new(config: MeowConfig) -> Self {
138 MeowClient {
139 executor: Default::default(),
140 config,
141 global_progress_listener: Arc::new(RwLock::new(Vec::new())),
142 closed: Arc::new(AtomicBool::new(false)),
143 }
144 }
145
146 /// Returns a `reqwest::Client` aligned with this client's configuration.
147 ///
148 /// - If [`MeowConfig::with_http_client`] injected a custom client, this
149 /// returns its clone.
150 /// - Otherwise, this builds a new client from `http_timeout` and
151 /// `tcp_keepalive`.
152 ///
153 /// # Errors
154 ///
155 /// Returns [`MeowError`] with `HttpClientBuildFailed` when client creation
156 /// fails.
157 ///
158 /// # Examples
159 ///
160 /// ```no_run
161 /// use rusty_cat::api::{MeowClient, MeowConfig};
162 ///
163 /// let client = MeowClient::new(MeowConfig::default());
164 /// let http = client.http_client()?;
165 /// let _ = http;
166 /// # Ok::<(), rusty_cat::api::MeowError>(())
167 /// ```
168 pub fn http_client(&self) -> Result<reqwest::Client, MeowError> {
169 if let Some(c) = self.config.http_client_ref() {
170 return Ok(c.clone());
171 }
172 reqwest::Client::builder()
173 .timeout(self.config.http_timeout())
174 .tcp_keepalive(self.config.tcp_keepalive())
175 .build()
176 .map_err(|e| {
177 MeowError::from_source(
178 InnerErrorCode::HttpClientBuildFailed,
179 format!(
180 "build reqwest client failed (timeout={:?}, keepalive={:?})",
181 self.config.http_timeout(),
182 self.config.tcp_keepalive()
183 ),
184 e,
185 )
186 })
187 }
188
189 fn get_exec(&self) -> Result<&Executor, MeowError> {
190 if let Some(exec) = self.executor.get() {
191 crate::meow_flow_log!("executor", "reuse existing executor");
192 return Ok(exec);
193 }
194 let default_http_transfer = DefaultHttpTransfer::try_with_http_timeouts(
195 self.config.http_timeout(),
196 self.config.tcp_keepalive(),
197 )?;
198 crate::meow_flow_log!(
199 "executor",
200 "initializing DefaultHttpTransfer (timeout={:?}, tcp_keepalive={:?})",
201 self.config.http_timeout(),
202 self.config.tcp_keepalive()
203 );
204 let exec = Executor::new(
205 self.config.clone(),
206 Arc::new(default_http_transfer),
207 self.global_progress_listener.clone(),
208 )?;
209 let _ = self.executor.set(exec);
210 self.executor.get().ok_or_else(|| {
211 crate::meow_flow_log!(
212 "executor",
213 "executor init race failed after set; returning RuntimeCreationFailedError"
214 );
215 MeowError::from_code_str(
216 InnerErrorCode::RuntimeCreationFailedError,
217 "executor init race failed",
218 )
219 })
220 }
221
222 /// Ensures the client is still open.
223 ///
224 /// Returns `ClientClosed` if [`Self::close`] was called successfully.
225 fn ensure_open(&self) -> Result<(), MeowError> {
226 if self.closed.load(Ordering::SeqCst) {
227 crate::meow_flow_log!("client", "ensure_open failed: client already closed");
228 Err(MeowError::from_code_str(
229 InnerErrorCode::ClientClosed,
230 "meow client is already closed",
231 ))
232 } else {
233 Ok(())
234 }
235 }
236
237 /// Registers a global progress listener for all tasks.
238 ///
239 /// # Parameters
240 ///
241 /// - `listener`: Callback receiving [`FileTransferRecord`] updates.
242 ///
243 /// # Returns
244 ///
245 /// Returns a listener ID used by
246 /// [`Self::unregister_global_progress_listener`].
247 ///
248 /// # Usage rules
249 ///
250 /// Keep callback execution short and panic-free. A heavy callback can slow
251 /// down global event delivery.
252 ///
253 /// # Errors
254 ///
255 /// Returns `LockPoisoned` when listener storage lock is poisoned.
256 ///
257 /// # Examples
258 ///
259 /// ```no_run
260 /// use rusty_cat::api::{MeowClient, MeowConfig};
261 ///
262 /// let client = MeowClient::new(MeowConfig::default());
263 /// let listener_id = client.register_global_progress_listener(|record| {
264 /// println!("task={} progress={:.2}", record.task_id(), record.progress());
265 /// })?;
266 /// let _ = listener_id;
267 /// # Ok::<(), rusty_cat::api::MeowError>(())
268 /// ```
269 pub fn register_global_progress_listener<F>(
270 &self,
271 listener: F,
272 ) -> Result<GlobalProgressListenerId, MeowError>
273 where
274 F: Fn(FileTransferRecord) + Send + Sync + 'static,
275 {
276 let id = GlobalProgressListenerId::new_v4();
277 crate::meow_flow_log!("listener", "register global listener: id={:?}", id);
278 let mut guard = self.global_progress_listener.write().map_err(|e| {
279 MeowError::from_code(
280 InnerErrorCode::LockPoisoned,
281 format!("register global listener lock poisoned: {}", e),
282 )
283 })?;
284 guard.push((id, Arc::new(listener)));
285 Ok(id)
286 }
287
288 /// Unregisters one previously registered global progress listener.
289 ///
290 /// Returns `Ok(false)` when the ID does not exist.
291 ///
292 /// # Errors
293 ///
294 /// Returns `LockPoisoned` when listener storage lock is poisoned.
295 ///
296 /// # Examples
297 ///
298 /// ```no_run
299 /// use rusty_cat::api::{MeowClient, MeowConfig};
300 ///
301 /// let client = MeowClient::new(MeowConfig::default());
302 /// let id = client.register_global_progress_listener(|_| {})?;
303 /// let removed = client.unregister_global_progress_listener(id)?;
304 /// assert!(removed);
305 /// # Ok::<(), rusty_cat::api::MeowError>(())
306 /// ```
307 pub fn unregister_global_progress_listener(
308 &self,
309 id: GlobalProgressListenerId,
310 ) -> Result<bool, MeowError> {
311 let mut g = self.global_progress_listener.write().map_err(|e| {
312 MeowError::from_code(
313 InnerErrorCode::LockPoisoned,
314 format!("unregister global listener lock poisoned: {}", e),
315 )
316 })?;
317 if let Some(pos) = g.iter().position(|(k, _)| *k == id) {
318 g.remove(pos);
319 crate::meow_flow_log!(
320 "listener",
321 "unregister global listener success: id={:?}",
322 id
323 );
324 Ok(true)
325 } else {
326 crate::meow_flow_log!("listener", "unregister global listener missed: id={:?}", id);
327 Ok(false)
328 }
329 }
330
331 /// Removes all registered global progress listeners.
332 ///
333 /// # Errors
334 ///
335 /// Returns `LockPoisoned` when listener storage lock is poisoned.
336 ///
337 /// # Examples
338 ///
339 /// ```no_run
340 /// use rusty_cat::api::{MeowClient, MeowConfig};
341 ///
342 /// let client = MeowClient::new(MeowConfig::default());
343 /// client.clear_global_listener()?;
344 /// # Ok::<(), rusty_cat::api::MeowError>(())
345 /// ```
346 pub fn clear_global_listener(&self) -> Result<(), MeowError> {
347 crate::meow_flow_log!("listener", "clear all global listeners");
348 self.global_progress_listener
349 .write()
350 .map_err(|e| {
351 MeowError::from_code(
352 InnerErrorCode::LockPoisoned,
353 format!("clear global listeners lock poisoned: {}", e),
354 )
355 })?
356 .clear();
357 Ok(())
358 }
359
360 /// Sets or clears the global debug log listener.
361 ///
362 /// - Pass `Some(listener)` to set/replace.
363 /// - Pass `None` to clear.
364 ///
365 /// This affects all `MeowClient` instances in the current process.
366 ///
367 /// # Errors
368 ///
369 /// Returns [`DebugLogListenerError`] when the internal global listener lock
370 /// is poisoned.
371 ///
372 /// # Examples
373 ///
374 /// ```no_run
375 /// use std::sync::Arc;
376 /// use rusty_cat::api::{Log, MeowClient, MeowConfig};
377 ///
378 /// let client = MeowClient::new(MeowConfig::default());
379 /// client.set_debug_log_listener(Some(Arc::new(|log: Log| {
380 /// println!("{log}");
381 /// })))?;
382 ///
383 /// // Clear listener when no longer needed.
384 /// client.set_debug_log_listener(None)?;
385 /// # Ok::<(), rusty_cat::api::DebugLogListenerError>(())
386 /// ```
387 pub fn set_debug_log_listener(
388 &self,
389 listener: Option<DebugLogListener>,
390 ) -> Result<(), DebugLogListenerError> {
391 set_debug_log_listener(listener)
392 }
393}
394
395impl MeowClient {
396 /// Submits a transfer task to the internal scheduler and returns its
397 /// [`TaskId`].
398 ///
399 /// The actual upload/download execution is dispatched to an internal
400 /// worker system thread. This method only performs lightweight validation
401 /// and submission, so it does not block the caller thread waiting for full
402 /// transfer completion.
403 ///
404 /// `try_enqueue` is also the recovery entrypoint after process restart.
405 /// If the application was killed during a previous upload/download,
406 /// restart your process and call `try_enqueue` again to resume that
407 /// transfer workflow.
408 ///
409 /// # Back-pressure semantics (why the `try_` prefix)
410 ///
411 /// Internally this method uses
412 /// [`tokio::sync::mpsc::Sender::try_send`] to hand the `Enqueue` command
413 /// to the scheduler worker, **not** `send().await`. That means:
414 ///
415 /// - The `await` point in this function is used for task normalization
416 /// (e.g. resolving upload breakpoints, building [`InnerTask`]), **not**
417 /// for waiting on command-queue capacity.
418 /// - If the command queue is momentarily full (bursty enqueue under
419 /// [`MeowConfig::command_queue_capacity`]), this method returns an
420 /// immediate `CommandSendFailed` error instead of suspending the
421 /// caller until a slot frees up.
422 /// - Other control APIs ([`Self::pause`], [`Self::resume`],
423 /// [`Self::cancel`], [`Self::snapshot`]) use `send().await` and **do**
424 /// wait for queue capacity. Only enqueue is fail-fast.
425 ///
426 /// Callers that want to batch-enqueue under burst load should either:
427 ///
428 /// 1. size [`MeowConfig::command_queue_capacity`] appropriately, or
429 /// 2. retry on `CommandSendFailed` with their own back-off, or
430 /// 3. rate-limit enqueue calls on the caller side.
431 ///
432 /// The name explicitly carries `try_` so this fail-fast behavior is
433 /// visible at the call site. If a fully-awaiting variant is introduced
434 /// later it should be named `enqueue` (without the `try_` prefix).
435 ///
436 /// # Parameters
437 ///
438 /// - `task`: Built by upload/download task builders.
439 /// - `progress_cb`: Per-task callback invoked with transfer progress.
440 /// - `complete_cb`: Callback fired once when task reaches
441 /// [`crate::transfer_status::TransferStatus::Complete`]. The second
442 /// argument is provider-defined payload returned by upload protocol
443 /// `complete_upload`; download tasks usually receive `None`.
444 ///
445 /// # Usage rules
446 ///
447 /// - `task` must be non-empty (required path/name/url and valid upload size).
448 /// - Callback should be lightweight and non-blocking.
449 /// - Store returned task ID for subsequent task control operations.
450 /// - `try_enqueue` is asynchronous task submission, not synchronous transfer.
451 /// - For restart recovery, re-enqueue the same logical task (same
452 /// upload/download target and compatible checkpoint context) so the
453 /// runtime can continue from existing local/remote progress.
454 ///
455 /// # Errors
456 ///
457 /// Returns:
458 /// - `ClientClosed` if the client was closed.
459 /// - `ParameterEmpty` if the task is invalid/empty.
460 /// - `CommandSendFailed` if the scheduler command queue is full at the
461 /// moment of submission (see back-pressure semantics above).
462 /// - Any runtime initialization errors from the executor.
463 ///
464 /// # Examples
465 ///
466 /// ```no_run
467 /// use rusty_cat::api::{DownloadPounceBuilder, MeowClient, MeowConfig};
468 ///
469 /// # async fn run() -> Result<(), rusty_cat::api::MeowError> {
470 /// let client = MeowClient::new(MeowConfig::default());
471 /// let task = DownloadPounceBuilder::new(
472 /// "example.bin",
473 /// "./downloads/example.bin",
474 /// 1024 * 1024,
475 /// "https://example.com/example.bin",
476 /// )
477 /// .build();
478 ///
479 /// let task_id = client
480 /// .try_enqueue(
481 /// task,
482 /// |record| {
483 /// println!("status={:?} progress={:.2}", record.status(), record.progress());
484 /// },
485 /// |task_id, payload| {
486 /// println!("task {task_id} completed, payload={payload:?}");
487 /// },
488 /// )
489 /// .await?;
490 /// println!("enqueued task: {task_id}");
491 /// # Ok(())
492 /// # }
493 /// ```
494 pub async fn try_enqueue<PCB, CCB>(
495 &self,
496 task: PounceTask,
497 progress_cb: PCB,
498 complete_cb: CCB,
499 ) -> Result<TaskId, MeowError>
500 where
501 PCB: Fn(FileTransferRecord) + Send + Sync + 'static,
502 CCB: Fn(TaskId, Option<String>) + Send + Sync + 'static,
503 {
504 self.ensure_open()?;
505 if task.is_empty() {
506 crate::meow_flow_log!("try_enqueue", "reject empty task");
507 return Err(MeowError::from_code1(InnerErrorCode::ParameterEmpty));
508 }
509
510 crate::meow_flow_log!("try_enqueue", "task={:?}", task);
511
512 let progress: ProgressCb = Arc::new(progress_cb);
513 let complete: Option<CompleteCb> = Some(Arc::new(complete_cb) as CompleteCb);
514 let callbacks = TaskCallbacks::new(Some(progress), complete);
515
516 let (def_up, def_down) = default_breakpoint_arcs();
517 let inner = InnerTask::from_pounce(
518 task,
519 self.config.breakpoint_download_http().clone(),
520 self.config.http_client_ref().cloned(),
521 def_up,
522 def_down,
523 )
524 .await?;
525
526 let task_id = self.get_exec()?.try_enqueue(inner, callbacks)?;
527 crate::meow_flow_log!("try_enqueue", "try_enqueue success: task_id={:?}", task_id);
528 Ok(task_id)
529 }
530
531 // pub async fn get_task_status(&self, task_id: TaskId)-> Result<FileTransferRecord, MeowError> {
532 // todo!(arman) -
533 // }
534
535 /// Pauses a running or pending task by ID.
536 ///
537 /// This API sends a control command to the internal scheduler worker
538 /// thread. It does not execute transfer pause logic on the caller thread.
539 ///
540 /// # Usage rules
541 ///
542 /// Call this with a valid task ID returned by [`Self::enqueue`].
543 ///
544 /// # Errors
545 ///
546 /// Returns `ClientClosed`, `TaskNotFound`, or state-transition errors.
547 ///
548 /// # Examples
549 ///
550 /// ```no_run
551 /// use rusty_cat::api::{MeowClient, MeowConfig, TaskId};
552 ///
553 /// # async fn run(task_id: TaskId) -> Result<(), rusty_cat::api::MeowError> {
554 /// let client = MeowClient::new(MeowConfig::default());
555 /// client.pause(task_id).await?;
556 /// # Ok(())
557 /// # }
558 /// ```
559 pub async fn pause(&self, task_id: TaskId) -> Result<(), MeowError> {
560 self.ensure_open()?;
561 crate::meow_flow_log!("client_api", "pause called: task_id={:?}", task_id);
562 self.get_exec()?.pause(task_id).await
563 }
564
565 /// Resumes a previously paused task.
566 ///
567 /// The same [`TaskId`] continues to identify the task after resume.
568 /// The resume command is forwarded to the internal scheduler worker
569 /// thread, so caller thread is not responsible for running transfer logic.
570 ///
571 /// # Errors
572 ///
573 /// Returns `ClientClosed`, `TaskNotFound`, or `InvalidTaskState`.
574 ///
575 /// # Examples
576 ///
577 /// ```no_run
578 /// use rusty_cat::api::{MeowClient, MeowConfig, TaskId};
579 ///
580 /// # async fn run(task_id: TaskId) -> Result<(), rusty_cat::api::MeowError> {
581 /// let client = MeowClient::new(MeowConfig::default());
582 /// client.resume(task_id).await?;
583 /// # Ok(())
584 /// # }
585 /// ```
586 pub async fn resume(&self, task_id: TaskId) -> Result<(), MeowError> {
587 self.ensure_open()?;
588 crate::meow_flow_log!("client_api", "resume called: task_id={:?}", task_id);
589 self.get_exec()?.resume(task_id).await
590 }
591
592 /// Cancels a task by ID.
593 ///
594 /// Cancellation is requested through the internal scheduler worker thread.
595 /// Transfer cancellation execution happens in background runtime workers.
596 ///
597 /// # Usage rules
598 ///
599 /// Cancellation is best-effort; protocol-specific cleanup may run.
600 ///
601 /// # Errors
602 ///
603 /// Returns `ClientClosed`, `TaskNotFound`, or runtime cancellation errors.
604 ///
605 /// # Examples
606 ///
607 /// ```no_run
608 /// use rusty_cat::api::{MeowClient, MeowConfig, TaskId};
609 ///
610 /// # async fn run(task_id: TaskId) -> Result<(), rusty_cat::api::MeowError> {
611 /// let client = MeowClient::new(MeowConfig::default());
612 /// client.cancel(task_id).await?;
613 /// # Ok(())
614 /// # }
615 /// ```
616 pub async fn cancel(&self, task_id: TaskId) -> Result<(), MeowError> {
617 self.ensure_open()?;
618 crate::meow_flow_log!("client_api", "cancel called: task_id={:?}", task_id);
619 self.get_exec()?.cancel(task_id).await
620 }
621
622 /// Returns a snapshot of queue and active transfer groups.
623 ///
624 /// Useful for diagnostics and external monitoring dashboards.
625 /// Snapshot collection is coordinated by internal scheduler worker state.
626 ///
627 /// # Errors
628 ///
629 /// Returns `ClientClosed`, runtime command delivery errors, or scheduler
630 /// snapshot retrieval errors.
631 ///
632 /// # Examples
633 ///
634 /// ```no_run
635 /// use rusty_cat::api::{MeowClient, MeowConfig};
636 ///
637 /// # async fn run() -> Result<(), rusty_cat::api::MeowError> {
638 /// let client = MeowClient::new(MeowConfig::default());
639 /// let snap = client.snapshot().await?;
640 /// println!("queued={}, active={}", snap.queued_groups, snap.active_groups);
641 /// # Ok(())
642 /// # }
643 /// ```
644 pub async fn snapshot(&self) -> Result<TransferSnapshot, MeowError> {
645 self.ensure_open()?;
646 crate::meow_flow_log!("client_api", "snapshot called");
647 self.get_exec()?.snapshot().await
648 }
649
650 /// Closes this client and its underlying executor.
651 ///
652 /// After a successful close:
653 /// - New task operations are rejected.
654 /// - Existing runtime resources are released.
655 ///
656 /// # Idempotency
657 ///
658 /// Calling `close` more than once returns `ClientClosed`.
659 ///
660 /// # Retry behavior
661 ///
662 /// If executor close fails, the closed flag is rolled back so caller can
663 /// retry close.
664 ///
665 /// # Errors
666 ///
667 /// Returns `ClientClosed` when already closed, or underlying executor close
668 /// errors when shutdown is not completed.
669 ///
670 /// # Examples
671 ///
672 /// ```no_run
673 /// use rusty_cat::api::{MeowClient, MeowConfig};
674 ///
675 /// # async fn run() -> Result<(), rusty_cat::api::MeowError> {
676 /// let client = MeowClient::new(MeowConfig::default());
677 /// client.close().await?;
678 /// # Ok(())
679 /// # }
680 /// ```
681 pub async fn close(&self) -> Result<(), MeowError> {
682 if self
683 .closed
684 .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
685 .is_err()
686 {
687 crate::meow_flow_log!("client_api", "close rejected: already closed");
688 return Err(MeowError::from_code_str(
689 InnerErrorCode::ClientClosed,
690 "meow client is already closed",
691 ));
692 }
693 if let Some(exec) = self.executor.get() {
694 crate::meow_flow_log!("client_api", "close forwarding to executor");
695 if let Err(e) = exec.close().await {
696 // Roll back closed flag so caller can retry close.
697 self.closed.store(false, Ordering::SeqCst);
698 return Err(e);
699 }
700 Ok(())
701 } else {
702 crate::meow_flow_log!("client_api", "close with no executor initialized");
703 Ok(())
704 }
705 }
706
707 /// Returns whether this client is currently closed.
708 ///
709 /// # Examples
710 ///
711 /// ```no_run
712 /// use rusty_cat::api::{MeowClient, MeowConfig};
713 ///
714 /// # async fn run() {
715 /// let client = MeowClient::new(MeowConfig::default());
716 /// let _closed = client.is_closed().await;
717 /// # }
718 /// ```
719 pub async fn is_closed(&self) -> bool {
720 self.closed.load(Ordering::SeqCst)
721 }
722}