mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
fix(watch-history): a snapshot succeeds if ANY relay accepts (not all)
This is why logged-in removal "worked but threw a flag error", and likely why new watches stopped appearing in history. publishMonthRecord reported success only when EVERY write relay accepted (acceptedCount === relays.length). With a 20+ relay list many always reject (auth-required/rate-limited/not-in-write-set), so every snapshot — removals AND new-watch tracking — was marked "partial-relay-acceptance" -> ok:false -> "watch-history-snapshot-failed". The data WAS published (major relays accepted), but the UI showed a failure and the queue treated it as not-yet-synced. A watch-history event is durable if any relay accepted it (reads take the newest copy per d-tag), so success = acceptedCount > 0; only a zero-accept publish is a retryable failure. Also dropped the redundant partialAcceptance alias to stay within the file-size budget. Tests + mutation-verified: 1-of-3 accept -> ok/non-retryable; 0-of-3 -> failure/retryable. Reverting to the all-relays criterion fails the partial case. Build + full lint green. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
0756dd36d8
commit
565a961823
2 files changed
+82
-5
No files matched your search
@@ -1572,13 +1572,13 @@ class WatchHistoryManager {
|
||||
}
|
||||
}
|
||||
this.lastCreatedAt = createdAtCursor;
|
||||
const partialAcceptance = anyPartial;
|
||||
const success = acceptedCount === relays.length && !anyRejected;
|
||||
// Durable if ANY relay accepted (reads take newest per d-tag); all-relays was a false failure on large lists.
|
||||
const success = acceptedCount > 0;
|
||||
let errorCode = null;
|
||||
if (!success) {
|
||||
if (anyRejected) {
|
||||
errorCode = "publish-rejected";
|
||||
} else if (partialAcceptance) {
|
||||
} else if (anyPartial) {
|
||||
errorCode = "partial-relay-acceptance";
|
||||
}
|
||||
}
|
||||
@@ -1597,7 +1597,7 @@ class WatchHistoryManager {
|
||||
},
|
||||
skippedCount: skipped.length,
|
||||
source: options.source || "manual",
|
||||
partial: partialAcceptance,
|
||||
partial: anyPartial,
|
||||
};
|
||||
if (!success && errorCode) {
|
||||
result.error = errorCode;
|
||||
@@ -1607,7 +1607,7 @@ class WatchHistoryManager {
|
||||
actor: actorKey,
|
||||
monthIdentifier,
|
||||
success,
|
||||
partialAcceptance,
|
||||
partialAcceptance: anyPartial,
|
||||
error: result.error || null,
|
||||
acceptedCount,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
// A watch-history snapshot must succeed when ANY relay accepts it — a large/flaky
|
||||
// relay list always has rejectors, and reads take the newest copy per d-tag, so
|
||||
// one accepting relay is durable. Requiring ALL relays made every removal report
|
||||
// a false "watch-history-snapshot-failed" and stopped new watches from
|
||||
// persisting. Only a zero-accept publish is a (retryable) failure.
|
||||
|
||||
import "./test-helpers/setup-localstorage.mjs";
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { createWatchHistoryManager } from "../js/nostr/watchHistory.js";
|
||||
|
||||
const ACTOR = "a".repeat(64);
|
||||
const SESSION_KEY = "b".repeat(64);
|
||||
const GOOD = "wss://good.relay";
|
||||
|
||||
function makeManager(acceptUrls) {
|
||||
const accepted = new Set(acceptUrls);
|
||||
const toolkit = {
|
||||
nip04: { encrypt: async (_p, _pub, txt) => `enc:${txt}` },
|
||||
};
|
||||
return createWatchHistoryManager({
|
||||
getActivePubkey: () => ACTOR,
|
||||
getSessionActor: () => ({ pubkey: ACTOR, privateKey: SESSION_KEY }),
|
||||
ensureSessionActor: async () => ACTOR,
|
||||
resolveActiveSigner: () => null,
|
||||
shouldRequestExtensionPermissions: () => false,
|
||||
signEventWithPrivateKey: (event) => ({ ...event, id: `id-${event.created_at}-${Math.random()}`, sig: "sig" }),
|
||||
ensureNostrTools: async () => toolkit,
|
||||
getCachedNostrTools: () => toolkit,
|
||||
getWriteRelays: () => [GOOD, "wss://bad1.relay", "wss://bad2.relay"],
|
||||
getRelayFallback: () => [GOOD],
|
||||
getPool: () => ({
|
||||
publish(urls) {
|
||||
const url = Array.isArray(urls) ? urls[0] : urls;
|
||||
const ok = accepted.has(url);
|
||||
return {
|
||||
on(name, handler) {
|
||||
if (ok && name === "ok") setTimeout(handler, 0);
|
||||
if (!ok && name === "failed") setTimeout(() => handler("rejected"), 0);
|
||||
return this;
|
||||
},
|
||||
};
|
||||
},
|
||||
async list() {
|
||||
return [];
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
test("a month accepted by only ONE of several relays is a success (not a false failure)", async () => {
|
||||
const manager = makeManager([GOOD]); // 1 of 3 relays accepts
|
||||
try {
|
||||
const result = await manager.publishRecords(
|
||||
{ "2026-06": [{ type: "e", value: "watched-x", watchedAt: 1 }] },
|
||||
{ actorPubkey: ACTOR },
|
||||
);
|
||||
assert.equal(result.ok, true, "partial acceptance must report success");
|
||||
assert.equal(result.retryable, false, "a partial accept is not retryable");
|
||||
} finally {
|
||||
manager.clear();
|
||||
}
|
||||
});
|
||||
|
||||
test("a month rejected by ALL relays is a retryable failure", async () => {
|
||||
const manager = makeManager([]); // zero relays accept
|
||||
try {
|
||||
const result = await manager.publishRecords(
|
||||
{ "2026-06": [{ type: "e", value: "watched-x", watchedAt: 1 }] },
|
||||
{ actorPubkey: ACTOR },
|
||||
);
|
||||
assert.equal(result.ok, false, "zero acceptance is a failure");
|
||||
assert.equal(result.retryable, true, "a total failure should be retryable");
|
||||
} finally {
|
||||
manager.clear();
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user