mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Harden profile data loading
This commit is contained in:
1 parent
5f439d6863
commit
b774de6d76
7 files changed
+237
-59
No files matched your search
+31
-49
@@ -180,6 +180,7 @@ class SubscriptionsManager {
|
|||||||
this.subscribedPubkeys = new Set();
|
this.subscribedPubkeys = new Set();
|
||||||
this.subsEventId = null;
|
this.subsEventId = null;
|
||||||
this.subsEventCreatedAt = null;
|
this.subsEventCreatedAt = null;
|
||||||
|
this.subsEventPubkey = null;
|
||||||
// Last live-subscription event id we triggered a refresh for (loop guard).
|
// Last live-subscription event id we triggered a refresh for (loop guard).
|
||||||
this._lastHandledListEventId = null;
|
this._lastHandledListEventId = null;
|
||||||
// Most recently fetched list ciphertext, reused by decrypt retries so they
|
// Most recently fetched list ciphertext, reused by decrypt retries so they
|
||||||
@@ -247,6 +248,7 @@ class SubscriptionsManager {
|
|||||||
this.subscribedPubkeys = new Set(cachedSnapshot.subscribedPubkeys);
|
this.subscribedPubkeys = new Set(cachedSnapshot.subscribedPubkeys);
|
||||||
this.subsEventId = cachedSnapshot.eventId;
|
this.subsEventId = cachedSnapshot.eventId;
|
||||||
this.subsEventCreatedAt = cachedSnapshot.createdAt;
|
this.subsEventCreatedAt = cachedSnapshot.createdAt;
|
||||||
|
this.subsEventPubkey = cachedSnapshot.eventPubkey;
|
||||||
this.currentUserPubkey = normalizedUserPubkey;
|
this.currentUserPubkey = normalizedUserPubkey;
|
||||||
this.uiReady = true;
|
this.uiReady = true;
|
||||||
this.dataReady = true;
|
this.dataReady = true;
|
||||||
@@ -281,6 +283,7 @@ class SubscriptionsManager {
|
|||||||
subscribedPubkeys: Array.from(this.subscribedPubkeys),
|
subscribedPubkeys: Array.from(this.subscribedPubkeys),
|
||||||
eventId: this.subsEventId,
|
eventId: this.subsEventId,
|
||||||
createdAt: this.subsEventCreatedAt,
|
createdAt: this.subsEventCreatedAt,
|
||||||
|
eventPubkey: this.subsEventPubkey,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -389,63 +392,39 @@ class SubscriptionsManager {
|
|||||||
profileCache.getProfileData(normalizedUserPubkey, "subscriptions"),
|
profileCache.getProfileData(normalizedUserPubkey, "subscriptions"),
|
||||||
);
|
);
|
||||||
hadCachedSnapshot = cachedSnapshot.hasSnapshot;
|
hadCachedSnapshot = cachedSnapshot.hasSnapshot;
|
||||||
const shouldForceFullFetch = !cachedSnapshot.hasSnapshot;
|
// Legacy snapshots did not record the source author. Fetch their full
|
||||||
|
// authoritative state once so a cache created by an older buggy build
|
||||||
|
// cannot keep suppressing the real list with an invalid timestamp.
|
||||||
|
const shouldForceFullFetch =
|
||||||
|
!cachedSnapshot.hasSnapshot ||
|
||||||
|
cachedSnapshot.eventPubkey !== normalizedUserPubkey;
|
||||||
const incrementalSince = shouldForceFullFetch
|
const incrementalSince = shouldForceFullFetch
|
||||||
? 0
|
? 0
|
||||||
: computeIncrementalSinceWithOverlap(cachedSnapshot.createdAt, 1);
|
: computeIncrementalSinceWithOverlap(cachedSnapshot.createdAt, 1);
|
||||||
|
|
||||||
// Fetch user and session actor subscription lists in parallel to reduce
|
// Subscription lists are private, account-scoped state. In particular,
|
||||||
// total relay wait time during login.
|
// a BitLogin session actor can differ from the active profile pubkey.
|
||||||
const fetchPromises = [
|
// Never mix its events into this read: choosing the newest event across
|
||||||
nostrClient.fetchListIncrementally({
|
// both accounts could overwrite the active account's subscriptions.
|
||||||
kind: SUBSCRIPTION_SET_KIND,
|
const events = await nostrClient.fetchListIncrementally({
|
||||||
pubkey: normalizedUserPubkey,
|
kind: SUBSCRIPTION_SET_KIND,
|
||||||
dTag: SUBSCRIPTION_LIST_IDENTIFIER,
|
pubkey: normalizedUserPubkey,
|
||||||
relayUrls,
|
dTag: SUBSCRIPTION_LIST_IDENTIFIER,
|
||||||
since: incrementalSince,
|
relayUrls,
|
||||||
timeoutMs: 12000,
|
since: incrementalSince,
|
||||||
}),
|
timeoutMs: 12000,
|
||||||
];
|
});
|
||||||
|
|
||||||
const normalizedSessionActorPubkey = normalizeNostrPubkey(
|
|
||||||
nostrClient?.sessionActor?.pubkey,
|
|
||||||
);
|
|
||||||
const shouldFetchSessionActor =
|
|
||||||
normalizedSessionActorPubkey &&
|
|
||||||
normalizedSessionActorPubkey !== normalizedUserPubkey;
|
|
||||||
if (shouldFetchSessionActor) {
|
|
||||||
const sessionCachedSnapshot = parseCachedSubscriptionSnapshot(
|
|
||||||
profileCache.getProfileData(
|
|
||||||
normalizedSessionActorPubkey,
|
|
||||||
"subscriptions",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
const shouldForceSessionFetch = !sessionCachedSnapshot.hasSnapshot;
|
|
||||||
const sessionSince = shouldForceSessionFetch
|
|
||||||
? 0
|
|
||||||
: computeIncrementalSinceWithOverlap(sessionCachedSnapshot.createdAt, 1);
|
|
||||||
fetchPromises.push(
|
|
||||||
nostrClient.fetchListIncrementally({
|
|
||||||
kind: SUBSCRIPTION_SET_KIND,
|
|
||||||
pubkey: normalizedSessionActorPubkey,
|
|
||||||
dTag: SUBSCRIPTION_LIST_IDENTIFIER,
|
|
||||||
relayUrls,
|
|
||||||
since: sessionSince,
|
|
||||||
timeoutMs: 12000,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchResults = await Promise.all(fetchPromises);
|
|
||||||
let events = fetchResults[0] || [];
|
|
||||||
if (shouldFetchSessionActor && fetchResults[1]?.length) {
|
|
||||||
events = events.concat(fetchResults[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (events.length) {
|
if (events.length) {
|
||||||
const deduped = new Map();
|
const deduped = new Map();
|
||||||
for (const event of events) {
|
for (const event of events) {
|
||||||
if (!event || typeof event !== "object" || !event.id) {
|
if (
|
||||||
|
!event ||
|
||||||
|
typeof event !== "object" ||
|
||||||
|
!event.id ||
|
||||||
|
(event.pubkey &&
|
||||||
|
normalizeHexPubkey(event.pubkey) !== normalizedUserPubkey)
|
||||||
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const existing = deduped.get(event.id);
|
const existing = deduped.get(event.id);
|
||||||
@@ -471,6 +450,7 @@ class SubscriptionsManager {
|
|||||||
this.subscribedPubkeys.clear();
|
this.subscribedPubkeys.clear();
|
||||||
this.subsEventId = null;
|
this.subsEventId = null;
|
||||||
this.subsEventCreatedAt = null;
|
this.subsEventCreatedAt = null;
|
||||||
|
this.subsEventPubkey = null;
|
||||||
this.uiReady = true;
|
this.uiReady = true;
|
||||||
this.dataReady = true;
|
this.dataReady = true;
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
@@ -513,6 +493,7 @@ class SubscriptionsManager {
|
|||||||
this.subsEventCreatedAt = Number.isFinite(newest.created_at)
|
this.subsEventCreatedAt = Number.isFinite(newest.created_at)
|
||||||
? newest.created_at
|
? newest.created_at
|
||||||
: null;
|
: null;
|
||||||
|
this.subsEventPubkey = normalizedUserPubkey;
|
||||||
|
|
||||||
let decryptResult;
|
let decryptResult;
|
||||||
try {
|
try {
|
||||||
@@ -687,6 +668,7 @@ class SubscriptionsManager {
|
|||||||
this.subscribedPubkeys.clear();
|
this.subscribedPubkeys.clear();
|
||||||
this.subsEventId = null;
|
this.subsEventId = null;
|
||||||
this.subsEventCreatedAt = null;
|
this.subsEventCreatedAt = null;
|
||||||
|
this.subsEventPubkey = null;
|
||||||
this._lastHandledListEventId = null;
|
this._lastHandledListEventId = null;
|
||||||
this._pendingDecryptEvent = null;
|
this._pendingDecryptEvent = null;
|
||||||
this.currentUserPubkey = null;
|
this.currentUserPubkey = null;
|
||||||
|
|||||||
@@ -196,6 +196,7 @@ export function parseCachedSubscriptionSnapshot(cached) {
|
|||||||
subscribedPubkeys: normalized,
|
subscribedPubkeys: normalized,
|
||||||
eventId: null,
|
eventId: null,
|
||||||
createdAt: null,
|
createdAt: null,
|
||||||
|
eventPubkey: null,
|
||||||
hasSnapshot: true,
|
hasSnapshot: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -220,6 +221,7 @@ export function parseCachedSubscriptionSnapshot(cached) {
|
|||||||
const createdAt = Number.isFinite(createdAtCandidate)
|
const createdAt = Number.isFinite(createdAtCandidate)
|
||||||
? Math.floor(createdAtCandidate)
|
? Math.floor(createdAtCandidate)
|
||||||
: null;
|
: null;
|
||||||
|
const eventPubkey = normalizeHexPubkey(cached.eventPubkey);
|
||||||
const hasSnapshot =
|
const hasSnapshot =
|
||||||
Array.isArray(cached.subscribedPubkeys) ||
|
Array.isArray(cached.subscribedPubkeys) ||
|
||||||
Array.isArray(cached.subPubkeys) ||
|
Array.isArray(cached.subPubkeys) ||
|
||||||
@@ -229,6 +231,7 @@ export function parseCachedSubscriptionSnapshot(cached) {
|
|||||||
subscribedPubkeys: normalized,
|
subscribedPubkeys: normalized,
|
||||||
eventId,
|
eventId,
|
||||||
createdAt,
|
createdAt,
|
||||||
|
eventPubkey,
|
||||||
hasSnapshot,
|
hasSnapshot,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -237,6 +240,7 @@ export function parseCachedSubscriptionSnapshot(cached) {
|
|||||||
subscribedPubkeys: [],
|
subscribedPubkeys: [],
|
||||||
eventId: null,
|
eventId: null,
|
||||||
createdAt: null,
|
createdAt: null,
|
||||||
|
eventPubkey: null,
|
||||||
hasSnapshot: false,
|
hasSnapshot: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -753,7 +753,11 @@ export class ProfileDirectMessageActions {
|
|||||||
handleDirectMessagesUpdated(detail = {}) {
|
handleDirectMessagesUpdated(detail = {}) {
|
||||||
if (
|
if (
|
||||||
this.controller.activeMessagesRequest &&
|
this.controller.activeMessagesRequest &&
|
||||||
detail?.reason !== "load-incremental"
|
detail?.reason !== "load-incremental" &&
|
||||||
|
// A persisted, locally encrypted snapshot is safe to show while the
|
||||||
|
// slower relay history/decryption pass is still running. Suppressing it
|
||||||
|
// made the DM pane appear blank until that full pass completed.
|
||||||
|
detail?.reason !== "snapshot"
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,6 +79,8 @@ export class ProfilePlaylistsController {
|
|||||||
this.playlists = [];
|
this.playlists = [];
|
||||||
this.expanded = new Set();
|
this.expanded = new Set();
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
|
this.refreshRequestId = 0;
|
||||||
|
this.fetchCreatorPlaylists = fetchCreatorPlaylists;
|
||||||
this._drag = null;
|
this._drag = null;
|
||||||
this._dropTarget = null;
|
this._dropTarget = null;
|
||||||
this._onChanged = () => {
|
this._onChanged = () => {
|
||||||
@@ -139,16 +141,22 @@ export class ProfilePlaylistsController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const requestId = ++this.refreshRequestId;
|
||||||
this.setLoading(true);
|
this.setLoading(true);
|
||||||
let playlists = [];
|
let playlists = [];
|
||||||
try {
|
try {
|
||||||
playlists = await fetchCreatorPlaylists(this.pubkey, {
|
playlists = await this.fetchCreatorPlaylists(this.pubkey, {
|
||||||
includeEmpty: true,
|
includeEmpty: true,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
devLogger.warn("[playlists] Failed to load playlists for pane:", error);
|
devLogger.warn("[playlists] Failed to load playlists for pane:", error);
|
||||||
}
|
}
|
||||||
this.playlists = playlists;
|
this.playlists = playlists;
|
||||||
|
this.setLoading(false);
|
||||||
|
// Show the playlist structure as soon as the list itself is available.
|
||||||
|
// Resolving every referenced video only improves labels/thumbnails and
|
||||||
|
// must not keep the management pane blank on a cold cache.
|
||||||
|
this.render();
|
||||||
|
|
||||||
// Fetch the referenced creators' videos so the expanded sublists can show
|
// Fetch the referenced creators' videos so the expanded sublists can show
|
||||||
// real titles/thumbnails (cache may be cold in the modal).
|
// real titles/thumbnails (cache may be cold in the modal).
|
||||||
@@ -165,15 +173,16 @@ export class ProfilePlaylistsController {
|
|||||||
}
|
}
|
||||||
const nostrService = this.mainController.services?.nostrService;
|
const nostrService = this.mainController.services?.nostrService;
|
||||||
if (authors.size && typeof nostrService?.fetchVideosByAuthors === "function") {
|
if (authors.size && typeof nostrService?.fetchVideosByAuthors === "function") {
|
||||||
try {
|
void nostrService.fetchVideosByAuthors([...authors])
|
||||||
await nostrService.fetchVideosByAuthors([...authors]);
|
.catch(() => {
|
||||||
} catch (error) {
|
// Best effort — keep the coordinate labels if metadata is unavailable.
|
||||||
// best effort — fall back to coordinate labels
|
})
|
||||||
}
|
.then(() => {
|
||||||
|
if (this.refreshRequestId === requestId) {
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setLoading(false);
|
|
||||||
this.render();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// coord -> { title, thumbnail } from the app's active videos.
|
// coord -> { title, thumbnail } from the app's active videos.
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import test from "node:test";
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
|
||||||
|
import { ProfileDirectMessageActions } from "../js/ui/profileModal/ProfileDirectMessageActions.js";
|
||||||
|
|
||||||
|
test("DM pane renders a persisted snapshot while the relay load is still active", () => {
|
||||||
|
const scheduled = [];
|
||||||
|
const actor = "a".repeat(64);
|
||||||
|
const controller = {
|
||||||
|
activeMessagesRequest: Symbol("relay-load"),
|
||||||
|
directMessagesCache: [],
|
||||||
|
directMessagesLastActor: null,
|
||||||
|
helper: { resolveActiveDmActor: () => actor },
|
||||||
|
renderer: {
|
||||||
|
scheduleDirectMessagesRender: (detail) => scheduled.push(detail),
|
||||||
|
setMessagesLoadingState: () => {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const actions = new ProfileDirectMessageActions({}, controller);
|
||||||
|
const messages = [{ event: { id: "cached-message" } }];
|
||||||
|
|
||||||
|
actions.handleDirectMessagesUpdated({ messages, reason: "snapshot" });
|
||||||
|
|
||||||
|
assert.deepEqual(controller.directMessagesCache, messages);
|
||||||
|
assert.equal(controller.directMessagesLastActor, actor);
|
||||||
|
assert.deepEqual(scheduled, [{ messages, actorPubkey: actor, reason: "snapshot" }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("DM pane waits for relay increments but ignores unrelated updates during a load", () => {
|
||||||
|
const controller = {
|
||||||
|
activeMessagesRequest: Symbol("relay-load"),
|
||||||
|
directMessagesCache: [],
|
||||||
|
directMessagesLastActor: null,
|
||||||
|
helper: { resolveActiveDmActor: () => "a".repeat(64) },
|
||||||
|
renderer: {
|
||||||
|
scheduleDirectMessagesRender: () => assert.fail("unrelated update must not render"),
|
||||||
|
setMessagesLoadingState: () => {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const actions = new ProfileDirectMessageActions({}, controller);
|
||||||
|
|
||||||
|
actions.handleDirectMessagesUpdated({ messages: [{ event: { id: "other" } }], reason: "load" });
|
||||||
|
|
||||||
|
assert.deepEqual(controller.directMessagesCache, []);
|
||||||
|
});
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import test from "node:test";
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
|
||||||
|
import { ProfilePlaylistsController } from "../js/ui/profileModal/ProfilePlaylistsController.js";
|
||||||
|
|
||||||
|
test("playlist refresh renders the list before deferred video metadata completes", async () => {
|
||||||
|
const controller = new ProfilePlaylistsController({
|
||||||
|
getActivePubkey: () => "a".repeat(64),
|
||||||
|
services: {
|
||||||
|
nostrService: {
|
||||||
|
fetchVideosByAuthors: () => new Promise(() => {}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const renders = [];
|
||||||
|
let resolveMetadata;
|
||||||
|
controller.listEl = {};
|
||||||
|
controller.setLoading = () => {};
|
||||||
|
controller.render = () => renders.push(controller.playlists.map((playlist) => playlist.id));
|
||||||
|
controller.fetchCreatorPlaylists = async () => [{
|
||||||
|
id: "existing",
|
||||||
|
items: [{ type: "a", value: `30078:${"b".repeat(64)}:video` }],
|
||||||
|
}];
|
||||||
|
controller.mainController.services.nostrService.fetchVideosByAuthors = () =>
|
||||||
|
new Promise((resolve) => {
|
||||||
|
resolveMetadata = resolve;
|
||||||
|
});
|
||||||
|
|
||||||
|
await controller.refresh();
|
||||||
|
assert.deepEqual(renders, [["existing"]], "list renders before metadata request settles");
|
||||||
|
|
||||||
|
resolveMetadata();
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
assert.deepEqual(renders, [["existing"], ["existing"]], "metadata completion refreshes labels later");
|
||||||
|
});
|
||||||
@@ -1324,6 +1324,7 @@ test("loadSubscriptions uses overlap window and deterministic latest-event tie-b
|
|||||||
subscribedPubkeys: [oldAuthor],
|
subscribedPubkeys: [oldAuthor],
|
||||||
createdAt: 1000,
|
createdAt: 1000,
|
||||||
eventId: "cached-event",
|
eventId: "cached-event",
|
||||||
|
eventPubkey: pubkey,
|
||||||
});
|
});
|
||||||
|
|
||||||
nostrClient.fetchListIncrementally = async (params = {}) => {
|
nostrClient.fetchListIncrementally = async (params = {}) => {
|
||||||
@@ -1378,6 +1379,104 @@ test("loadSubscriptions uses overlap window and deterministic latest-event tie-b
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("loadSubscriptions never applies a different BitLogin session actor's list", async () => {
|
||||||
|
const SubscriptionsManager = subscriptions.constructor;
|
||||||
|
const manager = new SubscriptionsManager();
|
||||||
|
const activePubkey = "a".repeat(64);
|
||||||
|
const sessionPubkey = "b".repeat(64);
|
||||||
|
const activeAuthor = "c".repeat(64);
|
||||||
|
const sessionAuthor = "d".repeat(64);
|
||||||
|
const originalFetchIncremental = nostrClient.fetchListIncrementally;
|
||||||
|
const originalSessionActor = nostrClient.sessionActor;
|
||||||
|
const originalPool = nostrClient.pool;
|
||||||
|
const originalDecryptSubscriptionEvent = manager.decryptSubscriptionEvent;
|
||||||
|
|
||||||
|
nostrClient.sessionActor = { pubkey: sessionPubkey };
|
||||||
|
nostrClient.pool = { list: async () => [] };
|
||||||
|
nostrClient.fetchListIncrementally = async ({ pubkey }) => {
|
||||||
|
assert.equal(pubkey, activePubkey, "only the active account may be fetched");
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "active-list",
|
||||||
|
pubkey: activePubkey,
|
||||||
|
created_at: 100,
|
||||||
|
content: "active-ciphertext",
|
||||||
|
tags: [["encrypted", "nip04"]],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "foreign-list",
|
||||||
|
pubkey: sessionPubkey,
|
||||||
|
created_at: 200,
|
||||||
|
content: "foreign-ciphertext",
|
||||||
|
tags: [["encrypted", "nip04"]],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
|
manager.decryptSubscriptionEvent = async (event) => ({
|
||||||
|
ok: true,
|
||||||
|
plaintext: JSON.stringify([["p", event.id === "active-list" ? activeAuthor : sessionAuthor]]),
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await manager.updateFromRelays(activePubkey, { allowPermissionPrompt: false });
|
||||||
|
assert.equal(manager.subsEventId, "active-list");
|
||||||
|
assert.deepEqual(manager.getSubscribedAuthors(), [activeAuthor]);
|
||||||
|
} finally {
|
||||||
|
nostrClient.fetchListIncrementally = originalFetchIncremental;
|
||||||
|
nostrClient.sessionActor = originalSessionActor;
|
||||||
|
nostrClient.pool = originalPool;
|
||||||
|
manager.decryptSubscriptionEvent = originalDecryptSubscriptionEvent;
|
||||||
|
localStorage.clear();
|
||||||
|
profileCache.memoryCache.clear();
|
||||||
|
profileCache.activePubkey = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("loadSubscriptions repairs a legacy cached list with a full active-account fetch", async () => {
|
||||||
|
const SubscriptionsManager = subscriptions.constructor;
|
||||||
|
const manager = new SubscriptionsManager();
|
||||||
|
const activePubkey = "e".repeat(64);
|
||||||
|
const staleAuthor = "f".repeat(64);
|
||||||
|
const correctAuthor = "1".repeat(64);
|
||||||
|
const originalFetchIncremental = nostrClient.fetchListIncrementally;
|
||||||
|
const originalPool = nostrClient.pool;
|
||||||
|
const originalDecryptSubscriptionEvent = manager.decryptSubscriptionEvent;
|
||||||
|
|
||||||
|
profileCache.setProfileData(activePubkey, "subscriptions", {
|
||||||
|
subscribedPubkeys: [staleAuthor],
|
||||||
|
eventId: "legacy-foreign-event",
|
||||||
|
createdAt: 9999,
|
||||||
|
});
|
||||||
|
nostrClient.pool = { list: async () => [] };
|
||||||
|
nostrClient.fetchListIncrementally = async (params) => {
|
||||||
|
assert.equal(params.since, 0, "legacy source-less cache must not suppress a full fetch");
|
||||||
|
return [{
|
||||||
|
id: "correct-active-event",
|
||||||
|
pubkey: activePubkey,
|
||||||
|
created_at: 100,
|
||||||
|
content: "correct-ciphertext",
|
||||||
|
tags: [["encrypted", "nip04"]],
|
||||||
|
}];
|
||||||
|
};
|
||||||
|
manager.decryptSubscriptionEvent = async () => ({
|
||||||
|
ok: true,
|
||||||
|
plaintext: JSON.stringify([["p", correctAuthor]]),
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await manager.updateFromRelays(activePubkey, { allowPermissionPrompt: false });
|
||||||
|
assert.deepEqual(manager.getSubscribedAuthors(), [correctAuthor]);
|
||||||
|
assert.equal(manager.subsEventPubkey, activePubkey);
|
||||||
|
} finally {
|
||||||
|
nostrClient.fetchListIncrementally = originalFetchIncremental;
|
||||||
|
nostrClient.pool = originalPool;
|
||||||
|
manager.decryptSubscriptionEvent = originalDecryptSubscriptionEvent;
|
||||||
|
localStorage.clear();
|
||||||
|
profileCache.memoryCache.clear();
|
||||||
|
profileCache.activePubkey = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test("handleSubscriptionListEvent does not re-trigger on the live-sub echo (loop guard)", () => {
|
test("handleSubscriptionListEvent does not re-trigger on the live-sub echo (loop guard)", () => {
|
||||||
const SubscriptionsManager = subscriptions.constructor;
|
const SubscriptionsManager = subscriptions.constructor;
|
||||||
const manager = new SubscriptionsManager();
|
const manager = new SubscriptionsManager();
|
||||||
|
|||||||
Reference in new issue
Block a user