@monygroupcorp/micro-web3 0.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +6 -0
- package/dist/{micro-web3.cjs.js → micro-web3.cjs} +3 -3
- package/dist/micro-web3.cjs.map +1 -0
- package/dist/micro-web3.esm.js +2 -2
- package/dist/micro-web3.esm.js.map +1 -1
- package/dist/micro-web3.umd.js +2 -2
- package/dist/micro-web3.umd.js.map +1 -1
- package/docs/plans/2026-01-22-event-indexer.md +3642 -0
- package/monygroupcorp-micro-web3-0.1.3.tgz +0 -0
- package/package.json +2 -2
- package/rollup.config.cjs +1 -1
- package/src/components/FloatingWalletButton/FloatingWalletButton.js +26 -5
- package/src/components/SettingsModal/SettingsModal.js +371 -0
- package/src/components/SyncProgressBar/SyncProgressBar.js +238 -0
- package/src/components/WalletButton/WalletButton.js +213 -0
- package/src/index.js +18 -2
- package/src/indexer/EntityResolver.js +218 -0
- package/src/indexer/Patterns.js +277 -0
- package/src/indexer/QueryEngine.js +149 -0
- package/src/indexer/SyncEngine.js +494 -0
- package/src/indexer/index.js +13 -0
- package/src/services/BlockchainService.js +30 -0
- package/src/services/ContractCache.js +20 -2
- package/src/services/EventIndexer.js +399 -0
- package/src/storage/IndexedDBAdapter.js +423 -0
- package/src/storage/IndexerSettings.js +88 -0
- package/src/storage/MemoryAdapter.js +194 -0
- package/src/storage/StorageAdapter.js +129 -0
- package/src/storage/index.js +41 -0
- package/dist/micro-web3.cjs.js.map +0 -1
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@monygroupcorp/micro-web3",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "A lean, reusable Web3 toolkit with components for wallet connection, IPFS, and common Web3 UI patterns.",
|
|
5
|
-
"main": "dist/micro-web3.cjs
|
|
5
|
+
"main": "dist/micro-web3.cjs",
|
|
6
6
|
"module": "dist/micro-web3.esm.js",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"scripts": {
|
package/rollup.config.cjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Component, eventBus } from '@monygroupcorp/microact';
|
|
2
2
|
import WalletModal from '../Wallet/WalletModal.js';
|
|
3
|
+
import { SettingsModal } from '../SettingsModal/SettingsModal.js';
|
|
3
4
|
import { ethers } from 'ethers';
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -12,6 +13,7 @@ export class FloatingWalletButton extends Component {
|
|
|
12
13
|
super();
|
|
13
14
|
this.walletService = props.walletService;
|
|
14
15
|
this.walletModal = null;
|
|
16
|
+
this.settingsModal = null;
|
|
15
17
|
this.state = {
|
|
16
18
|
walletConnected: false,
|
|
17
19
|
address: null,
|
|
@@ -230,13 +232,19 @@ export class FloatingWalletButton extends Component {
|
|
|
230
232
|
|
|
231
233
|
// Create WalletModal if not exists
|
|
232
234
|
if (!this.walletModal) {
|
|
233
|
-
this.walletModal = new WalletModal(
|
|
235
|
+
this.walletModal = new WalletModal({
|
|
234
236
|
providerMap,
|
|
235
237
|
walletIcons,
|
|
236
|
-
async (walletType) => {
|
|
238
|
+
onWalletSelected: async (walletType) => {
|
|
237
239
|
await this.handleWalletSelection(walletType);
|
|
238
|
-
}
|
|
239
|
-
);
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
if (!this.modalRoot) {
|
|
244
|
+
this.modalRoot = document.createElement('div');
|
|
245
|
+
document.body.appendChild(this.modalRoot);
|
|
246
|
+
}
|
|
247
|
+
this.walletModal.mount(this.modalRoot);
|
|
240
248
|
}
|
|
241
249
|
|
|
242
250
|
this.walletModal.show();
|
|
@@ -637,6 +645,7 @@ export class FloatingWalletButton extends Component {
|
|
|
637
645
|
this.handleMenuItemClick(route);
|
|
638
646
|
} else if (action === 'settings') {
|
|
639
647
|
this.setState({ menuOpen: false });
|
|
648
|
+
this.openSettings();
|
|
640
649
|
}
|
|
641
650
|
});
|
|
642
651
|
});
|
|
@@ -657,12 +666,25 @@ export class FloatingWalletButton extends Component {
|
|
|
657
666
|
}
|
|
658
667
|
}
|
|
659
668
|
|
|
669
|
+
openSettings() {
|
|
670
|
+
if (!this.settingsModal) {
|
|
671
|
+
this.settingsModal = new SettingsModal({ eventBus: eventBus });
|
|
672
|
+
}
|
|
673
|
+
this.settingsModal.show();
|
|
674
|
+
}
|
|
675
|
+
|
|
660
676
|
onUnmount() {
|
|
661
677
|
// Clean up wallet modal
|
|
662
678
|
if (this.walletModal) {
|
|
663
679
|
this.walletModal.hide();
|
|
664
680
|
this.walletModal = null;
|
|
665
681
|
}
|
|
682
|
+
|
|
683
|
+
// Clean up settings modal
|
|
684
|
+
if (this.settingsModal) {
|
|
685
|
+
this.settingsModal.hide();
|
|
686
|
+
this.settingsModal = null;
|
|
687
|
+
}
|
|
666
688
|
}
|
|
667
689
|
|
|
668
690
|
escapeHtml(text) {
|
|
@@ -672,4 +694,3 @@ export class FloatingWalletButton extends Component {
|
|
|
672
694
|
return div.innerHTML;
|
|
673
695
|
}
|
|
674
696
|
}
|
|
675
|
-
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
// src/components/SettingsModal/SettingsModal.js
|
|
2
|
+
|
|
3
|
+
import { IndexerSettings } from '../../storage/index.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* SettingsModal - Modal for user settings including indexer storage controls.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* const modal = new SettingsModal({ eventBus });
|
|
10
|
+
* modal.show();
|
|
11
|
+
*/
|
|
12
|
+
class SettingsModal {
|
|
13
|
+
constructor({ eventBus }) {
|
|
14
|
+
this.eventBus = eventBus;
|
|
15
|
+
this.element = null;
|
|
16
|
+
this.overlay = null;
|
|
17
|
+
this.isVisible = false;
|
|
18
|
+
this.state = {
|
|
19
|
+
storageEnabled: true,
|
|
20
|
+
storageUsed: 0,
|
|
21
|
+
storageQuota: 0,
|
|
22
|
+
clearing: false
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async show() {
|
|
27
|
+
if (this.isVisible) return;
|
|
28
|
+
|
|
29
|
+
// Load current settings
|
|
30
|
+
const settings = IndexerSettings.get();
|
|
31
|
+
const estimate = await IndexerSettings.getStorageEstimate();
|
|
32
|
+
|
|
33
|
+
this.state = {
|
|
34
|
+
storageEnabled: settings.storageEnabled,
|
|
35
|
+
storageUsed: estimate.used || 0,
|
|
36
|
+
storageQuota: estimate.quota || 0,
|
|
37
|
+
clearing: false
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
this._createModal();
|
|
41
|
+
this.isVisible = true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
hide() {
|
|
45
|
+
if (!this.isVisible) return;
|
|
46
|
+
|
|
47
|
+
if (this.overlay && this.overlay.parentNode) {
|
|
48
|
+
this.overlay.parentNode.removeChild(this.overlay);
|
|
49
|
+
}
|
|
50
|
+
this.overlay = null;
|
|
51
|
+
this.element = null;
|
|
52
|
+
this.isVisible = false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
_createModal() {
|
|
56
|
+
// Inject styles if needed
|
|
57
|
+
SettingsModal.injectStyles();
|
|
58
|
+
|
|
59
|
+
// Create overlay
|
|
60
|
+
this.overlay = document.createElement('div');
|
|
61
|
+
this.overlay.className = 'mw3-settings-overlay';
|
|
62
|
+
this.overlay.addEventListener('click', (e) => {
|
|
63
|
+
if (e.target === this.overlay) this.hide();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Create modal
|
|
67
|
+
this.element = document.createElement('div');
|
|
68
|
+
this.element.className = 'mw3-settings-modal';
|
|
69
|
+
this.element.innerHTML = this._render();
|
|
70
|
+
|
|
71
|
+
this.overlay.appendChild(this.element);
|
|
72
|
+
document.body.appendChild(this.overlay);
|
|
73
|
+
|
|
74
|
+
this._setupEventListeners();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
_render() {
|
|
78
|
+
const { storageEnabled, storageUsed, storageQuota, clearing } = this.state;
|
|
79
|
+
const usedMB = (storageUsed / (1024 * 1024)).toFixed(1);
|
|
80
|
+
const quotaMB = (storageQuota / (1024 * 1024)).toFixed(0);
|
|
81
|
+
|
|
82
|
+
return `
|
|
83
|
+
<div class="mw3-settings-header">
|
|
84
|
+
<h2>Settings</h2>
|
|
85
|
+
<button class="mw3-settings-close" data-action="close">×</button>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<div class="mw3-settings-content">
|
|
89
|
+
<div class="mw3-settings-section">
|
|
90
|
+
<h3>Data Storage</h3>
|
|
91
|
+
<p class="mw3-settings-description">
|
|
92
|
+
Event data is cached locally to improve performance. You can disable this to save space.
|
|
93
|
+
</p>
|
|
94
|
+
|
|
95
|
+
<div class="mw3-settings-row">
|
|
96
|
+
<label class="mw3-settings-toggle">
|
|
97
|
+
<input type="checkbox" ${storageEnabled ? 'checked' : ''} data-setting="storageEnabled">
|
|
98
|
+
<span class="mw3-toggle-slider"></span>
|
|
99
|
+
<span class="mw3-toggle-label">Enable local event storage</span>
|
|
100
|
+
</label>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<div class="mw3-settings-info">
|
|
104
|
+
<span>Storage used: ${usedMB} MB${quotaMB > 0 ? ` / ${quotaMB} MB` : ''}</span>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<div class="mw3-settings-row">
|
|
108
|
+
<button class="mw3-settings-btn mw3-settings-btn--danger" data-action="clearData" ${clearing ? 'disabled' : ''}>
|
|
109
|
+
${clearing ? 'Clearing...' : 'Clear All Cached Data'}
|
|
110
|
+
</button>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<p class="mw3-settings-note">
|
|
114
|
+
Disabling storage means event history will need to be re-fetched each session.
|
|
115
|
+
</p>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<div class="mw3-settings-footer">
|
|
120
|
+
<button class="mw3-settings-btn mw3-settings-btn--primary" data-action="close">Done</button>
|
|
121
|
+
</div>
|
|
122
|
+
`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
_setupEventListeners() {
|
|
126
|
+
// Close button
|
|
127
|
+
this.element.querySelectorAll('[data-action="close"]').forEach(btn => {
|
|
128
|
+
btn.addEventListener('click', () => this.hide());
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Storage toggle
|
|
132
|
+
const toggle = this.element.querySelector('[data-setting="storageEnabled"]');
|
|
133
|
+
if (toggle) {
|
|
134
|
+
toggle.addEventListener('change', (e) => {
|
|
135
|
+
const enabled = e.target.checked;
|
|
136
|
+
IndexerSettings.set({ storageEnabled: enabled });
|
|
137
|
+
this.state.storageEnabled = enabled;
|
|
138
|
+
|
|
139
|
+
// Emit event so indexer can react
|
|
140
|
+
this.eventBus.emit('settings:storageChanged', { enabled });
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Clear data button
|
|
145
|
+
const clearBtn = this.element.querySelector('[data-action="clearData"]');
|
|
146
|
+
if (clearBtn) {
|
|
147
|
+
clearBtn.addEventListener('click', async () => {
|
|
148
|
+
if (this.state.clearing) return;
|
|
149
|
+
|
|
150
|
+
this.state.clearing = true;
|
|
151
|
+
this._updateContent();
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
await IndexerSettings.clearAllData();
|
|
155
|
+
|
|
156
|
+
// Update storage estimate
|
|
157
|
+
const estimate = await IndexerSettings.getStorageEstimate();
|
|
158
|
+
this.state.storageUsed = estimate.used || 0;
|
|
159
|
+
this.state.storageQuota = estimate.quota || 0;
|
|
160
|
+
|
|
161
|
+
// Emit event so indexer knows to resync
|
|
162
|
+
this.eventBus.emit('settings:dataCleared', {});
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.error('[SettingsModal] Failed to clear data:', error);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
this.state.clearing = false;
|
|
168
|
+
this._updateContent();
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ESC key to close
|
|
173
|
+
this._escHandler = (e) => {
|
|
174
|
+
if (e.key === 'Escape') this.hide();
|
|
175
|
+
};
|
|
176
|
+
document.addEventListener('keydown', this._escHandler);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
_updateContent() {
|
|
180
|
+
if (this.element) {
|
|
181
|
+
this.element.innerHTML = this._render();
|
|
182
|
+
this._setupEventListeners();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
static injectStyles() {
|
|
187
|
+
if (document.getElementById('mw3-settings-styles')) return;
|
|
188
|
+
|
|
189
|
+
const style = document.createElement('style');
|
|
190
|
+
style.id = 'mw3-settings-styles';
|
|
191
|
+
style.textContent = `
|
|
192
|
+
.mw3-settings-overlay {
|
|
193
|
+
position: fixed;
|
|
194
|
+
top: 0;
|
|
195
|
+
left: 0;
|
|
196
|
+
right: 0;
|
|
197
|
+
bottom: 0;
|
|
198
|
+
background: rgba(0, 0, 0, 0.7);
|
|
199
|
+
display: flex;
|
|
200
|
+
align-items: center;
|
|
201
|
+
justify-content: center;
|
|
202
|
+
z-index: 10000;
|
|
203
|
+
backdrop-filter: blur(4px);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.mw3-settings-modal {
|
|
207
|
+
background: linear-gradient(135deg, #1a1a2e, #16213e);
|
|
208
|
+
border: 1px solid rgba(218, 165, 32, 0.3);
|
|
209
|
+
border-radius: 12px;
|
|
210
|
+
width: 90%;
|
|
211
|
+
max-width: 400px;
|
|
212
|
+
color: #fff;
|
|
213
|
+
font-family: 'Lato', system-ui, sans-serif;
|
|
214
|
+
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.mw3-settings-header {
|
|
218
|
+
display: flex;
|
|
219
|
+
align-items: center;
|
|
220
|
+
justify-content: space-between;
|
|
221
|
+
padding: 1rem 1.25rem;
|
|
222
|
+
border-bottom: 1px solid rgba(218, 165, 32, 0.2);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.mw3-settings-header h2 {
|
|
226
|
+
margin: 0;
|
|
227
|
+
font-size: 1.125rem;
|
|
228
|
+
font-weight: 600;
|
|
229
|
+
color: rgba(218, 165, 32, 0.95);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.mw3-settings-close {
|
|
233
|
+
background: none;
|
|
234
|
+
border: none;
|
|
235
|
+
color: rgba(255, 255, 255, 0.6);
|
|
236
|
+
font-size: 1.5rem;
|
|
237
|
+
cursor: pointer;
|
|
238
|
+
padding: 0;
|
|
239
|
+
line-height: 1;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.mw3-settings-close:hover {
|
|
243
|
+
color: #fff;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.mw3-settings-content {
|
|
247
|
+
padding: 1.25rem;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.mw3-settings-section h3 {
|
|
251
|
+
margin: 0 0 0.5rem 0;
|
|
252
|
+
font-size: 0.9rem;
|
|
253
|
+
font-weight: 600;
|
|
254
|
+
color: rgba(255, 255, 255, 0.9);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.mw3-settings-description {
|
|
258
|
+
margin: 0 0 1rem 0;
|
|
259
|
+
font-size: 0.8rem;
|
|
260
|
+
color: rgba(255, 255, 255, 0.6);
|
|
261
|
+
line-height: 1.4;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.mw3-settings-row {
|
|
265
|
+
margin-bottom: 1rem;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.mw3-settings-toggle {
|
|
269
|
+
display: flex;
|
|
270
|
+
align-items: center;
|
|
271
|
+
gap: 0.75rem;
|
|
272
|
+
cursor: pointer;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.mw3-settings-toggle input {
|
|
276
|
+
display: none;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.mw3-toggle-slider {
|
|
280
|
+
width: 44px;
|
|
281
|
+
height: 24px;
|
|
282
|
+
background: rgba(255, 255, 255, 0.2);
|
|
283
|
+
border-radius: 12px;
|
|
284
|
+
position: relative;
|
|
285
|
+
transition: background 0.2s;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.mw3-toggle-slider::after {
|
|
289
|
+
content: '';
|
|
290
|
+
position: absolute;
|
|
291
|
+
top: 2px;
|
|
292
|
+
left: 2px;
|
|
293
|
+
width: 20px;
|
|
294
|
+
height: 20px;
|
|
295
|
+
background: #fff;
|
|
296
|
+
border-radius: 50%;
|
|
297
|
+
transition: transform 0.2s;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.mw3-settings-toggle input:checked + .mw3-toggle-slider {
|
|
301
|
+
background: rgba(218, 165, 32, 0.8);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.mw3-settings-toggle input:checked + .mw3-toggle-slider::after {
|
|
305
|
+
transform: translateX(20px);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.mw3-toggle-label {
|
|
309
|
+
font-size: 0.875rem;
|
|
310
|
+
color: rgba(255, 255, 255, 0.9);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.mw3-settings-info {
|
|
314
|
+
font-size: 0.75rem;
|
|
315
|
+
color: rgba(255, 255, 255, 0.5);
|
|
316
|
+
margin-bottom: 1rem;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.mw3-settings-note {
|
|
320
|
+
font-size: 0.75rem;
|
|
321
|
+
color: rgba(255, 255, 255, 0.4);
|
|
322
|
+
margin: 0;
|
|
323
|
+
font-style: italic;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.mw3-settings-btn {
|
|
327
|
+
padding: 0.625rem 1rem;
|
|
328
|
+
border-radius: 6px;
|
|
329
|
+
font-size: 0.875rem;
|
|
330
|
+
font-weight: 500;
|
|
331
|
+
cursor: pointer;
|
|
332
|
+
border: none;
|
|
333
|
+
transition: all 0.2s;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
.mw3-settings-btn--primary {
|
|
337
|
+
background: rgba(218, 165, 32, 0.9);
|
|
338
|
+
color: #1a1a2e;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.mw3-settings-btn--primary:hover {
|
|
342
|
+
background: rgba(218, 165, 32, 1);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.mw3-settings-btn--danger {
|
|
346
|
+
background: rgba(255, 99, 71, 0.2);
|
|
347
|
+
color: rgba(255, 99, 71, 0.9);
|
|
348
|
+
border: 1px solid rgba(255, 99, 71, 0.3);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.mw3-settings-btn--danger:hover {
|
|
352
|
+
background: rgba(255, 99, 71, 0.3);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.mw3-settings-btn:disabled {
|
|
356
|
+
opacity: 0.5;
|
|
357
|
+
cursor: not-allowed;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.mw3-settings-footer {
|
|
361
|
+
padding: 1rem 1.25rem;
|
|
362
|
+
border-top: 1px solid rgba(218, 165, 32, 0.2);
|
|
363
|
+
display: flex;
|
|
364
|
+
justify-content: flex-end;
|
|
365
|
+
}
|
|
366
|
+
`;
|
|
367
|
+
document.head.appendChild(style);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export { SettingsModal };
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
// src/components/SyncProgressBar/SyncProgressBar.js
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SyncProgressBar - UI component showing indexer sync status.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* import { SyncProgressBar } from '@monygroupcorp/micro-web3/components';
|
|
8
|
+
* const progressBar = new SyncProgressBar({ indexer, eventBus });
|
|
9
|
+
* progressBar.mount(document.getElementById('sync-status'));
|
|
10
|
+
*/
|
|
11
|
+
class SyncProgressBar {
|
|
12
|
+
constructor({ indexer, eventBus }) {
|
|
13
|
+
if (!indexer) {
|
|
14
|
+
throw new Error('SyncProgressBar requires indexer instance');
|
|
15
|
+
}
|
|
16
|
+
if (!eventBus) {
|
|
17
|
+
throw new Error('SyncProgressBar requires eventBus instance');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
this.indexer = indexer;
|
|
21
|
+
this.eventBus = eventBus;
|
|
22
|
+
this.element = null;
|
|
23
|
+
this.unsubscribers = [];
|
|
24
|
+
|
|
25
|
+
this.state = {
|
|
26
|
+
status: 'initializing',
|
|
27
|
+
progress: 0,
|
|
28
|
+
currentBlock: 0,
|
|
29
|
+
latestBlock: 0,
|
|
30
|
+
error: null
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
mount(container) {
|
|
35
|
+
if (typeof container === 'string') {
|
|
36
|
+
container = document.querySelector(container);
|
|
37
|
+
}
|
|
38
|
+
if (!container) {
|
|
39
|
+
throw new Error('SyncProgressBar: Invalid container');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Create element
|
|
43
|
+
this.element = document.createElement('div');
|
|
44
|
+
this.element.className = 'mw3-sync-progress';
|
|
45
|
+
container.appendChild(this.element);
|
|
46
|
+
|
|
47
|
+
// Subscribe to events
|
|
48
|
+
this._setupListeners();
|
|
49
|
+
|
|
50
|
+
// Initial render
|
|
51
|
+
this._updateState(this.indexer.sync.getStatus());
|
|
52
|
+
this._render();
|
|
53
|
+
|
|
54
|
+
return this;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
unmount() {
|
|
58
|
+
// Cleanup subscriptions
|
|
59
|
+
for (const unsub of this.unsubscribers) {
|
|
60
|
+
unsub();
|
|
61
|
+
}
|
|
62
|
+
this.unsubscribers = [];
|
|
63
|
+
|
|
64
|
+
// Remove element
|
|
65
|
+
if (this.element && this.element.parentNode) {
|
|
66
|
+
this.element.parentNode.removeChild(this.element);
|
|
67
|
+
}
|
|
68
|
+
this.element = null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
_setupListeners() {
|
|
72
|
+
const listeners = [
|
|
73
|
+
['indexer:syncProgress', (data) => {
|
|
74
|
+
this._updateState({
|
|
75
|
+
status: 'syncing',
|
|
76
|
+
progress: data.progress,
|
|
77
|
+
currentBlock: data.currentBlock,
|
|
78
|
+
latestBlock: data.latestBlock
|
|
79
|
+
});
|
|
80
|
+
}],
|
|
81
|
+
['indexer:syncComplete', () => {
|
|
82
|
+
this._updateState({ status: 'synced', progress: 1 });
|
|
83
|
+
}],
|
|
84
|
+
['indexer:error', (data) => {
|
|
85
|
+
this._updateState({ status: 'error', error: data.message });
|
|
86
|
+
}],
|
|
87
|
+
['indexer:paused', () => {
|
|
88
|
+
this._updateState({ status: 'paused' });
|
|
89
|
+
}],
|
|
90
|
+
['indexer:resumed', () => {
|
|
91
|
+
this._updateState({ status: 'syncing' });
|
|
92
|
+
}]
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
for (const [event, handler] of listeners) {
|
|
96
|
+
const unsub = this.eventBus.on(event, handler);
|
|
97
|
+
this.unsubscribers.push(unsub);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
_updateState(updates) {
|
|
102
|
+
this.state = { ...this.state, ...updates };
|
|
103
|
+
this._render();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
_render() {
|
|
107
|
+
if (!this.element) return;
|
|
108
|
+
|
|
109
|
+
const { status, progress, currentBlock, latestBlock, error } = this.state;
|
|
110
|
+
|
|
111
|
+
// Update class for state
|
|
112
|
+
this.element.className = `mw3-sync-progress mw3-sync-progress--${status}`;
|
|
113
|
+
|
|
114
|
+
if (status === 'syncing') {
|
|
115
|
+
const percent = Math.round(progress * 100);
|
|
116
|
+
this.element.innerHTML = `
|
|
117
|
+
<div class="mw3-sync-progress__bar" style="width: ${percent}%"></div>
|
|
118
|
+
<span class="mw3-sync-progress__text">Syncing... ${percent}%</span>
|
|
119
|
+
`;
|
|
120
|
+
} else if (status === 'synced') {
|
|
121
|
+
this.element.innerHTML = `
|
|
122
|
+
<span class="mw3-sync-progress__text">Synced to block ${latestBlock.toLocaleString()}</span>
|
|
123
|
+
`;
|
|
124
|
+
} else if (status === 'error') {
|
|
125
|
+
this.element.innerHTML = `
|
|
126
|
+
<span class="mw3-sync-progress__text">Sync failed: ${error || 'Unknown error'}</span>
|
|
127
|
+
<button class="mw3-sync-progress__retry">Retry</button>
|
|
128
|
+
`;
|
|
129
|
+
|
|
130
|
+
// Add retry handler
|
|
131
|
+
const retryBtn = this.element.querySelector('.mw3-sync-progress__retry');
|
|
132
|
+
if (retryBtn) {
|
|
133
|
+
retryBtn.onclick = () => this.indexer.sync.resync();
|
|
134
|
+
}
|
|
135
|
+
} else if (status === 'paused') {
|
|
136
|
+
this.element.innerHTML = `
|
|
137
|
+
<span class="mw3-sync-progress__text">Sync paused</span>
|
|
138
|
+
<button class="mw3-sync-progress__resume">Resume</button>
|
|
139
|
+
`;
|
|
140
|
+
|
|
141
|
+
const resumeBtn = this.element.querySelector('.mw3-sync-progress__resume');
|
|
142
|
+
if (resumeBtn) {
|
|
143
|
+
resumeBtn.onclick = () => this.indexer.sync.resume();
|
|
144
|
+
}
|
|
145
|
+
} else {
|
|
146
|
+
this.element.innerHTML = `
|
|
147
|
+
<span class="mw3-sync-progress__text">Initializing...</span>
|
|
148
|
+
`;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Inject default styles into document.
|
|
154
|
+
* Call once at app startup if not using custom CSS.
|
|
155
|
+
*/
|
|
156
|
+
static injectStyles() {
|
|
157
|
+
if (document.getElementById('mw3-sync-progress-styles')) return;
|
|
158
|
+
|
|
159
|
+
const style = document.createElement('style');
|
|
160
|
+
style.id = 'mw3-sync-progress-styles';
|
|
161
|
+
style.textContent = `
|
|
162
|
+
.mw3-sync-progress {
|
|
163
|
+
position: relative;
|
|
164
|
+
height: 24px;
|
|
165
|
+
background: var(--mw3-sync-bg, #f0f0f0);
|
|
166
|
+
border-radius: 4px;
|
|
167
|
+
overflow: hidden;
|
|
168
|
+
font-family: system-ui, sans-serif;
|
|
169
|
+
font-size: 12px;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.mw3-sync-progress__bar {
|
|
173
|
+
position: absolute;
|
|
174
|
+
top: 0;
|
|
175
|
+
left: 0;
|
|
176
|
+
height: 100%;
|
|
177
|
+
background: var(--mw3-sync-bar, #4caf50);
|
|
178
|
+
transition: width 0.3s ease;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.mw3-sync-progress__text {
|
|
182
|
+
position: relative;
|
|
183
|
+
z-index: 1;
|
|
184
|
+
display: flex;
|
|
185
|
+
align-items: center;
|
|
186
|
+
justify-content: center;
|
|
187
|
+
height: 100%;
|
|
188
|
+
color: var(--mw3-sync-text, #333);
|
|
189
|
+
padding: 0 8px;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.mw3-sync-progress--synced {
|
|
193
|
+
background: var(--mw3-sync-bar, #4caf50);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.mw3-sync-progress--synced .mw3-sync-progress__text {
|
|
197
|
+
color: white;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.mw3-sync-progress--error {
|
|
201
|
+
background: var(--mw3-sync-error, #f44336);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.mw3-sync-progress--error .mw3-sync-progress__text {
|
|
205
|
+
color: white;
|
|
206
|
+
justify-content: space-between;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.mw3-sync-progress__retry,
|
|
210
|
+
.mw3-sync-progress__resume {
|
|
211
|
+
background: rgba(255,255,255,0.2);
|
|
212
|
+
border: 1px solid rgba(255,255,255,0.4);
|
|
213
|
+
border-radius: 3px;
|
|
214
|
+
color: white;
|
|
215
|
+
padding: 2px 8px;
|
|
216
|
+
cursor: pointer;
|
|
217
|
+
font-size: 11px;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.mw3-sync-progress__retry:hover,
|
|
221
|
+
.mw3-sync-progress__resume:hover {
|
|
222
|
+
background: rgba(255,255,255,0.3);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.mw3-sync-progress--paused {
|
|
226
|
+
background: var(--mw3-sync-paused, #ff9800);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.mw3-sync-progress--paused .mw3-sync-progress__text {
|
|
230
|
+
color: white;
|
|
231
|
+
justify-content: space-between;
|
|
232
|
+
}
|
|
233
|
+
`;
|
|
234
|
+
document.head.appendChild(style);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export { SyncProgressBar };
|