Add payload refactor plan
This commit is contained in:
285
TASKS.md
Normal file
285
TASKS.md
Normal file
@@ -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.
|
||||||
Reference in New Issue
Block a user