Render the forum shell, resources and contacts on the server
This commit is contained in:
parent
a7b5d8f223
commit
40e0be35d9
16 changed files with 259 additions and 30 deletions
74
src/lib/ssr/relay.ts
Normal file
74
src/lib/ssr/relay.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import { SimplePool } from "@nostr/tools";
|
||||
import type { Event } from "@nostr/tools/core";
|
||||
import type { Filter } from "@nostr/tools/filter";
|
||||
import { RELAY_URL } from "$lib/config";
|
||||
import { PROFILE_RELAYS } from "$lib/forum/profiles";
|
||||
import type { Query } from "$lib/forum/query";
|
||||
|
||||
// Anonymous relay access for server rendering. The server has no signer, so
|
||||
// it only ever sees what the relay serves the public; members get their
|
||||
// private rooms once the client takes over. Every query is bounded so a slow
|
||||
// relay degrades to an empty page, never a hung request, and results are
|
||||
// cached briefly to absorb crawler bursts.
|
||||
const QUERY_TIMEOUT = 2500;
|
||||
const PROFILE_TIMEOUT = 1500;
|
||||
const CACHE_TTL = 30_000;
|
||||
const CACHE_MAX = 2000;
|
||||
|
||||
const pool = new SimplePool();
|
||||
const cache = new Map<string, { at: number; result: Promise<Event[]> }>();
|
||||
|
||||
// Resolve at EOSE from every relay or at the deadline, whichever comes first,
|
||||
// with whatever arrived: a stalled relay must not cost the events the others
|
||||
// already delivered.
|
||||
function boundedQuery(
|
||||
relays: string[],
|
||||
filter: Filter,
|
||||
maxWait: number,
|
||||
): Promise<Event[]> {
|
||||
return new Promise((resolve) => {
|
||||
const events: Event[] = [];
|
||||
let done = false;
|
||||
let sub: { close(): void } | null = null;
|
||||
const finish = () => {
|
||||
if (done) return;
|
||||
done = true;
|
||||
clearTimeout(timer);
|
||||
sub?.close();
|
||||
resolve(events);
|
||||
};
|
||||
const timer = setTimeout(finish, maxWait);
|
||||
try {
|
||||
sub = pool.subscribeMany(relays, filter, {
|
||||
onevent: (e) => events.push(e),
|
||||
oneose: finish,
|
||||
});
|
||||
if (done) sub.close(); // EOSE fired synchronously
|
||||
} catch {
|
||||
finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function cachedQuery(
|
||||
relays: string[],
|
||||
filter: Filter,
|
||||
maxWait: number,
|
||||
): Promise<Event[]> {
|
||||
const key = relays.join(",") + "|" + JSON.stringify(filter);
|
||||
const now = Date.now();
|
||||
const hit = cache.get(key);
|
||||
if (hit && now - hit.at < CACHE_TTL) return hit.result;
|
||||
if (cache.size >= CACHE_MAX) cache.clear();
|
||||
const result = boundedQuery(relays, filter, maxWait);
|
||||
cache.set(key, { at: now, result });
|
||||
return result;
|
||||
}
|
||||
|
||||
export const forumQuery: Query = (filter) =>
|
||||
cachedQuery([RELAY_URL], filter, QUERY_TIMEOUT);
|
||||
|
||||
// Profiles mostly live off the forum relay; ask it too for members who only
|
||||
// published there.
|
||||
export const profileQuery: Query = (filter) =>
|
||||
cachedQuery([RELAY_URL, ...PROFILE_RELAYS], filter, PROFILE_TIMEOUT);
|
||||
29
src/lib/ssr/shell.ts
Normal file
29
src/lib/ssr/shell.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { MODE, GROUP_ID } from "$lib/config";
|
||||
import { fetchGroup, fetchGroups, fetchRoomAdmins } from "$lib/forum/groups";
|
||||
import { fetchResources } from "$lib/forum/resources";
|
||||
import { fetchPartials } from "$lib/forum/partials";
|
||||
import type { ForumShell } from "$lib/forum/snapshot";
|
||||
import { forumQuery } from "./relay";
|
||||
|
||||
// Everything the layout needs on every page: the group(s), the admin set,
|
||||
// sidebar resources and partials.
|
||||
export async function loadShell(): Promise<ForumShell> {
|
||||
const [group, groups, resources, partials] = await Promise.all([
|
||||
MODE === "simple" ? fetchGroup(forumQuery, GROUP_ID) : null,
|
||||
MODE === "full" ? fetchGroups(forumQuery) : [],
|
||||
fetchResources(forumQuery),
|
||||
fetchPartials(forumQuery),
|
||||
]);
|
||||
const adminsByRoom =
|
||||
MODE === "full"
|
||||
? await fetchRoomAdmins(
|
||||
forumQuery,
|
||||
groups.map((g) => g.id),
|
||||
)
|
||||
: {};
|
||||
const admins =
|
||||
MODE === "full"
|
||||
? [...new Set(Object.values(adminsByRoom).flat())]
|
||||
: (group?.admins ?? []);
|
||||
return { group, groups, adminsByRoom, admins, resources, partials };
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue