mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-30 17:32:26 +00:00
Handle background block list fetch fallbacks
This commit is contained in:
1 parent
d73bd4c5e9
commit
b0f0c86f19
2 files changed
+278
-40
No files matched your search
+120
-38
@@ -54,7 +54,10 @@ class TinyEventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
export const USER_BLOCK_EVENTS = Object.freeze({ CHANGE: "change" });
|
||||
export const USER_BLOCK_EVENTS = Object.freeze({
|
||||
CHANGE: "change",
|
||||
STATUS: "status",
|
||||
});
|
||||
|
||||
const FAST_BLOCKLIST_RELAY_LIMIT = 3;
|
||||
const FAST_BLOCKLIST_TIMEOUT_MS = 2500;
|
||||
@@ -138,7 +141,42 @@ class UserBlockListManager {
|
||||
const statusCallback =
|
||||
typeof options?.statusCallback === "function" ? options.statusCallback : null;
|
||||
|
||||
statusCallback?.({ status: "loading", relays: Array.from(nostrClient.relays || []) });
|
||||
const emitStatus = (detail) => {
|
||||
if (!detail || typeof detail !== "object") {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
statusCallback?.(detail);
|
||||
} catch (callbackError) {
|
||||
userLogger.warn(
|
||||
"[UserBlockList] statusCallback threw while emitting status",
|
||||
callbackError,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
this.emitter.emit(USER_BLOCK_EVENTS.STATUS, detail);
|
||||
} catch (emitterError) {
|
||||
userLogger.warn(
|
||||
"[UserBlockList] Failed to dispatch status event",
|
||||
emitterError,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
emitStatus({ status: "loading", relays: Array.from(nostrClient.relays || []) });
|
||||
|
||||
const applyBlockedPubkeys = (nextValues, meta = {}) => {
|
||||
const nextSet = new Set(Array.isArray(nextValues) ? nextValues : []);
|
||||
this.blockedPubkeys = nextSet;
|
||||
|
||||
this.emitter.emit(USER_BLOCK_EVENTS.CHANGE, {
|
||||
action: "sync",
|
||||
blockedPubkeys: Array.from(this.blockedPubkeys),
|
||||
...meta,
|
||||
});
|
||||
};
|
||||
|
||||
const permissionResult = await requestDefaultExtensionPermissions();
|
||||
if (!permissionResult.ok) {
|
||||
@@ -152,8 +190,8 @@ class UserBlockListManager {
|
||||
permissionResult.error instanceof Error
|
||||
? permissionResult.error
|
||||
: new Error("Extension permissions required to load block list.");
|
||||
statusCallback?.({ status: "error", error });
|
||||
statusCallback?.({ status: "settled" });
|
||||
emitStatus({ status: "error", error });
|
||||
emitStatus({ status: "settled" });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -163,11 +201,11 @@ class UserBlockListManager {
|
||||
);
|
||||
this.reset();
|
||||
this.loaded = true;
|
||||
statusCallback?.({
|
||||
emitStatus({
|
||||
status: "error",
|
||||
error: new Error("nip04.decrypt is unavailable"),
|
||||
});
|
||||
statusCallback?.({ status: "settled" });
|
||||
emitStatus({ status: "settled" });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -192,8 +230,8 @@ class UserBlockListManager {
|
||||
this.blockEventId = null;
|
||||
this.blockEventCreatedAt = null;
|
||||
this.loaded = true;
|
||||
statusCallback?.({ status: "applied-empty" });
|
||||
statusCallback?.({ status: "settled" });
|
||||
emitStatus({ status: "applied-empty" });
|
||||
emitStatus({ status: "settled" });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -203,6 +241,16 @@ class UserBlockListManager {
|
||||
const fetchFromRelay = (relayUrl, timeoutMs, requireEvent) =>
|
||||
new Promise((resolve, reject) => {
|
||||
let settled = false;
|
||||
const pool = nostrClient?.pool;
|
||||
if (!pool || typeof pool.list !== "function") {
|
||||
const poolError = new Error(
|
||||
"nostrClient.pool.list is unavailable; cannot query block list.",
|
||||
);
|
||||
poolError.code = "pool-unavailable";
|
||||
poolError.relay = relayUrl;
|
||||
reject(poolError);
|
||||
return;
|
||||
}
|
||||
const timer = setTimeout(() => {
|
||||
if (settled) {
|
||||
return;
|
||||
@@ -218,7 +266,7 @@ class UserBlockListManager {
|
||||
}, timeoutMs);
|
||||
|
||||
Promise.resolve()
|
||||
.then(() => nostrClient.pool.list([relayUrl], [filter]))
|
||||
.then(() => pool.list([relayUrl], [filter]))
|
||||
.then((result) => {
|
||||
if (settled) {
|
||||
return;
|
||||
@@ -259,19 +307,22 @@ class UserBlockListManager {
|
||||
fetchFromRelay(relayUrl, BACKGROUND_BLOCKLIST_TIMEOUT_MS, false)
|
||||
);
|
||||
|
||||
const applyEvents = async (events, { skipIfEmpty = false } = {}) => {
|
||||
const applyEvents = async (
|
||||
events,
|
||||
{ skipIfEmpty = false, source = "fast" } = {},
|
||||
) => {
|
||||
if (!Array.isArray(events) || !events.length) {
|
||||
if (skipIfEmpty) {
|
||||
return;
|
||||
}
|
||||
if (this.lastPublishedCreatedAt !== null || this.blockEventCreatedAt !== null) {
|
||||
statusCallback?.({ status: "stale", reason: "empty-result" });
|
||||
emitStatus({ status: "stale", reason: "empty-result", source });
|
||||
return;
|
||||
}
|
||||
this.blockedPubkeys.clear();
|
||||
this.blockEventId = null;
|
||||
this.blockEventCreatedAt = null;
|
||||
statusCallback?.({ status: "applied-empty" });
|
||||
applyBlockedPubkeys([], { source, reason: "empty-result" });
|
||||
emitStatus({ status: "applied-empty", source });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -284,13 +335,13 @@ class UserBlockListManager {
|
||||
return;
|
||||
}
|
||||
if (this.lastPublishedCreatedAt !== null || this.blockEventCreatedAt !== null) {
|
||||
statusCallback?.({ status: "stale", reason: "empty-result" });
|
||||
emitStatus({ status: "stale", reason: "empty-result", source });
|
||||
return;
|
||||
}
|
||||
this.blockedPubkeys.clear();
|
||||
this.blockEventId = null;
|
||||
this.blockEventCreatedAt = null;
|
||||
statusCallback?.({ status: "applied-empty" });
|
||||
applyBlockedPubkeys([], { source, reason: "empty-result" });
|
||||
emitStatus({ status: "applied-empty", source });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -304,7 +355,7 @@ class UserBlockListManager {
|
||||
);
|
||||
|
||||
if (newestCreatedAt < guardCreatedAt) {
|
||||
statusCallback?.({ status: "stale", event: newest, guardCreatedAt });
|
||||
emitStatus({ status: "stale", event: newest, guardCreatedAt, source });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -314,7 +365,7 @@ class UserBlockListManager {
|
||||
newest?.id &&
|
||||
newest.id !== this.blockEventId
|
||||
) {
|
||||
statusCallback?.({ status: "stale", event: newest, guardCreatedAt });
|
||||
emitStatus({ status: "stale", event: newest, guardCreatedAt, source });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -322,7 +373,7 @@ class UserBlockListManager {
|
||||
this.blockEventCreatedAt = Number.isFinite(newestCreatedAt)
|
||||
? newestCreatedAt
|
||||
: this.blockEventCreatedAt;
|
||||
statusCallback?.({ status: "confirmed", event: newest });
|
||||
emitStatus({ status: "confirmed", event: newest, source });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -332,8 +383,8 @@ class UserBlockListManager {
|
||||
: null;
|
||||
|
||||
if (!newest?.content) {
|
||||
this.blockedPubkeys.clear();
|
||||
statusCallback?.({ status: "applied-empty", event: newest });
|
||||
applyBlockedPubkeys([], { source, reason: "empty-event", event: newest });
|
||||
emitStatus({ status: "applied-empty", event: newest, source });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -342,8 +393,13 @@ class UserBlockListManager {
|
||||
decrypted = await window.nostr.nip04.decrypt(normalized, newest.content);
|
||||
} catch (err) {
|
||||
userLogger.error("[UserBlockList] Failed to decrypt block list:", err);
|
||||
this.blockedPubkeys.clear();
|
||||
statusCallback?.({ status: "error", event: newest, error: err });
|
||||
applyBlockedPubkeys([], {
|
||||
source,
|
||||
reason: "decrypt-error",
|
||||
event: newest,
|
||||
error: err,
|
||||
});
|
||||
emitStatus({ status: "error", event: newest, error: err, source });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -363,16 +419,26 @@ class UserBlockListManager {
|
||||
}
|
||||
return true;
|
||||
});
|
||||
this.blockedPubkeys = new Set(sanitized);
|
||||
statusCallback?.({
|
||||
applyBlockedPubkeys(sanitized, {
|
||||
source,
|
||||
reason: "applied",
|
||||
event: newest,
|
||||
});
|
||||
emitStatus({
|
||||
status: "applied",
|
||||
event: newest,
|
||||
blockedPubkeys: Array.from(this.blockedPubkeys),
|
||||
source,
|
||||
});
|
||||
} catch (err) {
|
||||
userLogger.error("[UserBlockList] Failed to parse block list:", err);
|
||||
this.blockedPubkeys.clear();
|
||||
statusCallback?.({ status: "error", event: newest, error: err });
|
||||
applyBlockedPubkeys([], {
|
||||
source,
|
||||
reason: "parse-error",
|
||||
event: newest,
|
||||
error: err,
|
||||
});
|
||||
emitStatus({ status: "error", event: newest, error: err, source });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -407,13 +473,15 @@ class UserBlockListManager {
|
||||
}
|
||||
|
||||
if (!aggregated.length) {
|
||||
return;
|
||||
return { foundEvents: false };
|
||||
}
|
||||
|
||||
await applyEvents(aggregated, { skipIfEmpty: true });
|
||||
await applyEvents(aggregated, { skipIfEmpty: true, source: "background" });
|
||||
return { foundEvents: true };
|
||||
})
|
||||
.catch((error) => {
|
||||
userLogger.error("[UserBlockList] background block list refresh failed:", error);
|
||||
return { foundEvents: false, error };
|
||||
});
|
||||
|
||||
let fastResult = null;
|
||||
@@ -436,25 +504,39 @@ class UserBlockListManager {
|
||||
}
|
||||
|
||||
if (fastResult?.events?.length) {
|
||||
await applyEvents(fastResult.events);
|
||||
await applyEvents(fastResult.events, { source: "fast" });
|
||||
background.catch(() => {});
|
||||
return;
|
||||
}
|
||||
|
||||
this.blockedPubkeys.clear();
|
||||
this.blockEventId = null;
|
||||
this.blockEventCreatedAt = null;
|
||||
statusCallback?.({ status: "applied-empty" });
|
||||
background.catch(() => {});
|
||||
if (backgroundRelays.length || fastPromises.length) {
|
||||
emitStatus({
|
||||
status: "awaiting-background",
|
||||
relays: backgroundRelays.length ? backgroundRelays : fastRelays,
|
||||
});
|
||||
}
|
||||
|
||||
const backgroundOutcome = await background;
|
||||
|
||||
if (backgroundOutcome?.foundEvents) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (backgroundOutcome?.error) {
|
||||
emitStatus({ status: "error", error: backgroundOutcome.error, source: "background" });
|
||||
return;
|
||||
}
|
||||
|
||||
await applyEvents([], { source: "background" });
|
||||
} catch (error) {
|
||||
userLogger.error("[UserBlockList] loadBlocks failed:", error);
|
||||
this.blockedPubkeys.clear();
|
||||
applyBlockedPubkeys([], { source: "fast", reason: "load-error", error });
|
||||
this.blockEventId = null;
|
||||
this.blockEventCreatedAt = null;
|
||||
statusCallback?.({ status: "error", error });
|
||||
emitStatus({ status: "error", error });
|
||||
} finally {
|
||||
this.loaded = true;
|
||||
statusCallback?.({ status: "settled" });
|
||||
emitStatus({ status: "settled" });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+158
-2
@@ -1,7 +1,7 @@
|
||||
import "./test-helpers/setup-localstorage.mjs";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const { userBlocks } = await import("../js/userBlocks.js");
|
||||
const { userBlocks, USER_BLOCK_EVENTS } = await import("../js/userBlocks.js");
|
||||
const { nostrClient } = await import("../js/nostr.js");
|
||||
|
||||
if (typeof globalThis.window === "undefined") {
|
||||
@@ -55,6 +55,12 @@ await (async () => {
|
||||
nostrClient.pool = pool;
|
||||
nostrClient.relays = relays;
|
||||
|
||||
assert.equal(
|
||||
typeof nostrClient.pool?.list,
|
||||
"function",
|
||||
"nostrClient.pool.list should be stubbed for block list tests",
|
||||
);
|
||||
|
||||
userBlocks.blockedPubkeys = new Set();
|
||||
userBlocks.blockEventId = null;
|
||||
userBlocks.loaded = false;
|
||||
@@ -62,7 +68,7 @@ await (async () => {
|
||||
try {
|
||||
const loadPromise = userBlocks.loadBlocks(actor);
|
||||
|
||||
await Promise.resolve();
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
assert.deepEqual(
|
||||
calls,
|
||||
@@ -75,6 +81,7 @@ await (async () => {
|
||||
{
|
||||
id: "event-old",
|
||||
created_at: 1700,
|
||||
pubkey: actor,
|
||||
content: "ciphertext-old",
|
||||
},
|
||||
],
|
||||
@@ -82,6 +89,7 @@ await (async () => {
|
||||
{
|
||||
id: "event-new",
|
||||
created_at: 1800,
|
||||
pubkey: actor,
|
||||
content: "ciphertext-new",
|
||||
},
|
||||
],
|
||||
@@ -113,3 +121,151 @@ await (async () => {
|
||||
window.nostr = originalNostr;
|
||||
}
|
||||
})();
|
||||
|
||||
await (async () => {
|
||||
const actor = "a".repeat(64);
|
||||
const initialBlocked = "b".repeat(64);
|
||||
const backgroundBlocked = "c".repeat(64);
|
||||
const relays = [
|
||||
"wss://fast-one.example",
|
||||
"wss://fast-two.example",
|
||||
"wss://fast-three.example",
|
||||
"wss://background.example",
|
||||
];
|
||||
|
||||
const originalNostr = window.nostr;
|
||||
const originalPool = nostrClient.pool;
|
||||
const originalRelays = Array.isArray(nostrClient.relays)
|
||||
? [...nostrClient.relays]
|
||||
: nostrClient.relays;
|
||||
const originalBlocked = new Set(userBlocks.blockedPubkeys);
|
||||
const originalBlockEventId = userBlocks.blockEventId;
|
||||
const originalBlockEventCreatedAt = userBlocks.blockEventCreatedAt;
|
||||
const originalLoaded = userBlocks.loaded;
|
||||
|
||||
const calls = [];
|
||||
const resolvers = [];
|
||||
|
||||
window.nostr = {
|
||||
...(originalNostr || {}),
|
||||
nip04: {
|
||||
...((originalNostr && originalNostr.nip04) || {}),
|
||||
decrypt: async () =>
|
||||
JSON.stringify({
|
||||
blockedPubkeys: [backgroundBlocked],
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
const pool = originalPool ? { ...originalPool } : {};
|
||||
pool.list = (targets) => {
|
||||
const relay = Array.isArray(targets) ? targets[0] : undefined;
|
||||
calls.push(relay);
|
||||
let resolve;
|
||||
const promise = new Promise((res) => {
|
||||
resolve = res;
|
||||
});
|
||||
resolvers.push({ relay, resolve });
|
||||
return promise;
|
||||
};
|
||||
|
||||
nostrClient.pool = pool;
|
||||
nostrClient.relays = relays;
|
||||
|
||||
assert.equal(
|
||||
typeof nostrClient.pool?.list,
|
||||
"function",
|
||||
"nostrClient.pool.list should be stubbed for background relay tests",
|
||||
);
|
||||
|
||||
userBlocks.blockedPubkeys = new Set([initialBlocked]);
|
||||
userBlocks.blockEventId = "existing-event";
|
||||
userBlocks.blockEventCreatedAt = 1_000;
|
||||
userBlocks.loaded = false;
|
||||
|
||||
const statusEvents = [];
|
||||
const changeEvents = [];
|
||||
const unsubscribeStatus = userBlocks.on(USER_BLOCK_EVENTS.STATUS, (detail) => {
|
||||
statusEvents.push(detail);
|
||||
});
|
||||
const unsubscribeChange = userBlocks.on(USER_BLOCK_EVENTS.CHANGE, (detail) => {
|
||||
changeEvents.push(detail);
|
||||
});
|
||||
|
||||
try {
|
||||
const loadPromise = userBlocks.loadBlocks(actor);
|
||||
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
assert.deepEqual(
|
||||
calls,
|
||||
relays,
|
||||
"loadBlocks should query fast and background relays concurrently",
|
||||
);
|
||||
|
||||
for (const { relay, resolve } of resolvers) {
|
||||
if (relay !== relays[3]) {
|
||||
resolve([]);
|
||||
}
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
assert.deepEqual(
|
||||
userBlocks.getBlockedPubkeys(),
|
||||
[initialBlocked],
|
||||
"existing blocked list should be preserved while waiting for background relays",
|
||||
);
|
||||
|
||||
const backgroundResolver = resolvers.find((entry) => entry.relay === relays[3]);
|
||||
backgroundResolver.resolve([
|
||||
{
|
||||
id: "background-event",
|
||||
pubkey: actor,
|
||||
created_at: 2_000,
|
||||
content: "ciphertext-background",
|
||||
},
|
||||
]);
|
||||
|
||||
await loadPromise;
|
||||
|
||||
assert.equal(
|
||||
userBlocks.blockEventId,
|
||||
"background-event",
|
||||
"background relay event should be applied after fast relay failures",
|
||||
);
|
||||
assert.deepEqual(
|
||||
userBlocks.getBlockedPubkeys(),
|
||||
[backgroundBlocked],
|
||||
"block list should hydrate from background relay payload",
|
||||
);
|
||||
|
||||
assert(
|
||||
statusEvents.some((detail) => detail?.status === "awaiting-background"),
|
||||
"status events should indicate when background relays are pending",
|
||||
);
|
||||
assert(
|
||||
statusEvents.some((detail) => detail?.status === "applied"),
|
||||
"status events should report when the background payload is applied",
|
||||
);
|
||||
assert(
|
||||
changeEvents.some(
|
||||
(detail) =>
|
||||
detail?.action === "sync" &&
|
||||
Array.isArray(detail?.blockedPubkeys) &&
|
||||
detail.blockedPubkeys.includes(backgroundBlocked),
|
||||
),
|
||||
"change events should emit sync updates after applying background payloads",
|
||||
);
|
||||
} finally {
|
||||
unsubscribeStatus?.();
|
||||
unsubscribeChange?.();
|
||||
userBlocks.blockedPubkeys = originalBlocked;
|
||||
userBlocks.blockEventId = originalBlockEventId;
|
||||
userBlocks.blockEventCreatedAt = originalBlockEventCreatedAt;
|
||||
userBlocks.loaded = originalLoaded;
|
||||
nostrClient.pool = originalPool;
|
||||
nostrClient.relays = originalRelays;
|
||||
window.nostr = originalNostr;
|
||||
}
|
||||
})();
|
||||
Reference in new issue
Block a user