1// Shared state for the vendored <nostr-name> element. Names resolve from this2// 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 element4// reads window.nostrSharedMetadataLoader when its module evaluates, so this5// file must be imported first.6import { SimplePool } from "@nostr/tools/pool";7import { bareNostrUser, nostrUserFromEvent } from "@nostr/gadgets/metadata";89const pool = window.nostrSharedPool || new SimplePool();10window.nostrSharedPool = pool;1112// 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};1819const CACHE = "tiny:names:v1";20const FRESH = 6 * 60 * 60; // seconds a profile is trusted before a refetch21const MISSING = 10 * 60; // seconds before asking again about a key with no profile22const BATCH = 50; // milliseconds to gather keys before one request23const LIMIT = 100; // authors per request2425const 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};3132const users = new Map(); // pubkey -> NostrUser, from cache or the relay33const pending = new Map(); // pubkey -> [resolve]34const refreshed = new Set(); // pubkeys explicitly refreshed once in this document35let timer = null;3637const userFor = (pubkey, entry) => entry?.event ? nostrUserFromEvent(entry.event) : bareNostrUser(pubkey);3839const 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};6566// 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};8081window.nostrSharedMetadataLoader = window.nostrSharedMetadataLoader || (request => load(typeof request === "string" ? {pubkey: request} : request));82window.tinyNames = Object.freeze({load, relayURL, cacheKey: CACHE});