Files
bitvid/dist/js/feedEngine/sorters.js
T
google-labs-jules[bot]andPR0M3TH3AN 5e1709b900 perf: Optimize docs scrollspy using IntersectionObserver
Replaces the O(N) getBoundingClientRect polling loop in the docs scroll spy with an IntersectionObserver. This eliminates potential layout thrashing and reduces main thread CPU usage during scrolling.

- Replaces `window.addEventListener("scroll")` with `IntersectionObserver`.
- Maintains active section state using `passedHeadings` set.
- Updates `setupScrollSpy` and `clearScrollSpy` to manage the observer.
- Preserves existing behavior where the active section is the last one that has passed the top offset (96px). Note: Activation now occurs when the header *fully* exits the top offset area, which is a minor behavior change (delayed by header height) but improves performance and stability.

Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-05 21:04:36 +00:00

469 lines
13 KiB
JavaScript

// js/feedEngine/sorters.js
import { normalizeHashtag } from "../utils/hashtagNormalization.js";
export function createChronologicalSorter({
direction = "desc",
postedAtResolver,
} = {}) {
const normalizedDirection = direction === "asc" ? "asc" : "desc";
return function chronologicalSorter(items = [], context = {}) {
if (!Array.isArray(items)) {
return [];
}
const copy = [...items];
const getTimestamp = (entry) => {
if (!entry || typeof entry !== "object") {
return Number.NEGATIVE_INFINITY;
}
const video = entry.video;
if (!video || typeof video !== "object") {
return Number.NEGATIVE_INFINITY;
}
const resolvePostedAtHook = () => {
if (typeof postedAtResolver === "function") {
try {
const resolved = postedAtResolver({ entry, context, video });
if (Number.isFinite(resolved)) {
return Math.floor(resolved);
}
} catch (error) {
context?.log?.("[chronological-sorter] postedAtResolver threw", error);
}
}
const hook = context?.hooks?.timestamps;
if (!hook || typeof hook !== "object") {
return null;
}
const candidates = [
hook.getKnownVideoPostedAt,
hook.getKnownPostedAt,
hook.getVideoPostedAt,
hook.resolveVideoPostedAt,
];
for (const candidate of candidates) {
if (typeof candidate !== "function") {
continue;
}
try {
const value = candidate(video, { entry, context });
if (Number.isFinite(value)) {
return Math.floor(value);
}
} catch (error) {
context?.log?.(
"[chronological-sorter] timestamps hook threw",
error,
);
}
}
return null;
};
const hookTimestamp = resolvePostedAtHook();
if (Number.isFinite(hookTimestamp)) {
return hookTimestamp;
}
const rootCreatedAt = Number(video.rootCreatedAt);
if (Number.isFinite(rootCreatedAt)) {
return Math.floor(rootCreatedAt);
}
const nip71CreatedAt = Number(video?.nip71Source?.created_at);
if (Number.isFinite(nip71CreatedAt)) {
return Math.floor(nip71CreatedAt);
}
const createdAt = Number(video.created_at);
if (Number.isFinite(createdAt)) {
return Math.floor(createdAt);
}
return Number.NEGATIVE_INFINITY;
};
copy.sort((a, b) => {
const aMuted =
a?.metadata?.moderation?.trustedMuted === true ||
a?.video?.moderation?.trustedMuted === true;
const bMuted =
b?.metadata?.moderation?.trustedMuted === true ||
b?.video?.moderation?.trustedMuted === true;
if (aMuted !== bMuted) {
return aMuted ? 1 : -1;
}
const aTs = getTimestamp(a);
const bTs = getTimestamp(b);
const normalizedATs = Number.isFinite(aTs) ? aTs : Number.NEGATIVE_INFINITY;
const normalizedBTs = Number.isFinite(bTs) ? bTs : Number.NEGATIVE_INFINITY;
if (normalizedATs !== normalizedBTs) {
const diff = normalizedATs - normalizedBTs;
return normalizedDirection === "asc" ? diff : -diff;
}
const aId = typeof a?.video?.id === "string" ? a.video.id : "";
const bId = typeof b?.video?.id === "string" ? b.video.id : "";
return normalizedDirection === "asc"
? aId.localeCompare(bId)
: bId.localeCompare(aId);
});
return copy;
};
}
function buildTagVector(item) {
const vector = new Map();
const video = item?.video;
if (!video || typeof video !== "object") {
return vector;
}
const addTag = (rawTag) => {
if (typeof rawTag !== "string") {
return;
}
const tag = normalizeHashtag(rawTag);
if (!tag) {
return;
}
vector.set(tag, (vector.get(tag) || 0) + 1);
};
if (Array.isArray(video.tags)) {
for (const tag of video.tags) {
if (Array.isArray(tag) && tag[0] === "t") {
addTag(tag[1]);
}
}
}
if (Array.isArray(video.nip71?.hashtags)) {
for (const tag of video.nip71.hashtags) {
addTag(tag);
}
}
return vector;
}
function cosineSimilarity(vectorA, vectorB) {
if (!vectorA || !vectorB || vectorA.size === 0 || vectorB.size === 0) {
return 0;
}
let dot = 0;
let normA = 0;
let normB = 0;
for (const value of vectorA.values()) {
normA += value * value;
}
for (const value of vectorB.values()) {
normB += value * value;
}
if (!Number.isFinite(normA) || !Number.isFinite(normB) || normA <= 0 || normB <= 0) {
return 0;
}
const smaller = vectorA.size <= vectorB.size ? vectorA : vectorB;
const larger = smaller === vectorA ? vectorB : vectorA;
for (const [tag, value] of smaller.entries()) {
const other = larger.get(tag);
if (Number.isFinite(other)) {
dot += value * other;
}
}
const similarity = dot / (Math.sqrt(normA) * Math.sqrt(normB));
if (!Number.isFinite(similarity)) {
return 0;
}
if (similarity > 1) {
return 1;
}
if (similarity < -1) {
return -1;
}
return similarity;
}
function resolveItemTimestamp(item) {
const rootCreatedAt = Number(item?.video?.rootCreatedAt);
if (Number.isFinite(rootCreatedAt)) {
return Math.floor(rootCreatedAt);
}
const createdAt = Number(item?.video?.created_at);
if (Number.isFinite(createdAt)) {
return Math.floor(createdAt);
}
return Number.NEGATIVE_INFINITY;
}
function stableVideoId(item) {
return typeof item?.video?.id === "string" ? item.video.id : "";
}
function compareByTimestampId(a, b) {
const aTs = resolveItemTimestamp(a);
const bTs = resolveItemTimestamp(b);
if (aTs !== bTs) {
return bTs - aTs;
}
return stableVideoId(a).localeCompare(stableVideoId(b));
}
export function createExploreDiversitySorter({
lambda = 0.7,
stageName = "explore-diversity-sorter",
} = {}) {
const normalizedLambda = Number.isFinite(lambda)
? Math.min(1, Math.max(0, lambda))
: 0.7;
return function exploreDiversitySorter(items = [], context = {}) {
if (!Array.isArray(items)) {
return [];
}
const candidates = [];
const rest = [];
for (const item of items) {
const score = item?.metadata?.exploreScore;
if (Number.isFinite(score)) {
candidates.push(item);
} else {
rest.push(item);
}
}
if (candidates.length === 0) {
return [...rest].sort(compareByTimestampId);
}
let maxScore = Number.NEGATIVE_INFINITY;
let minScore = Number.POSITIVE_INFINITY;
for (const item of candidates) {
const score = Number(item?.metadata?.exploreScore);
if (!Number.isFinite(score)) {
continue;
}
if (score > maxScore) {
maxScore = score;
}
if (score < minScore) {
minScore = score;
}
}
const scoreSpan = maxScore - minScore;
const normalizeScore = (score) => {
const numeric = Number(score);
if (!Number.isFinite(numeric)) {
return 0;
}
if (!Number.isFinite(scoreSpan) || scoreSpan <= 0) {
return 1;
}
const normalized = (numeric - minScore) / scoreSpan;
if (normalized <= 0) {
return 0;
}
if (normalized >= 1) {
return 1;
}
return normalized;
};
const vectors = new Map();
for (const item of candidates) {
vectors.set(item, buildTagVector(item));
}
const selected = [];
const remaining = [...candidates];
// Cache max similarity for each remaining candidate against the selected set
// Map<CandidateItem, { penalty: number, similarItemId: string }>
const similarityCache = new Map();
for (const candidate of remaining) {
similarityCache.set(candidate, { penalty: 0, similarItemId: "" });
}
while (remaining.length > 0) {
let bestCandidate = null;
let bestMmr = Number.NEGATIVE_INFINITY;
let bestRaw = Number.NEGATIVE_INFINITY;
let bestSimilarity = 0;
let bestSimilarItemId = "";
let bestRawCandidate = null;
let bestRawScore = Number.NEGATIVE_INFINITY;
for (const candidate of remaining) {
const rawScore = normalizeScore(candidate?.metadata?.exploreScore);
if (rawScore > bestRawScore) {
bestRawScore = rawScore;
bestRawCandidate = candidate;
} else if (rawScore === bestRawScore && bestRawCandidate) {
const tie = compareByTimestampId(candidate, bestRawCandidate);
if (tie < 0) {
bestRawCandidate = candidate;
}
}
}
for (const candidate of remaining) {
const rawScore = normalizeScore(candidate?.metadata?.exploreScore);
const { penalty, similarItemId } = similarityCache.get(candidate);
const mmrScore =
normalizedLambda * rawScore - (1 - normalizedLambda) * penalty;
if (mmrScore > bestMmr) {
bestCandidate = candidate;
bestMmr = mmrScore;
bestRaw = rawScore;
bestSimilarity = penalty;
bestSimilarItemId = similarItemId;
continue;
}
if (mmrScore === bestMmr) {
if (rawScore !== bestRaw) {
if (rawScore > bestRaw) {
bestCandidate = candidate;
bestRaw = rawScore;
bestSimilarity = penalty;
bestSimilarItemId = similarItemId;
}
continue;
}
if (compareByTimestampId(candidate, bestCandidate) < 0) {
bestCandidate = candidate;
bestRaw = rawScore;
bestSimilarity = penalty;
bestSimilarItemId = similarItemId;
}
}
}
if (!bestCandidate) {
break;
}
selected.push(bestCandidate);
if (
bestRawCandidate &&
bestRawCandidate !== bestCandidate &&
bestSimilarity > 0
) {
const selectedId = stableVideoId(bestCandidate);
const competitorId = stableVideoId(bestRawCandidate);
if (selectedId && competitorId && selectedId !== competitorId) {
context?.addWhy?.({
stage: stageName,
type: "diversity",
reason: "explore-diversity",
selectedId,
competitorIds: [competitorId],
similarity: bestSimilarity,
similarItemId: bestSimilarItemId || null,
});
}
}
const index = remaining.indexOf(bestCandidate);
if (index >= 0) {
remaining.splice(index, 1);
similarityCache.delete(bestCandidate);
}
// Update similarity cache for remaining candidates against the newly selected item
const bestCandidateVector = vectors.get(bestCandidate);
for (const candidate of remaining) {
const candidateVector = vectors.get(candidate);
const similarity = cosineSimilarity(
candidateVector,
bestCandidateVector,
);
const cache = similarityCache.get(candidate);
if (similarity > cache.penalty) {
cache.penalty = similarity;
cache.similarItemId = stableVideoId(bestCandidate);
}
}
}
const orderedRest = [...rest].sort(compareByTimestampId);
return selected.concat(orderedRest);
};
}
export function createKidsScoreSorter({
stageName = "kids-score-sorter",
} = {}) {
return function kidsScoreSorter(items = [], context = {}) {
if (!Array.isArray(items)) {
return [];
}
const copy = [...items];
copy.sort((a, b) => {
const aScore = Number(a?.metadata?.kidsScore);
const bScore = Number(b?.metadata?.kidsScore);
const aHasScore = Number.isFinite(aScore);
const bHasScore = Number.isFinite(bScore);
const normalizedAScore = aHasScore ? aScore : Number.NEGATIVE_INFINITY;
const normalizedBScore = bHasScore ? bScore : Number.NEGATIVE_INFINITY;
if (normalizedAScore !== normalizedBScore) {
return normalizedBScore - normalizedAScore;
}
const aRootCreatedAt = Number(a?.video?.rootCreatedAt);
const bRootCreatedAt = Number(b?.video?.rootCreatedAt);
const normalizedARoot =
Number.isFinite(aRootCreatedAt) ? aRootCreatedAt : Number.NEGATIVE_INFINITY;
const normalizedBRoot =
Number.isFinite(bRootCreatedAt) ? bRootCreatedAt : Number.NEGATIVE_INFINITY;
if (normalizedARoot !== normalizedBRoot) {
return normalizedBRoot - normalizedARoot;
}
return stableVideoId(a).localeCompare(stableVideoId(b));
});
if (context?.addWhy && typeof context.addWhy === "function") {
context.addWhy({
stage: stageName,
type: "sort",
reason: "kids-score",
});
}
return copy;
};
}