@monygroupcorp/micro-web3 0.1.3 → 1.2.1

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.
@@ -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 };
package/src/index.js CHANGED
@@ -4,6 +4,10 @@ import WalletService from './services/WalletService.js';
4
4
  import ContractCache from './services/ContractCache.js';
5
5
  import PriceService from './services/PriceService.js';
6
6
  import * as IpfsService from './services/IpfsService.js';
7
+ import EventIndexer from './services/EventIndexer.js';
8
+
9
+ // Storage adapters and settings
10
+ import { IndexedDBAdapter, MemoryAdapter, IndexerSettings } from './storage/index.js';
7
11
 
8
12
  // UI Components
9
13
  import { FloatingWalletButton } from './components/FloatingWalletButton/FloatingWalletButton.js';
@@ -19,6 +23,8 @@ import { BondingCurve } from './components/BondingCurve/BondingCurve.js';
19
23
  import { PriceDisplay } from './components/Display/PriceDisplay.js';
20
24
  import { BalanceDisplay } from './components/Display/BalanceDisplay.js';
21
25
  import { MessagePopup } from './components/Util/MessagePopup.js';
26
+ import { SyncProgressBar } from './components/SyncProgressBar/SyncProgressBar.js';
27
+ import { SettingsModal } from './components/SettingsModal/SettingsModal.js';
22
28
 
23
29
  export {
24
30
  // Services
@@ -27,6 +33,12 @@ export {
27
33
  ContractCache,
28
34
  PriceService,
29
35
  IpfsService,
36
+ EventIndexer,
37
+
38
+ // Storage adapters and settings
39
+ IndexedDBAdapter,
40
+ MemoryAdapter,
41
+ IndexerSettings,
30
42
 
31
43
  // UI Components
32
44
  FloatingWalletButton,
@@ -41,5 +53,7 @@ export {
41
53
  BondingCurve,
42
54
  PriceDisplay,
43
55
  BalanceDisplay,
44
- MessagePopup
56
+ MessagePopup,
57
+ SyncProgressBar,
58
+ SettingsModal
45
59
  };
@@ -0,0 +1,218 @@
1
+ // src/indexer/EntityResolver.js
2
+
3
+ /**
4
+ * Entity resolver for EventIndexer.
5
+ * Provides the Entities API for domain-level queries.
6
+ */
7
+ class EntityResolver {
8
+ constructor(queryEngine, entityDefinitions = {}) {
9
+ this.queryEngine = queryEngine;
10
+ this.definitions = entityDefinitions;
11
+ this.entityAPIs = {};
12
+
13
+ // Create API for each entity
14
+ for (const [name, definition] of Object.entries(entityDefinitions)) {
15
+ this.entityAPIs[name] = this._createEntityAPI(name, definition);
16
+ }
17
+ }
18
+
19
+ /**
20
+ * Get entity API by name.
21
+ * @param {string} name - Entity name
22
+ * @returns {EntityQueryable}
23
+ */
24
+ getEntity(name) {
25
+ return this.entityAPIs[name];
26
+ }
27
+
28
+ /**
29
+ * Get all entity APIs (for proxy access).
30
+ * @returns {Object}
31
+ */
32
+ getAllEntities() {
33
+ return this.entityAPIs;
34
+ }
35
+
36
+ _createEntityAPI(name, definition) {
37
+ const self = this;
38
+
39
+ return {
40
+ /**
41
+ * Query entities.
42
+ * @param {EntityWhereClause} where - Filter conditions
43
+ * @returns {Promise<Entity[]>}
44
+ */
45
+ async query(where = {}) {
46
+ return self._queryEntities(name, definition, where);
47
+ },
48
+
49
+ /**
50
+ * Get single entity by key.
51
+ * @param {string|number} key - Entity key value
52
+ * @returns {Promise<Entity|null>}
53
+ */
54
+ async get(key) {
55
+ const entities = await self._queryEntities(name, definition, {
56
+ [definition.key]: key
57
+ });
58
+ return entities[0] || null;
59
+ },
60
+
61
+ /**
62
+ * Subscribe to entity changes.
63
+ * @param {EntityWhereClause} where - Filter conditions
64
+ * @param {Function} callback - Called when entities change
65
+ * @returns {Function} Unsubscribe function
66
+ */
67
+ subscribe(where, callback) {
68
+ // Subscribe to source event and related events
69
+ const eventTypes = self._getRelevantEventTypes(definition);
70
+
71
+ return self.queryEngine.subscribe(eventTypes, async () => {
72
+ const entities = await self._queryEntities(name, definition, where);
73
+ callback(entities);
74
+ });
75
+ },
76
+
77
+ /**
78
+ * Count entities.
79
+ * @param {EntityWhereClause} where - Filter conditions
80
+ * @returns {Promise<number>}
81
+ */
82
+ async count(where = {}) {
83
+ const entities = await self._queryEntities(name, definition, where);
84
+ return entities.length;
85
+ }
86
+ };
87
+ }
88
+
89
+ async _queryEntities(name, definition, where = {}) {
90
+ // Separate status filter from direct filters
91
+ const statusFilter = where.status;
92
+ const directFilters = { ...where };
93
+ delete directFilters.status;
94
+
95
+ // Query source events
96
+ const sourceResult = await this.queryEngine.query(definition.source, {
97
+ where: directFilters,
98
+ limit: 10000 // Get all matching source events
99
+ });
100
+
101
+ // Get event checker for status/computed evaluation
102
+ const eventChecker = await this.queryEngine.getEventChecker();
103
+
104
+ // Prefetch events needed for status evaluation
105
+ const relevantEventTypes = this._getRelevantEventTypes(definition);
106
+ await eventChecker.prefetch(relevantEventTypes);
107
+
108
+ // Build entities from source events
109
+ const entities = [];
110
+ for (const sourceEvent of sourceResult.events) {
111
+ const entity = await this._buildEntity(name, definition, sourceEvent, eventChecker);
112
+
113
+ // Apply status filter
114
+ if (statusFilter && entity.status !== statusFilter) {
115
+ continue;
116
+ }
117
+
118
+ entities.push(entity);
119
+ }
120
+
121
+ return entities;
122
+ }
123
+
124
+ async _buildEntity(name, definition, sourceEvent, eventChecker) {
125
+ const entity = {
126
+ _type: name,
127
+ _sourceEvent: sourceEvent,
128
+ ...sourceEvent.data
129
+ };
130
+
131
+ // Add key field explicitly
132
+ entity[definition.key] = sourceEvent.data[definition.key] || sourceEvent.indexed[definition.key];
133
+
134
+ // Evaluate status
135
+ if (definition.status) {
136
+ entity.status = this._evaluateStatus(entity, definition.status, eventChecker);
137
+ }
138
+
139
+ // Evaluate computed fields
140
+ if (definition.computed) {
141
+ for (const [fieldName, computeFn] of Object.entries(definition.computed)) {
142
+ try {
143
+ entity[fieldName] = computeFn(entity, eventChecker);
144
+ } catch (error) {
145
+ console.error(`[EntityResolver] Error computing ${fieldName}:`, error);
146
+ entity[fieldName] = null;
147
+ }
148
+ }
149
+ }
150
+
151
+ // Resolve relations (lazy - only resolve when accessed)
152
+ if (definition.relations) {
153
+ for (const [relationName, relationDef] of Object.entries(definition.relations)) {
154
+ Object.defineProperty(entity, relationName, {
155
+ get: () => this._resolveRelation(entity, relationDef),
156
+ enumerable: true
157
+ });
158
+ }
159
+ }
160
+
161
+ return entity;
162
+ }
163
+
164
+ _evaluateStatus(entity, statusDef, eventChecker) {
165
+ // Check each status condition in order
166
+ for (const [statusName, condition] of Object.entries(statusDef)) {
167
+ if (statusName === 'default') continue;
168
+
169
+ try {
170
+ if (condition(entity, eventChecker)) {
171
+ return statusName;
172
+ }
173
+ } catch (error) {
174
+ console.error(`[EntityResolver] Error evaluating status ${statusName}:`, error);
175
+ }
176
+ }
177
+
178
+ return statusDef.default || 'unknown';
179
+ }
180
+
181
+ async _resolveRelation(entity, relationDef) {
182
+ const relatedEntity = this.entityAPIs[relationDef.entity];
183
+ if (!relatedEntity) return null;
184
+
185
+ const foreignKeyValue = entity[relationDef.foreignKey];
186
+ if (foreignKeyValue === undefined) return null;
187
+
188
+ if (relationDef.type === 'many') {
189
+ return relatedEntity.query({ [relationDef.foreignKey]: foreignKeyValue });
190
+ } else {
191
+ return relatedEntity.get(foreignKeyValue);
192
+ }
193
+ }
194
+
195
+ _getRelevantEventTypes(definition) {
196
+ const types = new Set([definition.source]);
197
+
198
+ // Add events referenced in status conditions
199
+ if (definition.status) {
200
+ for (const condition of Object.values(definition.status)) {
201
+ if (typeof condition === 'function') {
202
+ // Try to extract event names from function source
203
+ // This is a best-effort heuristic
204
+ const fnStr = condition.toString();
205
+ const matches = fnStr.match(/events\.has\(['"](\w+)['"]/g) || [];
206
+ for (const match of matches) {
207
+ const eventName = match.match(/['"](\w+)['"]/)?.[1];
208
+ if (eventName) types.add(eventName);
209
+ }
210
+ }
211
+ }
212
+ }
213
+
214
+ return Array.from(types);
215
+ }
216
+ }
217
+
218
+ export default EntityResolver;