mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 16:02:26 +00:00
Merge pull request #957 from PR0M3TH3AN/codex/fix-nostr-relay-connection-issues
Fix Nostr bootstrap guard for SimplePool compatibility
This commit is contained in:
4 files changed
+431
-17
No files matched your search
@@ -79,6 +79,7 @@ import {
|
||||
nostrToolsBootstrapFailure,
|
||||
readToolkitFromScope,
|
||||
resolveSimplePoolConstructor,
|
||||
shimLegacySimplePoolMethods,
|
||||
} from "./toolkit.js";
|
||||
import { devLogger, userLogger } from "../utils/logger.js";
|
||||
import {
|
||||
@@ -3123,6 +3124,7 @@ export class NostrClient {
|
||||
|
||||
const creation = Promise.resolve().then(() => {
|
||||
const instance = new SimplePool();
|
||||
shimLegacySimplePoolMethods(instance);
|
||||
this.pool = instance;
|
||||
return instance;
|
||||
});
|
||||
|
||||
+315
-4
@@ -25,6 +25,65 @@ const globalScope =
|
||||
? 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 &&
|
||||
@@ -210,10 +269,14 @@ function isSimplePoolConstructor(candidate) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
typeof prototype.sub === "function" &&
|
||||
typeof prototype.close === "function"
|
||||
);
|
||||
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) {
|
||||
@@ -274,3 +337,251 @@ export function resolveSimplePoolConstructor(tools, scope = globalScope) {
|
||||
}
|
||||
|
||||
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) {
|
||||
// subscribeMany only accepts a single filter, so reuse subscribeMap-style batching
|
||||
// by issuing one subscription per filter.
|
||||
const subs = filterList.map((filter) =>
|
||||
subscribeMany(relayList, filter, subscribeParams)
|
||||
);
|
||||
closer = {
|
||||
close: (reason) => {
|
||||
subs.forEach((sub) => {
|
||||
try {
|
||||
sub?.close?.(reason);
|
||||
} catch (error) {
|
||||
devLogger.warn("[nostr] Failed to close SimplePool subscription.", error);
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
} 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 {
|
||||
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) => {
|
||||
if (!subscription || typeof subscription.on !== "function") {
|
||||
resolve(events);
|
||||
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);
|
||||
}
|
||||
resolve(events);
|
||||
});
|
||||
subscription.on("close", () => {
|
||||
resolve(events);
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
pool[SIMPLE_POOL_SHIM_KEY] = true;
|
||||
return pool;
|
||||
}
|
||||
+27
-13
@@ -584,19 +584,33 @@ export function bootstrapNostrTools() {
|
||||
return activeTools;
|
||||
};
|
||||
|
||||
try {
|
||||
Object.defineProperty(scope, "NostrTools", {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get() {
|
||||
return activeTools;
|
||||
},
|
||||
set(value) {
|
||||
applyActive(value);
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
userLogger.warn("[bitvid] Failed to install NostrTools guard.", error);
|
||||
const existingDescriptor =
|
||||
scope && typeof scope === "object"
|
||||
? Object.getOwnPropertyDescriptor(scope, "NostrTools")
|
||||
: null;
|
||||
|
||||
const canInstallGuard =
|
||||
!existingDescriptor || existingDescriptor.configurable === true;
|
||||
|
||||
if (canInstallGuard) {
|
||||
try {
|
||||
Object.defineProperty(scope, "NostrTools", {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get() {
|
||||
return activeTools;
|
||||
},
|
||||
set(value) {
|
||||
applyActive(value);
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
userLogger.warn("[bitvid] Failed to install NostrTools guard.", error);
|
||||
}
|
||||
} else {
|
||||
devLogger.info(
|
||||
"[bitvid] Skipping NostrTools guard; existing property is non-configurable."
|
||||
);
|
||||
}
|
||||
|
||||
applyActive(existingGlobalTools);
|
||||
|
||||
@@ -29,6 +29,7 @@ const {
|
||||
getCachedNostrTools,
|
||||
ensureNostrTools,
|
||||
resolveSimplePoolConstructor,
|
||||
shimLegacySimplePoolMethods,
|
||||
} = toolkitModule;
|
||||
|
||||
const baselineToolkit = getCachedNostrTools() || bootstrapToolkit;
|
||||
@@ -165,3 +166,89 @@ test("resolveSimplePoolConstructor handles supported permutations", () => {
|
||||
"returns null when no candidates are available"
|
||||
);
|
||||
});
|
||||
|
||||
test("shimLegacySimplePoolMethods adds legacy sub/list wrappers", async () => {
|
||||
let capturedRequests = null;
|
||||
let capturedParams = null;
|
||||
const closeReasons = [];
|
||||
|
||||
const mockPool = {
|
||||
subscribeMap(requests, params) {
|
||||
capturedRequests = requests;
|
||||
capturedParams = params;
|
||||
return {
|
||||
close(reason) {
|
||||
closeReasons.push(reason);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
shimLegacySimplePoolMethods(mockPool);
|
||||
|
||||
assert.strictEqual(typeof mockPool.sub, "function");
|
||||
assert.strictEqual(typeof mockPool.list, "function");
|
||||
|
||||
const relays = ["wss://relay.one", "wss://relay.two"];
|
||||
const filters = [{ kinds: [1] }, { authors: ["abc"] }];
|
||||
|
||||
const sub = mockPool.sub(relays, filters, {
|
||||
alreadyHaveEvent(id) {
|
||||
return id === "skip";
|
||||
},
|
||||
});
|
||||
|
||||
assert.ok(Array.isArray(capturedRequests));
|
||||
assert.strictEqual(capturedRequests.length, relays.length * filters.length);
|
||||
assert.ok(capturedRequests.every((entry) => entry && entry.url && entry.filter));
|
||||
assert.ok(capturedParams);
|
||||
assert.strictEqual(typeof capturedParams.onevent, "function");
|
||||
assert.strictEqual(typeof capturedParams.oneose, "function");
|
||||
assert.strictEqual(typeof capturedParams.onclose, "function");
|
||||
assert.strictEqual(typeof capturedParams.alreadyHaveEvent, "function");
|
||||
|
||||
const receivedEvents = [];
|
||||
const eosePromise = new Promise((resolve) => sub.on("eose", resolve));
|
||||
const closePromise = new Promise((resolve) => sub.on("close", resolve));
|
||||
sub.on("event", (event) => {
|
||||
receivedEvents.push(event.id);
|
||||
});
|
||||
|
||||
assert.strictEqual(capturedParams.alreadyHaveEvent("skip"), true);
|
||||
assert.strictEqual(capturedParams.alreadyHaveEvent("id-1"), false);
|
||||
assert.strictEqual(capturedParams.alreadyHaveEvent("id-1"), true);
|
||||
|
||||
capturedParams.onevent({ id: "id-1" });
|
||||
capturedParams.onevent({ id: "id-2" });
|
||||
capturedParams.oneose();
|
||||
capturedParams.onclose(["done"]);
|
||||
|
||||
await eosePromise;
|
||||
const closePayload = await closePromise;
|
||||
|
||||
assert.deepStrictEqual(receivedEvents, ["id-1", "id-2"]);
|
||||
assert.deepStrictEqual(closePayload, ["done"]);
|
||||
|
||||
sub.unsub();
|
||||
|
||||
closeReasons.length = 0;
|
||||
const immediateUnsub = mockPool.sub(["wss://relay.extra"], [{ kinds: [2] }]);
|
||||
immediateUnsub.unsub();
|
||||
assert.ok(closeReasons.includes("closed by caller"));
|
||||
|
||||
capturedParams = null;
|
||||
capturedRequests = null;
|
||||
closeReasons.length = 0;
|
||||
|
||||
const listPromise = mockPool.list(["wss://relay.example"], [{ kinds: [1] }]);
|
||||
assert.ok(capturedParams);
|
||||
capturedParams.onevent({ id: "evt-1" });
|
||||
capturedParams.onevent({ id: "evt-2" });
|
||||
capturedParams.oneose();
|
||||
|
||||
const listed = await listPromise;
|
||||
assert.deepStrictEqual(
|
||||
listed.map((event) => event.id),
|
||||
["evt-1", "evt-2"]
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user