Compare commits

...

5 Commits

5 changed files with 105 additions and 41 deletions

View File

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

View File

@@ -917,43 +917,65 @@ export function formatTimeAgo(timestamp) {
// Store timestamps for real-time updates
const timeAgoElements = new Map(); // element -> { timestamp, eventId }
// Track relay publish counts per event ID from broadcastProgress events.
// Keyed by event ID → number of successful relay publishes.
const relayCountByEventId = new Map();
// Track relay publish info per event ID from broadcastProgress events.
// Keyed by event ID → { count, urls } where urls is an array of relay URLs.
const relayInfoByEventId = new Map();
// Listen for broadcast progress events to capture final relay counts.
// Listen for broadcast progress events to capture final relay counts and URLs.
// The worker emits ndkBroadcastProgress with phase 'done' containing the
// total successful count. We store it so post cards can show "21r - 1h".
// total successful count and the list of relay URLs. We store it so post
// cards can show "21r - 1h" with a hover tooltip listing the relays.
if (typeof window !== 'undefined') {
window.addEventListener('ndkBroadcastProgress', (event) => {
const d = event.detail;
if (!d || d.phase !== 'done') return;
const eventId = d.eventId;
if (!eventId) return;
relayCountByEventId.set(eventId, d.successful || 0);
relayInfoByEventId.set(eventId, {
count: d.successful || 0,
urls: Array.isArray(d.relayUrls) ? d.relayUrls : [],
});
// Update any already-rendered time elements for this event.
timeAgoElements.forEach((info, element) => {
if (info.eventId === eventId && element.isConnected) {
element.textContent = formatTimeAgoWithRelays(info.timestamp, info.eventId);
updateTimeAgoElement(element, info);
}
});
});
}
/**
* Format time ago with optional relay count prefix.
* Shows "21r - 1h" when a relay count is available, else just "1h".
* @param {number} timestamp - Unix timestamp in seconds
* @param {string} [eventId] - Optional event ID to look up relay count
* Build the hover tooltip text for a relay list.
* Shows "Published to N relays:" followed by the URL list, truncated.
* @param {number} count - Number of successful relays
* @param {string[]} urls - Array of relay URLs
* @returns {string}
*/
function formatTimeAgoWithRelays(timestamp, eventId) {
const timeStr = formatTimeAgo(timestamp);
if (eventId && relayCountByEventId.has(eventId)) {
const count = relayCountByEventId.get(eventId);
return `${count}r - ${timeStr}`;
function buildRelayTooltip(count, urls) {
if (!count || count === 0) return '';
const header = `Published to ${count} relay${count === 1 ? '' : 's'}:`;
if (!urls || urls.length === 0) return header;
// Show up to 50 relay URLs in the tooltip; beyond that, show a summary.
if (urls.length <= 50) {
return header + '\n' + urls.join('\n');
}
return header + '\n' + urls.slice(0, 50).join('\n') + `\n… and ${urls.length - 50} more`;
}
/**
* Update a time-ago element's text and tooltip from its stored info.
* @param {HTMLElement} element - The element to update
* @param {{timestamp: number, eventId: string}} info - Stored info
*/
function updateTimeAgoElement(element, info) {
const timeStr = formatTimeAgo(info.timestamp);
if (info.eventId && relayInfoByEventId.has(info.eventId)) {
const relayInfo = relayInfoByEventId.get(info.eventId);
element.textContent = `${relayInfo.count}r - ${timeStr}`;
element.title = buildRelayTooltip(relayInfo.count, relayInfo.urls);
} else {
element.textContent = timeStr;
}
return timeStr;
}
/**
@@ -964,7 +986,7 @@ function formatTimeAgoWithRelays(timestamp, eventId) {
*/
export function registerTimeAgo(element, timestamp, eventId) {
timeAgoElements.set(element, { timestamp, eventId });
element.textContent = formatTimeAgoWithRelays(timestamp, eventId);
updateTimeAgoElement(element, { timestamp, eventId });
}
/**
@@ -974,7 +996,7 @@ export function registerTimeAgo(element, timestamp, eventId) {
export function updateTimeAgos() {
timeAgoElements.forEach((info, element) => {
if (element.isConnected) {
element.textContent = formatTimeAgoWithRelays(info.timestamp, info.eventId);
updateTimeAgoElement(element, info);
} else {
// Clean up detached elements
timeAgoElements.delete(element);

View File

@@ -1,5 +1,5 @@
{
"VERSION": "v0.7.71",
"VERSION_NUMBER": "0.7.71",
"BUILD_DATE": "2026-06-30T13:33:08.793Z"
"VERSION": "v0.7.76",
"VERSION_NUMBER": "0.7.76",
"BUILD_DATE": "2026-06-30T14:13:46.694Z"
}

View File

@@ -6204,6 +6204,20 @@ async function handlePublish(requestId, event, port) {
const allUrls = [...new Set([...outboxWriteUrls, ...activeBroadcastUrls])];
if (NDKRelaySet?.fromRelayUrls) {
targetRelaySet = NDKRelaySet.fromRelayUrls(allUrls, ndk);
// Set the connection timeout on all relays in the set to match
// the per-relay publish timeout. NDK's default
// connectionTimeout is 4400ms — we increase it so relays have
// time to connect even when hundreds are competing for browser
// connection slots.
if (targetRelaySet?.relays) {
for (const relay of targetRelaySet.relays) {
try {
if (relay.connectionTimeout !== undefined) {
relay.connectionTimeout = 60000;
}
} catch (_) { /* relay may be read-only */ }
}
}
console.log(`[Worker] Broadcasting to ${allUrls.length} relays ` +
`(${activeBroadcastUrls.length} broadcast + ${outboxWriteUrls.length} outbox, ` +
`${skippedRelayUrls.size} skipped)`);
@@ -6282,24 +6296,50 @@ async function handlePublish(requestId, event, port) {
}
// Publish to relays (with broadcast relay set if broadcasting, else default outbox).
// When broadcasting to many relays (potentially hundreds), use a much longer
// timeout than NDK's default 4400ms — temporary relays need time to establish
// WebSocket connections. Use 30s for broadcasts, default for normal publishes.
// Per-relay timeout for broadcasts: 60 seconds. NDK may not be able to handle
// hundreds of simultaneous WebSocket connections in true parallel — the browser
// has connection limits and NDK's pool may serialize some operations. A 60s
// per-relay timeout gives each relay ample time to connect and publish even when
// queued behind hundreds of others.
// requiredRelayCount=1 so NDK doesn't throw if only 1 of 600 relays succeeds.
const BROADCAST_TIMEOUT_MS = 30000;
const publishTimeoutMs = isBroadcast ? BROADCAST_TIMEOUT_MS : undefined;
const BROADCAST_PER_RELAY_TIMEOUT_MS = 60000;
const publishTimeoutMs = isBroadcast ? BROADCAST_PER_RELAY_TIMEOUT_MS : undefined;
const publishRequiredCount = isBroadcast ? 1 : undefined;
let relaySet;
try {
relaySet = await ndkEvent.publish(targetRelaySet, publishTimeoutMs, publishRequiredCount);
} catch (publishError) {
// NDK throws NDKPublishError if requiredRelayCount isn't met. With
// requiredRelayCount=1 this shouldn't happen for broadcasts, but handle
// it gracefully — the live progress listeners already captured whatever
// succeeded. Log and continue so we still report results to the page.
console.warn('[Worker] publish() threw (some relays may still have succeeded):', publishError?.message || publishError);
relaySet = null;
if (isBroadcast) {
// For broadcasts, race the publish against an overall deadline.
// NDK's publish() uses Promise.all internally, so it waits for ALL
// relays to resolve. The overall deadline is totalRelays × perRelayTimeout
// as a safety cap. With 600 relays × 60s = 36000s (10 hours) — this is
// effectively "wait until all relays finish" since the deadline will never
// fire before the per-relay timeouts resolve all promises.
const BROADCAST_OVERALL_DEADLINE_MS = Math.max(30000, totalTarget * BROADCAST_PER_RELAY_TIMEOUT_MS);
const publishPromise = ndkEvent.publish(targetRelaySet, publishTimeoutMs, publishRequiredCount)
.catch((err) => {
// NDKPublishError is expected when not all relays succeed —
// the live listeners already captured the successes.
console.warn('[Worker] Broadcast publish() resolved with error (expected):', err?.message || err);
return null;
});
const deadlinePromise = new Promise((resolve) => {
setTimeout(() => resolve('__DEADLINE__'), BROADCAST_OVERALL_DEADLINE_MS);
});
const result = await Promise.race([publishPromise, deadlinePromise]);
if (result === '__DEADLINE__') {
console.log(`[Worker] Broadcast overall deadline (${BROADCAST_OVERALL_DEADLINE_MS}ms) reached — ` +
`${publishedSoFar.size} succeeded, ${failedSoFar.size + timedOutSoFar.size} failed/timeout, ` +
`${totalTarget - publishedSoFar.size - failedSoFar.size - timedOutSoFar.size} still pending`);
}
relaySet = result && result !== '__DEADLINE__' ? result : null;
} else {
// Normal publish — await the full result as before.
try {
relaySet = await ndkEvent.publish(targetRelaySet, publishTimeoutMs, publishRequiredCount);
} catch (publishError) {
console.warn('[Worker] publish() threw:', publishError?.message || publishError);
relaySet = null;
}
}
// Final broadcast progress message with complete results.
@@ -6315,6 +6355,7 @@ async function handlePublish(requestId, event, port) {
successful: publishedSoFar.size,
failed: totalFailed,
latestRelay: null,
relayUrls: Array.from(publishedSoFar),
});
// Clean up listeners so the NDKEvent can be garbage-collected.
try {
@@ -6330,6 +6371,9 @@ async function handlePublish(requestId, event, port) {
// We use the relaySet result to count successful relays.
if (!isBroadcast) {
const successCount = relaySet ? relaySet.size : 0;
const successUrls = relaySet
? Array.from(relaySet).map(r => r?.url || '').filter(Boolean)
: [];
broadcast({
type: 'broadcastProgress',
sessionId: publishSessionId,
@@ -6340,6 +6384,7 @@ async function handlePublish(requestId, event, port) {
successful: successCount,
failed: 0,
latestRelay: null,
relayUrls: successUrls,
});
}

View File

@@ -833,12 +833,8 @@ import { initPostCards } from './js/post-interactions2.mjs';
: '';
spanBroadcastStatus.textContent = `📡 ${d.successful}/${d.total} relays · ${shortRelay}`;
} 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)` : '');
setTimeout(() => {
if (spanBroadcastStatus.textContent.startsWith('✅')) {
spanBroadcastStatus.textContent = '';
}
}, 10000);
}
});