a demonstration replicated social networking web app built with anproto wiredove.net/
social ed25519 protocols

add http fallback

+114 -17
+2 -1
connect.js
··· 1 1 import { apds } from 'apds' 2 2 import { makeRoom } from './gossip.js' 3 - import { makeWs} from './websocket.js' 3 + import { makeWs, startHttpGossip } from './websocket.js' 4 4 import { send } from './send.js' 5 5 6 6 await apds.start('wiredovedbversion1') ··· 9 9 await makeWs('ws://localhost:9000') 10 10 //await makeWs('wss://apds.anproto.com/') 11 11 //makeWs('wss://pub.wiredove.net/') 12 + await startHttpGossip('http://localhost:9000') 12 13 makeRoom('wiredovev1') 13 14 send('evSFOKnXaF9ZWSsff8bVfXP6+XnGZUj8XNp6bca590k=') 14 15 }
+112 -16
websocket.js
··· 4 4 5 5 const pubs = new Set() 6 6 const wsBackoff = new Map() 7 + const HTTP_POLL_INTERVAL_MS = 5000 8 + const httpState = { 9 + baseUrl: null, 10 + ready: false, 11 + pollTimer: null, 12 + lastSince: 0 13 + } 7 14 8 15 let wsReadyResolver 9 16 const createWsReadyPromise = () => new Promise(resolve => { ··· 17 24 }) 18 25 } 19 26 27 + const isHash = (msg) => typeof msg === 'string' && msg.length === 44 28 + 29 + const handleIncoming = async (msg) => { 30 + noteReceived(msg) 31 + if (isHash(msg)) { 32 + const blob = await apds.get(msg) 33 + if (blob) { 34 + if (pubs.size) { 35 + deliverWs(blob) 36 + } else { 37 + await sendHttp(blob) 38 + } 39 + } 40 + return 41 + } 42 + await render.shouldWe(msg) 43 + await apds.make(msg) 44 + await apds.add(msg) 45 + await render.blob(msg) 46 + } 47 + 48 + const toHttpBase = (wsUrl) => wsUrl.replace(/^ws:/, 'http:').replace(/^wss:/, 'https:') 49 + 50 + const scheduleHttpPoll = () => { 51 + if (httpState.pollTimer) { return } 52 + httpState.pollTimer = setTimeout(pollHttp, HTTP_POLL_INTERVAL_MS) 53 + } 54 + 55 + const pollHttp = async () => { 56 + httpState.pollTimer = null 57 + if (!httpState.ready || pubs.size) { 58 + scheduleHttpPoll() 59 + return 60 + } 61 + try { 62 + const url = new URL('/gossip/poll', httpState.baseUrl) 63 + url.searchParams.set('since', String(httpState.lastSince)) 64 + const res = await fetch(url.toString(), { cache: 'no-store' }) 65 + if (res.ok) { 66 + const data = await res.json() 67 + const messages = Array.isArray(data.messages) ? data.messages : [] 68 + for (const msg of messages) { 69 + await handleIncoming(msg) 70 + } 71 + if (Number.isFinite(data.nextSince)) { 72 + httpState.lastSince = Math.max(httpState.lastSince, data.nextSince) 73 + } 74 + } 75 + } catch (err) { 76 + console.warn('http gossip poll failed', err) 77 + } finally { 78 + scheduleHttpPoll() 79 + } 80 + } 81 + 82 + const sendHttp = async (msg) => { 83 + if (!httpState.ready) { return } 84 + try { 85 + const url = new URL('/gossip', httpState.baseUrl) 86 + const res = await fetch(url.toString(), { 87 + method: 'POST', 88 + headers: { 'Content-Type': 'text/plain' }, 89 + body: msg 90 + }) 91 + if (!res.ok) { return } 92 + const data = await res.json() 93 + const messages = Array.isArray(data.messages) ? data.messages : [] 94 + for (const reply of messages) { 95 + await handleIncoming(reply) 96 + } 97 + } catch (err) { 98 + console.warn('http gossip send failed', err) 99 + } 100 + } 101 + 102 + export const startHttpGossip = async (baseUrl) => { 103 + if (httpState.ready) { return } 104 + httpState.baseUrl = baseUrl 105 + httpState.ready = true 106 + try { 107 + const q = await apds.query() 108 + if (q && q.length) { 109 + const last = q[q.length - 1] 110 + const ts = parseInt(last?.ts || '0', 10) 111 + if (Number.isFinite(ts)) { 112 + httpState.lastSince = ts 113 + } 114 + } 115 + } catch (err) { 116 + console.warn('http gossip seed failed', err) 117 + } 118 + scheduleHttpPoll() 119 + } 120 + 20 121 export const sendWs = async (msg) => { 21 - if (pubs.size) { deliverWs(msg) } 122 + if (pubs.size) { 123 + deliverWs(msg) 124 + } else { 125 + await sendHttp(msg) 126 + } 22 127 } 23 128 24 - export const hasWs = () => pubs.size > 0 129 + export const hasWs = () => pubs.size > 0 || httpState.ready 25 130 26 131 registerNetworkSenders({ 27 132 sendWs, ··· 29 134 }) 30 135 31 136 export const makeWs = async (pub) => { 137 + const httpBase = toHttpBase(pub) 138 + await startHttpGossip(httpBase) 139 + 32 140 const getBackoff = () => { 33 141 let state = wsBackoff.get(pub) 34 142 if (!state) { ··· 119 227 } 120 228 121 229 ws.onmessage = async (m) => { 122 - noteReceived(m.data) 123 - if (m.data.length === 44) { 124 - //console.log('NEEDS' + m.data) 125 - const blob = await apds.get(m.data) 126 - if (blob) { 127 - ws.send(blob) 128 - } 129 - } else { 130 - await render.shouldWe(m.data) 131 - await apds.make(m.data) 132 - await apds.add(m.data) 133 - await render.blob(m.data) 134 - } 135 - } 230 + await handleIncoming(m.data) 231 + } 136 232 137 233 ws.onerror = () => { 138 234 scheduleReconnect()