feed: less eager rendering
ci/woodpecker/pr/woodpecker Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline was successful Details

before this change every new incoming text note called a render,
that filters, sorts and iterates all known text notes and creates
missing dom elements and appends into the right place.

this change throttles and debounces (both!) the render function,
that less checks have to be performed, especially on page load
when potentially 100s of events arrive within a short time.

it is important to throttle and debounce, else either the last call
is missed or no render is called while events are being received.

this change surfaced an error in recommend server that depended on
all known text notes already being rendered and inside the dom.
this function should probably be handled by render feed itself.
pull/67/head
OFF0 2 years ago
parent dffcbc6b2b
commit f4f951469f
Signed by: offbyn
GPG Key ID: 94A2F643C51F37FA

@ -1,5 +1,6 @@
import {relayPool, generatePrivateKey, getPublicKey, signEvent} from 'nostr-tools'; import {relayPool, generatePrivateKey, getPublicKey, signEvent} from 'nostr-tools';
import {zeroLeadingBitsCount} from './cryptoutils'; import {bounce} from './utils.js';
import {zeroLeadingBitsCount} from './cryptoutils.js';
import {elem, parseTextContent} from './domutil.js'; import {elem, parseTextContent} from './domutil.js';
import {dateTime, formatTime} from './timeutil.js'; import {dateTime, formatTime} from './timeutil.js';
// curl -H 'accept: application/nostr+json' https://relay.nostr.ch/ // curl -H 'accept: application/nostr+json' https://relay.nostr.ch/
@ -262,6 +263,33 @@ const hasEventTag = tag => tag[0] === 'e';
const isReply = ([tag, , , marker]) => tag === 'e' && marker !== 'mention'; const isReply = ([tag, , , marker]) => tag === 'e' && marker !== 'mention';
const isMention = ([tag, , , marker]) => tag === 'e' && marker === 'mention'; const isMention = ([tag, , , marker]) => tag === 'e' && marker === 'mention';
const renderFeed = bounce(() => {
const now = Math.floor(Date.now() * 0.001);
const sortedFeeds = textNoteList
// dont render notes from the future
.filter(note => note.created_at < now)
// if difficulty filter is configured dont render notes with too little pow
.filter(note => {
return !fitlerDifficulty || note.tags.some(([tag, , commitment]) => {
return tag === 'nonce' && commitment >= fitlerDifficulty && zeroLeadingBitsCount(note.id) >= fitlerDifficulty;
});
})
.sort(sortByCreatedAt).reverse();
sortedFeeds.forEach((evt, i) => {
if (feedDomMap[evt.id]) {
// TODO check eventRelayMap if event was published to different relays
return;
}
const article = createTextNote(evt, eventRelayMap[evt.id]);
if (i === 0) {
feedContainer.append(article);
} else {
feedDomMap[sortedFeeds[i - 1].id].before(article);
}
feedDomMap[evt.id] = article;
});
}, 17); // (16.666 rounded, a bit arbitrary but that it doesnt update more than 60x per s)
function handleTextNote(evt, relay) { function handleTextNote(evt, relay) {
if (eventRelayMap[evt.id]) { if (eventRelayMap[evt.id]) {
eventRelayMap[evt.id] = [relay, ...(eventRelayMap[evt.id])]; eventRelayMap[evt.id] = [relay, ...(eventRelayMap[evt.id])];
@ -345,33 +373,6 @@ const sortByCreatedAt = (evt1, evt2) => {
return evt1.created_at > evt2.created_at ? -1 : 1; return evt1.created_at > evt2.created_at ? -1 : 1;
}; };
function renderFeed() {
const now = Math.floor(Date.now() * 0.001);
const sortedFeeds = textNoteList
// dont render notes from the future
.filter(note => note.created_at < now)
// if difficulty filter is configured dont render notes with too little pow
.filter(note => {
return !fitlerDifficulty || note.tags.some(([tag, , commitment]) => {
return tag === 'nonce' && commitment >= fitlerDifficulty && zeroLeadingBitsCount(note.id) >= fitlerDifficulty;
});
})
.sort(sortByCreatedAt).reverse();
sortedFeeds.forEach((evt, i) => {
if (feedDomMap[evt.id]) {
// TODO check eventRelayMap if event was published to different relays
return;
}
const article = createTextNote(evt, eventRelayMap[evt.id]);
if (i === 0) {
feedContainer.append(article);
} else {
feedDomMap[sortedFeeds[i - 1].id].before(article);
}
feedDomMap[evt.id] = article;
});
}
function rerenderFeed() { function rerenderFeed() {
Object.keys(feedDomMap).forEach(key => delete feedDomMap[key]); Object.keys(feedDomMap).forEach(key => delete feedDomMap[key]);
Object.keys(replyDomMap).forEach(key => delete replyDomMap[key]); Object.keys(replyDomMap).forEach(key => delete replyDomMap[key]);
@ -548,7 +549,7 @@ function handleRecommendServer(evt, relay) {
const closestTextNotes = textNoteList const closestTextNotes = textNoteList
.filter(note => !fitlerDifficulty || note.tags.some(([tag, , commitment]) => tag === 'nonce' && commitment >= fitlerDifficulty)) .filter(note => !fitlerDifficulty || note.tags.some(([tag, , commitment]) => tag === 'nonce' && commitment >= fitlerDifficulty))
.sort(sortEventCreatedAt(evt.created_at)); .sort(sortEventCreatedAt(evt.created_at));
feedDomMap[closestTextNotes[0].id].after(art); feedDomMap[closestTextNotes[0].id]?.after(art); // TODO: note might not be in the dom yet, recommendedServers could be controlled by renderFeed
} }
feedDomMap[evt.id] = art; feedDomMap[evt.id] = art;
} }

@ -0,0 +1,22 @@
/**
* throttle and debounce given function in regular time interval,
* but with the difference that the last call will be debounced and therefore never missed.
* @param {*} function to throttle and debounce
* @param {*} time desired interval to execute function
* @returns callback
*/
export const bounce = (fn, time) => {
let throttle;
let debounce;
return (/*...args*/) => {
if (throttle) {
clearTimeout(debounce);
debounce = setTimeout(() => fn(/*...args*/), time);
return;
}
fn(/*...args*/);
throttle = setTimeout(() => {
throttle = false;
}, time);
};
};
Loading…
Cancel
Save