quotree #
visualize bluesky trend chains.
paste a bluesky post URL and quotree traces the chain of quote-posts that share its text (and optionally its image style), then draws the resulting tree.
live at https://sites.wisp.place/zzstoatzz.io/quotree.
how it works #
quotree is a static svelte app that runs entirely in the browser. there is no backend — all data comes from atproto services on the open network:
- microcosm does the heavy lifting:
constellation— global backlink index. used to find every post that quotes a given post (embed.record.uriandembed.record.record.uripaths).slingshot— edge cache for record/identity lookups. used to resolve handles, fetch records, and get mini did docs.
- bsky appview (
public.api.bsky.app) — profile lookups (avatar + handle). - the user's pds — optional, only when signed in. snapshots of a crawled tree are stored at
io.zzstoatzz.quotreeSnapshotand shared via URL hash.
the crawl starts at the post you give it, traces parent quotes upward to find the chain root, then follows constellation backlinks downward with a shared queue capped at ten active jobs. children are kept only if their text is similar enough to the root's (bigram dice coefficient, with a first-line fallback for prompt-style trends).
stack #
- svelte 5 (runes)
- vite + bun
- d3-hierarchy / d3-selection / d3-zoom for the tree
- @atcute for atproto client + OAuth (lazy-loaded)
develop #
bun install
bun run dev
build & deploy #
deployed to wisp.place (atproto static hosting):
bun run build
wispctl deploy zzstoatzz.io --path ./dist --site quotree --spa
the oauth-client-metadata.json in public/ is the client manifest used for production OAuth; localhost dev uses an inline loopback client.
crawl performance #
Backlink pages feed hydration work as they arrive. Independent branches share the same bounded queue; slow requests no longer hold up an entire batch. Ancestor records are reused within a crawl, and profile requests are shared by DID. Matching rules are unchanged; discovery and sibling order now depend on response completion. Requests to each service are spaced at least 250 ms apart (four starts per second per browser tab). HTTP 429 identifies the responding service, hostname, endpoint, and any valid Retry-After guidance, then stops the crawl without automatic retries; already active requests are allowed to settle. Failed backlink lookups reject the crawl rather than silently treating a partial tree as complete.
Run bun run test for HTTP integration tests covering pagination, filtering, deduplication, concurrency, ancestor cycles, and failures. The tests use a local service fixture, not the live network.
Before adding per-service request pacing, a comparison against the previous crawler on an 85-post, 20-author fixture with 15 ms response delays produced identical nodes and edges: 3.81 s / 395 requests before, 0.75 s / 324 requests after. These are scheduling measurements, not a live-network speed guarantee. The optional QUOTREE_BASELINE environment variable accepts a file URL to the previous crawler module (with its relative imports resolved) to repeat the comparison.
Further gains would require fewer remote lookups: batched AppView post hydration (which includes author data), or cached trees. AppView hydration would need a Slingshot fallback to retain record coverage. Quote counts should not be used to skip Constellation queries without accepting potentially different coverage. Long chains still have sequential discovery dependencies.
browser regression check #
Run node scripts/serve-browser-fixture.mjs and open the printed localhost URL. This serves the actual UI against a local HTTP fixture with delayed handle and record responses, without crawling public services. Paste https://bsky.app/profile/fixture.test/post/child: pasting must make no requests; double-clicking Go or pressing Enter repeatedly must produce one run, with immediate disabled controls and visible loading phases. It should finish with two participants. Inspect /__fixture/state for exactly one handle resolution and one read of each post.
Request /__fixture/reset?mode=limit to make the next handle lookup return HTTP 429; verify the attributed error and enabled controls. Request /__fixture/reset?mode=success to retry successfully. Check desktop and phone widths. The fixture is development-only and is not included in the production bundle.