Skip to main content

fraiseql_core/cache/
cascade_invalidator.rs

1//! Cascading cache invalidation for transitive view dependencies.
2//!
3//! Tracks view-to-view dependencies and propagates invalidation through the dependency graph.
4//! When a view is invalidated, all dependent views are also invalidated.
5
6use std::collections::{HashMap, HashSet, VecDeque};
7
8use serde::{Deserialize, Serialize};
9
10#[allow(unused_imports)] // Reason: used only in doc links for `# Errors` sections
11use crate::error::FraiseQLError;
12use crate::error::Result;
13
14/// Tracks transitive view-to-view dependencies for cascading invalidation.
15///
16/// # Architecture
17///
18/// When `v_user` depends on `v_raw_user`, and `v_analytics` depends on `v_user`:
19///
20/// ```text
21/// v_raw_user (source)
22///    ↓ (depends on)
23/// v_user (intermediate)
24///    ↓ (depends on)
25/// v_analytics (leaf)
26/// ```
27///
28/// Invalidating `v_raw_user` cascades to invalidate `v_user` and `v_analytics`.
29///
30/// # Example
31///
32/// ```rust
33/// use fraiseql_core::cache::cascade_invalidator::CascadeInvalidator;
34///
35/// let mut invalidator = CascadeInvalidator::new();
36///
37/// // Register that v_user depends on v_raw_user
38/// invalidator.add_dependency("v_user", "v_raw_user").unwrap();
39///
40/// // Register that v_analytics depends on v_user
41/// invalidator.add_dependency("v_analytics", "v_user").unwrap();
42///
43/// // Invalidate v_raw_user - cascades to v_user and v_analytics
44/// let affected = invalidator.cascade_invalidate("v_raw_user").unwrap();
45/// assert_eq!(affected.len(), 3); // v_raw_user, v_user, v_analytics
46/// ```
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct CascadeInvalidator {
49    /// Dependent view → list of views it depends on.
50    /// Example: `v_user` → [`v_raw_user`]
51    view_dependencies: HashMap<String, HashSet<String>>,
52
53    /// View → list of views that depend on it (reverse mapping).
54    /// Example: `v_raw_user` → [`v_user`]
55    dependents: HashMap<String, HashSet<String>>,
56
57    /// Statistics for monitoring.
58    stats: InvalidationStats,
59}
60
61/// Statistics for cascade invalidation operations.
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct InvalidationStats {
64    /// Total number of cascade invalidations performed.
65    pub total_cascades: u64,
66
67    /// Total views invalidated across all cascades.
68    pub total_invalidated: u64,
69
70    /// Average views affected per cascade.
71    pub average_affected: f64,
72
73    /// Maximum views affected in a single cascade.
74    pub max_affected: usize,
75}
76
77impl Default for InvalidationStats {
78    fn default() -> Self {
79        Self {
80            total_cascades:    0,
81            total_invalidated: 0,
82            average_affected:  0.0,
83            max_affected:      0,
84        }
85    }
86}
87
88impl CascadeInvalidator {
89    /// Create new cascade invalidator.
90    #[must_use]
91    pub fn new() -> Self {
92        Self {
93            view_dependencies: HashMap::new(),
94            dependents:        HashMap::new(),
95            stats:             InvalidationStats::default(),
96        }
97    }
98
99    /// Register a view dependency.
100    ///
101    /// Declares that `dependent_view` depends on `dependency_view`.
102    /// When `dependency_view` is invalidated, `dependent_view` will also be invalidated.
103    ///
104    /// # Arguments
105    ///
106    /// * `dependent_view` - View that depends on another
107    /// * `dependency_view` - View that is depended upon
108    ///
109    /// # Errors
110    ///
111    /// Returns [`FraiseQLError::Validation`] if `dependent_view` and
112    /// `dependency_view` are the same (self-dependency), or if adding the edge
113    /// would create a cycle in the dependency graph.
114    ///
115    /// # Example
116    ///
117    /// ```rust
118    /// use fraiseql_core::cache::cascade_invalidator::CascadeInvalidator;
119    ///
120    /// let mut invalidator = CascadeInvalidator::new();
121    /// invalidator.add_dependency("v_analytics", "v_user").unwrap();
122    /// ```
123    pub fn add_dependency(&mut self, dependent_view: &str, dependency_view: &str) -> Result<()> {
124        if dependent_view == dependency_view {
125            return Err(crate::error::FraiseQLError::Validation {
126                message: "View cannot depend on itself".to_string(),
127                path:    Some("cascade_invalidator::add_dependency".to_string()),
128            });
129        }
130
131        // Check for indirect cycles: would adding dependent → dependency create a cycle?
132        // That happens if dependency_view can already reach dependent_view via existing edges.
133        // We traverse the `dependents` graph (reverse edges) from dependency_view and check
134        // whether we ever reach dependent_view.
135        if self.can_reach(dependency_view, dependent_view) {
136            return Err(crate::error::FraiseQLError::Validation {
137                message: format!(
138                    "Adding dependency '{}' → '{}' would create a cycle",
139                    dependent_view, dependency_view
140                ),
141                path:    Some("cascade_invalidator::add_dependency".to_string()),
142            });
143        }
144
145        let dependent = dependent_view.to_string();
146        let dependency = dependency_view.to_string();
147
148        // Add forward mapping: dependent → dependency
149        self.view_dependencies
150            .entry(dependent.clone())
151            .or_default()
152            .insert(dependency.clone());
153
154        // Add reverse mapping: dependency → dependent
155        self.dependents.entry(dependency).or_default().insert(dependent);
156
157        Ok(())
158    }
159
160    /// Check whether `from` can reach `target` by following the forward dependency graph.
161    ///
162    /// Uses BFS over `view_dependencies` (from dependent to what it depends on). Returns
163    /// `true` if `target` is reachable from `from`, meaning `from` transitively depends on
164    /// `target`.
165    ///
166    /// This is used to detect cycles before adding a new edge: if `dependency_view` can
167    /// already reach `dependent_view`, adding `dependent_view → dependency_view` would
168    /// create a cycle.
169    fn can_reach(&self, from: &str, target: &str) -> bool {
170        let mut visited = HashSet::new();
171        let mut queue = VecDeque::new();
172        queue.push_back(from.to_string());
173
174        while let Some(current) = queue.pop_front() {
175            if current == target {
176                return true;
177            }
178            if !visited.insert(current.clone()) {
179                continue; // already visited
180            }
181            // Follow forward edges: what does `current` depend on?
182            if let Some(deps) = self.view_dependencies.get(&current) {
183                for dep in deps {
184                    if !visited.contains(dep.as_str()) {
185                        queue.push_back(dep.clone());
186                    }
187                }
188            }
189        }
190
191        false
192    }
193
194    /// Cascade invalidate a view and all dependent views.
195    ///
196    /// Uses breadth-first search to find all views that transitively depend on
197    /// the given view, and returns the complete set of invalidated views.
198    ///
199    /// # Algorithm
200    ///
201    /// 1. Start with the target view
202    /// 2. Find all views that directly depend on it
203    /// 3. For each dependent, recursively find views that depend on it
204    /// 4. Return complete set (target + all transitive dependents)
205    ///
206    /// # Arguments
207    ///
208    /// * `view` - View to invalidate
209    ///
210    /// # Returns
211    ///
212    /// Set of all invalidated views (including the target)
213    ///
214    /// # Errors
215    ///
216    /// Currently infallible — always returns `Ok`. The `Result` return type is
217    /// reserved for future cycle-detection logic that may return
218    /// [`FraiseQLError::Validation`] on circular dependency graphs.
219    ///
220    /// # Example
221    ///
222    /// ```rust
223    /// use fraiseql_core::cache::cascade_invalidator::CascadeInvalidator;
224    ///
225    /// let mut invalidator = CascadeInvalidator::new();
226    /// invalidator.add_dependency("v_user_stats", "v_user").unwrap();
227    /// invalidator.add_dependency("v_dashboard", "v_user_stats").unwrap();
228    ///
229    /// let invalidated = invalidator.cascade_invalidate("v_user").unwrap();
230    /// assert!(invalidated.contains("v_user"));
231    /// assert!(invalidated.contains("v_user_stats"));
232    /// assert!(invalidated.contains("v_dashboard"));
233    /// ```
234    pub fn cascade_invalidate(&mut self, view: &str) -> Result<HashSet<String>> {
235        let mut invalidated = HashSet::new();
236        let mut queue = VecDeque::new();
237
238        queue.push_back(view.to_string());
239        invalidated.insert(view.to_string());
240
241        // BFS to find all transitive dependents
242        while let Some(current_view) = queue.pop_front() {
243            if let Some(dependent_views) = self.dependents.get(&current_view) {
244                for dependent in dependent_views {
245                    if !invalidated.contains(dependent) {
246                        invalidated.insert(dependent.clone());
247                        queue.push_back(dependent.clone());
248                    }
249                }
250            }
251        }
252
253        // Update statistics
254        self.stats.total_cascades += 1;
255        #[allow(clippy::cast_possible_truncation)]
256        // Reason: invalidated.len() is a usize which fits in u64 on all supported 64-bit platforms
257        {
258            self.stats.total_invalidated += invalidated.len() as u64;
259        }
260        self.stats.max_affected = self.stats.max_affected.max(invalidated.len());
261        if self.stats.total_cascades > 0 {
262            #[allow(clippy::cast_precision_loss)]
263            // Reason: average_affected is a display metric; f64 precision loss on u64 counters is
264            // acceptable
265            {
266                self.stats.average_affected =
267                    self.stats.total_invalidated as f64 / self.stats.total_cascades as f64;
268            }
269        }
270
271        Ok(invalidated)
272    }
273
274    /// Get all views that depend on a given view (direct dependents only).
275    ///
276    /// For transitive dependents, use `cascade_invalidate()`.
277    ///
278    /// # Arguments
279    ///
280    /// * `view` - View to query
281    ///
282    /// # Returns
283    ///
284    /// Set of views that directly depend on the given view
285    pub fn get_direct_dependents(&self, view: &str) -> HashSet<String> {
286        self.dependents.get(view).cloned().unwrap_or_default()
287    }
288
289    /// Get all views that a given view depends on (direct dependencies only).
290    ///
291    /// # Arguments
292    ///
293    /// * `view` - View to query
294    ///
295    /// # Returns
296    ///
297    /// Set of views that the given view depends on
298    pub fn get_direct_dependencies(&self, view: &str) -> HashSet<String> {
299        self.view_dependencies.get(view).cloned().unwrap_or_default()
300    }
301
302    /// Get all views that transitively depend on a given view.
303    ///
304    /// Like `cascade_invalidate()` but non-mutating (for queries).
305    ///
306    /// # Arguments
307    ///
308    /// * `view` - View to query
309    ///
310    /// # Returns
311    ///
312    /// Set of all transitive dependents (including the view itself)
313    pub fn get_transitive_dependents(&self, view: &str) -> HashSet<String> {
314        let mut result = HashSet::new();
315        let mut queue = VecDeque::new();
316
317        queue.push_back(view.to_string());
318        result.insert(view.to_string());
319
320        while let Some(current) = queue.pop_front() {
321            if let Some(deps) = self.dependents.get(&current) {
322                for dep in deps {
323                    if !result.contains(dep) {
324                        result.insert(dep.clone());
325                        queue.push_back(dep.clone());
326                    }
327                }
328            }
329        }
330
331        result
332    }
333
334    /// Check if there's a dependency path between two views.
335    ///
336    /// Returns true if `dependent` transitively depends on `dependency`.
337    ///
338    /// # Arguments
339    ///
340    /// * `dependent` - Potentially dependent view
341    /// * `dependency` - Potentially depended-upon view
342    ///
343    /// # Returns
344    ///
345    /// true if there's a transitive dependency
346    pub fn has_dependency_path(&self, dependent: &str, dependency: &str) -> bool {
347        let mut visited = HashSet::new();
348        let mut queue = VecDeque::new();
349
350        queue.push_back(dependent.to_string());
351
352        while let Some(current) = queue.pop_front() {
353            if visited.contains(&current) {
354                continue;
355            }
356            visited.insert(current.clone());
357
358            if let Some(deps) = self.view_dependencies.get(&current) {
359                for dep in deps {
360                    if dep == dependency {
361                        return true;
362                    }
363                    queue.push_back(dep.clone());
364                }
365            }
366        }
367
368        false
369    }
370
371    /// Get cascade invalidation statistics.
372    pub fn stats(&self) -> InvalidationStats {
373        self.stats.clone()
374    }
375
376    /// Clear all registered dependencies.
377    pub fn clear(&mut self) {
378        self.view_dependencies.clear();
379        self.dependents.clear();
380        self.stats = InvalidationStats::default();
381    }
382
383    /// Get total number of views tracked.
384    pub fn view_count(&self) -> usize {
385        let mut views = HashSet::new();
386        views.extend(self.dependents.keys().cloned());
387        views.extend(self.view_dependencies.keys().cloned());
388        views.len()
389    }
390
391    /// Get total number of dependency edges.
392    pub fn dependency_count(&self) -> usize {
393        self.view_dependencies.values().map(|deps| deps.len()).sum()
394    }
395}
396
397impl Default for CascadeInvalidator {
398    fn default() -> Self {
399        Self::new()
400    }
401}
402
403#[cfg(test)]
404mod tests {
405    #![allow(clippy::unwrap_used)] // Reason: test code, panics are acceptable
406
407    use super::*;
408
409    #[test]
410    fn test_add_single_dependency() {
411        let mut invalidator = CascadeInvalidator::new();
412        invalidator.add_dependency("v_user", "v_raw_user").unwrap();
413
414        assert!(invalidator.get_direct_dependencies("v_user").contains("v_raw_user"));
415        assert!(invalidator.get_direct_dependents("v_raw_user").contains("v_user"));
416    }
417
418    #[test]
419    fn test_self_dependency_fails() {
420        let mut invalidator = CascadeInvalidator::new();
421        let result = invalidator.add_dependency("v_user", "v_user");
422        assert!(
423            matches!(result, Err(crate::error::FraiseQLError::Validation { .. })),
424            "expected Validation error for self-dependency, got: {result:?}"
425        );
426    }
427
428    #[test]
429    fn test_cascade_invalidate_single_level() {
430        let mut invalidator = CascadeInvalidator::new();
431        invalidator.add_dependency("v_user", "v_raw_user").unwrap();
432
433        let invalidated = invalidator.cascade_invalidate("v_raw_user").unwrap();
434        assert_eq!(invalidated.len(), 2);
435        assert!(invalidated.contains("v_raw_user"));
436        assert!(invalidated.contains("v_user"));
437    }
438
439    #[test]
440    fn test_cascade_invalidate_multiple_levels() {
441        let mut invalidator = CascadeInvalidator::new();
442        invalidator.add_dependency("v_user", "v_raw_user").unwrap();
443        invalidator.add_dependency("v_analytics", "v_user").unwrap();
444        invalidator.add_dependency("v_dashboard", "v_analytics").unwrap();
445
446        let invalidated = invalidator.cascade_invalidate("v_raw_user").unwrap();
447        assert_eq!(invalidated.len(), 4);
448        assert!(invalidated.contains("v_raw_user"));
449        assert!(invalidated.contains("v_user"));
450        assert!(invalidated.contains("v_analytics"));
451        assert!(invalidated.contains("v_dashboard"));
452    }
453
454    #[test]
455    fn test_cascade_invalidate_branching() {
456        let mut invalidator = CascadeInvalidator::new();
457        invalidator.add_dependency("v_user", "v_raw_user").unwrap();
458        invalidator.add_dependency("v_post", "v_raw_user").unwrap();
459        invalidator.add_dependency("v_analytics", "v_user").unwrap();
460        invalidator.add_dependency("v_dashboard", "v_post").unwrap();
461
462        let invalidated = invalidator.cascade_invalidate("v_raw_user").unwrap();
463        assert_eq!(invalidated.len(), 5);
464        assert!(invalidated.contains("v_raw_user"));
465        assert!(invalidated.contains("v_user"));
466        assert!(invalidated.contains("v_post"));
467        assert!(invalidated.contains("v_analytics"));
468        assert!(invalidated.contains("v_dashboard"));
469    }
470
471    #[test]
472    fn test_get_direct_dependents() {
473        let mut invalidator = CascadeInvalidator::new();
474        invalidator.add_dependency("v_user", "v_raw_user").unwrap();
475        invalidator.add_dependency("v_post", "v_raw_user").unwrap();
476
477        let dependents = invalidator.get_direct_dependents("v_raw_user");
478        assert_eq!(dependents.len(), 2);
479        assert!(dependents.contains("v_user"));
480        assert!(dependents.contains("v_post"));
481    }
482
483    #[test]
484    fn test_get_direct_dependencies() {
485        let mut invalidator = CascadeInvalidator::new();
486        invalidator.add_dependency("v_analytics", "v_user").unwrap();
487        invalidator.add_dependency("v_analytics", "v_post").unwrap();
488
489        let deps = invalidator.get_direct_dependencies("v_analytics");
490        assert_eq!(deps.len(), 2);
491        assert!(deps.contains("v_user"));
492        assert!(deps.contains("v_post"));
493    }
494
495    #[test]
496    fn test_get_transitive_dependents() {
497        let mut invalidator = CascadeInvalidator::new();
498        invalidator.add_dependency("v_user", "v_raw_user").unwrap();
499        invalidator.add_dependency("v_analytics", "v_user").unwrap();
500        invalidator.add_dependency("v_dashboard", "v_analytics").unwrap();
501
502        let transitive = invalidator.get_transitive_dependents("v_raw_user");
503        assert_eq!(transitive.len(), 4);
504        assert!(transitive.contains("v_raw_user"));
505        assert!(transitive.contains("v_user"));
506        assert!(transitive.contains("v_analytics"));
507        assert!(transitive.contains("v_dashboard"));
508    }
509
510    #[test]
511    fn test_has_dependency_path() {
512        let mut invalidator = CascadeInvalidator::new();
513        invalidator.add_dependency("v_user", "v_raw_user").unwrap();
514        invalidator.add_dependency("v_analytics", "v_user").unwrap();
515
516        assert!(invalidator.has_dependency_path("v_analytics", "v_raw_user"));
517        assert!(invalidator.has_dependency_path("v_analytics", "v_user"));
518        assert!(invalidator.has_dependency_path("v_user", "v_raw_user"));
519        assert!(!invalidator.has_dependency_path("v_raw_user", "v_analytics"));
520        assert!(!invalidator.has_dependency_path("v_raw_user", "v_user"));
521    }
522
523    #[test]
524    fn test_stats_tracking() {
525        let mut invalidator = CascadeInvalidator::new();
526        invalidator.add_dependency("v_user", "v_raw_user").unwrap();
527        invalidator.add_dependency("v_analytics", "v_user").unwrap();
528
529        invalidator.cascade_invalidate("v_raw_user").unwrap();
530        invalidator.cascade_invalidate("v_user").unwrap();
531
532        let stats = invalidator.stats();
533        assert_eq!(stats.total_cascades, 2);
534        assert_eq!(stats.total_invalidated, 5); // 3 (raw_user + user + analytics) + 2 (user + analytics)
535        assert_eq!(stats.max_affected, 3);
536    }
537
538    #[test]
539    fn test_clear() {
540        let mut invalidator = CascadeInvalidator::new();
541        invalidator.add_dependency("v_user", "v_raw_user").unwrap();
542        assert_eq!(invalidator.view_count(), 2);
543
544        invalidator.clear();
545        assert_eq!(invalidator.view_count(), 0);
546        assert_eq!(invalidator.dependency_count(), 0);
547    }
548
549    #[test]
550    fn test_view_and_dependency_count() {
551        let mut invalidator = CascadeInvalidator::new();
552        invalidator.add_dependency("v_user", "v_raw_user").unwrap();
553        invalidator.add_dependency("v_post", "v_raw_user").unwrap();
554        invalidator.add_dependency("v_analytics", "v_user").unwrap();
555
556        assert_eq!(invalidator.view_count(), 4);
557        assert_eq!(invalidator.dependency_count(), 3);
558    }
559
560    #[test]
561    fn test_diamond_dependency() {
562        let mut invalidator = CascadeInvalidator::new();
563        // Diamond: raw → [user, post] → analytics
564        invalidator.add_dependency("v_user", "v_raw_user").unwrap();
565        invalidator.add_dependency("v_post", "v_raw_user").unwrap();
566        invalidator.add_dependency("v_analytics", "v_user").unwrap();
567        invalidator.add_dependency("v_analytics", "v_post").unwrap();
568
569        let invalidated = invalidator.cascade_invalidate("v_raw_user").unwrap();
570        // raw_user, user, post, analytics (4 total)
571        assert_eq!(invalidated.len(), 4);
572        assert!(invalidated.contains("v_raw_user"));
573        assert!(invalidated.contains("v_user"));
574        assert!(invalidated.contains("v_post"));
575        assert!(invalidated.contains("v_analytics"));
576    }
577
578    #[test]
579    fn test_multiple_independent_chains() {
580        let mut invalidator = CascadeInvalidator::new();
581        // Chain 1: raw1 → user1 → analytics1
582        invalidator.add_dependency("v_user_1", "v_raw_1").unwrap();
583        invalidator.add_dependency("v_analytics_1", "v_user_1").unwrap();
584        // Chain 2: raw2 → user2 → analytics2
585        invalidator.add_dependency("v_user_2", "v_raw_2").unwrap();
586        invalidator.add_dependency("v_analytics_2", "v_user_2").unwrap();
587
588        let invalidated = invalidator.cascade_invalidate("v_raw_1").unwrap();
589        assert_eq!(invalidated.len(), 3); // Only chain 1
590        assert!(!invalidated.contains("v_raw_2"));
591        assert!(!invalidated.contains("v_user_2"));
592    }
593
594    #[test]
595    fn test_cycle_detection_via_has_dependency_path() {
596        let mut invalidator = CascadeInvalidator::new();
597        invalidator.add_dependency("v_user", "v_raw_user").unwrap();
598        invalidator.add_dependency("v_analytics", "v_user").unwrap();
599
600        // Verify no forward path from leaf to root
601        assert!(!invalidator.has_dependency_path("v_raw_user", "v_analytics"));
602    }
603
604    #[test]
605    fn test_indirect_cycle_detection() {
606        let mut invalidator = CascadeInvalidator::new();
607        // Build chain: A depends on B, B depends on C
608        invalidator.add_dependency("B", "A").unwrap();
609        invalidator.add_dependency("C", "B").unwrap();
610
611        // Adding C → A would create the cycle C→B→A→...→C — must be rejected.
612        // (C depends on B which depends on A; adding A→C would close the loop)
613        let result = invalidator.add_dependency("A", "C");
614        assert!(
615            matches!(result, Err(crate::error::FraiseQLError::Validation { .. })),
616            "expected Validation error for indirect cycle A→C→B→A, got: {result:?}"
617        );
618        let msg = result.unwrap_err().to_string();
619        assert!(msg.contains("cycle"), "error message should mention cycle, got: {msg}");
620    }
621
622    #[test]
623    fn test_three_node_cycle_rejected() {
624        let mut invalidator = CascadeInvalidator::new();
625        invalidator.add_dependency("B", "A").unwrap(); // B depends on A
626        invalidator.add_dependency("C", "B").unwrap(); // C depends on B
627        // A depends on C would close: A→C→B→A
628        let result = invalidator.add_dependency("A", "C");
629        assert!(
630            matches!(result, Err(crate::error::FraiseQLError::Validation { .. })),
631            "expected Validation error for three-node cycle A→C→B→A, got: {result:?}"
632        );
633    }
634
635    #[test]
636    fn test_serialization() {
637        let mut invalidator = CascadeInvalidator::new();
638        invalidator.add_dependency("v_user", "v_raw_user").unwrap();
639
640        let json = serde_json::to_string(&invalidator).expect("serialize should work");
641        let restored: CascadeInvalidator =
642            serde_json::from_str(&json).expect("deserialize should work");
643
644        assert_eq!(
645            restored.get_direct_dependents("v_raw_user"),
646            invalidator.get_direct_dependents("v_raw_user")
647        );
648    }
649
650    #[test]
651    fn cascade_invalidator_is_send_sync() {
652        // Invariant: CascadeInvalidator is read-only (graph data) after construction.
653        // Stats use an internal Mutex for thread-safe writes.
654        fn assert_send_sync<T: Send + Sync>() {}
655        assert_send_sync::<CascadeInvalidator>();
656    }
657
658    #[test]
659    fn cascade_invalidate_takes_shared_ref() {
660        let mut inv = CascadeInvalidator::new();
661        inv.add_dependency("v_b", "v_a").unwrap();
662        // Should work with &inv (not &mut inv) — cascade_invalidate takes &self.
663        let result = inv.cascade_invalidate("v_a").unwrap();
664        assert!(result.contains("v_b"));
665    }
666}