From c935578cf66e530fcf2cf58161330b0a6e263803 Mon Sep 17 00:00:00 2001 From: Frank Tovar Date: Wed, 1 Apr 2026 23:19:20 +0200 Subject: [PATCH] Add payload refactor plan --- TASKS.md | 285 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 285 insertions(+) create mode 100644 TASKS.md diff --git a/TASKS.md b/TASKS.md new file mode 100644 index 0000000..5d2575d --- /dev/null +++ b/TASKS.md @@ -0,0 +1,285 @@ +# Payload And Serialization Refactor Plan + +## Objective + +Reduce the risk of future Blazor Server circuit disconnects by shrinking payloads, removing unnecessary serialization hops, and making live refreshes more granular. + +The current payload split fixed the immediate failure, but it did not remove the most expensive transport pattern: + +- browser `fetch` +- JSON parse in JavaScript +- JS interop result marshalled over the Blazor circuit +- JSON deserialization in .NET + +That path means every read model still competes with the SignalR hub message ceiling and pays serialization cost twice. + +## Current Baseline + +- `GET /api/campaigns`: about `222 B` +- `GET /api/campaigns/{id}`: about `1.0 KB` +- `GET /api/characters/{id}/sheet`: about `11.3 KB` +- `GET /api/campaigns/{id}/log`: about `13.8 KB` +- Workspace refresh currently reloads roster, selected character sheet, and log together when the state SSE reports a version change. +- The API client still uses JS interop for all reads and writes through `rpgRollerApi.request`. + +## Target Outcomes + +- Keep normal interactive responses well below the default Blazor circuit limit without depending on hub-size increases. +- Eliminate double JSON handling for the workspace read path. +- Avoid retransmitting unchanged roster, sheet, and log data after every state change. +- Establish payload and allocation guardrails so regressions are detected in tests. + +## Recommended Delivery Order + +1. Remove JS interop from workspace API reads. +2. Split live refresh into change-specific reloads. +3. Make campaign log loading incremental instead of retransmitting the latest 100 entries. +4. Trim DTO shape and serialization overhead. +5. Add measurement, tests, and payload budgets. + +## Phase 1: Remove The JS Interop API Bottleneck + +### Goal + +Move workspace data reads off the `fetch -> JS -> SignalR -> .NET` path. + +### Recommendation + +Introduce a server-side workspace query facade and call it directly from Blazor components instead of routing workspace reads through `RpgRollerApiClient`. + +### Why This First + +- Highest impact on serialization overhead. +- Removes the hub-size ceiling from normal workspace query results. +- Simplifies error handling and reduces duplicate parsing logic. + +### Implementation Tasks + +- Add a scoped server-side query service for the authenticated workspace. +- Resolve the session token from the current `HttpContext` or a dedicated session abstraction. +- Move these read flows from `RpgRollerApiClient` to the query service: +- `GetMe` +- `GetCampaigns` +- `GetCharacterCampaignOptions` +- `GetCampaign` +- `GetCharacterSheet` +- `GetCampaignLog` +- Keep browser JS interop only for browser-only concerns: +- session storage +- SSE wiring +- DOM scrolling helpers +- Keep HTTP API endpoints for external callers and integration tests. +- Leave mutation endpoints in place initially, then decide whether mutations should also move server-side or stay as HTTP calls. + +### File Areas + +- `RpgRoller/Components/RpgRollerApiClient.cs` +- `RpgRoller/Components/Pages/Workspace.razor.cs` +- `RpgRoller/Api/SessionTokenHttpContextExtensions.cs` +- new workspace query service under `RpgRoller/Components` or `RpgRoller/Services` + +### Acceptance Criteria + +- Workspace reads no longer call `rpgRollerApi.request`. +- Opening play and management screens does not depend on JS interop payload size. +- Existing API integration tests still pass. + +## Phase 2: Replace Full Scope Refreshes With Targeted Refreshes + +### Goal + +Stop reloading roster, selected sheet, and log together for every state change. + +### Recommendation + +Replace the single campaign version event with typed change notifications or multiple independent versions. + +### Options + +- Preferred: emit typed SSE events such as `roster-changed`, `character-sheet-changed`, and `log-appended`. +- Acceptable: keep one event stream but include separate version counters for roster, character state, and log state. + +### Implementation Tasks + +- Extend the server-side state event model to expose change categories. +- Update mutation paths in `GameService` to mark the relevant change areas. +- Update `Workspace.razor.cs` so the handler refreshes only the affected slice: +- roster changes reload `CampaignRoster` +- skill and group changes reload `CharacterSheet` +- roll events append or refresh only the log +- avoid reloading the selected character sheet when another character changes +- avoid reloading the log when only roster metadata changes + +### File Areas + +- `RpgRoller/Api/StateEventEndpoints.cs` +- `RpgRoller/Services/GameService.cs` +- `RpgRoller/Components/Pages/Workspace.razor.cs` +- `RpgRoller/wwwroot/js/rpgroller-api.js` + +### Acceptance Criteria + +- A new roll does not trigger a roster reload. +- Renaming a character does not trigger a log reload unless log labels depend on that mutation. +- State refresh traffic is materially lower in browser and server traces. + +## Phase 3: Make Campaign Log Loading Incremental + +### Goal + +Stop retransmitting the same log entries after every roll. + +### Recommendation + +Add incremental log APIs and append on the client. + +### Implementation Tasks + +- Add query parameters such as: +- `afterRollId` +- `sinceTimestamp` +- `limit` +- retain an initial bounded load for first render +- add an incremental mode for live updates +- keep server ordering stable and deterministic +- update the workspace to append new entries instead of replacing the whole log +- trim old entries client-side to a fixed window +- preserve the visibility rules for GM, owner, and observers + +### Contract Changes + +- Introduce a dedicated log page result: +- entries +- cursor or last seen roll id +- optional `hasMore` + +### Acceptance Criteria + +- A new roll causes only the new log entry or entries to cross the wire. +- Reconnect can rebuild log state without downloading unnecessary history. +- Existing visibility behavior remains unchanged. + +## Phase 4: Split Log Summary From Log Detail + +### Goal + +Reduce the size of the hottest payload even further. + +### Recommendation + +Do not send full dice arrays and long breakdown strings for every log row by default. + +### Implementation Tasks + +- Introduce `CampaignLogListEntry` for the list view. +- Keep only fields needed to render the collapsed row: +- roll id +- roller label +- skill label +- character label +- result +- visibility +- timestamp +- compact summary text +- add `GET /api/rolls/{rollId}` or equivalent detail lookup for expanded inspection +- update the log UI to lazy-load detail when a row is expanded + +### Expected Benefit + +This should cut the log list payload materially because `Dice` and `Breakdown` are currently repeated for every row and are the least compressible fields in the list. + +## Phase 5: Trim DTO Shape To Match The View + +### Goal + +Remove repeated fields that are not needed by the consuming UI. + +### Recommendations + +- Replace `CampaignSummary.Gm` and `CampaignRoster.Gm` full `UserSummary` usage with a slimmer campaign GM DTO if the UI only needs `Id` and `DisplayName`. +- Remove parent-scope identifiers from child records where the endpoint already provides that scope. +- candidate examples: +- `CampaignLogEntry.CampaignId` +- `SkillSummary.CharacterId` inside `CharacterSheet` +- `SkillGroupSummary.CharacterId` inside `CharacterSheet` +- review whether owner ids are needed in all list views or whether some can be replaced with display labels and booleans + +### Guardrail + +Do not over-optimize DTOs until the consuming components have been made explicit. Only remove a field after all consumers are verified. + +## Phase 6: Reduce Serializer CPU And Allocation Overhead + +### Goal + +Lower per-request CPU and allocation cost after the major transport fixes are in place. + +### Recommendations + +- Introduce source-generated `System.Text.Json` contexts for the hot contracts. +- Reuse serializer options consistently rather than relying on repeated default metadata discovery. +- Review whether any list contracts can be exposed as arrays end-to-end to reduce intermediate allocations. +- If HTTP remains in the path for some calls, ensure response compression is enabled for normal API responses to reduce browser transfer cost. + +### Note + +This phase is worthwhile, but it should follow the transport refactor. Serializer tuning alone will not solve circuit-size problems. + +## Phase 7: Add Payload Budgets And Regression Tests + +### Goal + +Prevent a future regression from silently reintroducing oversized read models. + +### Implementation Tasks + +- Add integration tests that serialize representative contracts and assert upper bounds. +- Add service or API tests for log pagination and incremental fetch semantics. +- Add workspace tests for targeted refresh behavior. +- Add a small benchmark or diagnostic test for hot payload serialization if practical. +- Document soft payload budgets for any remaining JS interop responses. + +### Suggested Budgets + +- Any remaining JS interop response: prefer under `16 KB` +- initial character sheet response: target under `12 KB` +- initial log list response: target under `8 KB` after summary/detail split +- incremental live update response: target under `2 KB` + +## Delivery Notes + +- Do not raise the Blazor hub message limit again as the primary fix. +- Keep the existing HTTP API stable where possible so tests and external tooling do not break. +- Prefer introducing new, view-specific contracts instead of reusing broad aggregate models. +- Measure payload size with representative admin and non-admin datasets after each phase. + +## Proposed Milestones + +### Milestone A + +Move workspace reads off JS interop and keep behavior unchanged. + +### Milestone B + +Introduce targeted SSE-driven refreshes without yet changing log contract shape. + +### Milestone C + +Add incremental log loading and client append behavior. + +### Milestone D + +Split log summary from log detail and trim DTOs. + +### Milestone E + +Add serializer optimizations, payload budget tests, and final documentation updates. + +## Definition Of Done + +- Workspace read models are no longer limited by Blazor JS interop payload size. +- Live updates no longer reload unrelated slices. +- The campaign log is loaded incrementally. +- The hottest contracts are explicitly sized for their views. +- Payload budgets are enforced by tests. +- The default Blazor hub receive limit remains unchanged.