Repository navigation
perf(web): run the /lore list, plan-usage and invites reads in parallel - #712
Conversation
The three reads were Next.js server actions, which the app router runs one at a time per client and which cannot take an AbortSignal. On /lore they queued (plan usage -> pending invites -> memory list) on every load and every window focus, so the TopBar activity bar stayed on long after the visible panels had loaded. - useMemories calls POST /memories/list from the browser with React Query's signal (body building moved unchanged into the pure lib/memory-list.ts) - useMemoryTotal and usePendingInvitesForMe read new GET route handlers (/api/plan/usage, /api/invites/pending) that wrap the existing server functions Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com>
…r-action-reads rule - parallel-reads.spec.ts: useMemories, useMemoryTotal and usePendingInvitesForMe issue ordinary requests with React Query's signal and never call a server action (the action modules are mocked to throw) - read-routes.spec.ts: the two GET route handlers keep the wrapped functions' contract (null / [] when signed out) and are never cached - memory-list.spec.ts pins the POST /memories/list body - packages/web/CLAUDE.md, docs/decisions.md, CLAUDE.md: a client read is never a server action; docs/key-files.md and docs/storybook.md updated - LorePage story now renders the populated list (baseline needs refresh) Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com>
|
The dashboard preview for this PR — redeployed on each push that changes the web app.
Preview always points at this PR's latest commit; Deployment is the immutable build of |
⚪ 1 non-blocking findingMoves the /lore list, plan-usage and invites reads off server actions. Since the last review only CI's baseline commit landed (44 PNGs, no code); one doc gap on the plan-usage backlog claim. Warnings: Says only LorePage Default baseline moves; 659361a rewrote 44 PNGs (Empty + 42 untouched stories, 3 clash with #708) 1 advisory finding below the confidence bar — see Less certain below. Progress: open review threads 2 → 0 → 1 → 0 → 1 across the last 5 reviews
Less certain (1) — advisory, below the confidence bar
Review details
Memories used
Nothing to report — standards (2 docs), measurability (11 paths classified), integrations (not activated), 0 files skipped.
|
Addresses the pr-reviewer comment: #712 (comment) Refs: #712 Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com>
…b CLAUDE.md Addresses the pr-reviewer comment: #712 (comment) Refs: #712 Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com>
Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com>
…appended Addresses the pr-reviewer report finding: #712 (comment) Refs: #712 Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com>
…ed page landed Addresses the pr-reviewer comment: #712 (comment) Refs: #712 Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com>
|
/update-baselines |
Regenerated by the Update visual baselines workflow for PR #712.
|
✅ Update visual baselines: committed 44 updated baseline(s) as ℹ️ This commit was pushed with |
The Update visual baselines workflow pushed 659361a with GITHUB_TOKEN, which does not trigger CI. This empty commit runs ci.yml on the new baselines. Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com>
| * read as JSON. | ||
| * | ||
| * This exists for the reads that still need server-side code: there is no REST | ||
| * endpoint for them yet (`packages/web/CLAUDE.md` lists invites and plan usage |
There was a problem hiding this comment.
suggestion (low): ⚪ Plan usage missing from the backlog list it cites (non-blocking)
This says packages/web/CLAUDE.md lists plan usage as backlog, but that list (line 82) names only orgs, invites, tokens and audit log, so /api/plan/usage's direct RPC has no exemption.
Evidence: packages/web/CLAUDE.md:82 (backlog list) · docs/decisions.md:863 (same claim)
(sign-in, session, password), and the server actions covering surfaces the REST
API does not expose yet (orgs, invites, plan usage, tokens, audit log). Those server actionspr-reviewer · commit 659361a
…is backlog Addresses the pr-reviewer comment: #712 (comment) Refs: #713 Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com>
… and the CLI (#713) * perf(web): run the /lore list, plan-usage and invites reads in parallel The three reads were Next.js server actions, which the app router runs one at a time per client and which cannot take an AbortSignal. On /lore they queued (plan usage -> pending invites -> memory list) on every load and every window focus, so the TopBar activity bar stayed on long after the visible panels had loaded. - useMemories calls POST /memories/list from the browser with React Query's signal (body building moved unchanged into the pure lib/memory-list.ts) - useMemoryTotal and usePendingInvitesForMe read new GET route handlers (/api/plan/usage, /api/invites/pending) that wrap the existing server functions Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com> * test(web): pin parallel, abortable /lore reads; document the no-server-action-reads rule - parallel-reads.spec.ts: useMemories, useMemoryTotal and usePendingInvitesForMe issue ordinary requests with React Query's signal and never call a server action (the action modules are mocked to throw) - read-routes.spec.ts: the two GET route handlers keep the wrapped functions' contract (null / [] when signed out) and are never cached - memory-list.spec.ts pins the POST /memories/list body - packages/web/CLAUDE.md, docs/decisions.md, CLAUDE.md: a client read is never a server action; docs/key-files.md and docs/storybook.md updated - LorePage story now renders the populated list (baseline needs refresh) Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com> * feat: memory.quota — your memories against your plan cap on REST, MCP and the CLI GET /memories/quota, the memory.quota MCP tool and `lorekit quota` all answer { plan, personal: { count, limit, remaining }, org_count, total } from the actor-guarded lorekit_memory_count RPC. personal is the pair the cap trigger enforces; org-owned memories count against each org's own cap, so they sit beside it instead of being subtracted (the RPC's combined count measured against the personal limit made 'remaining' wrong for both). The dashboard reads it over REST too: the TopBar badge (total) from the browser with React Query's signal, Settings > Plan (personal pair) on the server. The interim /api/plan/usage route and the direct-RPC getPlanUsage server action are removed. Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com> * test: pin memory.quota in the permission, local-MCP and usage-stats guards; cover lorekit quota Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com> * address review comment: badge comment no longer claims it matches the plan page Addresses the pr-reviewer comment: #713 (comment) Refs: #713 Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com> * address review comment: list quota in the MCP, CLI and route references Addresses the pr-reviewer comment: #713 (comment) Refs: #713 Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com> * address review comment: log a failed quota read on the plan page Addresses the pr-reviewer comment: #713 (comment) Refs: #713 Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com> * address review comment: app-route header no longer claims plan usage is backlog Addresses the pr-reviewer comment: #712 (comment) Refs: #713 Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com> * chore: simplify pass (mechanical refactors) Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com> * fix(quota): narrow org_count and total to the key's reachable orgs lorekit_memory_count counts every org the token owner belongs to, so a personal-only or selected-orgs key could read how many memories its owner's other orgs hold. GET /memories/quota and memory.quota now pass the calling key to quotaForKey, which recounts org_count over effectiveOrgIds (0 under personal, only org_ids under selected) and recomputes total. The personal pair is unchanged: the cap is account-wide. Refs: #713 (comment) Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com> * docs(quota): say what a tenancy-narrowed token sees from memory.quota docs/api-tokens.md gains a quota row in "What a scoped token sees". The llms.txt template no longer claims every operation without a scope is refused for a scoped token: only the account-wide sweeps are, and quota is answered with its org half narrowed. The catalog `returns`, the response schema, and the MCP, CLI and limits docs say the same; llms.txt and the edge schema mirror are regenerated. Refs: #713 (comment) Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com> * address review comment: OpenAPI quota description names the token narrowing Addresses the pr-reviewer comment: #713 (comment) Refs: #713 Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com> * chore: simplify pass (mechanical refactors) Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com> --------- Co-authored-by: mads.thines <[email protected]> Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com>
Summary
On
/lorethe TopBar activity bar could stay on for seconds after everything on screen had loaded. Three reads were Next.js server actions: the memory badge, the pending-invites query and the Explorer list. The app router runs server actions one at a time per client, and they can't take anAbortSignal, so on every load and every window focus they queued in a fixed order: plan usage → invites → list. This PR moves all three off server actions so they run in parallel with the other reads and can be cancelled.Changes
useMemoriesnow callsPOST /memories/listfrom the browser (fetchMemoryPage) and passes React Query's signal, the same way the scope tree and facets already work. The body building moved unchanged from thelistMemoriesaction into the purelib/memory-list.ts, and the action was removed.useMemoryTotalandusePendingInvitesForMenow read new GET route handlers,/api/plan/usageand/api/invites/pending. These wrap the existinggetPlanUsage/listPendingInvitesForMewith the same contract (null/[]when signed out). They have no REST endpoint yet, so a route handler is the step that removes the queue without a schema/handler/OpenAPI change. The browser reads them throughgetAppRoute(path, signal)(lib/app-route.ts). The server-rendered pages still call the functions directly.retryUnlessSignedOut) and, when nothing is loaded yet, shows the Explorer's existing "Failed to load memories" state. A failed "Load more" keeps the loaded rows and offers "Try again" beside the button. Before, the action failed closed to an empty page, so an outage read as "No memories".lore.results_pagedis now sent only when the requested page actually lands, so a failed attempt, its retry, or a page cancelled by a filter change mid-load no longer counts as an appended page.parallel-reads.spec.tschecks that each hook sends an ordinary request with the signal and never calls a server action. The action modules are mocked to throw, and all 9 cases fail againstmain.read-routes.spec.tschecks the route contracts andno-store.memory-list.spec.tspins the list body.packages/web/CLAUDE.md("A client read is never a server action") plus the matchingdocs/decisions.mdentry andCLAUDE.mdkey-decision line.docs/key-files.mdanddocs/storybook.mdare updated.User-facing docs /
llms.txt: none apply. Nothing a user can do or configure changed; only the transport behind three dashboard reads did.Context
Measured from the
webservice's RUM and server spans (2026-10-20, www.lorekit.io):POST /lorestarted the millisecond the previous one ended. Server spans show the order never changed:lorekit.plan.get_usage→org_invitesread →memories/list.auth/v1/usercall inside the list action, plus a focus refetch queued behind it, kept the bar on for about 85 s after the visible data had loaded.Needs you
/update-baselines(it only runs for a maintainer's comment). ThePages/Lore Explorer→Defaultstory now renders the populated list from the MSW fixtures instead of "No memories in this scope", soLorePage.stories.tsx/Default-1-chromium-linux.pngchanges (960×720 → 960×1793).Emptyis unchanged. The baseline commit is pushed with the workflow token, so re-run CI (or push a commit) afterwards to turn the Storybook job andCI Summarygreen.packages/web/CLAUDE.md). Four other client surfaces still read through actions, off the/lorerefresh path:useAuditLog,LessonDetailSheet,OrganizationManagerandInviteDetailsDialog(listed inpackages/web/CLAUDE.md).Verification
npx vitest run --root packages/web: 116 files, 1647 tests pass.npx tsc --noEmit -p packages/web/tsconfig.json: clean.eslinton the changed files: 0 errors (one existing warning inmocks/memories.ts).Defaultbaseline above.