mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-30 09:22:24 +00:00
Merge pull request #1053 from PR0M3TH3AN/codex/add-retries-to-batchfetchprofiles-call
Improve profile hydration retry diagnostics
This commit is contained in:
2 files changed
+144
-22
No files matched your search
@@ -9,6 +9,8 @@ import { FEATURE_IMPROVED_COMMENT_FETCHING } from "../constants.js";
|
||||
const ROOT_PARENT_KEY = "__root__";
|
||||
const DEFAULT_INITIAL_LIMIT = 40;
|
||||
const DEFAULT_HYDRATION_DEBOUNCE_MS = 25;
|
||||
const PROFILE_FETCH_MAX_ATTEMPTS = 3;
|
||||
const PROFILE_FETCH_BACKOFF_MS = 50;
|
||||
const COMMENT_CACHE_PREFIX = "bitvid:comments:";
|
||||
const COMMENT_CACHE_TTL_MS = 5 * 60 * 1000;
|
||||
const COMMENT_CACHE_VERSION = 2;
|
||||
@@ -874,31 +876,66 @@ export default class CommentThreadService {
|
||||
return;
|
||||
}
|
||||
|
||||
const hydrationPromise = Promise.resolve(
|
||||
this.batchFetchProfiles(pubkeys),
|
||||
)
|
||||
.catch((error) => {
|
||||
if (this.logger?.warn) {
|
||||
this.logger.warn(
|
||||
"[commentThread] Profile hydration failed:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
this.emitError(error);
|
||||
})
|
||||
.then(() => {
|
||||
pubkeys.forEach((pubkey) => {
|
||||
const profile = this.getProfileFromCache(pubkey);
|
||||
if (profile) {
|
||||
this.profileCache.set(pubkey, profile);
|
||||
const pubkeyListLog = pubkeys.join(", ") || "(empty profile batch)";
|
||||
const hydrationPromise = (async () => {
|
||||
try {
|
||||
let attempt = 0;
|
||||
let lastError = null;
|
||||
|
||||
while (attempt < PROFILE_FETCH_MAX_ATTEMPTS) {
|
||||
try {
|
||||
await Promise.resolve(this.batchFetchProfiles(pubkeys));
|
||||
break;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
const attemptLabel = `${attempt + 1}/${PROFILE_FETCH_MAX_ATTEMPTS}`;
|
||||
if (this.logger?.dev?.warn) {
|
||||
this.logger.dev.warn(
|
||||
`[commentThread] Profile hydration attempt ${attemptLabel} failed for pubkeys: ${pubkeyListLog}.`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
|
||||
attempt += 1;
|
||||
if (attempt >= PROFILE_FETCH_MAX_ATTEMPTS) {
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
const backoffMs = PROFILE_FETCH_BACKOFF_MS * attempt;
|
||||
logDev(
|
||||
this.logger?.dev,
|
||||
`[commentThread] Retrying profile hydration in ${backoffMs}ms for pubkeys: ${pubkeyListLog}.`,
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, backoffMs));
|
||||
}
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
if (this.profileHydrationPromise === hydrationPromise) {
|
||||
this.profileHydrationPromise = null;
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = `[commentThread] Profile hydration failed for pubkeys: ${pubkeyListLog}.`;
|
||||
if (this.logger?.user?.warn) {
|
||||
this.logger.user.warn(errorMessage, error);
|
||||
} else if (this.logger?.warn) {
|
||||
this.logger.warn(errorMessage, error);
|
||||
}
|
||||
|
||||
if (this.logger?.dev?.warn && this.logger.dev !== this.logger.user) {
|
||||
this.logger.dev.warn(errorMessage, error);
|
||||
}
|
||||
|
||||
this.emitError(error);
|
||||
return;
|
||||
}
|
||||
|
||||
pubkeys.forEach((pubkey) => {
|
||||
const profile = this.getProfileFromCache(pubkey);
|
||||
if (profile) {
|
||||
this.profileCache.set(pubkey, profile);
|
||||
}
|
||||
});
|
||||
})().finally(() => {
|
||||
if (this.profileHydrationPromise === hydrationPromise) {
|
||||
this.profileHydrationPromise = null;
|
||||
}
|
||||
});
|
||||
|
||||
this.profileHydrationPromise = hydrationPromise;
|
||||
await hydrationPromise;
|
||||
|
||||
@@ -416,6 +416,91 @@ test("CommentThreadService normalizes mixed-case pubkeys during hydration", asyn
|
||||
);
|
||||
});
|
||||
|
||||
test("CommentThreadService retries profile hydration before succeeding", async () => {
|
||||
const profiles = new Map();
|
||||
const hydrationCalls = [];
|
||||
let attempts = 0;
|
||||
|
||||
const service = new CommentThreadService({
|
||||
getProfileCacheEntry: (pubkey) => profiles.get(pubkey),
|
||||
batchFetchProfiles: async (pubkeys) => {
|
||||
hydrationCalls.push([...pubkeys]);
|
||||
attempts += 1;
|
||||
if (attempts === 1) {
|
||||
throw new Error("temporary hydration failure");
|
||||
}
|
||||
pubkeys.forEach((pubkey) =>
|
||||
profiles.set(pubkey, { name: `Profile ${pubkey}` }),
|
||||
);
|
||||
},
|
||||
hydrationDebounceMs: 0,
|
||||
});
|
||||
|
||||
service.queueProfileForHydration("pk-retry-1");
|
||||
service.queueProfileForHydration("pk-retry-2");
|
||||
|
||||
for (let attempt = 0; attempt < 5 && !service.profileHydrationPromise; attempt += 1) {
|
||||
await tick(10);
|
||||
}
|
||||
await service.waitForProfileHydration();
|
||||
|
||||
assert.equal(attempts, 2, "hydration should retry failed batches once");
|
||||
assert.deepEqual(
|
||||
hydrationCalls,
|
||||
[
|
||||
["pk-retry-1", "pk-retry-2"],
|
||||
["pk-retry-1", "pk-retry-2"],
|
||||
],
|
||||
"hydration should reuse the same batch across retries",
|
||||
);
|
||||
assert.ok(service.getProfile("pk-retry-1"));
|
||||
assert.ok(service.getProfile("pk-retry-2"));
|
||||
});
|
||||
|
||||
test(
|
||||
"CommentThreadService surfaces profile hydration failures after retries",
|
||||
async () => {
|
||||
const errors = [];
|
||||
const userWarnings = [];
|
||||
const devWarnings = [];
|
||||
let attempts = 0;
|
||||
|
||||
const service = new CommentThreadService({
|
||||
batchFetchProfiles: async (pubkeys) => {
|
||||
attempts += 1;
|
||||
throw new Error(`hydration failed attempt ${attempts}`);
|
||||
},
|
||||
logger: {
|
||||
warn: (...args) => userWarnings.push(args),
|
||||
dev: { warn: (...args) => devWarnings.push(args) },
|
||||
user: { warn: (...args) => userWarnings.push(args) },
|
||||
},
|
||||
hydrationDebounceMs: 0,
|
||||
});
|
||||
|
||||
service.setCallbacks({ onError: (error) => errors.push(error) });
|
||||
service.queueProfileForHydration("pk-retry-fail");
|
||||
|
||||
await tick(200);
|
||||
await service.waitForProfileHydration();
|
||||
|
||||
assert.equal(attempts, 3, "hydration should respect the retry limit");
|
||||
assert.equal(errors.length, 1, "final failures should emit an error");
|
||||
assert.ok(
|
||||
userWarnings.some(([message]) =>
|
||||
typeof message === "string" && message.includes("pk-retry-fail"),
|
||||
),
|
||||
"user-visible warnings should include the failed pubkeys",
|
||||
);
|
||||
assert.ok(
|
||||
devWarnings.some(([message]) =>
|
||||
typeof message === "string" && message.includes("attempt 1/3")
|
||||
),
|
||||
"dev warnings should log the retry attempts",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
"listVideoComments accepts builder events without parent ids and filters replies",
|
||||
async () => {
|
||||
|
||||
Reference in new issue
Block a user