Skip to content

perf(notifications): one pulse poll, indexable inbox gates, one-statement roles (v1.18.0) - #103

Merged
krazymir merged 1 commit into
mainfrom
claude/codebase-performance-analysis-1aab11
Oct 21, 2026
Merged

krazymir merged 1 commit into
mainfrom
claude/codebase-performance-analysis-1aab11

Conversation

@krazymir

Copy link
Copy Markdown
Collaborator

What changed

Three fixes from a codebase performance audit (included as docs/performance-audit-2026-10-21.md). They cut the steady-state request and query load without changing what anyone sees. The spec change (INNOBOX_SPEC.md v0.19) was approved before implementation.

1. One pulse poll per tab (§12.2)

  • New GET /api/me/pulse returns { unreadCount, newChallenges, attention, banner }. attention is null for non-admins.
  • The shell polls it every 30 s only while the tab is visible. A hidden tab does not poll; a returning tab catches up at once if its last pulse is stale.
  • The bell no longer polls. Its list loads when opened and after mark-all-read.
  • GET /api/challenges/new-count and GET /api/admin/triage/attention are retired. The shell was their only caller.
  • The pulse is a background poller, so it stays off the presence allowlist.

2. Indexable inbox gates (§5, §12.1, migration 0051)

  • notifications gains challenge_id and solution_id. A trigger derives them from the payload target on insert and on payload update, and the migration backfills them once. Older link-only rows resolve from their /challenges/<n> and #SOL-<m> links.
  • The visibility gate, the trash filter, the delete cascade and mark-read-on-open now join by primary key. Before, they matched c.id::text against payload text, which scanned every challenge for every notification row on every 30 s poll.
  • The bell list returns its unread count from the same query via a window count, instead of running the gates twice.
  • One partial index (user_id, challenge_id) WHERE read_at IS NULL replaces the two per-type payload-expression indexes from 0026 and 0050.

3. Role resolution in one statement (§4.2, ENTRA_AUTH_SPEC "Role staleness")

  • The user row, the active and session check, the grants, the bootstrap group and the people access list come back from one SQL statement instead of 3–4 sequential ones.
  • getSessionUser is memoized per request only, keyed on Next's per-request headers object. Nothing is reused across requests, so the "no role cache" rule and invariant 1 are unchanged.
  • Bulk recipient resolution uses the same statement over an id list.

For reviewers

  • Link-only rows are gated slightly more strictly. Rows that carry only a deep link now resolve to their challenge, so the visibility gate applies to them too. Before, only the trash filter matched links. This errs in the safe direction.
  • The columns are derived, never written by callers. The trigger overwrites any supplied value, so no writer in web, worker or shared code had to change.
  • The worker digest's own role resolver is unchanged. Batching it is a separate audit finding.
  • The migration backfill is one UPDATE over notifications. It runs inside the migration transaction.

Testing

  • pnpm typecheck passes.
  • Unit tests pass: web 582, shared 511, worker 180. New tests cover the pulse helpers and the request memo.
  • DB suites pass on a throwaway database with all migrations: web 128, worker 18. New suites cover the one-statement resolver (one round trip, same grants as before, nothing cached), the derived columns and gates for every payload shape, and the pulse's scoping.
  • Not run locally: Playwright e2e, which runs in CI. The banner e2e waits on the pulse in a visible tab.

…ment roles (v1.18.0)

Cuts the steady-state request and query load without changing what anyone sees
(INNOBOX_SPEC.md v0.19: §4.2 Resolution, §5 notifications, §12.1, §12.2 pulse).

- One pulse per tab: GET /api/me/pulse returns the bell's unread count, the
  new-challenges count, the triage attention count (null for non-admins) and the
  system banner. The shell polls it every 30 s while the tab is visible and not at
  all while hidden; a returning tab catches up at once. The bell list loads on open.
  The new-count and triage/attention endpoints are retired.
- Indexable inbox gates: migration 0051 adds notifications.challenge_id and
  solution_id, derived from the payload target by a trigger and backfilled once.
  The visibility and trash gates, the delete cascade and mark-read-on-open now join
  by primary key instead of matching payload text against every challenge. One
  partial index replaces the two per-type payload indexes.
- Role resolution: the user row, active/session check, grants, bootstrap group and
  people access list come back from one SQL statement, memoized for one request
  only. No role cache across requests; invariant 1 unchanged.

Tests: unit tests for the pulse helpers and the request memo; DB tests for the
one-statement resolver, the derived columns and gates, and the pulse.
@krazymir
krazymir merged commit 24dc8ac into main Oct 21, 2026
14 checks passed
@krazymir
krazymir deleted the claude/codebase-performance-analysis-1aab11 branch October 21, 2026 08:31
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.

1 participant