mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
nip71: defer ingest until feed renders; diagnose display path
Live logs proved ingest works (subscribed to 17 whitelisted authors,
received 61 events, injected 39) — but injecting foreign authors during
cold-start triggers the moderation/WoT stage to hydrate their social graph
(kind 30000), which competes with the initial feed load and stalls it
("Fetching recent videos…" hanging, needs a refresh).
- startWhenFeedReady(): defer the first ingest subscription until the
native feed's first videos:updated (with a 12s fallback for empty
feeds), keeping ingest off the cold-start critical path. Wired in
applicationBootstrap (kept thin so the size cap holds).
- emitRefresh now logs how many ingested videos survive the feed filter
("N ingested of M in filtered feed") so we can tell whether foreign
videos are filtered out vs. injected-but-not-rendered.
Tests: defer-until-feed-ready + fallback-timer; mutation-verified.
1236/1237 unit (known UploadModal flake).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
c4df08d5a1
commit
d1246dc113
3 files changed
+98
-5
No files matched your search
@@ -36,6 +36,11 @@ const SUBSCRIPTION_KEY = "nip71-ingest";
|
||||
// subscribing.
|
||||
const OPEN_RETRY_DELAY_MS = 2000;
|
||||
const MAX_OPEN_ATTEMPTS = 30; // ~60s of retries
|
||||
// Defer the first subscription until the native feed has rendered (signalled by
|
||||
// the first videos:updated). Injecting foreign authors hydrates their WoT graph
|
||||
// (kind 30000), which during cold-start competes with the initial feed load; a
|
||||
// fallback covers the empty-feed case where no videos:updated ever fires.
|
||||
const FEED_READY_FALLBACK_MS = 12000;
|
||||
|
||||
export function createNip71IngestService({
|
||||
nostrClient,
|
||||
@@ -48,6 +53,7 @@ export function createNip71IngestService({
|
||||
openRetryDelayMs = OPEN_RETRY_DELAY_MS,
|
||||
maxOpenAttempts = MAX_OPEN_ATTEMPTS,
|
||||
refreshThrottleMs = REFRESH_THROTTLE_MS,
|
||||
feedReadyFallbackMs = FEED_READY_FALLBACK_MS,
|
||||
} = {}) {
|
||||
let subscription = null;
|
||||
let buffer = [];
|
||||
@@ -58,6 +64,9 @@ export function createNip71IngestService({
|
||||
let started = false;
|
||||
let lastRefreshAt = 0;
|
||||
let pendingRefreshTimer = null;
|
||||
let deferArmed = false;
|
||||
let deferTimer = null;
|
||||
let offFeedReady = null;
|
||||
|
||||
function isAvailable() {
|
||||
return (
|
||||
@@ -199,6 +208,13 @@ export function createNip71IngestService({
|
||||
} catch (error) {
|
||||
logger?.warn?.("[nip71Ingest] getFilteredActiveVideos failed", error);
|
||||
}
|
||||
// Confirm ingested videos survive the feed filter (whitelist/NSFW/etc.).
|
||||
const foreignCount = Array.isArray(videos)
|
||||
? videos.filter((v) => v?.source === "nip71-ingest").length
|
||||
: 0;
|
||||
userLogger.info(
|
||||
`[nip71Ingest] refresh: ${foreignCount} ingested of ${videos.length} in filtered feed`,
|
||||
);
|
||||
if (typeof nostrService?.emit === "function") {
|
||||
nostrService.emit("videos:updated", {
|
||||
videos,
|
||||
@@ -208,6 +224,35 @@ export function createNip71IngestService({
|
||||
}
|
||||
}
|
||||
|
||||
// Defer the first start until the native feed has rendered (first
|
||||
// videos:updated), with a fallback timer for the empty-feed case. Keeps ingest
|
||||
// off the cold-start critical path.
|
||||
function startWhenFeedReady() {
|
||||
if (started || deferArmed || !isAvailable()) {
|
||||
return false;
|
||||
}
|
||||
deferArmed = true;
|
||||
|
||||
const begin = () => {
|
||||
if (typeof offFeedReady === "function") {
|
||||
offFeedReady();
|
||||
offFeedReady = null;
|
||||
}
|
||||
if (deferTimer) {
|
||||
clearTimeout(deferTimer);
|
||||
deferTimer = null;
|
||||
}
|
||||
deferArmed = false;
|
||||
start();
|
||||
};
|
||||
|
||||
if (typeof nostrService?.on === "function") {
|
||||
offFeedReady = nostrService.on("videos:updated", begin);
|
||||
}
|
||||
deferTimer = setTimeout(begin, feedReadyFallbackMs);
|
||||
return true;
|
||||
}
|
||||
|
||||
function start() {
|
||||
if (started || !isAvailable()) {
|
||||
return false;
|
||||
@@ -313,6 +358,19 @@ export function createNip71IngestService({
|
||||
|
||||
function stop() {
|
||||
closeSubscription();
|
||||
if (typeof offFeedReady === "function") {
|
||||
try {
|
||||
offFeedReady();
|
||||
} catch (error) {
|
||||
// best-effort
|
||||
}
|
||||
offFeedReady = null;
|
||||
}
|
||||
if (deferTimer) {
|
||||
clearTimeout(deferTimer);
|
||||
deferTimer = null;
|
||||
}
|
||||
deferArmed = false;
|
||||
if (typeof offWhitelistChange === "function") {
|
||||
try {
|
||||
offWhitelistChange();
|
||||
@@ -326,6 +384,7 @@ export function createNip71IngestService({
|
||||
|
||||
return {
|
||||
isAvailable,
|
||||
startWhenFeedReady,
|
||||
start,
|
||||
stop,
|
||||
restart,
|
||||
|
||||
@@ -858,6 +858,12 @@ export default class ApplicationBootstrap {
|
||||
// Inbound NIP-71 ingest: pull video events published by other Nostr apps
|
||||
// (scoped to whitelisted authors while whitelist mode is on) and surface
|
||||
// them in the feed. Gated by FEATURE_NIP71_INGEST. Best-effort.
|
||||
//
|
||||
// Deferred until the native feed has rendered once: injecting foreign
|
||||
// authors triggers the moderation/WoT stage to hydrate their social graph
|
||||
// (kind 30000), which during cold-start competes with the initial feed load
|
||||
// and can stall it. Starting after the first videos:updated keeps ingest off
|
||||
// the critical path; a fallback timer covers the empty-feed case.
|
||||
const nip71IngestService = createNip71IngestService({
|
||||
nostrClient,
|
||||
nostrService: app.nostrService,
|
||||
@@ -865,11 +871,7 @@ export default class ApplicationBootstrap {
|
||||
});
|
||||
app.nip71IngestService = nip71IngestService;
|
||||
if (nip71IngestService.isAvailable()) {
|
||||
try {
|
||||
nip71IngestService.start();
|
||||
} catch (error) {
|
||||
devLogger.warn("[applicationBootstrap] NIP-71 ingest start failed", error);
|
||||
}
|
||||
nip71IngestService.startWhenFeedReady();
|
||||
nostrUnsubscribes.push(() => nip71IngestService.stop());
|
||||
}
|
||||
nostrUnsubscribes.push(
|
||||
|
||||
@@ -43,10 +43,17 @@ function makeHarness({ whitelistOn = false, whitelist = [] } = {}) {
|
||||
getActiveKey: activeKey,
|
||||
getSubscriptionManager: () => manager,
|
||||
};
|
||||
const feedListeners = new Set();
|
||||
const nostrService = {
|
||||
emit(name, detail) {
|
||||
emitted.push({ name, detail });
|
||||
},
|
||||
on(name, fn) {
|
||||
if (name === "videos:updated") {
|
||||
feedListeners.add(fn);
|
||||
}
|
||||
return () => feedListeners.delete(fn);
|
||||
},
|
||||
getFilteredActiveVideos: () => Array.from(activeMap.values()),
|
||||
};
|
||||
const accessControl = {
|
||||
@@ -72,6 +79,7 @@ function makeHarness({ whitelistOn = false, whitelist = [] } = {}) {
|
||||
},
|
||||
fireEvent: (e) => onEvent?.(e),
|
||||
triggerWhitelistChange: () => whitelistListener?.(),
|
||||
triggerFeedReady: () => feedListeners.forEach((fn) => fn({})),
|
||||
nostrClient,
|
||||
nostrService,
|
||||
accessControl,
|
||||
@@ -218,6 +226,30 @@ test("re-subscribes when the whitelist changes", () => {
|
||||
assert.deepEqual(h.capturedFilters[0].authors, ["aa", "cc"], "subscription re-scoped after change");
|
||||
});
|
||||
|
||||
test("startWhenFeedReady defers subscription until the native feed renders", () => {
|
||||
const h = makeHarness({ whitelistOn: true, whitelist: ["aa"] });
|
||||
const svc = makeService(h, { feedReadyFallbackMs: 100000 });
|
||||
|
||||
assert.equal(svc.startWhenFeedReady(), true);
|
||||
assert.equal(h.capturedFilters, null, "does not subscribe during cold-start");
|
||||
|
||||
// Native feed renders -> first videos:updated -> ingest starts now.
|
||||
h.triggerFeedReady();
|
||||
assert.ok(h.capturedFilters, "subscribes once the feed is ready");
|
||||
assert.deepEqual(h.capturedFilters[0].authors, ["aa"]);
|
||||
svc.stop();
|
||||
});
|
||||
|
||||
test("startWhenFeedReady falls back to starting if the feed never emits", async () => {
|
||||
const h = makeHarness({ whitelistOn: true, whitelist: ["aa"] });
|
||||
const svc = makeService(h, { feedReadyFallbackMs: 5 });
|
||||
svc.startWhenFeedReady();
|
||||
assert.equal(h.capturedFilters, null);
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
assert.ok(h.capturedFilters, "fallback timer started ingest");
|
||||
svc.stop();
|
||||
});
|
||||
|
||||
test("coalesces refresh signals so an event burst can't storm the feed", () => {
|
||||
const h = makeHarness();
|
||||
const svc = makeService(h, { refreshThrottleMs: 10000 });
|
||||
|
||||
Reference in new issue
Block a user