a demonstration replicated social networking web app built with anproto
wiredove.net/
social
ed25519
protocols
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}