mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
603 lines
15 KiB
JavaScript
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;
|
|
}
|