11use crate :: {
2- CrdtError , Delta , ExportSlot , HotOp , LamportClock , MAX_EXPORT_SLOTS , NetworkId , NodeId , NodeInput , PeerId , Registry , RegistryDelta , ResourceEntry , Rev , SourceValue , TimeStamp ,
2+ CrdtError , Delta , ExportSlot , History , HotOp , LamportClock , MAX_EXPORT_SLOTS , NetworkId , NodeId , NodeInput , PeerId , Registry , RegistryDelta , ResourceEntry , Rev , SourceValue , TimeStamp ,
33 apply_attribute_delta, reverse_attribute_delta,
44} ;
5- use std:: collections:: HashMap ;
65
76#[ derive( Clone , Debug ) ]
87pub struct Document {
@@ -20,9 +19,10 @@ pub struct Document {
2019 /// working registry keeps the staging-time timestamps. Benign while the local monotonic clock makes
2120 /// new edits win
2221 pub ( crate ) retired_snapshot : Registry ,
23- /// User's cursor in their local history chain.
24- pub ( crate ) head : Rev ,
25- pub ( crate ) history : HashMap < Rev , Delta > ,
22+ /// User's cursor in their local history chain. `None` on an empty document (no commits yet).
23+ pub ( crate ) head : Option < Rev > ,
24+ /// Retired delta DAG in topological (append) order. See [`History`](crate::History).
25+ pub ( crate ) history : History ,
2626 /// Revs undone past (most-recent last), so `redo` can re-apply them. Local-view state the DAG can't
2727 /// recover (a parent may have several children). A new edit while non-empty clears it.
2828 pub ( crate ) redo_stack : Vec < Rev > ,
@@ -52,34 +52,50 @@ impl Document {
5252
5353 pub ( crate ) fn restore_node_from_history ( & mut self , target : RegistryTarget , node_id : NodeId ) -> Result < ( ) , CrdtError > {
5454 let delta = self
55- . history_iter ( )
56- . find ( |d| matches ! ( d. reverse, RegistryDelta :: AddNode { id, .. } if id == node_id) )
57- . ok_or ( CrdtError :: NodeNotInHistory ( node_id) ) ?
58- . clone ( ) ;
55+ . find_in_ancestry ( |d| matches ! ( d. reverse, RegistryDelta :: AddNode { id, .. } if id == node_id) )
56+ . ok_or ( CrdtError :: NodeNotInHistory ( node_id) ) ?;
5957 self . revert_delta ( target, delta)
6058 }
6159
6260 pub ( crate ) fn restore_network_from_history ( & mut self , target : RegistryTarget , network_id : NetworkId ) -> Result < ( ) , CrdtError > {
6361 // Find the Delta whose forward op removed this network. Its `reverse` is `AddNetwork`,
6462 // which is what we want to re-apply.
6563 let delta = self
66- . history_iter ( )
67- . find ( |d| matches ! ( d. reverse, RegistryDelta :: AddNetwork { id, .. } if id == network_id) )
68- . ok_or ( CrdtError :: NetworkNotInHistory ( network_id) ) ?
69- . clone ( ) ;
64+ . find_in_ancestry ( |d| matches ! ( d. reverse, RegistryDelta :: AddNetwork { id, .. } if id == network_id) )
65+ . ok_or ( CrdtError :: NetworkNotInHistory ( network_id) ) ?;
7066 self . revert_delta ( target, delta)
7167 }
7268
69+ /// Search every delta reachable from `head` (following all parents, including a merge's
70+ /// `extra_parents`) for the first matching `predicate`, breadth-first. Resurrection needs full
71+ /// ancestry reachability, so a node added only on a merged-in branch is still found.
72+ fn find_in_ancestry ( & self , predicate : impl Fn ( & Delta ) -> bool ) -> Option < Delta > {
73+ let mut queue: std:: collections:: VecDeque < Rev > = self . head . into_iter ( ) . collect ( ) ;
74+ let mut seen: std:: collections:: HashSet < Rev > = self . head . into_iter ( ) . collect ( ) ;
75+ while let Some ( rev) = queue. pop_front ( ) {
76+ let Some ( delta) = self . history . get ( rev) else { continue } ;
77+ if predicate ( delta) {
78+ return Some ( delta. clone ( ) ) ;
79+ }
80+ for parent in delta. all_parents ( ) {
81+ if seen. insert ( parent) {
82+ queue. push_back ( parent) ;
83+ }
84+ }
85+ }
86+ None
87+ }
88+
7389 /// Apply a delta's `reverse` as the new forward op (silent-zone undo). Force-applied: structural
7490 /// ops are idempotent, and LWW arms assign the reverse value unconditionally even though it carries
7591 /// the same timestamp as the forward op it undoes.
7692 pub ( crate ) fn revert_delta ( & mut self , target : RegistryTarget , mut delta : Delta ) -> Result < ( ) , CrdtError > {
77- std:: mem:: swap ( & mut delta. kind , & mut delta. reverse ) ;
78- for parent in & delta. parents {
79- if !self . history . contains_key ( parent) {
80- return Err ( CrdtError :: NotFoundInHistory ( * parent) ) ;
93+ for parent in delta. all_parents ( ) {
94+ if !self . history . contains ( parent) {
95+ return Err ( CrdtError :: NotFoundInHistory ( parent) ) ;
8196 }
8297 }
98+ std:: mem:: swap ( & mut delta. kind , & mut delta. reverse ) ;
8399 self . apply_op_with ( target, delta. kind , delta. timestamp , ApplyMode :: Force )
84100 }
85101
@@ -104,13 +120,13 @@ impl Document {
104120 /// targets, Remove on missing ones) since hot ops already produced the structural state.
105121 /// The point is to bump field timestamps to T_retire via the LWW arms.
106122 pub fn apply_delta ( & mut self , delta : Delta ) -> Result < ( ) , CrdtError > {
107- for parent in & delta. parents {
108- if !self . history . contains_key ( parent) {
109- return Err ( CrdtError :: NotFoundInHistory ( * parent) ) ;
123+ for parent in delta. all_parents ( ) {
124+ if !self . history . contains ( parent) {
125+ return Err ( CrdtError :: NotFoundInHistory ( parent) ) ;
110126 }
111127 }
112128 self . apply_op_idempotent ( delta. kind . clone ( ) , delta. timestamp ) ?;
113- self . history . insert ( delta . id , delta) ;
129+ self . history . push ( delta) ;
114130 Ok ( ( ) )
115131 }
116132
@@ -271,7 +287,8 @@ impl Document {
271287 RegistryDelta :: ChangeDocumentAttribute { delta } => {
272288 apply_attribute_delta ( delta, timestamp, force, & mut registry. attributes ) ;
273289 }
274- RegistryDelta :: Other ( _) => { }
290+ // Merge is a structural sync point only; it mutates no registry state.
291+ RegistryDelta :: Merge { .. } | RegistryDelta :: Other ( _) => { }
275292 }
276293 Ok ( ( ) )
277294 }
@@ -407,35 +424,10 @@ impl Document {
407424 let snapshot = registry. resources . get ( & id) . cloned ( ) . unwrap_or_default ( ) ;
408425 RegistryDelta :: AddResource { id, entry : snapshot }
409426 }
427+ RegistryDelta :: Merge { extra_parents } => RegistryDelta :: Merge { extra_parents : extra_parents. clone ( ) } ,
410428 & RegistryDelta :: Other ( _) => RegistryDelta :: Other ( serde_json:: Value :: Null ) ,
411429 } )
412430 }
413-
414- /// Retired-only walk from `head` along first parents. Hot ops are excluded by design.
415- fn history_iter ( & self ) -> HistoryIter < ' _ > {
416- HistoryIter {
417- document : self ,
418- parent_rev : self . head ,
419- }
420- }
421- }
422-
423- struct HistoryIter < ' a > {
424- document : & ' a Document ,
425- parent_rev : Rev ,
426- }
427-
428- impl < ' a > Iterator for HistoryIter < ' a > {
429- type Item = & ' a Delta ;
430-
431- fn next ( & mut self ) -> Option < Self :: Item > {
432- let delta = self . document . history . get ( & self . parent_rev ) ?;
433- // First parent only for now. Local-chain walking (filter by author) is a follow-up. The root
434- // delta has no parents, so fall back to the `0` sentinel: the next `get` misses and ends the
435- // walk *after* yielding the root (using `?` here would drop the root instead).
436- self . parent_rev = delta. parents . first ( ) . copied ( ) . unwrap_or ( 0 ) ;
437- Some ( delta)
438- }
439431}
440432
441433/// Which of a [`Document`]'s two registries an apply targets: the working copy (retired state plus
0 commit comments