Skip to main content

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​

  1. A transport failure after an idempotent send retries the same key within the retry budget.
  2. 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.
  3. Other projected mutations fail closed on an incomplete envelope; no hidden reads, no partial batch.
  4. 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).
  5. 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.