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
snapWalletandsnapOwnershipthat read store state into plainWalletSnapshotandOwnershipSnapshotvalues. - 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.newShipsfrom 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— forcredits,gems,pullTickets.useInventoryStore— forownedShipIds().PullResponsefrom./pullService— to reconstruct before-ownership and pass through to the pull describer.ResolvedRewardfrom../data/reward-cards— chest reward payloads provided by caller.isNoOpBatchfrom../data/reward-types— gate before enqueueing.- Pure describers from
./reward-describers:describeRunRewards,describeMissionRewards,describePullRewards,describeChestRewards, plus theWalletSnapshot,OwnershipSnapshot,MissionCollectReceipt,RunRewardsSummarytypes.
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
commitFnruns 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 viafinalize_runRPC, pull via server RPC). - Does not contain reward-shaping logic. All
RewardBatchconstruction 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 —
sourceKeydedupe is enforced insideuseRewardQueueStore.enqueue.
Signals
_runKillsparameter onfinalizeRunRewardsis currently unused (leading underscore).finalizeRunRewardstakes the after-snapshot immediately after the before-snapshot — thefinalize_runRPC has already applied wallet changes server-side before this function runs, so no local commit step exists between them.finalizePullRewardsconstructs the before-OwnershipSnapshotby cloning current ownership and removing every id inpullResponse.newShips. Wallet snapshot for pull is taken once (post-commit) since pull does not produce a wallet diff in the batch.finalizeChestRewardsaccepts a hardcodedsource: '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
beforeOwnershipso 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 inreward-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
commitFnpattern (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.
isNoOpBatchrejection 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 Setfor ownership) so describers can compare references safely without store-state aliasing.