diff --git a/src/lib/utils/websocket_utils.ts b/src/lib/utils/websocket_utils.ts index bad0818..ab6ef5b 100644 --- a/src/lib/utils/websocket_utils.ts +++ b/src/lib/utils/websocket_utils.ts @@ -22,42 +22,87 @@ export interface NostrFilter { limit?: number; } +type ResolveCallback = (value: T | PromiseLike) => void; +type RejectCallback = (reason?: any) => void; +type EventHandler = (ev: Event) => void; +type EventHandlerReject = (reject: RejectCallback) => EventHandler; +type EventHandlerResolve = (resolve: ResolveCallback) => EventHandlerReject; + +function handleMessage( + ev: MessageEvent, + subId: string, + resolve: (event: NostrEvent) => void, + reject: (reason: any) => void +) { + const data = JSON.parse(ev.data); + + if (data[1] !== subId) { + return; + } + + switch (data[0]) { + case "EVENT": + break; + case "CLOSED": + reject(new Error(`[WebSocket Utils]: Subscription ${subId} closed`)); + break; + case "EOSE": + reject(new Error(`[WebSocket Utils]: Event not found`)); + break; + } + + const event = data[2] as NostrEvent; + if (!event) { + return; + } + + resolve(event); +} + +function handleError( + ev: Event, + reject: (reason: any) => void +) { + reject(ev); +} + export async function fetchNostrEvent(filter: NostrFilter): Promise { // TODO: Improve relay selection when relay management is implemented. const ws = await WebSocketPool.instance.acquire("wss://thecitadel.nostr1.com"); const subId = crypto.randomUUID(); + // AI-NOTE: Currying is used here to abstract the internal handler logic away from the WebSocket + // handling logic. The message and error handlers themselves can be refactored without affecting + // the WebSocket handling logic. + const curriedMessageHandler: (subId: string) => EventHandlerResolve = + (subId) => + (resolve) => + (reject) => + (ev: MessageEvent) => + handleMessage(ev, subId, resolve, reject); + const curriedErrorHandler: EventHandlerReject = + (reject) => + (ev: Event) => + handleError(ev, reject); + + // AI-NOTE: These variables store references to partially-applied handlers so that the `finally` + // block receives the correct references to clean up the listeners. + let messageHandler: EventHandler; + let errorHandler: EventHandler; + const res = new Promise((resolve, reject) => { - ws.addEventListener("message", (ev) => { - const data = JSON.parse(ev.data); - - if (data[1] !== subId) { - return; - } - - switch (data[0]) { - case "EVENT": - break; - case "CLOSED": - reject(new Error(`[WebSocket Utils]: Subscription ${subId} closed`)); - break; - case "EOSE": - reject(new Error(`[WebSocket Utils]: Event not found`)); - break; - } - - const event = data[2] as NostrEvent; - if (!event) { - return; - } - - resolve(event); - }); - - ws.addEventListener("error", (ev) => { - reject(ev); - }); - }).withTimeout(2000); + messageHandler = curriedMessageHandler(subId)(resolve)(reject); + errorHandler = curriedErrorHandler(reject); + + ws.addEventListener("message", messageHandler); + ws.addEventListener("error", errorHandler); + }) + .withTimeout(2000) + .finally(() => { + ws.removeEventListener("message", messageHandler); + ws.removeEventListener("error", errorHandler); + WebSocketPool.instance.release(ws); + }); ws.send(JSON.stringify(["REQ", subId, filter])); return res; @@ -70,14 +115,14 @@ export async function fetchEventById(id: string): Promise { try { const event = await fetchNostrEvent({ ids: [id], limit: 1 }); if (!event) { - throw error(404, `Event not found for ID: ${id}`); + error(404, `Event not found for ID: ${id}`); } return event; } catch (err) { if (err && typeof err === "object" && "status" in err) { throw err; } - throw error(404, `Failed to fetch event by ID: ${err}`); + error(404, `Failed to fetch event by ID: ${err}`); } } @@ -88,14 +133,14 @@ export async function fetchEventByDTag(dTag: string): Promise { try { const event = await fetchNostrEvent({ "#d": [dTag], limit: 1 }); if (!event) { - throw error(404, `Event not found for d-tag: ${dTag}`); + error(404, `Event not found for d-tag: ${dTag}`); } return event; } catch (err) { if (err && typeof err === "object" && "status" in err) { throw err; } - throw error(404, `Failed to fetch event by d-tag: ${err}`); + error(404, `Failed to fetch event by d-tag: ${err}`); } } @@ -112,14 +157,14 @@ export async function fetchEventByNaddr(naddr: string): Promise { }; const event = await fetchNostrEvent(filter); if (!event) { - throw error(404, `Event not found for naddr: ${naddr}`); + error(404, `Event not found for naddr: ${naddr}`); } return event; } catch (err) { if (err && typeof err === "object" && "status" in err) { throw err; } - throw error(404, `Failed to fetch event by naddr: ${err}`); + error(404, `Failed to fetch event by naddr: ${err}`); } } @@ -131,13 +176,13 @@ export async function fetchEventByNevent(nevent: string): Promise { const decoded = neventDecode(nevent); const event = await fetchNostrEvent({ ids: [decoded.id], limit: 1 }); if (!event) { - throw error(404, `Event not found for nevent: ${nevent}`); + error(404, `Event not found for nevent: ${nevent}`); } return event; } catch (err) { if (err && typeof err === "object" && "status" in err) { throw err; } - throw error(404, `Failed to fetch event by nevent: ${err}`); + error(404, `Failed to fetch event by nevent: ${err}`); } } diff --git a/src/routes/publication/+page.server.ts b/src/routes/publication/+page.server.ts index 1b66af2..fa30a0d 100644 --- a/src/routes/publication/+page.server.ts +++ b/src/routes/publication/+page.server.ts @@ -25,17 +25,17 @@ export const load: PageServerLoad = ({ url }) => { if (id) { // Check if id is an naddr or nevent if (id.startsWith(IDENTIFIER_PREFIXES.NADDR)) { - throw redirect(301, `${ROUTES.NADDR}/${id}`); + redirect(301, `${ROUTES.NADDR}/${id}`); } else if (id.startsWith(IDENTIFIER_PREFIXES.NEVENT)) { - throw redirect(301, `${ROUTES.NEVENT}/${id}`); + redirect(301, `${ROUTES.NEVENT}/${id}`); } else { // Assume it's a hex ID - throw redirect(301, `${ROUTES.ID}/${id}`); + redirect(301, `${ROUTES.ID}/${id}`); } } else if (dTag) { - throw redirect(301, `${ROUTES.D_TAG}/${dTag}`); + redirect(301, `${ROUTES.D_TAG}/${dTag}`); } // If no query parameters, redirect to the start page - throw redirect(301, ROUTES.START); + redirect(301, ROUTES.START); }; \ No newline at end of file diff --git a/src/routes/publication/[type]/[identifier]/+layout.server.ts b/src/routes/publication/[type]/[identifier]/+layout.server.ts index f97639a..26e28b4 100644 --- a/src/routes/publication/[type]/[identifier]/+layout.server.ts +++ b/src/routes/publication/[type]/[identifier]/+layout.server.ts @@ -1,23 +1,40 @@ import { error } from "@sveltejs/kit"; import type { LayoutServerLoad } from "./$types"; +import { fetchEventByDTag, fetchEventById, fetchEventByNaddr, fetchEventByNevent } from "../../../../lib/utils/websocket_utils.ts"; +import type { NostrEvent } from "../../../../lib/utils/websocket_utils.ts"; export const load: LayoutServerLoad = async ({ params, url }) => { const { type, identifier } = params; - // Validate the identifier type for SSR - const validTypes = ['id', 'd', 'naddr', 'nevent']; - if (!validTypes.includes(type)) { - throw error(400, `Unsupported identifier type: ${type}`); + let indexEvent: NostrEvent; + + // Handle different identifier types + switch (type) { + case 'id': + indexEvent = await fetchEventById(identifier); + break; + case 'd': + indexEvent = await fetchEventByDTag(identifier); + break; + case 'naddr': + indexEvent = await fetchEventByNaddr(identifier); + break; + case 'nevent': + indexEvent = await fetchEventByNevent(identifier); + break; + default: + error(400, `Unsupported identifier type: ${type}`); } - // Provide basic metadata for SSR - actual fetching will happen on client - const title = "Alexandria Publication"; - const summary = "Alexandria is a digital library, utilizing Nostr events for curated publications and wiki pages."; - const image = "/screenshots/old_books.jpg"; + // Extract metadata for meta tags + const title = indexEvent.tags.find((tag) => tag[0] === "title")?.[1] || "Alexandria Publication"; + const summary = indexEvent.tags.find((tag) => tag[0] === "summary")?.[1] || + "Alexandria is a digital library, utilizing Nostr events for curated publications and wiki pages."; + const image = indexEvent.tags.find((tag) => tag[0] === "image")?.[1] || "/screenshots/old_books.jpg"; const currentUrl = `${url.origin}${url.pathname}`; return { - indexEvent: null, // Will be fetched on client side + indexEvent, metadata: { title, summary, diff --git a/src/routes/publication/[type]/[identifier]/+layout.svelte b/src/routes/publication/[type]/[identifier]/+layout.svelte index ce533f6..a3b7be6 100644 --- a/src/routes/publication/[type]/[identifier]/+layout.svelte +++ b/src/routes/publication/[type]/[identifier]/+layout.svelte @@ -1,4 +1,5 @@