@@ -267,6 +267,20 @@ var App = (function() {
267267 var _chainRecoveryDone = false ;
268268 /** Whether chain recovery is currently running */
269269 var _chainRecoveryBusy = false ;
270+ /** Set of block numbers already processed during recovery to prevent duplicates */
271+ var _recoveryProcessedBlocks = { } ;
272+
273+ /**
274+ * Process a single block during recovery, skipping if already seen.
275+ * Prevents duplicate items/XP when the same block appears in
276+ * multiple accounts' chains.
277+ */
278+ function _processRecoveryBlock ( blockNum , block ) {
279+ if ( _recoveryProcessedBlocks [ blockNum ] ) return ;
280+ _recoveryProcessedBlocks [ blockNum ] = true ;
281+ var processed = BlockProcessor . processBlock ( block , blockNum ) ;
282+ StateEngine . processBlock ( processed ) ;
283+ }
270284
271285 /**
272286 * Recover the current user's full action history by traversing the
@@ -282,6 +296,9 @@ var App = (function() {
282296 * After recovery finishes, normal 24-hour forward polling takes over
283297 * for shared/world state (other players' guild invites, world boss,
284298 * marketplace listings, etc.).
299+ *
300+ * @param {number } headBlock - current chain head from dgp
301+ * @param {Function } callback - called when recovery is complete
285302 */
286303 function _recoverChainHistory ( headBlock , callback ) {
287304 if ( _chainRecoveryBusy ) { callback ( ) ; return ; }
@@ -313,9 +330,12 @@ var App = (function() {
313330 // Collect block numbers that are OLDER than the 24h window
314331 // (blocks inside the window will be processed by normal polling)
315332 var historicalBlocks = [ ] ;
333+ var seen = { } ;
316334 for ( var i = 0 ; i < actions . length ; i ++ ) {
317- if ( actions [ i ] . blockNum < recentWindowStart ) {
318- historicalBlocks . push ( actions [ i ] . blockNum ) ;
335+ var bn = actions [ i ] . blockNum ;
336+ if ( bn < recentWindowStart && ! seen [ bn ] ) {
337+ seen [ bn ] = true ;
338+ historicalBlocks . push ( bn ) ;
319339 }
320340 }
321341
@@ -337,11 +357,18 @@ var App = (function() {
337357 function processNext ( ) {
338358 if ( idx >= historicalBlocks . length ) {
339359 // After user's own chain is recovered, discover other players
340- _recoverKnownAccountsChains ( recentWindowStart , function ( ) {
360+ _recoverKnownAccountsChains ( recentWindowStart , headBlock , function ( ) {
361+ // Set headBlock to current chain head so checkpoint is up-to-date.
362+ // Without this, worldState.headBlock would point to the last
363+ // historical block and a crash+restart would trigger a huge catch-up.
364+ var state = StateEngine . getState ( ) ;
365+ state . headBlock = headBlock ;
366+
341367 StateEngine . saveCheckpoint ( function ( ) {
342368 console . log ( 'App: Chain history recovery complete,' , historicalBlocks . length , 'blocks processed' ) ;
343369 _chainRecoveryDone = true ;
344370 _chainRecoveryBusy = false ;
371+ _recoveryProcessedBlocks = { } ; // free memory
345372 callback ( ) ;
346373 } ) ;
347374 } ) ;
@@ -360,8 +387,7 @@ var App = (function() {
360387 return ;
361388 }
362389
363- var processed = BlockProcessor . processBlock ( block , blockNum ) ;
364- StateEngine . processBlock ( processed ) ;
390+ _processRecoveryBlock ( blockNum , block ) ;
365391 idx ++ ;
366392
367393 // Small delay to avoid hammering the node
@@ -380,8 +406,9 @@ var App = (function() {
380406 * marketplace listings, and the social feed account list.
381407 * Processes up to 100 actions per account, only blocks older than
382408 * the 24h window (to avoid overlap with normal polling).
409+ * Uses _recoveryProcessedBlocks to skip blocks already seen.
383410 */
384- function _recoverKnownAccountsChains ( recentWindowStart , callback ) {
411+ function _recoverKnownAccountsChains ( recentWindowStart , currentHead , callback ) {
385412 var state = StateEngine . getState ( ) ;
386413 var user = VizAccount . getCurrentUser ( ) ;
387414 var others = [ ] ;
@@ -419,11 +446,12 @@ var App = (function() {
419446 return ;
420447 }
421448
422- // Collect historical blocks for this account
449+ // Collect historical blocks for this account, skip already-processed
423450 var blocks = [ ] ;
424451 for ( var i = 0 ; i < actions . length ; i ++ ) {
425- if ( actions [ i ] . blockNum < recentWindowStart ) {
426- blocks . push ( actions [ i ] . blockNum ) ;
452+ var bn = actions [ i ] . blockNum ;
453+ if ( bn < recentWindowStart && ! _recoveryProcessedBlocks [ bn ] ) {
454+ blocks . push ( bn ) ;
427455 }
428456 }
429457
@@ -441,10 +469,10 @@ var App = (function() {
441469 return ;
442470 }
443471
444- viz . api . getBlock ( blocks [ bIdx ] , function ( bErr , block ) {
472+ var blockNum = blocks [ bIdx ] ;
473+ viz . api . getBlock ( blockNum , function ( bErr , block ) {
445474 if ( ! bErr && block ) {
446- var processed = BlockProcessor . processBlock ( block , blocks [ bIdx ] ) ;
447- StateEngine . processBlock ( processed ) ;
475+ _processRecoveryBlock ( blockNum , block ) ;
448476 }
449477 bIdx ++ ;
450478 setTimeout ( nextBlock , 10 ) ;
@@ -486,10 +514,11 @@ var App = (function() {
486514 // On a fresh device (no checkpoint), first recover the user's
487515 // full action history via backward chain traversal before
488516 // starting the normal 24h forward replay.
517+ // Keep _pollBusy=true so no other poll ticks fire during recovery.
489518 if ( ! _chainRecoveryDone ) {
490- _pollBusy = false ;
491519 _recoverChainHistory ( headBlock , function ( ) {
492- // Recovery done — next poll tick will set the 24h window
520+ // Recovery done — release lock, next tick sets 24h window
521+ _pollBusy = false ;
493522 } ) ;
494523 return ;
495524 }
0 commit comments