mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
[watch-history] thread chunkDecryptLimit through resolve + expose deferred count
Service-layer plumbing for bounded watch-history loading (slice B part 2). manager.resolve() now forwards chunkDecryptLimit to fetch() and records the resulting deferred-chunk count in a per-actor side-channel, so resolve() keeps returning a bare items array (its sole caller depends on that) while the "more older chunks available" signal is still retrievable. Exposed via getWatchHistoryDeferredChunkCount() on the manager and the NostrClient facade. clear() resets the side-channel. Still inert by default (no caller passes a finite limit yet); this is the substrate for activating fast-first-render. Tested: resolve under chunkDecryptLimit:1 returns only the newest chunk's items and reports deferredChunkCount=1, reset to 0 after clear(). Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
11ff205fc0
commit
362016ead6
3 files changed
+150
No files matched your search
@@ -61,6 +61,7 @@ import {
|
||||
removeWatchHistoryItem as removeWatchHistoryItemWithManager,
|
||||
fetchWatchHistory as fetchWatchHistoryWithManager,
|
||||
resolveWatchHistory as resolveWatchHistoryWithManager,
|
||||
getWatchHistoryDeferredChunkCount as getWatchHistoryDeferredChunkCountFromManager,
|
||||
} from "./watchHistory.js";
|
||||
import {
|
||||
buildVideoPostEvent,
|
||||
@@ -978,6 +979,12 @@ export class NostrClient {
|
||||
options,
|
||||
);
|
||||
}
|
||||
getWatchHistoryDeferredChunkCount(actorInput) {
|
||||
return getWatchHistoryDeferredChunkCountFromManager(
|
||||
this.watchHistory,
|
||||
actorInput,
|
||||
);
|
||||
}
|
||||
async listVideoViewEvents(pointer, options = {}) {
|
||||
return listVideoViewEventsForClient(this, pointer, options);
|
||||
}
|
||||
|
||||
@@ -962,6 +962,10 @@ class WatchHistoryManager {
|
||||
this.cacheTtlMs = 0;
|
||||
this.fingerprints = new Map();
|
||||
this.lastCreatedAt = 0;
|
||||
// Per-actor count of older chunks left undecrypted by the last bounded
|
||||
// fetch (see fetch()'s chunkDecryptLimit). Read by the service to decide
|
||||
// whether to offer "load older".
|
||||
this.deferredChunkCounts = new Map();
|
||||
|
||||
profileCache.subscribe((event, detail) => {
|
||||
if (event === "profileChanged") {
|
||||
@@ -2314,7 +2318,18 @@ class WatchHistoryManager {
|
||||
|
||||
const fetchResult = await this.fetch(resolvedActor, {
|
||||
forceRefresh: options.forceRefresh || false,
|
||||
...(options.chunkDecryptLimit !== undefined
|
||||
? { chunkDecryptLimit: options.chunkDecryptLimit }
|
||||
: {}),
|
||||
});
|
||||
// Surface how many older chunks were left undecrypted so the service can
|
||||
// decide whether to offer "load older". Stored per-actor side-channel so
|
||||
// resolve() can keep returning a plain items array (its sole caller relies
|
||||
// on that contract).
|
||||
this.setDeferredChunkCount(
|
||||
actorKey,
|
||||
Number(fetchResult?.deferredChunkCount) || 0,
|
||||
);
|
||||
const merged = mergeWatchHistoryItemsWithFallback(
|
||||
{
|
||||
version: 2,
|
||||
@@ -2386,9 +2401,27 @@ class WatchHistoryManager {
|
||||
this.cache.clear();
|
||||
this.fingerprints.clear();
|
||||
this.refreshPromises.clear();
|
||||
this.deferredChunkCounts.clear();
|
||||
this.lastCreatedAt = 0;
|
||||
this.storage = null;
|
||||
}
|
||||
|
||||
setDeferredChunkCount(actorInput, count) {
|
||||
const actorKey = normalizeActorKey(actorInput);
|
||||
if (!actorKey) {
|
||||
return;
|
||||
}
|
||||
const normalized = Number.isFinite(count) && count > 0 ? Math.floor(count) : 0;
|
||||
this.deferredChunkCounts.set(actorKey, normalized);
|
||||
}
|
||||
|
||||
getDeferredChunkCount(actorInput) {
|
||||
const actorKey = normalizeActorKey(actorInput);
|
||||
if (!actorKey) {
|
||||
return 0;
|
||||
}
|
||||
return this.deferredChunkCounts.get(actorKey) || 0;
|
||||
}
|
||||
}
|
||||
|
||||
export function getWatchHistoryCacheTtlMs(manager) {
|
||||
@@ -2448,6 +2481,10 @@ export function resolveWatchHistory(manager, actorInput, options = {}) {
|
||||
return manager.resolve(actorInput, options);
|
||||
}
|
||||
|
||||
export function getWatchHistoryDeferredChunkCount(manager, actorInput) {
|
||||
return manager.getDeferredChunkCount(actorInput);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a watch-history manager. Callers supply the nostr client hooks needed
|
||||
* to sign, encrypt, fetch, and publish events. All dependencies are optional;
|
||||
|
||||
@@ -7,7 +7,9 @@ import {
|
||||
createWatchHistoryManager,
|
||||
fetchWatchHistory,
|
||||
getWatchHistoryCacheTtlMs,
|
||||
getWatchHistoryDeferredChunkCount,
|
||||
getWatchHistoryStorage,
|
||||
resolveWatchHistory,
|
||||
} from "../../js/nostr/watchHistory.js";
|
||||
import { WATCH_HISTORY_KIND } from "../../js/config.js";
|
||||
import { NOTE_TYPES } from "../../js/nostrEventSchemas.js";
|
||||
@@ -559,6 +561,110 @@ test("fetchWatchHistory caps uncached chunk decryption to chunkDecryptLimit, new
|
||||
}
|
||||
});
|
||||
|
||||
test("resolveWatchHistory threads chunkDecryptLimit and records the deferred chunk count", async () => {
|
||||
// resolve() returns a bare items array (its sole caller depends on that), so
|
||||
// the "more older chunks available" signal is exposed via a per-actor
|
||||
// side-channel the service reads to decide whether to offer "load older".
|
||||
const actorHex = "8".repeat(64);
|
||||
const newCipher = "cmVzb2x2ZS1uZXctY2lwaGVy"; // base64, no "?iv=" => nip44
|
||||
const oldCipher = "cmVzb2x2ZS1vbGQtY2lwaGVy";
|
||||
|
||||
const pointerEvent = {
|
||||
id: "pointer-resolve-cap",
|
||||
pubkey: actorHex,
|
||||
created_at: 1_700_600_000,
|
||||
content: JSON.stringify({
|
||||
version: 2,
|
||||
snapshot: "snap-resolve",
|
||||
items: [],
|
||||
chunkIndex: 0,
|
||||
totalChunks: 2,
|
||||
}),
|
||||
tags: [
|
||||
["snapshot", "snap-resolve"],
|
||||
["a", `${WATCH_HISTORY_KIND}:${actorHex}:chunk-r-new`],
|
||||
["a", `${WATCH_HISTORY_KIND}:${actorHex}:chunk-r-old`],
|
||||
],
|
||||
};
|
||||
const newChunk = {
|
||||
id: "chunk-r-new-id",
|
||||
pubkey: actorHex,
|
||||
created_at: 1_700_600_200,
|
||||
content: newCipher,
|
||||
tags: [["d", "chunk-r-new"], ["encrypted", "nip44"]],
|
||||
};
|
||||
const oldChunk = {
|
||||
id: "chunk-r-old-id",
|
||||
pubkey: actorHex,
|
||||
created_at: 1_700_500_000,
|
||||
content: oldCipher,
|
||||
tags: [["d", "chunk-r-old"], ["encrypted", "nip44"]],
|
||||
};
|
||||
|
||||
const signer = {
|
||||
nip44Decrypt: async (_pubkey, payload) =>
|
||||
JSON.stringify({
|
||||
version: 2,
|
||||
items: [
|
||||
{
|
||||
type: "e",
|
||||
value: payload === newCipher ? "r-new" : "r-old",
|
||||
watchedAt: payload === newCipher ? 200 : 100,
|
||||
},
|
||||
],
|
||||
}),
|
||||
};
|
||||
|
||||
let listCall = 0;
|
||||
const pool = {
|
||||
async list() {
|
||||
listCall += 1;
|
||||
if (listCall === 1) {
|
||||
return [pointerEvent];
|
||||
}
|
||||
return [oldChunk, newChunk];
|
||||
},
|
||||
};
|
||||
|
||||
const manager = createWatchHistoryManager({
|
||||
getActivePubkey: () => actorHex,
|
||||
resolveActiveSigner: () => signer,
|
||||
shouldRequestExtensionPermissions: () => false,
|
||||
getPool: () => pool,
|
||||
getReadRelays: () => ["wss://relay.example"],
|
||||
});
|
||||
|
||||
try {
|
||||
assert.equal(
|
||||
getWatchHistoryDeferredChunkCount(manager, actorHex),
|
||||
0,
|
||||
"no deferred chunks before any fetch",
|
||||
);
|
||||
const items = await resolveWatchHistory(manager, actorHex, {
|
||||
forceRefresh: true,
|
||||
chunkDecryptLimit: 1,
|
||||
});
|
||||
assert.ok(Array.isArray(items), "resolve must still return a bare items array");
|
||||
assert.deepStrictEqual(
|
||||
items.map((item) => item.value),
|
||||
["r-new"],
|
||||
"only the newest chunk's items should resolve under the cap",
|
||||
);
|
||||
assert.equal(
|
||||
getWatchHistoryDeferredChunkCount(manager, actorHex),
|
||||
1,
|
||||
"the deferred side-channel should report the older chunk",
|
||||
);
|
||||
} finally {
|
||||
manager.clear();
|
||||
assert.equal(
|
||||
getWatchHistoryDeferredChunkCount(manager, actorHex),
|
||||
0,
|
||||
"clear() must reset the deferred chunk side-channel",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("publishWatchHistorySnapshot uses injected nostr-tools helpers when signer cannot encrypt", async () => {
|
||||
const actorPubkey = "f".repeat(64);
|
||||
const sessionPrivateKey = "a".repeat(64);
|
||||
|
||||
Reference in new issue
Block a user