Commit Graph
4 Commits
Author SHA1 Message Date
Michel de Graaf 460031b532 feat(distributed): serve children in the search tree
The client read the distributed network but never carried it: a peer
dialling with a D connection was logged and dropped, and AcceptChildren
was hardcoded false, so this client took from the tree without ever
relaying a search for anyone else.

With accept_children set, peers hang from us: each is told where our
branch sits when it connects and again when the branch moves, every
search that reaches us is passed down, and a child that goes away loses
its slot. Ten at a time, matching Nicotine+, since each costs a socket
and a copy of the network's whole search stream — which is also why it is
opt-in, through the new accept_children config key (ignored without a
listener, since a child would have nothing to dial).
2026-09-09 16:47:55 +00:00
Michel de Graaf 9d6a5aaa98 fix(lib): harden the distributed leaf against hostile and slow parents
Review of the leaf found the seams a stranger on the other end of a D
connection could pull at, and a few that a slow one would.

- A code-93 envelope is unwrapped once, as Nicotine+ does; a frame made
  of nothing but envelopes used to recurse once per byte.
- A branch level is validated before it is used: negative levels leave
  the candidate unadoptable and the largest one no longer overflows.
- The link reads through the crate's MessageReader, so a frame split
  across a read timeout is no longer torn or mistaken for a closed link.
- Every event a link raises carries its link number, so a stale thread
  cannot adopt, close or move a newer link to the same user.
- Later branch facts from the parent move our own place and are told to
  the server; a fresh login resets the leaf, whose stance is now the one
  place the four announcement messages come from.
- Candidates from a later batch join the ones in flight instead of
  cancelling them, capped at ten; searches from the parent are budgeted
  at fifty a second; a reply queued for a searcher the server cannot
  place expires after a minute.
- Truncated frames and half-read PossibleParents entries are dropped
  instead of yielding an empty root, a level of zero or a phantom
  candidate.

The e2e now has a searcher of its own, so the answer must go to whoever
asked and not to the parent that relayed it.
2026-09-08 01:45:07 +02:00
Michel de Graaf 7d7b8fe98c 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.
2026-09-08 01:45:07 +02:00
Michel de Graaf 1976b49792 feat(lib): decode distributed frames and announce a parentless stance
The distributed search network carries searches from parent to child
over connections whose frames have a one-byte code; the library had no
reader for them. This adds the codec for Search, BranchLevel,
BranchRoot and the embedded envelope, with the identifier check that
keeps malformed frames from being answered, and completes the post-login
stance Nicotine+ sends: HaveNoParent, BranchRoot (ourselves),
BranchLevel 0 and AcceptChildren false, in place of HaveNoParent alone.
2026-09-08 01:45:07 +02:00