You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
208 lines
6.1 KiB
208 lines
6.1 KiB
import { WebSocketPool } from "../data_structures/websocket_pool.ts"; |
|
import { error } from "@sveltejs/kit"; |
|
import { naddrDecode, neventDecode } from "../utils.ts"; |
|
import { activeInboxRelays, activeOutboxRelays } from "../ndk.ts"; |
|
import { get } from "svelte/store"; |
|
|
|
export interface NostrEvent { |
|
id: string; |
|
pubkey: string; |
|
created_at: number; |
|
kind: number; |
|
tags: string[][]; |
|
content: string; |
|
sig: string; |
|
} |
|
|
|
export interface NostrFilter { |
|
ids?: string[]; |
|
authors?: string[]; |
|
kinds?: number[]; |
|
[tag: `#${string}`]: string[] | undefined; |
|
since?: number; |
|
until?: number; |
|
limit?: number; |
|
} |
|
|
|
type ResolveCallback<T> = (value: T | PromiseLike<T>) => void; |
|
type RejectCallback = (reason?: any) => void; |
|
type EventHandler = (ev: Event) => void; |
|
type MessageEventHandler = (ev: MessageEvent) => void; |
|
type EventHandlerReject = (reject: RejectCallback) => EventHandler; |
|
type EventHandlerResolve<T> = (resolve: ResolveCallback<T>) => (reject: RejectCallback) => MessageEventHandler; |
|
|
|
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<NostrEvent> { |
|
// AI-NOTE: Updated to use active relay stores instead of hardcoded relay URL |
|
// This ensures the function uses the user's configured relays and can find events |
|
// across multiple relays rather than being limited to a single hardcoded relay. |
|
|
|
// Get available relays from the active relay stores |
|
const inboxRelays = get(activeInboxRelays); |
|
const outboxRelays = get(activeOutboxRelays); |
|
|
|
// Combine all available relays, prioritizing inbox relays |
|
const availableRelays = [...inboxRelays, ...outboxRelays]; |
|
|
|
if (availableRelays.length === 0) { |
|
throw new Error("[WebSocket Utils]: No relays available for fetching events"); |
|
} |
|
|
|
// Select a relay - prefer inbox relays if available, otherwise use any available relay |
|
const selectedRelay = inboxRelays.length > 0 ? inboxRelays[0] : availableRelays[0]; |
|
|
|
const ws = await WebSocketPool.instance.acquire(selectedRelay); |
|
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) => (resolve: ResolveCallback<NostrEvent>) => (reject: RejectCallback) => MessageEventHandler = |
|
(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: MessageEventHandler; |
|
let errorHandler: EventHandler; |
|
|
|
const res = new Promise<NostrEvent>((resolve, reject) => { |
|
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; |
|
} |
|
|
|
/** |
|
* Fetches an event by hex ID, throwing a SvelteKit 404 error if not found. |
|
*/ |
|
export async function fetchEventById(id: string): Promise<NostrEvent> { |
|
try { |
|
const event = await fetchNostrEvent({ ids: [id], limit: 1 }); |
|
if (!event) { |
|
error(404, `Event not found for ID: ${id}`); |
|
} |
|
return event; |
|
} catch (err) { |
|
if (err && typeof err === "object" && "status" in err) { |
|
throw err; |
|
} |
|
error(404, `Failed to fetch event by ID: ${err}`); |
|
} |
|
} |
|
|
|
/** |
|
* Fetches an event by d tag, throwing a 404 if not found. |
|
*/ |
|
export async function fetchEventByDTag(dTag: string): Promise<NostrEvent> { |
|
try { |
|
const event = await fetchNostrEvent({ "#d": [dTag], limit: 1 }); |
|
if (!event) { |
|
error(404, `Event not found for d-tag: ${dTag}`); |
|
} |
|
return event; |
|
} catch (err) { |
|
if (err && typeof err === "object" && "status" in err) { |
|
throw err; |
|
} |
|
error(404, `Failed to fetch event by d-tag: ${err}`); |
|
} |
|
} |
|
|
|
/** |
|
* Fetches an event by naddr identifier. |
|
*/ |
|
export async function fetchEventByNaddr(naddr: string): Promise<NostrEvent> { |
|
try { |
|
const decoded = naddrDecode(naddr); |
|
const filter = { |
|
kinds: [decoded.kind], |
|
authors: [decoded.pubkey], |
|
"#d": [decoded.identifier], |
|
}; |
|
const event = await fetchNostrEvent(filter); |
|
if (!event) { |
|
error(404, `Event not found for naddr: ${naddr}`); |
|
} |
|
return event; |
|
} catch (err) { |
|
if (err && typeof err === "object" && "status" in err) { |
|
throw err; |
|
} |
|
error(404, `Failed to fetch event by naddr: ${err}`); |
|
} |
|
} |
|
|
|
/** |
|
* Fetches an event by nevent identifier. |
|
*/ |
|
export async function fetchEventByNevent(nevent: string): Promise<NostrEvent> { |
|
try { |
|
const decoded = neventDecode(nevent); |
|
const event = await fetchNostrEvent({ ids: [decoded.id], limit: 1 }); |
|
if (!event) { |
|
error(404, `Event not found for nevent: ${nevent}`); |
|
} |
|
return event; |
|
} catch (err) { |
|
if (err && typeof err === "object" && "status" in err) { |
|
throw err; |
|
} |
|
error(404, `Failed to fetch event by nevent: ${err}`); |
|
} |
|
}
|
|
|