diff --git a/TASKS.md b/TASKS.md index eeaa7a4..e69de29 100644 --- a/TASKS.md +++ b/TASKS.md @@ -1,287 +0,0 @@ -# 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.