| … | … | @@ -12,7 +12,7 @@ import { View, Text, FlatList, RefreshControl, ActivityIndicator, StyleSheet, Pr |
| 12 | 12 | import { startNode, stopNode } from './worklet' |
| 13 | 13 | import Pair from './Pair' |
| 14 | 14 | import Post from './Post' |
| 15 | import { Thread, Profile, Search, Compose, Settings, Invite, Identities, Communities, People, Header, Center } from './Screens' | |
| 15 | import { Thread, Profile, Search, Compose, Settings, Invite, Identities, Communities, People, Header, Center, requireApi } from './Screens' | |
| 16 | 16 | import { apiFromStatus } from '../lib/favela-api' |
| 17 | 17 | |
| 18 | 18 | export default function App () { |
| … | … | @@ -96,15 +96,15 @@ function Shell ({ client, status, onPaired }) { |
| 96 | 96 | return ( |
| 97 | 97 | <SafeAreaView style={s.fill}> |
| 98 | 98 | <Header title={title} onBack={nav.pop} /> |
| 99 | {top.name === 'thread' ? <Thread client={client} uri={top.uri} nav={nav} /> : null} | |
| 100 | {top.name === 'profile' ? <Profile client={client} did={top.did} nav={nav} /> : null} | |
| 99 | {top.name === 'thread' ? <Thread client={client} api={api} uri={top.uri} nav={nav} /> : null} | |
| 100 | {top.name === 'profile' ? <Profile client={client} api={api} did={top.did} nav={nav} /> : null} | |
| 101 | 101 | {top.name === 'compose' ? <Compose client={client} replyTo={top.replyTo || null} onDone={nav.pop} /> : null} |
| 102 | {top.name === 'tag' ? <TagFeed client={client} tag={top.tag} nav={nav} /> : null} | |
| 102 | {top.name === 'tag' ? <TagFeed client={client} api={api} tag={top.tag} nav={nav} /> : null} | |
| 103 | 103 | {top.name === 'pair' ? <Pair client={client} onPaired={async (r) => { await onPaired(r); nav.pop() }} /> : null} |
| 104 | 104 | {top.name === 'invite' ? <Invite client={client} /> : null} |
| 105 | 105 | {top.name === 'identities' ? <Identities client={client} current={acting} onPick={(d) => { setActingDid(d.did); nav.pop() }} /> : null} |
| 106 | 106 | {top.name === 'communities' ? <Communities client={client} nav={nav} /> : null} |
| 107 | {top.name === 'people' ? <People client={client} did={top.did} kind={top.kind} nav={nav} /> : null} | |
| 107 | {top.name === 'people' ? <People client={client} api={api} did={top.did} kind={top.kind} nav={nav} /> : null} | |
| 108 | 108 | </SafeAreaView> |
| 109 | 109 | ) |
| 110 | 110 | } |
| … | … | @@ -123,7 +123,7 @@ function Shell ({ client, status, onPaired }) { |
| 123 | 123 | : null} |
| 124 | 124 | <View style={s.fill}> |
| 125 | 125 | {tab === 'home' ? <Timeline client={client} nav={nav} acting={acting} api={api} /> : null} |
| 126 | {tab === 'search' ? <Search client={client} nav={nav} /> : null} | |
| 126 | {tab === 'search' ? <Search client={client} api={api} nav={nav} /> : null} | |
| 127 | 127 | {tab === 'browse' ? <Communities client={client} nav={nav} /> : null} |
| 128 | 128 | {tab === 'settings' ? <Settings client={client} status={status} nav={nav} /> : null} |
| 129 | 129 | </View> |
| … | … | @@ -157,22 +157,16 @@ function Timeline ({ client, nav, acting, api }) { |
| 157 | 157 | const community = acting && !acting.primary ? acting.did : null |
| 158 | 158 | const load = useCallback(async (reset) => { |
| 159 | 159 | try { |
| 160 | let r | |
| 161 | if (api) { | |
| 162 | // Both feeds now come from favela-web, through the ONE client that carries the session — a raw | |
| 163 | // fetch here would silently drop the cookie and the rows would come back without liked/reposted | |
| 164 | // state, with no error to notice. | |
| 165 | r = community ? await api.author({ did: community, limit: 30 }) : await api.timeline({ limit: 30 }) | |
| 166 | } else { | |
| 167 | r = community | |
| 168 | ? await client.author({ did: community, limit: 30, cursor: reset ? null : cursor }) | |
| 169 | : await client.timeline({ limit: 30, cursor: reset ? null : cursor }) | |
| 170 | } | |
| 160 | // Both feeds come from favela-web, through the ONE client that carries the session — a raw fetch | |
| 161 | // here would silently drop the cookie and the rows would come back without liked/reposted state, | |
| 162 | // with no error to notice. | |
| 163 | const src = requireApi(api) | |
| 164 | const r = community ? await src.author({ did: community, limit: 30 }) : await src.timeline({ limit: 30 }) | |
| 171 | 165 | setPosts((prev) => (reset ? (r.posts || []) : [...prev, ...(r.posts || [])])) |
| 172 | 166 | setCursor(r.cursor) |
| 173 | 167 | setErr(null) |
| 174 | 168 | } catch (e) { setErr(e.message) } |
| 175 | }, [client, cursor, community, api]) | |
| 169 | }, [cursor, community, api]) | |
| 176 | 170 | |
| 177 | 171 | // Reload when the identity changes — otherwise switching shows the previous identity's feed. |
| 178 | 172 | useEffect(() => { setPosts([]); setCursor(null); load(true) }, [community]) // eslint-disable-line react-hooks/exhaustive-deps |
| … | … | @@ -203,12 +197,14 @@ function Timeline ({ client, nav, acting, api }) { |
| 203 | 197 | ) |
| 204 | 198 | } |
| 205 | 199 | |
| 206 | function TagFeed ({ client, tag, nav }) { | |
| 200 | function TagFeed ({ client, api, tag, nav }) { | |
| 207 | 201 | const [posts, setPosts] = useState([]) |
| 208 | 202 | const [err, setErr] = useState(null) |
| 209 | 203 | useEffect(() => { |
| 210 | client.tag({ tag, limit: 30 }).then((r) => setPosts(r.posts || [])).catch((e) => setErr(e.message)) | |
| 211 | }, [client, tag]) | |
| 204 | // favela-web calls this route /api/feed; the legacy verb called it `tag`. Same query either way. | |
| 205 | const p = requireApi(api).tagFeed({ tag, limit: 30 }) | |
| 206 | p.then((r) => setPosts(r.posts || [])).catch((e) => setErr(e.message)) | |
| 207 | }, [client, api, tag]) | |
| 212 | 208 | if (err) return <Center><Text style={s.err}>{err}</Text></Center> |
| 213 | 209 | return ( |
| 214 | 210 | <FlatList |
| … | … | @@ -51,9 +51,19 @@ export function PersonRow ({ client, person, onOpen }) { |
| 51 | 51 | ) |
| 52 | 52 | } |
| 53 | 53 | |
| 54 | // The read surface lives in favela-web now. A screen must NOT silently fall back to the worklet's own | |
| 55 | // appview: that is a SECOND index over the same records, so a fallback would quietly show a different | |
| 56 | // answer to the same question. If favela-web is down, say so — the node log names the reason. | |
| 57 | export function requireApi (api) { | |
| 58 | if (!api) throw new Error('favela-web is not running — the node log says why') | |
| 59 | return api | |
| 60 | } | |
| 61 | ||
| 54 | 62 | // Followers / following as a browsable list. |
| 55 | export function People ({ client, did, kind, nav }) { | |
| 56 | const { loading, error, data, reload } = useAsync(() => client.people({ did, kind }), [client, did, kind]) | |
| 63 | export function People ({ client, api, did, kind, nav }) { | |
| 64 | // hydrate=1 gets profiles in the SAME request; the fallback is the legacy verb, for a phone whose | |
| 65 | // favela-web did not start. | |
| 66 | const { loading, error, data, reload } = useAsync(() => requireApi(api).people({ did, kind }), [api, did, kind]) | |
| 57 | 67 | if (loading || error) return <Loading error={error} onRetry={reload} /> |
| 58 | 68 | const people = (data && data.people) || [] |
| 59 | 69 | if (!people.length) return <Center><Text style={s.dim}>no {kind} held for this account yet</Text></Center> |
| … | … | @@ -199,11 +209,12 @@ function Loading ({ error, onRetry }) { |
| 199 | 209 | // ── thread ─────────────────────────────────────────────────────────────────────────────────────────── |
| 200 | 210 | // A post and its replies. The web page renders ancestors + the post + descendants; the appview's |
| 201 | 211 | // getPostThread returns exactly that tree, so this walks it rather than re-deriving structure. |
| 202 | export function Thread ({ client, uri, nav }) { | |
| 212 | export function Thread ({ client, api, uri, nav }) { | |
| 203 | 213 | const { loading, error, data, reload } = useAsync(async () => { |
| 204 | const [{ thread }, { replies }] = await Promise.all([client.thread({ uri }), client.replies({ uri })]) | |
| 214 | const src = requireApi(api) | |
| 215 | const [{ thread }, { replies }] = await Promise.all([src.thread({ uri }), src.replies({ uri })]) | |
| 205 | 216 | return { thread, replies } |
| 206 | }, [uri]) | |
| 217 | }, [uri, api]) | |
| 207 | 218 | |
| 208 | 219 | if (loading || error) return <Loading error={error} onRetry={reload} /> |
| 209 | 220 | const root = data.thread && (data.thread.post || data.thread) |
| … | … | @@ -227,12 +238,13 @@ export function Thread ({ client, uri, nav }) { |
| 227 | 238 | } |
| 228 | 239 | |
| 229 | 240 | // ── profile ────────────────────────────────────────────────────────────────────────────────────────── |
| 230 | export function Profile ({ client, did, nav }) { | |
| 241 | export function Profile ({ client, api, did, nav }) { | |
| 231 | 242 | const [busy, setBusy] = useState(false) |
| 232 | 243 | const { loading, error, data, reload } = useAsync(async () => { |
| 233 | const [{ profile }, feed] = await Promise.all([client.profile({ did }), client.author({ did, limit: 30 })]) | |
| 244 | const src = requireApi(api) | |
| 245 | const [{ profile }, feed] = await Promise.all([src.profile({ did }), src.author({ did, limit: 30 })]) | |
| 234 | 246 | return { profile, posts: feed.posts || [] } |
| 235 | }, [did]) | |
| 247 | }, [did, api]) | |
| 236 | 248 | |
| 237 | 249 | const toggleFollow = async () => { |
| 238 | 250 | setBusy(true) |
| … | … | @@ -268,7 +280,7 @@ export function Profile ({ client, did, nav }) { |
| 268 | 280 | } |
| 269 | 281 | |
| 270 | 282 | // ── search ─────────────────────────────────────────────────────────────────────────────────────────── |
| 271 | export function Search ({ client, nav }) { | |
| 283 | export function Search ({ client, api, nav }) { | |
| 272 | 284 | const [q, setQ] = useState('') |
| 273 | 285 | const [kind, setKind] = useState('posts') |
| 274 | 286 | const [res, setRes] = useState(null) |
| … | … | @@ -277,7 +289,7 @@ export function Search ({ client, nav }) { |
| 277 | 289 | const go = async () => { |
| 278 | 290 | if (!q.trim()) return |
| 279 | 291 | setBusy(true) |
| 280 | try { setRes(await client.search({ q: q.trim(), kind })) } catch (e) { setRes({ error: e.message }) } finally { setBusy(false) } | |
| 292 | try { setRes(await requireApi(api).search({ q: q.trim(), kind })) } catch (e) { setRes({ error: e.message }) } finally { setBusy(false) } | |
| 281 | 293 | } |
| 282 | 294 | |
| 283 | 295 | return ( |
| … | … | @@ -66,6 +66,11 @@ export class FavelaApi { |
| 66 | 66 | search ({ q, kind = 'posts', limit = 25 } = {}) { return this.get('/api/search', { q, kind, limit }) } |
| 67 | 67 | follows ({ did } = {}) { return this.get('/api/follows', { did }) } |
| 68 | 68 | followers ({ did } = {}) { return this.get('/api/followers', { did }) } |
| 69 | // hydrate=1 — profiles included, so a list of PEOPLE is one request rather than N+1. The bare-list | |
| 70 | // form above stays for callers that only need DIDs and would discard the hydration. | |
| 71 | people ({ did, kind = 'follows', limit = 200 } = {}) { | |
| 72 | return this.get(kind === 'followers' ? '/api/followers' : '/api/follows', { did, limit, hydrate: 1 }) | |
| 73 | } | |
| 69 | 74 | tagFeed ({ tag, limit = 30 } = {}) { return this.get('/api/feed', { tag, limit }) } |
| 70 | 75 | } |
| 71 | 76 |
| … | … | @@ -127,19 +127,9 @@ export class WorkletClient { |
| 127 | 127 | |
| 128 | 128 | // Typed wrappers. Thin on purpose — they exist so a screen never types an op string, not to add logic. |
| 129 | 129 | status () { return this.request('status') } |
| 130 | timeline (params) { return this.request('timeline', params) } | |
| 131 | author (params) { return this.request('author', params) } | |
| 132 | profile (params) { return this.request('profile', params) } | |
| 133 | 130 | collections () { return this.request('collections') } |
| 134 | 131 | // The ported surface. Thin by design — they exist so a screen never types an op string, not to add |
| 135 | 132 | // logic; every one of them maps to an appview query or a device write in the worklet. |
| 136 | thread (params) { return this.request('thread', params) } | |
| 137 | post (params) { return this.request('post', params) } | |
| 138 | replies (params) { return this.request('replies', params) } | |
| 139 | tag (params) { return this.request('tag', params) } | |
| 140 | search (params) { return this.request('search', params) } | |
| 141 | follows (params) { return this.request('follows', params) } | |
| 142 | followers (params) { return this.request('followers', params) } | |
| 143 | 133 | compose (params) { return this.request('compose', params) } |
| 144 | 134 | deletePost (params) { return this.request('deletePost', params) } |
| 145 | 135 | follow (params) { return this.request('follow', params) } |
| … | … | @@ -154,7 +144,6 @@ export class WorkletClient { |
| 154 | 144 | viewDid (params) { return this.request('viewDid', params) } |
| 155 | 145 | trackedDids () { return this.request('trackedDids') } |
| 156 | 146 | untrackDid (params) { return this.request('untrackDid', params) } |
| 157 | people (params) { return this.request('people', params) } | |
| 158 | 147 | syncFollows (params) { return this.request('syncFollows', params) } |
| 159 | 148 | devices () { return this.request('devices') } |
| 160 | 149 | revokeDevice (params) { return this.request('revokeDevice', params) } |
| … | … | @@ -305,16 +305,6 @@ const handlers = { |
| 305 | 305 | // scope 'following' (default) — you + everyone you follow + anything you track (communities) |
| 306 | 306 | // scope 'all' — every author in the local index, unfiltered (what it used to do) |
| 307 | 307 | // scope '<did>' — one account/community |
| 308 | async timeline ({ limit = 30, cursor = null, scope = 'following', viewer = null } = {}) { | |
| 309 | const me = viewerDid(viewer) | |
| 310 | let authors = null | |
| 311 | if (scope && scope.startsWith('did:')) authors = [scope] | |
| 312 | else if (scope !== 'all') { | |
| 313 | const following = me ? await state.appview.following(me).catch(() => []) : [] | |
| 314 | authors = [me, ...(following || []), ...readTracked()].filter(Boolean) | |
| 315 | } | |
| 316 | return state.appview.getTimeline({ limit, cursor, authors, viewerDid: me }) | |
| 317 | }, | |
| 318 | 308 | |
| 319 | 309 | // Pull the repos of accounts you follow but do not host, so the scoped timeline has something in it. |
| 320 | 310 | // Bounded and non-blocking on purpose: this is a phone, each one is a topic join plus a verify, and a |
| … | … | @@ -334,12 +324,6 @@ const handlers = { |
| 334 | 324 | } |
| 335 | 325 | return { queued: todo.length, following: following.length } |
| 336 | 326 | }, |
| 337 | async author ({ did, limit = 30, cursor = null, filter } = {}) { | |
| 338 | return state.appview.getAuthorFeed(did, { limit, cursor, filter }) | |
| 339 | }, | |
| 340 | async profile ({ did } = {}) { | |
| 341 | return { profile: await state.appview.getProfile(did) } | |
| 342 | }, | |
| 343 | 327 | // The pairing ceremony runs IN THE WORKLET, never in the UI. lib/adopt.js touches keys, proofs and the |
| 344 | 328 | // blind-pairing tunnel; a React Native component must not. The screen collects an invite and shows |
| 345 | 329 | // progress — that is the whole of its involvement. |
| … | … | @@ -407,19 +391,6 @@ const handlers = { |
| 407 | 391 | |
| 408 | 392 | // People, hydrated. `follows`/`followers` returned bare DID strings, which a UI can only render as |
| 409 | 393 | // hex — the reason there is no way to browse anyone. Same queries, with the profile attached. |
| 410 | async people ({ did = null, kind = 'follows', limit = 200 } = {}) { | |
| 411 | const subject = did || viewerDid() | |
| 412 | const dids = kind === 'followers' | |
| 413 | ? await state.appview.followers(subject) | |
| 414 | : await state.appview.following(subject) | |
| 415 | const out = [] | |
| 416 | for (const d of (dids || []).slice(0, limit)) { | |
| 417 | let p = null | |
| 418 | try { p = await state.appview.getProfile(d) } catch {} | |
| 419 | out.push({ did: d, handle: (p && p.handle) || null, displayName: (p && p.displayName) || null, avatar: (p && p.avatar) || null, description: (p && p.description) || null }) | |
| 420 | } | |
| 421 | return { people: out, count: (dids || []).length } | |
| 422 | }, | |
| 423 | 394 | |
| 424 | 395 | // ── identities ──────────────────────────────────────────────────────────────────────────────────── |
| 425 | 396 | // Every DID this node hosts, not just the first. The daemon has always been multi-DID — a user's own |
| … | … | @@ -499,44 +470,6 @@ const handlers = { |
| 499 | 470 | // and every query are shared verbatim, so a screen here answers the same question the web page does |
| 500 | 471 | // from the same code. `viewerDid` is this node's own DID so viewer state (liked/reposted/muted) is |
| 501 | 472 | // hydrated before anything crosses IPC, which is what keeps a row a render rather than a lookup. |
| 502 | async thread ({ uri, depth = 6, parentHeight = 80, viewer = null } = {}) { | |
| 503 | if (!uri) throw new Error('thread requires { uri }') | |
| 504 | return { thread: await state.appview.getPostThread(uri, { depth, parentHeight, viewerDid: viewerDid(viewer) }) } | |
| 505 | }, | |
| 506 | async post ({ uri, viewer = null } = {}) { | |
| 507 | if (!uri) throw new Error('post requires { uri }') | |
| 508 | return { post: await state.appview.getPost(uri, { viewerDid: viewerDid(viewer) }) } | |
| 509 | }, | |
| 510 | async replies ({ uri, limit = 50, viewer = null } = {}) { | |
| 511 | if (!uri) throw new Error('replies requires { uri }') | |
| 512 | return { replies: await state.appview.getReplies(uri, { limit, viewerDid: viewerDid(viewer) }) } | |
| 513 | }, | |
| 514 | async tag ({ tag, limit = 30, cursor = null, viewer = null } = {}) { | |
| 515 | if (!tag) throw new Error('tag requires { tag }') | |
| 516 | return state.appview.getTagFeed(tag, { limit, cursor, viewerDid: viewerDid(viewer) }) | |
| 517 | }, | |
| 518 | async search ({ q, kind = 'posts', limit = 25, viewer = null } = {}) { | |
| 519 | if (!q) throw new Error('search requires { q }') | |
| 520 | // The two differ in shape at the source and the op must not "tidy" that: searchPosts already | |
| 521 | // returns { posts, cursor, truncated } (truncated matters — a scan-capped result is not an empty | |
| 522 | // one), searchActors returns a bare array. Wrapping the first again produced {posts:{posts:[…]}}. | |
| 523 | return kind === 'actors' | |
| 524 | ? { actors: await state.appview.searchActors(q, { limit }) } | |
| 525 | : await state.appview.searchPosts(q, { limit, viewerDid: viewerDid(viewer) }) | |
| 526 | }, | |
| 527 | async follows ({ did } = {}) { return { dids: await state.appview.following(did || viewerDid()) } }, | |
| 528 | async followers ({ did } = {}) { return { dids: await state.appview.followers(did || viewerDid()) } }, | |
| 529 | ||
| 530 | // ── the ported write surface ─────────────────────────────────────────────────────────────────────── | |
| 531 | // ⛔ EVERY WRITE GOES THROUGH THE DEVICE, never appview.put* directly. hpp is the authority for record | |
| 532 | // BYTES: it hashes what it writes and commits that hash to the autobase. A second writer into its | |
| 533 | // store is verbatim the 2026-07-31 clobber that permanently destroyed two records. The appview is | |
| 534 | // updated AFTERWARDS so the local index reflects the write immediately (favela-web does the same) — | |
| 535 | // that is an index update, not a second authority. | |
| 536 | // | |
| 537 | // The record SHAPES are not rebuilt here: favela-web's Writer builds them (scope->collection, facets, | |
| 538 | // embeds, langs, the 300-grapheme cap) and this hands it the in-process device instead of a socket. | |
| 539 | // Re-deriving those shapes is how a uri-only strongRef shipped once already. | |
| 540 | 473 | async compose ({ text, replyTo = null, quoteOf = null, lang = null, scope = 'public' } = {}) { |
| 541 | 474 | const w = await writer() |
| 542 | 475 | const langs = /^[a-z]{2}(-[A-Za-z0-9]{2,8})?$/.test(String(lang || '')) ? [String(lang)] : ['en'] |
| … | … | @@ -92,31 +92,22 @@ console.log('\n# status') |
| 92 | 92 | assert(s.signing === false, 'signing:false with no DID paired — honest about being read-only') |
| 93 | 93 | } |
| 94 | 94 | |
| 95 | console.log('\n# timeline on an empty node') | |
| 96 | { | |
| 97 | const t = await client.timeline({ limit: 10 }) | |
| 98 | assert(Array.isArray(t.posts), 'timeline returns a posts array') | |
| 99 | assert(t.posts.length === 0, 'empty on a fresh node (not an error)') | |
| 100 | } | |
| 101 | ||
| 102 | 95 | console.log('\n# collections — the #33 ledger, reachable from the phone') |
| 103 | 96 | { |
| 104 | 97 | const c = await client.collections() |
| 105 | 98 | assert(Array.isArray(c.ledger), 'collections returns the ingest ledger') |
| 106 | 99 | } |
| 107 | 100 | |
| 108 | console.log('\n# the ported READ surface answers on an empty node') | |
| 101 | // ⛔ THE READ SURFACE IS NO LONGER AN IPC VERB. timeline/thread/replies/search/follows/followers/ | |
| 102 | // people/tag/author/profile moved to favela-web's JSON routes (favela-web 0.3.51+), reached through | |
| 103 | // lib/favela-api.js — one engine, one index, one definition. Their absence here is the POINT: a verb | |
| 104 | // that still answered would be a second implementation reading a second appview, which is the bug this | |
| 105 | // port exists to remove. lib/favela-api.js has its own test; screens call it, never this client. | |
| 106 | console.log('\n# the read verbs are GONE — the routes own that surface now') | |
| 109 | 107 | { |
| 110 | const t = await client.thread({ uri: 'at://did:plc:nobody/app.bsky.feed.post/none' }) | |
| 111 | assert('thread' in t, 'thread responds (null thread on an unknown uri is an answer, not an error)') | |
| 112 | const r = await client.replies({ uri: 'at://did:plc:nobody/app.bsky.feed.post/none' }) | |
| 113 | assert(Array.isArray(r.replies), 'replies returns an array') | |
| 114 | const se = await client.search({ q: 'anything' }) | |
| 115 | assert(Array.isArray(se.posts), 'search returns posts') | |
| 116 | const sa = await client.search({ q: 'anything', kind: 'actors' }) | |
| 117 | assert(Array.isArray(sa.actors), 'search kind:actors returns actors') | |
| 118 | const f = await client.follows({ did: 'did:plc:nobody' }) | |
| 119 | assert(Array.isArray(f.dids), 'follows returns a did array') | |
| 108 | for (const dead of ['timeline', 'thread', 'replies', 'search', 'follows', 'followers', 'people', 'tag', 'author', 'profile']) { | |
| 109 | assert(typeof client[dead] !== 'function', dead + ' is no longer an IPC verb') | |
| 110 | } | |
| 120 | 111 | } |
| 121 | 112 | |
| 122 | 113 | console.log('\n# the WRITE surface refuses cleanly on a node with no identity') |
| … | … | @@ -140,8 +131,10 @@ console.log('\n# an unknown op is an error, not a hang') |
| 140 | 131 | |
| 141 | 132 | console.log('\n# id correlation — concurrent requests do not cross') |
| 142 | 133 | { |
| 143 | const [a, b, c] = await Promise.all([client.status(), client.timeline({ limit: 1 }), client.status()]) | |
| 144 | assert(a.ready === true && c.ready === true && Array.isArray(b.posts), | |
| 134 | // Three DIFFERENT ops in flight at once — the point is that each reply finds its own caller, so they | |
| 135 | // must be distinguishable by shape, not just by count. | |
| 136 | const [a, b, c] = await Promise.all([client.status(), client.hostedDids(), client.trackedDids()]) | |
| 137 | assert(a.ready === true && Array.isArray(b.dids) && Array.isArray(c.dids), | |
| 145 | 138 | 'three in flight resolve to their OWN replies') |
| 146 | 139 | } |
| 147 | 140 |