Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
25508c6
feat(network): add disconnectPeer + isBootstrapOrRelayPeer helpers
lukyrys Jun 13, 2026
d043cb6
feat(lishnet): disconnect lishnet-exclusive peers on leave + onNetwor…
lukyrys Jun 13, 2026
a1d3690
feat(transfer): disable downloads whose last joined lishnet was left
lukyrys Jun 13, 2026
898d494
feat(downloader): drop peer from peer manager on network peer:disconnect
lukyrys Jun 13, 2026
2228b8c
docs(protocol): mark out-of-scope TODOs (chunk AbortSignal, per-netwo…
lukyrys Jul 3, 2026
fc1362c
fix(protocol): gate unicast LISH discovery behind shared joined lishnet
lukyrys Jul 3, 2026
3161670
fix(transfer): broadcast download:disabled on last-lishnet leave
lukyrys Jul 2, 2026
a93b3a9
fix(protocol): extend lishnet serve-gate to getChunk requests
lukyrys Jul 3, 2026
a3b9c39
test(downloader): add onPeerDisconnect to MockNetwork
lukyrys Jun 22, 2026
a83208c
test(factory-reset): align peers default expectation with wipePeers=true
lukyrys Jul 2, 2026
5744095
test(downloader): cover peer:disconnect drop handling
lukyrys Jul 3, 2026
0010fb5
test(lishnet): cover leave-network exclusive peer disconnect
lukyrys Jul 3, 2026
4a78862
test(e2e): add onPeerDisconnect to transfer-integration MockNetwork
lukyrys Jul 2, 2026
bba1741
test(dev): add live 2-node leave-network verification script
lukyrys Jul 3, 2026
7d413eb
test(dev): add interactive 2-node scenario for manual UI checks
lukyrys Jul 3, 2026
caa7823
fix(network): limit leave disconnect exemption to real infrastructure
lukyrys Jul 8, 2026
96c81a4
fix(network): keep peer-disconnect subscriptions across node restarts
lukyrys Jul 8, 2026
c1fb566
test(dev): poll downloadEnabled list in leave verify fallback
lukyrys Jul 8, 2026
7df3ba5
fix(network): clear native keep-alive tag on peer disconnect
lukyrys Jul 21, 2026
b5f61c5
fix(transfer): drop runtime download-enabled flag on last-lishnet leave
lukyrys Jul 21, 2026
9992b23
chore(test): drop dev verification scripts from the branch
lukyrys Jul 21, 2026
6c5027b
fix(transfer): stop sourcing a download from a left lishnet and cance…
lukyrys Jul 21, 2026
deb2971
fix(network): prune bootstrap exemption for removed or left-lishnet p…
lukyrys Jul 21, 2026
0cac337
fix(network): use final p2p id for relayed bootstrap exemption
lukyrys Jul 22, 2026
f151caf
fix(downloader): dispose peer-disconnect handler on terminal error
lukyrys Jul 22, 2026
97bbabf
fix(network): suppress left peers from redial maintenance
lukyrys Jul 22, 2026
7dba87b
fix(transfer): resume suspended downloads when lishnet rejoins
lukyrys Jul 22, 2026
c9b788d
fix(network): disconnect offline bootstrap peers of left lishnet
lukyrys Jul 22, 2026
5511386
fix(downloader): dispose network handlers on successful completion
lukyrys Jul 22, 2026
709e03d
fix(transfer): keep DB enabled flag when no lishnet is joined
lukyrys Jul 22, 2026
6d6189e
fix(transfer): drop resume suspension only after enable succeeds
lukyrys Jul 22, 2026
20dedfb
fix(downloader): re-attach left network to running download on rejoin
lukyrys Jul 22, 2026
76ab8f3
fix(network): consult redial suppression in all maintenance dial paths
lukyrys Jul 22, 2026
709c3d3
fix(protocol): fail closed on serve-gate when remote peer id unknown
lukyrys Jul 22, 2026
fe15912
fix(network): lift redial suppression when bootstrap peer is re-confi…
lukyrys Jul 22, 2026
b8e90dc
fix(network): skip discovery re-dial for left-suppressed peers
lukyrys Jul 22, 2026
c4a1caa
fix(network): keep left-net bootstrap peer subscribing another joined…
lukyrys Jul 22, 2026
4b459ba
fix(transfer): only suspend enabled downloads on network leave
lukyrys Jul 22, 2026
4196863
fix(network): lift all redial suppression on network rejoin
lukyrys Jul 22, 2026
aa607be
fix(protocol): soften getLishs gate so search reaches unsynced peers
lukyrys Jul 22, 2026
a22c309
fix(network): forget peerStore entry on leave so disconnect survives …
lukyrys Jul 22, 2026
25f826f
fix(transfer): hide leave-disabled downloads from active transfers
lukyrys Jul 22, 2026
871173d
refactor(network): track redial suppression per lishnet
lukyrys Jul 22, 2026
d3b88db
fix(transfer): destroy transient download when its last lishnet leaves
lukyrys Jul 22, 2026
86dc71e
fix(transfer): bind suspended-download resume to original lishnets
lukyrys Jul 22, 2026
d951c2f
Merge remote-tracking branch 'origin/main' into fix/428-leave-network…
lukyrys Jul 22, 2026
304a417
fix(network): gate suppression clear and direct-set on shared topic
lukyrys Jul 22, 2026
9651703
fix(network): suppress recently-seen offline content peers on leave
lukyrys Jul 22, 2026
302ed5b
fix(network): require infrastructure peers to share a topic to list
lukyrys Jul 22, 2026
ddda6e2
fix(transfer): keep leave-suspended download stopped until bound rejoin
lukyrys Jul 22, 2026
0e88111
Merge remote-tracking branch 'origin/main' into fix/428-leave-network…
lukyrys Jul 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 114 additions & 1 deletion backend/src/api/transfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,95 @@ export function initTransferHandlers(networks: Networks, dataServer: DataServer,
const activeDownloaders = new Map<string, Downloader>();
setActiveDownloadersRef(activeDownloaders);

// LISHs whose download was suspended because their last joined lishnet was left,
// mapped to the lishnets they were bound to. Their DB enabled flag stays on (see
// onNetworkLeft), so onNetworkJoined resumes them — but only when a BOUND lishnet
// re-joins, never rebinding to an unrelated one. An empty bound set means "no known
// binding" (startup with no joined lishnet, where the fresh downloader would bind
// to whatever is enabled at resume time) → resume on any join. Cleared when the
// user explicitly enables/disables the download so a rejoin never overrides a
// deliberate user action.
const networkSuspended = new Map<string, Set<string>>();

// When a lishnet is left, stop any download bound EXCLUSIVELY to it: a
// downloader keeps running as long as at least one of its networks is still
// joined (multi-network downloads can still source chunks elsewhere). Only
// when none of its networks remain joined is there no peer source left, so we
// disable it (leaving DB/enabled flags untouched — a re-join can resume it).
networks.onNetworkLeft = (networkID: string) => {
for (const [lishID, dl] of activeDownloaders) {
const ids = dl.getNetworkIDs?.() ?? [];
if (!ids.includes(networkID)) continue;
if (ids.some(id => networks.isJoined(id))) {
// Another joined lishnet can still source this download — keep it
// running but stop using the network we just left, otherwise the
// downloader keeps broadcasting WANTs and probing peers on a topic
// we are no longer part of.
dl.removeNetwork?.(networkID);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Re-add rejoined lishnets to surviving downloads

When an enabled download originally spans networks A and B, leaving A while B remains joined takes this branch and permanently filters A out of the downloader's networkIDs. Since onNetworkJoined only handles downloads in networkSuspended, surviving multi-network downloads are never updated when A rejoins, so they continue broadcasting/probing only on B until a restart or manual toggle; if B has no useful sources, the download stays stalled even though the source network was rejoined.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 20dedfb — the downloader keeps an immutable originalNetworkIDs snapshot and gains addNetwork(); onNetworkJoined re-attaches a rejoined lishnet to every running download that originally spanned it (no-op for foreign or already-active networks), so surviving multi-network downloads resume sourcing from the rejoined topic.

continue;
}
// Drop the runtime enabled flag (no DB persist) so `lishs.list` reports the
// download as stopped and restartDownloadIfEnabled cannot silently revive it
// while no usable lishnet is joined. The DB flag stays untouched, so an app
// restart with the lishnet re-joined resumes the download.
const wasEnabled = downloadEnabledLishs.has(lishID);
downloadEnabledLishs.delete(lishID);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restore paused downloads when the lishnet rejoins

When the last joined lishnet for an enabled download is left, this removes the LISH from the in-memory enabled set while intentionally leaving the DB flag enabled. In the same process, re-enabling that lishnet only runs joinNetwork/bootstrap dialing; the startup auto-resume timer has already run and nothing reloads downloadEnabledLishs or calls enableDownload, so the download stays stopped despite being persisted as enabled until the user manually toggles it or restarts after rejoining. Either persist the disabled state or re-enable these DB-enabled downloads when their network becomes joined again.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 7dba87b — LISHs disabled because their last joined lishnet was left are tracked as network-suspended; a new onNetworkJoined hook re-enables them when a bound lishnet is re-joined in-process. An explicit user enable/disable clears the suspension so a rejoin never overrides a deliberate user action.

// Cancel any pending error-recovery timer for this LISH — otherwise
// ErrorRecovery, holding the captured downloadWasEnabled=true, could
// re-enable the download once the IO condition clears even though the
// user just stopped it by leaving the network.
recovery.stop(lishID);
if (wasEnabled) {
// Persisted download — retain the disabled downloader and remember it as
// suspended-by-leave so onNetworkJoined can resume it after rejoin.
console.log(`[Transfer] ${lishID.slice(0, 8)}: last joined lishnet left, disabling download`);
dl.disable();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Block manual re-enable until a bound lishnet rejoins

Fresh evidence in the current code: this path retains a disabled downloader in activeDownloaders, and the existing enableDownload active-downloader branch calls dl.enable() without checking that any of its networkIDs is joined. If the user clicks enable after leaving the last lishnet but before rejoining it, the downloader resumes and broadcasts/probes on the left topic instead of staying suspended; keep it suspended or remove/rebuild it unless a bound network is joined.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ddda6e2 — enableDownload now refuses to resume a leave-suspended download (in networkSuspended) unless one of its ORIGINAL bound lishnets is currently joined; it drops the runtime flag while keeping the DB intent, so a manual enable after leaving the last lishnet keeps it suspended instead of broadcasting on the left topic.

// Bind resume to the download's ORIGINAL lishnets (not the current set,
// which removeNetwork may have shrunk) so only a re-join of a lishnet this
// download actually belongs to resumes it.
networkSuspended.set(lishID, new Set(dl.getOriginalNetworkIDs?.() ?? dl.getNetworkIDs?.() ?? []));
} else {
// Transient download (from the `download` handler, never enabled/persisted)
// has no resume claim — destroy it and drop it from the map instead of
// leaking a disabled downloader with a dangling download() promise and
// registered network handlers (a fresh start of the same LISH would
// otherwise overwrite the map entry without disposing this one).
console.log(`[Transfer] ${lishID.slice(0, 8)}: last joined lishnet left, dropping transient download`);
dl.destroy().catch(err => console.error(`[Transfer] ${lishID.slice(0, 8)}: destroy on leave failed:`, err?.message ?? err));
activeDownloaders.delete(lishID);
}
// dl.disable() alone emits nothing over WS — tell the FE the download
// stopped.
broadcast?.('transfer.download:disabled', { lishID });
}
};

// When a previously-left lishnet is re-joined in-process, resume downloads that
// were suspended because it was their last joined network. Their DB enabled flag
// was intentionally left on (see onNetworkLeft), so re-enabling here restores the
// pre-leave state without waiting for an app restart. Only downloads still bound
// to the re-joined network and still suspended are resumed.
networks.onNetworkJoined = (networkID: string) => {
// Re-attach the rejoined network to still-running multi-network downloaders
// that dropped it when it was left (no-op if never bound to it or already active).
for (const dl of activeDownloaders.values()) dl.addNetwork?.(networkID);
// Resume a suspended download only when a lishnet it is BOUND to re-joins (an
// empty bound set = no known binding → resume on any join). Drop the suspension
// ONLY once the resume actually succeeds — a transient failure (busy verifying,
// still no joined lishnet) must be retried on the next join.
for (const [lishID, bound] of [...networkSuspended]) {
if (bound.size > 0 && !bound.has(networkID)) continue;
enableDownload({ lishID })
.then(r => {
if (r.success) {
networkSuspended.delete(lishID);
console.log(`[Transfer] ${lishID.slice(0, 8)}: lishnet re-joined, download resumed`);
}
})
.catch(err => console.error(`[Transfer] resume-on-rejoin ${lishID.slice(0, 8)} failed:`, err?.message ?? err));
}
};

// Error recovery: auto-retry when IO conditions clear
const recovery = new ErrorRecovery({
attemptRecover: async (lishID, downloadWasEnabled, uploadWasEnabled): Promise<boolean> => {
Expand Down Expand Up @@ -181,6 +270,7 @@ export function initTransferHandlers(networks: Networks, dataServer: DataServer,

function disableDownload(p: { lishID: string }): { success: boolean } {
assert(p, ['lishID']);
networkSuspended.delete(p.lishID);
recovery.stop(p.lishID);
downloadEnabledLishs.delete(p.lishID);
persistDownloadEnabled?.(p.lishID, false);
Expand All @@ -202,6 +292,16 @@ export function initTransferHandlers(networks: Networks, dataServer: DataServer,
persistDownloadEnabled?.(p.lishID, true);
const dl = activeDownloaders.get(p.lishID);
if (dl) {
// A download suspended by leaving its last lishnet is retained (disabled) for
// resume on rejoin. Enabling it before a bound lishnet is re-joined would make
// it broadcast WANTs / probe on a topic we already left — keep it suspended.
const boundIDs = dl.getOriginalNetworkIDs?.() ?? dl.getNetworkIDs?.() ?? [];
if (networkSuspended.has(p.lishID) && boundIDs.length > 0 && !boundIDs.some((id: string) => networks.isJoined(id))) {
// Drop the runtime enabled flag we just set; the DB flag (persisted above)
// stays true so a later rejoin of a bound lishnet resumes the download.
downloadEnabledLishs.delete(p.lishID);
return { success: false };
}
// If downloader is in error state, destroy it and create a fresh one
if (dl.getError()) {
console.debug(`[Transfer] ${p.lishID.slice(0, 8)}: destroying error-state downloader, will create fresh`);
Expand Down Expand Up @@ -268,8 +368,16 @@ export function initTransferHandlers(networks: Networks, dataServer: DataServer,
const network = networks.getRunningNetwork();
const joinedNetworks = networks.getEnabled().map(n => n.networkID);
if (joinedNetworks.length === 0) {
// No lishnet is joined to source this download. Keep the DB enabled flag
// ON — clearing it would permanently forget the user's intent so a later
// rejoin could never resume. Drop only the in-memory active flag and mark
// it suspended so onNetworkJoined resumes it once a lishnet is (re-)joined.
downloadEnabledLishs.delete(p.lishID);
persistDownloadEnabled?.(p.lishID, false);
// No active downloader and no joined lishnet → no known network binding
// (the fresh downloader would bind to getEnabled(), empty here). Store the
// empty bound set so onNetworkJoined resumes on any join, since the DB has
// no per-download network to restrict to.
networkSuspended.set(p.lishID, new Set(joinedNetworks));
return { success: false };
}
const downloadDir = lish.directory ?? join(dataDir, 'downloads', Date.now().toString());
Expand Down Expand Up @@ -361,7 +469,11 @@ export function initTransferHandlers(networks: Networks, dataServer: DataServer,
const enabled = getEnabledUploads();
// Active downloads — report the allocation phase distinctly so the UI can show
// "allocating" after a reconnect instead of falling back to idle (no peers yet).
// A downloader disabled by leaving its last lishnet stays in the map (retained
// for resume on rejoin) but is stopped — skip it so the LISH is not reported as
// still downloading after transfer.download:disabled.
for (const [lishID, dl] of activeDownloaders) {
if (dl.isDisabled?.()) continue;
const type = dl.isAllocating?.() ? 'allocating' : 'downloading';
transfers.push({ lishID, type, peers: dl.getPeerCount?.() ?? 0, bytesPerSecond: 0 });
}
Expand Down Expand Up @@ -491,6 +603,7 @@ export function initTransferHandlers(networks: Networks, dataServer: DataServer,
}
activeDownloaders.clear();
downloadEnabledLishs.clear();
networkSuspended.clear();
clearAllUploads();
recovery.stopAll();
}
Expand Down
125 changes: 125 additions & 0 deletions backend/src/lishnet/lishnets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ export class Networks {
private _onPeerCountChange: ((counts: { networkID: string; count: number }[]) => void) | null = null;
// Callback for bootstrap status changes
private _onBootstrapStatusChange: ((networkID: string, status: BootstrapStatus) => void) | null = null;
// Callback fired after a lishnet is left (topic unsubscribed). Lets higher
// layers (e.g. transfer) stop downloads bound exclusively to that lishnet.
private _onNetworkLeft: ((networkID: string) => void) | null = null;
// Callback fired after a lishnet is (re-)joined in-process. Lets higher layers
// resume downloads that were suspended when this lishnet was previously left.
private _onNetworkJoined: ((networkID: string) => void) | null = null;

constructor(db: Database, dataDir: string, dataServer: DataServer, settings: Settings) {
this.db = db;
Expand Down Expand Up @@ -50,6 +56,27 @@ export class Networks {
this._onBootstrapStatusChange = cb;
}

/**
* Set a callback fired right after a lishnet is left (its topic has been
* unsubscribed and removed from {@link joinedNetworks}). The callback runs
* synchronously from {@link leaveNetwork}; consumers should not assume any
* particular peer/connection state beyond "this lishnet is no longer joined".
*/
set onNetworkLeft(cb: ((networkID: string) => void) | null) {
this._onNetworkLeft = cb;
}

/**
* Set a callback fired right after a lishnet is (re-)joined via {@link joinNetwork}
* (its topic subscribed and added to {@link joinedNetworks}). Lets higher layers
* (e.g. transfer) resume downloads that were suspended when the lishnet was
* previously left. NOT fired for the initial startup join — startup has its own
* auto-resume path.
*/
set onNetworkJoined(cb: ((networkID: string) => void) | null) {
this._onNetworkJoined = cb;
}

init(): void {
console.log('✓ Networks initialized');
}
Expand Down Expand Up @@ -120,24 +147,113 @@ export class Networks {
// ordering — the process-level error handlers in app.ts are the safety net.
this.network.subscribeTopic(id);
this.joinedNetworks.add(id);
// Rejoin is an explicit "I want peers back" — lift the redial suppression for
// THIS lishnet's left peers (bootstrap and content) so maintenance and discovery
// may reconnect them. Scoped per-network: still-left lishnets stay suppressed.
this.network.clearRedialSuppressionForNetwork(id);

const net = this.get(id);
if (net && net.bootstrapPeers.length > 0) await this.network.addBootstrapPeers(net.bootstrapPeers, id, 'configured');

console.log(`✓ Joined lishnet: ${net?.name ?? id}`);

// Notify higher layers (e.g. transfer) so downloads suspended when this
// lishnet was last left can resume now that it is joined again.
this._onNetworkJoined?.(id);
}

/**
* Leave a lishnet (unsubscribe from its topic).
*/
/** Peer IDs (the /p2p/<id> component) of a list of bootstrap multiaddr strings. */
private static bootstrapPeerIDsOf(bootstrapPeers: string[]): string[] {
const ids: string[] = [];
for (const addr of bootstrapPeers) {
// Relayed multiaddrs (.../p2p/<relay>/p2p-circuit/p2p/<target>) carry two
// /p2p components; the bootstrap peer identity is the FINAL one (the target),
// not the relay. Match all and take the last.
const matches = [...addr.matchAll(/\/p2p\/([^/]+)/g)];
const last = matches[matches.length - 1];
if (last) ids.push(last[1]!);
}
return ids;
}

/** Configured-bootstrap peer IDs of a single network. */
private configuredBootstrapPeerIDsOf(networkID: string): Set<string> {
return new Set(Networks.bootstrapPeerIDsOf(this.get(networkID)?.bootstrapPeers ?? []));
}

/** Configured-bootstrap peer IDs of every joined network except `exceptID`. */
private configuredBootstrapPeerIDsElsewhere(exceptID: string): Set<string> {
const out = new Set<string>();
for (const nid of this.joinedNetworks) {
if (nid === exceptID) continue;
for (const pid of Networks.bootstrapPeerIDsOf(this.get(nid)?.bootstrapPeers ?? [])) out.add(pid);
}
return out;
}

private async leaveNetwork(id: string): Promise<void> {
if (!this.joinedNetworks.has(id)) return;

// Snapshot the topic subscribers BEFORE unsubscribing — unsubscribeTopic
// tears the topic out of pubsub, after which getTopicPeers(id) returns [].
// Union with recently-seen members (TTL) so a content peer that is momentarily
// disconnected at leave time — but still holds a peerStore entry — is also
// suppressed; a live-subscriber-only snapshot would miss it and maintenance
// could redial it back after we left.
const leftPeers = new Set<string>(this.network.getTopicPeers(id));
for (const pid of this.network.getRecentTopicMembers(id)) leftPeers.add(pid);

this.network.unsubscribeTopic(id);
this.joinedNetworks.delete(id);

// Subscribers of any OTHER joined lishnet must stay connected (shared
// infrastructure). Compute this set BEFORE the bootstrap cleanup so that loop
// can skip them too — a bootstrap of the left net that also subscribes another
// joined net would otherwise be hung up here.
const stillJoinedPeers = new Set<string>();
for (const otherID of this.joinedNetworks) {
for (const pid of this.network.getTopicPeers(otherID)) stillJoinedPeers.add(pid);
}

// Drop the exemption AND actively disconnect every configured bootstrap peer
// exclusive to the left lishnet — including ones offline at leave time. Such
// a peer never appears in leftPeers (the topic-subscriber snapshot), so the
// content-peer loop below would miss it: its keep-alive tag would survive and
// redial maintenance / ReconnectQueue would reconnect it within ~30s. Keep it
// if it still subscribes another joined lishnet, or if it is an active circuit
// relay we depend on. disconnectPeer is a safe no-op hangUp for an unconnected
// peer and always strips keep-alive + suppresses redial.
const stillConfigured = this.configuredBootstrapPeerIDsElsewhere(id);
for (const pid of this.configuredBootstrapPeerIDsOf(id)) {
if (stillConfigured.has(pid)) continue;
this.network.pruneConfiguredBootstrapPeer(pid);
if (stillJoinedPeers.has(pid)) continue;
if (this.network.isBootstrapOrRelayPeer(pid)) continue;
await this.network.disconnectPeer(pid, id);
}

// Disconnect peers that belonged exclusively to the lishnet we just left.
// A peer is kept connected if it is still a subscriber of any OTHER joined
// lishnet, or if it is a bootstrap/relay peer (shared infrastructure other
// networks depend on). Everything else is a plain content peer with no
// remaining reason to stay connected, so hang it up via the single
// Network.disconnectPeer entry point (which also clears the keep-alive tag
// so ReconnectQueue does not immediately re-dial it).
for (const pid of leftPeers) {
if (stillJoinedPeers.has(pid)) continue;
if (this.network.isBootstrapOrRelayPeer(pid)) continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Mark kept relay peers as left

When an exclusive left-lishnet peer is an active relay/bootstrap that we intentionally keep connected, this continue skips disconnectPeer, so no redial suppression is recorded. The new listing gate refuses left peers only via isRedialSuppressed, so a kept relay from a network the user just left can still call getLishs and receive the shared catalog while we remain in any other lishnet. Record suppression without hanging up, or make canListSharesTo require current topic sharing for these kept infrastructure peers.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 302ed5b — canListSharesTo now requires a kept infrastructure peer (active relay/bootstrap, isBootstrapOrRelayPeer) to currently share a joined topic; a relay of a network we just left can no longer browse our catalog. Ordinary content peers still get the soft gate for the SUBSCRIBE-lag search-fallback window.

await this.network.disconnectPeer(pid, id);
}

const net = this.get(id);
console.log(`✓ Left lishnet: ${net?.name ?? id}`);

// Notify higher layers (e.g. transfer) so downloads bound exclusively to
// this lishnet can be stopped.
this._onNetworkLeft?.(id);
}

/**
Expand Down Expand Up @@ -308,6 +424,15 @@ export class Networks {
const existing = this.get(id);
if (!existing) return null;
const cleaned = bootstrapPeers.filter(p => typeof p === 'string' && p.trim().length > 0);
// Drop the bootstrap-exemption for peer IDs removed from this network's
// config, unless still configured for another joined network. Prevents a
// removed bootstrap entry from lingering as infrastructure that a later
// leave-network would refuse to disconnect.
const nextIDs = new Set(Networks.bootstrapPeerIDsOf(cleaned));
const elsewhere = this.configuredBootstrapPeerIDsElsewhere(id);
for (const pid of Networks.bootstrapPeerIDsOf(existing.bootstrapPeers)) {
if (!nextIDs.has(pid) && !elsewhere.has(pid)) this.network.pruneConfiguredBootstrapPeer(pid);
}
const next: LISHNetworkConfig = { ...existing, bootstrapPeers: cleaned };
updateLISHnet(this.db, next);
this.network.pruneBootstrapStatus(id, cleaned);
Expand Down
Loading