a demonstration replicated social networking web app built with anproto wiredove.net/
social ed25519 protocols
at master 164 lines 4.4 kB view raw
1import { apds } from 'apds' 2import { isBlockedAuthor } from './moderation.js' 3 4const activity = new Map() 5 6const TICK_MS = 15 * 1000 7const REFRESH_MS = 5 * 60 * 1000 8const BOOTSTRAP_MAX = 20 9 10const HOT_INTEREST_MS = 15 * 60 * 1000 11const HOT_SEEN_MS = 24 * 60 * 60 * 1000 12const WARM_INTEREST_MS = 24 * 60 * 60 * 1000 13const WARM_SEEN_MS = 30 * 24 * 60 * 60 * 1000 14 15const MIN_REQUEST_MS = { 16 hot: 15 * 1000, 17 warm: 20 * 60 * 1000, 18 cold: 6 * 60 * 60 * 1000 19} 20 21const BATCH = { 22 hot: 4, 23 warm: 2, 24 cold: 1 25} 26 27let pubkeys = [] 28let tiered = { hot: [], warm: [], cold: [] } 29let tierIndex = { hot: 0, warm: 0, cold: 0 } 30let syncTimer = null 31let lastRefresh = 0 32let needsRebuild = true 33let tickRunning = false 34 35const nowMs = () => Date.now() 36 37const getEntry = (pubkey) => { 38 const existing = activity.get(pubkey) 39 if (existing) { return existing } 40 const entry = { lastSeen: 0, lastInterest: 0, lastRequested: 0 } 41 activity.set(pubkey, entry) 42 return entry 43} 44 45const parseOpenedTimestamp = (opened) => { 46 if (!opened || opened.length < 13) { return 0 } 47 const ts = Number.parseInt(opened.substring(0, 13), 10) 48 return Number.isNaN(ts) ? 0 : ts 49} 50 51const classify = (entry, now) => { 52 const seenAge = entry.lastSeen ? now - entry.lastSeen : Infinity 53 const interestAge = entry.lastInterest ? now - entry.lastInterest : Infinity 54 if (interestAge <= HOT_INTEREST_MS || seenAge <= HOT_SEEN_MS) { return 'hot' } 55 if (interestAge <= WARM_INTEREST_MS || seenAge <= WARM_SEEN_MS) { return 'warm' } 56 return 'cold' 57} 58 59const rebuildTiers = () => { 60 const next = { hot: [], warm: [], cold: [] } 61 const now = nowMs() 62 pubkeys.forEach(pubkey => { 63 const entry = getEntry(pubkey) 64 next[classify(entry, now)].push(pubkey) 65 }) 66 tiered = next 67 tierIndex = { hot: 0, warm: 0, cold: 0 } 68 needsRebuild = false 69} 70 71const pickCandidates = (tier, count, now) => { 72 const list = tiered[tier] 73 if (!list.length) { return [] } 74 const picked = [] 75 let attempts = 0 76 while (picked.length < count && attempts < list.length) { 77 const idx = tierIndex[tier] % list.length 78 tierIndex[tier] = (tierIndex[tier] + 1) % list.length 79 const pubkey = list[idx] 80 const entry = getEntry(pubkey) 81 if (now - entry.lastRequested >= MIN_REQUEST_MS[tier]) { 82 picked.push(pubkey) 83 } 84 attempts += 1 85 } 86 return picked 87} 88 89const bootstrapActivity = async () => { 90 let count = 0 91 for (const pubkey of pubkeys) { 92 if (count >= BOOTSTRAP_MAX) { break } 93 const entry = getEntry(pubkey) 94 if (entry.lastSeen) { continue } 95 const latest = await apds.getLatest(pubkey) 96 const opened = latest?.opened 97 const ts = parseOpenedTimestamp(opened) 98 if (ts) { entry.lastSeen = ts } 99 count += 1 100 } 101} 102 103const refreshPubkeys = async () => { 104 try { 105 const next = await apds.getPubkeys() 106 pubkeys = Array.isArray(next) ? next : [] 107 } catch (err) { 108 console.warn('getPubkeys failed', err) 109 pubkeys = [] 110 } 111 const filtered = [] 112 for (const pubkey of pubkeys) { 113 if (!pubkey || pubkey.length !== 44) { continue } 114 if (await isBlockedAuthor(pubkey)) { continue } 115 filtered.push(pubkey) 116 } 117 pubkeys = filtered 118 lastRefresh = nowMs() 119 needsRebuild = true 120 await bootstrapActivity() 121} 122 123export const noteSeen = async (pubkey) => { 124 if (!pubkey || pubkey.length !== 44) { return } 125 if (await isBlockedAuthor(pubkey)) { return } 126 const entry = getEntry(pubkey) 127 entry.lastSeen = nowMs() 128 needsRebuild = true 129} 130 131export const noteInterest = async (pubkey) => { 132 if (!pubkey || pubkey.length !== 44) { return } 133 if (await isBlockedAuthor(pubkey)) { return } 134 const entry = getEntry(pubkey) 135 entry.lastInterest = nowMs() 136 needsRebuild = true 137} 138 139export const startSync = async (sendFn) => { 140 if (syncTimer) { return } 141 await refreshPubkeys() 142 syncTimer = setInterval(async () => { 143 if (tickRunning) { return } 144 tickRunning = true 145 try { 146 const now = nowMs() 147 if (now - lastRefresh > REFRESH_MS) { 148 await refreshPubkeys() 149 } 150 if (needsRebuild) { rebuildTiers() } 151 const hot = pickCandidates('hot', BATCH.hot, now) 152 const warm = pickCandidates('warm', BATCH.warm, now) 153 const cold = pickCandidates('cold', BATCH.cold, now) 154 const batch = [...hot, ...warm, ...cold] 155 batch.forEach(pubkey => { 156 const entry = getEntry(pubkey) 157 entry.lastRequested = now 158 sendFn(pubkey) 159 }) 160 } finally { 161 tickRunning = false 162 } 163 }, TICK_MS) 164}