From f5ae449e3693643a0d2c57ed961ed5ee555e6437 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Tue, 20 May 2025 08:54:14 +0200 Subject: [PATCH] added timeout to existing functions, revamped timeout and cardactions and fixed asciidoc browser console error --- README.md | 2 +- src/lib/components/PublicationHeader.svelte | 1 + src/lib/components/util/CardActions.svelte | 161 ++++++++---------- .../components/util/CopyToClipboard.svelte | 30 +++- src/lib/parser.ts | 45 +++++ src/lib/utils/nostrUtils.ts | 118 ++++++++++--- 6 files changed, 235 insertions(+), 122 deletions(-) diff --git a/README.md b/README.md index 1438b73..7b89069 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ To run the container, in detached mode (-d): docker run -d --rm --name=gc-alexandria -p 4174:80 gc-alexandria ``` -The container is then viewable on your [local machine](http://localhost:4174). +The container is then viewable on your [local machine](http://localhost:4173). If you want to see the container process (assuming it's the last process to start), enter: diff --git a/src/lib/components/PublicationHeader.svelte b/src/lib/components/PublicationHeader.svelte index a82f188..2f28ccc 100644 --- a/src/lib/components/PublicationHeader.svelte +++ b/src/lib/components/PublicationHeader.svelte @@ -29,6 +29,7 @@ let image: string = $derived(event.getMatchingTags('image')[0]?.[1] ?? null); let authorPubkey: string = $derived(event.getMatchingTags('p')[0]?.[1] ?? null); + console.log("PublicationHeader event:", event); {#if title != null && href != null} diff --git a/src/lib/components/util/CardActions.svelte b/src/lib/components/util/CardActions.svelte index bbc2b73..f7cc569 100644 --- a/src/lib/components/util/CardActions.svelte +++ b/src/lib/components/util/CardActions.svelte @@ -1,17 +1,21 @@
@@ -142,22 +133,18 @@
  • - +
  • - +
  • @@ -214,7 +201,7 @@
    Identifier: {identifier}
    {/if} View Event Details diff --git a/src/lib/components/util/CopyToClipboard.svelte b/src/lib/components/util/CopyToClipboard.svelte index d0bbba3..cb49a6b 100644 --- a/src/lib/components/util/CopyToClipboard.svelte +++ b/src/lib/components/util/CopyToClipboard.svelte @@ -1,27 +1,41 @@ - diff --git a/src/lib/parser.ts b/src/lib/parser.ts index d877985..85286d4 100644 --- a/src/lib/parser.ts +++ b/src/lib/parser.ts @@ -152,6 +152,10 @@ export default class Pharos { } parse(content: string, options?: ProcessorOptions | undefined): void { + + // Ensure the content is valid AsciiDoc and has a header and the doctype book + content = ensureAsciiDocHeader(content); + try { this.html = this.asciidoctor.convert(content, { 'extension_registry': this.pharosExtensions, @@ -1119,3 +1123,44 @@ export const tocUpdate = writable(0); // Whenever you update the publication tree, call: tocUpdate.update(n => n + 1); + +function ensureAsciiDocHeader(content: string): string { + const lines = content.split(/\r?\n/); + let headerIndex = -1; + let hasDoctype = false; + + // Find the first non-empty line as header + for (let i = 0; i < lines.length; i++) { + if (lines[i].trim() === '') continue; + if (lines[i].trim().startsWith('=')) { + headerIndex = i; + console.debug('[Pharos] AsciiDoc document header:', lines[i].trim()); + break; + } else { + throw new Error('AsciiDoc document is missing a header at the top.'); + } + } + + if (headerIndex === -1) { + throw new Error('AsciiDoc document is missing a header.'); + } + + // Check for doctype in the next non-empty line after header + let nextLine = headerIndex + 1; + while (nextLine < lines.length && lines[nextLine].trim() === '') { + nextLine++; + } + if (nextLine < lines.length && lines[nextLine].trim().startsWith(':doctype:')) { + hasDoctype = true; + } + + // Insert doctype immediately after header if not present + if (!hasDoctype) { + lines.splice(headerIndex + 1, 0, ':doctype: book'); + } + + // Log the state of the lines before returning + console.debug('[Pharos] AsciiDoc lines after header/doctype normalization:', lines.slice(0, 5)); + + return lines.join('\n'); +} diff --git a/src/lib/utils/nostrUtils.ts b/src/lib/utils/nostrUtils.ts index 123a19a..6cd5df3 100644 --- a/src/lib/utils/nostrUtils.ts +++ b/src/lib/utils/nostrUtils.ts @@ -185,6 +185,55 @@ export async function getNpubFromNip05(nip05: string): Promise { } } +/** + * Generic utility function to add a timeout to any promise + * Can be used in two ways: + * 1. Method style: promise.withTimeout(5000) + * 2. Function style: withTimeout(promise, 5000) + * + * @param thisOrPromise Either the promise to timeout (function style) or the 'this' context (method style) + * @param timeoutMsOrPromise Timeout duration in milliseconds (function style) or the promise (method style) + * @returns The promise result if completed before timeout, otherwise throws an error + * @throws Error with message 'Timeout' if the promise doesn't resolve within timeoutMs + */ +export function withTimeout( + thisOrPromise: Promise | number, + timeoutMsOrPromise?: number | Promise +): Promise { + // Handle method-style call (promise.withTimeout(5000)) + if (typeof thisOrPromise === 'number') { + const timeoutMs = thisOrPromise; + const promise = timeoutMsOrPromise as Promise; + return Promise.race([ + promise, + new Promise((_, reject) => + setTimeout(() => reject(new Error('Timeout')), timeoutMs) + ) + ]); + } + + // Handle function-style call (withTimeout(promise, 5000)) + const promise = thisOrPromise; + const timeoutMs = timeoutMsOrPromise as number; + return Promise.race([ + promise, + new Promise((_, reject) => + setTimeout(() => reject(new Error('Timeout')), timeoutMs) + ) + ]); +} + +// Add the method to Promise prototype +declare global { + interface Promise { + withTimeout(timeoutMs: number): Promise; + } +} + +Promise.prototype.withTimeout = function(this: Promise, timeoutMs: number): Promise { + return withTimeout(timeoutMs, this); +}; + /** * Fetches an event using a two-step relay strategy: * 1. First tries standard relays with timeout @@ -196,42 +245,59 @@ export async function fetchEventWithFallback( filterOrId: string | NDKFilter, timeoutMs: number = 3000 ): Promise { - const allRelays = Array.from(new Set([...standardRelays, ...bootstrapRelays])); + // Get user relays if logged in + const userRelays = ndk.activeUser ? + Array.from(ndk.pool?.relays.values() || []) + .filter(r => r.status === 1) // Only use connected relays + .map(r => r.url) : + []; + + // Create three relay sets in priority order const relaySets = [ - NDKRelaySet.fromRelayUrls(standardRelays, ndk), - NDKRelaySet.fromRelayUrls(allRelays, ndk) + NDKRelaySet.fromRelayUrls(standardRelays, ndk), // 1. Standard relays + NDKRelaySet.fromRelayUrls(userRelays, ndk), // 2. User relays (if logged in) + NDKRelaySet.fromRelayUrls(bootstrapRelays, ndk) // 3. Bootstrap relays (last resort) ]; - async function withTimeout(promise: Promise): Promise { - return Promise.race([ - promise, - new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), timeoutMs)) - ]); - } - try { let found: NDKEvent | null = null; + const triedRelaySets: string[] = []; - // Try standard relays first - if (typeof filterOrId === 'string' && /^[0-9a-f]{64}$/i.test(filterOrId)) { - found = await withTimeout(ndk.fetchEvent({ ids: [filterOrId] }, undefined, relaySets[0])); - if (!found) { - // Fallback to all relays - found = await withTimeout(ndk.fetchEvent({ ids: [filterOrId] }, undefined, relaySets[1])); - } - } else { - const filter = typeof filterOrId === 'string' ? { ids: [filterOrId] } : filterOrId; - const results = await withTimeout(ndk.fetchEvents(filter, undefined, relaySets[0])); - found = results instanceof Set ? Array.from(results)[0] as NDKEvent : null; - if (!found) { - // Fallback to all relays - const fallbackResults = await withTimeout(ndk.fetchEvents(filter, undefined, relaySets[1])); - found = fallbackResults instanceof Set ? Array.from(fallbackResults)[0] as NDKEvent : null; + // Helper function to try fetching from a relay set + async function tryFetchFromRelaySet(relaySet: NDKRelaySet, setName: string): Promise { + if (relaySet.relays.size === 0) return null; + triedRelaySets.push(setName); + + if (typeof filterOrId === 'string' && /^[0-9a-f]{64}$/i.test(filterOrId)) { + return await ndk.fetchEvent({ ids: [filterOrId] }, undefined, relaySet).withTimeout(timeoutMs); + } else { + const filter = typeof filterOrId === 'string' ? { ids: [filterOrId] } : filterOrId; + const results = await ndk.fetchEvents(filter, undefined, relaySet).withTimeout(timeoutMs); + return results instanceof Set ? Array.from(results)[0] as NDKEvent : null; } } + // Try each relay set in order + for (const [index, relaySet] of relaySets.entries()) { + const setName = index === 0 ? 'standard relays' : + index === 1 ? 'user relays' : + 'bootstrap relays'; + + found = await tryFetchFromRelaySet(relaySet, setName); + if (found) break; + } + if (!found) { - console.warn('Event not found after timeout. Some relays may be offline or slow.'); + const timeoutSeconds = timeoutMs / 1000; + const relayUrls = relaySets.map((set, i) => { + const setName = i === 0 ? 'standard relays' : + i === 1 ? 'user relays' : + 'bootstrap relays'; + const urls = Array.from(set.relays).map(r => r.url); + return urls.length > 0 ? `${setName} (${urls.join(', ')})` : null; + }).filter(Boolean).join(', then '); + + console.warn(`Event not found after ${timeoutSeconds}s timeout. Tried ${relayUrls}. Some relays may be offline or slow.`); return null; }