Coverage Summary for Class: Site (it.unibo.tuprolog.argumentation.parallel.federation)

Class Method, % Branch, % Line, % Instruction, %
Site 92.8% (64/69) 76.8% (175/228) 94.3% (246/261) 93% (2353/2530)
Site$CatalogNetwork 100% (7/7) 75% (9/12) 100% (15/15) 97.1% (200/206)
Site$Companion 100% (2/2) 100% (2/2) 100% (12/12)
Site$Hosted 100% (1/1) 100% (3/3) 100% (12/12)
Site$Opening 100% (2/2) 100% (3/3) 100% (12/12)
Site$Peer 100% (1/1) 100% (6/6) 100% (27/27)
Site$placed$lambda$0$$inlined$sortedBy$1 0% (0/1)
Site$Session 100% (1/1) 100% (4/4) 100% (17/17)
Site$share$$inlined$sortedByDescending$1 0% (0/1)
Site$WorkerNetwork 100% (2/2) 100% (3/3) 100% (15/15)
Total 92% (80/87) 76.7% (184/240) 94.9% (282/297) 93.5% (2648/2831)


 package it.unibo.tuprolog.argumentation.parallel.federation
 
 import it.unibo.tuprolog.argumentation.Flags
 import it.unibo.tuprolog.argumentation.parallel.message.Add
 import it.unibo.tuprolog.argumentation.parallel.message.Address
 import it.unibo.tuprolog.argumentation.parallel.message.AnswerResponse
 import it.unibo.tuprolog.argumentation.parallel.message.Configure
 import it.unibo.tuprolog.argumentation.parallel.message.EvaluationResponse
 import it.unibo.tuprolog.argumentation.parallel.message.KbMessage
 import it.unibo.tuprolog.argumentation.parallel.message.Load
 import it.unibo.tuprolog.argumentation.parallel.message.MessageCodec
 import it.unibo.tuprolog.argumentation.parallel.message.NO_GRAPH
 import it.unibo.tuprolog.argumentation.parallel.message.Rebuild
 import it.unibo.tuprolog.argumentation.parallel.message.RequireAnswer
 import it.unibo.tuprolog.argumentation.parallel.message.RequireEvaluation
 import it.unibo.tuprolog.argumentation.parallel.message.RequireSolution
 import it.unibo.tuprolog.argumentation.parallel.message.Reset
 import it.unibo.tuprolog.argumentation.parallel.message.STOPPED
 import it.unibo.tuprolog.argumentation.parallel.message.SolutionResponse
 import it.unibo.tuprolog.argumentation.parallel.message.Stop
 import it.unibo.tuprolog.argumentation.parallel.node.Catalog
 import it.unibo.tuprolog.argumentation.parallel.node.Network
 import it.unibo.tuprolog.argumentation.parallel.node.Node
 import it.unibo.tuprolog.argumentation.parallel.node.Worker
 import kotlin.concurrent.Volatile
 import kotlin.random.Random
 
 /**
  * An instance of the distributed solver, on any platform: a catalog for a theory of its own, the workers it runs on
  * [scheduler] (at most [workers] for a graph, null for no limit), and its peers, other sites on the links its
  * transports give it (see [Link]), on this machine or another, whatever their platform.
  *
  * The graph of its theory is built by its workers and by those its peers lend (if they do, see [lends]): the theory
  * is split into as many parts as they can run together, or into [parts] if given, the parts placed on the sites in
  * proportion to their workers. The nodes of the site share the terms of their messages, written as text only towards
  * other sites (or always, with [textual]). A graph is built anew if a site running some of its workers leaves while
  * it is built, or a link its messages went through closes; the workers it ran for a site that left are dropped.
  *
  * A site may have delegates, sites working for it alone (the threads of a JS runtime, see [delegate]): it offers
  * their workers with its own, and runs the workers it is given on them as well, its delegates reaching the other
  * sites through it.
  *
  * Sites opening links of their own ([introducible], as browsers over WebRTC) are introduced to each other by the sites
  * they are both linked to, which carry what they need to open the link ([signal]).
  *
  * A site may require a [code] of the sites linking to it ([requiresCode]): one proving it knows it is paired with it,
  * and proves it knows the token of their pairing instead from then on, linked again with no code while the two run;
  * one proving no such secret is refused, and after a few wrong codes from an address its codes are refused for a
  * while. Neither secret goes over a link (see [Pairing]), and a site linking to another with one checks that the
  * other knows it as well. Nothing but the greetings is read on a link before its site is admitted.
  *
  * Each graph built is a session of its site: the workers of a session, wherever they run, only hear from those of the
  * same session, so that nothing of a graph given up reaches the next one.
  *
  * The catalog of the site evaluates the statements of its clients ([solve]) and solves their goals as the standard
  * solver does (see [it.unibo.tuprolog.argumentation.parallel.cluster.ClusterSolver]); [stats] tells what the
  * federation does, as the site knows it.
  */
 class Site(
     private val scheduler: Scheduler,
     private val workers: Int?,
     private val parts: Int? = null,
     flags: Flags = Flags.defaults(),
     private val lends: Boolean = true,
     private val textual: Boolean = false,
     private val introducible: Boolean = false,
     val id: String = newId(),
 ) : LinkListener {
     private data class Peer(
         val link: Link,
         val capacity: Int?,
         val lends: Boolean,
         val delegate: Boolean,
         val introducible: Boolean,
     )
 
     private class Hosted(
         val queue: TaskQueue,
         val node: Node,
     )
 
     // A session known here: the sites of its workers by their addresses, the sites linked to this one when the
     // session began, the only ones its messages go to directly, and the site through which they reach the others (the
     // one that placed the workers here), so that the messages from a node to another always take the same way
     private class Session(
         val workers: Map<String, String>,
         val direct: Set<String>,
         val via: String,
     )
 
     // The state shared by the threads of the site, as immutable values replaced under the lock: the peers by site, the
     // links of each, the links to the delegates, the sessions known and the workers run here (by session and address)
     private val lock = Any()
 
     @Volatile private var peers: Map<String, Peer> = emptyMap()
 
     @Volatile private var links: Map<Link, String> = emptyMap()
 
     @Volatile private var delegateLinks: Set<Link> = emptySet()
 
     @Volatile private var placements: Map<String, Session> = emptyMap()
 
     @Volatile private var hosted: Map<Pair<String, String>, Hosted> = emptyMap()
 
     // The sites this one was introduced to, by the site that introduced them
     @Volatile private var introducers: Map<String, String> = emptyMap()
 
     // The session of the graph of the theory of this site, the one built last
     @Volatile private var session: String = "$id#0"
 
     // The last evaluation the catalog answered
     @Volatile private var evaluation: EvaluationStats? = null
 
     // The code of the site and its pairings, the links this site opened waiting to be greeted (told then, or told why
     // they are refused), and the sites heard on the local network
     private val pairing = Pairing()
     private val greetings = mutableMapOf<Link, (refusal: String?) -> Unit>()
 
     // The links open, each with the nonce this site gave on it (for the other to prove its secret on) and whether it
     // greeted the other site on it
     private class Opening(
         val nonce: String,
     ) {
         var greeted = false
     }
 
     private val openings = mutableMapOf<Link, Opening>()
 
     @Volatile private var heard: List<NearbySite> = emptyList()
 
     private var sessions = 0
 
     // The clients waiting for an answer: how each is given it, and what it is given if stopped
     private val clients = mutableMapOf<String, Pair<(KbMessage) -> Unit, KbMessage>>()
     private var clientIds = 0
     private var runIds = 0
 
     private val catalogQueue = scheduler.queue()
 
     // made at its first message: a site may only run workers for its peers
     private val catalog by lazy { Catalog(CatalogNetwork(), parts, flags) }
 
     /** The sites this one is federated with, its delegates included */
     val peerSites: Set<String> get() = peers.keys
 
     /** The name of the site, as its beacons tell the sites nearby (see [Beacons]) */
     @Volatile var name: String = ""
 
     /**
      * Whether the sites linking to this one must present its [code], or the token of a pairing made with it: refused
      * otherwise. Its delegates and the links it opens are not checked.
      */
     @Volatile var requiresCode: Boolean = false
 
     /**
      * The code the sites linking to this one prove they know, if it requires one: four digits, random, renewed once a
      * site pairs with this one
      */
     val code: String get() = pairing.code
 
     /** A new [code], the one before refused from now on (the pairings made stay) */
     fun newCode(): String = pairing.renew()
 
     /** The sites paired with this one (one proved the code of the other): linked again with no code while they run */
     val pairedSites: Set<String> get() = pairing.paired
 
     /** The sites heard on the local network (see [Beacons]) that are not peers of this one */
     val nearby: List<NearbySite> get() = heard.filter { it.id !in peers }
 
     /** Called whenever the sites heard on the local network change */
     var onNearbyChanged: (() -> Unit)? = null
 
     // The sites heard on the local network, now
     internal fun heard(sites: List<NearbySite>) {
         if (sites == heard) return
         heard = sites
         onNearbyChanged?.invoke()
     }
 
     // The token of the pairing with [site], to present linking to it again
     internal fun tokenOf(site: String): String? = pairing.tokenOf(site)
 
     // [link], opened by this site, told [done] once the site at its other end greets this one (null), refuses it or
     // closes (why)
     internal fun greeting(
         link: Link,
         done: (refusal: String?) -> Unit,
     ) = locked(lock) { greetings[link] = done }
 
     // [link] no longer waited for (see [greeting]): its waiter gave up
     internal fun forgetGreeting(link: Link) = locked(lock) { greetings.remove(link) }
 
     // Whether the site at the other end of [link] greeted this one, and was admitted
     internal fun admitted(link: Link): Boolean = link in links
 
     // The workers this site runs, for the tests
     internal val workersRun: Int get() = hosted.size
 
     // The arguments of the graph of the theory of this site, for the tests
     internal val argumentsBuilt: Int get() = catalog.argumentsBuilt
 
     internal val graphArguments: List<it.unibo.tuprolog.core.Term> get() = catalog.graphArguments
 
     /**
      * What the federation does, as this site knows it: itself, its peers, the sites it reaches through them (those
      * introduced to it, and those running workers of a graph it runs some of), and the last evaluation it answered
      */
     val stats: FederationStats
         get() {
             val linked = peers
             val reached =
                 introducers.toList() +
                     placements.flatMap { (session, known) -> (known.workers.values + ownerOf(session)).map { it to known.via } }
             val through = reached.filter { (site, via) -> site != id && site !in linked && via != id }.toMap()
             val sites =
                 listOf(SiteStats(id, offered, delegate = false, via = null)) +
                     linked.map { (site, peer) -> SiteStats(site, peer.capacity, peer.delegate, via = null) } +
                     through.map { (site, via) -> SiteStats(site, workers = null, delegate = false, via = via) }
             return FederationStats(id, sites, evaluation, nearby)
         }
 
     /** Called whenever a site joins or leaves the peers of this one */
     var onPeersChanged: (() -> Unit)? = null
 
     // Called with a site opening links of its own this one is introduced to, if it opens such links as well: by the
     // transport opening those links (see RtcLinks)
     internal var onIntroduced: ((site: String) -> Unit)? = null
 
     // Called with what a site sent this one to open a link of their own (see [signal]), by the same transport
     internal var onSignal: ((from: String, payload: String) -> Unit)? = null
 
     /** Sends [payload] to [to], a peer or a site this one was introduced to, to open a link of their own */
     fun signal(
         to: String,
         payload: String,
     ) {
         val next = if (to in peers) to else introducers[to]
         next?.let { peers[it]?.link?.send(FrameCodec.encode(Frame.Signal(id, to, payload))) }
     }
 
     /** The flags of the solver of the catalog: those set apply from the next evaluation on, the graph built anew */
     var flags: Flags = flags
         set(value) {
             field = value
             toCatalog(Configure(value.toString()))
         }
 
     /** Replaces the theory with [theory], as Prolog text */
     fun load(theory: String) = toCatalog(Load(theory))
 
     /** Adds [clause], as Prolog text, to the theory */
     fun add(clause: String) = toCatalog(Add(clause))
 
     /** Forgets the theory */
     fun reset() = toCatalog(Reset)
 
     /** Evaluates [statement], as Prolog text: [reply] is given the arguments for it by label */
     fun solve(
         statement: String,
         reply: (EvaluationResponse) -> Unit,
     ) = ask({ RequireEvaluation(statement, it) }, EvaluationResponse(emptyList(), emptyList(), emptyList(), STOPPED)) {
         reply(it as EvaluationResponse)
     }
 
     // Solves [goal], Prolog text, as the standard solver does, in a run of its own: [reply] is given its first solution
     internal fun query(
         goal: String,
         reply: (SolutionResponse) -> Unit,
     ) {
         val run = locked(lock) { "run_${runIds++}" }
         ask({ RequireSolution(run, goal, it) }, stopped(run)) { reply(it as SolutionResponse) }
     }
 
     // The next solution of the goal solved in [run]
     internal fun next(
         run: String,
         reply: (SolutionResponse) -> Unit,
     ) = ask({ RequireSolution(run, null, it) }, stopped(run)) { reply(it as SolutionResponse) }
 
     // The statements for [query], Prolog text, by label, as the standard solver answers it
     internal fun answer(
         query: String,
         reply: (AnswerResponse) -> Unit,
     ) = ask({ RequireAnswer(query, it) }, AnswerResponse(null, STOPPED)) { reply(it as AnswerResponse) }
 
     /**
      * Stops what the catalog evaluates for the clients of this site: their evaluations end at once, as stopped, and
      * the graph being built for them is given up (the resolution under way interrupted, where it can be); the next
      * ones are answered as before. Nothing is done if none is under way.
      */
     fun stop() {
         val stopped = locked(lock) { clients.values.toList().also { clients.clear() } }
         if (stopped.isEmpty()) return
         catalogQueue.interrupt()
         toCatalog(Stop)
         stopped.forEach { (reply, answer) -> reply(answer) }
     }
 
     // The catalog asked [request], for a client of this site given its answer ([stopped] if stopped)
     private fun ask(
         request: (client: String) -> KbMessage,
         stopped: KbMessage,
         reply: (KbMessage) -> Unit,
     ) {
         val client = locked(lock) { "client_${clientIds++}".also { clients[it] = reply to stopped } }
         toCatalog(request(client))
     }
 
     // What a client solving a goal in [run] is given if stopped
     private fun stopped(run: String) = SolutionResponse(run, null, NO_GRAPH, false, "", stopped = true)
 
     /** [link], just opened, leads to a delegate of this site: the site offers its workers, and runs workers on them */
     fun delegate(link: Link) {
         locked(lock) { delegateLinks = delegateLinks + link }
         opened(link)
     }
 
     // Every link begins with the greetings of its ends: on a link to a delegate, this site greets the other at once; on
     // a link it opened, once the other spoke first (challenging it, or greeting it); on another link, it challenges the
     // other first, with a nonce to prove its secret on (see [Pairing]), and greets it once greeted
     override fun opened(link: Link) {
         val nonce = locked(lock) { openingOf(link).nonce }
         when {
             link in delegateLinks -> greet(link)
             link.outgoing || link in links -> Unit
             else -> link.send(FrameCodec.encode(Frame.Challenge(nonce)))
         }
     }
 
     // Whether a site linking to this one on [link] must prove it knows its code
     private fun guarded(link: Link) = requiresCode && !link.outgoing && link !in delegateLinks
 
     // The opening of [link], made at its first frame if that comes before the link is told open (as on a link in memory)
     private fun openingOf(link: Link): Opening = openings.getOrPut(link) { Opening(Pairing.newNonce()) }
 
     // A frame that is none (a malformed one, or one of another version) closes its link alone, as one coming before the
     // greetings of its site
     override fun received(
         link: Link,
         frame: String,
     ) {
         val decoded = runCatching { FrameCodec.decode(frame) }.getOrElse { return link.close() }
         if (link !in links) return greeting(link, decoded)
         when (decoded) {
             is Frame.Hello -> greetedAgain(link, decoded)
             is Frame.Challenge, is Frame.Refused -> Unit
             is Frame.Placement -> placed(link, decoded)
             is Frame.Forget -> locked(lock) { forget(decoded.site) }
             is Frame.Introduce ->
                 if (introducible && decoded.site != id && decoded.site !in peers) {
                     links[link]?.let { introducer -> locked(lock) { introducers = introducers + (decoded.site to introducer) } }
                     onIntroduced?.invoke(decoded.site)
                 }
             is Frame.Signal ->
                 if (decoded.to == id) {
                     onSignal?.invoke(decoded.from, decoded.payload)
                 } else {
                     peers[decoded.to]?.link?.send(frame)
                 }
             is Frame.Lost ->
                 if (ownerOf(decoded.session) != id) {
                     forward(decoded.session, ownerOf(decoded.session), frame)
                 } else if (decoded.session == session) {
                     toCatalogOf(decoded.session, Rebuild)
                 }
             is Frame.Deliver -> {
                 val (session, to) = decoded
                 val site = siteOf(session, to) ?: return
                 when {
                     // a message between sites with no link between them, or to a delegate
                     site != id -> forward(session, site, frame)
                     to == Address.CATALOG -> message(link, decoded.message)?.let { toCatalogOf(session, it) }
                     else ->
                         worker(session, to)?.let { worker ->
                             // read on the thread of the worker, as reading its terms takes a while
                             worker.queue.post { message(link, decoded.message)?.let { worker.node.receive(it) } }
                         }
                 }
             }
         }
     }
 
     // The message written in [text], from the other end of [link]: none if it is not one, the link closed then
     private fun message(
         link: Link,
         text: String,
     ): KbMessage? = runCatching { MessageCodec.decode(text) }.getOrElse { null.also { link.close() } }
 
     // A frame on [link] before its site is admitted: its greetings alone, any other closing it
     private fun greeting(
         link: Link,
         frame: Frame,
     ) = when (frame) {
         is Frame.Challenge -> greet(link, proof = link.code?.let { Pairing.proof(it, id, frame.nonce) })
         is Frame.Hello -> greeted(link, frame)
         is Frame.Refused -> refused(link, frame.reason)
         else -> link.close()
     }
 
     // This site greeting the other on [link], once: with the proof of its secret on the nonce of the other, and the
     // token of the pairing made just now with it
     private fun greet(
         link: Link,
         proof: String? = null,
         token: String? = null,
     ) {
         val nonce = locked(lock) { openingOf(link).takeUnless { it.greeted }?.also { it.greeted = true }?.nonce } ?: return
         link.send(hello(link, nonce, proof, token))
     }
 
     // [link], opened by this site, refused (why told to its waiter, see [greeting]), and closed
     private fun refused(
         link: Link,
         reason: String,
     ) {
         locked(lock) { greetings.remove(link) }?.invoke(reason)
         link.close()
     }
 
     override fun closed(link: Link) {
         locked(lock) { greetings.remove(link) }?.invoke("The link closed")
         val (lost, broken) =
             locked(lock) {
                 val site = links[link]
                 links = links - link
                 openings -= link
                 delegateLinks = delegateLinks - link
                 val replacement = links.entries.firstOrNull { it.value == site }?.key
                 when {
                     site == null || peers[site]?.link != link -> null to emptyList()
                     replacement != null -> {
                         peers = peers + (site to peers.getValue(site).copy(link = replacement))
                         null to emptyList()
                     }
                     else -> {
                         val peer = peers.getValue(site)
                         peers = peers - site
                         forget(site)
                         // the sessions whose messages went to it directly: some may be lost
                         (site to peer.delegate) to placements.filterValues { site in it.direct }.keys.toList()
                     }
                 }
             }
         val (site, delegate) = lost ?: return
         broken.forEach { session ->
             if (ownerOf(session) == id) {
                 toCatalogOf(session, Rebuild)
             } else {
                 forward(session, ownerOf(session), FrameCodec.encode(Frame.Lost(session)))
             }
         }
         // the delegates forget the workers they ran for it, the other sites learn of the workers offered now
         if (delegate) announce() else toDelegates(FrameCodec.encode(Frame.Forget(site)))
         onPeersChanged?.invoke()
     }
 
     // The greeting of the site at the other end of [link], not admitted yet: refused if it is this very site or of
     // another version. On a link this site opened presenting a secret (a code, or the token of a pairing), the other
     // site proves it knows it as well, its token remembered if it gave one, or the link is closed (a site claiming the
     // identifier of another, say); on another link, a site that did not prove it knows the code this one requires, or
     // the token of their pairing, is refused (and told). The site admitted is greeted in turn, with the proof of the
     // secret it proved, on its nonce: a peer then, or a second link to a peer, kept in case the first closes.
     private fun greeted(
         link: Link,
         hello: Frame.Hello,
     ) {
         if (hello.site == id || hello.version != VERSION) return link.close()
         val nonce = locked(lock) { openingOf(link).nonce }
         val secret = link.code.takeIf { link.outgoing }
         val proven =
             when {
                 secret != null -> {
                     if (!Pairing.proves(
                             hello.proof,
                             secret,
                             hello.site,
                             nonce,
                         )
                     ) {
                         return refused(link, "${hello.site} did not prove the code")
                     }
                     hello.token?.let { pairing.remember(hello.site, it) }
                     null
                 }
                 link.outgoing -> null
                 guarded(link) ->
                     when (val admission = pairing.admission(hello.site, link.address, nonce, hello.proof)) {
                         is Pairing.Refusal -> {
                             link.send(FrameCodec.encode(Frame.Refused(admission.reason)))
                             return link.close()
                         }
                         is Pairing.Proven -> admission
                     }
                 else -> hello.proof?.let { pairing.proven(hello.site, nonce, it) }
             }
         val joined =
             locked(lock) {
                 links = links + (link to hello.site)
                 (hello.site !in peers).also { joining ->
                     val delegate = link in delegateLinks
                     if (joining) peers = peers + (hello.site to Peer(link, hello.capacity, hello.lends, delegate, hello.introducible))
                 }
             }
         greet(link, proven?.let { known -> hello.nonce?.let { Pairing.proof(known.secret, id, it) } }, proven?.token)
         locked(lock) { greetings.remove(link) }?.invoke(null)
         if (joined) {
             if (peers[hello.site]?.delegate == true) announce()
             if (hello.introducible) introduce(hello.site)
             onPeersChanged?.invoke()
         }
     }
 
     // The greeting of a peer again on [link]: the workers it offers updated (see [announce]); a greeting of another
     // site closes the link, which stays that of the site first greeted on it
     private fun greetedAgain(
         link: Link,
         hello: Frame.Hello,
     ) {
         val same =
             locked(lock) {
                 (links[link] == hello.site).also { same ->
                     val known = peers[hello.site]
                     if (same && known?.link == link) {
                         peers = peers + (hello.site to known.copy(capacity = hello.capacity, lends = hello.lends))
                     }
                 }
             }
         if (!same) link.close()
     }
 
     // The workers placed here for [placement], run here and on the delegates in proportion to their workers: the
     // delegates are told where they all are
     private fun placed(
         link: Link,
         placement: Frame.Placement,
     ) {
         val delegated =
             locked(lock) {
                 val delegates = peers.filterValues { it.delegate }
                 val mine =
                     placement.workers
                         .filterValues { it == id }
                         .keys
                         .sortedBy { Address.indexOf(it) }
                 val workers =
                     if (delegates.isEmpty() || mine.isEmpty()) {
                         placement.workers
                     } else {
                         placement.workers + share(mine, delegates)
                     }
                 place(placement.session, workers, links[link] ?: ownerOf(placement.session))
                 workers
             }
         toDelegates(FrameCodec.encode(Frame.Placement(placement.session, delegated)))
     }
 
     // The placement of the workers of [session], reaching the sites with no link to this one through [via]: those of
     // the earlier sessions of the same site are dropped
     private fun place(
         session: String,
         workers: Map<String, String>,
         via: String,
     ) {
         forget(ownerOf(session))
         val direct = (workers.values.toSet() + ownerOf(session)).filter { it in peers }.toSet()
         placements = placements + (session to Session(workers, direct, via))
     }
 
     // The sessions of [site] and their workers forgotten
     private fun forget(site: String) {
         placements = placements.filterKeys { ownerOf(it) != site }
         hosted = hosted.filterKeys { ownerOf(it.first) != site }
     }
 
     // The site of the node at [to] in [session], if known
     private fun siteOf(
         session: String,
         to: String,
     ): String? = if (to == Address.CATALOG) ownerOf(session) else placements[session]?.workers?.get(to)
 
     // [frame] of [session] sent towards [site]: directly if the session goes there so, through the site it came
     // through otherwise
     private fun forward(
         session: String,
         site: String,
         frame: String,
     ) {
         val known = placements[session]
         val next = if (known == null || site in known.direct) site else known.via
         peers[next]?.link?.send(frame)
     }
 
     private fun toDelegates(frame: String) = peers.values.filter { it.delegate }.forEach { it.link.send(frame) }
 
     // The greeting of this site on [link] (see [Frame.Hello]): to a delegate, the workers of this site alone; to another
     // site, those of its delegates as well (null if one has no limit)
     private fun hello(
         link: Link,
         nonce: String? = null,
         proof: String? = null,
         token: String? = null,
     ): String {
         val capacity = if (link in delegateLinks) workers else offered
         return FrameCodec.encode(Frame.Hello(id, VERSION, capacity, lends, introducible, nonce, proof, token))
     }
 
     // The workers this site offers the other sites: its own and those of its delegates (null if one has no limit)
     private val offered: Int?
         get() {
             val delegated = peers.values.filter { it.delegate }.map { it.capacity }
             return if (workers == null || null in delegated) null else workers + delegated.sumOf { it ?: 0 }
         }
 
     // [site], just joined, and the other peers opening links of their own introduced to each other
     private fun introduce(site: String) {
         val joined = peers[site] ?: return
         peers
             .filter { (other, peer) -> other != site && peer.introducible && !peer.delegate }
             .forEach { (other, peer) ->
                 peer.link.send(FrameCodec.encode(Frame.Introduce(site)))
                 joined.link.send(FrameCodec.encode(Frame.Introduce(other)))
             }
     }
 
     // The other sites told again of the workers this one offers, as its delegates come and go
     private fun announce() = peers.values.filterNot { it.delegate }.forEach { it.link.send(hello(it.link)) }
 
     private fun toCatalog(message: KbMessage) = deliver(catalogQueue, catalog, message)
 
     // [message] of the graph of [session] to the catalog: handled if the catalog still builds that graph when it comes
     // to it (one it gave up builds a graph anew, and the messages of the old one queued before reach none)
     private fun toCatalogOf(
         session: String,
         message: KbMessage,
     ) = deliver(catalogQueue, catalog, message) { session == this.session }
 
     private fun deliver(
         queue: TaskQueue,
         node: Node,
         message: KbMessage,
         current: () -> Boolean = { true },
     ) {
         if (textual) {
             val text = MessageCodec.encode(message)
             queue.post { if (current()) node.receive(MessageCodec.decode(text)) }
         } else {
             queue.post { if (current()) node.receive(message) }
         }
     }
 
     // The worker at [address] for [session], run here: made at its first message, if the session is known
     private fun worker(
         session: String,
         address: String,
     ): Hosted? =
         hosted[session to address] ?: locked(lock) {
             if (session !in placements) {
                 null
             } else {
                 hosted[session to address] ?: Hosted(scheduler.queue(), Worker(address, WorkerNetwork(session))).also {
                     hosted = hosted + ((session to address) to it)
                 }
             }
         }
 
     // A message of [session] to the node at [to]: to a client of this site, to a node here, or to the site of the node
     private fun route(
         session: String,
         to: String,
         message: KbMessage,
     ) {
         val site =
             when {
                 to == Address.CATALOG || Address.isWorker(to) -> siteOf(session, to) ?: return
                 else -> return locked(lock) { clients.remove(to) }?.first?.invoke(message) ?: Unit
             }
         when {
             site != id -> forward(session, site, FrameCodec.encode(Frame.Deliver(session, to, MessageCodec.encode(message))))
             to == Address.CATALOG -> toCatalogOf(session, message)
             else -> worker(session, to)?.let { deliver(it.queue, it.node, message) }
         }
     }
 
     // The workers at [addresses] shared among this site and [others], in proportion to their workers, each site given
     // consecutive ones
     private fun share(
         addresses: List<String>,
         others: Map<String, Peer>,
     ): Map<String, String> {
         val sites = listOf(id to workers) + others.map { (site, peer) -> site to peer.capacity }
         val weights = sites.map { (_, capacity) -> (capacity ?: addresses.size).toDouble() }
         val total = weights.sum()
         if (total == 0.0) return addresses.associateWith { id }
         val quotas = weights.map { it * addresses.size / total }
         val shares = quotas.map { it.toInt() }.toMutableList()
         quotas.indices
             .sortedByDescending { quotas[it] - shares[it] }
             .take(addresses.size - shares.sum())
             .forEach { shares[it]++ }
         return sites.indices
             .flatMap { site -> List(shares[site]) { sites[site].first } }
             .zip(addresses) { site, address -> address to site }
             .toMap()
     }
 
     private inner class CatalogNetwork : Network {
         override fun send(
             to: String,
             message: KbMessage,
         ) = route(session, to, message)
 
         // The workers of this site and those of its peers lending theirs, no limit if one has none: one at least, run
         // here if no site has any
         override val capacity: Int?
             get() {
                 val lent = peers.values.filter { it.lends }.map { it.capacity }
                 return if (workers == null || null in lent) null else maxOf(1, workers + lent.sumOf { it ?: 0 })
             }
 
         override fun building(workers: Int) {
             val (lenders, frame) =
                 locked(lock) {
                     val next = "$id#${++sessions}"
                     val lenders = peers.filterValues { it.lends }
                     val placed = share(List(workers) { Address.worker(it) }, lenders)
                     place(next, placed, id)
                     session = next
                     lenders.values to FrameCodec.encode(Frame.Placement(next, placed))
                 }
             // every lender is told, so that those placed none drop the workers of the graphs built before
             lenders.forEach { it.link.send(frame) }
         }
 
         override fun siteOf(address: String): String = siteOf(session, address) ?: id
 
         override fun evaluated(stats: EvaluationStats) {
             evaluation = stats
         }
     }
 
     private inner class WorkerNetwork(
         private val session: String,
     ) : Network {
         override fun send(
             to: String,
             message: KbMessage,
         ) = route(session, to, message)
     }
 
     companion object {
         /** The version a site tells its peers: sites of different versions do not federate */
         const val VERSION: String = "arg2p-parallel/2 $ARG2P_VERSION"
 
         // A site identifier with no '#', which separates it from the number of a session
         private fun newId() = "site-" + Random.nextLong().toULong().toString(16)
 
         private fun ownerOf(session: String) = session.substringBeforeLast('#')
     }
 }