diff options
Diffstat (limited to 'src/purgatory')
| -rw-r--r-- | src/purgatory/mod.rs | 260 | ||||
| -rw-r--r-- | src/purgatory/sync/context.rs | 7 | ||||
| -rw-r--r-- | src/purgatory/types.rs | 39 |
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; | |||
| 17 | mod types; | 17 | mod types; |
| 18 | 18 | ||
| 19 | pub use helpers::{can_apply_state, can_satisfy_state, extract_refs_from_state, get_unpushed_refs}; | 19 | pub use helpers::{can_apply_state, can_satisfy_state, extract_refs_from_state, get_unpushed_refs}; |
| 20 | pub use types::{PrPurgatoryEntry, RefPair, RefUpdate, StatePurgatoryEntry}; | 20 | pub use types::{AnnouncementPurgatoryEntry, PrPurgatoryEntry, RefPair, RefUpdate, StatePurgatoryEntry}; |
| 21 | 21 | ||
| 22 | use dashmap::DashMap; | 22 | use dashmap::DashMap; |
| 23 | use nostr_sdk::prelude::*; | 23 | use 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)] |
| 123 | pub struct Purgatory { | 124 | pub 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 | ||
| 7 | use nostr_sdk::prelude::*; | 7 | use nostr_sdk::prelude::*; |
| 8 | use serde::{Deserialize, Serialize}; | 8 | use serde::{Deserialize, Serialize}; |
| 9 | use std::collections::HashSet; | ||
| 10 | use std::path::PathBuf; | ||
| 9 | use std::time::Instant; | 11 | use 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)] | ||
| 128 | pub 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 | } | ||