diff --git a/radicle-cli/examples/git/git-push-amend.md b/radicle-cli/examples/git/git-push-amend.md index 436245f2..b3cd985c 100644 --- a/radicle-cli/examples/git/git-push-amend.md +++ b/radicle-cli/examples/git/git-push-amend.md @@ -22,7 +22,7 @@ $ git commit --amend -m "Neue Änderungen" --allow-empty -q ``` ~alice (stderr) $ git push rad master -f ✓ Canonical head updated to 9170c8795d3a78f0381a0ffafb20ea69fb0f5b6b -✓ Synced with 1 node(s) +✓ Synced with 1 seed(s) To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi + fb25886...9170c87 master -> master (forced update) ``` diff --git a/radicle-cli/examples/git/git-push-converge.md b/radicle-cli/examples/git/git-push-converge.md index f6ca2bc8..2ceca3ac 100644 --- a/radicle-cli/examples/git/git-push-converge.md +++ b/radicle-cli/examples/git/git-push-converge.md @@ -94,7 +94,7 @@ $ git push rad -f warn: could not determine canonical tip for `refs/heads/master` warn: no commit found with at least 3 vote(s) (threshold not met) warn: it is recommended to find a commit to agree upon -✓ Synced with 2 node(s) +✓ Synced with 2 seed(s) To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi d09e634..0f9bd80 master -> master ``` @@ -118,7 +118,7 @@ become the canonical `master`. ``` ~bob (stderr) $ git push rad ✓ Canonical head updated to 3a75f66dd0020c9a0355cc6ec21f15de989e2001 -✓ Synced with 2 node(s) +✓ Synced with 2 seed(s) To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk 2a37862..0f9bd80 master -> master ``` @@ -139,7 +139,7 @@ HEAD is now at 0f9bd80 Merge remote-tracking branch 'eve/master' ``` ~eve (stderr) $ git push rad ✓ Canonical head updated to 0f9bd8035c04b3f73f5408e73e8454879b20800b -✓ Synced with 2 node(s) +✓ Synced with 2 seed(s) To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkux1aUQD2voWWukVb5nNUR7thrHveQG4pDQua8nVhib7Z 3a75f66..0f9bd80 master -> master ``` diff --git a/radicle-cli/examples/git/git-push-rollback.md b/radicle-cli/examples/git/git-push-rollback.md index 5cacb2c0..be55bc18 100644 --- a/radicle-cli/examples/git/git-push-rollback.md +++ b/radicle-cli/examples/git/git-push-rollback.md @@ -36,7 +36,7 @@ Fast-forward ``` ~alice (stderr) $ git push rad ✓ Canonical head updated to 319a7dc3b195368ded4b099f8c90bbb80addccd3 -✓ Synced with 1 node(s) +✓ Synced with 1 seed(s) To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi f2de534..319a7dc master -> master ``` @@ -55,7 +55,7 @@ push and the new canonical head becomes the previous commit again: ``` ~alice (stderr) $ git push rad -f ✓ Canonical head updated to f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354 -✓ Synced with 1 node(s) +✓ Synced with 1 seed(s) To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi + 319a7dc...f2de534 master -> master (forced update) ``` diff --git a/radicle-cli/examples/git/git-push.md b/radicle-cli/examples/git/git-push.md index 63263982..9b29e55c 100644 --- a/radicle-cli/examples/git/git-push.md +++ b/radicle-cli/examples/git/git-push.md @@ -59,7 +59,7 @@ f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354 refs/heads/master ``` $ rad sync --announce -✓ Synced with 1 node(s) +✓ Synced with 1 seed(s) ``` Note that it is forbidden to delete the default/canonical branch: diff --git a/radicle-cli/examples/git/git-tag.md b/radicle-cli/examples/git/git-tag.md index d495d7c8..7ce21e61 100644 --- a/radicle-cli/examples/git/git-tag.md +++ b/radicle-cli/examples/git/git-tag.md @@ -12,7 +12,7 @@ $ git tag v1.0 -a -m "Release v1.0" ``` ~alice (stderr) $ git push rad v1.0 -✓ Synced with 1 node(s) +✓ Synced with 1 seed(s) To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi * [new tag] v1.0 -> v1.0 ``` @@ -64,7 +64,7 @@ Updated tag 'v1.0' (was be18ed6) ``` ~alice (stderr) $ git push rad v1.0 -f -✓ Synced with 1 node(s) +✓ Synced with 1 seed(s) To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi + be18ed6...9dbdebc v1.0 -> v1.0 (forced update) ``` diff --git a/radicle-cli/examples/rad-id-threshold-soft-fork.md b/radicle-cli/examples/rad-id-threshold-soft-fork.md index eb9f18fc..47a9682b 100644 --- a/radicle-cli/examples/rad-id-threshold-soft-fork.md +++ b/radicle-cli/examples/rad-id-threshold-soft-fork.md @@ -13,7 +13,7 @@ $ rad issue open --title "Add Bob as a delegate" --description "We agreed to add │ We agreed to add me as a delegate, so I am creating an issue │ │ to track that work │ ╰──────────────────────────────────────────────────────────────╯ -✓ Synced with 1 node(s) +✓ Synced with 1 seed(s) ``` and if we inspect Alice's references, then we will see the following: diff --git a/radicle-cli/examples/rad-id-threshold.md b/radicle-cli/examples/rad-id-threshold.md index 61b81832..fe139deb 100644 --- a/radicle-cli/examples/rad-id-threshold.md +++ b/radicle-cli/examples/rad-id-threshold.md @@ -71,7 +71,7 @@ To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkE ``` ~alice $ rad sync -a -✓ Synced with 1 node(s) +✓ Synced with 1 seed(s) ``` ``` ~alice diff --git a/radicle-cli/examples/rad-inbox.md b/radicle-cli/examples/rad-inbox.md index 13a714a8..e58acea3 100644 --- a/radicle-cli/examples/rad-inbox.md +++ b/radicle-cli/examples/rad-inbox.md @@ -7,7 +7,7 @@ Your inbox is empty. ``` ~bob $ cd heartwood $ rad issue open --title "No license file" --description "..." -q -✓ Synced with 1 node(s) +✓ Synced with 1 seed(s) $ git commit -m "Change copyright" --allow-empty -q $ git push rad HEAD:bob/copy $ cd .. @@ -113,7 +113,7 @@ Now let's do an identity update. $ rad id update --title "Modify description" --description "Use website" --payload xyz.radicle.project description '"https://radicle.xyz"' -q [..] $ rad sync -a -✓ Synced with 1 node(s) +✓ Synced with 1 seed(s) ``` ``` ~bob diff --git a/radicle-cli/examples/rad-init-private-clone.md b/radicle-cli/examples/rad-init-private-clone.md index c61191f0..d1f9c2a6 100644 --- a/radicle-cli/examples/rad-init-private-clone.md +++ b/radicle-cli/examples/rad-init-private-clone.md @@ -14,14 +14,14 @@ Fetching rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu from the network, found 1 potential s ✗ Error: no seeds found for rad:z2ug5mwNKZB8KGpBDRTrWHAMbvHCu ``` -She allows Bob to view the repository. And when she syncs, one node (Bob) gets +She allows Bob to view the repository. And when she syncs, one seed (Bob) gets the refs. ``` ~alice $ rad id update --title "Allow Bob" --description "" --allow did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk -q ... $ rad sync --announce --timeout 3 -✓ Synced with 1 node(s) +✓ Synced with 1 seed(s) ``` Bob can now fetch the private repo without specifying a seed, because he knows diff --git a/radicle-cli/examples/rad-patch-checkout-force.md b/radicle-cli/examples/rad-patch-checkout-force.md index 76bdd096..56732ccb 100644 --- a/radicle-cli/examples/rad-patch-checkout-force.md +++ b/radicle-cli/examples/rad-patch-checkout-force.md @@ -14,7 +14,7 @@ $ git commit -v -m "Define power requirements" ``` ~alice (stderr) $ git push rad -o patch.message="Define power requirements" -o patch.message="See details." HEAD:refs/patches ✓ Patch aa45913e757cacd46972733bddee5472c78fa32a opened -✓ Synced with 1 node(s) +✓ Synced with 1 seed(s) To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi * [new reference] HEAD -> refs/patches ``` @@ -51,7 +51,7 @@ To compare against your previous revision aa45913, run: git range-diff f2de534[..] 3e674d1[..] 27857ec[..] -✓ Synced with 1 node(s) +✓ Synced with 1 seed(s) To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi 3e674d1..27857ec flux-capacitor-power -> patches/aa45913e757cacd46972733bddee5472c78fa32a ``` diff --git a/radicle-cli/examples/rad-patch-delete.md b/radicle-cli/examples/rad-patch-delete.md index fcd19cee..5e085646 100644 --- a/radicle-cli/examples/rad-patch-delete.md +++ b/radicle-cli/examples/rad-patch-delete.md @@ -11,7 +11,7 @@ $ git commit -m "Introduce license" ``` ~alice (stderr) $ git push rad -o patch.draft -o patch.message="Define LICENSE for project" HEAD:refs/patches ✓ Patch 6c61ef1716ad8a5c11e04dd7a3fec51e01fba70b drafted -✓ Synced with 2 node(s) +✓ Synced with 2 seed(s) To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi * [new reference] HEAD -> refs/patches ``` @@ -28,7 +28,7 @@ $ rad patch comment 6c61ef1 -m "I think we should use MIT" │ bob (you) now 833db19 │ │ I think we should use MIT │ ╰───────────────────────────╯ -✓ Synced with 2 node(s) +✓ Synced with 2 seed(s) ``` ``` ~alice @@ -55,7 +55,7 @@ $ rad patch comment 6c61ef1 --reply-to 833db19 -m "Thanks, I'll add it!" │ alice (you) now 1803a38 │ │ Thanks, I'll add it! │ ╰─────────────────────────╯ -✓ Synced with 2 node(s) +✓ Synced with 2 seed(s) ``` ``` ~alice @@ -75,7 +75,7 @@ To compare against your previous revision 6c61ef1, run: git range-diff f2de534[..] 717c900[..] 1cc8cd9[..] -✓ Synced with 2 node(s) +✓ Synced with 2 seed(s) To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi 717c900..1cc8cd9 prepare-license -> patches/6c61ef1716ad8a5c11e04dd7a3fec51e01fba70b ``` @@ -83,7 +83,7 @@ To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkE ``` ~bob $ rad patch review 6c61ef1 --accept -m "LGTM!" ✓ Patch 6c61ef1 accepted -✓ Synced with 2 node(s) +✓ Synced with 2 seed(s) $ rad patch show 6c61ef1 -v ╭─────────────────────────────────────────────────────────────────────╮ │ Title Define LICENSE for project │ @@ -105,7 +105,7 @@ $ rad patch show 6c61ef1 -v ``` ~bob $ rad patch delete 6c61ef1 -✓ Synced with 2 node(s) +✓ Synced with 2 seed(s) ``` ``` ~alice @@ -133,7 +133,7 @@ Alice should no longer have the patch: ``` ~alice $ rad patch delete 6c61ef1 -✓ Synced with 2 node(s) +✓ Synced with 2 seed(s) ``` ``` ~seed (fails) diff --git a/radicle-cli/examples/rad-patch-open-explore.md b/radicle-cli/examples/rad-patch-open-explore.md index dee83571..adc9da82 100644 --- a/radicle-cli/examples/rad-patch-open-explore.md +++ b/radicle-cli/examples/rad-patch-open-explore.md @@ -5,7 +5,7 @@ $ git checkout -b changes -q $ git commit --allow-empty -q -m "Changes" $ git push rad HEAD:refs/patches ✓ Patch acab0ec777a97d013f30be5d5d1aec32562ecb02 opened -✓ Synced with 1 node(s) +✓ Synced with 1 seed(s) https://app.radicle.xyz/nodes/[..]/rad:z3yXbb1sR6UG6ixxV2YF9jUP7ABra/patches/acab0ec777a97d013f30be5d5d1aec32562ecb02 @@ -23,7 +23,7 @@ To compare against your previous revision acab0ec, run: git range-diff f2de534[..] e12525d[..] b2b6432[..] -✓ Synced with 1 node(s) +✓ Synced with 1 seed(s) https://app.radicle.xyz/nodes/[..]/rad:z3yXbb1sR6UG6ixxV2YF9jUP7ABra/patches/acab0ec777a97d013f30be5d5d1aec32562ecb02 @@ -39,7 +39,7 @@ $ git merge changes -q $ git push rad master ✓ Patch acab0ec777a97d013f30be5d5d1aec32562ecb02 merged ✓ Canonical head updated to b2b6432af93f8fe188e32d400263021b602cfec8 -✓ Synced with 1 node(s) +✓ Synced with 1 seed(s) https://app.radicle.xyz/nodes/[..]/rad:z3yXbb1sR6UG6ixxV2YF9jUP7ABra/tree/b2b6432af93f8fe188e32d400263021b602cfec8 diff --git a/radicle-cli/examples/rad-patch-pull-update.md b/radicle-cli/examples/rad-patch-pull-update.md index 44b6f7ba..5fcd89ff 100644 --- a/radicle-cli/examples/rad-patch-pull-update.md +++ b/radicle-cli/examples/rad-patch-pull-update.md @@ -43,7 +43,7 @@ our fork: ``` ~bob (stderr) $ cd heartwood $ git push rad master -✓ Synced with 1 node(s) +✓ Synced with 1 seed(s) To rad://zhbMU4DUXrzB8xT6qAJh6yZ7bFMK/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk * [new branch] master -> master ``` @@ -55,7 +55,7 @@ $ git checkout -b bob/feature -q $ git commit --allow-empty -m "Bob's commit #1" -q $ git push rad -o sync -o patch.message="Bob's patch" HEAD:refs/patches ✓ Patch 55b9721ed7f6bfec38f43729e9b6631c5dc812fb opened -✓ Synced with 1 node(s) +✓ Synced with 1 seed(s) To rad://zhbMU4DUXrzB8xT6qAJh6yZ7bFMK/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk * [new reference] HEAD -> refs/patches ``` @@ -88,7 +88,7 @@ To compare against your previous revision 55b9721, run: git range-diff f2de534[..] bdcdb30[..] cad2666[..] -✓ Synced with 1 node(s) +✓ Synced with 1 seed(s) To rad://zhbMU4DUXrzB8xT6qAJh6yZ7bFMK/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk bdcdb30..cad2666 bob/feature -> patches/55b9721ed7f6bfec38f43729e9b6631c5dc812fb ``` diff --git a/radicle-cli/examples/rad-push-and-pull-patches.md b/radicle-cli/examples/rad-push-and-pull-patches.md index c8e0d67e..b935f185 100644 --- a/radicle-cli/examples/rad-push-and-pull-patches.md +++ b/radicle-cli/examples/rad-push-and-pull-patches.md @@ -38,7 +38,7 @@ To compare against your previous revision d004b67, run: git range-diff f2de534[..] 8d5f1ba[..] c2aaf1c[..] -✓ Synced with 1 node(s) +✓ Synced with 1 seed(s) To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi * [new branch] patch/d004b67 -> patches/d004b67355456c46de10c0d287e4a791ad1a6945 ``` @@ -61,7 +61,7 @@ To compare against your previous revision d004b67, run: git range-diff f2de534[..] 8d5f1ba[..] d9f8caf[..] -✓ Synced with 1 node(s) +✓ Synced with 1 seed(s) To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi c2aaf1c..d9f8caf patch/d004b67 -> patches/d004b67355456c46de10c0d287e4a791ad1a6945 ``` diff --git a/radicle-cli/examples/rad-sync.md b/radicle-cli/examples/rad-sync.md index 48237209..e3381c9b 100644 --- a/radicle-cli/examples/rad-sync.md +++ b/radicle-cli/examples/rad-sync.md @@ -26,7 +26,7 @@ wait for nodes to announce that they have fetched those refs. ``` $ rad sync --announce -✓ Synced with 2 node(s) +✓ Synced with 2 seed(s) ``` Now, when we run `rad sync status` again, we can see that `bob` and @@ -48,7 +48,7 @@ be up to date. ``` $ rad sync --announce -✓ Nothing to announce, already in sync with 2 node(s) (see `rad sync status`) +✓ Nothing to announce, already in sync with 2 seed(s) (see `rad sync status`) ``` We can also use the `--fetch` option to only fetch objects: @@ -69,7 +69,7 @@ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from the network, found 2 potential s ✓ Target met: 2 seed(s) 🌱 Fetched from z6Mkux1…nVhib7Z 🌱 Fetched from z6Mkt67…v4N1tRk -✓ Nothing to announce, already in sync with 2 node(s) (see `rad sync status`) +✓ Nothing to announce, already in sync with 2 seed(s) (see `rad sync status`) ``` It's also possible to use the `--seed` flag to only sync with a specific node: @@ -93,7 +93,7 @@ $ rad sync --replicas 1 Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from the network, found 2 potential seed(s). ✓ Target met: 1 seed(s) 🌱 Fetched from z6Mkux1…nVhib7Z -✓ Synced with 1 node(s) +✓ Synced with 1 seed(s) ``` Note that we see `✓ Fetched repository from 1 seed(s)` and `✓ Synced diff --git a/radicle-cli/examples/workflow/4-patching-contributor.md b/radicle-cli/examples/workflow/4-patching-contributor.md index 181072c9..a0455161 100644 --- a/radicle-cli/examples/workflow/4-patching-contributor.md +++ b/radicle-cli/examples/workflow/4-patching-contributor.md @@ -91,5 +91,5 @@ And let's leave a quick comment for our team: ``` $ rad patch comment e4934b6d9dbe01ce3c7fbb5b77a80d5f1dacdc46 --message 'I cannot wait to get back to the 90s!' -q 8c66f87afadc7c7c857f8bb92973c25f64e75776 -✓ Synced with 1 node(s) +✓ Synced with 1 seed(s) ``` diff --git a/radicle-cli/examples/workflow/5-patching-maintainer.md b/radicle-cli/examples/workflow/5-patching-maintainer.md index de3c2f26..9951438d 100644 --- a/radicle-cli/examples/workflow/5-patching-maintainer.md +++ b/radicle-cli/examples/workflow/5-patching-maintainer.md @@ -77,7 +77,7 @@ Great, all fixed up, lets accept and merge the code. ``` $ rad patch review e4934b6 --revision 9d62420 --accept ✓ Patch e4934b6 accepted -✓ Synced with 1 node(s) +✓ Synced with 1 seed(s) $ git checkout master Your branch is up to date with 'rad/master'. $ git merge patch/e4934b6 @@ -93,7 +93,7 @@ Fast-forward $ git push rad master ✓ Patch e4934b6d9dbe01ce3c7fbb5b77a80d5f1dacdc46 merged at revision 9d62420 ✓ Canonical head updated to f567f695d25b4e8fb63b5f5ad2a584529826e908 -✓ Synced with 1 node(s) +✓ Synced with 1 seed(s) To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi f2de534..f567f69 master -> master ``` @@ -135,5 +135,5 @@ patch, marking it as solved: ``` $ rad issue state 9037b7a --solved ✓ Issue 9037b7a is now solved -✓ Synced with 1 node(s) +✓ Synced with 1 seed(s) ``` diff --git a/radicle-cli/src/commands/sync.rs b/radicle-cli/src/commands/sync.rs index 85e8633d..7b617443 100644 --- a/radicle-cli/src/commands/sync.rs +++ b/radicle-cli/src/commands/sync.rs @@ -1,4 +1,5 @@ use std::cmp::Ordering; +use std::collections::BTreeMap; use std::collections::BTreeSet; use std::collections::HashSet; use std::ffi::OsString; @@ -289,7 +290,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> { ); } - match options.op { + match &options.op { Operation::Status => { let rid = match options.rid { Some(rid) => rid, @@ -315,15 +316,15 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> { }; let settings = settings.clone().with_profile(&profile); - if [SyncDirection::Fetch, SyncDirection::Both].contains(&direction) { + if [SyncDirection::Fetch, SyncDirection::Both].contains(direction) { if !profile.policies()?.is_seeding(&rid)? { anyhow::bail!("repository {rid} is not seeded"); } let result = fetch(rid, settings.clone(), &mut node, &profile)?; display_fetch_result(&result, options.verbose) } - if [SyncDirection::Announce, SyncDirection::Both].contains(&direction) { - announce_refs(rid, settings, options.debug, &mut node, &profile)?; + if [SyncDirection::Announce, SyncDirection::Both].contains(direction) { + announce_refs(rid, settings, &mut node, &profile, &options)?; } } Operation::Synchronize(SyncMode::Inventory) => { @@ -417,9 +418,9 @@ fn sync_status( fn announce_refs( rid: RepoId, settings: SyncSettings, - debug: bool, node: &mut Node, profile: &Profile, + options: &Options, ) -> anyhow::Result<()> { let Ok(repo) = profile.storage.repository(rid) else { return Err(anyhow!( @@ -437,16 +438,19 @@ fn announce_refs( } } - crate::node::announce( + let result = crate::node::announce( &repo, settings, SyncReporting { - debug, + debug: options.debug, ..SyncReporting::default() }, node, profile, )?; + if let Some(result) = result { + print_announcer_result(&result, options.verbose) + } Ok(()) } @@ -816,3 +820,72 @@ fn display_success<'a>( } } } + +fn print_announcer_result(result: &sync::AnnouncerResult, verbose: bool) { + match result { + sync::AnnouncerResult::Success(success) if verbose => { + // N.b. Printing how many seeds were synced with is printed + // elsewhere + match success.outcome() { + sync::announce::SuccessfulOutcome::MinReplicationFactor { preferred, synced } + | sync::announce::SuccessfulOutcome::MaxReplicationFactor { preferred, synced } => { + if preferred == 0 { + term::success!("Synced {} seed(s)", term::format::positive(synced)); + } else { + term::success!( + "Synced {} preferred seed(s) and {} total seed(s)", + term::format::positive(preferred), + term::format::positive(synced) + ); + } + } + } + print_synced(success.synced()); + } + sync::AnnouncerResult::Success(_) => { + // Successes are ignored when `!verbose`. + } + sync::AnnouncerResult::TimedOut(result) => { + if result.synced().is_empty() { + term::error("All seeds timed out, use `rad sync -v` to see the list of seeds"); + return; + } + let timed_out = result.timed_out(); + term::warning(format!( + "{} seed(s) timed out, use `rad sync -v` to see the list of seeds", + timed_out.len(), + )); + if verbose { + print_synced(result.synced()); + for node in timed_out { + term::warning(format!("{} timed out", term::format::node(node))); + } + } + } + sync::AnnouncerResult::NoNodes(result) => { + term::info!("Announcement could not sync with anymore seeds."); + if verbose { + print_synced(result.synced()) + } + } + } +} + +fn print_synced(synced: &BTreeMap) { + for (node, status) in synced.iter() { + let mut message = format!("🌱 Synced with {}", term::format::node(node)); + + match status { + sync::announce::SyncStatus::AlreadySynced => { + message.push_str(&format!("{}", term::format::dim(" (already in sync)"))); + } + sync::announce::SyncStatus::Synced { duration } => { + message.push_str(&format!( + "{}", + term::format::dim(format!("in {}s", duration.as_secs())) + )); + } + } + term::info!("{}", message); + } +} diff --git a/radicle-cli/src/node.rs b/radicle-cli/src/node.rs index c1e838dc..af482dca 100644 --- a/radicle-cli/src/node.rs +++ b/radicle-cli/src/node.rs @@ -2,13 +2,11 @@ use core::time; use std::collections::BTreeSet; use std::io; use std::io::Write; -use std::ops::ControlFlow; -use radicle::node::{self, sync, AnnounceResult}; +use radicle::node::sync; use radicle::node::{Handle as _, NodeId}; use radicle::storage::{ReadRepository, RepositoryError}; use radicle::{Node, Profile}; -use radicle_term::format; use crate::terminal as term; @@ -82,6 +80,8 @@ pub enum SyncError { Node(#[from] radicle::node::Error), #[error("all seeds timed out")] AllSeedsTimedOut, + #[error(transparent)] + Target(#[from] sync::announce::TargetError), } impl SyncError { @@ -159,137 +159,107 @@ pub fn announce( reporting: SyncReporting, node: &mut Node, profile: &Profile, -) -> Result { +) -> Result, SyncError> { match announce_(repo, settings, reporting, node, profile) { Ok(result) => Ok(result), Err(e) if e.is_connection_err() => { term::hint("Node is stopped. To announce changes to the network, start it with `rad node start`."); - Ok(AnnounceResult::default()) + Ok(None) } Err(e) => Err(e), } } -fn announce_( +fn announce_( repo: &R, settings: SyncSettings, mut reporting: SyncReporting, node: &mut Node, profile: &Profile, -) -> Result { +) -> Result, SyncError> +where + R: ReadRepository, +{ + let me = profile.id(); let rid = repo.id(); let doc = repo.identity_doc()?; - let mut settings = settings.with_profile(profile); - let unsynced: Vec<_> = if doc.is_public() { - // All seeds. - let all = node.seeds(rid)?; - if all.is_empty() { - term::info!(&mut reporting.completion; "No seeds found for {rid}."); - return Ok(AnnounceResult::default()); - } - // Seeds in sync with us. - let synced = all - .iter() - .filter(|s| s.is_synced()) - .map(|s| s.nid) - .collect::>(); - // Replicas not counting our local replica. - let replicas = synced.iter().filter(|nid| *nid != profile.id()).count(); - // Maximum replication factor we can achieve. - let max_replicas = all.iter().filter(|s| &s.nid != profile.id()).count(); - // If the seeds we specified in the sync settings are all synced. - let is_seeds_synced = settings.seeds.iter().all(|s| synced.contains(s)); - // If we met our desired replica count. Note that this can never exceed the maximum count. - let is_replicas_synced = replicas >= settings.replicas.lower_bound().min(max_replicas); - // Nothing to do if we've met our sync state. - if is_seeds_synced && is_replicas_synced { - term::success!( - &mut reporting.completion; - "Nothing to announce, already in sync with {replicas} node(s) (see `rad sync status`)" + let settings = settings.with_profile(profile); + let n_preferred_seeds = settings.seeds.len(); + + let config = match sync::PrivateNetwork::private_repo(&doc) { + None => { + let (synced, unsynced) = node.seeds(rid)?.iter().fold( + (BTreeSet::new(), BTreeSet::new()), + |(mut synced, mut unsynced), seed| { + if seed.is_synced() { + synced.insert(seed.nid); + } else { + unsynced.insert(seed.nid); + } + (synced, unsynced) + }, ); - return Ok(AnnounceResult::default()); + sync::AnnouncerConfig::public(*me, settings.replicas, settings.seeds, synced, unsynced) + } + Some(network) => { + let sessions = node.sessions()?; + let network = + network.restrict(|nid| sessions.iter().any(|s| s.nid == *nid && s.is_connected())); + sync::AnnouncerConfig::private(*me, settings.replicas, network) } - // Return nodes we can announce to. They don't have to be connected directly. - all.iter() - .filter(|s| !s.is_synced() && &s.nid != profile.id()) - .map(|s| s.nid) - .collect() - } else { - node.sessions()? - .into_iter() - .filter(|s| s.state.is_connected() && doc.is_visible_to(&s.nid.into())) - .map(|s| s.nid) - .collect() }; - - if unsynced.is_empty() { - term::info!(&mut reporting.completion; "No seeds to announce to for {rid}. (see `rad sync status`)"); - return Ok(AnnounceResult::default()); - } - // Cap the replicas to the maximum achievable. - // Nb. It's impossible to know if a replica follows our node. This means that if we announce - // only our refs, and the replica doesn't follow us, it won't fetch from us. - settings.replicas = settings.replicas.min(unsynced.len()); - + let announcer = match sync::Announcer::new(config) { + Ok(announcer) => announcer, + Err(err) => match err { + sync::AnnouncerError::AlreadySynced(result) => { + term::success!( + &mut reporting.completion; + "Nothing to announce, already in sync with {} seed(s) (see `rad sync status`)", + term::format::positive(result.synced()), + ); + return Ok(None); + } + sync::AnnouncerError::NoSeeds => { + term::info!( + &mut reporting.completion; + "{}", + term::format::yellow("No seeds found for {rid}.") + ); + return Ok(None); + } + sync::AnnouncerError::Target(err) => return Err(err.into()), + }, + }; + let target = announcer.target(); + let min_replicas = target.replicas().lower_bound(); let mut spinner = term::spinner_to( - format!("Found {} seed(s)..", unsynced.len()), + format!("Found {} seed(s)..", announcer.progress().unsynced()), reporting.completion.clone(), reporting.progress.clone(), ); - let result = node.announce( - rid, - unsynced, - settings.timeout, - |event, replicas| match event { - node::AnnounceEvent::Announced => ControlFlow::Continue(()), - node::AnnounceEvent::RefsSynced { remote, time } => { - spinner.message(format!( - "Synced with {} in {}..", - format::dim(remote), - format::dim(format!("{time:?}")) - )); - // We're done syncing when both of these conditions are met: - // - // 1. We've matched or exceeded our target replica count. - // 2. We've synced with one of the seeds specified manually. - if replicas.len() >= settings.replicas.lower_bound() - && (settings.seeds.is_empty() - || settings.seeds.iter().any(|s| replicas.contains_key(s))) - { - ControlFlow::Break(()) - } else { - ControlFlow::Continue(()) - } - } - }, - )?; - - if result.synced.is_empty() { - spinner.failed(); - } else { - spinner.message(format!("Synced with {} node(s)", result.synced.len())); - spinner.finish(); - - if reporting.debug { - for (seed, time) in &result.synced { - writeln!( - &mut reporting.completion, - " {}", - term::format::dim(format!("Synced with {seed} in {time:?}")), - ) - .ok(); - } + match node.announce(rid, settings.timeout, announcer, |node, progress| { + spinner.message(format!( + "Synced with {}, {} of {} preferred seeds, and {} of at least {} replica(s).", + term::format::node(node), + term::format::secondary(progress.preferred()), + term::format::secondary(n_preferred_seeds), + term::format::secondary(progress.synced()), + term::format::secondary(min_replicas), + )); + }) { + Ok(result) => { + spinner.message(format!( + "Synced with {} seed(s)", + term::format::positive(result.synced().len()) + )); + spinner.finish(); + Ok(Some(result)) + } + Err(err) => { + spinner.error("Sync failed: {err}"); + Err(err.into()) } } - for seed in &result.timed_out { - if settings.seeds.contains(seed) { - term::notice!(&mut reporting.completion; "Seed {seed} timed out.."); - } - } - if result.synced.is_empty() { - return Err(SyncError::AllSeedsTimedOut); - } - Ok(result) } diff --git a/radicle-cli/tests/commands.rs b/radicle-cli/tests/commands.rs index 8edc3c2d..83ebf1f3 100644 --- a/radicle-cli/tests/commands.rs +++ b/radicle-cli/tests/commands.rs @@ -553,8 +553,8 @@ fn rad_id_threshold() { .follow(seed.id, Some(Alias::new("seed"))) .unwrap(); - alice.connect(&seed); - bob.connect(&seed).connect(&alice); + alice.connect(&seed).connect(&bob); + bob.connect(&seed); alice.routes_to(&[(acme, seed.id)]); seed.handle.fetch(acme, alice.id, DEFAULT_TIMEOUT).unwrap(); diff --git a/radicle-remote-helper/src/push.rs b/radicle-remote-helper/src/push.rs index 7459f6e6..41d1785c 100644 --- a/radicle-remote-helper/src/push.rs +++ b/radicle-remote-helper/src/push.rs @@ -878,18 +878,20 @@ fn sync( let mut urls = Vec::new(); - for seed in profile.config.preferred_seeds.iter() { - if result.synced(&seed.id).is_some() { - for resource in updated { - let url = profile - .config - .public_explorer - .url(seed.addr.host.clone(), repo.id) - .resource(resource); + if let Some(result) = result { + for seed in profile.config.preferred_seeds.iter() { + if result.is_synced(&seed.id) { + for resource in updated { + let url = profile + .config + .public_explorer + .url(seed.addr.host.clone(), repo.id) + .resource(resource); - urls.push(url); + urls.push(url); + } + break; } - break; } } diff --git a/radicle/src/node.rs b/radicle/src/node.rs index 38c6821a..2037d452 100644 --- a/radicle/src/node.rs +++ b/radicle/src/node.rs @@ -817,37 +817,6 @@ impl From> for Seeds { } } -/// Announcement result returned by [`Node::announce`]. -#[derive(Debug, Default)] -pub struct AnnounceResult { - /// Nodes that timed out. - pub timed_out: Vec, - /// Nodes that synced. - pub synced: Vec<(NodeId, time::Duration)>, -} - -impl AnnounceResult { - /// Check if a node synced successfully. - pub fn synced(&self, nid: &NodeId) -> Option { - self.synced - .iter() - .find(|(id, _)| id == nid) - .map(|(_, time)| *time) - } -} - -/// A sync event, emitted by [`Node::announce`]. -#[derive(Debug)] -pub enum AnnounceEvent { - /// Refs were synced with the given node. - RefsSynced { - remote: NodeId, - time: time::Duration, - }, - /// Refs were announced to all given nodes. - Announced, -} - #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(tag = "status", rename_all = "camelCase")] pub enum FetchResult { @@ -1161,25 +1130,24 @@ impl Node { pub fn announce( &mut self, rid: RepoId, - seeds: impl IntoIterator, timeout: time::Duration, - mut callback: impl FnMut(AnnounceEvent, &HashMap) -> ControlFlow<()>, - ) -> Result { - let events = self.subscribe(timeout)?; + mut announcer: sync::Announcer, + mut report: impl FnMut(&NodeId, sync::announce::Progress), + ) -> Result { + let mut events = self.subscribe(timeout)?; let refs = self.announce_refs(rid)?; - let mut unsynced = seeds.into_iter().collect::>(); - let mut synced = HashMap::new(); - let mut timed_out: Vec = Vec::new(); let started = time::Instant::now(); - callback(AnnounceEvent::Announced, &synced); - - for e in events { + loop { + let Some(e) = events.next() else { + // Consider the announcement as timed out if there are no more + // events + return Ok(announcer.timed_out()); + }; let elapsed = started.elapsed(); if elapsed >= timeout { - timed_out.extend(unsynced.iter()); - break; + return Ok(announcer.timed_out()); } match e { Ok(Event::RefsSynced { @@ -1188,37 +1156,29 @@ impl Node { at, }) if rid == rid_ && refs.at == at => { log::debug!(target: "radicle", "Received {e:?}"); - - unsynced.remove(&remote); - // We can receive synced events from nodes we didn't directly announce to, - // and it's possible to receive duplicates as well. - if synced.insert(remote, elapsed).is_none() { - let event = AnnounceEvent::RefsSynced { - remote, - time: elapsed, - }; - if callback(event, &synced).is_break() { - break; + match announcer.synced_with(remote, elapsed) { + ControlFlow::Continue(progress) => { + report(&remote, progress); + } + ControlFlow::Break(finished) => { + return Ok(finished.into()); } } } Ok(_) => {} Err(Error::TimedOut) => { - timed_out.extend(unsynced.iter()); - break; + return Ok(announcer.timed_out()); } Err(e) => return Err(e), } - if unsynced.is_empty() { - break; - } + // Ensure that the announcer is still waiting for nodes to be + // in-sync with + announcer = match announcer.can_continue() { + ControlFlow::Continue(cont) => cont, + ControlFlow::Break(finished) => return Ok(finished.into()), + }; } - - Ok(AnnounceResult { - timed_out, - synced: synced.into_iter().collect(), - }) } } diff --git a/radicle/src/node/sync.rs b/radicle/src/node/sync.rs index 5e42f2a5..23130bda 100644 --- a/radicle/src/node/sync.rs +++ b/radicle/src/node/sync.rs @@ -1,3 +1,6 @@ +pub mod announce; +pub use announce::{Announcer, AnnouncerConfig, AnnouncerError, AnnouncerResult}; + pub mod fetch; pub use fetch::{Fetcher, FetcherConfig, FetcherError, FetcherResult}; diff --git a/radicle/src/node/sync/announce.rs b/radicle/src/node/sync/announce.rs new file mode 100644 index 00000000..6b0b396b --- /dev/null +++ b/radicle/src/node/sync/announce.rs @@ -0,0 +1,464 @@ +use std::{ + collections::{BTreeMap, BTreeSet}, + ops::ControlFlow, + time, +}; + +use crate::node::NodeId; + +use super::{PrivateNetwork, ReplicationFactor}; + +pub struct Announcer { + local_node: NodeId, + target: Target, + synced: BTreeMap, + to_sync: BTreeSet, +} + +impl Announcer { + /// Construct a new [`Announcer`] from the [`AnnouncerConfig`]. + /// + /// This will ensure that the local [`NodeId`], provided in the + /// [`AnnouncerConfig`], will be removed from all sets. + /// + /// # Errors + /// + /// Returns the following errors: + /// + /// - [`AnnouncerError::NoSeeds`]: both sets of already synchronized and + /// un-synchronized nodes were empty + /// of nodes were empty + /// - [`AnnouncerError::AlreadySynced`]: no more nodes are available for + /// synchronizing with + /// - [`AnnouncerError::Target`]: the target has no preferred seeds and no + /// replicas + pub fn new(mut config: AnnouncerConfig) -> Result { + // N.b. ensure that local node is none of the sets + config.preferred_seeds.remove(&config.local_node); + config.synced.remove(&config.local_node); + config.unsynced.remove(&config.local_node); + + if config.synced.is_empty() && config.unsynced.is_empty() { + return Err(AnnouncerError::NoSeeds); + } + + if config.unsynced.is_empty() { + let preferred = config.synced.intersection(&config.preferred_seeds).count(); + return Err(AlreadySynced { + preferred, + synced: config.synced.len(), + } + .into()); + } + + let replicas = config.replicas.min(config.unsynced.len()); + let announcer = Self { + local_node: config.local_node, + target: Target::new(config.preferred_seeds, replicas) + .map_err(AnnouncerError::Target)?, + synced: config + .synced + .into_iter() + .map(|nid| (nid, SyncStatus::AlreadySynced)) + .collect(), + to_sync: config.unsynced, + }; + match announcer.is_target_reached() { + None => Ok(announcer), + Some(outcome) => match outcome { + SuccessfulOutcome::MinReplicationFactor { preferred, synced } => { + Err(AlreadySynced { preferred, synced }.into()) + } + SuccessfulOutcome::MaxReplicationFactor { preferred, synced } => { + Err(AlreadySynced { preferred, synced }.into()) + } + }, + } + } + + /// Mark the `node` as synchronized, with the given `duration` it took to + /// synchronize with. + /// + /// If the target for the [`Announcer`] has been reached, then a [`Success`] is + /// returned via [`ControlFlow::Break`]. Otherwise, [`Progress`] is returned + /// via [`ControlFlow::Continue`]. + /// + /// The caller decides whether they wish to continue the announcement process. + pub fn synced_with( + &mut self, + node: NodeId, + duration: time::Duration, + ) -> ControlFlow { + if node == self.local_node { + return ControlFlow::Continue(self.progress()); + } + self.to_sync.remove(&node); + self.synced.insert(node, SyncStatus::Synced { duration }); + self.finished() + } + + /// Complete the [`Announcer`] process returning a [`AnnouncerResult`]. + /// + /// If the target for the [`Announcer`] has been reached, then the result + /// will be [`AnnouncerResult::Success`], otherwise, it will be + /// [`AnnouncerResult::TimedOut`]. + pub fn timed_out(self) -> AnnouncerResult { + match self.is_target_reached() { + None => TimedOut { + synced: self.synced, + timed_out: self.to_sync, + } + .into(), + Some(outcome) => Success { + outcome, + synced: self.synced, + } + .into(), + } + } + + /// Check if the [`Announcer`] can continue synchronizing with more nodes. + /// If there are no more nodes, then [`NoNodes`] is returned in the + /// [`ControlFlow::Break`], otherwise the [`Announcer`] is returned as-is in + /// the [`ControlFlow::Continue`]. + pub fn can_continue(self) -> ControlFlow { + if self.to_sync.is_empty() { + ControlFlow::Break(NoNodes { + synced: self.synced, + }) + } else { + ControlFlow::Continue(self) + } + } + + /// Get all the nodes to be synchronized with. + pub fn to_sync(&self) -> BTreeSet { + self.to_sync + .iter() + .filter(|node| *node != &self.local_node) + .copied() + .collect() + } + + /// Get the [`Target`] of the [`Announcer`]. + pub fn target(&self) -> &Target { + &self.target + } + + /// Get the [`Progress`] of the [`Announcer`]. + pub fn progress(&self) -> Progress { + let (synced, preferred) = self.success_counts(); + let unsynced = self.to_sync.len().saturating_sub(synced); + Progress { + preferred, + synced, + unsynced, + } + } + + fn finished(&self) -> ControlFlow { + let progress = self.progress(); + self.is_target_reached() + .map_or(ControlFlow::Continue(progress), |outcome| { + ControlFlow::Break(Success { + outcome, + synced: self.synced.clone(), + }) + }) + } + + fn is_target_reached(&self) -> Option { + let (preferred, synced) = self.success_counts(); + let reached_preferred = self.target.preferred_seeds.is_empty() + || preferred >= self.target.preferred_seeds.len(); + + let replicas = self.target.replicas(); + let min = replicas.lower_bound(); + match replicas.upper_bound() { + None => (reached_preferred && synced >= min) + .then_some(SuccessfulOutcome::MinReplicationFactor { preferred, synced }), + Some(max) => (reached_preferred && synced >= max) + .then_some(SuccessfulOutcome::MaxReplicationFactor { preferred, synced }), + } + } + + fn success_counts(&self) -> (usize, usize) { + self.synced + .keys() + .fold((0, 0), |(mut preferred, mut succeeded), nid| { + succeeded += 1; + if self.target.preferred_seeds.contains(nid) { + preferred += 1; + } + (preferred, succeeded) + }) + } +} + +/// Configuration of the [`Announcer`]. +pub struct AnnouncerConfig { + local_node: NodeId, + replicas: ReplicationFactor, + preferred_seeds: BTreeSet, + synced: BTreeSet, + unsynced: BTreeSet, +} + +impl AnnouncerConfig { + /// Setup a private network `AnnouncerConfig`, populating the + /// [`AnnouncerConfig`]'s preferred seeds with the allowed set from the + /// [`PrivateNetwork`]. + /// + /// `replicas` is the target number of seeds the [`Announcer`] should reach + /// before stopping. + /// + /// `local` is the [`NodeId`] of the local node, to ensure it is + /// excluded from the [`Announcer`] process. + pub fn private(local: NodeId, replicas: ReplicationFactor, network: PrivateNetwork) -> Self { + AnnouncerConfig { + local_node: local, + replicas, + preferred_seeds: network.allowed.clone(), + synced: BTreeSet::new(), + unsynced: network.allowed, + } + } + + /// Setup a public `AnnouncerConfig`. + /// + /// `preferred_seeds` is the target set of preferred seeds that [`Announcer`] should + /// attempt to synchronize with. + /// + /// `synced` and `unsynced` are the set of nodes that are currently + /// synchronized and un-synchronized with, respectively. + /// + /// `replicas` is the target number of seeds the [`Announcer`] should reach + /// before stopping. + /// + /// `local` is the [`NodeId`] of the local node, to ensure it is + /// excluded from the [`Announcer`] process. + pub fn public( + local: NodeId, + replicas: ReplicationFactor, + preferred_seeds: BTreeSet, + synced: BTreeSet, + unsynced: BTreeSet, + ) -> Self { + Self { + local_node: local, + replicas, + preferred_seeds, + synced, + unsynced, + } + } +} + +/// Result of running an [`Announcer`] process. +pub enum AnnouncerResult { + /// The target of the [`Announcer`] was successfully met. + Success(Success), + /// The [`Announcer`] process was timed out, and all un-synchronized nodes + /// are marked as timed out. + /// + /// Note that some nodes still may have synchronized. + TimedOut(TimedOut), + /// The [`Announcer`] ran out of nodes to synchronize with. + /// + /// Note that some nodes still may have synchronized. + NoNodes(NoNodes), +} + +impl AnnouncerResult { + /// Get the synchronized nodes, regardless of the result. + pub fn synced(&self) -> &BTreeMap { + match self { + AnnouncerResult::Success(Success { synced, .. }) => synced, + AnnouncerResult::TimedOut(TimedOut { synced, .. }) => synced, + AnnouncerResult::NoNodes(NoNodes { synced }) => synced, + } + } + + /// Check if a given node is synchronized with. + pub fn is_synced(&self, node: &NodeId) -> bool { + let synced = self.synced(); + synced.contains_key(node) + } +} + +impl From for AnnouncerResult { + fn from(s: Success) -> Self { + Self::Success(s) + } +} + +impl From for AnnouncerResult { + fn from(to: TimedOut) -> Self { + Self::TimedOut(to) + } +} + +impl From for AnnouncerResult { + fn from(no: NoNodes) -> Self { + Self::NoNodes(no) + } +} + +pub struct NoNodes { + synced: BTreeMap, +} + +impl NoNodes { + /// Get the set of synchronized nodes + pub fn synced(&self) -> &BTreeMap { + &self.synced + } +} + +pub struct TimedOut { + synced: BTreeMap, + timed_out: BTreeSet, +} + +impl TimedOut { + /// Get the set of synchronized nodes + pub fn synced(&self) -> &BTreeMap { + &self.synced + } + + /// Get the set of timed out nodes + pub fn timed_out(&self) -> &BTreeSet { + &self.timed_out + } +} + +pub struct Success { + outcome: SuccessfulOutcome, + synced: BTreeMap, +} + +impl Success { + /// Get the [`SuccessfulOutcome`] of the success. + pub fn outcome(&self) -> SuccessfulOutcome { + self.outcome + } + + /// Get the set of synchronized nodes. + pub fn synced(&self) -> &BTreeMap { + &self.synced + } +} + +/// Error in constructing the [`Announcer`]. +pub enum AnnouncerError { + /// Both sets of already synchronized and un-synchronized nodes were empty + /// of nodes were empty. + AlreadySynced(AlreadySynced), + /// No more nodes are available for synchronizing with. + NoSeeds, + /// The target could not be constructed. + Target(TargetError), +} + +impl From for AnnouncerError { + fn from(value: AlreadySynced) -> Self { + Self::AlreadySynced(value) + } +} + +pub struct AlreadySynced { + preferred: usize, + synced: usize, +} + +impl AlreadySynced { + /// Get the number of preferred nodes that are already synchronized. + pub fn preferred(&self) -> usize { + self.preferred + } + + /// Get the total number of nodes that are already synchronized. + pub fn synced(&self) -> usize { + self.synced + } +} + +/// The status of the synchronized node. +#[derive(Clone, Copy, Debug)] +pub enum SyncStatus { + /// The node was already synchronized before starting the [`Announcer`] + /// process. + AlreadySynced, + /// The node was synchronized as part of the [`Announcer`] process, marking + /// the amount of time that passed to synchronize with the node. + Synced { duration: time::Duration }, +} + +/// Progress of the [`Announcer`] process. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Progress { + preferred: usize, + synced: usize, + unsynced: usize, +} + +impl Progress { + /// The number of preferred seeds that are synchronized. + pub fn preferred(&self) -> usize { + self.preferred + } + + /// The number of seeds that are synchronized. + pub fn synced(&self) -> usize { + self.synced + } + + /// The number of seeds that are un-synchronized. + pub fn unsynced(&self) -> usize { + self.unsynced + } +} + +#[derive(Debug, thiserror::Error)] +#[non_exhaustive] +#[error("a minimum number of replicas or set of preferred seeds must be provided")] +pub struct TargetError; + +/// The target for the [`Announcer`] to reach. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Target { + preferred_seeds: BTreeSet, + replicas: ReplicationFactor, +} + +impl Target { + pub fn new( + preferred_seeds: BTreeSet, + replicas: ReplicationFactor, + ) -> Result { + if replicas.lower_bound() == 0 && preferred_seeds.is_empty() { + Err(TargetError) + } else { + Ok(Self { + preferred_seeds, + replicas, + }) + } + } + + /// Get the set of preferred seeds that are trying to be synchronized with. + pub fn preferred_seeds(&self) -> &BTreeSet { + &self.preferred_seeds + } + + /// Get the number of replicas that is trying to be reached. + pub fn replicas(&self) -> &ReplicationFactor { + &self.replicas + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SuccessfulOutcome { + MinReplicationFactor { preferred: usize, synced: usize }, + MaxReplicationFactor { preferred: usize, synced: usize }, +}