| … | … | @@ -101,7 +101,7 @@ src/ |
| 101 | 101 | └── relay/ |
| 102 | 102 | └── server.js self-hosted hiverelay with @hyperswarm/dht-relay endpoint |
| 103 | 103 | |
| 104 | landing/ static site for p2pbuilders.com | |
| 104 | landing/ static site for p2pbuilders.org | |
| 105 | 105 | public/ parked browser/desktop UI (dev mode) — not the terminal app |
| 106 | 106 | ios-app/ minimal Xcode project using bare-kit-pear (parked) |
| 107 | 107 | scripts/ |
| … | … | @@ -138,7 +138,7 @@ async function viewFeed ({ board, query, guard, token }) { |
| 138 | 138 | const sort = query.sort || prefs.sort || 'hot' |
| 139 | 139 | guard(skeleton()) |
| 140 | 140 | let posts = board === 'all' ? await data.listAllPosts() : await data.listPostsIn(board) |
| 141 | const blocked = await data.blockedSet([], prefs.blocklistSubs()) | |
| 141 | const blocked = await data.blockedSet(await data.blockedTargets(identity.me().pubkey), prefs.blocklistSubs()) | |
| 142 | 142 | posts = posts.filter(p => !p.deleted && !blocked.has(p.author) && !prefs.isHidden(p.board + '/' + p.cid)) |
| 143 | 143 | posts = await data.withTallies(posts) |
| 144 | 144 | await primeNames(posts.map(p => p.author)) |
| … | … | @@ -296,11 +296,11 @@ async function viewProfile ({ pub, guard, token }) { |
| 296 | 296 | const profile = await data.getProfile(pub) |
| 297 | 297 | const act = await data.userActivity(pub) |
| 298 | 298 | const inputs = await data.weightInputsFor(pub) |
| 299 | const followers = mine ? await data.following(me.pubkey) : [] | |
| 299 | const myFollowing = mine ? [] : await data.following(me.pubkey) | |
| 300 | 300 | await primeNames([pub]) |
| 301 | 301 | if (token !== renderToken) return |
| 302 | 302 | const items = [...act.posts.map(p => ({ k: 'post', t: p.createdAt, p })), ...act.comments.map(c => ({ k: 'comment', t: c.createdAt, c }))].sort((a, b) => b.t - a.t).slice(0, 60) |
| 303 | const isFollowing = followers.includes(pub) | |
| 303 | const isFollowing = myFollowing.includes(pub) | |
| 304 | 304 | const feed = items.length ? items.map(it => it.k === 'post' |
| 305 | 305 | ? `<li class="act"><span class="atag">post</span> <a href="${buildRoute(['b', it.p.board, 'item', it.p.cid])}">${esc(it.p.title)}</a> <span class="dim">in b/${esc(it.p.board)} · ${timeAgo(it.p.createdAt)}</span></li>` |
| 306 | 306 | : `<li class="act"><span class="atag">comment</span> on <a href="${buildRoute(['b', it.c.board || 'front', 'item', it.c.postCid])}">${esc(it.c.postTitle || 'a post')}</a> <span class="dim">${timeAgo(it.c.createdAt)}</span><div class="md small">${renderMarkdown(it.c.body)}</div></li>`).join('') : '<li class="dim">No activity yet.</li>' |
| … | … | @@ -254,6 +254,10 @@ export class Data { |
| 254 | 254 | return data |
| 255 | 255 | } |
| 256 | 256 | unblock (target) { return this.block(target, false, false) } |
| 257 | async blockedTargets (pub) { | |
| 258 | const rows = await this.sync.list(keys.blocksBy(pub), { limit: 1000 }) | |
| 259 | return rows.map(r => r.value).filter(v => v && v.active).map(v => v.target) | |
| 260 | } | |
| 257 | 261 | |
| 258 | 262 | async publishBlocklist (list) { |
| 259 | 263 | const me = this.me() |
| … | … | @@ -166,9 +166,17 @@ class GossipSync { |
| 166 | 166 | async _onBus (m) { |
| 167 | 167 | if (!m || !m.t) return |
| 168 | 168 | const me = this.getMe() |
| 169 | if (m.t === 'hello' && m.pub && m.pub !== me) { | |
| 169 | if (m.pub === me) { | |
| 170 | // A sibling tab running the SAME user wrote our outbox (tabs share | |
| 171 | // localStorage). Nothing to ingest — the records are already on disk — | |
| 172 | // but our merged cache predates them. Drop it and re-render. No | |
| 173 | // re-broadcast here, or two same-user tabs would ping-pong forever. | |
| 174 | if (m.t === 'hello' || m.t === 'outbox') { this._invalidate(); this._emit() } | |
| 175 | return | |
| 176 | } | |
| 177 | if (m.t === 'hello' && m.pub) { | |
| 170 | 178 | this._addPeer(m.pub); await this._broadcastMine(); this._invalidate(); this._emit() |
| 171 | } else if (m.t === 'outbox' && m.pub && m.pub !== me) { | |
| 179 | } else if (m.t === 'outbox' && m.pub) { | |
| 172 | 180 | this._addPeer(m.pub) |
| 173 | 181 | const incoming = m.view || {} |
| 174 | 182 | const secure = isSecure() |
| … | … | @@ -188,7 +196,11 @@ class GossipSync { |
| 188 | 196 | const sk = stickyKey(typeFromKey(k), iv) |
| 189 | 197 | if (!cur[k] || (sk != null ? nameWins(iv, cur[k]) : laterRecord(iv, cur[k]))) { cur[k] = iv; changed = true } |
| 190 | 198 | } |
| 191 | if (changed) { this._write(outboxKey(m.pub), cur); this._invalidate() } | |
| 199 | if (changed) this._write(outboxKey(m.pub), cur) | |
| 200 | // Invalidate even when nothing was written: when tabs share storage the | |
| 201 | // sender's tab already persisted these records, so `changed` is false | |
| 202 | // here while our merged cache still predates them. | |
| 203 | this._invalidate() | |
| 192 | 204 | this._emit() |
| 193 | 205 | } |
| 194 | 206 | } |
| … | … | @@ -105,6 +105,34 @@ async function main () { |
| 105 | 105 | const notBlocked = await alice.data.blockedSet([], []) |
| 106 | 106 | ok(!notBlocked.has('spammerPubkeyXYZ'), 'unsubscribed = not blocked') |
| 107 | 107 | |
| 108 | console.log('\n— own blocks —') | |
| 109 | await alice.data.block(bob.pub, true) | |
| 110 | ok((await alice.data.blockedTargets(alice.pub)).includes(bob.pub), 'blockedTargets lists own active blocks') | |
| 111 | const withOwn = await alice.data.blockedSet(await alice.data.blockedTargets(alice.pub), []) | |
| 112 | ok(withOwn.has(bob.pub), 'own block flows into the effective blocked set') | |
| 113 | await alice.data.unblock(bob.pub) | |
| 114 | ok(!(await alice.data.blockedTargets(alice.pub)).includes(bob.pub), 'unblock deactivates the record') | |
| 115 | ||
| 116 | console.log('\n— shared-storage tabs (browser multi-tab dev mode) —') | |
| 117 | // Two "tabs": distinct users, SAME storage object (like two browser tabs | |
| 118 | // sharing localStorage). The sender's tab persists records before the bus | |
| 119 | // message arrives, so the receiver ingests nothing new — its merged cache | |
| 120 | // must still be invalidated or it renders a stale (empty) view forever. | |
| 121 | const tabHub = makeHub() | |
| 122 | const tabStorage = mem() | |
| 123 | const idT1 = new DevIdentity(tabStorage, mem()); await idT1.ready(); await idT1.createUser('tab1') | |
| 124 | const idT2 = new DevIdentity(tabStorage, mem()); await idT2.ready(); await idT2.createUser('tab2') | |
| 125 | const tv = makeValidator(BITS) | |
| 126 | const tab1 = new GossipSync({ storage: tabStorage, bus: tabHub.connect(), getMe: () => idT1.me().pubkey, validate: tv }) | |
| 127 | const tab2 = new GossipSync({ storage: tabStorage, bus: tabHub.connect(), getMe: () => idT2.me().pubkey, validate: tv }) | |
| 128 | await tab1.ready(); await tab2.ready() | |
| 129 | const d1 = createData(tab1, idT1, { minBits: BITS }) | |
| 130 | const d2 = createData(tab2, idT2, { minBits: BITS }) | |
| 131 | await d1.range?.() // warm nothing; just ensure instances are live | |
| 132 | await tab1.list('post!') // build tab1's merged cache BEFORE tab2 writes | |
| 133 | const tp = await d2.submitPost({ board: 'front', title: 'from tab2' }) | |
| 134 | ok((await d1.listAllPosts()).some(x => x.cid === tp.cid), 'sibling-tab write reaches the other tab live (cache invalidated)') | |
| 135 | ||
| 108 | 136 | console.log('\n— profiles —') |
| 109 | 137 | await alice.data.setProfile({ nick: 'alice', bio: 'builds p2p' }) |
| 110 | 138 | ok((await alice.data.nickOf(alice.pub)) === 'alice', 'nick resolves from profile') |