|
|
|
|
@@ -6280,35 +6280,97 @@ 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));
|
|
|
|
|
}
|
|
|
|
|
const 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++) {
|
|
|
|
|
const batch = batches[bi];
|
|
|
|
|
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.
|
|
|
|
|
}
|
|
|
|
|
// 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,
|
|
|
|
|
});
|
|
|
|
|
console.log(`[Worker] Batch ${bi + 1}/${totalBatches} complete — ` +
|
|
|
|
|
`${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 +6387,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 +6487,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)`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|