Files
bitvid/js/nostr/toolkit.js
T

603 lines
15 KiB
JavaScript

// js/nostr/toolkit.js
import { isDevMode } from "../config.js";
import { nostrToolsReady } from "../nostrToolsBootstrap.js";
import { devLogger, userLogger } from "../utils/logger.js";
/**
* The default relay set bitvid bootstraps with before loading a user's
* preferences.
*/
export const DEFAULT_RELAY_URLS = Object.freeze([
"wss://relay.damus.io",
"wss://nos.lol",
"wss://relay.snort.social",
"wss://relay.primal.net",
]);
export const RELAY_URLS = Array.from(DEFAULT_RELAY_URLS);
const globalScope =
typeof window !== "undefined"
? window
: typeof globalThis !== "undefined"
? globalThis
: null;
const SIMPLE_POOL_SHIM_KEY = Symbol.for("__bitvidSimplePoolShimApplied__");
const noop = () => {};
function normalizeRelayList(relays) {
if (!Array.isArray(relays)) {
if (typeof relays === "string" && relays.trim()) {
return [relays.trim()];
}
return [];
}
return relays
.map((relay) => (typeof relay === "string" ? relay.trim() : ""))
.filter((relay) => relay);
}
function normalizeFilterList(filters) {
if (!filters) {
return [];
}
if (!Array.isArray(filters)) {
if (typeof filters === "object" && filters !== null) {
return [filters];
}
return [];
}
return filters.filter((candidate) => candidate && typeof candidate === "object");
}
function safeInvoke(handler, args, contextLabel) {
if (typeof handler !== "function") {
return;
}
try {
handler(...args);
} catch (error) {
devLogger.warn(
`[nostr] Legacy SimplePool ${contextLabel} handler threw:`,
error
);
}
}
function createLegacySubscriptionContext(relays, filters) {
const relayList = normalizeRelayList(relays);
const filterList = normalizeFilterList(filters);
const requests = [];
if (relayList.length && filterList.length) {
relayList.forEach((url) => {
filterList.forEach((filter) => {
requests.push({ url, filter });
});
});
}
return { relayList, filterList, requests };
}
const nostrToolsReadySource =
globalScope &&
globalScope.nostrToolsReady &&
typeof globalScope.nostrToolsReady.then === "function"
? globalScope.nostrToolsReady
: nostrToolsReady;
function ensureCanonicalCrypto(candidate, scope = globalScope) {
if (!candidate || typeof candidate !== "object") {
return candidate;
}
const canonical =
scope && typeof scope === "object"
? scope.__BITVID_CANONICAL_NOSTR_TOOLS__ || null
: null;
if (!canonical || typeof canonical !== "object") {
return candidate;
}
const needsNip04 = !candidate.nip04 && canonical.nip04;
const needsNip44 = !candidate.nip44 && canonical.nip44;
if (!needsNip04 && !needsNip44) {
return candidate;
}
if (Object.isFrozen(candidate)) {
const augmented = { ...candidate };
if (needsNip04) {
augmented.nip04 = canonical.nip04;
}
if (needsNip44) {
augmented.nip44 = canonical.nip44;
}
return Object.freeze(augmented);
}
if (needsNip04) {
candidate.nip04 = canonical.nip04;
}
if (needsNip44) {
candidate.nip44 = canonical.nip44;
}
return candidate;
}
export function normalizeToolkitCandidate(candidate) {
if (
candidate &&
typeof candidate === "object" &&
candidate.ok !== false &&
typeof candidate.then !== "function"
) {
return ensureCanonicalCrypto(candidate);
}
return null;
}
export function readToolkitFromScope(scope = globalScope) {
if (!scope || typeof scope !== "object") {
return null;
}
const candidates = [];
const canonical = scope.__BITVID_CANONICAL_NOSTR_TOOLS__;
if (canonical) {
candidates.push(canonical);
}
const direct = scope.NostrTools;
if (direct) {
candidates.push(direct);
}
const nestedWindow =
scope.window && scope.window !== scope && typeof scope.window === "object"
? scope.window
: null;
if (nestedWindow) {
if (nestedWindow.__BITVID_CANONICAL_NOSTR_TOOLS__) {
candidates.push(nestedWindow.__BITVID_CANONICAL_NOSTR_TOOLS__);
}
if (nestedWindow.NostrTools) {
candidates.push(nestedWindow.NostrTools);
}
}
for (const candidate of candidates) {
const normalized = normalizeToolkitCandidate(candidate);
if (normalized) {
return normalized;
}
}
return null;
}
const __nostrToolsBootstrapResult = await (async () => {
try {
const result = await nostrToolsReadySource;
if (result && typeof result === "object" && result.ok === false) {
return {
toolkit: normalizeToolkitCandidate(readToolkitFromScope()),
failure: result,
};
}
const normalized = normalizeToolkitCandidate(result);
if (normalized) {
return { toolkit: normalized, failure: null };
}
return {
toolkit: normalizeToolkitCandidate(readToolkitFromScope()),
failure: null,
};
} catch (error) {
return {
toolkit: normalizeToolkitCandidate(readToolkitFromScope()),
failure: error,
};
}
})();
let cachedNostrTools = __nostrToolsBootstrapResult.toolkit || null;
export const nostrToolsBootstrapFailure =
__nostrToolsBootstrapResult.failure || null;
if (!cachedNostrTools && nostrToolsBootstrapFailure && isDevMode) {
userLogger.warn(
"[nostr] nostr-tools helpers unavailable after bootstrap.",
nostrToolsBootstrapFailure
);
}
export function rememberNostrTools(candidate) {
const normalized = normalizeToolkitCandidate(candidate);
if (normalized) {
cachedNostrTools = normalized;
}
}
export function getCachedNostrTools() {
const fallback = readToolkitFromScope();
if (cachedNostrTools && fallback && fallback !== cachedNostrTools) {
rememberNostrTools(fallback);
} else if (!cachedNostrTools && fallback) {
rememberNostrTools(fallback);
}
return cachedNostrTools || fallback || null;
}
export async function ensureNostrTools() {
if (cachedNostrTools) {
return cachedNostrTools;
}
try {
const result = await nostrToolsReadySource;
rememberNostrTools(result);
} catch (error) {
devLogger.warn("[nostr] Failed to resolve nostr-tools helpers.", error);
}
if (!cachedNostrTools) {
rememberNostrTools(readToolkitFromScope());
}
return cachedNostrTools || null;
}
function isSimplePoolConstructor(candidate) {
if (typeof candidate !== "function") {
return false;
}
const prototype = candidate.prototype;
if (!prototype || typeof prototype !== "object") {
return false;
}
const hasSubscribeMethod =
typeof prototype.sub === "function" ||
typeof prototype.subscribe === "function" ||
typeof prototype.subscribeMany === "function";
const hasCloseMethod = typeof prototype.close === "function";
return hasSubscribeMethod && hasCloseMethod;
}
function unwrapSimplePool(candidate) {
if (!candidate) {
return null;
}
if (isSimplePoolConstructor(candidate)) {
return candidate;
}
if (typeof candidate === "object") {
if (isSimplePoolConstructor(candidate.SimplePool)) {
return candidate.SimplePool;
}
if (isSimplePoolConstructor(candidate.default)) {
return candidate.default;
}
}
return null;
}
export function resolveSimplePoolConstructor(tools, scope = globalScope) {
const candidates = [
tools?.SimplePool,
tools?.pool?.SimplePool,
tools?.pool,
tools?.SimplePool?.SimplePool,
tools?.SimplePool?.default,
tools?.pool?.default,
tools?.default?.SimplePool,
tools?.default?.pool?.SimplePool,
tools?.default?.pool,
];
if (scope && typeof scope === "object") {
candidates.push(scope?.SimplePool);
candidates.push(scope?.pool?.SimplePool);
candidates.push(scope?.pool);
const scopedTools =
scope?.NostrTools && scope.NostrTools !== tools ? scope.NostrTools : null;
if (scopedTools) {
candidates.push(scopedTools.SimplePool);
candidates.push(scopedTools.pool?.SimplePool);
candidates.push(scopedTools.pool);
}
}
for (const candidate of candidates) {
const resolved = unwrapSimplePool(candidate);
if (resolved) {
return resolved;
}
}
return null;
}
export { isSimplePoolConstructor, unwrapSimplePool };
export function shimLegacySimplePoolMethods(pool) {
if (!pool || typeof pool !== "object" || pool[SIMPLE_POOL_SHIM_KEY]) {
return pool || null;
}
const subscribeMap =
typeof pool.subscribeMap === "function" ? pool.subscribeMap.bind(pool) : null;
const subscribeMany =
typeof pool.subscribeMany === "function" ? pool.subscribeMany.bind(pool) : null;
const subscribeSingle =
typeof pool.subscribe === "function" ? pool.subscribe.bind(pool) : null;
const canSubscribe = subscribeMap || subscribeMany || subscribeSingle;
if (canSubscribe && typeof pool.sub !== "function") {
pool.sub = function legacySub(relays, filters, opts = {}) {
const { relayList, filterList, requests } =
createLegacySubscriptionContext(relays, filters);
const listeners = {
event: new Set(),
eose: new Set(),
close: new Set(),
};
if (!requests.length) {
const stub = {
sub() {
return stub;
},
on() {
return stub;
},
off() {
return stub;
},
unsub: noop,
};
queueMicrotask(() => {
listeners.eose.forEach((listener) => safeInvoke(listener, [], "eose listener"));
listeners.close.forEach((listener) =>
safeInvoke(listener, [["no-filters"]], "close listener")
);
});
return stub;
}
const seenIds = new Set();
const subscribeParams = { ...opts };
const originalOnevent = subscribeParams.onevent;
const originalOneose = subscribeParams.oneose;
const originalOnclose = subscribeParams.onclose;
const originalAlreadyHaveEvent = subscribeParams.alreadyHaveEvent;
delete subscribeParams.onevent;
delete subscribeParams.oneose;
delete subscribeParams.onclose;
delete subscribeParams.alreadyHaveEvent;
subscribeParams.alreadyHaveEvent = (id) => {
if (!id) {
return false;
}
if (seenIds.has(id)) {
return true;
}
let skip = false;
if (typeof originalAlreadyHaveEvent === "function") {
try {
skip = originalAlreadyHaveEvent(id) === true;
} catch (error) {
devLogger.warn(
"[nostr] Legacy SimplePool alreadyHaveEvent hook threw:",
error
);
}
}
if (skip) {
return true;
}
seenIds.add(id);
return false;
};
let closed = false;
const invokeEventListeners = (event) => {
if (closed) {
return;
}
if (event && typeof event === "object" && event.id) {
seenIds.add(event.id);
}
safeInvoke(originalOnevent, [event], "options.onevent");
listeners.event.forEach((listener) =>
safeInvoke(listener, [event], "event listener")
);
};
const invokeEoseListeners = () => {
if (closed) {
return;
}
safeInvoke(originalOneose, [], "options.oneose");
listeners.eose.forEach((listener) => safeInvoke(listener, [], "eose listener"));
};
const invokeCloseListeners = (reasons) => {
if (closed) {
return;
}
closed = true;
safeInvoke(originalOnclose, [reasons], "options.onclose");
listeners.close.forEach((listener) =>
safeInvoke(listener, [reasons], "close listener")
);
};
subscribeParams.onevent = (event) => {
invokeEventListeners(event);
};
subscribeParams.oneose = () => {
invokeEoseListeners();
};
subscribeParams.onclose = (reasons) => {
invokeCloseListeners(reasons);
};
let closer;
try {
if (subscribeMap) {
closer = subscribeMap(requests, subscribeParams);
} else if (subscribeMany) {
closer = subscribeMany(relayList, filterList, subscribeParams);
} else if (subscribeSingle && filterList.length) {
closer = subscribeSingle(relayList, filterList[0], subscribeParams);
}
} catch (error) {
devLogger.warn("[nostr] Failed to open SimplePool subscription.", error);
throw error;
}
const legacySub = {
sub() {
devLogger.warn("[nostr] legacy sub.sub() is unsupported with the new SimplePool API.");
return legacySub;
},
on(type, handler) {
if (typeof handler !== "function") {
return legacySub;
}
if (type === "event") {
listeners.event.add(handler);
} else if (type === "eose") {
listeners.eose.add(handler);
} else if (type === "close" || type === "closed") {
listeners.close.add(handler);
}
return legacySub;
},
off(type, handler) {
if (typeof handler !== "function") {
return legacySub;
}
if (type === "event") {
listeners.event.delete(handler);
} else if (type === "eose") {
listeners.eose.delete(handler);
} else if (type === "close" || type === "closed") {
listeners.close.delete(handler);
}
return legacySub;
},
unsub() {
if (closed) {
return;
}
closed = true;
try {
if (typeof closer === "function") {
closer();
} else if (closer && typeof closer.close === "function") {
closer.close("closed by caller");
}
} catch (error) {
devLogger.warn("[nostr] Failed to close SimplePool subscription.", error);
}
},
};
return legacySub;
};
}
if (typeof pool.list !== "function") {
pool.list = async function legacyList(relays, filters, opts = {}) {
const events = [];
let subscription;
try {
subscription = this.sub(relays, filters, opts);
} catch (error) {
throw error;
}
return new Promise((resolve) => {
let timer = null;
const finish = () => {
if (timer) {
clearTimeout(timer);
timer = null;
}
resolve(events);
};
if (!subscription || typeof subscription.on !== "function") {
finish();
return;
}
subscription.on("event", (event) => {
events.push(event);
});
subscription.on("eose", () => {
try {
subscription.unsub?.();
} catch (error) {
devLogger.warn("[nostr] Failed to unsubscribe after list EOSE.", error);
}
finish();
});
subscription.on("close", () => {
finish();
});
// Prevent hanging indefinitely if relays fail to EOSE
const timeoutMs =
Number.isFinite(opts.timeout) && opts.timeout > 0
? opts.timeout
: 7000;
timer = setTimeout(() => {
devLogger.warn(`[toolkit] pool.list timed out after ${timeoutMs}ms.`);
timer = null;
try {
subscription.unsub?.();
} catch (error) {
// ignore
}
finish();
}, timeoutMs);
});
};
}
pool[SIMPLE_POOL_SHIM_KEY] = true;
return pool;
}