feat(lib): join the distributed search network as a leaf

The server hands every client a list of possible parents and expects it to
hang from one so searches also travel peer to peer. Until now we told the
server we were a lone root and left it at that, which the network tolerates
but does not favour.

A leaf now dials each PossibleParents candidate over a D connection, adopts
the first that passes a search down after stating its branch, announces the
new branch root and level to the server, and answers those searches the same
way as ones from the server. Losing the parent or a ResetDistributed puts it
back to a parentless stance. The leaf takes no children.

The Leaf state machine is pure so its rules are unit-tested without sockets;
the e2e test drives a real client from a mock parent, since soulfind never
hands out parents.
This commit is contained in:
Michel de Graaf
2026-09-08 01:45:07 +02:00
parent 1976b49792
commit 7d7b8fe98c
9 changed files with 840 additions and 10 deletions
+79
View File
@@ -17,6 +17,9 @@ impl Client {
own_username: String,
) {
thread::spawn(move || {
if let Ok(mut ctx) = client_context.write_safe() {
ctx.leaf = super::distributed::Leaf::new(&own_username);
}
let mut last_sweep = Instant::now();
loop {
let next = reader.recv_timeout(CONNECT_SWEEP_INTERVAL);
@@ -696,6 +699,82 @@ impl Client {
);
}
}
ClientOperation::PossibleParents(candidates) => {
let (dials, ops) = match client_context.write_safe() {
Ok(mut ctx) => (
ctx.leaf.consider(candidates),
ctx.operations.clone(),
),
Err(_) => continue,
};
let Some(ops) = ops else { continue };
for dial in dials {
super::distributed::spawn_link(
dial,
own_username.clone(),
ops.clone(),
);
}
}
ClientOperation::ParentBranchLevel { parent, level } => {
if let Ok(mut ctx) = client_context.write_safe() {
ctx.leaf.branch_level(&parent, level);
}
}
ClientOperation::ParentBranchRoot { parent, root } => {
if let Ok(mut ctx) = client_context.write_safe() {
ctx.leaf.branch_root(&parent, &root);
}
}
ClientOperation::ParentSearch {
parent,
username,
token,
query,
} => {
let Ok(mut ctx) = client_context.write_safe() else {
continue;
};
if let Some(branch) = ctx.leaf.search_from(&parent) {
super::distributed::announce(
ctx.server_sender.as_ref(),
&branch,
true,
);
}
if ctx.leaf.is_parent(&parent)
&& let Some(ops) = &ctx.operations
{
let _ = ops.send(ClientOperation::IncomingSearch {
username,
token,
query,
});
}
}
ClientOperation::ParentClosed { parent } => {
let Ok(mut ctx) = client_context.write_safe() else {
continue;
};
if ctx.leaf.closed(&parent) {
super::distributed::announce(
ctx.server_sender.as_ref(),
&ctx.leaf.branch(),
false,
);
}
}
ClientOperation::ResetDistributed => {
let Ok(mut ctx) = client_context.write_safe() else {
continue;
};
ctx.leaf.reset();
super::distributed::announce(
ctx.server_sender.as_ref(),
&ctx.leaf.branch(),
false,
);
}
ClientOperation::PrivilegedUsers(users) => {
if let Ok(mut ctx) = client_context.write_safe() {
debug!(