Migrate active-peer to /events?peer= query param #16

Closed
opened 2026-05-16 22:22:42 +02:00 by arne · 1 comment
Owner

What to build

Implement ADR 0004. Replace the tab-ID handshake + POST mechanism with a query parameter on the /events SSE connection.

Server (internal/web/events.go):

  • handleEvents reads ?peer=<url> from the request, validates it, stores it on Subscriber.activePeer via the hub.
  • Delete mintTabID, subscriberRegistry, handleActivePeer, the POST /events/active-peer route registration, the synthesized downstream ready SSE frame, and the push-diag.events.subscribe / push-diag.active_peer.post log lines tied to this path.

Client (internal/web/static/live.js):

  • Open /events?peer=<url> where <url> comes from peerFromPath(location.pathname).
  • On path change (htmx:afterSwap, popstate, page resume): close the current EventSource and open a new one with the new peer.
  • Delete the tab-ready listener, the active-peer POST, the tabID state.

Tests (internal/web/events_test.go):

  • Update to use ?peer= in place of the POST.
  • Drop the tab-ID / registry tests.

Acceptance criteria

  • POST /events/active-peer route returns 404 (handler deleted)
  • GET /events?peer=https://example.com registers the subscriber with the correct active peer
  • Two browser tabs on the same session, each open to a different thread, each maintain their own correct active peer (verifiable via the existing watermark auto-advance test)
  • Auto-advance on inbound for the open thread still works
  • Push suppression on anyActive=true still works
  • Net deletion of >100 LOC across server + client

Blocked by

None - can start immediately

## What to build Implement [ADR 0004](docs/adr/0004-active-peer-via-events-query-param.md). Replace the tab-ID handshake + POST mechanism with a query parameter on the `/events` SSE connection. **Server (`internal/web/events.go`):** - `handleEvents` reads `?peer=<url>` from the request, validates it, stores it on `Subscriber.activePeer` via the hub. - Delete `mintTabID`, `subscriberRegistry`, `handleActivePeer`, the `POST /events/active-peer` route registration, the synthesized downstream `ready` SSE frame, and the `push-diag.events.subscribe` / `push-diag.active_peer.post` log lines tied to this path. **Client (`internal/web/static/live.js`):** - Open `/events?peer=<url>` where `<url>` comes from `peerFromPath(location.pathname)`. - On path change (htmx:afterSwap, popstate, page resume): close the current EventSource and open a new one with the new peer. - Delete the `tab-ready` listener, the active-peer POST, the `tabID` state. **Tests (`internal/web/events_test.go`):** - Update to use `?peer=` in place of the POST. - Drop the tab-ID / registry tests. ## Acceptance criteria - [ ] `POST /events/active-peer` route returns 404 (handler deleted) - [ ] `GET /events?peer=https://example.com` registers the subscriber with the correct active peer - [ ] Two browser tabs on the same session, each open to a different thread, each maintain their own correct active peer (verifiable via the existing watermark auto-advance test) - [ ] Auto-advance on inbound for the open thread still works - [ ] Push suppression on `anyActive=true` still works - [ ] Net deletion of >100 LOC across server + client ## Blocked by None - can start immediately
Author
Owner

This was generated by AI during triage.

Agent Brief

Category: enhancement
Summary: Replace the tab-ID + POST-active-peer mechanism with a peer query parameter on the /events SSE connection. Net deletion of ~150 LOC across server and client.

Current behavior:
Each browser tab opens a long-lived /events SSE stream. On stream open, the server mints a per-tab token (tabID) and emits a synthesized ready event carrying it. The browser stashes the tabID in JS memory, then issues POST /events/active-peer with {peer, tab_id} on every navigation (initial, htmx:afterSwap, popstate, page resume). The server keys a subscriber registry on (sessionID, tabID) to disambiguate two tabs sharing one session cookie, and uses it to route the SetActivePeer call to the right Subscriber atomic.

Desired behavior:
The browser opens /events?peer=<url> where <url> is derived from location.pathname (empty string when not on a thread). When the path changes — htmx swap, popstate, page resume — the bridge closes the current EventSource and opens a new one with the new peer. The server reads the query parameter on each subscribe, stores it on Subscriber.activePeer, and discards everything related to per-tab identity. Two tabs sharing a session each have their own /events connection and therefore their own Subscriber and activePeer — naturally distinguished, no tokens needed.

The bet captured in ADR 0004: posta-server's Last-Event-ID replay covers the ~50ms event-loss window during an active-peer change. If we observe lost events in practice, the fix is upstream, not by reintroducing tab-IDs.

Key behavioral details:

  • An inbox-or-non-thread view connects with peer= (empty). Server treats missing and empty identically: subscriber has empty active peer.
  • Validation: reuse the existing peer-URL validation helper (the one currently used by the POST handler that's being deleted). Don't write a parallel validator.
  • Auto-advance read-watermark on inbound for the active peer must continue to work — it reads Subscriber.activePeer, so the change is transparent once the field is populated correctly.
  • Push suppression must continue to work for the same reason.
  • tab-ready / ready synthesized SSE event is deleted from the wire. The /events HTTP response itself is the browser's signal that the stream is live.

What gets deleted:

Server side:

  • The POST /events/active-peer route registration and its handler.
  • The subscriber registry (the (sessionID, tabID)-keyed map and the Register / Deregister / SetActivePeer methods on it).
  • The tab-ID minting helper.
  • The synthesized downstream ready SSE frame in the events handler.
  • push-diag.events.subscribe and push-diag.active_peer.post log lines tied to this path (the broader push-diag cleanup is a separate issue, but these specific lines die naturally with the code).

Client side (in internal/web/static/live.js):

  • The tab-ready (currently ready) listener on the EventSource.
  • The active-peer fetch() to /events/active-peer.
  • The tabID module-level state.
  • The deferred-until-ready syncing logic — replaced by attaching the peer at connection-open time.

Tests:

  • The (sessionID, tabID) registry tests in events_test.go.
  • Tests that exercise the POST endpoint.
  • Add: a test that GET /events?peer=https://example.com results in a subscriber whose ActivePeer() returns the right URL.
  • Add: a test confirming two parallel /events connections with different ?peer= values each maintain their own active peer correctly.

Acceptance criteria:

  • POST /events/active-peer returns 404 (handler and route deleted)
  • GET /events?peer=https://example.com registers a subscriber whose active peer is https://example.com
  • GET /events?peer= registers a subscriber whose active peer is the empty string
  • Invalid peer URL in query → 400
  • Two browser tabs on the same session, one on /c/foo and one on /c/bar, each have their own correct active peer (verifiable via auto-advance behavior)
  • Existing tests for inbound → contact-changed + thread-updated translation still pass
  • Existing tests for read-watermark auto-advance still pass
  • Existing tests for push suppression on anyActive=true still pass
  • Net deletion is meaningful (>100 LOC across server + client + tests combined)

Out of scope:

  • The pure-function bridge refactor (separate, blocking issue follows this one)
  • The auto-flow bug (separate; if this issue's reconnect-on-nav happens to fix it, fine, but don't expand scope)
  • Push-diag log cleanup beyond the lines that die naturally with the deleted code
  • Any change to upstream posta-server protocol behavior
> *This was generated by AI during triage.* ## Agent Brief **Category:** enhancement **Summary:** Replace the tab-ID + POST-active-peer mechanism with a `peer` query parameter on the `/events` SSE connection. Net deletion of ~150 LOC across server and client. **Current behavior:** Each browser tab opens a long-lived `/events` SSE stream. On stream open, the server mints a per-tab token (`tabID`) and emits a synthesized `ready` event carrying it. The browser stashes the `tabID` in JS memory, then issues `POST /events/active-peer` with `{peer, tab_id}` on every navigation (initial, htmx:afterSwap, popstate, page resume). The server keys a subscriber registry on `(sessionID, tabID)` to disambiguate two tabs sharing one session cookie, and uses it to route the SetActivePeer call to the right `Subscriber` atomic. **Desired behavior:** The browser opens `/events?peer=<url>` where `<url>` is derived from `location.pathname` (empty string when not on a thread). When the path changes — htmx swap, popstate, page resume — the bridge closes the current `EventSource` and opens a new one with the new peer. The server reads the query parameter on each subscribe, stores it on `Subscriber.activePeer`, and discards everything related to per-tab identity. Two tabs sharing a session each have their own `/events` connection and therefore their own `Subscriber` and `activePeer` — naturally distinguished, no tokens needed. The bet captured in ADR 0004: posta-server's `Last-Event-ID` replay covers the ~50ms event-loss window during an active-peer change. If we observe lost events in practice, the fix is upstream, not by reintroducing tab-IDs. **Key behavioral details:** - An inbox-or-non-thread view connects with `peer=` (empty). Server treats missing and empty identically: subscriber has empty active peer. - Validation: reuse the existing peer-URL validation helper (the one currently used by the POST handler that's being deleted). Don't write a parallel validator. - Auto-advance read-watermark on inbound for the active peer must continue to work — it reads `Subscriber.activePeer`, so the change is transparent once the field is populated correctly. - Push suppression must continue to work for the same reason. - `tab-ready` / `ready` synthesized SSE event is deleted from the wire. The `/events` HTTP response itself is the browser's signal that the stream is live. **What gets deleted:** Server side: - The `POST /events/active-peer` route registration and its handler. - The subscriber registry (the `(sessionID, tabID)`-keyed map and the `Register` / `Deregister` / `SetActivePeer` methods on it). - The tab-ID minting helper. - The synthesized downstream `ready` SSE frame in the events handler. - `push-diag.events.subscribe` and `push-diag.active_peer.post` log lines tied to this path (the broader push-diag cleanup is a separate issue, but these specific lines die naturally with the code). Client side (in `internal/web/static/live.js`): - The `tab-ready` (currently `ready`) listener on the `EventSource`. - The active-peer `fetch()` to `/events/active-peer`. - The `tabID` module-level state. - The deferred-until-`ready` syncing logic — replaced by attaching the peer at connection-open time. Tests: - The `(sessionID, tabID)` registry tests in `events_test.go`. - Tests that exercise the POST endpoint. - Add: a test that `GET /events?peer=https://example.com` results in a subscriber whose `ActivePeer()` returns the right URL. - Add: a test confirming two parallel `/events` connections with different `?peer=` values each maintain their own active peer correctly. **Acceptance criteria:** - [ ] `POST /events/active-peer` returns 404 (handler and route deleted) - [ ] `GET /events?peer=https://example.com` registers a subscriber whose active peer is `https://example.com` - [ ] `GET /events?peer=` registers a subscriber whose active peer is the empty string - [ ] Invalid peer URL in query → 400 - [ ] Two browser tabs on the same session, one on `/c/foo` and one on `/c/bar`, each have their own correct active peer (verifiable via auto-advance behavior) - [ ] Existing tests for inbound → contact-changed + thread-updated translation still pass - [ ] Existing tests for read-watermark auto-advance still pass - [ ] Existing tests for push suppression on `anyActive=true` still pass - [ ] Net deletion is meaningful (>100 LOC across server + client + tests combined) **Out of scope:** - The pure-function bridge refactor (separate, blocking issue follows this one) - The auto-flow bug (separate; if this issue's reconnect-on-nav happens to fix it, fine, but don't expand scope) - Push-diag log cleanup beyond the lines that die naturally with the deleted code - Any change to upstream posta-server protocol behavior
arne closed this issue 2026-05-17 08:30:58 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
posta/chat#16
No description provided.