// src/net/libp2p-worker.ts
import { parentPort } from "worker_threads";
import { gossipsub } from "@chainsafe/libp2p-gossipsub";
import { noise } from "@chainsafe/libp2p-noise";
import { yamux } from "@chainsafe/libp2p-yamux";
import { identify } from "@libp2p/identify";
import { ping } from "@libp2p/ping";
import { tcp } from "@libp2p/tcp";
import { multiaddr } from "@multiformats/multiaddr";
import { createLibp2p } from "libp2p";
function blocksTopic(chainId) {
  return `/asentum/${chainId.toString()}/blocks/1.0.0`;
}
function txsTopic(chainId) {
  return `/asentum/${chainId.toString()}/txs/1.0.0`;
}
function votesTopic(chainId) {
  return `/asentum/${chainId.toString()}/votes/1.0.0`;
}
function proposalsTopic(chainId) {
  return `/asentum/${chainId.toString()}/proposals/1.0.0`;
}
var libp2p;
var topicMap;
var persistentPeers = /* @__PURE__ */ new Set();
var peerIdToMultiaddr = /* @__PURE__ */ new Map();
function getPubSub() {
  if (!libp2p) throw new Error("libp2p not started in worker");
  return libp2p.services.pubsub;
}
function post(msg, transfers = []) {
  if (!parentPort) return;
  parentPort.postMessage(msg, transfers);
}
async function startLibp2p(opts) {
  topicMap = {
    blocks: blocksTopic(opts.chainId),
    txs: txsTopic(opts.chainId),
    votes: votesTopic(opts.chainId),
    proposals: proposalsTopic(opts.chainId)
  };
  libp2p = await createLibp2p({
    addresses: {
      listen: [`/ip4/${opts.listenHost}/tcp/${opts.listenPort}`]
    },
    transports: [tcp()],
    connectionEncrypters: [noise()],
    streamMuxers: [yamux()],
    connectionManager: {
      // Worker's event loop is dedicated to libp2p, so keep-alive
      // primitives (ping, gossipsub heartbeat, mesh maintenance) run
      // reliably. BUT in prod cross-DC conditions (25ms+ RTT), the
      // default upgrade-timeout (5s) sometimes fires before identify+
      // gossipsub-subscription-exchange completes — libp2p flags the
      // peer as "stalled", closes the connection. Then ping never
      // gets a chance to keep it alive. Increasing the upgrade
      // timeout absorbs prod-realistic latency.
      //
      // Also force minConnections = baked count so the pruner never
      // drops below the cluster size — at 4 baked we want all 4
      // connections held open forever, no exceptions.
      minConnections: 16,
      maxConnections: 1e3,
      inboundConnectionThreshold: 1e3,
      inboundUpgradeTimeout: 6e4,
      outboundUpgradeTimeout: 6e4
    },
    services: {
      identify: identify(),
      ping: ping(),
      // gossipsub mesh parameters: defaults are D=6, Dlo=4, Dhi=12 — built
      // for IPFS-scale topologies with hundreds of peers. In a small
      // bonded-validator cluster (4–20 nodes) those defaults are
      // unreachable: each node has at most N-1 peers, less than Dlo,
      // so gossipsub never settles. Every heartbeat it emits a fresh
      // GRAFT for the missing slot, then PRUNE-s when the peer fails
      // some liveness gate, then GRAFTs again — the visible symptom is
      // peer:connect → peer:disconnect every few seconds and v0
      // silently falls out of the mesh while v1/v2/v3 keep committing
      // without it.
      //
      // D=3, Dlo=2, Dhi=4 fits a 4-baked cluster cleanly and
      // still scales: a 20-validator network would have 19 peers per
      // node, well above Dhi=4, so gossipsub will just hold its full
      // mesh at 4. Revisit these knobs if the bonded set grows past
      // a few dozen and we need more redundancy per node.
      pubsub: gossipsub({
        allowPublishToZeroTopicPeers: true,
        emitSelf: false,
        // floodPublish: publish reaches every known peer, not just the
        // gossipsub mesh subset. For a small bonded cluster this is
        // cheap (~4 peers × small messages) and sidesteps the mesh-
        // membership instability entirely: a node that's transiently
        // outside the mesh still receives every block/vote/proposal,
        // so it can't silently fall behind. Without this we observed
        // v0 (the bootstrap target) committing h=2..4 and then missing
        // every h=5 proposal because gossipsub had pruned it from the
        // active mesh — while v1/v2/v3 kept committing past it.
        floodPublish: true,
        D: 3,
        Dlo: 2,
        Dhi: 4,
        Dout: 0,
        Dlazy: 3
      })
    }
  });
  await libp2p.start();
  libp2p.addEventListener("peer:connect", (evt) => {
    const id = evt.detail.toString();
    post({ type: "peer-event", event: "connect", peerId: id });
    if (!libp2p) return;
    void (async () => {
      try {
        const peerIdObj = (await import("@libp2p/peer-id")).peerIdFromString(id);
        const peer = await libp2p.peerStore.get(peerIdObj);
        const addr = peer.addresses[0]?.multiaddr;
        if (addr) {
          const maStr = `${addr.toString()}/p2p/${id}`;
          if (!peerIdToMultiaddr.has(id)) {
            peerIdToMultiaddr.set(id, maStr);
            persistentPeers.add(maStr);
          }
        }
      } catch {
      }
    })();
  });
  libp2p.addEventListener("peer:disconnect", (evt) => {
    const id = evt.detail.toString();
    post({ type: "peer-event", event: "disconnect", peerId: id });
    const ma = peerIdToMultiaddr.get(id);
    if (ma && persistentPeers.has(ma)) {
      setTimeout(() => {
        if (!libp2p) return;
        libp2p.dial(multiaddr(ma)).catch((err) => {
          post({ type: "error", message: `auto-redial ${ma} failed: ${String(err)}` });
        });
      }, 2e3);
    }
  });
  const pubsub = getPubSub();
  pubsub.subscribe(topicMap.blocks);
  pubsub.subscribe(topicMap.txs);
  pubsub.subscribe(topicMap.votes);
  pubsub.subscribe(topicMap.proposals);
  pubsub.addEventListener("message", (evt) => {
    const { topic, data } = evt.detail;
    const copy = new Uint8Array(data.length);
    copy.set(data);
    if (!topicMap) return;
    if (topic === topicMap.blocks) {
      post({ type: "received-block", bytes: copy }, [copy.buffer]);
    } else if (topic === topicMap.txs) {
      post({ type: "received-tx", bytes: copy }, [copy.buffer]);
    } else if (topic === topicMap.votes) {
      post({ type: "received-vote", bytes: copy }, [copy.buffer]);
    } else if (topic === topicMap.proposals) {
      post({ type: "received-proposal", bytes: copy }, [copy.buffer]);
    }
  });
  const multiaddrs = libp2p.getMultiaddrs().map((m) => m.toString());
  const peerId = libp2p.peerId.toString();
  post({ type: "started", peerId, multiaddrs });
}
async function handleMessage(msg) {
  const type = msg.type;
  if (type === "start") {
    try {
      await startLibp2p(msg.opts);
    } catch (err) {
      post({ type: "error", message: `start failed: ${String(err)}` });
    }
    return;
  }
  if (!libp2p || !topicMap) {
    post({ type: "error", message: `worker not started (got ${type})` });
    return;
  }
  if (type === "publish-block") {
    await getPubSub().publish(topicMap.blocks, msg.bytes).catch((err) => {
      post({ type: "error", message: `publish-block failed: ${String(err)}` });
    });
    return;
  }
  if (type === "publish-tx") {
    await getPubSub().publish(topicMap.txs, msg.bytes).catch((err) => {
      post({ type: "error", message: `publish-tx failed: ${String(err)}` });
    });
    return;
  }
  if (type === "publish-vote") {
    await getPubSub().publish(topicMap.votes, msg.bytes).catch((err) => {
      post({ type: "error", message: `publish-vote failed: ${String(err)}` });
    });
    return;
  }
  if (type === "publish-proposal") {
    await getPubSub().publish(topicMap.proposals, msg.bytes).catch((err) => {
      post({ type: "error", message: `publish-proposal failed: ${String(err)}` });
    });
    return;
  }
  if (type === "dial-peer") {
    const maStr = msg.multiaddr;
    persistentPeers.add(maStr);
    try {
      const ma = multiaddr(maStr);
      const conn = await libp2p.dial(ma);
      const peerId = conn.remotePeer;
      peerIdToMultiaddr.set(peerId.toString(), maStr);
      await libp2p.peerStore.merge(peerId, {
        tags: { "bootstrap-keep-alive": { value: 100, ttl: void 0 } }
      });
    } catch (err) {
      post({ type: "error", message: `dial-peer failed: ${String(err)}` });
    }
    return;
  }
  if (type === "stop") {
    try {
      await libp2p.stop();
      libp2p = void 0;
      topicMap = void 0;
      post({ type: "stopped" });
    } catch (err) {
      post({ type: "error", message: `stop failed: ${String(err)}` });
    }
    return;
  }
  post({ type: "error", message: `unknown message type: ${type}` });
}
if (parentPort) {
  parentPort.on("message", (msg) => {
    void handleMessage(msg).catch((err) => {
      post({ type: "error", message: `handler crash: ${String(err)}` });
    });
  });
}
//# sourceMappingURL=libp2p-worker.js.map