PURPOSE

Commit wrappers that orchestrate the full snapshot, lock, commit, describe, enqueue cycle for every reward source. These are the “commit” layer of commit-then-describe: each finalizer captures a before-snapshot, locks display overrides to before-state values, runs the domain commit, captures the after-snapshot, calls a pure describer to build a RewardBatch, and enqueues that batch (with dedupe via sourceKey and no-op batch rejection). Callers replace direct store mutations with these finalizers — each one is the single entry point for its reward source.

OWNS

  • The shape of the snapshot, lock, commit, describe, enqueue sequence per reward source.
  • Local snapshot helpers snapWallet and snapOwnership that read store state into plain WalletSnapshot and OwnershipSnapshot values.
  • The four exported finalizers: finalizeRunRewards, finalizeMissionRewards, finalizePullRewards, finalizeChestRewards.
  • The ordering rule that display overrides are locked BEFORE the domain commit so UI components see before-values during the theatrical reveal.
  • The reconstruction of before-ownership in pull (subtracting pullResponse.newShips from current ownership) because the server RPC has already committed inventory changes.
  • The decision to skip display-override locking for pull rewards (pull engine has its own custom reveal flow).

READS FROM

  • useWalletStore — for credits, gems, pullTickets.
  • useInventoryStore — for ownedShipIds().
  • PullResponse from ./pullService — to reconstruct before-ownership and pass through to the pull describer.
  • ResolvedReward from ../data/reward-cards — chest reward payloads provided by caller.
  • isNoOpBatch from ../data/reward-types — gate before enqueueing.
  • Pure describers from ./reward-describers: describeRunRewards, describeMissionRewards, describePullRewards, describeChestRewards, plus the WalletSnapshot, OwnershipSnapshot, MissionCollectReceipt, RunRewardsSummary types.

PUSHES TO

  • useDisplayOverrideStore.lockForBatch(batch) — locks display values to the before-snapshot prior to the reveal (skipped for pull).
  • useRewardQueueStore.enqueue(batch) — enqueues the described batch for the reveal pipeline.
  • For mission and chest sources, the caller-supplied commitFn runs inside the finalizer to perform the actual wallet/inventory mutations between snapshots.

DOES NOT

  • Does not mutate wallet, inventory, or any domain store directly. Mutations happen either inside the caller-supplied commitFn (mission, chest) or already happened server-side before the finalizer is called (arcade run via finalize_run RPC, pull via server RPC).
  • Does not contain reward-shaping logic. All RewardBatch construction lives in the pure describers.
  • Does not lock display overrides for pull — pull has revealMode:'custom'.
  • Does not enqueue no-op batches (filtered via isNoOpBatch).
  • Does not return values. All four finalizers return void; their effect is the queued batch and any locked display state.
  • Does not own dedupe — sourceKey dedupe is enforced inside useRewardQueueStore.enqueue.

Signals

  • _runKills parameter on finalizeRunRewards is currently unused (leading underscore).
  • finalizeRunRewards takes the after-snapshot immediately after the before-snapshot — the finalize_run RPC has already applied wallet changes server-side before this function runs, so no local commit step exists between them.
  • finalizePullRewards constructs the before-OwnershipSnapshot by cloning current ownership and removing every id in pullResponse.newShips. Wallet snapshot for pull is taken once (post-commit) since pull does not produce a wallet diff in the batch.
  • finalizeChestRewards accepts a hardcoded source: 'journey_chest' literal — the parameter exists for future chest sources but the type is currently a single literal.
  • Mission and chest finalizers capture the before-wallet snapshot, then invoke commitFn(), then capture after-wallet — the commit happens between snapshots.
  • Mission finalizer also captures beforeOwnership so the describer can detect new-vs-dupe ship grants from the receipt.

Entry points

  • finalizeRunRewards(rewards: RunRewardsSummary, _runKills: number, runId: string): void — arcade run end.
  • finalizeMissionRewards(receipt: MissionCollectReceipt, commitFn: () => void): void — mission collect.
  • finalizePullRewards(pullResponse: PullResponse, txnId: string): void — gacha pull.
  • finalizeChestRewards(chestRewards: ResolvedReward[], chestId: string, source: 'journey_chest', commitFn: () => void): void — journey chest open.

Pattern notes

  • Commit-then-describe split: the finalizers are the only impure step (store reads, store writes, RPC-dependent ordering). All description (turning before/after snapshots into a RewardBatch) is delegated to pure functions in reward-describers.ts, which makes the describer trivially testable without store mocks.
  • Display-override locking is deliberately ordered: lock fires AFTER describe but BEFORE enqueue. Locking with lockForBatch(batch) consumes the batch’s before-values; enqueuing then drives the reveal which eventually clears the lock.
  • The caller-supplied commitFn pattern (mission, chest) keeps domain mutation logic at the call site while still routing through the finalizer for snapshot ordering. The finalizer never reaches into domain stores to perform the mutation itself.
  • Pull is the asymmetric source: server-authoritative commit means the function reconstructs before-state from the response payload rather than snapshotting before a local commit.
  • isNoOpBatch rejection is the last gate before enqueue across all four sources, so a finalizer call that produces no actual reward change is a silent no-op (no enqueue, no display lock).
  • Snapshot helpers return fresh plain objects (new Set for ownership) so describers can compare references safely without store-state aliasing.