improvent for invites
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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)`);
|
||||
|
||||
Reference in New Issue
Block a user