tangled
alpha
login
or
join now
evbogue.com
/
wiredove
1
fork
atom
a demonstration replicated social networking web app built with anproto
wiredove.net/
social
ed25519
protocols
1
fork
atom
overview
issues
pulls
pipelines
Improve initial sync feed handling
Everett Bogue
2 months ago
185bbb6f
9a95d98a
+315
-95
6 changed files
expand all
collapse all
unified
split
adder.js
composer.js
connect.js
network_queue.js
send.js
settings.js
+240
-67
adder.js
···
1
1
import { render } from './render.js'
2
2
import { apds } from 'apds'
3
3
4
4
+
const getController = () => {
5
5
+
if (!window.__feedController) {
6
6
+
window.__feedController = {
7
7
+
feeds: new Map(),
8
8
+
getFeed(src) {
9
9
+
const state = this.feeds.get(src)
10
10
+
if (state && state.container && !document.body.contains(state.container)) {
11
11
+
this.feeds.delete(src)
12
12
+
return null
13
13
+
}
14
14
+
return state || null
15
15
+
}
16
16
+
}
17
17
+
}
18
18
+
return window.__feedController
19
19
+
}
20
20
+
21
21
+
const normalizeTimestamp = (ts) => {
22
22
+
const value = Number.parseInt(ts, 10)
23
23
+
return Number.isNaN(value) ? 0 : value
24
24
+
}
25
25
+
4
26
const addPosts = async (posts, div) => {
5
27
for (const post of posts) {
6
28
const ts = post.ts || (post.opened ? Number.parseInt(post.opened.substring(0, 13), 10) : 0)
···
27
49
return 0
28
50
}
29
51
30
30
-
const isAscending = (log) => {
31
31
-
if (!log || log.length < 2) { return false }
32
32
-
let left = 0
33
33
-
let right = 0
52
52
+
const sortDesc = (a, b) => b.ts - a.ts
53
53
+
54
54
+
const buildEntries = (log) => {
55
55
+
if (!log) { return [] }
56
56
+
const entries = []
57
57
+
const seen = new Set()
34
58
for (const post of log) {
35
35
-
left = getTimestamp(post)
36
36
-
if (left) { break }
59
59
+
if (!post || !post.hash) { continue }
60
60
+
if (seen.has(post.hash)) { continue }
61
61
+
seen.add(post.hash)
62
62
+
const ts = getTimestamp(post)
63
63
+
entries.push({ hash: post.hash, ts })
64
64
+
}
65
65
+
entries.sort(sortDesc)
66
66
+
return entries
67
67
+
}
68
68
+
69
69
+
const insertEntry = (state, entry) => {
70
70
+
if (!entry || !entry.hash || !entry.ts) { return -1 }
71
71
+
if (state.seen.has(entry.hash)) { return -1 }
72
72
+
const list = state.entries
73
73
+
const prevLen = list.length
74
74
+
let lo = 0
75
75
+
let hi = list.length
76
76
+
while (lo < hi) {
77
77
+
const mid = Math.floor((lo + hi) / 2)
78
78
+
if (list[mid].ts >= entry.ts) {
79
79
+
lo = mid + 1
80
80
+
} else {
81
81
+
hi = mid
82
82
+
}
37
83
}
38
38
-
for (let i = log.length - 1; i >= 0; i--) {
39
39
-
right = getTimestamp(log[i])
40
40
-
if (right) { break }
84
84
+
list.splice(lo, 0, entry)
85
85
+
state.seen.add(entry.hash)
86
86
+
if (lo <= state.cursor) {
87
87
+
if (state.cursor === prevLen && lo === prevLen) { return lo }
88
88
+
state.cursor += 1
89
89
+
}
90
90
+
return lo
91
91
+
}
92
92
+
93
93
+
const isAtTop = () => {
94
94
+
const scrollEl = document.scrollingElement || document.documentElement || document.body
95
95
+
const scrollTop = scrollEl.scrollTop || window.scrollY || 0
96
96
+
return scrollTop <= 10
97
97
+
}
98
98
+
99
99
+
const ensureBanner = (state) => {
100
100
+
if (state.banner && state.banner.parentNode === state.container) { return state.banner }
101
101
+
const banner = document.createElement('div')
102
102
+
banner.className = 'new-posts-banner'
103
103
+
banner.style.display = 'none'
104
104
+
const button = document.createElement('button')
105
105
+
button.type = 'button'
106
106
+
button.className = 'new-posts-button'
107
107
+
button.addEventListener('click', async () => {
108
108
+
await flushPending(state)
109
109
+
})
110
110
+
banner.appendChild(button)
111
111
+
state.container.insertBefore(banner, state.container.firstChild)
112
112
+
state.banner = banner
113
113
+
state.bannerButton = button
114
114
+
return banner
115
115
+
}
116
116
+
117
117
+
const updateBanner = (state) => {
118
118
+
if (!state.banner || !state.bannerButton) { return }
119
119
+
const count = state.pending.length
120
120
+
if (!count) {
121
121
+
state.banner.style.display = 'none'
122
122
+
return
123
123
+
}
124
124
+
state.bannerButton.textContent = `Show ${count} new post${count === 1 ? '' : 's'}`
125
125
+
state.banner.style.display = 'block'
126
126
+
}
127
127
+
128
128
+
const renderEntry = async (state, entry) => {
129
129
+
const div = render.insertByTimestamp(state.container, entry.hash, entry.ts)
130
130
+
if (!div) { return }
131
131
+
if (entry.blob) {
132
132
+
await render.blob(entry.blob)
133
133
+
} else {
134
134
+
const sig = await apds.get(entry.hash)
135
135
+
if (sig) { await render.blob(sig) }
136
136
+
}
137
137
+
state.rendered.add(entry.hash)
138
138
+
}
139
139
+
140
140
+
const flushPending = async (state) => {
141
141
+
if (!state.pending.length) { return }
142
142
+
const pending = state.pending.slice().sort(sortDesc)
143
143
+
state.pending = []
144
144
+
updateBanner(state)
145
145
+
for (const entry of pending) {
146
146
+
await renderEntry(state, entry)
147
147
+
state.latestVisibleTs = Math.max(state.latestVisibleTs || 0, entry.ts)
148
148
+
if (!state.oldestVisibleTs) { state.oldestVisibleTs = entry.ts }
149
149
+
}
150
150
+
}
151
151
+
152
152
+
const enqueuePost = async (state, entry) => {
153
153
+
if (!entry || !entry.hash || !entry.ts) { return }
154
154
+
insertEntry(state, entry)
155
155
+
if (!state.latestVisibleTs) {
156
156
+
await renderEntry(state, entry)
157
157
+
state.latestVisibleTs = entry.ts
158
158
+
state.oldestVisibleTs = entry.ts
159
159
+
return
160
160
+
}
161
161
+
if (entry.ts < state.oldestVisibleTs && state.rendered.size < state.pageSize) {
162
162
+
await renderEntry(state, entry)
163
163
+
state.oldestVisibleTs = entry.ts
164
164
+
return
165
165
+
}
166
166
+
const inWindow = state.oldestVisibleTs && entry.ts >= state.oldestVisibleTs && entry.ts <= state.latestVisibleTs
167
167
+
if (entry.ts > state.latestVisibleTs) {
168
168
+
if (isAtTop()) {
169
169
+
await renderEntry(state, entry)
170
170
+
state.latestVisibleTs = entry.ts
171
171
+
if (!state.oldestVisibleTs) { state.oldestVisibleTs = entry.ts }
172
172
+
} else {
173
173
+
state.pending.push(entry)
174
174
+
updateBanner(state)
175
175
+
}
176
176
+
return
177
177
+
}
178
178
+
if (inWindow) {
179
179
+
await renderEntry(state, entry)
180
180
+
state.latestVisibleTs = Math.max(state.latestVisibleTs, entry.ts)
181
181
+
state.oldestVisibleTs = Math.min(state.oldestVisibleTs || entry.ts, entry.ts)
41
182
}
42
42
-
return left && right ? left < right : false
183
183
+
}
184
184
+
185
185
+
window.__feedEnqueue = async (src, entry) => {
186
186
+
const controller = getController()
187
187
+
const state = controller.getFeed(src)
188
188
+
if (!state) { return false }
189
189
+
await enqueuePost(state, entry)
190
190
+
return true
43
191
}
44
192
45
193
export const adder = (log, src, div) => {
46
46
-
if (log && log[0]) {
47
47
-
let index = 0
48
48
-
const ascending = isAscending(log)
49
49
-
let loading = false
50
50
-
let armed = false
51
51
-
const sentinelId = 'scroll-sentinel'
194
194
+
if (!div) { return }
195
195
+
const pageSize = 25
196
196
+
const entries = buildEntries(log || [])
197
197
+
let loading = false
198
198
+
let armed = false
199
199
+
const sentinelId = 'scroll-sentinel'
200
200
+
201
201
+
let posts = []
202
202
+
const state = {
203
203
+
src,
204
204
+
container: div,
205
205
+
entries,
206
206
+
cursor: 0,
207
207
+
seen: new Set(entries.map(entry => entry.hash)),
208
208
+
rendered: new Set(),
209
209
+
pending: [],
210
210
+
pageSize,
211
211
+
latestVisibleTs: 0,
212
212
+
oldestVisibleTs: 0,
213
213
+
banner: null,
214
214
+
bannerButton: null
215
215
+
}
216
216
+
getController().feeds.set(src, state)
217
217
+
ensureBanner(state)
52
218
53
53
-
let posts = []
54
54
-
const takeSlice = () => {
55
55
-
if (ascending) {
56
56
-
const end = log.length - index
57
57
-
const start = Math.max(0, end - 25)
58
58
-
posts = log.slice(start, end).reverse()
59
59
-
} else {
60
60
-
posts = log.slice(index, index + 25)
219
219
+
const takeSlice = () => {
220
220
+
posts = []
221
221
+
if (state.cursor >= entries.length) { return posts }
222
222
+
let idx = state.cursor
223
223
+
while (idx < entries.length && posts.length < pageSize) {
224
224
+
const entry = entries[idx]
225
225
+
if (!state.rendered.has(entry.hash)) {
226
226
+
posts.push(entry)
61
227
}
62
62
-
index = index + 25
63
63
-
return posts
228
228
+
idx += 1
64
229
}
230
230
+
state.cursor = idx
231
231
+
return posts
232
232
+
}
65
233
66
66
-
const ensureSentinel = () => {
67
67
-
let sentinel = document.getElementById(sentinelId)
68
68
-
if (!sentinel) {
69
69
-
sentinel = document.createElement('div')
70
70
-
sentinel.id = sentinelId
71
71
-
sentinel.style.height = '1px'
234
234
+
const ensureSentinel = () => {
235
235
+
let sentinel = document.getElementById(sentinelId)
236
236
+
if (!sentinel) {
237
237
+
sentinel = document.createElement('div')
238
238
+
sentinel.id = sentinelId
239
239
+
sentinel.style.height = '1px'
240
240
+
}
241
241
+
if (sentinel.parentNode && sentinel.parentNode !== div) {
242
242
+
sentinel.parentNode.removeChild(sentinel)
243
243
+
}
244
244
+
div.appendChild(sentinel)
245
245
+
return sentinel
246
246
+
}
247
247
+
248
248
+
const loadNext = async () => {
249
249
+
if (loading) { return }
250
250
+
if (window.location.hash.substring(1) !== src) { return }
251
251
+
loading = true
252
252
+
try {
253
253
+
const next = takeSlice()
254
254
+
if (!next.length) { return false }
255
255
+
await addPosts(next, div)
256
256
+
for (const entry of next) {
257
257
+
state.rendered.add(entry.hash)
72
258
}
73
73
-
if (sentinel.parentNode && sentinel.parentNode !== div) {
74
74
-
sentinel.parentNode.removeChild(sentinel)
259
259
+
if (!state.latestVisibleTs && next[0]) {
260
260
+
state.latestVisibleTs = normalizeTimestamp(next[0].ts)
75
261
}
76
76
-
div.appendChild(sentinel)
77
77
-
return sentinel
78
78
-
}
79
79
-
80
80
-
const loadNext = async () => {
81
81
-
if (loading) { return }
82
82
-
if (window.location.hash.substring(1) !== src) { return }
83
83
-
loading = true
84
84
-
try {
85
85
-
const next = takeSlice()
86
86
-
if (!next.length) { return false }
87
87
-
await addPosts(next, div)
88
88
-
ensureSentinel()
89
89
-
return true
90
90
-
} finally {
91
91
-
loading = false
262
262
+
if (next[next.length - 1]) {
263
263
+
state.oldestVisibleTs = normalizeTimestamp(next[next.length - 1].ts)
92
264
}
265
265
+
ensureSentinel()
266
266
+
return true
267
267
+
} finally {
268
268
+
loading = false
93
269
}
94
94
-
95
95
-
void loadNext()
96
96
-
const armScroll = () => {
97
97
-
armed = true
98
98
-
}
99
99
-
window.addEventListener('scroll', armScroll, { passive: true, once: true })
100
100
-
const sentinel = ensureSentinel()
101
101
-
const observer = new IntersectionObserver(async (entries) => {
102
102
-
const entry = entries[0]
103
103
-
if (!entry || !entry.isIntersecting) { return }
104
104
-
if (!armed) { return }
105
105
-
const hasMore = await loadNext()
106
106
-
if (hasMore === false) {
107
107
-
observer.disconnect()
108
108
-
}
109
109
-
}, { root: null, rootMargin: '0px 0px', threshold: 0 })
270
270
+
}
110
271
111
111
-
observer.observe(sentinel)
272
272
+
void loadNext()
273
273
+
const armScroll = () => {
274
274
+
armed = true
112
275
}
276
276
+
window.addEventListener('scroll', armScroll, { passive: true, once: true })
277
277
+
const sentinel = ensureSentinel()
278
278
+
const observer = new IntersectionObserver(async (entries) => {
279
279
+
const entry = entries[0]
280
280
+
if (!entry || !entry.isIntersecting) { return }
281
281
+
if (!armed) { return }
282
282
+
await loadNext()
283
283
+
}, { root: null, rootMargin: '0px 0px', threshold: 0 })
284
284
+
285
285
+
observer.observe(sentinel)
113
286
}
+14
-3
composer.js
···
120
120
const scroller = document.getElementById('scroller')
121
121
const opened = await apds.open(signed)
122
122
const ts = opened ? opened.substring(0, 13) : Date.now().toString()
123
123
-
const placeholder = render.insertByTimestamp(scroller, hash, ts)
124
124
-
if (placeholder) {
125
125
-
await render.blob(signed)
123
123
+
if (window.__feedEnqueue) {
124
124
+
const src = window.location.hash.substring(1)
125
125
+
const queued = await window.__feedEnqueue(src, { hash, ts: Number.parseInt(ts, 10), blob: signed })
126
126
+
if (!queued) {
127
127
+
const placeholder = render.insertByTimestamp(scroller, hash, ts)
128
128
+
if (placeholder) {
129
129
+
await render.blob(signed)
130
130
+
}
131
131
+
}
132
132
+
} else {
133
133
+
const placeholder = render.insertByTimestamp(scroller, hash, ts)
134
134
+
if (placeholder) {
135
135
+
await render.blob(signed)
136
136
+
}
126
137
}
127
138
overlay.remove()
128
139
}
+2
connect.js
···
1
1
import { apds } from 'apds'
2
2
import { makeRoom } from './gossip.js'
3
3
import { makeWs} from './websocket.js'
4
4
+
import { send } from './send.js'
4
5
5
6
await apds.start('wiredovedbversion1')
6
7
···
9
10
//await makeWs('wss://apds.anproto.com/')
10
11
makeWs('wss://pub.wiredove.net/')
11
12
makeRoom('wiredovev1')
13
13
+
send('evSFOKnXaF9ZWSsff8bVfXP6+XnGZUj8XNp6bca590k=')
12
14
}
+53
-19
network_queue.js
···
1
1
const SEND_DELAY_MS = 100
2
2
+
const HASH_RETRY_MS = 800
2
3
const queue = []
3
4
const pending = new Map()
4
5
let drainTimer = null
5
6
let draining = false
7
7
+
let nextHashTarget = 'ws'
6
8
7
9
const senders = {
8
10
ws: null,
···
16
18
return null
17
19
}
18
20
19
19
-
const normalizeTargets = (targets) => {
20
20
-
if (!targets || targets === 'both') { return { ws: true, gossip: true } }
21
21
-
if (targets === 'ws') { return { ws: true, gossip: false } }
22
22
-
if (targets === 'gossip') { return { ws: false, gossip: true } }
23
23
-
return { ws: true, gossip: true }
24
24
-
}
21
21
+
const isHash = (msg) => typeof msg === 'string' && msg.length === 44
22
22
+
23
23
+
const flipTarget = (target) => (target === 'ws' ? 'gossip' : 'ws')
25
24
26
25
export const registerNetworkSenders = (config = {}) => {
27
26
if (config.sendWs) { senders.ws = config.sendWs }
···
46
45
queue.splice(index, 1)
47
46
}
48
47
48
48
+
const pickHashTarget = (item) => {
49
49
+
const wsReady = isTargetReady('ws')
50
50
+
const gossipReady = isTargetReady('gossip')
51
51
+
if (!item.sent.ws && !item.sent.gossip) {
52
52
+
const preferred = nextHashTarget
53
53
+
const preferredReady = preferred === 'ws' ? wsReady : gossipReady
54
54
+
if (preferredReady) { return preferred }
55
55
+
const fallback = flipTarget(preferred)
56
56
+
const fallbackReady = fallback === 'ws' ? wsReady : gossipReady
57
57
+
if (fallbackReady) { return fallback }
58
58
+
return null
59
59
+
}
60
60
+
if (item.sent.ws && item.sent.gossip) { return null }
61
61
+
const firstTarget = item.firstTarget
62
62
+
if (!firstTarget) { return null }
63
63
+
const otherTarget = flipTarget(firstTarget)
64
64
+
const otherReady = otherTarget === 'ws' ? wsReady : gossipReady
65
65
+
if (!item.sent[otherTarget] && otherReady && Date.now() - item.sentAt[firstTarget] >= HASH_RETRY_MS) {
66
66
+
return otherTarget
67
67
+
}
68
68
+
return null
69
69
+
}
70
70
+
49
71
const drainQueue = () => {
50
72
drainTimer = null
51
73
if (draining) {
···
56
78
try {
57
79
for (let i = 0; i < queue.length; i += 1) {
58
80
const item = queue[i]
59
59
-
const wsReady = item.targets.ws && !item.sent.ws && isTargetReady('ws')
60
60
-
const gossipReady = item.targets.gossip && !item.sent.gossip && isTargetReady('gossip')
81
81
+
if (item.kind === 'hash') {
82
82
+
const target = pickHashTarget(item)
83
83
+
if (!target) { continue }
84
84
+
sendToTarget(target, item.msg)
85
85
+
item.sent[target] = true
86
86
+
item.sentAt[target] = Date.now()
87
87
+
if (!item.firstTarget) {
88
88
+
item.firstTarget = target
89
89
+
nextHashTarget = flipTarget(target)
90
90
+
}
91
91
+
if (item.sent.ws && item.sent.gossip) {
92
92
+
cleanupItem(item, i)
93
93
+
}
94
94
+
break
95
95
+
}
96
96
+
const wsReady = !item.sent.ws && isTargetReady('ws')
97
97
+
const gossipReady = !item.sent.gossip && isTargetReady('gossip')
61
98
if (!wsReady && !gossipReady) { continue }
62
99
if (wsReady) {
63
100
sendToTarget('ws', item.msg)
···
67
104
sendToTarget('gossip', item.msg)
68
105
item.sent.gossip = true
69
106
}
70
70
-
const wsDone = !item.targets.ws || item.sent.ws
71
71
-
const gossipDone = !item.targets.gossip || item.sent.gossip
72
72
-
if (wsDone && gossipDone) {
73
73
-
cleanupItem(item, i)
74
74
-
}
107
107
+
const wsDone = item.sent.ws
108
108
+
const gossipDone = item.sent.gossip
109
109
+
if (wsDone && gossipDone) { cleanupItem(item, i) }
75
110
break
76
111
}
77
112
} finally {
···
82
117
}
83
118
}
84
119
85
85
-
export const queueSend = (msg, targets = 'both') => {
120
120
+
export const queueSend = (msg) => {
86
121
const key = getKey(msg)
87
87
-
const targetFlags = normalizeTargets(targets)
88
122
if (key && pending.has(key)) {
89
123
const item = pending.get(key)
90
90
-
item.targets.ws = item.targets.ws || targetFlags.ws
91
91
-
item.targets.gossip = item.targets.gossip || targetFlags.gossip
92
124
if (!drainTimer) { drainTimer = setTimeout(drainQueue, 0) }
93
125
return
94
126
}
95
127
const item = {
96
128
msg,
97
129
key,
98
98
-
targets: targetFlags,
99
99
-
sent: { ws: false, gossip: false }
130
130
+
kind: isHash(msg) ? 'hash' : 'blob',
131
131
+
sent: { ws: false, gossip: false },
132
132
+
sentAt: { ws: 0, gossip: 0 },
133
133
+
firstTarget: null
100
134
}
101
135
queue.push(item)
102
136
if (key) { pending.set(key, item) }
+1
-1
send.js
···
2
2
3
3
export const send = async (m) => {
4
4
console.log('SENDING' + m)
5
5
-
queueSend(m, 'both')
5
5
+
queueSend(m)
6
6
}
+5
-5
settings.js
···
77
77
if (log) {
78
78
const ar = []
79
79
for (const msg of log) {
80
80
-
queueSend(msg.sig, 'ws')
80
80
+
queueSend(msg.sig)
81
81
if (msg.text) {
82
82
-
queueSend(msg.text, 'ws')
82
82
+
queueSend(msg.text)
83
83
const yaml = await apds.parseYaml(msg.text)
84
84
if (yaml.image && !ar.includes(yaml.image)) {
85
85
const get = await apds.get(yaml.image)
86
86
if (get) {
87
87
-
queueSend(get, 'ws')
87
87
+
queueSend(get)
88
88
ar.push(yaml.image)
89
89
}
90
90
}
···
95
95
const src = image.match(/!\[.*?\]\((.*?)\)/)[1]
96
96
const imgBlob = await apds.get(src)
97
97
if (imgBlob && !ar.includes(src)) {
98
98
-
queueSend(imgBlob, 'ws')
98
98
+
queueSend(imgBlob)
99
99
ar.push(src)
100
100
}
101
101
}
···
104
104
}
105
105
if (!msg.text) {
106
106
const get = await apds.get(msg.opened.substring(13))
107
107
-
if (get) { queueSend(get, 'ws') }
107
107
+
if (get) { queueSend(get) }
108
108
}
109
109
}
110
110
}