improvent for invites

This commit is contained in:
Raven Scott
2025-12-17 21:13:46 -05:00
parent d3b61e9823
commit d1ee8b979a
6 changed files with 477 additions and 75 deletions
+1 -1
View File
@@ -236,7 +236,7 @@ Configure via `.env` (copy from `default.env`):
| `DISABLE_DNS_SERVER` | `false` | Disable DNS server |
| `DISABLE_PROXY_SERVER` | `false` | Disable proxy servers |
| `DISABLE_VIRTUAL_INTERFACES` | `false` | Disable virtual interfaces |
| `ALLOW_ANY_WRITER_INVITES` | `false` | Allow joiners to issue invites |
| `ALLOW_ANY_WRITER_INVITES` | `true` | Allow joiners to issue invites |
| `FULL_PERSISTENCE` | `false` | Keep Holesail connections indefinitely |
### Consensus Settings
+1 -1
View File
@@ -712,7 +712,7 @@ Configure via `.env` (copy from `default.env`):
- **DISABLE_DNS_SERVER**: Disable DNS server (default: `false`).
- **DISABLE_PROXY_SERVER**: Disable proxy servers (default: `false`).
- **DISABLE_VIRTUAL_INTERFACES**: Disable virtual interface creation (default: `false`).
- **ALLOW_ANY_WRITER_INVITES**: Allow joiners to issue invites (default: `false`).
- **ALLOW_ANY_WRITER_INVITES**: Allow joiners to issue invites (default: `true`).
- **CONSENSUS_QUORUM_THRESHOLD**: Percentage of active peers that must vote to meet quorum (0.0-1.0, default: `0.5`). For example, `0.5` means 50% of peers must vote.
- **CONSENSUS_MIN_VOTES**: Minimum number of votes required regardless of peer count (default: `2`). Ensures quorum even in small networks.
- **CONSENSUS_TIE_BREAKER**: Strategy for breaking ties between claimants with equal votes (default: `timestamp`). Options: `timestamp` (prefer oldest claim), `claimant_age` (prefer longest history), `lexicographic` (alphabetical ordering).
@@ -8,6 +8,7 @@ const { trackRequest, trackRequestWithTiming } = require('../../../maintenance/m
const { createErrorResponse } = require('../../../infrastructure/error_handler');
const execAsync = promisify(exec);
const { state } = require('../../../infrastructure/state');
async function handleDiagnosticsRoutes(req, res) {
const urlPath = req.urlPath || new URL(req.url, `https://${req.headers.host}`).pathname;
@@ -502,6 +503,30 @@ async function handleDiagnosticsRoutes(req, res) {
return true;
}
// GET /api/diagnostics/invites
if (method === 'GET' && urlPath === '/api/diagnostics/invites') {
try {
if (!state.diagnoseInviteIssues) {
res.writeHead(503, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Invite diagnostics not available - system not fully initialized' }));
trackRequest(urlPath, false);
return true;
}
const diagnostics = state.diagnoseInviteIssues();
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(diagnostics));
trackRequest(urlPath, true);
return true;
} catch (err) {
logError('DiagnosticsRoute', `Invite diagnostics error: ${err.message}`);
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: err.message }));
trackRequest(urlPath, false);
return true;
}
}
return false;
}
+9
View File
@@ -872,6 +872,15 @@
<button onclick="testConnection()" class="w-full px-4 py-2 bg-primary text-white rounded hover:bg-primary-hover">Test Connection</button>
</div>
</div>
<!-- Invite Diagnostics -->
<div class="theme-glass rounded-lg p-4 col-span-1 lg:col-span-2">
<h4 class="text-lg font-semibold mb-4">Invite Diagnostics</h4>
<div class="space-y-3">
<p class="text-sm text-gray-400">Diagnose invite system issues including channel health, peer connections, and pending acknowledgments.</p>
<button onclick="runInviteDiagnostics()" class="w-full px-4 py-2 bg-primary text-white rounded hover:bg-primary-hover">Run Invite Diagnostics</button>
</div>
</div>
</div>
<!-- Results -->
@@ -666,6 +666,176 @@ function closeDiagnosticResult(resultId) {
}
}
// Run invite diagnostics
async function runInviteDiagnostics() {
const buttonEl = document.querySelector('button[onclick="runInviteDiagnostics()"]');
// Show loading indicator
if (buttonEl) {
buttonEl.disabled = true;
buttonEl.innerHTML = '<span class="inline-block animate-spin rounded-full h-4 w-4 border-t-2 border-white mr-2"></span>Diagnosing...';
}
try {
const response = await fetch('/api/diagnostics/invites');
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
}
const diagnostics = await response.json();
displayInviteDiagnostics(diagnostics);
} catch (err) {
console.error('Invite diagnostics failed:', err);
if (window.showNotification) window.showNotification('Invite diagnostics failed: ' + err.message, 'error');
} finally {
// Restore button
if (buttonEl) {
buttonEl.disabled = false;
buttonEl.innerHTML = 'Run Invite Diagnostics';
}
}
}
// Display invite diagnostics results
function displayInviteDiagnostics(diagnostics) {
const resultId = 'invite-diagnostics-' + Date.now();
const resultDiv = document.createElement('div');
resultDiv.id = resultId;
resultDiv.className = 'bg-gray-800 rounded-lg p-4 mb-4 border border-gray-600';
const headerDiv = document.createElement('div');
headerDiv.className = 'flex justify-between items-center mb-3';
const titleDiv = document.createElement('div');
titleDiv.className = 'flex items-center';
titleDiv.innerHTML = `
<h3 class="text-lg font-semibold text-white">Invite Diagnostics</h3>
<span class="ml-2 text-sm text-gray-400">${new Date(diagnostics.timestamp).toLocaleString()}</span>
`;
const buttonContainer = document.createElement('div');
buttonContainer.className = 'flex space-x-2';
headerDiv.appendChild(titleDiv);
headerDiv.appendChild(buttonContainer);
const contentDiv = document.createElement('div');
contentDiv.className = 'space-y-3';
// Basic info
contentDiv.innerHTML += `
<div class="grid grid-cols-2 gap-4">
<div class="bg-gray-700 rounded p-3">
<div class="text-sm text-gray-400">Node Type</div>
<div class="text-lg font-semibold text-white">${diagnostics.nodeType}</div>
</div>
<div class="bg-gray-700 rounded p-3">
<div class="text-sm text-gray-400">DNS Pass Initialized</div>
<div class="text-lg font-semibold ${diagnostics.dnsPassInitialized ? 'text-green-400' : 'text-red-400'}">
${diagnostics.dnsPassInitialized ? 'Yes' : 'No'}
</div>
</div>
</div>
`;
// Peer connections
contentDiv.innerHTML += `
<div class="bg-gray-700 rounded p-3">
<div class="text-sm text-gray-400 mb-2">Peer Connections</div>
<div class="text-lg font-semibold text-white">${diagnostics.connectedPeers} connected</div>
</div>
`;
// Failed peers
if (diagnostics.failedInvitePeers.length > 0) {
contentDiv.innerHTML += `
<div class="bg-red-900 border border-red-600 rounded p-3">
<div class="text-sm text-red-400 mb-2">Failed Invite Peers</div>
<div class="text-sm text-red-300">${diagnostics.failedInvitePeers.join(', ')}</div>
</div>
`;
}
// Pending ACKs
if (diagnostics.pendingInviteAcks.length > 0) {
contentDiv.innerHTML += `
<div class="bg-yellow-900 border border-yellow-600 rounded p-3">
<div class="text-sm text-yellow-400 mb-2">Pending Invite Acknowledgments</div>
<div class="text-sm text-yellow-300">${diagnostics.pendingInviteAcks.length} pending</div>
</div>
`;
}
// Connection issues
if (diagnostics.connectionIssues.length > 0) {
contentDiv.innerHTML += `
<div class="bg-orange-900 border border-orange-600 rounded p-3">
<div class="text-sm text-orange-400 mb-2">Connection Issues</div>
<div class="text-sm text-orange-300">${diagnostics.connectionIssues.join('<br>')}</div>
</div>
`;
}
// Channel health summary
const healthyInviteChannels = Object.values(diagnostics.inviteChannels).filter(ch => ch.opened).length;
const healthyRequestChannels = Object.values(diagnostics.requestChannels).filter(ch => ch.opened).length;
contentDiv.innerHTML += `
<div class="bg-gray-700 rounded p-3">
<div class="text-sm text-gray-400 mb-2">Channel Health</div>
<div class="grid grid-cols-2 gap-4 text-sm">
<div>
<span class="text-gray-300">Invite Channels:</span>
<span class="ml-2 font-semibold ${healthyInviteChannels === diagnostics.connectedPeers ? 'text-green-400' : 'text-yellow-400'}">
${healthyInviteChannels}/${diagnostics.connectedPeers} healthy
</span>
</div>
<div>
<span class="text-gray-300">Request Channels:</span>
<span class="ml-2 font-semibold ${healthyRequestChannels === diagnostics.connectedPeers ? 'text-green-400' : 'text-yellow-400'}">
${healthyRequestChannels}/${diagnostics.connectedPeers} healthy
</span>
</div>
</div>
</div>
`;
// Recommendations
if (diagnostics.recommendations.length > 0) {
contentDiv.innerHTML += `
<div class="bg-blue-900 border border-blue-600 rounded p-3">
<div class="text-sm text-blue-400 mb-2">Recommendations</div>
<ul class="text-sm text-blue-300 space-y-1">
${diagnostics.recommendations.map(rec => `<li>• ${rec}</li>`).join('')}
</ul>
</div>
`;
}
resultDiv.appendChild(headerDiv);
resultDiv.appendChild(contentDiv);
// Add close button
const closeBtn = document.createElement('button');
closeBtn.id = `close-btn-${resultId}`;
closeBtn.className = 'ml-2 px-2 py-1 text-xs bg-gray-500 text-white rounded hover:bg-gray-600';
closeBtn.textContent = 'Close';
closeBtn.addEventListener('click', () => {
closeDiagnosticResult(resultId);
});
buttonContainer.appendChild(closeBtn);
// Add to diagnostics results container
const resultsContainer = document.getElementById('diagnostics-results');
if (resultsContainer) {
resultsContainer.insertBefore(resultDiv, resultsContainer.firstChild);
}
// Scroll to top of results
resultDiv.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
// Setup Enter key handlers for diagnostics forms
function setupDiagnosticsEnterHandlers() {
// Return early if handlers have already been set up
@@ -756,6 +926,7 @@ window.fetchBandwidth = fetchBandwidth;
window.renderDiagnostics = renderDiagnostics;
window.cancelStream = cancelStream;
window.closeDiagnosticResult = closeDiagnosticResult;
window.runInviteDiagnostics = runInviteDiagnostics;
// Setup Enter key handlers when DOM is ready
if (document.readyState === 'loading') {
+270 -73
View File
@@ -303,6 +303,112 @@ async function main() {
}
let dnsPass = null;
let core = null;
// Helper function to safely get dnsPass and ensure state consistency
function getDnsPass() {
// Always prioritize state.dnsPass as the source of truth
if (state.dnsPass && !dnsPass) {
dnsPass = state.dnsPass;
logDebug('Swarm', 'Synchronized local dnsPass from state');
} else if (dnsPass && !state.dnsPass) {
state.dnsPass = dnsPass;
logDebug('Swarm', 'Synchronized state.dnsPass from local');
} else if (dnsPass !== state.dnsPass) {
// They differ - state.dnsPass takes precedence
logWarn('Swarm', 'dnsPass state inconsistency detected, using state.dnsPass as authoritative');
dnsPass = state.dnsPass;
}
return dnsPass || state.dnsPass;
}
// Helper function to safely set dnsPass and ensure state consistency
function setDnsPass(newPass) {
dnsPass = newPass;
state.dnsPass = newPass;
if (newPass) {
core = newPass.base;
logDebug('Swarm', 'dnsPass and core updated consistently');
}
}
// Comprehensive invite diagnostics function
function diagnoseInviteIssues() {
const diagnostics = {
timestamp: new Date().toISOString(),
nodeType: isMaster ? 'master' : 'joiner',
dnsPassInitialized: !!getDnsPass(),
connectedPeers: connectedPeers.size,
failedInvitePeers: Array.from(failedInvitePeers),
pendingInviteAcks: Array.from(pendingInviteAcks.keys()),
inviteChannels: {},
requestChannels: {},
connectionIssues: [],
recommendations: []
};
// Check invite channels for all peers
for (const peerId of connectedPeers) {
const inviteChannelInfo = channelManager.getChannelInfo(CORE_DOMAIN, 'invite');
const requestChannelInfo = channelManager.getChannelInfo(CORE_DOMAIN, 'request');
const invitePeerChannel = inviteChannelInfo?.peerChannels?.get(peerId);
const requestPeerChannel = requestChannelInfo?.peerChannels?.get(peerId);
diagnostics.inviteChannels[peerId] = {
exists: !!invitePeerChannel,
opened: invitePeerChannel?.channel?.opened || false,
localOpened: invitePeerChannel?.localOpened || false,
remoteOpened: invitePeerChannel?.remoteOpened || false
};
diagnostics.requestChannels[peerId] = {
exists: !!requestPeerChannel,
opened: requestPeerChannel?.channel?.opened || false,
localOpened: requestPeerChannel?.localOpened || false,
remoteOpened: requestPeerChannel?.remoteOpened || false
};
// Check for connection issues
const channels = peerChannels.get(peerId);
if (channels?.conn) {
const conn = channels.conn;
if (conn.destroyed) {
diagnostics.connectionIssues.push(`${peerId}: connection destroyed`);
} else if (conn.socket && conn.socket.destroyed) {
diagnostics.connectionIssues.push(`${peerId}: socket destroyed`);
}
} else {
diagnostics.connectionIssues.push(`${peerId}: no connection object`);
}
}
// Generate recommendations
if (!diagnostics.dnsPassInitialized && !isMaster) {
diagnostics.recommendations.push('Joiner node has not received invite yet - check master node and network connectivity');
}
if (diagnostics.failedInvitePeers.length > 0) {
diagnostics.recommendations.push(`${diagnostics.failedInvitePeers.length} peers have failed invite attempts - may need manual intervention or network issues`);
}
if (diagnostics.pendingInviteAcks.length > 0) {
diagnostics.recommendations.push(`${diagnostics.pendingInviteAcks.length} invites pending acknowledgment - check network latency or peer responsiveness`);
}
if (diagnostics.connectionIssues.length > 0) {
diagnostics.recommendations.push(`${diagnostics.connectionIssues.length} connection issues detected - check network stability`);
}
const healthyChannels = Object.values(diagnostics.inviteChannels).filter(ch => ch.opened).length;
if (healthyChannels < connectedPeers.size) {
diagnostics.recommendations.push(`${connectedPeers.size - healthyChannels} invite channels not fully open - may cause invite delivery issues`);
}
return diagnostics;
}
// Make diagnostics function available globally for admin interface
state.diagnoseInviteIssues = diagnoseInviteIssues;
const connectedPeers = new Set();
const peerChannels = new Map();
const failedInvitePeers = new Set(); // Track peers that cannot provide invites
@@ -326,8 +432,8 @@ async function main() {
logWarn('Swarm', 'Ignoring invite as this is the master');
return;
}
// Check both local and state to handle edge cases
if (dnsPass || state.dnsPass) {
// Check if we already have dnsPass
if (getDnsPass()) {
logWarn('Swarm', 'Pass already initialized, ignoring invite');
return;
}
@@ -335,13 +441,12 @@ async function main() {
logDebug('Swarm', 'Processing received invite...');
// Follow Autopass pairing pattern: pair -> finished -> ready
const pair = Autopass.pair(store, invHex);
dnsPass = await pair.finished();
const newPass = await pair.finished();
logDebug('Swarm', 'Autopass pair finished');
await dnsPass.ready();
await newPass.ready();
logDebug('Swarm', 'Paired Autopass ready');
// Keep local and state in sync
state.dnsPass = dnsPass;
core = dnsPass.base;
// Use helper to ensure consistent state
setDnsPass(newPass);
logDebug('Swarm', `Core retrieved for joiner. Writable: ${core.writable}`);
// Sync initial data
await core.update();
@@ -421,9 +526,8 @@ async function main() {
if (message === 'request_invite') {
logInfo('Swarm', `Processing invite request from peer ${peerId}`);
// Use state.dnsPass as source of truth (follows Autopass pattern)
// Check both for compatibility, but state.dnsPass is authoritative
const pass = state.dnsPass || dnsPass;
// Get dnsPass safely using helper function
const pass = getDnsPass();
if (!pass) {
// This is expected for joiners that haven't received an invite yet
if (isMaster) {
@@ -496,7 +600,7 @@ async function main() {
return;
}
const pass = state.dnsPass || dnsPass;
const pass = getDnsPass();
if (pass && (isMaster || process.env.ALLOW_ANY_WRITER_INVITES === 'true')) {
// We can create an invite - send it back through the relay chain
try {
@@ -594,56 +698,88 @@ async function main() {
logInfo('Main', 'Registered core p2ns channels via channel-manager');
// Helper function to check connection stability for invite operations
function isConnectionStable(peerId, conn) {
if (!conn || conn.destroyed) {
return false;
}
// Check if peer is still in connected peers set
if (!connectedPeers.has(peerId)) {
return false;
}
// Check if peer channels exist and are valid
const channels = peerChannels.get(peerId);
if (!channels || channels.conn !== conn) {
return false;
}
// Basic connection health check - ensure socket exists and is not destroyed
if (conn.socket && conn.socket.destroyed) {
return false;
}
return true;
}
// Helper function to send proactive invite with acknowledgment and retry
async function sendProactiveInviteWithRetry(pass, peerId, conn, isReconnection, maxRetries = 3) {
const ackTimeout = parseInt(process.env.INVITE_ACK_TIMEOUT || '5000', 10);
async function sendProactiveInviteWithRetry(pass, peerId, conn, isReconnection, maxRetries = 5) {
const baseAckTimeout = parseInt(process.env.INVITE_ACK_TIMEOUT || '10000', 10); // Increased default
let retryCount = 0;
const attemptSend = async () => {
if (conn.destroyed || !connectedPeers.has(peerId)) {
logDebug('Swarm', `Connection closed, stopping invite retry for ${peerId}`);
if (!isConnectionStable(peerId, conn)) {
logDebug('Swarm', `Connection unstable or closed, stopping invite retry for ${peerId}`);
pendingInviteAcks.delete(peerId);
return;
}
if (retryCount >= maxRetries) {
logWarn('Swarm', `Max invite retries (${maxRetries}) reached for peer ${peerId}`);
failedInvitePeers.add(peerId); // Mark as failed to avoid future attempts
pendingInviteAcks.delete(peerId);
return;
}
retryCount++;
try {
const inv = await pass.createInvite();
logInfo('Swarm', `Created proactive invite for peer ${peerId} (attempt ${retryCount}/${maxRetries}): ${inv.toString('hex').substring(0, 20)}...`);
const success = channelManager.sendToPeer(CORE_DOMAIN, 'invite', peerId, inv.toString('hex'));
if (success) {
logInfo('Swarm', `Proactive invite sent to ${isReconnection ? 'reconnected' : 'new'} peer ${peerId}`);
// Set up ack timeout - retry if no ack received
// Set up ack timeout with exponential backoff
const ackTimeout = baseAckTimeout * Math.pow(1.5, retryCount - 1); // Exponential backoff
const timeout = setTimeout(() => {
const pending = pendingInviteAcks.get(peerId);
if (pending && pending.retryCount < maxRetries) {
logWarn('Swarm', `No invite_ack received from ${peerId}, retrying...`);
logWarn('Swarm', `No invite_ack received from ${peerId} within ${Math.round(ackTimeout/1000)}s, retrying (attempt ${pending.retryCount + 1}/${maxRetries})...`);
attemptSend();
} else {
} else if (pending) {
logWarn('Swarm', `No invite_ack received from ${peerId} after ${maxRetries} attempts, giving up`);
failedInvitePeers.add(peerId);
pendingInviteAcks.delete(peerId);
}
}, ackTimeout);
pendingInviteAcks.set(peerId, { timeout, retryCount });
} else {
// Retry immediately if send failed
logWarn('Swarm', `Failed to send invite to ${peerId}, retrying...`);
setTimeout(attemptSend, 1000);
// Retry with exponential backoff if send failed
const retryDelay = Math.min(1000 * Math.pow(2, retryCount - 1), 10000); // Cap at 10 seconds
logWarn('Swarm', `Failed to send invite to ${peerId}, retrying in ${retryDelay}ms...`);
setTimeout(attemptSend, retryDelay);
}
} catch (err) {
logError('Swarm', `Error sending proactive invite to peer ${peerId}: ${err.message}`);
setTimeout(attemptSend, 2000);
// Retry with exponential backoff on error
const retryDelay = Math.min(2000 * Math.pow(2, retryCount - 1), 15000); // Cap at 15 seconds
setTimeout(attemptSend, retryDelay);
}
};
await attemptSend();
}
@@ -657,7 +793,8 @@ async function main() {
if (!isMaster) {
const retryIntervalMs = parseInt(process.env.INVITE_RETRY_INTERVAL || '15000', 10);
const relayAfterAttempts = parseInt(process.env.INVITE_RELAY_AFTER_ATTEMPTS || '3', 10);
logInfo('Swarm', `Starting persistent invite retry loop (interval: ${retryIntervalMs}ms, relay after ${relayAfterAttempts} attempts)`);
const maxRetryAttempts = parseInt(process.env.MAX_INVITE_RETRY_ATTEMPTS || '20', 10);
logInfo('Swarm', `Starting persistent invite retry loop (interval: ${retryIntervalMs}ms, relay after ${relayAfterAttempts} attempts, max attempts: ${maxRetryAttempts})`);
persistentInviteRetryInterval = setInterval(() => {
const pass = state.dnsPass || dnsPass;
@@ -670,7 +807,15 @@ async function main() {
}
retryAttemptCount++;
// Stop after maximum attempts to avoid infinite retry
if (retryAttemptCount >= maxRetryAttempts) {
logWarn('Swarm', `Reached maximum invite retry attempts (${maxRetryAttempts}), stopping persistent retry loop`);
clearInterval(persistentInviteRetryInterval);
persistentInviteRetryInterval = null;
return;
}
// Broadcast invite request to all connected peers
if (state.broadcastInviteRequest) {
logDebug('Swarm', `Persistent retry (attempt ${retryAttemptCount}): broadcasting invite request to all peers`);
@@ -964,12 +1109,11 @@ async function main() {
core.on('append', listenerRefs.coreAppend);
}
if (isMaster) {
dnsPass = new Autopass(store);
const newPass = new Autopass(store);
logInfo('Main', 'Autopass instance created for master');
await dnsPass.ready();
await newPass.ready();
logInfo('Main', 'Autopass ready');
state.dnsPass = dnsPass;
core = dnsPass.base;
setDnsPass(newPass);
// Ensure the core is fully ready and updated from disk
await core.ready();
@@ -1243,27 +1387,45 @@ async function main() {
// Send proactive invite after waiting for bidirectional channel
const proactiveInviteDelay = parseInt(process.env.MASTER_PROACTIVE_INVITE_DELAY || '500', 10);
setTimeout(async () => {
// Double-check connection is still valid
if (conn.destroyed || !connectedPeers.has(peerId)) {
logDebug('Swarm', `Connection closed before proactive invite could be sent to ${peerId}`);
// Double-check connection is still stable
if (!isConnectionStable(peerId, conn)) {
logDebug('Swarm', `Connection unstable before proactive invite could be sent to ${peerId}`);
return;
}
// Wait for invite channel to be bidirectionally open before sending
// Ensure channels are initialized before sending invite
const inviteChannelInfo = channelManager.getChannelInfo(CORE_DOMAIN, 'invite');
const peerChannel = inviteChannelInfo?.peerChannels?.get(peerId);
if (peerChannel) {
const channelReady = await channelManager.waitForBidirectionalOpen(peerChannel, 5000);
if (!channelReady) {
logWarn('Swarm', `Invite channel not ready for peer ${peerId}, will retry`);
// Schedule retry
setTimeout(async () => {
if (!conn.destroyed && connectedPeers.has(peerId)) {
await sendProactiveInviteWithRetry(pass, peerId, conn, isReconnection, 3);
}
}, 2000);
return;
}
const requestChannelInfo = channelManager.getChannelInfo(CORE_DOMAIN, 'request');
const invitePeerChannel = inviteChannelInfo?.peerChannels?.get(peerId);
const requestPeerChannel = requestChannelInfo?.peerChannels?.get(peerId);
// Wait for invite channel to be bidirectionally open before sending (with timeout)
let inviteChannelReady = false;
if (invitePeerChannel) {
inviteChannelReady = await channelManager.waitForBidirectionalOpen(invitePeerChannel, 3000);
}
// Also check request channel readiness for acknowledgment handling
let requestChannelReady = false;
if (requestPeerChannel) {
requestChannelReady = await channelManager.waitForBidirectionalOpen(requestPeerChannel, 1000);
}
// If invite channel isn't ready but request channel is, we can still proceed
// Protomux will queue messages even if channels aren't fully open
if (!inviteChannelReady && !requestChannelReady) {
logWarn('Swarm', `Neither invite nor request channels ready for peer ${peerId}, will retry`);
// Schedule retry
setTimeout(async () => {
if (isConnectionStable(peerId, conn)) {
await sendProactiveInviteWithRetry(pass, peerId, conn, isReconnection, 3);
}
}, 2000);
return;
}
if (!inviteChannelReady) {
logDebug('Swarm', `Invite channel not ready for peer ${peerId}, but proceeding with request channel available`);
}
// Use channel-manager to send invite with acknowledgment tracking
@@ -1276,18 +1438,30 @@ async function main() {
logDebug('Swarm', 'Requesting invite from peer (parallel mode)...');
// Send request to this peer
const sendInviteRequestToPeer = (targetPeerId, targetConn) => {
const pass = state.dnsPass || dnsPass;
const sendInviteRequestToPeer = async (targetPeerId, targetConn) => {
const pass = getDnsPass();
if (pass) {
logDebug('Swarm', 'dnsPass already initialized, skipping invite request');
return;
}
if (targetConn.destroyed || !connectedPeers.has(targetPeerId)) {
logDebug('Swarm', `Connection to peer ${targetPeerId} not available`);
return;
}
// Check if request channel is available and ready
const requestChannelInfo = channelManager.getChannelInfo(CORE_DOMAIN, 'request');
const requestPeerChannel = requestChannelInfo?.peerChannels?.get(targetPeerId);
if (requestPeerChannel) {
// Give channel a moment to be ready, but don't block too long
const channelReady = await channelManager.waitForBidirectionalOpen(requestPeerChannel, 2000);
if (!channelReady) {
logDebug('Swarm', `Request channel not ready for peer ${targetPeerId}, but proceeding (protomux will queue)`);
}
}
try {
const sent = channelManager.sendToPeer(CORE_DOMAIN, 'request', targetPeerId, 'request_invite');
if (sent) {
@@ -1301,29 +1475,29 @@ async function main() {
};
// Give channels a moment to initialize before sending request
setTimeout(() => {
sendInviteRequestToPeer(peerId, conn);
setTimeout(async () => {
await sendInviteRequestToPeer(peerId, conn);
}, 100);
// Also request from all other connected peers (parallel requests)
setTimeout(() => {
const pass = state.dnsPass || dnsPass;
setTimeout(async () => {
const pass = getDnsPass();
if (pass) return; // Already got invite
for (const [otherPeerId, channels] of peerChannels) {
if (otherPeerId === peerId) continue; // Skip the peer we just connected to
if (failedInvitePeers.has(otherPeerId)) continue; // Skip failed peers
const otherConn = channels.conn;
if (otherConn && !otherConn.destroyed) {
sendInviteRequestToPeer(otherPeerId, otherConn);
await sendInviteRequestToPeer(otherPeerId, otherConn);
}
}
}, 200);
}
// Function to broadcast invite request to all connected peers
function broadcastInviteRequest() {
async function broadcastInviteRequest() {
const pass = state.dnsPass || dnsPass;
if (pass) {
logDebug('Swarm', 'Already have dnsPass, no need to broadcast invite request');
@@ -1331,19 +1505,42 @@ async function main() {
}
let sentCount = 0;
const broadcastPromises = [];
for (const [otherPeerId, channels] of peerChannels) {
if (failedInvitePeers.has(otherPeerId)) continue;
const otherConn = channels.conn;
if (!otherConn || otherConn.destroyed) continue;
try {
const sent = channelManager.sendToPeer(CORE_DOMAIN, 'request', otherPeerId, 'request_invite');
if (sent) sentCount++;
} catch (err) {
logError('Swarm', `Error broadcasting invite request to ${otherPeerId}: ${err.message}`);
}
// Check channel readiness before sending
const requestChannelInfo = channelManager.getChannelInfo(CORE_DOMAIN, 'request');
const requestPeerChannel = requestChannelInfo?.peerChannels?.get(otherPeerId);
const sendPromise = (async () => {
if (requestPeerChannel) {
// Give channel a moment to be ready, but don't block too long
const channelReady = await channelManager.waitForBidirectionalOpen(requestPeerChannel, 1000);
if (!channelReady) {
logDebug('Swarm', `Request channel not ready for broadcast to peer ${otherPeerId}, but proceeding`);
}
}
try {
const sent = channelManager.sendToPeer(CORE_DOMAIN, 'request', otherPeerId, 'request_invite');
if (sent) sentCount++;
return sent;
} catch (err) {
logError('Swarm', `Error broadcasting invite request to ${otherPeerId}: ${err.message}`);
return false;
}
})();
broadcastPromises.push(sendPromise);
}
// Wait for all broadcast attempts to complete
await Promise.allSettled(broadcastPromises);
if (sentCount > 0) {
logInfo('Swarm', `Broadcast invite request to ${sentCount} peer(s)`);