Skip to content

Commit 5f2b396

Browse files
author
web3blind
committed
Improve VIZ history source resilience
1 parent 87977e0 commit 5f2b396

11 files changed

Lines changed: 989 additions & 75 deletions

File tree

app/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@
125125
<!-- Blockchain layer -->
126126
<script src="js/blockchain/connection.js"></script>
127127
<script src="js/blockchain/account.js"></script>
128+
<script src="js/blockchain/history-source.js"></script>
128129
<script src="js/blockchain/broadcast.js"></script>
129130
<script src="js/blockchain/invite.js"></script>
130131

app/js/blockchain/connection.js

Lines changed: 82 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ var VizConnection = (function() {
1717
var latencies = {};
1818
var onConnectCallbacks = [];
1919
var onDisconnectCallbacks = [];
20+
var lastHistoryCapability = null;
2021

2122
/**
2223
* Initialize connection — pick best node or use saved preference
@@ -42,34 +43,21 @@ var VizConnection = (function() {
4243
}
4344

4445
/**
45-
* Test all nodes and connect to the fastest one
46+
* Connect to the first configured node and keep later nodes as ordered fallbacks.
47+
* The first node is the primary user-facing endpoint; probing every HTTP node at
48+
* startup can create noisy CORS/preflight failures in browsers, so fallback nodes
49+
* are touched only when the primary connection fails.
4650
*/
4751
function _selectBestNode(callback) {
48-
var pending = cfg.NODES.length;
49-
var bestLatency = Infinity;
50-
var bestNode = cfg.NODES[0];
51-
52-
cfg.NODES.forEach(function(node, index) {
53-
_measureLatency(node, function(latency) {
54-
pending--;
55-
if (latency >= 0 && latency < bestLatency) {
56-
bestLatency = latency;
57-
bestNode = node;
58-
currentNodeIndex = index;
59-
}
60-
latencies[node] = latency;
61-
62-
if (pending <= 0) {
63-
console.log('Best node:', bestNode, 'latency:', bestLatency + 'ms');
64-
_connectToNode(bestNode, function(err) {
65-
if (err) {
66-
_tryNextNode(callback);
67-
} else {
68-
callback(null, dgp);
69-
}
70-
});
71-
}
72-
});
52+
currentNodeIndex = 0;
53+
var primaryNode = cfg.NODES[0];
54+
console.log('Primary node:', primaryNode);
55+
_connectToNode(primaryNode, function(err) {
56+
if (err) {
57+
_tryNextNode(callback);
58+
} else {
59+
callback(null, dgp);
60+
}
7361
});
7462
}
7563

@@ -305,11 +293,79 @@ var VizConnection = (function() {
305293
_connectToNode(node, callback);
306294
}
307295

296+
/**
297+
* Check whether the current VIZ node can serve recent and older blocks.
298+
* This is advisory only: the app must keep running even when historical
299+
* blocks are unavailable and can later use an archive mirror for recovery.
300+
* @param {Function} callback - (err, capability)
301+
*/
302+
function checkHistoryCapability(callback) {
303+
callback = callback || function() {};
304+
var capability = {
305+
live: connected,
306+
recentBlocks: false,
307+
historicalBlocks: false,
308+
checkedAt: Date.now(),
309+
node: currentNode
310+
};
311+
312+
function finish() {
313+
lastHistoryCapability = capability;
314+
callback(null, capability);
315+
}
316+
317+
function probeBlock(blockNum, done) {
318+
if (!blockNum || blockNum <= 0) {
319+
done(false);
320+
return;
321+
}
322+
viz.api.getBlock(blockNum, function(err, block) {
323+
done(!err && !!block);
324+
});
325+
}
326+
327+
function runWithHead(headBlock) {
328+
if (!headBlock || headBlock <= 0) {
329+
finish();
330+
return;
331+
}
332+
capability.live = true;
333+
var recentBlock = Math.max(1, headBlock - 100);
334+
var oldBlock = Math.max(1, headBlock - 60000);
335+
probeBlock(recentBlock, function(recentOk) {
336+
capability.recentBlocks = recentOk;
337+
probeBlock(oldBlock, function(oldOk) {
338+
capability.historicalBlocks = oldOk;
339+
finish();
340+
});
341+
});
342+
}
343+
344+
if (dgp && dgp.head_block_number) {
345+
runWithHead(dgp.head_block_number);
346+
return;
347+
}
348+
349+
viz.api.getDynamicGlobalProperties(function(err, response) {
350+
if (err || !response || !response.head_block_number) {
351+
finish();
352+
return;
353+
}
354+
runWithHead(response.head_block_number);
355+
});
356+
}
357+
358+
function getHistoryCapability() {
359+
return lastHistoryCapability;
360+
}
361+
308362
return {
309363
init: init,
310364
getDGP: getDGP,
311365
isConnected: isConnected,
312366
getCurrentNode: getCurrentNode,
367+
getHistoryCapability: getHistoryCapability,
368+
checkHistoryCapability: checkHistoryCapability,
313369
onConnect: onConnect,
314370
onDisconnect: onDisconnect,
315371
switchNode: switchNode
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
/**
2+
* Viz Magic — History Source
3+
* Read-only source layer for old public chain history.
4+
* VIZ RPC remains the live source; optional archive mirrors can serve old
5+
* blocks when a public node no longer keeps enough history.
6+
*/
7+
var HistorySource = (function() {
8+
'use strict';
9+
10+
function _makeError(message) {
11+
try { return new Error(message); } catch (e) { return { message: message }; }
12+
}
13+
14+
function _archiveMirrors() {
15+
if (typeof VizMagicConfig === 'undefined' || !VizMagicConfig.HISTORY_ARCHIVE_MIRRORS) {
16+
return [];
17+
}
18+
return VizMagicConfig.HISTORY_ARCHIVE_MIRRORS;
19+
}
20+
21+
function _normalizeMirror(mirror) {
22+
if (typeof mirror === 'string') {
23+
return { url: mirror };
24+
}
25+
return mirror || {};
26+
}
27+
28+
function _mirrorUrl(mirror, blockNum) {
29+
var pattern = mirror.url || mirror.blockUrl || '';
30+
if (!pattern) return '';
31+
if (pattern.indexOf('{block}') !== -1) {
32+
return pattern.replace('{block}', encodeURIComponent(String(blockNum)));
33+
}
34+
return pattern.replace(/\/$/, '') + '/' + encodeURIComponent(String(blockNum)) + '.json';
35+
}
36+
37+
function _extractBlockFromMirrorPayload(payload) {
38+
if (!payload) return null;
39+
if (payload.previous && payload.timestamp && payload.transactions) return payload;
40+
if (payload.block && payload.block.previous && payload.block.transactions) return payload.block;
41+
if (payload.result && payload.result.previous && payload.result.transactions) return payload.result;
42+
if (payload.data && payload.data.block && payload.data.block.transactions) return payload.data.block;
43+
return null;
44+
}
45+
46+
function _requestJson(url, timeoutMs, callback) {
47+
if (typeof XMLHttpRequest === 'undefined') {
48+
callback(_makeError('HTTP archive fetch is unavailable'));
49+
return;
50+
}
51+
var xhr = new XMLHttpRequest();
52+
var completed = false;
53+
var timer = setTimeout(function() {
54+
if (completed) return;
55+
completed = true;
56+
try { xhr.abort(); } catch (e) {}
57+
callback(_makeError('Archive mirror timeout'));
58+
}, timeoutMs || 6000);
59+
60+
xhr.onreadystatechange = function() {
61+
if (xhr.readyState !== 4 || completed) return;
62+
completed = true;
63+
clearTimeout(timer);
64+
if (xhr.status < 200 || xhr.status >= 300) {
65+
callback(_makeError('Archive mirror HTTP ' + xhr.status));
66+
return;
67+
}
68+
try {
69+
callback(null, JSON.parse(xhr.responseText));
70+
} catch (parseErr) {
71+
callback(parseErr);
72+
}
73+
};
74+
try {
75+
xhr.open('GET', url, true);
76+
xhr.send(null);
77+
} catch (err) {
78+
if (completed) return;
79+
completed = true;
80+
clearTimeout(timer);
81+
callback(err);
82+
}
83+
}
84+
85+
function _getBlockFromMirrors(blockNum, index, callback) {
86+
var mirrors = _archiveMirrors();
87+
if (!mirrors.length || index >= mirrors.length) {
88+
callback(_makeError('Block unavailable from VIZ RPC and archive mirrors'));
89+
return;
90+
}
91+
var mirror = _normalizeMirror(mirrors[index]);
92+
var url = _mirrorUrl(mirror, blockNum);
93+
if (!url) {
94+
_getBlockFromMirrors(blockNum, index + 1, callback);
95+
return;
96+
}
97+
_requestJson(url, mirror.timeoutMs || 6000, function(err, payload) {
98+
var block = err ? null : _extractBlockFromMirrorPayload(payload);
99+
if (block) {
100+
callback(null, block);
101+
return;
102+
}
103+
_getBlockFromMirrors(blockNum, index + 1, callback);
104+
});
105+
}
106+
107+
function getBlock(blockNum, callback) {
108+
callback = callback || function() {};
109+
if (!blockNum || blockNum <= 0) {
110+
callback(_makeError('Invalid block number'));
111+
return;
112+
}
113+
if (typeof viz === 'undefined' || !viz.api || !viz.api.getBlock) {
114+
_getBlockFromMirrors(blockNum, 0, callback);
115+
return;
116+
}
117+
viz.api.getBlock(blockNum, function(err, block) {
118+
if (!err && block) {
119+
callback(null, block);
120+
return;
121+
}
122+
_getBlockFromMirrors(blockNum, 0, callback);
123+
});
124+
}
125+
126+
function getAccountProtocol(account, protocol, callback) {
127+
callback = callback || function() {};
128+
if (!account || !protocol) {
129+
callback(_makeError('Account and protocol are required'));
130+
return;
131+
}
132+
if (typeof VizAccount === 'undefined' || !VizAccount.getAccountProtocol) {
133+
callback(_makeError('Account protocol lookup is unavailable'));
134+
return;
135+
}
136+
VizAccount.getAccountProtocol(account, protocol, callback);
137+
}
138+
139+
function getAccountActions(account, protocol, options, callback) {
140+
if (typeof options === 'function') {
141+
callback = options;
142+
options = {};
143+
}
144+
options = options || {};
145+
callback = callback || function() {};
146+
if (protocol !== VizMagicConfig.PROTOCOLS.VM) {
147+
callback(null, []);
148+
return;
149+
}
150+
if (typeof VMProtocol === 'undefined' || !VMProtocol.traverseChain) {
151+
callback(_makeError('VM protocol traversal is unavailable'));
152+
return;
153+
}
154+
VMProtocol.traverseChain(account, options.limit || 5000, callback, HistorySource);
155+
}
156+
157+
function getCapabilities(callback) {
158+
callback = callback || function() {};
159+
if (typeof VizConnection !== 'undefined' && VizConnection.checkHistoryCapability) {
160+
VizConnection.checkHistoryCapability(callback);
161+
return;
162+
}
163+
callback(null, {
164+
live: false,
165+
recentBlocks: false,
166+
historicalBlocks: false,
167+
checkedAt: Date.now(),
168+
node: ''
169+
});
170+
}
171+
172+
return {
173+
getBlock: getBlock,
174+
getAccountProtocol: getAccountProtocol,
175+
getAccountActions: getAccountActions,
176+
getCapabilities: getCapabilities
177+
};
178+
})();

app/js/config.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ var VizMagicConfig = (function() {
1111
'https://node.viz.cx/'
1212
];
1313

14+
/**
15+
* Optional read-only archive mirrors for old block fetches.
16+
* Keep empty until a public mirror is verified for CORS and block-shape parity.
17+
* Supported endpoint pattern: URL may contain {block}, for example
18+
* https://example.invalid/viz/block/{block}.json
19+
*/
20+
var HISTORY_ARCHIVE_MIRRORS = [];
21+
1422
/** Protocol identifiers registered on VIZ chain */
1523
var PROTOCOLS = {
1624
VM: 'VM', // Viz Magic — all game actions
@@ -199,6 +207,7 @@ var VizMagicConfig = (function() {
199207

200208
return {
201209
NODES: NODES,
210+
HISTORY_ARCHIVE_MIRRORS: HISTORY_ARCHIVE_MIRRORS,
202211
PROTOCOLS: PROTOCOLS,
203212
APP_VERSION: APP_VERSION,
204213
STORAGE_PREFIX: STORAGE_PREFIX,

app/js/i18n/en.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,7 @@ var LangEN = {
360360
conn_connecting: 'Establishing connection to the World...',
361361
conn_connected: 'Connection to the World restored!',
362362
conn_disconnected: 'Connection to the World slumbers. Some actions unavailable.',
363+
conn_history_limited: 'This VIZ node only provides recent history. Older character and world recovery may need an archive mirror.',
363364

364365
// Navigation (new)
365366
nav_map: 'Map',

app/js/i18n/ru.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,7 @@ var LangRU = {
360360
conn_connecting: 'Связь с Миром устанавливается...',
361361
conn_connected: 'Связь с Миром восстановлена!',
362362
conn_disconnected: 'Связь с Миром дремлет. Некоторые действия недоступны.',
363+
conn_history_limited: 'Этот VIZ-узел показывает только недавнюю историю. Для старого восстановления персонажа и мира может понадобиться архивное зеркало.',
363364

364365
// Navigation (new)
365366
nav_map: 'Карта',

0 commit comments

Comments
 (0)