@@ -263,6 +263,114 @@ var App = (function() {
263263 * Every POLL_INTERVAL_MS checks chain head and processes new blocks
264264 * through BlockProcessor → StateEngine → checkpoint save.
265265 */
266+ /** Whether we have already run the historical chain recovery */
267+ var _chainRecoveryDone = false ;
268+ /** Whether chain recovery is currently running */
269+ var _chainRecoveryBusy = false ;
270+
271+ /**
272+ * Recover the current user's full action history by traversing the
273+ * backward-linked chain of VM custom operations (custom_sequence_block_num).
274+ * Only runs once per session when there is no IndexedDB checkpoint
275+ * (i.e. fresh device / cleared storage).
276+ *
277+ * Processes each historical block through the normal
278+ * BlockProcessor → StateEngine pipeline so inventory, XP, crafting,
279+ * guild-create and every other action type are replayed exactly
280+ * the same way as during live polling.
281+ *
282+ * After recovery finishes, normal 24-hour forward polling takes over
283+ * for shared/world state (other players' guild invites, world boss,
284+ * marketplace listings, etc.).
285+ */
286+ function _recoverChainHistory ( headBlock , callback ) {
287+ if ( _chainRecoveryBusy ) { callback ( ) ; return ; }
288+ _chainRecoveryBusy = true ;
289+
290+ var user = VizAccount . getCurrentUser ( ) ;
291+ if ( ! user ) {
292+ _chainRecoveryDone = true ;
293+ _chainRecoveryBusy = false ;
294+ callback ( ) ;
295+ return ;
296+ }
297+
298+ var recentWindowStart = Math . max ( 1 , headBlock - 28800 ) ;
299+
300+ _updateSyncStatus ( 0 , true ) ;
301+ console . log ( 'App: Starting chain history recovery for' , user ) ;
302+
303+ // Traverse the full backward chain (up to 5000 actions)
304+ VMProtocol . traverseChain ( user , 5000 , function ( err , actions ) {
305+ if ( err || ! actions || actions . length === 0 ) {
306+ console . log ( 'App: No chain history found, falling back to 24h window' ) ;
307+ _chainRecoveryDone = true ;
308+ _chainRecoveryBusy = false ;
309+ callback ( ) ;
310+ return ;
311+ }
312+
313+ // Collect block numbers that are OLDER than the 24h window
314+ // (blocks inside the window will be processed by normal polling)
315+ var historicalBlocks = [ ] ;
316+ for ( var i = 0 ; i < actions . length ; i ++ ) {
317+ if ( actions [ i ] . blockNum < recentWindowStart ) {
318+ historicalBlocks . push ( actions [ i ] . blockNum ) ;
319+ }
320+ }
321+
322+ if ( historicalBlocks . length === 0 ) {
323+ console . log ( 'App: All actions within 24h window, no extra recovery needed' ) ;
324+ _chainRecoveryDone = true ;
325+ _chainRecoveryBusy = false ;
326+ callback ( ) ;
327+ return ;
328+ }
329+
330+ // Sort ascending so state is built in chronological order
331+ historicalBlocks . sort ( function ( a , b ) { return a - b ; } ) ;
332+
333+ console . log ( 'App: Recovering' , historicalBlocks . length , 'historical blocks (oldest:' , historicalBlocks [ 0 ] , ')' ) ;
334+
335+ var idx = 0 ;
336+
337+ function processNext ( ) {
338+ if ( idx >= historicalBlocks . length ) {
339+ // Save checkpoint after full recovery
340+ StateEngine . saveCheckpoint ( function ( ) {
341+ console . log ( 'App: Chain history recovery complete,' , historicalBlocks . length , 'blocks processed' ) ;
342+ _chainRecoveryDone = true ;
343+ _chainRecoveryBusy = false ;
344+ callback ( ) ;
345+ } ) ;
346+ return ;
347+ }
348+
349+ var pct = ( idx / historicalBlocks . length ) * 80 ; // 0-80% for recovery
350+ _updateSyncStatus ( pct , true ) ;
351+
352+ var blockNum = historicalBlocks [ idx ] ;
353+ viz . api . getBlock ( blockNum , function ( bErr , block ) {
354+ if ( bErr || ! block ) {
355+ console . log ( 'App: Could not fetch historical block' , blockNum ) ;
356+ idx ++ ;
357+ processNext ( ) ;
358+ return ;
359+ }
360+
361+ var processed = BlockProcessor . processBlock ( block , blockNum ) ;
362+ StateEngine . processBlock ( processed ) ;
363+ idx ++ ;
364+
365+ // Small delay to avoid hammering the node
366+ setTimeout ( processNext , 10 ) ;
367+ } ) ;
368+ }
369+
370+ processNext ( ) ;
371+ } ) ;
372+ }
373+
266374 function _startBlockPolling ( ) {
267375 if ( _pollTimer ) return ;
268376
@@ -288,6 +396,16 @@ var App = (function() {
288396 // world boss encounters survive reloads and fresh logins.
289397 // 28800 blocks ≈ 24 hours — matches DUEL_ACCEPT_WINDOW.
290398 if ( _lastPolledBlock === 0 ) {
399+ // On a fresh device (no checkpoint), first recover the user's
400+ // full action history via backward chain traversal before
401+ // starting the normal 24h forward replay.
402+ if ( ! _chainRecoveryDone ) {
403+ _pollBusy = false ;
404+ _recoverChainHistory ( headBlock , function ( ) {
405+ // Recovery done — next poll tick will set the 24h window
406+ } ) ;
407+ return ;
408+ }
291409 _lastPolledBlock = Math . max ( 1 , headBlock - 28800 ) ;
292410 }
293411
0 commit comments