Skip to content

Instantly share code, notes, and snippets.

@braydonf
Last active September 5, 2024 15:05
Show Gist options
  • Save braydonf/7a86c9b316e6939ae7d8dcb8b1dd601c to your computer and use it in GitHub Desktop.
Save braydonf/7a86c9b316e6939ae7d8dcb8b1dd601c to your computer and use it in GitHub Desktop.
Nostr Amethyst Top Feed Sketch
'use strict';
const assert = require('assert');
const window_of_time_ms = 86400000 * 4; // 96 hours
class Note {
constructor(event) {
this.event = event;
this.weight = {
total: event.created_at,
considered: {
zap: new Set(),
comment: new Set(),
like: new Set()
}
}
}
addWeight(event, follows, followsTotal) {
// Only add weight for events from follows.
if (!follows[event.pubkey])
return;
// Only add weight if the event is from someone else.
if (event.pubkey == this.event.pubkey)
return;
// Only consider event types supported.
if (!this.weight.considered[event.type])
return;
// Only add weight if the pubkey has not already added
// weight for the event type.
if (this.weight.considered[event.type].has(event.pubkey))
return;
// Take the delta and make sure it's positive.
const delta = Math.max(0, event.created_at - this.event.created_at);
// Invert the delta around the maximum window of time.
let inverted = Math.max(0, window_of_time_ms - delta);
// Fit onto a curve.
let curved = inverted * (inverted / window_of_time_ms);
// Divide based on total follows.
let part = curved / followsTotal;
// Calculate the boost by the event type.
let boost = 0;
if (event.type == 'zap')
boost = Math.round(part * 0.5);
else if (event.type == 'comment')
boost = Math.round(part * 0.3);
else if (event.type == 'like')
boost = Math.round(part * 0.2);
this.weight.total += boost;
this.weight.considered[event.type].add(event.pubkey);
}
}
// This is the object that keep tracks of all the notes
// and the weight for each note.
class LocalCache {
constructor(account) {
// This keeps track of notes with additional data
// such as the calculated weight.
this.notes = {};
this.name = account.name;
this.pubkey = account.pubkey;
// This is the pubkeys that the account follows
// only zaps, likes and comments that are from these
// accounts should be considered in the weight scoring.
this.follows = account.follows;
this.followsTotal = Object.keys(this.follows).length;
}
// This adds an event to the cache, which will then
// calculate a new weight for referenced notes.
consume(event) {
switch(event.type) {
case 'zap':
case 'like':
case 'comment':
this.notes[event.ref].addWeight(event, this.follows, this.followsTotal);
break;
case 'note':
this.notes[event.hash] = new Note(event);
break;
default:
throw new Error('Unknown type.');
}
}
}
// Test constants.
const now = 1680762528677;
// For ease of test vectors.
const hour = 3600000;
const half_hour = 1800000;
const ten_min = 600000;
const five_min = 300000;
const one_min = 60000;
const half_min = 30000;
const ten_sec = 10000;
const five_sec = 5000;
const one_sec = 1000;
// Test Account.
const account = {
pubkey: "a9218d6e",
name: "Alice",
follows: {
"f09f8bce": {
name: "Bob"
},
"56f188e7": {
name: "Eve"
}
}
}
// Test Accounts.
const accounts = {
// Follows.
"f09f8bce": {
name: "Bob"
},
"56f188e7": {
name: "Eve"
},
// Not follows.
"a70c502d": {
name: "Carol"
},
"c222574e": {
name: "Chuck"
},
"d7d231fc": {
name: "Frank"
}
}
// Test Events.
const events = [
// 0. Base Notes.
// ==================================
{
hash: "07aa0c2e",
pubkey: "d7d231fc",
type: "note",
created_at: now + 97 * hour
},
{
hash: "8b0a7e26",
pubkey: "d7d231fc",
type: "note",
created_at: now + 96 * hour
},
{
hash: "05da2ea0",
pubkey: "d7d231fc",
type: "note",
created_at: now + 95 * hour
},
{
hash: "d823c519",
pubkey: "d7d231fc",
type: "note",
created_at: now + 93 * hour // 6.
},
{
hash: "58952b23",
pubkey: "d7d231fc",
type: "note",
created_at: now + 93 * hour // 6.
},
{
hash: "03412f1a",
pubkey: "d7d231fc",
type: "note",
created_at: now + 91 * hour // 1.
},
{
hash: "3dacb709",
pubkey: "d7d231fc",
type: "note",
created_at: now + 91 * hour // 1.
},
{
hash: "0f78c148",
pubkey: "d7d231fc",
type: "note",
created_at: now + 90 * hour // 2.
},
{
hash: "fb48205a",
pubkey: "d7d231fc",
type: "note",
created_at: now + 90 * hour // 2.
},
{
hash: "0f8ced1a",
pubkey: "d7d231fc",
type: "note",
created_at: now + 90 * hour // 2.
},
{
hash: "52dbe5c1",
pubkey: "d7d231fc",
type: "note",
created_at: now + 60 * hour
},
{
hash: "b2be7adb",
pubkey: "d7d231fc",
type: "note",
created_at: now + 50 * hour
},
{
hash: "06971f30",
pubkey: "d7d231fc",
type: "note",
created_at: now + 40 * hour
},
{
hash: "1c246b56",
pubkey: "d7d231fc",
type: "note",
created_at: now + 10 * hour // 4.
},
{
hash: "0789944a",
pubkey: "d7d231fc",
type: "note",
created_at: now + 10 * hour // 4.
},
{
hash: "a806f9d3",
pubkey: "d7d231fc",
type: "note",
created_at: now + 1 * hour // 3.
},
{
hash: "634846fe",
pubkey: "d7d231fc",
type: "note",
created_at: now + 1 * hour // 3.
},
{
hash: "633662f4",
pubkey: "d7d231fc",
type: "note",
created_at: now + 1 * hour // 3.
},
{
hash: "299a50b2",
pubkey: "d7d231fc",
type: "note",
created_at: now + 1 * hour // 3.
},
{
hash: "02674450",
pubkey: "d7d231fc",
type: "note",
created_at: now + 1 * hour // 3.
},
{
hash: "6b61bf87",
pubkey: "56f188e7",
type: "note",
created_at: now // 5.
},
{
hash: "a5cad1b1",
pubkey: "56f188e7",
type: "note",
created_at: now // 5.
},
// 1. More recent zaps (relative to note)
// should have greater weight.
// ===============================================
// Reference notes with same created time.
// Zap events from all follows (more recent to note)
{
hash: "17459700",
type: "zap",
ref: "03412f1a",
created_at: now + 91 * hour + half_hour,
pubkey: "f09f8bce"
},
{
hash: "057d0476",
type: "zap",
ref: "03412f1a",
created_at: now + 91 * hour + hour,
pubkey: "56f188e7"
},
// Zap events for all follows
{
hash: "433628bd",
type: "zap",
created_at: now + 91 * hour + 4 * hour,
ref: "3dacb709",
pubkey: "f09f8bce"
},
{
hash: "5a45dc40",
type: "zap",
created_at: now + 91 * hour + 2 * hour,
ref: "3dacb709",
pubkey: "56f188e7"
},
// 2. Weights should have priority:
// Zaps > Comments > Likes
// =======================================
// Reference notes with same created time.
// Commented by a follow.
{
hash: "9cdc8ac1",
type: "comment",
created_at: now + 90 * hour + hour,
ref: "0f78c148",
pubkey: "f09f8bce"
},
// Liked by a follow.
{
hash: "06feb1d9",
type: "like",
created_at: now + 90 * hour + hour,
ref: "fb48205a",
pubkey: "f09f8bce"
},
// Zapped by a follow.
{
hash: "bed5438b",
type: "zap",
created_at: now + 90 * hour + hour,
ref: "0f8ced1a",
pubkey: "f09f8bce"
},
// 3. Zaps (and Comments and Likes) should
// decay in meaning towards zero as the
// difference in time to the note approaches
// the end of the window.
// ===========================================
// Referenced notes with the same created time.
{
hash: "064a1d54",
type: "zap",
created_at: now + 97 * hour,
ref: "a806f9d3",
pubkey: "56f188e7"
},
{
hash: "45be9603",
type: "zap",
created_at: now + 33 * hour,
ref: "634846fe",
pubkey: "56f188e7"
},
{
hash: "8035aefe",
type: "zap",
created_at: now + 19 * hour,
ref: "633662f4",
pubkey: "56f188e7"
},
{
hash: "a85eeaa0",
type: "zap",
created_at: now + 7 * hour,
ref: "299a50b2",
pubkey: "56f188e7"
},
{
hash: "17639fcb",
type: "zap",
created_at: now + 2 * hour,
ref: "02674450",
pubkey: "56f188e7"
},
// 4. Zaps (and Comments and Likes) from pubkeys
// that the account does not follow should be
// ignored and not considered as part of the weight.
// ================================================
// Is not a follow (should not increase weight)
{
hash: "753cd9fe",
type: "zap",
created_at: now + 10 * hour + hour,
ref: "0789944a",
pubkey: "c222574e"
},
// Is a follow (should increase weight)
{
hash: "7dab8cc1",
type: "zap",
created_at: now + 10 * hour + hour,
ref: "1c246b56",
pubkey: "f09f8bce"
},
// 5. Zaps (and Comments and Likes) should not
// increase weight if the pubkey matches that
// of the referenced note.
// =========================================
{
hash: "48983494",
type: "zap",
created_at: now + hour,
ref: "a5cad1b1",
pubkey: "56f188e7"
},
// 6. Multiple Zaps (and Comments and Likes) should
// not increase the weight if the pubkey was already
// added to the weight.
{
hash: "fcb000da",
type: "zap",
created_at: now + 93 * hour + half_hour,
ref: "58952b23",
pubkey: "56f188e7"
},
{
hash: "bdf0ffb5",
type: "zap",
created_at: now + 93 * hour + half_hour + one_min,
ref: "58952b23",
pubkey: "56f188e7"
},
{
hash: "bdf0ffb5",
type: "zap",
created_at: now + 93 * hour + half_hour,
ref: "d823c519",
pubkey: "56f188e7"
},
]
function main() {
const cache = new LocalCache(account);
for (let [hash, event] of Object.entries(events))
cache.consume(event);
// Check notes have been consumed.
const notesLength = Object.keys(cache.notes).length;
assert(notesLength == 22);
let a, b, c, d, e = null;
// Check 1.
a = cache.notes["03412f1a"].weight.total;
b = cache.notes["3dacb709"].weight.total;
assert(a > b);
console.log('PASSED 1.');
// Check 2.
a = cache.notes["0f8ced1a"].weight.total;
b = cache.notes["0f78c148"].weight.total;
c = cache.notes["fb48205a"].weight.total;
assert(a > b);
assert(b > c);
console.log('PASSED 2.');
// Check 3.
a = cache.notes["02674450"].weight.total;
b = cache.notes["299a50b2"].weight.total;
c = cache.notes["633662f4"].weight.total;
d = cache.notes["634846fe"].weight.total;
e = cache.notes["a806f9d3"].weight.total;
assert(a > b);
assert(b > c);
assert(c > d);
assert(d > e);
console.log('PASSED 3.');
// Check 4.
a = cache.notes["0789944a"].weight.total;
b = cache.notes["0789944a"].event.created_at;
c = cache.notes["1c246b56"].weight.total;
d = cache.notes["1c246b56"].event.created_at;
assert(a == b);
assert(c != d);
console.log('PASSED 4.');
// Check 5.
a = cache.notes["a5cad1b1"].weight.total;
b = cache.notes["a5cad1b1"].event.created_at;
assert(a == b);
console.log('PASSED 5.');
// Check 6.
a = cache.notes["58952b23"].weight.total;
b = cache.notes["d823c519"].weight.total;
c = cache.notes["58952b23"].event.created_at;
d = cache.notes["d823c519"].event.created_at;
assert(a == b);
assert(c == d);
assert(a != c);
assert(b != d);
console.log('PASSED 6.');
}
if (require.main === module) {
main();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment