Mutation projections
The SDK applies server-owned state from the same response that confirms a state-changing mutation. A normal successful mutation never invalidates local state and re-reads each capability.
Normal flow
client sends intent + idempotency key
-> server validates and commits the mutation atomically
-> server stores a bounded replay receipt and post-commit projections
-> one response returns the result + every affected full snapshot
-> SDK validates every projection and the required capability set
-> SDK applies all accepted slots in one state-store batch
-> one ordered publication; zero follow-up reads
The server owns wallet, inventory, stamina, progression, rewards, pity, mail, engagement and mission values. Receipt deltas are display data only. Since client contract 5.0 (server v3.1.0), mutation results no longer carry wallet, inventory or stamina: read them from the read-model cache, which the projections update.
Wire contract
{
"schema": 1,
"mutation_id": "txn-4f3...",
"replayed": false,
"result": { "receipt": {} },
"projections": [
{
"capability": "economy",
"schema": 1,
"state_revision": 42,
"snapshot": { "revision": 42, "balances": [] }
}
]
}
projections holds complete post-commit snapshots for every capability the
operation's policy requires, even when a value did not change.
state_revision is monotonic and server-owned; a replay returns the original
projections.
The SDK rejects unknown or duplicate capabilities, malformed snapshots,
unsupported schemas, invalid revisions and non-canonical ordering. Before
anything is applied, a missing required capability fails as
PROJECTV_AUTHORITATIVE_PROJECTION_INCOMPLETE and a missing state slot as
PROJECTV_AUTHORITATIVE_STATE_REQUIRED_UNBOUND. A valid older revision is
ignored as stale.
Recovery
- A transport failure after an idempotent send retries the same key within the retry budget.
- Shop purchase is the only capability-owned exception: a success response missing a projection triggers one coalesced same-key reconciliation of its capability set. The purchase stays a failure until that batch is applied.
- Other projected mutations fail closed on an incomplete envelope; no hidden reads, no partial batch.
- An ambiguous non-idempotent mutation is not replayed. Its slots are marked
for reconciliation and the caller gets a typed recovery result (presenters
surface it as
UnknownOutcome/CanSync). - Concurrent reconciliations share one key; the marker clears only after a fresh authoritative batch is applied.
Targets: normal mutation and persisted replay are one RPC with zero follow-up reads; a missing projection costs one bounded reconciliation; one coherent publication per mutation; no client arithmetic for server state.
Mission settlement
Mission runs use player mutations (mission/admit, mission/activate,
mission/complete, mission/cancel) in the same envelope. mission/complete
settles the run; its result carries the granted rewards, mailed rewards and
the server-scored stars, which the gameplay presenter exposes as
ProjectVGameplayDataResult. The state Clients render afterwards always comes
from server data, never from that reward list. A win claims no drop slots
because the server grants every admitted drop.
Level-up
player/level_up (server v3.2.0, ADR 0009) spends EXP items, adds their XP
and takes every affordable level in one Core receipt, so the response projects
characters, inventory, economy and progression together. The SDK keeps the
request in the progression journal only to resend it with the same request ID
after a lost response; the server replays the original projections. Its result
replaced use_character_xp_items plus one upgrade_character per level.
Player sync
player/sync returns the same projections array as a mutation, one entry per
syncable capability, without a mutation_id. ProjectVPlayerSyncMapper maps
it from the generated DTO: it skips capabilities with state_revision 0 (no
stored state yet; their reads stay the source of truth), orders the rest by
capability, requires schema 1 and a snapshot, and rejects duplicates. Economy,
inventory and stamina are mapped to typed snapshots whose revision must match
state_revision; other capabilities keep their original JSON for their slot.
The batch is applied with a synthetic player-sync-<guid> mutation ID through
the same projection sink as mutations.
Optional projections
Typed slots exist for the capabilities presenters render. Other reviewed
projections go to bounded opaque slots; unbound optional projections are
reported through ProjectVStateStore.AuthoritativeProjectionApplied instead
of being dropped silently.