Compare commits

...

7 Commits

4 changed files with 169 additions and 48 deletions

View File

@@ -393,6 +393,8 @@ function handleWorkerMessage(event) {
latestRelay: message.latestRelay,
latestError: message.latestError,
relayUrls: message.relayUrls || [],
batchCurrent: message.batchCurrent,
batchTotal: message.batchTotal,
}
}));
} else if (message.type === 'startupStatus') {

View File

@@ -1,5 +1,5 @@
{
"VERSION": "v0.7.77",
"VERSION_NUMBER": "0.7.77",
"BUILD_DATE": "2026-06-30T14:21:55.493Z"
"VERSION": "v0.7.84",
"VERSION_NUMBER": "0.7.84",
"BUILD_DATE": "2026-06-30T15:36:36.887Z"
}

View File

@@ -6210,8 +6210,14 @@ async function handlePublish(requestId, event, port) {
const publishSessionId = `pub_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
const publishedSoFar = new Set();
const failedSoFar = new Set(); // explicit rejections (auth-required, blocked, etc.)
const timedOutSoFar = new Set(); // timeouts — NOT skip-marked (could be slow connect)
const timedOutSoFar = new Set(); // timeouts
const totalTarget = isBroadcast ? allBroadcastUrls.length : 0;
// Track current batch for progress display. Updated by the batch loop.
let currentBatch = 0;
let totalBatches = 0;
// Debug: track publish start time for timing analysis (defined in outer
// scope so the batch loop and silent-failure detection can access it).
const publishStartTime = Date.now();
if (isBroadcast && totalTarget > 0) {
console.log(`[Worker] Broadcasting to ${allBroadcastUrls.length} relays ` +
@@ -6233,8 +6239,10 @@ async function handlePublish(requestId, event, port) {
ndkEvent.on('relay:published', (relay) => {
const url = relay?.url || String(relay || '');
if (!url) return;
const elapsed = ((Date.now() - publishStartTime) / 1000).toFixed(1);
publishedSoFar.add(url);
trackRelayWrite(url);
console.log(`[Worker][broadcast] ✅ ${url} published (${elapsed}s) — batch ${currentBatch}/${totalBatches}, ${publishedSoFar.size}/${totalTarget} total`);
broadcast({
type: 'broadcastProgress',
sessionId: publishSessionId,
@@ -6243,6 +6251,8 @@ async function handlePublish(requestId, event, port) {
successful: publishedSoFar.size,
failed: failedSoFar.size + timedOutSoFar.size,
latestRelay: url,
batchCurrent: currentBatch,
batchTotal: totalBatches,
});
});
@@ -6250,11 +6260,14 @@ async function handlePublish(requestId, event, port) {
const url = relay?.url || String(relay || '');
if (!url) return;
const errMsg = err?.message || String(err) || '';
const elapsed = ((Date.now() - publishStartTime) / 1000).toFixed(1);
const isTimeout = /timeout/i.test(errMsg);
if (isTimeout) {
timedOutSoFar.add(url);
console.log(`[Worker][broadcast] ⏱️ ${url} TIMEOUT (${elapsed}s) — batch ${currentBatch}/${totalBatches}`);
} else {
failedSoFar.add(url);
console.log(`[Worker][broadcast] ❌ ${url} REJECTED (${elapsed}s): ${errMsg} — batch ${currentBatch}/${totalBatches}`);
}
broadcast({
type: 'broadcastProgress',
@@ -6265,6 +6278,8 @@ async function handlePublish(requestId, event, port) {
failed: failedSoFar.size + timedOutSoFar.size,
latestRelay: url,
latestError: errMsg,
batchCurrent: currentBatch,
batchTotal: totalBatches,
});
});
}
@@ -6280,35 +6295,114 @@ async function handlePublish(requestId, event, port) {
let relaySet = null;
if (isBroadcast && totalTarget > 0 && NDKRelaySet?.fromRelayUrls) {
// Split all URLs into batches.
const batches = [];
for (let i = 0; i < allBroadcastUrls.length; i += BATCH_SIZE) {
batches.push(allBroadcastUrls.slice(i, i + BATCH_SIZE));
}
console.log(`[Worker] Publishing in ${batches.length} batches of ${BATCH_SIZE} (per-relay timeout: ${PER_RELAY_TIMEOUT_MS}ms)`);
for (let bi = 0; bi < batches.length; bi++) {
const batch = batches[bi];
const batchSet = NDKRelaySet.fromRelayUrls(batch, ndk);
// Set connection timeout on each relay in this batch.
if (batchSet?.relays) {
for (const relay of batchSet.relays) {
try {
if (relay.connectionTimeout !== undefined) {
relay.connectionTimeout = PER_RELAY_TIMEOUT_MS;
}
} catch (_) { /* relay may be read-only */ }
}
}
// --- Phase 1: Publish to the user's standard outbox relays first ---
// This ensures the post lands on the user's primary relays quickly,
// and lets us send the response back to the page so the composer
// clears immediately. The broadcast relay batches run afterward.
if (outboxWriteUrls.length > 0) {
console.log(`[Worker] Phase 1: Publishing to ${outboxWriteUrls.length} outbox relays first`);
try {
await ndkEvent.publish(batchSet, PER_RELAY_TIMEOUT_MS, 1);
} catch (batchErr) {
// NDKPublishError is expected when not all relays in the batch succeed.
// The live listeners already captured successes/failures.
const outboxSet = NDKRelaySet.fromRelayUrls(outboxWriteUrls, ndk);
await ndkEvent.publish(outboxSet, 10000, 1);
} catch (outboxErr) {
console.warn('[Worker] Outbox publish error (expected if some relays fail):', outboxErr?.message || outboxErr);
}
console.log(`[Worker] Phase 1 complete: ${publishedSoFar.size} outbox relays succeeded`);
}
// --- Send response back to the page NOW so the composer clears ---
// The post is already on the user's outbox relays. The broadcast
// batches continue in the background.
const earlyRelayResults = {
successful: Array.from(publishedSoFar),
failed: [],
};
port.postMessage({
type: 'response',
requestId: requestId,
data: {
success: true,
relayResults: earlyRelayResults,
totalRelays: earlyRelayResults.successful.length,
}
});
console.log('[Worker] Sent early publish response — composer can clear now');
// --- Phase 2: Broadcast to remaining relays in batches (background) ---
// Only publish to relays that aren't already in the outbox set
// (those were already published in Phase 1).
const broadcastOnlyUrls = activeBroadcastUrls.filter(u => !outboxWriteUrls.includes(u));
if (broadcastOnlyUrls.length > 0) {
const batches = [];
for (let i = 0; i < broadcastOnlyUrls.length; i += BATCH_SIZE) {
batches.push(broadcastOnlyUrls.slice(i, i + BATCH_SIZE));
}
totalBatches = batches.length;
console.log(`[Worker] Phase 2: Broadcasting to ${broadcastOnlyUrls.length} relays in ${totalBatches} batches of ${BATCH_SIZE}`);
// Emit a progress event showing batch info before starting batches.
broadcast({
type: 'broadcastProgress',
sessionId: publishSessionId,
phase: 'progress',
total: totalTarget,
successful: publishedSoFar.size,
failed: failedSoFar.size + timedOutSoFar.size,
latestRelay: null,
batchCurrent: 0,
batchTotal: totalBatches,
});
for (let bi = 0; bi < batches.length; bi++) {
currentBatch = bi + 1; // update before publish so per-relay events include it
const batch = batches[bi];
const batchStartTime = Date.now();
console.log(`[Worker][broadcast] Batch ${bi + 1}/${totalBatches} starting — ${batch.length} relays: ${batch.slice(0, 5).join(', ')}${batch.length > 5 ? '...' : ''}`);
const batchSet = NDKRelaySet.fromRelayUrls(batch, ndk);
if (batchSet?.relays) {
for (const relay of batchSet.relays) {
try {
if (relay.connectionTimeout !== undefined) {
relay.connectionTimeout = PER_RELAY_TIMEOUT_MS;
}
} catch (_) { /* relay may be read-only */ }
}
}
try {
await ndkEvent.publish(batchSet, PER_RELAY_TIMEOUT_MS, 1);
} catch (batchErr) {
// Expected — some relays in this batch may fail.
}
// --- Detect silent failures ---
// NDK's relay.publish() can resolve false without emitting
// relay:publish:failed (e.g., DNS errors, connection refused,
// HTTP handshake failures). These relays are not in any of
// our tracked sets. Mark them as timed out so they get
// skip-marked and don't waste time on the next publish.
for (const url of batch) {
if (!publishedSoFar.has(url) && !failedSoFar.has(url) && !timedOutSoFar.has(url)) {
timedOutSoFar.add(url);
const elapsed = ((Date.now() - publishStartTime) / 1000).toFixed(1);
console.log(`[Worker][broadcast] ⏱️ ${url} SILENT TIMEOUT (${elapsed}s) — batch ${currentBatch}/${totalBatches} (no response, likely connection failure)`);
}
}
// Emit a progress event with batch info after each batch.
broadcast({
type: 'broadcastProgress',
sessionId: publishSessionId,
phase: 'progress',
total: totalTarget,
successful: publishedSoFar.size,
failed: failedSoFar.size + timedOutSoFar.size,
latestRelay: null,
batchCurrent: bi + 1,
batchTotal: totalBatches,
});
const batchElapsed = ((Date.now() - batchStartTime) / 1000).toFixed(1);
console.log(`[Worker] Batch ${bi + 1}/${totalBatches} complete (${batchElapsed}s) — ` +
`${publishedSoFar.size}/${totalTarget} total succeeded, ` +
`${failedSoFar.size + timedOutSoFar.size} failed/timeout so far`);
}
console.log(`[Worker] Batch ${bi + 1}/${batches.length} complete — ` +
`${publishedSoFar.size}/${totalTarget} succeeded, ` +
`${failedSoFar.size + timedOutSoFar.size} failed/timeout so far`);
}
// Final broadcast progress message with complete results.
@@ -6325,14 +6419,37 @@ async function handlePublish(requestId, event, port) {
latestRelay: null,
relayUrls: Array.from(publishedSoFar),
});
// Clean up listeners so the NDKEvent can be garbage-collected.
try {
ndkEvent.removeAllListeners('relay:published');
ndkEvent.removeAllListeners('relay:publish:failed');
} catch (_cleanupErr) {
// removeAllListeners may not exist on all EventEmitter impls; ignore.
} catch (_cleanupErr) { /* ignore */ }
// --- Failure marking (background, after all batches) ---
// Skip-mark both explicit rejections AND timeouts. A relay that
// times out is either down, too slow, or unreachable — skip it
// next time to avoid wasting time on it.
const allFailed = [...Array.from(failedSoFar), ...Array.from(timedOutSoFar)];
const newSkips = allFailed
.filter(url => broadcastRelayUrls.has(url) && !skippedRelayUrls.has(url));
if (newSkips.length > 0) {
for (const url of newSkips) {
const isTimeout = timedOutSoFar.has(url);
skippedRelayUrls.set(url, {
reason: isTimeout ? 'publish-timeout' : 'publish-rejected',
timestamp: Math.floor(Date.now() / 1000),
});
}
scheduleSkipMarkSave();
const timeoutCount = newSkips.filter(u => timedOutSoFar.has(u)).length;
const rejectCount = newSkips.length - timeoutCount;
console.log(`[Worker] Skip-marked ${newSkips.length} failed broadcast relays ` +
`(${rejectCount} rejected, ${timeoutCount} timed out)`);
}
// relaySet stays null for broadcasts — we use publishedSoFar for results.
// Return early — we already sent the response above. The rest of
// handlePublish (relayResults, kind 10002 sync, etc.) is skipped for
// broadcasts since we've handled everything here.
return;
} else {
// Normal (non-broadcast) publish — await the full result as before.
try {
@@ -6402,27 +6519,25 @@ async function handlePublish(requestId, event, port) {
console.log('[Worker] ❌ Failed relays:', relayResults.failed.length);
// --- Failure marking for broadcast relays ---------------------------------
// Persist skip-marks ONLY for broadcast relays that EXPLICITLY rejected
// the publish (auth-required, blocked, invalid, etc.). We do NOT skip-mark
// relays that timed out — with hundreds of relays, a timeout often means
// the relay was slow to connect, not that it's broken. The relay:publish:failed
// listener above separates timeouts (timedOutSoFar) from explicit rejections
// (failedSoFar) by checking the error message for "timeout".
// Skip-mark both explicit rejections AND timeouts. A relay that times
// out is either down, too slow, or unreachable — skip it next time.
if (isBroadcast) {
const newSkips = Array.from(failedSoFar)
const allFailed = [...Array.from(failedSoFar), ...Array.from(timedOutSoFar)];
const newSkips = allFailed
.filter(url => broadcastRelayUrls.has(url) && !skippedRelayUrls.has(url));
if (newSkips.length > 0) {
for (const url of newSkips) {
const isTimeout = timedOutSoFar.has(url);
skippedRelayUrls.set(url, {
reason: 'publish-rejected',
reason: isTimeout ? 'publish-timeout' : 'publish-rejected',
timestamp: Math.floor(Date.now() / 1000),
});
}
scheduleSkipMarkSave();
console.log(`[Worker] Skip-marked ${newSkips.length} rejected broadcast relays ` +
`(${timedOutSoFar.size} timeouts excluded from skip-marking)`);
} else if (timedOutSoFar.size > 0) {
console.log(`[Worker] ${timedOutSoFar.size} broadcast relays timed out (not skip-marked — will retry next publish)`);
const timeoutCount2 = newSkips.filter(u => timedOutSoFar.has(u)).length;
const rejectCount2 = newSkips.length - timeoutCount2;
console.log(`[Worker] Skip-marked ${newSkips.length} failed broadcast relays ` +
`(${rejectCount2} rejected, ${timeoutCount2} timed out)`);
}
}

View File

@@ -831,7 +831,11 @@ import { initPostCards } from './js/post-interactions2.mjs';
const shortRelay = d.latestRelay
? d.latestRelay.replace(/^wss?:\/\//, '').replace(/\/.*$/, '').slice(0, 30)
: '';
spanBroadcastStatus.textContent = `📡 ${d.successful}/${d.total} relays · ${shortRelay}`;
const batchInfo = (d.batchTotal && d.batchTotal > 0)
? ` · ${d.batchCurrent}/${d.batchTotal} batches`
: '';
const relayInfo = shortRelay ? ` · ${shortRelay}` : '';
spanBroadcastStatus.textContent = `📡 ${d.successful}/${d.total} relays${batchInfo}${relayInfo}`;
} else if (d.phase === 'done') {
// Keep the result displayed until the next publish overwrites it.
spanBroadcastStatus.textContent = `${d.successful}/${d.total} relays` + (d.failed > 0 ? ` (${d.failed} failed)` : '');