Skip to content

Mentions show up when they arrive: mentions and responses change domains (migration 0032) - #79

Merged
vgururao merged 1 commit into
blygger:mainfrom
aneeshsathe:fix/mentions-live
Oct 20, 2026
Merged

vgururao merged 1 commit into
blygger:mainfrom
aneeshsathe:fix/mentions-live

Conversation

@aneeshsathe

Copy link
Copy Markdown
Contributor

Mentions show up when they arrive: mentions and responses change domains (migration 0032)

Diagnosis

On the live blyg, mentions are delivered and verified quickly. Every recent inbound mention went from first_seen to verified_at in 1–4 s, and outbound sends succeed on the first attempt. Delivery is not the problem. What lags is the views.

  • Public pages (the real lag). PUBLIC_DOMAINS in src/html-cache.ts had no mentions domain ("Inbound mentions have no domain yet and still wait out the 60-second window"). The edge entry is max-age=60, stale-while-revalidate=300, so a newly verified response, or a hide or show, could take up to about 6 minutes to appear on the item's public page. The Studio even said so: "The public page can take about a minute to reflect a change here".
  • Studio (correct, but expensive). The brief assumed an open mentions view never refreshed. Tested on main (b6b2cd5), that is not the case. MentionsPage already calls usePoll on both lists. A new e2e test, written first and run against main, passed there: a verified mention showed up within one 15 s tick, and a resumed tab checked at once. But because inbound, outbound and mentionSources were not keyed on /api/changes, every tick re-read both lists in full plus one source card per row, even when nothing had changed.
  • No notification anywhere else. No badge or count outside the mentions tab tells the owner that something arrived. This PR does not add one; see Follow-ups.

Fix

Migration 0032_mentions_change_domain.sql adds two counters to change_state and six triggers, written in 0024's remote-safe shape (SELECT RAISE … WHERE, and every CASE closed inside parentheses):

Domain Moves on Read by
mentions any mentions_out insert, update or delete; a mentions_in change to a row whose status is, or was, verified/gone (the rows GET /mentions lists) Studio mentions lists, and (with reading) each mention's source card
responses a mentions_in change to a row that is, or was, verified and not hidden (exactly what listPublicResponses shows) public HTML cache key

There are two counters instead of one because the public condition has to be narrow. Anyone can POST a claim. If pending rows rolled the public key, a stranger could empty the edge cache at the receiver's rate cap. A pending claim, its failure and the daily prune of failed claims move neither counter. They still pay the one state-row write every tracked table pays, and they are still refused if the state row is missing.

  • src/change-state.ts: CHANGE_DOMAINS gains mentions and responses. openapi.json and the SDK were regenerated: ChangeState.domains derives from the list.
  • src/html-cache.ts: responses joins PUBLIC_DOMAINS, with a comment explaining the trade-off.
  • src/ui/data.ts: inbound and outbound read through cachedQuery(…, 'mentions'). mentionSources uses ['mentions', 'reading'], because a source card joins imported items and subscriptions, and both advance reading. readIfChanged now accepts several domains, labelled with their sum: counters only increase within an epoch, so the sum moves exactly when one of them does.
  • Resume handling: no change needed. Polling already stops while hidden and refreshes immediately on visibilitychange back to visible and on focus, for every domain. The new e2e test asserts that no reads happen while hidden and that a check runs at once on resume.
  • src/mentions/store.ts: pruneFailedInbound counts deleted rows with RETURNING id. D1's meta.changes now includes the trigger's state-row write (the oracle already documents that only its zero/nonzero meaning holds), which made the count double.
  • src/ui/catalog.tsx: the "about a minute … cached at the edge" note is removed.
  • scripts/reset-change-epoch.sql resets the new counters. docs/d1-polling-cache-operations.md documents the domains, the deploy order (0032 before the Worker) and the restore steps.

Tests

  • Worker oracle (test/polling-cache.oracle.test.ts, polling-cache-trigger-cases.ts): mentions_in and mentions_out are trigger families, which gives each one the insert/update/delete effect, missing-state rejection and every-column-in-guard checks. There are also 28 one-field branches, plus a new receiving witness that walks a claim's whole life (claim, fail, re-claim, verify, hide, re-claim while hidden, gone, show while pending, verify, prune). Each step's expected domains are checked against observed differences in GET /api/mentions and in the public page's responses section, not against the trigger text.
  • test/html-cache.test.ts: the public key moves on verify, hide and show. It does not move on a pending claim, its failure, a re-verification of a hidden row, or outbound queue work. The item page renders a new response and drops a hidden one.
  • test/migration-remote-shape.test.ts: 0032 passes.
  • UI (test-ui): mention lists and source cards re-read only when their revision moves. A two-domain query reloads on either domain and on no other.
  • e2e (e2e/polling-cache.spec.ts): with the Studio open on the mentions view, a fixture-only side channel in scripts/e2e-server.ts writes a verified mention (the Worker ships no such route). It appears in the open view within one poll. After that, a hidden tab makes no reads over 60 s, and an owner hide made in the meantime shows as soon as visibility returns.

Results (local, on the commit):

  • npm run typecheck: clean
  • npm test: 142 files, 1785 passed, 5 skipped
  • npm run test:ui: 12 files, 99 passed
  • npx playwright test (desktop + mobile): 337 passed, 1 skipped
  • npm run test:polling-cache:mutations: all 9 controls rejected at their checkpoints
  • npm run test:mutations (oracle): every control caught
  • Not run locally: auth-security mutations (~13 min, no auth code touched; CI runs it because migrations/ changed)

Follow-ups (not in this PR)

  • A mentions badge or unseen count in the tab bar would be the actual "notification". It needs a studio-private "seen" marker, so it is a design question.
  • The lineage-glyph extension reads verified mentions server-side under its own GET route. It is operator-compiled and not keyed on any domain.

🤖 Generated with Claude Code

Webmentions verify in seconds, but their views did not follow. Two
revision counters on change_state, with triggers on mentions_in and
mentions_out in 0024's remote-safe shape:

- mentions: any outbound change, and inbound changes to rows the Studio
  lists (verified or gone). The Studio's mention lists and source cards
  now read through the revision envelope instead of re-reading both lists
  and every source card on each 15 s tick.
- responses: inbound changes to rows the public responses list shows
  (verified, not hidden). It joins the public HTML cache key, so a new
  response or a hide/show reaches the item page at once. Pending claims
  and their failure move neither counter, so spam cannot churn the edge
  cache.

pruneFailedInbound counts deleted rows via RETURNING: D1's meta.changes
now includes the trigger's state-row write. The mentions view drops its
'about a minute' edge-cache note. Oracle families, field branches and a
status-transition receiving witness cover both domains; an e2e test
writes a verified mention through a fixture-only side channel and sees it
in the open view within one poll, and after a resume.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vgururao
vgururao merged commit eceb3a7 into blygger:main Oct 20, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants