upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src/purgatory
diff options
context:
space:
mode:
Diffstat (limited to 'src/purgatory')
-rw-r--r--src/purgatory/mod.rs260
-rw-r--r--src/purgatory/sync/context.rs7
-rw-r--r--src/purgatory/types.rs39
3 files changed, 273 insertions, 33 deletions
diff --git a/src/purgatory/mod.rs b/src/purgatory/mod.rs
index 47798a6..3b5514b 100644
--- a/src/purgatory/mod.rs
+++ b/src/purgatory/mod.rs
@@ -17,7 +17,7 @@ pub mod sync;
17mod types; 17mod types;
18 18
19pub use helpers::{can_apply_state, can_satisfy_state, extract_refs_from_state, get_unpushed_refs}; 19pub use helpers::{can_apply_state, can_satisfy_state, extract_refs_from_state, get_unpushed_refs};
20pub use types::{PrPurgatoryEntry, RefPair, RefUpdate, StatePurgatoryEntry}; 20pub use types::{AnnouncementPurgatoryEntry, PrPurgatoryEntry, RefPair, RefUpdate, StatePurgatoryEntry};
21 21
22use dashmap::DashMap; 22use dashmap::DashMap;
23use nostr_sdk::prelude::*; 23use nostr_sdk::prelude::*;
@@ -100,7 +100,8 @@ struct PurgatoryState {
100 100
101/// Main purgatory structure holding events awaiting git data. 101/// Main purgatory structure holding events awaiting git data.
102/// 102///
103/// Provides thread-safe concurrent access to two separate stores: 103/// Provides thread-safe concurrent access to three separate stores:
104/// - Announcements indexed by (pubkey, identifier)
104/// - State events indexed by repository identifier 105/// - State events indexed by repository identifier
105/// - PR events indexed by event ID 106/// - PR events indexed by event ID
106/// 107///
@@ -121,6 +122,10 @@ struct PurgatoryState {
121/// that we've already determined have no git data available. 122/// that we've already determined have no git data available.
122#[derive(Clone)] 123#[derive(Clone)]
123pub struct Purgatory { 124pub struct Purgatory {
125 /// Repository announcements (kind 30617) indexed by (owner pubkey, identifier).
126 /// Key: (PublicKey, String) where String is the repository identifier.
127 announcement_purgatory: Arc<DashMap<(PublicKey, String), AnnouncementPurgatoryEntry>>,
128
124 /// State events (kind 30618) indexed by repository identifier. 129 /// State events (kind 30618) indexed by repository identifier.
125 /// Multiple state events can wait for the same identifier (different maintainers). 130 /// Multiple state events can wait for the same identifier (different maintainers).
126 state_events: Arc<DashMap<String, Vec<StatePurgatoryEntry>>>, 131 state_events: Arc<DashMap<String, Vec<StatePurgatoryEntry>>>,
@@ -145,6 +150,7 @@ impl Purgatory {
145 /// Create a new empty purgatory. 150 /// Create a new empty purgatory.
146 pub fn new(git_data_path: impl Into<PathBuf>) -> Self { 151 pub fn new(git_data_path: impl Into<PathBuf>) -> Self {
147 Self { 152 Self {
153 announcement_purgatory: Arc::new(DashMap::new()),
148 state_events: Arc::new(DashMap::new()), 154 state_events: Arc::new(DashMap::new()),
149 pr_events: Arc::new(DashMap::new()), 155 pr_events: Arc::new(DashMap::new()),
150 sync_queue: Arc::new(DashMap::new()), 156 sync_queue: Arc::new(DashMap::new()),
@@ -513,9 +519,171 @@ impl Purgatory {
513 self.pr_events.remove(event_id); 519 self.pr_events.remove(event_id);
514 } 520 }
515 521
522 // =========================================================================
523 // Announcement Purgatory Methods
524 // =========================================================================
525
526 /// Add a repository announcement to purgatory.
527 ///
528 /// The announcement will be held until git data arrives, at which point
529 /// it will be promoted to the database and served to clients.
530 ///
531 /// # Arguments
532 /// * `event` - The announcement event (kind 30617)
533 /// * `identifier` - The repository identifier from the 'd' tag
534 /// * `owner` - The owner pubkey (event author)
535 /// * `repo_path` - Path to the bare git repository
536 /// * `relays` - Relay URLs from the announcement (for sync registration)
537 pub fn add_announcement(
538 &self,
539 event: Event,
540 identifier: String,
541 owner: PublicKey,
542 repo_path: PathBuf,
543 relays: HashSet<String>,
544 ) {
545 let now = Instant::now();
546 let entry = AnnouncementPurgatoryEntry {
547 event,
548 identifier: identifier.clone(),
549 owner,
550 repo_path,
551 relays,
552 created_at: now,
553 expires_at: now + DEFAULT_EXPIRY,
554 soft_expired: false,
555 };
556
557 let key = (owner, identifier);
558 self.announcement_purgatory.insert(key.clone(), entry);
559
560 tracing::debug!(
561 owner = %key.0,
562 identifier = %key.1,
563 "Added announcement to purgatory"
564 );
565 }
566
567 /// Find an announcement in purgatory by owner and identifier.
568 ///
569 /// # Arguments
570 /// * `owner` - The owner pubkey
571 /// * `identifier` - The repository identifier
572 ///
573 /// # Returns
574 /// The announcement entry if found, None otherwise
575 pub fn find_announcement(&self, owner: &PublicKey, identifier: &str) -> Option<AnnouncementPurgatoryEntry> {
576 let key = (*owner, identifier.to_string());
577 self.announcement_purgatory.get(&key).map(|entry| entry.clone())
578 }
579
580 /// Get all announcements in purgatory for a given identifier.
581 ///
582 /// This is used for authorization - state events and git pushes need to
583 /// check purgatory announcements for maintainer validation.
584 ///
585 /// # Arguments
586 /// * `identifier` - The repository identifier
587 ///
588 /// # Returns
589 /// Vector of announcement entries for this identifier
590 pub fn get_announcements_by_identifier(&self, identifier: &str) -> Vec<AnnouncementPurgatoryEntry> {
591 self.announcement_purgatory
592 .iter()
593 .filter(|entry| entry.key().1 == identifier)
594 .map(|entry| entry.value().clone())
595 .collect()
596 }
597
598 /// Remove an announcement from purgatory.
599 ///
600 /// # Arguments
601 /// * `owner` - The owner pubkey
602 /// * `identifier` - The repository identifier
603 pub fn remove_announcement(&self, owner: &PublicKey, identifier: &str) {
604 let key = (*owner, identifier.to_string());
605 self.announcement_purgatory.remove(&key);
606 tracing::debug!(
607 owner = %owner,
608 identifier = %identifier,
609 "Removed announcement from purgatory"
610 );
611 }
612
613 /// Promote an announcement from purgatory to active status.
614 ///
615 /// This is called when git data arrives. The announcement event is returned
616 /// so it can be saved to the database.
617 ///
618 /// # Arguments
619 /// * `owner` - The owner pubkey
620 /// * `identifier` - The repository identifier
621 ///
622 /// # Returns
623 /// The announcement event if found, None otherwise
624 pub fn promote_announcement(&self, owner: &PublicKey, identifier: &str) -> Option<Event> {
625 let key = (*owner, identifier.to_string());
626 self.announcement_purgatory.remove(&key).map(|(_, entry)| {
627 tracing::info!(
628 owner = %owner,
629 identifier = %identifier,
630 "Promoted announcement from purgatory to database"
631 );
632 entry.event
633 })
634 }
635
636 /// Check if there's an announcement in purgatory for the given owner and identifier.
637 ///
638 /// # Arguments
639 /// * `owner` - The owner pubkey
640 /// * `identifier` - The repository identifier
641 ///
642 /// # Returns
643 /// true if an announcement exists in purgatory, false otherwise
644 pub fn has_purgatory_announcement(&self, owner: &PublicKey, identifier: &str) -> bool {
645 let key = (*owner, identifier.to_string());
646 self.announcement_purgatory.contains_key(&key)
647 }
648
649 /// Extend the expiry for an announcement in purgatory.
650 ///
651 /// This is called when state events arrive for a purgatory announcement,
652 /// indicating the repository is actively receiving metadata.
653 ///
654 /// # Arguments
655 /// * `owner` - The owner pubkey
656 /// * `identifier` - The repository identifier
657 /// * `duration` - Minimum duration to guarantee from now
658 pub fn extend_announcement_expiry(&self, owner: &PublicKey, identifier: &str, duration: Duration) {
659 let key = (*owner, identifier.to_string());
660 if let Some(mut entry) = self.announcement_purgatory.get_mut(&key) {
661 let now = Instant::now();
662 let new_expiry = now + duration;
663 if entry.expires_at < new_expiry {
664 entry.expires_at = new_expiry;
665 // If soft-expired, revive it
666 if entry.soft_expired {
667 entry.soft_expired = false;
668 tracing::debug!(
669 owner = %owner,
670 identifier = %identifier,
671 "Revived soft-expired announcement"
672 );
673 }
674 }
675 }
676 }
677
678 /// Get count of announcements in purgatory.
679 pub fn announcement_count(&self) -> usize {
680 self.announcement_purgatory.len()
681 }
682
516 /// Get all event IDs currently stored in purgatory AND previously expired events. 683 /// Get all event IDs currently stored in purgatory AND previously expired events.
517 /// 684 ///
518 /// Returns a HashSet of all event IDs for: 685 /// Returns a HashSet of all event IDs for:
686 /// - Announcements currently held in purgatory
519 /// - State events currently held in purgatory 687 /// - State events currently held in purgatory
520 /// - PR events currently held in purgatory 688 /// - PR events currently held in purgatory
521 /// - Events that previously expired from purgatory without finding git data 689 /// - Events that previously expired from purgatory without finding git data
@@ -530,6 +698,11 @@ impl Purgatory {
530 pub fn event_ids(&self) -> HashSet<EventId> { 698 pub fn event_ids(&self) -> HashSet<EventId> {
531 let mut ids = HashSet::new(); 699 let mut ids = HashSet::new();
532 700
701 // Collect announcement event IDs
702 for entry in self.announcement_purgatory.iter() {
703 ids.insert(entry.value().event.id);
704 }
705
533 // Collect state event IDs 706 // Collect state event IDs
534 for entry in self.state_events.iter() { 707 for entry in self.state_events.iter() {
535 for state_entry in entry.value().iter() { 708 for state_entry in entry.value().iter() {
@@ -609,9 +782,28 @@ impl Purgatory {
609 /// will be filtered out during future negentropy/REQ sync operations. 782 /// will be filtered out during future negentropy/REQ sync operations.
610 /// 783 ///
611 /// # Returns 784 /// # Returns
612 /// Tuple of (num_state_removed, num_pr_removed) 785 /// Tuple of (num_announcement_removed, num_state_removed, num_pr_removed)
613 pub fn cleanup(&self) -> (usize, usize) { 786 pub fn cleanup(&self) -> (usize, usize, usize) {
614 let now = Instant::now(); 787 let now = Instant::now();
788
789 // Remove expired announcements and mark them as expired
790 let expired_announcements: Vec<(PublicKey, String, EventId)> = self
791 .announcement_purgatory
792 .iter()
793 .filter(|entry| entry.value().expires_at <= now)
794 .map(|entry| {
795 let key = entry.key();
796 let event_id = entry.value().event.id;
797 (key.0.clone(), key.1.clone(), event_id)
798 })
799 .collect();
800
801 let announcement_removed = expired_announcements.len();
802 for (owner, identifier, event_id) in expired_announcements {
803 self.mark_expired(event_id);
804 self.announcement_purgatory.remove(&(owner, identifier));
805 }
806
615 let mut state_removed = 0; 807 let mut state_removed = 0;
616 808
617 // Remove expired state events and mark them as expired 809 // Remove expired state events and mark them as expired
@@ -655,17 +847,17 @@ impl Purgatory {
655 self.pr_events.remove(&event_id_str); 847 self.pr_events.remove(&event_id_str);
656 } 848 }
657 849
658 (state_removed, pr_removed) 850 (announcement_removed, state_removed, pr_removed)
659 } 851 }
660 852
661 /// Remove expired entries from purgatory (legacy method). 853 /// Remove expired entries from purgatory (legacy method).
662 /// 854 ///
663 /// # Returns 855 /// # Returns
664 /// Total number of entries removed (state + PR events) 856 /// Total number of entries removed (announcement + state + PR events)
665 #[deprecated(since = "0.1.0", note = "Use cleanup() instead for separate counts")] 857 #[deprecated(since = "0.1.0", note = "Use cleanup() instead for separate counts")]
666 pub fn remove_expired(&self) -> usize { 858 pub fn remove_expired(&self) -> usize {
667 let (state, pr) = self.cleanup(); 859 let (announcement, state, pr) = self.cleanup();
668 state + pr 860 announcement + state + pr
669 } 861 }
670 862
671 /// Remove old expired event records. 863 /// Remove old expired event records.
@@ -699,11 +891,12 @@ impl Purgatory {
699 /// Get current count of entries in purgatory. 891 /// Get current count of entries in purgatory.
700 /// 892 ///
701 /// # Returns 893 /// # Returns
702 /// Tuple of (state_event_count, pr_event_count) 894 /// Tuple of (announcement_count, state_event_count, pr_event_count)
703 pub fn count(&self) -> (usize, usize) { 895 pub fn count(&self) -> (usize, usize, usize) {
896 let announcement_count = self.announcement_purgatory.len();
704 let state_count: usize = self.state_events.iter().map(|e| e.value().len()).sum(); 897 let state_count: usize = self.state_events.iter().map(|e| e.value().len()).sum();
705 let pr_count = self.pr_events.len(); 898 let pr_count = self.pr_events.len();
706 (state_count, pr_count) 899 (announcement_count, state_count, pr_count)
707 } 900 }
708 901
709 /// Get count of expired events being tracked. 902 /// Get count of expired events being tracked.
@@ -717,6 +910,7 @@ impl Purgatory {
717 /// Clear all entries from purgatory (for testing). 910 /// Clear all entries from purgatory (for testing).
718 #[cfg(test)] 911 #[cfg(test)]
719 pub fn clear(&self) { 912 pub fn clear(&self) {
913 self.announcement_purgatory.clear();
720 self.state_events.clear(); 914 self.state_events.clear();
721 self.pr_events.clear(); 915 self.pr_events.clear();
722 self.sync_queue.clear(); 916 self.sync_queue.clear();
@@ -990,7 +1184,8 @@ mod tests {
990 #[test] 1184 #[test]
991 fn test_purgatory_creation() { 1185 fn test_purgatory_creation() {
992 let purgatory = Purgatory::new(PathBuf::new()); 1186 let purgatory = Purgatory::new(PathBuf::new());
993 let (state_count, pr_count) = purgatory.count(); 1187 let (announcement_count, state_count, pr_count) = purgatory.count();
1188 assert_eq!(announcement_count, 0);
994 assert_eq!(state_count, 0); 1189 assert_eq!(state_count, 0);
995 assert_eq!(pr_count, 0); 1190 assert_eq!(pr_count, 0);
996 } 1191 }
@@ -1008,7 +1203,8 @@ mod tests {
1008 purgatory.add_state(event.clone(), "test-repo".to_string(), keys.public_key()); 1203 purgatory.add_state(event.clone(), "test-repo".to_string(), keys.public_key());
1009 purgatory.add_pr(event, "test-event-id".to_string(), "abc123".to_string()); 1204 purgatory.add_pr(event, "test-event-id".to_string(), "abc123".to_string());
1010 1205
1011 let (state_count, pr_count) = purgatory.count(); 1206 let (announcement_count, state_count, pr_count) = purgatory.count();
1207 assert_eq!(announcement_count, 0);
1012 assert_eq!(state_count, 1); 1208 assert_eq!(state_count, 1);
1013 assert_eq!(pr_count, 1); 1209 assert_eq!(pr_count, 1);
1014 } 1210 }
@@ -1213,7 +1409,7 @@ fn test_cleanup_removes_expired_entries() {
1213 purgatory.add_pr_placeholder("pr-456".to_string(), "commit-def".to_string()); 1409 purgatory.add_pr_placeholder("pr-456".to_string(), "commit-def".to_string());
1214 1410
1215 // Verify entries are there 1411 // Verify entries are there
1216 let (state_count, pr_count) = purgatory.count(); 1412 let (_, state_count, pr_count) = purgatory.count();
1217 assert_eq!(state_count, 1); 1413 assert_eq!(state_count, 1);
1218 assert_eq!(pr_count, 2); 1414 assert_eq!(pr_count, 2);
1219 1415
@@ -1231,14 +1427,14 @@ fn test_cleanup_removes_expired_entries() {
1231 } 1427 }
1232 1428
1233 // Run cleanup 1429 // Run cleanup
1234 let (state_removed, pr_removed) = purgatory.cleanup(); 1430 let (_, state_removed, pr_removed) = purgatory.cleanup();
1235 1431
1236 // Verify counts 1432 // Verify counts
1237 assert_eq!(state_removed, 1); 1433 assert_eq!(state_removed, 1);
1238 assert_eq!(pr_removed, 2); 1434 assert_eq!(pr_removed, 2);
1239 1435
1240 // Verify entries are gone 1436 // Verify entries are gone
1241 let (state_count, pr_count) = purgatory.count(); 1437 let (_, state_count, pr_count) = purgatory.count();
1242 assert_eq!(state_count, 0); 1438 assert_eq!(state_count, 0);
1243 assert_eq!(pr_count, 0); 1439 assert_eq!(pr_count, 0);
1244} 1440}
@@ -1260,14 +1456,14 @@ fn test_cleanup_preserves_non_expired_entries() {
1260 purgatory.add_pr(pr_event, "pr-123".to_string(), "commit-abc".to_string()); 1456 purgatory.add_pr(pr_event, "pr-123".to_string(), "commit-abc".to_string());
1261 1457
1262 // Run cleanup 1458 // Run cleanup
1263 let (state_removed, pr_removed) = purgatory.cleanup(); 1459 let (_, state_removed, pr_removed) = purgatory.cleanup();
1264 1460
1265 // Nothing should be removed 1461 // Nothing should be removed
1266 assert_eq!(state_removed, 0); 1462 assert_eq!(state_removed, 0);
1267 assert_eq!(pr_removed, 0); 1463 assert_eq!(pr_removed, 0);
1268 1464
1269 // Verify entries are still there 1465 // Verify entries are still there
1270 let (state_count, pr_count) = purgatory.count(); 1466 let (_, state_count, pr_count) = purgatory.count();
1271 assert_eq!(state_count, 1); 1467 assert_eq!(state_count, 1);
1272 assert_eq!(pr_count, 1); 1468 assert_eq!(pr_count, 1);
1273} 1469}
@@ -1314,14 +1510,14 @@ fn test_cleanup_mixed_expired_and_fresh() {
1314 } 1510 }
1315 1511
1316 // Run cleanup 1512 // Run cleanup
1317 let (state_removed, pr_removed) = purgatory.cleanup(); 1513 let (_, state_removed, pr_removed) = purgatory.cleanup();
1318 1514
1319 // One of each should be removed 1515 // One of each should be removed
1320 assert_eq!(state_removed, 1); 1516 assert_eq!(state_removed, 1);
1321 assert_eq!(pr_removed, 1); 1517 assert_eq!(pr_removed, 1);
1322 1518
1323 // Verify remaining counts 1519 // Verify remaining counts
1324 let (state_count, pr_count) = purgatory.count(); 1520 let (_, state_count, pr_count) = purgatory.count();
1325 assert_eq!(state_count, 1); // One state event remains 1521 assert_eq!(state_count, 1); // One state event remains
1326 assert_eq!(pr_count, 1); // One PR event remains 1522 assert_eq!(pr_count, 1); // One PR event remains
1327} 1523}
@@ -1391,7 +1587,7 @@ fn test_expired_event_tracking() {
1391 } 1587 }
1392 1588
1393 // Run cleanup 1589 // Run cleanup
1394 let (state_removed, pr_removed) = purgatory.cleanup(); 1590 let (_, state_removed, pr_removed) = purgatory.cleanup();
1395 assert_eq!(state_removed, 1); 1591 assert_eq!(state_removed, 1);
1396 assert_eq!(pr_removed, 1); 1592 assert_eq!(pr_removed, 1);
1397 1593
@@ -1501,7 +1697,7 @@ fn test_expired_events_prevent_readdition() {
1501 } 1697 }
1502 1698
1503 // Event should NOT be re-added 1699 // Event should NOT be re-added
1504 let (state_count, _) = purgatory.count(); 1700 let (_, state_count, _) = purgatory.count();
1505 assert_eq!(state_count, 0, "Event should not be re-added to purgatory"); 1701 assert_eq!(state_count, 0, "Event should not be re-added to purgatory");
1506} 1702}
1507 1703
@@ -1520,7 +1716,7 @@ fn test_pr_placeholder_not_marked_expired() {
1520 } 1716 }
1521 1717
1522 // Run cleanup 1718 // Run cleanup
1523 let (_, pr_removed) = purgatory.cleanup(); 1719 let (_, _, pr_removed) = purgatory.cleanup();
1524 assert_eq!(pr_removed, 1); 1720 assert_eq!(pr_removed, 1);
1525 1721
1526 // Expired count should be 0 (placeholders don't have event IDs to track) 1722 // Expired count should be 0 (placeholders don't have event IDs to track)
@@ -1606,7 +1802,7 @@ async fn test_save_and_restore_state_events() {
1606 assert!(!state_file.exists()); 1802 assert!(!state_file.exists());
1607 1803
1608 // Verify state events were restored 1804 // Verify state events were restored
1609 let (state_count, _) = purgatory2.count(); 1805 let (_, state_count, _) = purgatory2.count();
1610 assert_eq!(state_count, 2); 1806 assert_eq!(state_count, 2);
1611 1807
1612 let restored_entries = purgatory2.find_state("test-repo"); 1808 let restored_entries = purgatory2.find_state("test-repo");
@@ -1662,7 +1858,7 @@ async fn test_save_and_restore_pr_events() {
1662 purgatory2.restore_from_disk(&state_file).unwrap(); 1858 purgatory2.restore_from_disk(&state_file).unwrap();
1663 1859
1664 // Verify PR event was restored 1860 // Verify PR event was restored
1665 let (_, pr_count) = purgatory2.count(); 1861 let (_, _, pr_count) = purgatory2.count();
1666 assert_eq!(pr_count, 1); 1862 assert_eq!(pr_count, 1);
1667 1863
1668 let restored_entry = purgatory2.find_pr("pr-event-id").unwrap(); 1864 let restored_entry = purgatory2.find_pr("pr-event-id").unwrap();
@@ -1691,7 +1887,7 @@ async fn test_save_and_restore_pr_placeholders() {
1691 purgatory2.restore_from_disk(&state_file).unwrap(); 1887 purgatory2.restore_from_disk(&state_file).unwrap();
1692 1888
1693 // Verify placeholder was restored 1889 // Verify placeholder was restored
1694 let (_, pr_count) = purgatory2.count(); 1890 let (_, _, pr_count) = purgatory2.count();
1695 assert_eq!(pr_count, 1); 1891 assert_eq!(pr_count, 1);
1696 1892
1697 let restored_entry = purgatory2.find_pr("placeholder-id").unwrap(); 1893 let restored_entry = purgatory2.find_pr("placeholder-id").unwrap();
@@ -1769,7 +1965,7 @@ async fn test_save_and_restore_empty_purgatory() {
1769 purgatory2.restore_from_disk(&state_file).unwrap(); 1965 purgatory2.restore_from_disk(&state_file).unwrap();
1770 1966
1771 // Verify purgatory is still empty 1967 // Verify purgatory is still empty
1772 let (state_count, pr_count) = purgatory2.count(); 1968 let (_, state_count, pr_count) = purgatory2.count();
1773 assert_eq!(state_count, 0); 1969 assert_eq!(state_count, 0);
1774 assert_eq!(pr_count, 0); 1970 assert_eq!(pr_count, 0);
1775 assert_eq!(purgatory2.expired_count(), 0); 1971 assert_eq!(purgatory2.expired_count(), 0);
@@ -1789,7 +1985,7 @@ async fn test_restore_missing_file() {
1789 assert!(result.is_err()); 1985 assert!(result.is_err());
1790 1986
1791 // Purgatory should remain empty 1987 // Purgatory should remain empty
1792 let (state_count, pr_count) = purgatory.count(); 1988 let (_, state_count, pr_count) = purgatory.count();
1793 assert_eq!(state_count, 0); 1989 assert_eq!(state_count, 0);
1794 assert_eq!(pr_count, 0); 1990 assert_eq!(pr_count, 0);
1795} 1991}
@@ -1811,7 +2007,7 @@ async fn test_restore_corrupted_json() {
1811 assert!(result.is_err()); 2007 assert!(result.is_err());
1812 2008
1813 // Purgatory should remain empty 2009 // Purgatory should remain empty
1814 let (state_count, pr_count) = purgatory.count(); 2010 let (_, state_count, pr_count) = purgatory.count();
1815 assert_eq!(state_count, 0); 2011 assert_eq!(state_count, 0);
1816 assert_eq!(pr_count, 0); 2012 assert_eq!(pr_count, 0);
1817} 2013}
@@ -2044,7 +2240,7 @@ async fn test_mixed_pr_events_and_placeholders() {
2044 purgatory2.restore_from_disk(&state_file).unwrap(); 2240 purgatory2.restore_from_disk(&state_file).unwrap();
2045 2241
2046 // Verify both were restored correctly 2242 // Verify both were restored correctly
2047 let (_, pr_count) = purgatory2.count(); 2243 let (_, _, pr_count) = purgatory2.count();
2048 assert_eq!(pr_count, 2); 2244 assert_eq!(pr_count, 2);
2049 2245
2050 // Verify PR event 2246 // Verify PR event
@@ -2141,7 +2337,7 @@ async fn test_comprehensive_roundtrip() {
2141 purgatory.cleanup(); 2337 purgatory.cleanup();
2142 2338
2143 // Verify initial state 2339 // Verify initial state
2144 let (state_count, pr_count) = purgatory.count(); 2340 let (_, state_count, pr_count) = purgatory.count();
2145 assert_eq!(state_count, 2); // state1, state2 (expired_event was cleaned up) 2341 assert_eq!(state_count, 2); // state1, state2 (expired_event was cleaned up)
2146 assert_eq!(pr_count, 2); // pr-1, pr-2 2342 assert_eq!(pr_count, 2); // pr-1, pr-2
2147 assert_eq!(purgatory.expired_count(), 1); // expired_event 2343 assert_eq!(purgatory.expired_count(), 1); // expired_event
@@ -2154,7 +2350,7 @@ async fn test_comprehensive_roundtrip() {
2154 purgatory2.restore_from_disk(&state_file).unwrap(); 2350 purgatory2.restore_from_disk(&state_file).unwrap();
2155 2351
2156 // Verify all data was restored correctly 2352 // Verify all data was restored correctly
2157 let (state_count2, pr_count2) = purgatory2.count(); 2353 let (_, state_count2, pr_count2) = purgatory2.count();
2158 assert_eq!(state_count2, 2); 2354 assert_eq!(state_count2, 2);
2159 assert_eq!(pr_count2, 2); 2355 assert_eq!(pr_count2, 2);
2160 assert_eq!(purgatory2.expired_count(), 1); 2356 assert_eq!(purgatory2.expired_count(), 1);
diff --git a/src/purgatory/sync/context.rs b/src/purgatory/sync/context.rs
index 33c2d12..778cdb8 100644
--- a/src/purgatory/sync/context.rs
+++ b/src/purgatory/sync/context.rs
@@ -279,7 +279,12 @@ impl SyncContext for RealSyncContext {
279 } 279 }
280 280
281 async fn fetch_repository_data(&self, identifier: &str) -> Result<RepositoryData> { 281 async fn fetch_repository_data(&self, identifier: &str) -> Result<RepositoryData> {
282 crate::git::authorization::fetch_repository_data(&self.database, identifier).await 282 crate::git::authorization::fetch_repository_data_with_purgatory(
283 &self.database,
284 &self.purgatory,
285 identifier,
286 )
287 .await
283 } 288 }
284 289
285 fn collect_needed_oids(&self, identifier: &str) -> HashSet<String> { 290 fn collect_needed_oids(&self, identifier: &str) -> HashSet<String> {
diff --git a/src/purgatory/types.rs b/src/purgatory/types.rs
index 919504b..d891bc9 100644
--- a/src/purgatory/types.rs
+++ b/src/purgatory/types.rs
@@ -6,6 +6,8 @@
6 6
7use nostr_sdk::prelude::*; 7use nostr_sdk::prelude::*;
8use serde::{Deserialize, Serialize}; 8use serde::{Deserialize, Serialize};
9use std::collections::HashSet;
10use std::path::PathBuf;
9use std::time::Instant; 11use std::time::Instant;
10 12
11/// Default value for Instant fields during deserialization 13/// Default value for Instant fields during deserialization
@@ -113,3 +115,40 @@ pub struct PrPurgatoryEntry {
113 #[serde(skip, default = "instant_now")] 115 #[serde(skip, default = "instant_now")]
114 pub expires_at: Instant, 116 pub expires_at: Instant,
115} 117}
118
119/// Entry for a repository announcement (kind 30617) waiting in purgatory.
120///
121/// Announcements are held in purgatory until git data arrives, proving
122/// the repository has actual content. This prevents serving announcements
123/// for empty repositories.
124///
125/// Note: `Instant` fields cannot be serialized directly. Use the `persistence`
126/// module to convert to/from serializable wrapper types.
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct AnnouncementPurgatoryEntry {
129 /// The nostr announcement event (kind 30617)
130 pub event: Event,
131
132 /// The repository identifier from the event's 'd' tag
133 pub identifier: String,
134
135 /// The owner pubkey (event author)
136 pub owner: PublicKey,
137
138 /// Path to the bare git repository
139 pub repo_path: PathBuf,
140
141 /// Relay URLs from the announcement (for sync registration)
142 pub relays: HashSet<String>,
143
144 /// When this entry was added to purgatory
145 #[serde(skip, default = "instant_now")]
146 pub created_at: Instant,
147
148 /// Expiry deadline (30 min from creation, may be extended)
149 #[serde(skip, default = "instant_now")]
150 pub expires_at: Instant,
151
152 /// Whether the bare repo has been deleted (soft expiry)
153 pub soft_expired: bool,
154}