Skip to content

feat(live): live on everything, and the fun layer - #45

Merged
AmrMsCLL merged 4 commits into
mainfrom
feat/live
Nov 7, 2026
Merged

AmrMsCLL merged 4 commits into
mainfrom
feat/live

Conversation

@AmrMsCLL

@AmrMsCLL AmrMsCLL commented Nov 7, 2026

Copy link
Copy Markdown
Member

Phase 6 of the hub plan: live on everything, and the fun layer. Plan: .claude/.Plans/as2026-11-07-graphite-hub-phase-6.md.

What changed

  • One topic map, one audit. app/topics.ts is the catalogue of topics the server publishes, the set that patches in place (presence, reading, editing, views, ping, map) and ROUTE_TOPICS, the full subscription of every route module; every route's handle is topicsFor('<module>'), with reading:<id> and editing:<id> keyed from the match. server/live-audit.test.ts reads app/routes.ts, checks every component route uses the map, fires every topic through the real createClient with each route's union and asserts a frame reaches exactly the routes that depend on it, that no route exceeds the 12-topic cap, and that patched topics never revalidate while the rest do. The cap stays at 12: stats leaves the home page and console leaves home and status (every console event is private, so it only ever triggered a revalidation).
  • Presence from the stream itself. Each /live connection is a tab; server/presence.ts keeps time-scored Redis sorted sets (presence:tabs, presence:online, presence:reading:<key>, map:countries, all with TTLs), counts inside a 90-second window (10 minutes for the map), publishes only on change, sweeps every 30 seconds and forgets the previous process's tabs on start. Patched topics carry their detail to everyone; the client overlays it on loader data, so counts tick without a revalidation.
  • "Editing now" comes from Hocuspocus awareness (onAwarenessUpdate, onDisconnect) and is published on editing:<post> when the set of handles changes.
  • View counters with the plan's rules: the client reports a view after 10 s visible and a quarter scrolled; the server refuses known bots and more than 30 reports a minute per (hashed) address, salts a daily random value kept in Redis only, hashes salt + address + agent into a visitor id that is never stored, and PFADDs it into a two-day HyperLogLog. Only a new register writes the views row, bumps the in-memory total and publishes views. The shown figure is the sum of daily uniques.
  • The visitor map on /status: a dot-matrix world rasterised once from Natural Earth's public-domain 110m countries (app/components/kit/map-data.ts, 96 columns, 174 centroids), lit from CF-IPCountry when the peer is in Cloudflare's ranges (or in a DEV_LOGIN run), fading ten minutes after the last tab, named with Intl.DisplayNames. /privacy gains entries for views, reading now and the map.
  • Sign everyone out, live. signOutEveryone publishes sessions; the live module pushes that frame to every member stream regardless of topics and ends the stream, the tab revalidates with no viewer and reconnects as a visitor, Hocuspocus closes every connection; a member removed by the sync gets the same by login. The admin action redirects the owner to /.
  • Reconnecting indicator: the live pulse in the header (n here) turns amber and reads "reconnecting" while the stream is down; on reopen the client revalidates once, since only recorded events replay.
  • Fun layer: online dots on member avatars (rail, people list, orbit, members page), "n reading now" on posts and projects, "Amr is editing" on a post, view counts on post cards, windows and both headers with the count-up, the map lighting up. All motion behind site.motion and prefers-reduced-motion.
  • Fixes: the post's about panel no longer nests an anchor in the window link (the hydration error on every post page); bounced in server/subscribers.ts returns its promise and server/sends.ts awaits it, so a failed publish reaches the job's error capture instead of an empty catch.

Migration

server/migrations/008-views.sql, additive:

CREATE TABLE views (
  kind TEXT NOT NULL CHECK (kind IN ('post', 'project')),
  key TEXT NOT NULL,
  day TEXT NOT NULL,
  views INTEGER NOT NULL DEFAULT 0,
  PRIMARY KEY (kind, key, day)
);

Redis keys added, each with a TTL: presence:* (2 min), map:countries (10 min), views:salt:<day> and views:<kind>:<key>:<day> (2 days), limit:views:<hash> (1 min). views rows are kept for ever (plan).

Checks

npm run typecheck, npx eslint ., npx prettier --check ., npm run build, npm run size (largest page routes/editor 147.7 KB, every page within 150 KB). Specs run: server/live-audit.test.ts, server/live.test.ts, server/presence.test.ts, server/views.test.ts, server/auth.test.ts, server/sends.test.ts, server/subscribers.test.ts (57 passing).

Exit proofs, measured locally

Against Redis on 6390 and the dev server on 3100, with Playwright tabs open; latency is from the write request leaving the sender to the DOM mutation on the open tab, same machine clock:

Event How Latency
Release on GitHub signed release webhook to /internal/github (GITHUB_WEBHOOK_SECRET) 229 ms
Deploy on the VPS signed report to /internal/deploys (DEPLOY_REPORT_SECRET) 232 ms
Member saves their profile editor tab removes a block; member page tab 526 ms
Owner reorders the home page home.sections set from the admin tab; home tab 562 ms
Sign everyone out admin confirm; a second signed-in tab shows the signed-out state 241 ms

A script fetched a published post 1,000 times and reported a view each time (1,000 × 204): the views row rose by exactly one for that visitor (the HyperLogLog counted 2: the script and the browser tab). The map lit Egypt, Germany and Japan from streams opened with CF-IPCountry; the pulse read "reconnecting" while the server was down and "live" again on restart; "Amr is editing" appeared when the editor opened and left when it closed; two tabs on a post read "2 reading now".

Dependencies

None added.

@github-actions github-actions Bot added the size/xl Over 600 changed lines, excluding lockfiles label Nov 7, 2026
@AmrMsCLL
AmrMsCLL merged commit 83fc583 into main Nov 7, 2026
11 checks passed
@AmrMsCLL
AmrMsCLL deleted the feat/live branch November 7, 2026 00:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/xl Over 600 changed lines, excluding lockfiles

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant