Skip to content

Commit 0e4aee1

Browse files
web3blindclaude
andcommitted
Recover full inventory on new device via backward chain traversal
When no IndexedDB checkpoint exists (fresh device/browser), traverse the user's VM custom_sequence_block_num backward chain to replay all historical game actions before falling back to the 24h polling window. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 48a9819 commit 0e4aee1

1 file changed

Lines changed: 118 additions & 0 deletions

File tree

app/js/ui/app.js

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)