Browse Source

Merge branch 'master' of ssh://onedev.gitcitadel.eu:6611/Alexandria/gc-alexandria into feature/text-entry

master
silberengel 7 months ago
parent
commit
06812b1771
  1. 121
      src/lib/utils/websocket_utils.ts
  2. 10
      src/routes/publication/+page.server.ts
  3. 35
      src/routes/publication/[type]/[identifier]/+layout.server.ts
  4. 5
      src/routes/publication/[type]/[identifier]/+layout.svelte
  5. 66
      src/routes/publication/[type]/[identifier]/+layout.ts
  6. 18
      src/routes/publication/[type]/[identifier]/+page.server.ts
  7. 69
      src/routes/publication/[type]/[identifier]/+page.ts

121
src/lib/utils/websocket_utils.ts

@ -22,42 +22,87 @@ export interface NostrFilter { @@ -22,42 +22,87 @@ export interface NostrFilter {
limit?: number;
}
type ResolveCallback<T> = (value: T | PromiseLike<T>) => void;
type RejectCallback = (reason?: any) => void;
type EventHandler = (ev: Event) => void;
type EventHandlerReject = (reject: RejectCallback) => EventHandler;
type EventHandlerResolve<T> = (resolve: ResolveCallback<T>) => 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<NostrEvent> {
// 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<NostrEvent> =
(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<NostrEvent>((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<NostrEvent> { @@ -70,14 +115,14 @@ export async function fetchEventById(id: string): Promise<NostrEvent> {
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<NostrEvent> { @@ -88,14 +133,14 @@ export async function fetchEventByDTag(dTag: string): Promise<NostrEvent> {
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<NostrEvent> { @@ -112,14 +157,14 @@ export async function fetchEventByNaddr(naddr: string): Promise<NostrEvent> {
};
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<NostrEvent> { @@ -131,13 +176,13 @@ export async function fetchEventByNevent(nevent: string): Promise<NostrEvent> {
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}`);
}
}

10
src/routes/publication/+page.server.ts

@ -25,17 +25,17 @@ export const load: PageServerLoad = ({ url }) => { @@ -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);
};

35
src/routes/publication/[type]/[identifier]/+layout.server.ts

@ -1,23 +1,40 @@ @@ -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,

5
src/routes/publication/[type]/[identifier]/+layout.svelte

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
<script lang="ts">
import { browser } from "$app/environment";
import type { LayoutProps } from "./$types";
let { data, children }: LayoutProps = $props();
@ -26,4 +27,6 @@ @@ -26,4 +27,6 @@
<meta name="twitter:image" content={metadata.image} />
</svelte:head>
{@render children()}
{#if browser}
{@render children()}
{/if}

66
src/routes/publication/[type]/[identifier]/+layout.ts

@ -1,65 +1 @@ @@ -1,65 +1 @@
import { error } from "@sveltejs/kit";
import type { LayoutLoad } from "./$types";
import { fetchEventByDTag, fetchEventById, fetchEventByNaddr, fetchEventByNevent } from "../../../../lib/utils/websocket_utils.ts";
import type { NostrEvent } from "../../../../lib/utils/websocket_utils.ts";
import { browser } from "$app/environment";
export const load: LayoutLoad = async ({ params, url }) => {
const { type, identifier } = params;
// Only fetch on the client side where WebSocket is available
if (!browser) {
// Return basic metadata for SSR
return {
indexEvent: null,
metadata: {
title: "Alexandria Publication",
summary: "Alexandria is a digital library, utilizing Nostr events for curated publications and wiki pages.",
image: "/screenshots/old_books.jpg",
currentUrl: `${url.origin}${url.pathname}`,
},
};
}
let indexEvent: NostrEvent;
try {
// 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:
throw error(400, `Unsupported identifier type: ${type}`);
}
// 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,
metadata: {
title,
summary,
image,
currentUrl,
},
};
} catch (err) {
console.error('Failed to fetch publication:', err);
throw error(404, `Failed to load publication: ${err}`);
}
};
export const ssr = true;

18
src/routes/publication/[type]/[identifier]/+page.server.ts

@ -1,18 +0,0 @@ @@ -1,18 +0,0 @@
import { error } from "@sveltejs/kit";
import type { PageServerLoad } from "./$types";
export const load: PageServerLoad = async ({ params }) => {
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}`);
}
// Provide basic data for SSR - actual fetching will happen on client
return {
publicationType: "", // Will be determined on client side
indexEvent: null, // Will be fetched on client side
};
};

69
src/routes/publication/[type]/[identifier]/+page.ts

@ -2,53 +2,38 @@ import { error } from "@sveltejs/kit"; @@ -2,53 +2,38 @@ import { error } from "@sveltejs/kit";
import type { PageLoad } from "./$types";
import { fetchEventByDTag, fetchEventById, fetchEventByNaddr, fetchEventByNevent } from "../../../../lib/utils/websocket_utils.ts";
import type { NostrEvent } from "../../../../lib/utils/websocket_utils.ts";
import { browser } from "$app/environment";
export const load: PageLoad = async ({ params }: { params: { type: string; identifier: string } }) => {
const { type, identifier } = params;
// Only fetch on the client side where WebSocket is available
if (!browser) {
// Return basic data for SSR
return {
publicationType: "",
indexEvent: null,
};
}
let indexEvent: NostrEvent;
let indexEvent: NostrEvent | null;
try {
// 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:
throw error(400, `Unsupported identifier type: ${type}`);
}
// 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}`);
}
if (!indexEvent) {
throw error(404, `Event not found for ${type}: ${identifier}`);
}
if (!indexEvent) {
error(404, `Event not found for ${type}: ${identifier}`);
}
const publicationType = indexEvent.tags.find((tag) => tag[0] === "type")?.[1] ?? "";
const publicationType = indexEvent.tags.find((tag) => tag[0] === "type")?.[1] ?? "";
return {
publicationType,
indexEvent,
};
} catch (err) {
console.error('Failed to fetch publication:', err);
throw error(404, `Failed to load publication: ${err}`);
}
};
return {
publicationType,
indexEvent,
};
};

Loading…
Cancel
Save