tinyrelayfile
tinyrelay/scripts/nostr-name-setup.mjs/

scripts/nostr-name-setup.mjs raw
1// Shared state for the vendored <nostr-name> element. Names resolve from this
2// relay only: tiny is a personal relay, so the profiles that matter live here,
3// and asking public indexers would leak who is being looked at. The element
4// reads window.nostrSharedMetadataLoader when its module evaluates, so this
5// file must be imported first.
6import { SimplePool } from "@nostr/tools/pool";
7import { bareNostrUser, nostrUserFromEvent } from "@nostr/gadgets/metadata";
8
9const pool = window.nostrSharedPool || new SimplePool();
10window.nostrSharedPool = pool;
11
12// The relay listens at the tenant root: wss://host for the default tenant,
13// wss://host/r/<name> for the others.
14const relayURL = () => {
15 const root = (location.pathname.match(/^\/r\/[^/]+/) || [""])[0];
16 return (location.protocol === "https:" ? "wss://" : "ws://") + location.host + root;
17};
18
19const CACHE = "tiny:names:v1";
20const FRESH = 6 * 60 * 60; // seconds a profile is trusted before a refetch
21const MISSING = 10 * 60; // seconds before asking again about a key with no profile
22const BATCH = 50; // milliseconds to gather keys before one request
23const LIMIT = 100; // authors per request
24
25const readCache = () => {
26 try { return JSON.parse(localStorage.getItem(CACHE) || "{}") || {}; } catch { return {}; }
27};
28const writeCache = cache => {
29 try { localStorage.setItem(CACHE, JSON.stringify(cache)); } catch {}
30};
31
32const users = new Map(); // pubkey -> NostrUser, from cache or the relay
33const pending = new Map(); // pubkey -> [resolve]
34const refreshed = new Set(); // pubkeys explicitly refreshed once in this document
35let timer = null;
36
37const userFor = (pubkey, entry) => entry?.event ? nostrUserFromEvent(entry.event) : bareNostrUser(pubkey);
38
39const flush = async () => {
40 timer = null;
41 const keys = [...pending.keys()].slice(0, LIMIT);
42 if (!keys.length) return;
43 const cache = readCache();
44 const now = Math.floor(Date.now() / 1000);
45 let events = [];
46 try {
47 events = await pool.querySync([relayURL()], {kinds: [0], authors: keys});
48 } catch {}
49 const newest = new Map();
50 for (const event of events) {
51 if (!newest.has(event.pubkey) || newest.get(event.pubkey).created_at < event.created_at) newest.set(event.pubkey, event);
52 }
53 for (const key of keys) {
54 const event = newest.get(key) || cache[key]?.event || null;
55 cache[key] = {event, at: now};
56 const user = userFor(key, cache[key]);
57 users.set(key, user);
58 refreshed.add(key);
59 for (const resolve of pending.get(key) || []) resolve(user);
60 pending.delete(key);
61 }
62 writeCache(cache);
63 if (pending.size) timer = setTimeout(flush, BATCH);
64};
65
66// load answers like @nostr/gadgets' loadNostrUser: a NostrUser from cache,
67// refreshed from this relay when stale, with a placeholder while unknown.
68const load = ({pubkey, refresh = false}) => {
69 const key = String(pubkey || "").toLowerCase();
70 if (!/^[0-9a-f]{64}$/.test(key)) return Promise.resolve(bareNostrUser(key || "0".repeat(64)));
71 const force = refresh && !refreshed.has(key);
72 const cached = readCache()[key];
73 const now = Math.floor(Date.now() / 1000);
74 if (!force && cached && now - cached.at < (cached.event ? FRESH : MISSING)) return Promise.resolve(users.get(key) || userFor(key, cached));
75 return new Promise(resolve => {
76 pending.set(key, [...(pending.get(key) || []), resolve]);
77 if (!timer) timer = setTimeout(flush, BATCH);
78 });
79};
80
81window.nostrSharedMetadataLoader = window.nostrSharedMetadataLoader || (request => load(typeof request === "string" ? {pubkey: request} : request));
82window.tinyNames = Object.freeze({load, relayURL, cacheKey: CACHE});

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs