jj workspaces over the network
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(core): add Y.Doc structure and sync protocol

+659
+659
crates/tandem-core/src/sync.rs
··· 1 + //! Synchronization primitives using Yrs CRDT 2 + 3 + use crate::types::{ChangeId, ChangeRecord, PresenceInfo}; 4 + use serde_json; 5 + use std::collections::HashMap; 6 + use std::sync::{Arc, RwLock}; 7 + use yrs::updates::decoder::Decode; 8 + use yrs::updates::encoder::Encode; 9 + use yrs::{Doc, Map, ReadTxn, StateVector, Transact, TransactionMut, Update, WriteTxn}; 10 + 11 + /// Y.Doc structure for forge sync 12 + /// 13 + /// Structure: 14 + /// - Y.Map("changes") → {record_id: ChangeRecord} 15 + /// - Y.Map("bookmarks") → {name: ChangeId} 16 + /// - Y.Map("presence") → {user_id: PresenceInfo} 17 + /// - Subdocuments keyed by hash → Y.Map("data") → base64-encoded blob content 18 + pub struct ForgeDoc { 19 + doc: Doc, 20 + subdocs: Arc<RwLock<HashMap<String, Arc<Doc>>>>, 21 + } 22 + 23 + impl ForgeDoc { 24 + pub fn new() -> Self { 25 + Self { 26 + doc: Doc::new(), 27 + subdocs: Arc::new(RwLock::new(HashMap::new())), 28 + } 29 + } 30 + 31 + /// Get the underlying Y.Doc for sync operations 32 + pub fn doc(&self) -> &Doc { 33 + &self.doc 34 + } 35 + 36 + // Change operations 37 + 38 + /// Insert a change record into the CRDT 39 + pub fn insert_change(&self, record: &ChangeRecord) { 40 + let mut txn = self.doc.transact_mut(); 41 + let changes = txn.get_or_insert_map("changes"); 42 + let record_id = record.record_id.to_string(); 43 + let json = serde_json::to_string(record).expect("Failed to serialize ChangeRecord"); 44 + changes.insert(&mut txn, record_id, json); 45 + } 46 + 47 + /// Get all records for a specific change_id (handles divergence) 48 + pub fn get_change_records(&self, change_id: &ChangeId) -> Vec<ChangeRecord> { 49 + let txn = self.doc.transact(); 50 + let changes = match txn.get_map("changes") { 51 + Some(map) => map, 52 + None => return Vec::new(), 53 + }; 54 + 55 + let mut records = Vec::new(); 56 + for (_key, value) in changes.iter(&txn) { 57 + if let Ok(json) = value.cast::<String>() { 58 + if let Ok(record) = serde_json::from_str::<ChangeRecord>(&json) { 59 + if record.change_id == *change_id { 60 + records.push(record); 61 + } 62 + } 63 + } 64 + } 65 + records 66 + } 67 + 68 + /// Get all change records 69 + pub fn get_all_change_records(&self) -> Vec<ChangeRecord> { 70 + let txn = self.doc.transact(); 71 + let changes = match txn.get_map("changes") { 72 + Some(map) => map, 73 + None => return Vec::new(), 74 + }; 75 + 76 + let mut records = Vec::new(); 77 + for (_key, value) in changes.iter(&txn) { 78 + if let Ok(json) = value.cast::<String>() { 79 + if let Ok(record) = serde_json::from_str::<ChangeRecord>(&json) { 80 + records.push(record); 81 + } 82 + } 83 + } 84 + records 85 + } 86 + 87 + /// Mark a change record as hidden (for abandoned changes) 88 + pub fn mark_change_hidden(&self, record_id: &str) { 89 + let mut txn = self.doc.transact_mut(); 90 + let changes = txn.get_or_insert_map("changes"); 91 + 92 + if let Some(value) = changes.get(&txn, record_id) { 93 + if let Ok(json) = value.cast::<String>() { 94 + if let Ok(mut record) = serde_json::from_str::<ChangeRecord>(&json) { 95 + record.visible = false; 96 + let updated_json = serde_json::to_string(&record) 97 + .expect("Failed to serialize ChangeRecord"); 98 + changes.insert(&mut txn, record_id, updated_json); 99 + } 100 + } 101 + } 102 + } 103 + 104 + // Bookmark operations 105 + 106 + /// Set a bookmark to point at a change 107 + pub fn set_bookmark(&self, name: &str, target: &ChangeId) { 108 + let mut txn = self.doc.transact_mut(); 109 + let bookmarks = txn.get_or_insert_map("bookmarks"); 110 + let target_str = target.to_string(); 111 + bookmarks.insert(&mut txn, name, target_str); 112 + } 113 + 114 + /// Get the change a bookmark points to 115 + pub fn get_bookmark(&self, name: &str) -> Option<ChangeId> { 116 + let txn = self.doc.transact(); 117 + let bookmarks = txn.get_map("bookmarks")?; 118 + let value = bookmarks.get(&txn, name)?; 119 + let target_str = value.cast::<String>().ok()?; 120 + target_str.parse().ok() 121 + } 122 + 123 + /// Get all bookmarks 124 + pub fn get_all_bookmarks(&self) -> Vec<(String, ChangeId)> { 125 + let txn = self.doc.transact(); 126 + let bookmarks = match txn.get_map("bookmarks") { 127 + Some(map) => map, 128 + None => return Vec::new(), 129 + }; 130 + 131 + let mut result = Vec::new(); 132 + for (key, value) in bookmarks.iter(&txn) { 133 + if let Ok(target_str) = value.cast::<String>() { 134 + if let Ok(change_id) = target_str.parse() { 135 + result.push((key.to_string(), change_id)); 136 + } 137 + } 138 + } 139 + result 140 + } 141 + 142 + /// Remove a bookmark 143 + pub fn remove_bookmark(&self, name: &str) { 144 + let mut txn = self.doc.transact_mut(); 145 + let bookmarks = txn.get_or_insert_map("bookmarks"); 146 + bookmarks.remove(&mut txn, name); 147 + } 148 + 149 + // Presence operations 150 + 151 + /// Update presence information for a user 152 + pub fn update_presence(&self, info: &PresenceInfo) { 153 + let mut txn = self.doc.transact_mut(); 154 + let presence = txn.get_or_insert_map("presence"); 155 + let json = serde_json::to_string(info).expect("Failed to serialize PresenceInfo"); 156 + presence.insert(&mut txn, info.user_id.as_str(), json); 157 + } 158 + 159 + /// Get presence information for a user 160 + pub fn get_presence(&self, user_id: &str) -> Option<PresenceInfo> { 161 + let txn = self.doc.transact(); 162 + let presence = txn.get_map("presence")?; 163 + let value = presence.get(&txn, user_id)?; 164 + let json = value.cast::<String>().ok()?; 165 + serde_json::from_str(&json).ok() 166 + } 167 + 168 + /// Get all presence information 169 + pub fn get_all_presence(&self) -> Vec<PresenceInfo> { 170 + let txn = self.doc.transact(); 171 + let presence = match txn.get_map("presence") { 172 + Some(map) => map, 173 + None => return Vec::new(), 174 + }; 175 + 176 + let mut result = Vec::new(); 177 + for (_key, value) in presence.iter(&txn) { 178 + if let Ok(json) = value.cast::<String>() { 179 + if let Ok(info) = serde_json::from_str::<PresenceInfo>(&json) { 180 + result.push(info); 181 + } 182 + } 183 + } 184 + result 185 + } 186 + 187 + /// Remove presence information for a user 188 + pub fn remove_presence(&self, user_id: &str) { 189 + let mut txn = self.doc.transact_mut(); 190 + let presence = txn.get_or_insert_map("presence"); 191 + presence.remove(&mut txn, user_id); 192 + } 193 + 194 + // Content/subdocument operations 195 + 196 + /// Check if content for a hash is available locally 197 + pub fn has_content(&self, hash: &str) -> bool { 198 + let subdocs = self.subdocs.read().unwrap(); 199 + if let Some(subdoc) = subdocs.get(hash) { 200 + let txn = subdoc.transact(); 201 + if let Some(data_map) = txn.get_map("data") { 202 + return data_map.get(&txn, "content").is_some(); 203 + } 204 + } 205 + false 206 + } 207 + 208 + /// Get content if available locally (doesn't fetch) 209 + pub fn get_content(&self, hash: &str) -> Option<Vec<u8>> { 210 + let subdocs = self.subdocs.read().unwrap(); 211 + let subdoc = subdocs.get(hash)?; 212 + let txn = subdoc.transact(); 213 + let data_map = txn.get_map("data")?; 214 + let base64_str = data_map.get(&txn, "content")?.cast::<String>().ok()?; 215 + base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &base64_str).ok() 216 + } 217 + 218 + /// Store content locally (for content we've fetched or created) 219 + pub fn put_content(&self, hash: &str, content: Vec<u8>) { 220 + let subdoc = self.get_subdoc(hash); 221 + let mut txn = subdoc.transact_mut(); 222 + let data_map = txn.get_or_insert_map("data"); 223 + let base64_str = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &content); 224 + data_map.insert(&mut txn, "content", base64_str); 225 + } 226 + 227 + /// Get a subdocument for a specific hash (creates if doesn't exist) 228 + /// The subdocument can be synced independently 229 + pub fn get_subdoc(&self, hash: &str) -> Arc<Doc> { 230 + let mut subdocs = self.subdocs.write().unwrap(); 231 + subdocs 232 + .entry(hash.to_string()) 233 + .or_insert_with(|| Arc::new(Doc::new())) 234 + .clone() 235 + } 236 + 237 + /// List all subdocument hashes we have locally 238 + pub fn list_local_content(&self) -> Vec<String> { 239 + let subdocs = self.subdocs.read().unwrap(); 240 + subdocs.keys().cloned().collect() 241 + } 242 + 243 + /// Encode subdoc state vector for requesting content 244 + pub fn encode_subdoc_state_vector(&self, hash: &str) -> Option<Vec<u8>> { 245 + let subdocs = self.subdocs.read().unwrap(); 246 + let subdoc = subdocs.get(hash)?; 247 + let txn = subdoc.transact(); 248 + Some(txn.state_vector().encode_v1()) 249 + } 250 + 251 + /// Apply update to a subdocument 252 + pub fn apply_subdoc_update(&self, hash: &str, update: &[u8]) -> Result<(), yrs::encoding::read::Error> { 253 + let subdoc = self.get_subdoc(hash); 254 + let mut txn = subdoc.transact_mut(); 255 + let update = Update::decode_v1(update)?; 256 + let _result = txn.apply_update(update); 257 + Ok(()) 258 + } 259 + 260 + // Sync protocol 261 + 262 + /// Encode the current state vector for sync 263 + pub fn encode_state_vector(&self) -> Vec<u8> { 264 + let txn = self.doc.transact(); 265 + txn.state_vector().encode_v1() 266 + } 267 + 268 + /// Encode an update based on a remote state vector 269 + pub fn encode_update_from(&self, state_vector: &[u8]) -> Vec<u8> { 270 + let txn = self.doc.transact(); 271 + let remote_sv = StateVector::decode_v1(state_vector) 272 + .expect("Failed to decode state vector"); 273 + txn.encode_diff_v1(&remote_sv) 274 + } 275 + 276 + /// Apply an update from a remote peer 277 + pub fn apply_update(&self, update: &[u8]) -> Result<(), yrs::encoding::read::Error> { 278 + let mut txn = self.doc.transact_mut(); 279 + let update = Update::decode_v1(update)?; 280 + let _result = txn.apply_update(update); 281 + Ok(()) 282 + } 283 + 284 + // Transactions for atomic operations 285 + 286 + /// Execute a function within a transaction for atomic updates 287 + pub fn transact<F, R>(&self, f: F) -> R 288 + where 289 + F: FnOnce(&mut TransactionMut) -> R, 290 + { 291 + let mut txn = self.doc.transact_mut(); 292 + f(&mut txn) 293 + } 294 + } 295 + 296 + impl Default for ForgeDoc { 297 + fn default() -> Self { 298 + Self::new() 299 + } 300 + } 301 + 302 + #[cfg(test)] 303 + mod tests { 304 + use super::*; 305 + use crate::types::{Identity, TreeHash}; 306 + use chrono::Utc; 307 + use uuid::Uuid; 308 + 309 + fn create_test_record(change_id: ChangeId) -> ChangeRecord { 310 + ChangeRecord { 311 + record_id: Uuid::new_v4(), 312 + change_id, 313 + tree: TreeHash([0u8; 20]), 314 + parents: vec![], 315 + description: "Test change".to_string(), 316 + author: Identity { 317 + email: "test@example.com".to_string(), 318 + name: Some("Test User".to_string()), 319 + }, 320 + timestamp: Utc::now(), 321 + visible: true, 322 + } 323 + } 324 + 325 + #[test] 326 + fn test_insert_and_retrieve_change_records() { 327 + let doc = ForgeDoc::new(); 328 + let change_id = ChangeId::new(); 329 + let record = create_test_record(change_id); 330 + 331 + doc.insert_change(&record); 332 + 333 + let retrieved = doc.get_change_records(&change_id); 334 + assert_eq!(retrieved.len(), 1); 335 + assert_eq!(retrieved[0].change_id, change_id); 336 + assert_eq!(retrieved[0].record_id, record.record_id); 337 + } 338 + 339 + #[test] 340 + fn test_multiple_records_same_change_id() { 341 + let doc = ForgeDoc::new(); 342 + let change_id = ChangeId::new(); 343 + 344 + let record1 = create_test_record(change_id); 345 + let record2 = create_test_record(change_id); 346 + 347 + doc.insert_change(&record1); 348 + doc.insert_change(&record2); 349 + 350 + let retrieved = doc.get_change_records(&change_id); 351 + assert_eq!(retrieved.len(), 2); 352 + 353 + let record_ids: Vec<_> = retrieved.iter().map(|r| r.record_id).collect(); 354 + assert!(record_ids.contains(&record1.record_id)); 355 + assert!(record_ids.contains(&record2.record_id)); 356 + } 357 + 358 + #[test] 359 + fn test_get_all_change_records() { 360 + let doc = ForgeDoc::new(); 361 + let change_id1 = ChangeId::new(); 362 + let change_id2 = ChangeId::new(); 363 + 364 + let record1 = create_test_record(change_id1); 365 + let record2 = create_test_record(change_id2); 366 + 367 + doc.insert_change(&record1); 368 + doc.insert_change(&record2); 369 + 370 + let all = doc.get_all_change_records(); 371 + assert_eq!(all.len(), 2); 372 + } 373 + 374 + #[test] 375 + fn test_mark_change_hidden() { 376 + let doc = ForgeDoc::new(); 377 + let change_id = ChangeId::new(); 378 + let record = create_test_record(change_id); 379 + let record_id = record.record_id.to_string(); 380 + 381 + doc.insert_change(&record); 382 + doc.mark_change_hidden(&record_id); 383 + 384 + let retrieved = doc.get_change_records(&change_id); 385 + assert_eq!(retrieved.len(), 1); 386 + assert_eq!(retrieved[0].visible, false); 387 + } 388 + 389 + #[test] 390 + fn test_bookmark_operations() { 391 + let doc = ForgeDoc::new(); 392 + let change_id = ChangeId::new(); 393 + 394 + doc.set_bookmark("main", &change_id); 395 + 396 + let retrieved = doc.get_bookmark("main"); 397 + assert_eq!(retrieved, Some(change_id)); 398 + 399 + let all = doc.get_all_bookmarks(); 400 + assert_eq!(all.len(), 1); 401 + assert_eq!(all[0].0, "main"); 402 + assert_eq!(all[0].1, change_id); 403 + 404 + doc.remove_bookmark("main"); 405 + assert_eq!(doc.get_bookmark("main"), None); 406 + } 407 + 408 + #[test] 409 + fn test_presence_operations() { 410 + let doc = ForgeDoc::new(); 411 + let change_id = ChangeId::new(); 412 + let info = PresenceInfo { 413 + user_id: "user1".to_string(), 414 + change_id, 415 + device: "laptop".to_string(), 416 + timestamp: Utc::now(), 417 + }; 418 + 419 + doc.update_presence(&info); 420 + 421 + let retrieved = doc.get_presence("user1"); 422 + assert!(retrieved.is_some()); 423 + assert_eq!(retrieved.unwrap().user_id, "user1"); 424 + 425 + let all = doc.get_all_presence(); 426 + assert_eq!(all.len(), 1); 427 + 428 + doc.remove_presence("user1"); 429 + assert_eq!(doc.get_presence("user1"), None); 430 + } 431 + 432 + #[test] 433 + fn test_sync_between_docs() { 434 + let doc1 = ForgeDoc::new(); 435 + let doc2 = ForgeDoc::new(); 436 + 437 + let change_id = ChangeId::new(); 438 + let record = create_test_record(change_id); 439 + 440 + // Insert into doc1 441 + doc1.insert_change(&record); 442 + doc1.set_bookmark("main", &change_id); 443 + 444 + // Sync from doc1 to doc2 445 + let sv2 = doc2.encode_state_vector(); 446 + let update = doc1.encode_update_from(&sv2); 447 + doc2.apply_update(&update).unwrap(); 448 + 449 + // Verify doc2 has the data 450 + let retrieved = doc2.get_change_records(&change_id); 451 + assert_eq!(retrieved.len(), 1); 452 + assert_eq!(retrieved[0].change_id, change_id); 453 + 454 + let bookmark = doc2.get_bookmark("main"); 455 + assert_eq!(bookmark, Some(change_id)); 456 + } 457 + 458 + #[test] 459 + fn test_bidirectional_sync() { 460 + let doc1 = ForgeDoc::new(); 461 + let doc2 = ForgeDoc::new(); 462 + 463 + let change_id1 = ChangeId::new(); 464 + let change_id2 = ChangeId::new(); 465 + let record1 = create_test_record(change_id1); 466 + let record2 = create_test_record(change_id2); 467 + 468 + // Insert different records into each doc 469 + doc1.insert_change(&record1); 470 + doc2.insert_change(&record2); 471 + 472 + // Sync doc1 -> doc2 473 + let sv2 = doc2.encode_state_vector(); 474 + let update1 = doc1.encode_update_from(&sv2); 475 + doc2.apply_update(&update1).unwrap(); 476 + 477 + // Sync doc2 -> doc1 478 + let sv1 = doc1.encode_state_vector(); 479 + let update2 = doc2.encode_update_from(&sv1); 480 + doc1.apply_update(&update2).unwrap(); 481 + 482 + // Both docs should have both records 483 + let all1 = doc1.get_all_change_records(); 484 + let all2 = doc2.get_all_change_records(); 485 + assert_eq!(all1.len(), 2); 486 + assert_eq!(all2.len(), 2); 487 + } 488 + 489 + #[test] 490 + fn test_atomic_transaction() { 491 + let doc = ForgeDoc::new(); 492 + let change_id1 = ChangeId::new(); 493 + let change_id2 = ChangeId::new(); 494 + 495 + // Use transaction to atomically insert multiple records 496 + doc.transact(|txn| { 497 + let changes = txn.get_or_insert_map("changes"); 498 + 499 + let record1 = create_test_record(change_id1); 500 + let json1 = serde_json::to_string(&record1).unwrap(); 501 + changes.insert(txn, record1.record_id.to_string(), json1); 502 + 503 + let record2 = create_test_record(change_id2); 504 + let json2 = serde_json::to_string(&record2).unwrap(); 505 + changes.insert(txn, record2.record_id.to_string(), json2); 506 + }); 507 + 508 + let all = doc.get_all_change_records(); 509 + assert_eq!(all.len(), 2); 510 + } 511 + 512 + #[test] 513 + fn test_state_vector_encoding() { 514 + let doc = ForgeDoc::new(); 515 + let change_id = ChangeId::new(); 516 + let record = create_test_record(change_id); 517 + 518 + doc.insert_change(&record); 519 + 520 + let sv = doc.encode_state_vector(); 521 + assert!(!sv.is_empty()); 522 + } 523 + 524 + #[test] 525 + fn test_put_and_get_content() { 526 + let doc = ForgeDoc::new(); 527 + let hash = "abc123"; 528 + let content = b"Hello, World!".to_vec(); 529 + 530 + doc.put_content(hash, content.clone()); 531 + 532 + assert!(doc.has_content(hash)); 533 + let retrieved = doc.get_content(hash); 534 + assert_eq!(retrieved, Some(content)); 535 + } 536 + 537 + #[test] 538 + fn test_has_content_returns_false_for_missing() { 539 + let doc = ForgeDoc::new(); 540 + assert!(!doc.has_content("nonexistent")); 541 + } 542 + 543 + #[test] 544 + fn test_get_content_returns_none_for_missing() { 545 + let doc = ForgeDoc::new(); 546 + assert_eq!(doc.get_content("nonexistent"), None); 547 + } 548 + 549 + #[test] 550 + fn test_list_local_content() { 551 + let doc = ForgeDoc::new(); 552 + let hash1 = "hash1"; 553 + let hash2 = "hash2"; 554 + 555 + doc.put_content(hash1, b"content1".to_vec()); 556 + doc.put_content(hash2, b"content2".to_vec()); 557 + 558 + let mut hashes = doc.list_local_content(); 559 + hashes.sort(); 560 + 561 + assert_eq!(hashes.len(), 2); 562 + assert!(hashes.contains(&hash1.to_string())); 563 + assert!(hashes.contains(&hash2.to_string())); 564 + } 565 + 566 + #[test] 567 + fn test_get_subdoc_creates_new() { 568 + let doc = ForgeDoc::new(); 569 + let hash = "test_hash"; 570 + 571 + let subdoc = doc.get_subdoc(hash); 572 + assert!(Arc::strong_count(&subdoc) >= 1); 573 + 574 + // Getting the same subdoc returns the same instance 575 + let subdoc2 = doc.get_subdoc(hash); 576 + assert!(Arc::ptr_eq(&subdoc, &subdoc2)); 577 + } 578 + 579 + #[test] 580 + fn test_encode_subdoc_state_vector() { 581 + let doc = ForgeDoc::new(); 582 + let hash = "test_hash"; 583 + 584 + doc.put_content(hash, b"some content".to_vec()); 585 + 586 + let sv = doc.encode_subdoc_state_vector(hash); 587 + assert!(sv.is_some()); 588 + assert!(!sv.unwrap().is_empty()); 589 + } 590 + 591 + #[test] 592 + fn test_encode_subdoc_state_vector_nonexistent() { 593 + let doc = ForgeDoc::new(); 594 + let sv = doc.encode_subdoc_state_vector("nonexistent"); 595 + assert_eq!(sv, None); 596 + } 597 + 598 + #[test] 599 + fn test_subdoc_sync_between_docs() { 600 + let doc1 = ForgeDoc::new(); 601 + let doc2 = ForgeDoc::new(); 602 + let hash = "sync_test"; 603 + let content = b"sync this content".to_vec(); 604 + 605 + // Put content in doc1 606 + doc1.put_content(hash, content.clone()); 607 + 608 + // Get state vector from doc2 for this hash 609 + let _subdoc2 = doc2.get_subdoc(hash); 610 + let sv2 = doc2.encode_subdoc_state_vector(hash).unwrap(); 611 + 612 + // Generate update from doc1 613 + let subdoc1 = doc1.get_subdoc(hash); 614 + let txn1 = subdoc1.transact(); 615 + let sv2_decoded = StateVector::decode_v1(&sv2).unwrap(); 616 + let update = txn1.encode_diff_v1(&sv2_decoded); 617 + drop(txn1); 618 + 619 + // Apply update to doc2 620 + doc2.apply_subdoc_update(hash, &update).unwrap(); 621 + 622 + // Verify content is synced 623 + assert!(doc2.has_content(hash)); 624 + let retrieved = doc2.get_content(hash); 625 + assert_eq!(retrieved, Some(content)); 626 + } 627 + 628 + #[test] 629 + fn test_apply_subdoc_update_creates_subdoc_if_missing() { 630 + let doc = ForgeDoc::new(); 631 + let hash = "new_hash"; 632 + 633 + // Create a dummy update (empty update) 634 + let temp_doc = Doc::new(); 635 + let txn = temp_doc.transact(); 636 + let sv = txn.state_vector(); 637 + let update = txn.encode_diff_v1(&sv); 638 + drop(txn); 639 + 640 + // Should not error even though subdoc doesn't exist yet 641 + let result = doc.apply_subdoc_update(hash, &update); 642 + assert!(result.is_ok()); 643 + 644 + // Subdoc should now exist 645 + assert!(doc.list_local_content().contains(&hash.to_string())); 646 + } 647 + 648 + #[test] 649 + fn test_content_base64_encoding() { 650 + let doc = ForgeDoc::new(); 651 + let hash = "binary_test"; 652 + let content = vec![0u8, 1, 2, 3, 255, 254, 253]; 653 + 654 + doc.put_content(hash, content.clone()); 655 + 656 + let retrieved = doc.get_content(hash); 657 + assert_eq!(retrieved, Some(content)); 658 + } 659 + }