Last active
September 5, 2024 15:05
-
-
Save braydonf/7a86c9b316e6939ae7d8dcb8b1dd601c to your computer and use it in GitHub Desktop.
Nostr Amethyst Top Feed Sketch
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'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