Files
RpgRoller/TASKS.md
2026-04-01 23:27:13 +02:00

288 lines
9.9 KiB
Markdown

# 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 most expensive transport pattern is:
- 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.