Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7cfd43b91b | ||
|
|
777ab312a5 | ||
|
|
2763dd9d6a | ||
|
|
a20b28cd21 | ||
|
|
456f0e1f2e | ||
|
|
4abb4bf33c |
@@ -392,6 +392,7 @@ function handleWorkerMessage(event) {
|
||||
failed: message.failed,
|
||||
latestRelay: message.latestRelay,
|
||||
latestError: message.latestError,
|
||||
relayUrls: message.relayUrls || [],
|
||||
}
|
||||
}));
|
||||
} else if (message.type === 'startupStatus') {
|
||||
|
||||
@@ -915,16 +915,78 @@ export function formatTimeAgo(timestamp) {
|
||||
}
|
||||
|
||||
// Store timestamps for real-time updates
|
||||
const timeAgoElements = new Map(); // element -> timestamp
|
||||
const timeAgoElements = new Map(); // element -> { timestamp, eventId }
|
||||
|
||||
// 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 and URLs.
|
||||
// The worker emits ndkBroadcastProgress with phase 'done' containing the
|
||||
// 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;
|
||||
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) {
|
||||
updateTimeAgoElement(element, info);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register an element for time updates
|
||||
* @param {HTMLElement} element - The element to update
|
||||
* @param {number} timestamp - Unix timestamp in seconds
|
||||
* @param {string} [eventId] - Optional event ID for relay count display
|
||||
*/
|
||||
export function registerTimeAgo(element, timestamp) {
|
||||
timeAgoElements.set(element, timestamp);
|
||||
element.textContent = formatTimeAgo(timestamp);
|
||||
export function registerTimeAgo(element, timestamp, eventId) {
|
||||
timeAgoElements.set(element, { timestamp, eventId });
|
||||
updateTimeAgoElement(element, { timestamp, eventId });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -932,9 +994,9 @@ export function registerTimeAgo(element, timestamp) {
|
||||
* Call this periodically (e.g., every 30 seconds)
|
||||
*/
|
||||
export function updateTimeAgos() {
|
||||
timeAgoElements.forEach((timestamp, element) => {
|
||||
timeAgoElements.forEach((info, element) => {
|
||||
if (element.isConnected) {
|
||||
element.textContent = formatTimeAgo(timestamp);
|
||||
updateTimeAgoElement(element, info);
|
||||
} else {
|
||||
// Clean up detached elements
|
||||
timeAgoElements.delete(element);
|
||||
@@ -1085,10 +1147,12 @@ export function renderFooterRow(eventId, eventData, options = {}) {
|
||||
container.appendChild(nutzapItem);
|
||||
container.appendChild(midControls);
|
||||
|
||||
// Time as the final item in the same row
|
||||
// Time as the final item in the same row.
|
||||
// Pass eventId so registerTimeAgo can prepend the relay count
|
||||
// (e.g., "21r - 1h") when a broadcastProgress 'done' event arrives.
|
||||
const timeEl = document.createElement('span');
|
||||
timeEl.className = 'divPostTime';
|
||||
registerTimeAgo(timeEl, eventData.created_at);
|
||||
registerTimeAgo(timeEl, eventData.created_at, eventId);
|
||||
container.appendChild(timeEl);
|
||||
|
||||
footerRow.appendChild(container);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"VERSION": "v0.7.68",
|
||||
"VERSION_NUMBER": "0.7.68",
|
||||
"BUILD_DATE": "2026-06-30T13:15:02.809Z"
|
||||
"VERSION": "v0.7.74",
|
||||
"VERSION_NUMBER": "0.7.74",
|
||||
"BUILD_DATE": "2026-06-30T14:06:37.971Z"
|
||||
}
|
||||
|
||||
@@ -4616,8 +4616,13 @@ async function handleWalletPayInvoice(requestId, invoice, mint, port) {
|
||||
const meltQuote = await wallet.createMeltQuote(pr);
|
||||
const quoteAmount = Number(meltQuote?.amount || 0);
|
||||
const quoteFeeReserve = Number(meltQuote?.fee_reserve || 0);
|
||||
// The mint's quoted fee_reserve is an estimate; the actual melt fee can be
|
||||
// slightly higher, causing "not enough inputs provided for melt" errors.
|
||||
// Add a safety buffer — any overpayment is returned as change proofs.
|
||||
const FEE_RESERVE_BUFFER_SATS = 10;
|
||||
const required = quoteAmount + quoteFeeReserve;
|
||||
log('mint attempt:quote', { mintUrl, quoteAmount, quoteFeeReserve, required });
|
||||
const sendAmount = required + FEE_RESERVE_BUFFER_SATS;
|
||||
log('mint attempt:quote', { mintUrl, quoteAmount, quoteFeeReserve, required, sendAmount });
|
||||
|
||||
if (!Number.isFinite(required) || required <= 0) {
|
||||
log('mint attempt:invalid-quote', { mintUrl, required });
|
||||
@@ -4625,28 +4630,28 @@ async function handleWalletPayInvoice(requestId, invoice, mint, port) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (proofBalance < required) {
|
||||
log('mint attempt:insufficient', { mintUrl, proofBalance, required });
|
||||
if (proofBalance < sendAmount) {
|
||||
log('mint attempt:insufficient', { mintUrl, proofBalance, sendAmount });
|
||||
attemptErrors.push({ mint: mintUrl, message: `Insufficient funds on mint (${proofBalance} sats)` });
|
||||
continue;
|
||||
}
|
||||
|
||||
const sendResult = await wallet.send(required, proofs);
|
||||
const sendResult = await wallet.send(sendAmount, proofs);
|
||||
const keep = Array.isArray(sendResult?.keep) ? sendResult.keep : [];
|
||||
const sendProofs = Array.isArray(sendResult?.send) ? sendResult.send : [];
|
||||
log('mint attempt:send-split', { mintUrl, keepCount: keep.length, sendCount: sendProofs.length });
|
||||
log('mint attempt:send-split', { mintUrl, keepCount: keep.length, sendCount: sendProofs.length, sendAmount });
|
||||
|
||||
const meltResult = await wallet.meltProofs(meltQuote, sendProofs);
|
||||
const change = Array.isArray(meltResult?.change) ? meltResult.change : [];
|
||||
upsertMintProofs(mintUrl, [...keep, ...change]);
|
||||
|
||||
const changeAmount = getProofTotal(change);
|
||||
const spentAmount = Math.max(0, required - changeAmount);
|
||||
const spentAmount = Math.max(0, sendAmount - changeAmount);
|
||||
paidAmountSats = Number.isFinite(quoteAmount) && quoteAmount > 0
|
||||
? quoteAmount
|
||||
: spentAmount;
|
||||
|
||||
log('mint attempt:success', { mintUrl, paidAmountSats, changeAmount, preimage: meltResult?.preimage || null });
|
||||
log('mint attempt:success', { mintUrl, paidAmountSats, changeAmount, spentAmount, preimage: meltResult?.preimage || null });
|
||||
|
||||
usedMint = mintUrl;
|
||||
payResult = meltResult;
|
||||
@@ -6199,6 +6204,19 @@ 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 (5s). NDK's default
|
||||
// connectionTimeout is 4400ms — we set it to 5000ms so the
|
||||
// connection attempt and publish share the same 5s budget.
|
||||
if (targetRelaySet?.relays) {
|
||||
for (const relay of targetRelaySet.relays) {
|
||||
try {
|
||||
if (relay.connectionTimeout !== undefined) {
|
||||
relay.connectionTimeout = 5000;
|
||||
}
|
||||
} catch (_) { /* relay may be read-only */ }
|
||||
}
|
||||
}
|
||||
console.log(`[Worker] Broadcasting to ${allUrls.length} relays ` +
|
||||
`(${activeBroadcastUrls.length} broadcast + ${outboxWriteUrls.length} outbox, ` +
|
||||
`${skippedRelayUrls.size} skipped)`);
|
||||
@@ -6277,24 +6295,49 @@ 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: 5 seconds. This covers both the WebSocket
|
||||
// connection attempt and the publish OK response for each relay. NDK runs all
|
||||
// relay publishes in parallel via Promise.all, so with 600 relays the overall
|
||||
// time is ~5s (the slowest relay), not 600×5s.
|
||||
// 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 = 5000;
|
||||
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 — in practice all relays run in parallel so the publish
|
||||
// should complete in ~perRelayTimeout, but the deadline ensures we never
|
||||
// hang indefinitely if something goes wrong.
|
||||
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.
|
||||
@@ -6303,11 +6346,14 @@ async function handlePublish(requestId, event, port) {
|
||||
broadcast({
|
||||
type: 'broadcastProgress',
|
||||
sessionId: publishSessionId,
|
||||
eventId: ndkEvent.id || null,
|
||||
eventKind: event.kind,
|
||||
phase: 'done',
|
||||
total: totalTarget,
|
||||
successful: publishedSoFar.size,
|
||||
failed: totalFailed,
|
||||
latestRelay: null,
|
||||
relayUrls: Array.from(publishedSoFar),
|
||||
});
|
||||
// Clean up listeners so the NDKEvent can be garbage-collected.
|
||||
try {
|
||||
@@ -6318,6 +6364,28 @@ async function handlePublish(requestId, event, port) {
|
||||
}
|
||||
}
|
||||
|
||||
// For non-broadcast publishes, emit a simplified broadcastProgress 'done'
|
||||
// event so post cards can still show the relay count (e.g., "3r - 1h").
|
||||
// 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,
|
||||
eventId: ndkEvent.id || null,
|
||||
eventKind: event.kind,
|
||||
phase: 'done',
|
||||
total: successCount,
|
||||
successful: successCount,
|
||||
failed: 0,
|
||||
latestRelay: null,
|
||||
relayUrls: successUrls,
|
||||
});
|
||||
}
|
||||
|
||||
// Get detailed relay results
|
||||
const relayResults = {
|
||||
successful: [],
|
||||
|
||||
Reference in New Issue
Block a user