- wallets.js: WalletManager state management with chain validation - verifier.js: WalletVerifier EIP-712 signing via window.ethereum - stream.js: WalletStreamManager with BroadcastChannel leader election, backoff + jitter - AGENTS.md: updated with coding guidelines
123 lines
4.9 KiB
JavaScript
123 lines
4.9 KiB
JavaScript
import { WalletManager } from '../wallets.js';
|
||
import { WalletStreamManager } from '../stream.js';
|
||
|
||
// --- 1. Infrastructure Mocks ---
|
||
const mockStorage = {};
|
||
globalThis.localStorage = {
|
||
getItem: (key) => mockStorage[key] || null,
|
||
setItem: (key, value) => { mockStorage[key] = String(value); },
|
||
clear: () => { Object.keys(mockStorage).forEach(k => delete mockStorage[k]); }
|
||
};
|
||
|
||
// Accelerate timers so leader election (200ms) and backoff happen instantly
|
||
const originalSetTimeout = globalThis.setTimeout;
|
||
globalThis.setTimeout = (callback, delay) => originalSetTimeout(callback, 1);
|
||
|
||
// Standard WebSocket Mock
|
||
class MockWebSocket {
|
||
constructor(url) {
|
||
this.readyState = 1; // Immediately OPEN
|
||
originalSetTimeout(() => { if (this.onopen) this.onopen(); }, 1);
|
||
}
|
||
send(data) { globalLastSentPayload = JSON.parse(data); }
|
||
close() { if (this.onclose) this.onclose({ code: 1000 }); }
|
||
}
|
||
globalThis.WebSocket = MockWebSocket;
|
||
|
||
// --- 2. The Cross-Tab Broadcast Router Mock ---
|
||
const activeChannels = new Set();
|
||
class MockBroadcastChannel {
|
||
constructor(name) {
|
||
this.name = name;
|
||
activeChannels.add(this);
|
||
}
|
||
postMessage(data) {
|
||
// Distribute the message to all OTHER open channel instances simulation offline cross-tab delivery
|
||
for (const channel of activeChannels) {
|
||
if (channel !== this && channel.onmessage) {
|
||
channel.onmessage({ data });
|
||
}
|
||
}
|
||
}
|
||
close() { activeChannels.delete(this); }
|
||
}
|
||
globalThis.BroadcastChannel = MockBroadcastChannel;
|
||
|
||
// Global tracking variables for assertions
|
||
let globalLastSentPayload = null;
|
||
|
||
function assert(condition, message) {
|
||
if (!condition) throw new Error(`❌ FAIL: ${message}`);
|
||
console.log(`✅ PASS: ${message}`);
|
||
}
|
||
|
||
// --- 3. The Multi-Tab Simulation Scenario ---
|
||
async function runMultiTabTest() {
|
||
console.log("🚀 Starting Multi-Tab Synchronization & Leader Election Test Suite...\n");
|
||
localStorage.clear();
|
||
|
||
// Seed initial wallet configuration
|
||
const wm = new WalletManager();
|
||
wm.addWallet("0x04f728C520C438A000f7A5E9d904F0e725FFAEFE", "base", "Shared Wallet");
|
||
|
||
// --- STEP 1: Boot First Tab ---
|
||
console.log("▶️ Initializing Tab 1...");
|
||
const tab1 = new WalletStreamManager(wm);
|
||
tab1.connect();
|
||
|
||
await new Promise(r => originalSetTimeout(r, 15)); // Await leader election window
|
||
assert(tab1.isLeader === true, "Tab 1 detected no existing leaders and claimed leadership role.");
|
||
assert(tab1.ws !== null, "Tab 1 successfully initialized a physical WebSocket network connection.");
|
||
|
||
// --- STEP 2: Boot Second Tab ---
|
||
console.log("\n▶️ Initializing Tab 2 (Simulating a new browser window/tab)...");
|
||
const tab2 = new WalletStreamManager(wm);
|
||
tab2.connect();
|
||
|
||
await new Promise(r => originalSetTimeout(r, 15));
|
||
assert(tab2.isLeader === false, "Tab 2 discovered active Leader and assigned itself as a Follower.");
|
||
assert(tab2.ws === undefined || tab2.ws === null, "Tab 2 kept its local WebSocket offline to save system resources.");
|
||
|
||
// --- STEP 3: Verify Event Cross-Broadcasting Flow ---
|
||
let tab2ReceivedData = false;
|
||
tab2.on('balance_update', (msg) => {
|
||
if (msg.balance === "9999") tab2ReceivedData = true;
|
||
});
|
||
|
||
// Simulate server pushing an update specifically to Tab 1's socket connection
|
||
tab1.ws.onmessage({
|
||
data: JSON.stringify({ type: "balance_update", balance: "9999" })
|
||
});
|
||
|
||
await new Promise(r => originalSetTimeout(r, 5));
|
||
assert(tab2ReceivedData === true, "Tab 1 received WebSocket data and pushed it locally across the BroadcastChannel to Tab 2.");
|
||
|
||
// --- STEP 4: Proxy Subscription Modification Check ---
|
||
console.log("\n▶️ Simulating Follower (Tab 2) subscribing to a new asset dynamically...");
|
||
const newAsset = { address: "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", chain: "bitcoin" };
|
||
|
||
globalLastSentPayload = null;
|
||
tab2.subscribeToNewWallet(newAsset);
|
||
|
||
await new Promise(r => originalSetTimeout(r, 5));
|
||
assert(globalLastSentPayload !== null, "Tab 2's subscription was successfully intercepted and routed to the channel.");
|
||
assert(globalLastSentPayload.action === "subscribe_wallet", "Tab 1 successfully executed the proxy network subscription on behalf of Tab 2.");
|
||
|
||
// --- STEP 5: Graceful Failover Handover (Leader Death Scenario) ---
|
||
console.log("\n🕵️♂️ Simulating user abruptly closing Tab 1 (Leader)...");
|
||
|
||
// Simulate browser exit/unload routine for Tab 1
|
||
tab1.disconnect();
|
||
// Dispatch the exit event if your module binds directly to window event listeners
|
||
if (tab1._handleExit) tab1._handleExit();
|
||
|
||
await new Promise(r => originalSetTimeout(r, 15)); // Await emergency election frame
|
||
|
||
assert(tab2.isLeader === true, "Tab 2 caught the LEADER_DEATH signal and successfully promoted itself to Leader.");
|
||
assert(tab2.ws !== null, "Tab 2 gracefully opened a brand new physical WebSocket connection to pick up the stream.");
|
||
|
||
console.log("\n🎉 Massive Success! Cross-tab synchronization, proxy forwarding, and leader failovers function perfectly.");
|
||
}
|
||
|
||
runMultiTabTest().catch(console.error);
|