@@ -44,7 +44,13 @@ impl GameService {
4444 let end = start
4545 . saturating_add ( query. limit as usize )
4646 . min ( record. events . len ( ) ) ;
47- let events = project ( & record, & rebuilt, viewer, start..end, & mut ReplayBudget :: default ( ) ) ?;
47+ let events = project (
48+ & record,
49+ & rebuilt,
50+ viewer,
51+ start..end,
52+ & mut ReplayBudget :: default ( ) ,
53+ ) ?;
4854 Ok ( EventPage {
4955 match_id : id. into ( ) ,
5056 revision : record. events . len ( ) as u64 ,
@@ -99,85 +105,105 @@ fn project_action(
99105}
100106
101107/// Bound the full bundle while paginated events remain available for large histories.
102- pub ( crate ) struct ReplayBudget { remaining : usize }
108+ pub ( crate ) struct ReplayBudget {
109+ remaining : usize ,
110+ }
103111
104112impl Default for ReplayBudget {
105- fn default ( ) -> Self { Self { remaining : 16 * 1024 * 1024 - 4096 } }
113+ fn default ( ) -> Self {
114+ Self {
115+ remaining : 16 * 1024 * 1024 - 4096 ,
116+ }
117+ }
106118}
107119
108120impl ReplayBudget {
109121 pub ( crate ) fn include ( & mut self , value : & impl serde:: Serialize ) -> Result < ( ) , ApiError > {
110- let size = serde_json:: to_vec ( value) . map_err ( |e| error:: engine ( e. into ( ) ) ) ?. len ( ) . saturating_add ( 2 ) ;
111- self . remaining = self . remaining . checked_sub ( size) . ok_or_else ( || ApiError :: new (
112- "REPLAY_TOO_LARGE" , "Replay exceeds the 16 MiB response budget" ,
113- "Read paginated /events to export this history in bounded chunks." ,
114- ) ) ?;
122+ let size = serde_json:: to_vec ( value)
123+ . map_err ( |e| error:: engine ( e. into ( ) ) ) ?
124+ . len ( )
125+ . saturating_add ( 2 ) ;
126+ self . remaining = self . remaining . checked_sub ( size) . ok_or_else ( || {
127+ ApiError :: new (
128+ "REPLAY_TOO_LARGE" ,
129+ "Replay exceeds the 16 MiB response budget" ,
130+ "Read paginated /events to export this history in bounded chunks." ,
131+ )
132+ } ) ?;
115133 Ok ( ( ) )
116134 }
117135}
118136
119137pub ( crate ) fn project (
120- record : & MatchRecord , rebuilt : & replay:: Reconstructed , viewer : Viewer ,
121- range : std:: ops:: Range < usize > , budget : & mut ReplayBudget ,
138+ record : & MatchRecord ,
139+ rebuilt : & replay:: Reconstructed ,
140+ viewer : Viewer ,
141+ range : std:: ops:: Range < usize > ,
142+ budget : & mut ReplayBudget ,
122143) -> Result < Vec < RecordedEvent > , ApiError > {
123144 let first = rebuilt. frames . first ( ) . ok_or_else ( error:: corrupt) ?;
124145 // Same-turn controls only change Frame metadata. The engine state at index
125146 // zero remains the pristine start, so no second puzzle generation is needed.
126147 let initial = replay:: Frame {
127- state : first. state . clone ( ) , turn : 0 , terminated : false , truncated : false ,
128- outcome : None , draw_offer : None ,
129- } . project ( & record. id , rebuilt. game . as_ref ( ) , viewer) ?;
130- let perfect = rebuilt. game . spec ( ) . information == Information :: Perfect ;
131- let finished = rebuilt. current ( ) ?. ended ( ) ;
132- let mut events = Vec :: new ( ) ;
133- for ( offset, event) in record. events [ range. clone ( ) ] . iter ( ) . enumerate ( ) {
134- let event = match event {
135- MatchEvent :: MatchCreated ( origin) => EventData :: Created {
136- game_id : origin. game_id . clone ( ) ,
137- engine_version : origin. engine_version . clone ( ) ,
138- config : rebuilt
139- . game
140- . normalize_config ( & origin. config )
141- . map_err ( error:: engine) ?,
142- seed : ( viewer == Viewer :: Omniscient ) . then_some ( origin. seed ) ,
143- initial_state : ( viewer == Viewer :: Omniscient ) . then ( || first. state . clone ( ) ) ,
144- seats : crate :: seats:: assignments ( origin, initial. returns . len ( ) ) ,
145- assists : origin. assists . clone ( ) ,
146- initial : Box :: new ( initial. clone ( ) ) ,
147- at_ms : origin. created_at_ms ,
148- } ,
149- MatchEvent :: Action ( action) => project_action ( action, viewer, perfect, finished) ?,
150- MatchEvent :: ForkedFrom { source, .. } => EventData :: ForkedFrom {
151- source : source. clone ( ) ,
152- } ,
153- MatchEvent :: Resigned { turn, seat, at_ms } => EventData :: Resigned {
154- turn : * turn,
155- seat : * seat,
156- at_ms : * at_ms,
157- } ,
158- MatchEvent :: DrawOffered { turn, seat, at_ms } => EventData :: DrawOffered {
159- turn : * turn,
160- seat : * seat,
161- at_ms : * at_ms,
162- } ,
163- MatchEvent :: MatchFinished {
164- turn,
165- returns,
166- terminated,
167- truncated,
168- } => EventData :: Finished {
169- turn : * turn,
170- returns : returns. clone ( ) ,
171- terminated : * terminated,
172- truncated : * truncated,
173- } ,
174- } ;
175- let projected = RecordedEvent {
176- sequence : ( range. start + offset) as u64 ,
177- event,
178- } ;
179- budget. include ( & projected) ?;
180- events. push ( projected) ;
181- }
148+ state : first. state . clone ( ) ,
149+ turn : 0 ,
150+ terminated : false ,
151+ truncated : false ,
152+ outcome : None ,
153+ draw_offer : None ,
154+ }
155+ . project ( & record. id , rebuilt. game . as_ref ( ) , viewer) ?;
156+ let perfect = rebuilt. game . spec ( ) . information == Information :: Perfect ;
157+ let finished = rebuilt. current ( ) ?. ended ( ) ;
158+ let mut events = Vec :: new ( ) ;
159+ for ( offset, event) in record. events [ range. clone ( ) ] . iter ( ) . enumerate ( ) {
160+ let event = match event {
161+ MatchEvent :: MatchCreated ( origin) => EventData :: Created {
162+ game_id : origin. game_id . clone ( ) ,
163+ engine_version : origin. engine_version . clone ( ) ,
164+ config : rebuilt
165+ . game
166+ . normalize_config ( & origin. config )
167+ . map_err ( error:: engine) ?,
168+ seed : ( viewer == Viewer :: Omniscient ) . then_some ( origin. seed ) ,
169+ initial_state : ( viewer == Viewer :: Omniscient ) . then ( || first. state . clone ( ) ) ,
170+ seats : crate :: seats:: assignments ( origin, initial. returns . len ( ) ) ,
171+ assists : origin. assists . clone ( ) ,
172+ initial : Box :: new ( initial. clone ( ) ) ,
173+ at_ms : origin. created_at_ms ,
174+ } ,
175+ MatchEvent :: Action ( action) => project_action ( action, viewer, perfect, finished) ?,
176+ MatchEvent :: ForkedFrom { source, .. } => EventData :: ForkedFrom {
177+ source : source. clone ( ) ,
178+ } ,
179+ MatchEvent :: Resigned { turn, seat, at_ms } => EventData :: Resigned {
180+ turn : * turn,
181+ seat : * seat,
182+ at_ms : * at_ms,
183+ } ,
184+ MatchEvent :: DrawOffered { turn, seat, at_ms } => EventData :: DrawOffered {
185+ turn : * turn,
186+ seat : * seat,
187+ at_ms : * at_ms,
188+ } ,
189+ MatchEvent :: MatchFinished {
190+ turn,
191+ returns,
192+ terminated,
193+ truncated,
194+ } => EventData :: Finished {
195+ turn : * turn,
196+ returns : returns. clone ( ) ,
197+ terminated : * terminated,
198+ truncated : * truncated,
199+ } ,
200+ } ;
201+ let projected = RecordedEvent {
202+ sequence : ( range. start + offset) as u64 ,
203+ event,
204+ } ;
205+ budget. include ( & projected) ?;
206+ events. push ( projected) ;
207+ }
182208 Ok ( events)
183209}
0 commit comments