1use serde::{Deserialize, Serialize};
4
5use super::{QueryError, QueryKey, QuerySignal, RetryPolicy};
6
7#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
9pub enum MutationStatus {
10 #[default]
12 Idle,
13 Loading,
15 Success,
17 Failure,
19}
20
21impl MutationStatus {
22 pub fn label(self) -> &'static str {
24 match self {
25 Self::Idle => "Idle",
26 Self::Loading => "Loading",
27 Self::Success => "Success",
28 Self::Failure => "Failure",
29 }
30 }
31
32 pub fn is_loading(self) -> bool {
34 matches!(self, Self::Loading)
35 }
36
37 pub fn is_idle(self) -> bool {
39 matches!(self, Self::Idle)
40 }
41
42 pub fn is_success(self) -> bool {
44 matches!(self, Self::Success)
45 }
46
47 pub fn is_failure(self) -> bool {
49 matches!(self, Self::Failure)
50 }
51}
52
53#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
58pub struct MutationResource<V, T, E = QueryError> {
59 key: Option<QueryKey>,
60 status: MutationStatus,
61 data: Option<T>,
62 error: Option<E>,
63 variables: Option<V>,
64 retry_count: u32,
65 cancelled_count: u64,
66 retry_policy: RetryPolicy,
67 #[serde(skip)]
72 last_updated_at_ms: Option<u64>,
73 #[serde(skip)]
74 signal: Option<QuerySignal>,
75 #[cfg(feature = "client")]
80 #[serde(skip)]
81 pub(crate) current_task: crate::core::current_task::CurrentTask,
82}
83
84fn completion_now_ms() -> u64 {
88 std::time::SystemTime::now()
89 .duration_since(std::time::UNIX_EPOCH)
90 .map(|d| d.as_millis() as u64)
91 .unwrap_or_default()
92}
93
94impl<V, T, E> MutationResource<V, T, E> {
95 pub fn new(retry_policy: RetryPolicy) -> Self {
97 Self {
98 key: None,
99 status: MutationStatus::Idle,
100 data: None,
101 error: None,
102 variables: None,
103 retry_count: 0,
104 cancelled_count: 0,
105 retry_policy,
106 last_updated_at_ms: None,
107 signal: None,
108 #[cfg(feature = "client")]
109 current_task: crate::core::current_task::CurrentTask::default(),
110 }
111 }
112
113 pub fn status(&self) -> MutationStatus {
115 self.status
116 }
117
118 #[cfg_attr(not(feature = "client"), allow(dead_code))]
124 pub(crate) fn last_updated_at_ms(&self) -> Option<u64> {
125 self.last_updated_at_ms
126 }
127
128 pub fn data(&self) -> Option<&T> {
130 self.data.as_ref()
131 }
132
133 pub fn error(&self) -> Option<&E> {
135 self.error.as_ref()
136 }
137
138 pub fn variables(&self) -> Option<&V> {
140 self.variables.as_ref()
141 }
142
143 pub fn retry_count(&self) -> u32 {
145 self.retry_count
146 }
147
148 pub fn cancelled_count(&self) -> u64 {
150 self.cancelled_count
151 }
152
153 pub fn retry_policy(&self) -> &RetryPolicy {
155 &self.retry_policy
156 }
157
158 pub fn set_retry_policy(&mut self, policy: RetryPolicy) {
163 self.retry_policy = policy;
164 }
165
166 pub fn is_loading(&self) -> bool {
168 self.status.is_loading()
169 }
170
171 pub fn is_idle(&self) -> bool {
173 self.status.is_idle()
174 }
175
176 pub fn is_success(&self) -> bool {
178 self.status.is_success()
179 }
180
181 pub fn is_failure(&self) -> bool {
183 self.status.is_failure()
184 }
185
186 pub fn key(&self) -> Option<&QueryKey> {
188 self.key.as_ref()
189 }
190
191 pub fn with_key(mut self, key: QueryKey) -> Self {
197 self.key = Some(key);
198 self
199 }
200
201 pub fn begin(&mut self, variables: V) {
208 if let Some(old_signal) = self.signal.as_ref() {
210 old_signal.cancel();
211 }
212 self.status = MutationStatus::Loading;
213 self.variables = Some(variables);
214 self.error = None;
215 self.retry_count = 0;
216 self.last_updated_at_ms = None;
217 self.signal = Some(QuerySignal::new());
218 }
219
220 pub fn complete_success(&mut self, data: T) {
222 self.status = MutationStatus::Success;
223 self.data = Some(data);
224 self.error = None;
225 self.last_updated_at_ms = Some(completion_now_ms());
226 self.signal = None;
227 }
228
229 pub fn complete_failure(&mut self, error: E) {
235 self.status = MutationStatus::Failure;
236 self.data = None;
237 self.error = Some(error);
238 self.retry_count = self.retry_count.saturating_add(1);
239 self.last_updated_at_ms = Some(completion_now_ms());
240 self.signal = None;
241 }
242
243 pub fn should_retry(&self) -> bool {
245 self.retry_policy.should_retry(self.retry_count)
246 }
247
248 pub fn retry(&mut self) -> bool {
253 if self.status != MutationStatus::Failure || !self.should_retry() {
254 return false;
255 }
256 self.status = MutationStatus::Loading;
257 self.error = None;
258 self.signal = Some(QuerySignal::new());
259 true
260 }
261
262 pub fn reset(&mut self) {
264 if let Some(signal) = self.signal.as_ref() {
265 signal.cancel();
266 }
267 self.status = MutationStatus::Idle;
268 self.data = None;
269 self.error = None;
270 self.variables = None;
271 self.retry_count = 0;
272 self.cancelled_count = 0;
273 self.last_updated_at_ms = None;
277 self.signal = None;
278 }
279
280 pub fn signal(&self) -> Option<&QuerySignal> {
282 self.signal.as_ref()
283 }
284
285 pub fn increment_retry(&mut self) {
290 self.retry_count = self.retry_count.saturating_add(1);
291 }
292
293 pub fn prepare_retry(&mut self) {
301 if self.status != MutationStatus::Loading {
302 return;
303 }
304 if let Some(old_signal) = self.signal.as_ref() {
306 old_signal.cancel();
307 }
308 self.error = None;
309 self.signal = Some(QuerySignal::new());
310 }
311
312 pub fn reset_retry_count(&mut self) {
317 self.retry_count = 0;
318 }
319
320 pub fn cancel(&mut self, error: E) {
329 if self.status != MutationStatus::Loading {
330 return;
331 }
332 self.cancelled_count = self.cancelled_count.saturating_add(1);
333 self.status = MutationStatus::Failure;
334 self.error = Some(error);
335 if let Some(signal) = self.signal.as_ref() {
336 signal.cancel();
337 }
338 self.signal = None;
339 }
340}
341
342#[cfg(feature = "client")]
343impl<V, T, E> MutationResource<V, T, E> {
344 pub(crate) fn set_current_task(&mut self, task: gpui::Task<()>) {
348 self.current_task.set(task);
349 }
350}
351
352#[cfg(test)]
353mod tests {
354 use super::*;
355
356 #[test]
357 fn new_mutation_is_idle() {
358 let m: MutationResource<String, String> = MutationResource::new(RetryPolicy::no_retries());
359 assert!(m.is_idle());
360 assert_eq!(m.status(), MutationStatus::Idle);
361 }
362
363 #[test]
364 fn begin_transitions_to_loading() {
365 let mut m: MutationResource<String, String> =
366 MutationResource::new(RetryPolicy::no_retries());
367 m.begin("vars".to_string());
368 assert!(m.is_loading());
369 assert_eq!(m.variables(), Some(&"vars".to_string()));
370 }
371
372 #[test]
373 fn complete_success_stores_data() {
374 let mut m: MutationResource<String, i32> = MutationResource::new(RetryPolicy::no_retries());
375 m.begin("vars".to_string());
376 m.complete_success(42);
377 assert!(m.is_success());
378 assert_eq!(m.data(), Some(&42));
379 }
380
381 #[test]
382 fn complete_failure_stores_error() {
383 let mut m: MutationResource<String, i32> = MutationResource::new(RetryPolicy::no_retries());
384 m.begin("vars".to_string());
385 m.complete_failure(QueryError::response("bad"));
386 assert!(m.is_failure());
387 assert_eq!(m.retry_count(), 1);
388 assert!(m.data().is_none(), "data should be cleared on failure");
389 }
390
391 #[test]
392 fn retry_from_failure() {
393 let mut m: MutationResource<String, i32> = MutationResource::new(RetryPolicy::new(2));
394 m.begin("vars".to_string());
395 m.complete_failure(QueryError::response("fail"));
396 assert!(m.retry());
397 assert!(m.is_loading());
398 }
399
400 #[test]
401 fn retry_respects_max() {
402 let mut m: MutationResource<String, i32> = MutationResource::new(RetryPolicy::new(1));
403 m.begin("vars".to_string());
404 m.complete_failure(QueryError::response("fail"));
405 assert!(!m.should_retry()); assert!(!m.retry());
407 }
408
409 #[test]
410 fn reset_clears_everything() {
411 let mut m: MutationResource<String, i32> = MutationResource::new(RetryPolicy::new(3));
412 m.begin("vars".to_string());
413 m.complete_success(99);
414 m.reset();
415 assert!(m.is_idle());
416 assert!(m.data().is_none());
417 assert_eq!(m.retry_count(), 0);
418 }
419
420 #[test]
421 fn cancel_cancels_signal() {
422 let mut m: MutationResource<String, i32> = MutationResource::new(RetryPolicy::no_retries());
423 m.begin("vars".to_string());
424 let signal = m.signal().unwrap().clone();
425 assert!(!signal.is_cancelled());
426 m.cancel(QueryError::cancelled("aborted"));
427 assert!(signal.is_cancelled());
428 }
429
430 #[test]
431 fn begin_cancels_old_signal() {
432 let mut m: MutationResource<String, i32> = MutationResource::new(RetryPolicy::no_retries());
433 m.begin("first".to_string());
434 let old_signal = m.signal().unwrap().clone();
435 assert!(!old_signal.is_cancelled());
436 m.begin("second".to_string());
438 assert!(old_signal.is_cancelled());
439 assert!(!m.signal().unwrap().is_cancelled());
441 }
442
443 #[test]
444 fn complete_failure_clears_previous_data() {
445 let mut m: MutationResource<String, i32> = MutationResource::new(RetryPolicy::new(2));
446 m.begin("vars".to_string());
447 m.complete_success(42);
448 assert_eq!(m.data(), Some(&42));
449 m.begin("vars2".to_string());
451 m.complete_failure(QueryError::response("fail"));
452 assert!(m.is_failure());
453 assert!(
454 m.data().is_none(),
455 "data from previous success must be cleared on failure"
456 );
457 }
458
459 #[test]
460 fn cancel_increments_cancelled_count() {
461 let mut m: MutationResource<String, i32> = MutationResource::new(RetryPolicy::no_retries());
462 assert_eq!(m.cancelled_count(), 0);
463 m.begin("vars".to_string());
464 m.cancel(QueryError::cancelled("aborted"));
465 assert_eq!(m.cancelled_count(), 1);
466 m.begin("vars2".to_string());
467 m.cancel(QueryError::cancelled("aborted2"));
468 assert_eq!(m.cancelled_count(), 2);
469 }
470
471 #[test]
472 fn reset_clears_cancelled_count() {
473 let mut m: MutationResource<String, i32> = MutationResource::new(RetryPolicy::no_retries());
474 m.begin("vars".to_string());
475 m.cancel(QueryError::cancelled("aborted"));
476 assert_eq!(m.cancelled_count(), 1);
477 m.reset();
478 assert_eq!(m.cancelled_count(), 0);
479 }
480
481 #[test]
482 fn cancel_on_idle_is_noop() {
483 let mut m: MutationResource<String, i32> = MutationResource::new(RetryPolicy::no_retries());
484 assert!(m.is_idle());
485 m.cancel(QueryError::cancelled("aborted"));
486 assert!(m.is_idle(), "cancel on Idle should be a no-op");
487 assert_eq!(m.cancelled_count(), 0);
488 assert!(m.error().is_none());
489 }
490
491 #[test]
492 fn cancel_on_success_is_noop() {
493 let mut m: MutationResource<String, i32> = MutationResource::new(RetryPolicy::no_retries());
494 m.begin("vars".to_string());
495 m.complete_success(42);
496 m.cancel(QueryError::cancelled("aborted"));
497 assert!(m.is_success(), "cancel on Success should be a no-op");
498 assert_eq!(m.cancelled_count(), 0);
499 assert_eq!(m.data(), Some(&42));
500 }
501
502 #[test]
503 fn cancel_on_failure_is_noop() {
504 let mut m: MutationResource<String, i32> = MutationResource::new(RetryPolicy::no_retries());
505 m.begin("vars".to_string());
506 m.complete_failure(QueryError::response("fail"));
507 m.cancel(QueryError::cancelled("aborted"));
508 assert!(m.is_failure(), "cancel on Failure should be a no-op");
509 assert_eq!(m.cancelled_count(), 0);
510 }
511
512 #[test]
513 fn begin_resets_retry_count() {
514 let mut m: MutationResource<String, i32> = MutationResource::new(RetryPolicy::new(2));
515 m.begin("vars".to_string());
516 m.complete_failure(QueryError::response("fail"));
517 assert_eq!(m.retry_count(), 1);
518 m.begin("vars2".to_string());
520 assert_eq!(m.retry_count(), 0, "begin() should reset retry_count");
521 }
522
523 #[test]
524 fn begin_resets_retry_count_allows_fresh_retries() {
525 let mut m: MutationResource<String, i32> = MutationResource::new(RetryPolicy::new(1));
526 m.begin("vars".to_string());
528 m.complete_failure(QueryError::response("fail"));
529 assert_eq!(m.retry_count(), 1);
530 assert!(
531 !m.should_retry(),
532 "retries exhausted after first invocation"
533 );
534 m.begin("vars2".to_string());
536 assert_eq!(m.retry_count(), 0);
537 assert!(
538 m.should_retry(),
539 "should_retry should be true after begin resets retry_count"
540 );
541 m.complete_failure(QueryError::response("fail again"));
542 assert_eq!(m.retry_count(), 1);
543 assert!(
544 !m.should_retry(),
545 "retries exhausted after second invocation"
546 );
547 }
548}