Gate subscriptions feed on nostr warmup

This commit is contained in:
thePR0M3TH3AN committed 2025-10-18 22:37:10 -04:00
1 parent 6e32486ed0
commit 34ef65db7b
3 files changed
+306 -30

No files matched your search

+79 -30
View File
@@ -117,6 +117,9 @@ export class NostrService {
this.videosByAuthorIndex = null;
this.authorIndexDirty = false;
this.moderationService = moderationService || null;
this.initialLoadPromise = null;
this.initialLoadResolved = false;
this.initialLoadResolve = null;
try {
if (this.moderationService && typeof this.moderationService.setNostrClient === "function") {
this.moderationService.setNostrClient(this.nostrClient);
@@ -142,6 +145,43 @@ export class NostrService {
this.emitter.emit(eventName, detail);
}
ensureInitialLoadDeferred() {
if (!this.initialLoadPromise) {
this.initialLoadResolved = false;
this.initialLoadPromise = new Promise((resolve) => {
this.initialLoadResolve = (value) => {
if (this.initialLoadResolved) {
return;
}
this.initialLoadResolved = true;
this.initialLoadResolve = null;
resolve(value);
};
});
}
return this.initialLoadPromise;
}
resolveInitialLoad(value) {
if (this.initialLoadResolved) {
return;
}
if (typeof this.initialLoadResolve === "function") {
try {
this.initialLoadResolve(value);
} finally {
this.initialLoadResolve = null;
this.initialLoadResolved = true;
}
}
}
awaitInitialLoad() {
return this.ensureInitialLoadDeferred();
}
getModerationService() {
if (!this.moderationService) {
return null;
@@ -396,42 +436,51 @@ export class NostrService {
isAuthorBlocked,
onVideos,
} = {}) {
await this.ensureAccessControlReady();
this.ensureInitialLoadDeferred();
if (forceFetch) {
this.clearVideoSubscription();
}
try {
await this.ensureAccessControlReady();
const applyAndNotify = (videos, reason) => {
const filtered = this.filterVideos(videos, {
blacklistedEventIds,
isAuthorBlocked,
});
this.cacheVideos(filtered);
if (typeof onVideos === "function") {
try {
onVideos(filtered, { reason });
} catch (error) {
userLogger.warn("[nostrService] onVideos handler threw", error);
}
if (forceFetch) {
this.clearVideoSubscription();
}
this.emit("videos:updated", { videos: filtered, reason });
return filtered;
};
const cached = this.nostrClient.getActiveVideos();
const initial = applyAndNotify(cached, "cache");
const applyAndNotify = (videos, reason) => {
const filtered = this.filterVideos(videos, {
blacklistedEventIds,
isAuthorBlocked,
});
this.cacheVideos(filtered);
if (typeof onVideos === "function") {
try {
onVideos(filtered, { reason });
} catch (error) {
userLogger.warn("[nostrService] onVideos handler threw", error);
}
}
this.emit("videos:updated", { videos: filtered, reason });
return filtered;
};
if (!getStoredVideoSubscription()) {
const subscription = this.nostrClient.subscribeVideos(() => {
const updated = this.nostrClient.getActiveVideos();
applyAndNotify(updated, "subscription");
});
this.setVideoSubscription(subscription);
this.emit("subscription:started", { subscription });
const cached = this.nostrClient.getActiveVideos();
const initial = applyAndNotify(cached, "cache");
this.resolveInitialLoad({ videos: initial, reason: "cache" });
if (!getStoredVideoSubscription()) {
const subscription = this.nostrClient.subscribeVideos(() => {
const updated = this.nostrClient.getActiveVideos();
applyAndNotify(updated, "subscription");
});
this.setVideoSubscription(subscription);
this.emit("subscription:started", { subscription });
}
return initial;
} catch (error) {
this.resolveInitialLoad({ videos: [], reason: "error", error });
throw error;
}
return initial;
}
async fetchVideos(options = {}) {
+90
View File
@@ -18,6 +18,7 @@ import { VideoListView } from "./ui/views/VideoListView.js";
import { ALLOW_NSFW_CONTENT } from "./config.js";
import { devLogger, userLogger } from "./utils/logger.js";
import moderationService from "./services/moderationService.js";
import nostrService from "./services/nostrService.js";
const getApp = () => getApplication();
@@ -36,6 +37,12 @@ class SubscriptionsManager {
this.lastRunOptions = null;
this.lastResult = null;
this.lastContainerId = null;
this.unsubscribeFromNostrUpdates = null;
this.pendingRefreshPromise = null;
this.scheduledRefreshDetail = null;
this.isRunningFeed = false;
this.hasRenderedOnce = false;
this.ensureNostrServiceListener();
}
/**
@@ -302,6 +309,7 @@ class SubscriptionsManager {
}
this.lastRunOptions = null;
this.lastResult = null;
this.hasRenderedOnce = Boolean(container);
return null;
}
@@ -316,6 +324,7 @@ class SubscriptionsManager {
limit,
containerId
};
this.hasRenderedOnce = false;
return null;
}
@@ -328,6 +337,7 @@ class SubscriptionsManager {
containerId
};
this.lastResult = { items: [], metadata: { reason: "no-subscriptions" } };
this.hasRenderedOnce = true;
return this.lastResult;
}
@@ -341,10 +351,24 @@ class SubscriptionsManager {
};
this.ensureFeedRegistered();
this.ensureNostrServiceListener();
if (typeof nostrService?.awaitInitialLoad === "function") {
try {
await nostrService.awaitInitialLoad();
} catch (error) {
devLogger.warn(
"[SubscriptionsManager] Failed to await nostrService initial load:",
error
);
}
}
const engine = this.getFeedEngine();
if (!engine || typeof engine.run !== "function") {
container.innerHTML =
"<p class='text-muted-strong'>Subscriptions are unavailable right now.</p>";
this.hasRenderedOnce = true;
return null;
}
@@ -362,6 +386,7 @@ class SubscriptionsManager {
};
try {
this.isRunningFeed = true;
const result = await engine.run("subscriptions", runOptions);
const videos = Array.isArray(result?.items)
@@ -378,6 +403,7 @@ class SubscriptionsManager {
this.lastResult = result;
this.renderSameGridStyle(result, containerId, { limit, reason });
this.hasRenderedOnce = true;
return result;
} catch (error) {
userLogger.error(
@@ -387,8 +413,72 @@ class SubscriptionsManager {
container.innerHTML =
"<p class='text-muted-strong'>Unable to load subscriptions right now.</p>";
this.lastResult = null;
this.hasRenderedOnce = Boolean(container);
return null;
} finally {
this.isRunningFeed = false;
this.processScheduledRefresh();
}
}
ensureNostrServiceListener() {
if (this.unsubscribeFromNostrUpdates || typeof nostrService?.on !== "function") {
return;
}
this.unsubscribeFromNostrUpdates = nostrService.on(
"videos:updated",
(detail) => {
this.handleNostrVideosUpdated(detail);
}
);
}
handleNostrVideosUpdated(detail) {
if (!detail || !Array.isArray(detail.videos) || !detail.videos.length) {
return;
}
this.scheduledRefreshDetail = detail;
this.processScheduledRefresh();
}
processScheduledRefresh() {
if (!this.lastRunOptions || !this.hasRenderedOnce) {
return null;
}
if (!this.scheduledRefreshDetail) {
return null;
}
if (this.isRunningFeed || this.pendingRefreshPromise) {
return null;
}
const detail = this.scheduledRefreshDetail;
this.scheduledRefreshDetail = null;
const refreshReason =
typeof detail?.reason === "string" && detail.reason
? `nostr:${detail.reason}`
: "nostr:update";
this.pendingRefreshPromise = this.refreshActiveFeed({ reason: refreshReason })
.catch((error) => {
devLogger.warn(
"[SubscriptionsManager] Failed to refresh after nostrService update:",
error
);
})
.finally(() => {
this.pendingRefreshPromise = null;
if (this.scheduledRefreshDetail) {
this.processScheduledRefresh();
}
});
return this.pendingRefreshPromise;
}
ensureFeedRegistered() {
+137
View File
@@ -1,8 +1,13 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { JSDOM } from "jsdom";
import { nostrClient } from "../js/nostr.js";
import { subscriptions } from "../js/subscriptions.js";
import { setApplication } from "../js/applicationContext.js";
import nostrService from "../js/services/nostrService.js";
import moderationService from "../js/services/moderationService.js";
test("loadSubscriptions aggregates relay results when one rejects", async () => {
const SubscriptionsManager = subscriptions.constructor;
@@ -120,3 +125,135 @@ test("loadSubscriptions aggregates relay results when one rejects", async () =>
}
});
test(
"showSubscriptionVideos waits for nostrService warm-up and refreshes after updates",
async () => {
const SubscriptionsManager = subscriptions.constructor;
const originalWindow = globalThis.window;
const originalDocument = globalThis.document;
const originalNavigator = globalThis.navigator;
const dom = new JSDOM(
"<!doctype html><div id=\"subscriptionsVideoList\"></div>",
{ url: "https://example.test/" }
);
globalThis.window = dom.window;
globalThis.document = dom.window.document;
globalThis.navigator = dom.window.navigator;
const manager = new SubscriptionsManager();
const originalAwaitInitialLoad = nostrService.awaitInitialLoad;
const originalAwaitUserBlockRefresh = moderationService.awaitUserBlockRefresh;
let resolveInitialLoad;
const initialLoadPromise = new Promise((resolve) => {
resolveInitialLoad = resolve;
});
nostrService.awaitInitialLoad = () => initialLoadPromise;
moderationService.awaitUserBlockRefresh = async () => {};
const renderCalls = [];
manager.renderSameGridStyle = (result, containerId, options) => {
renderCalls.push({ result, containerId, options });
};
manager.loadSubscriptions = async () => {
manager.loaded = true;
manager.subscribedPubkeys = new Set(["pub-1"]);
};
const feedResults = [
{ items: [] },
{ items: [{ video: { id: "video-1", pubkey: "pub-1" } }] },
];
const runCalls = [];
const feedEngine = {
async run(name, options) {
runCalls.push({ name, options });
return feedResults.shift() ?? { items: [] };
},
};
setApplication({
feedEngine,
videosMap: new Map(),
registerSubscriptionsFeed: () => {},
isAuthorBlocked: () => false,
blacklistedEventIds: new Set(),
});
const showPromise = manager.showSubscriptionVideos(
"viewer-pub",
"subscriptionsVideoList"
);
await Promise.resolve();
assert.equal(
runCalls.length,
0,
"engine.run should not execute before nostrService initial load resolves",
);
resolveInitialLoad();
await showPromise;
assert.equal(runCalls.length, 1, "engine.run should execute after warm-up");
assert.equal(renderCalls.length, 1, "initial render should occur once");
nostrService.emit("videos:updated", {
reason: "subscription",
videos: [{ id: "video-1", pubkey: "pub-1" }],
});
if (manager.pendingRefreshPromise) {
await manager.pendingRefreshPromise;
} else {
await Promise.resolve();
}
assert.equal(
runCalls.length,
2,
"engine.run should execute again after nostrService update",
);
assert.equal(renderCalls.length, 2, "render should run again after update");
assert.equal(
renderCalls[1].result.items[0].video.id,
"video-1",
"updated render should receive fresh videos",
);
if (typeof manager.unsubscribeFromNostrUpdates === "function") {
manager.unsubscribeFromNostrUpdates();
}
nostrService.awaitInitialLoad = originalAwaitInitialLoad;
moderationService.awaitUserBlockRefresh = originalAwaitUserBlockRefresh;
setApplication(null);
dom.window.close();
if (typeof originalWindow === "undefined") {
delete globalThis.window;
} else {
globalThis.window = originalWindow;
}
if (typeof originalDocument === "undefined") {
delete globalThis.document;
} else {
globalThis.document = originalDocument;
}
if (typeof originalNavigator === "undefined") {
delete globalThis.navigator;
} else {
globalThis.navigator = originalNavigator;
}
},
);