a demonstration replicated social networking web app built with anproto wiredove.net/
social ed25519 protocols
at master 115 lines 3.1 kB view raw
1const CACHE_KEY = 'wiredove.feedRowCache.v1' 2const MAX_ROWS = 3000 3 4let rows = null 5let persistTimer = null 6 7const nowMs = () => Date.now() 8 9const loadRows = () => { 10 if (rows) { return rows } 11 rows = new Map() 12 if (typeof localStorage === 'undefined') { return rows } 13 try { 14 const raw = localStorage.getItem(CACHE_KEY) 15 if (!raw) { return rows } 16 const parsed = JSON.parse(raw) 17 if (!Array.isArray(parsed)) { return rows } 18 for (const item of parsed) { 19 if (!item || typeof item.hash !== 'string' || item.hash.length !== 44) { continue } 20 rows.set(item.hash, item) 21 } 22 } catch (err) { 23 console.warn('feed row cache load failed', err) 24 } 25 return rows 26} 27 28const schedulePersist = () => { 29 if (persistTimer) { return } 30 persistTimer = setTimeout(() => { 31 persistTimer = null 32 if (typeof localStorage === 'undefined') { return } 33 try { 34 const data = Array.from(loadRows().values()) 35 localStorage.setItem(CACHE_KEY, JSON.stringify(data)) 36 } catch (err) { 37 console.warn('feed row cache persist failed', err) 38 } 39 }, 500) 40} 41 42const trimRows = () => { 43 const map = loadRows() 44 while (map.size > MAX_ROWS) { 45 const firstKey = map.keys().next().value 46 if (!firstKey) { break } 47 map.delete(firstKey) 48 } 49} 50 51export const parseOpenedTimestamp = (opened) => { 52 if (!opened || opened.length < 13) { return 0 } 53 const ts = Number.parseInt(opened.substring(0, 13), 10) 54 return Number.isFinite(ts) ? ts : 0 55} 56 57const summarize = (txt, maxLen = 140) => { 58 if (!txt || typeof txt !== 'string') { return '' } 59 const single = txt.replace(/\s+/g, ' ').trim() 60 if (single.length <= maxLen) { return single } 61 return single.substring(0, maxLen) + '...' 62} 63 64export const makeFeedRow = ({ hash, opened = null, author = '', contentHash = '', yaml = null, ts = 0 } = {}) => { 65 if (!hash || typeof hash !== 'string' || hash.length !== 44) { return null } 66 const openedTs = ts || parseOpenedTimestamp(opened) 67 const preview = yaml && yaml.body 68 ? summarize(yaml.body) 69 : (yaml && yaml.bio ? summarize(yaml.bio) : '') 70 const name = yaml && yaml.name ? yaml.name.trim() : '' 71 return { 72 hash, 73 ts: openedTs || 0, 74 opened: opened || null, 75 author: author || '', 76 contentHash: contentHash || '', 77 name, 78 preview, 79 replyCount: 0, 80 updatedAt: nowMs() 81 } 82} 83 84export const upsertFeedRow = (row) => { 85 if (!row || !row.hash) { return false } 86 const map = loadRows() 87 const prev = map.get(row.hash) || {} 88 const next = { 89 ...prev, 90 ...row, 91 updatedAt: nowMs() 92 } 93 map.delete(row.hash) 94 map.set(row.hash, next) 95 trimRows() 96 schedulePersist() 97 return true 98} 99 100export const getFeedRow = (hash) => { 101 if (!hash || typeof hash !== 'string') { return null } 102 const map = loadRows() 103 return map.get(hash) || null 104} 105 106export const attachCachedRows = (log) => { 107 if (!Array.isArray(log) || !log.length) { return log || [] } 108 const map = loadRows() 109 return log.map((entry) => { 110 if (!entry || !entry.hash) { return entry } 111 const row = map.get(entry.hash) 112 if (!row) { return entry } 113 return { ...entry, row } 114 }) 115}