@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.
- 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/package.json +2 -2
- package/rollup.config.cjs +1 -1
- package/src/components/FloatingWalletButton/FloatingWalletButton.js +53 -21
- package/src/components/SettingsModal/SettingsModal.js +371 -0
- package/src/components/SyncProgressBar/SyncProgressBar.js +238 -0
- package/src/index.js +15 -1
- 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
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
// src/storage/IndexedDBAdapter.js
|
|
2
|
+
|
|
3
|
+
import StorageAdapter from './StorageAdapter.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* IndexedDB storage adapter for EventIndexer.
|
|
7
|
+
* Provides persistent event storage with auto-generated indexes.
|
|
8
|
+
*/
|
|
9
|
+
class IndexedDBAdapter extends StorageAdapter {
|
|
10
|
+
constructor() {
|
|
11
|
+
super();
|
|
12
|
+
this.db = null;
|
|
13
|
+
this.config = null;
|
|
14
|
+
this.dbName = null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async initialize(config) {
|
|
18
|
+
this.config = config;
|
|
19
|
+
this.dbName = config.dbName;
|
|
20
|
+
|
|
21
|
+
// Check for existing version
|
|
22
|
+
const existingVersion = await this._getStoredVersion();
|
|
23
|
+
|
|
24
|
+
// Handle migration: if config version is higher, we need to clear and rebuild
|
|
25
|
+
const needsMigration = existingVersion !== null && config.version > existingVersion;
|
|
26
|
+
|
|
27
|
+
if (needsMigration) {
|
|
28
|
+
console.log(`[IndexedDBAdapter] Migrating from v${existingVersion} to v${config.version}`);
|
|
29
|
+
await this._deleteDatabase();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
const request = indexedDB.open(this.dbName, config.version);
|
|
34
|
+
|
|
35
|
+
request.onerror = () => {
|
|
36
|
+
reject(new Error(`Failed to open IndexedDB: ${request.error?.message}`));
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
request.onsuccess = () => {
|
|
40
|
+
this.db = request.result;
|
|
41
|
+
resolve();
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
request.onupgradeneeded = (event) => {
|
|
45
|
+
const db = event.target.result;
|
|
46
|
+
this._createStores(db, config.eventTypes);
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
_createStores(db, eventTypes) {
|
|
52
|
+
// Create store for each event type
|
|
53
|
+
for (const eventType of eventTypes) {
|
|
54
|
+
const storeName = `events_${eventType.name}`;
|
|
55
|
+
|
|
56
|
+
if (!db.objectStoreNames.contains(storeName)) {
|
|
57
|
+
const store = db.createObjectStore(storeName, { keyPath: 'id' });
|
|
58
|
+
|
|
59
|
+
// Create indexes for indexed params (from ABI)
|
|
60
|
+
for (const param of eventType.indexedParams || []) {
|
|
61
|
+
store.createIndex(`param_${param}`, `indexed.${param}`, { unique: false });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Always create blockNumber and timestamp indexes
|
|
65
|
+
store.createIndex('blockNumber', 'blockNumber', { unique: false });
|
|
66
|
+
store.createIndex('timestamp', 'timestamp', { unique: false });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Create sync state store
|
|
71
|
+
if (!db.objectStoreNames.contains('syncState')) {
|
|
72
|
+
db.createObjectStore('syncState', { keyPath: 'id' });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Create meta store for version tracking
|
|
76
|
+
if (!db.objectStoreNames.contains('meta')) {
|
|
77
|
+
db.createObjectStore('meta', { keyPath: 'key' });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async _getStoredVersion() {
|
|
82
|
+
return new Promise((resolve) => {
|
|
83
|
+
const request = indexedDB.open(this.dbName);
|
|
84
|
+
request.onsuccess = () => {
|
|
85
|
+
const db = request.result;
|
|
86
|
+
const version = db.version;
|
|
87
|
+
db.close();
|
|
88
|
+
resolve(version);
|
|
89
|
+
};
|
|
90
|
+
request.onerror = () => resolve(null);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async _deleteDatabase() {
|
|
95
|
+
return new Promise((resolve, reject) => {
|
|
96
|
+
if (this.db) {
|
|
97
|
+
this.db.close();
|
|
98
|
+
this.db = null;
|
|
99
|
+
}
|
|
100
|
+
const request = indexedDB.deleteDatabase(this.dbName);
|
|
101
|
+
request.onsuccess = () => resolve();
|
|
102
|
+
request.onerror = () => reject(request.error);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async putEvents(events) {
|
|
107
|
+
if (!this.db) throw new Error('IndexedDBAdapter not initialized');
|
|
108
|
+
if (events.length === 0) return;
|
|
109
|
+
|
|
110
|
+
// Group events by type for batch operations
|
|
111
|
+
const eventsByType = new Map();
|
|
112
|
+
for (const event of events) {
|
|
113
|
+
if (!eventsByType.has(event.type)) {
|
|
114
|
+
eventsByType.set(event.type, []);
|
|
115
|
+
}
|
|
116
|
+
eventsByType.get(event.type).push(event);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Write each type in a transaction
|
|
120
|
+
const promises = [];
|
|
121
|
+
for (const [type, typeEvents] of eventsByType) {
|
|
122
|
+
promises.push(this._putEventsOfType(type, typeEvents));
|
|
123
|
+
}
|
|
124
|
+
await Promise.all(promises);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async _putEventsOfType(type, events) {
|
|
128
|
+
const storeName = `events_${type}`;
|
|
129
|
+
|
|
130
|
+
// Check if store exists, create if needed
|
|
131
|
+
if (!this.db.objectStoreNames.contains(storeName)) {
|
|
132
|
+
// Need to reopen with version upgrade to add new store
|
|
133
|
+
await this._addStore(type);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return new Promise((resolve, reject) => {
|
|
137
|
+
const tx = this.db.transaction(storeName, 'readwrite');
|
|
138
|
+
const store = tx.objectStore(storeName);
|
|
139
|
+
|
|
140
|
+
for (const event of events) {
|
|
141
|
+
store.put(event);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
tx.oncomplete = () => resolve();
|
|
145
|
+
tx.onerror = () => reject(tx.error);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async _addStore(eventType) {
|
|
150
|
+
// Close current connection
|
|
151
|
+
const currentVersion = this.db.version;
|
|
152
|
+
this.db.close();
|
|
153
|
+
|
|
154
|
+
// Reopen with incremented version
|
|
155
|
+
return new Promise((resolve, reject) => {
|
|
156
|
+
const request = indexedDB.open(this.dbName, currentVersion + 1);
|
|
157
|
+
|
|
158
|
+
request.onupgradeneeded = (event) => {
|
|
159
|
+
const db = event.target.result;
|
|
160
|
+
const storeName = `events_${eventType}`;
|
|
161
|
+
if (!db.objectStoreNames.contains(storeName)) {
|
|
162
|
+
const store = db.createObjectStore(storeName, { keyPath: 'id' });
|
|
163
|
+
store.createIndex('blockNumber', 'blockNumber', { unique: false });
|
|
164
|
+
store.createIndex('timestamp', 'timestamp', { unique: false });
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
request.onsuccess = () => {
|
|
169
|
+
this.db = request.result;
|
|
170
|
+
resolve();
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
request.onerror = () => reject(request.error);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async getEvent(type, id) {
|
|
178
|
+
if (!this.db) throw new Error('IndexedDBAdapter not initialized');
|
|
179
|
+
|
|
180
|
+
const storeName = `events_${type}`;
|
|
181
|
+
if (!this.db.objectStoreNames.contains(storeName)) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return new Promise((resolve, reject) => {
|
|
186
|
+
const tx = this.db.transaction(storeName, 'readonly');
|
|
187
|
+
const store = tx.objectStore(storeName);
|
|
188
|
+
const request = store.get(id);
|
|
189
|
+
|
|
190
|
+
request.onsuccess = () => resolve(request.result || null);
|
|
191
|
+
request.onerror = () => reject(request.error);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async queryEvents(type, options = {}) {
|
|
196
|
+
if (!this.db) throw new Error('IndexedDBAdapter not initialized');
|
|
197
|
+
|
|
198
|
+
const storeName = `events_${type}`;
|
|
199
|
+
if (!this.db.objectStoreNames.contains(storeName)) {
|
|
200
|
+
return { events: [], total: 0, hasMore: false };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return new Promise((resolve, reject) => {
|
|
204
|
+
const tx = this.db.transaction(storeName, 'readonly');
|
|
205
|
+
const store = tx.objectStore(storeName);
|
|
206
|
+
|
|
207
|
+
// Get all events (we'll filter/sort in memory for complex queries)
|
|
208
|
+
// For simple indexed queries, we could use IDB indexes, but this is more flexible
|
|
209
|
+
const request = store.getAll();
|
|
210
|
+
|
|
211
|
+
request.onsuccess = () => {
|
|
212
|
+
let events = request.result;
|
|
213
|
+
|
|
214
|
+
// Apply where filters
|
|
215
|
+
if (options.where) {
|
|
216
|
+
events = events.filter(event => this._matchesWhere(event, options.where));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const total = events.length;
|
|
220
|
+
|
|
221
|
+
// Sort
|
|
222
|
+
const orderBy = options.orderBy || 'blockNumber';
|
|
223
|
+
const order = options.order || 'desc';
|
|
224
|
+
events.sort((a, b) => {
|
|
225
|
+
const aVal = this._getNestedValue(a, orderBy);
|
|
226
|
+
const bVal = this._getNestedValue(b, orderBy);
|
|
227
|
+
if (aVal < bVal) return order === 'asc' ? -1 : 1;
|
|
228
|
+
if (aVal > bVal) return order === 'asc' ? 1 : -1;
|
|
229
|
+
return 0;
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// Pagination
|
|
233
|
+
const offset = options.offset || 0;
|
|
234
|
+
const limit = options.limit || 100;
|
|
235
|
+
const paged = events.slice(offset, offset + limit);
|
|
236
|
+
|
|
237
|
+
resolve({
|
|
238
|
+
events: paged,
|
|
239
|
+
total,
|
|
240
|
+
hasMore: offset + limit < total
|
|
241
|
+
});
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
request.onerror = () => reject(request.error);
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async countEvents(type, where) {
|
|
249
|
+
if (!this.db) throw new Error('IndexedDBAdapter not initialized');
|
|
250
|
+
|
|
251
|
+
const storeName = `events_${type}`;
|
|
252
|
+
if (!this.db.objectStoreNames.contains(storeName)) {
|
|
253
|
+
return 0;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return new Promise((resolve, reject) => {
|
|
257
|
+
const tx = this.db.transaction(storeName, 'readonly');
|
|
258
|
+
const store = tx.objectStore(storeName);
|
|
259
|
+
|
|
260
|
+
if (!where) {
|
|
261
|
+
const request = store.count();
|
|
262
|
+
request.onsuccess = () => resolve(request.result);
|
|
263
|
+
request.onerror = () => reject(request.error);
|
|
264
|
+
} else {
|
|
265
|
+
// Need to filter manually
|
|
266
|
+
const request = store.getAll();
|
|
267
|
+
request.onsuccess = () => {
|
|
268
|
+
const count = request.result.filter(e => this._matchesWhere(e, where)).length;
|
|
269
|
+
resolve(count);
|
|
270
|
+
};
|
|
271
|
+
request.onerror = () => reject(request.error);
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async deleteEvents(type, ids) {
|
|
277
|
+
if (!this.db) throw new Error('IndexedDBAdapter not initialized');
|
|
278
|
+
|
|
279
|
+
const storeName = `events_${type}`;
|
|
280
|
+
if (!this.db.objectStoreNames.contains(storeName)) return;
|
|
281
|
+
|
|
282
|
+
return new Promise((resolve, reject) => {
|
|
283
|
+
const tx = this.db.transaction(storeName, 'readwrite');
|
|
284
|
+
const store = tx.objectStore(storeName);
|
|
285
|
+
|
|
286
|
+
for (const id of ids) {
|
|
287
|
+
store.delete(id);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
tx.oncomplete = () => resolve();
|
|
291
|
+
tx.onerror = () => reject(tx.error);
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async deleteEventsAfterBlock(blockNumber) {
|
|
296
|
+
if (!this.db) throw new Error('IndexedDBAdapter not initialized');
|
|
297
|
+
|
|
298
|
+
const storeNames = Array.from(this.db.objectStoreNames)
|
|
299
|
+
.filter(name => name.startsWith('events_'));
|
|
300
|
+
|
|
301
|
+
for (const storeName of storeNames) {
|
|
302
|
+
await this._deleteEventsAfterBlockInStore(storeName, blockNumber);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async _deleteEventsAfterBlockInStore(storeName, blockNumber) {
|
|
307
|
+
return new Promise((resolve, reject) => {
|
|
308
|
+
const tx = this.db.transaction(storeName, 'readwrite');
|
|
309
|
+
const store = tx.objectStore(storeName);
|
|
310
|
+
const index = store.index('blockNumber');
|
|
311
|
+
const range = IDBKeyRange.lowerBound(blockNumber, true); // exclusive
|
|
312
|
+
|
|
313
|
+
const request = index.openCursor(range);
|
|
314
|
+
request.onsuccess = (event) => {
|
|
315
|
+
const cursor = event.target.result;
|
|
316
|
+
if (cursor) {
|
|
317
|
+
cursor.delete();
|
|
318
|
+
cursor.continue();
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
tx.oncomplete = () => resolve();
|
|
323
|
+
tx.onerror = () => reject(tx.error);
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async getSyncState() {
|
|
328
|
+
if (!this.db) throw new Error('IndexedDBAdapter not initialized');
|
|
329
|
+
|
|
330
|
+
return new Promise((resolve, reject) => {
|
|
331
|
+
const tx = this.db.transaction('syncState', 'readonly');
|
|
332
|
+
const store = tx.objectStore('syncState');
|
|
333
|
+
const request = store.get('current');
|
|
334
|
+
|
|
335
|
+
request.onsuccess = () => resolve(request.result || null);
|
|
336
|
+
request.onerror = () => reject(request.error);
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async setSyncState(state) {
|
|
341
|
+
if (!this.db) throw new Error('IndexedDBAdapter not initialized');
|
|
342
|
+
|
|
343
|
+
return new Promise((resolve, reject) => {
|
|
344
|
+
const tx = this.db.transaction('syncState', 'readwrite');
|
|
345
|
+
const store = tx.objectStore('syncState');
|
|
346
|
+
const request = store.put({ id: 'current', ...state });
|
|
347
|
+
|
|
348
|
+
request.onsuccess = () => resolve();
|
|
349
|
+
request.onerror = () => reject(request.error);
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async getVersion() {
|
|
354
|
+
if (!this.db) return 0;
|
|
355
|
+
return this.db.version;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async setVersion(version) {
|
|
359
|
+
// Version is managed by IndexedDB upgrade mechanism
|
|
360
|
+
// This is a no-op, included for interface compliance
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async clear() {
|
|
364
|
+
if (!this.db) return;
|
|
365
|
+
|
|
366
|
+
const storeNames = Array.from(this.db.objectStoreNames);
|
|
367
|
+
|
|
368
|
+
return new Promise((resolve, reject) => {
|
|
369
|
+
const tx = this.db.transaction(storeNames, 'readwrite');
|
|
370
|
+
|
|
371
|
+
for (const storeName of storeNames) {
|
|
372
|
+
tx.objectStore(storeName).clear();
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
tx.oncomplete = () => resolve();
|
|
376
|
+
tx.onerror = () => reject(tx.error);
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async close() {
|
|
381
|
+
if (this.db) {
|
|
382
|
+
this.db.close();
|
|
383
|
+
this.db = null;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Same helpers as MemoryAdapter
|
|
388
|
+
_matchesWhere(event, where) {
|
|
389
|
+
for (const [key, value] of Object.entries(where)) {
|
|
390
|
+
const eventValue = this._getNestedValue(event, key);
|
|
391
|
+
|
|
392
|
+
if (Array.isArray(value)) {
|
|
393
|
+
if (!value.includes(eventValue)) return false;
|
|
394
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
395
|
+
if (value.$gt !== undefined && !(eventValue > value.$gt)) return false;
|
|
396
|
+
if (value.$gte !== undefined && !(eventValue >= value.$gte)) return false;
|
|
397
|
+
if (value.$lt !== undefined && !(eventValue < value.$lt)) return false;
|
|
398
|
+
if (value.$lte !== undefined && !(eventValue <= value.$lte)) return false;
|
|
399
|
+
if (value.$ne !== undefined && eventValue === value.$ne) return false;
|
|
400
|
+
} else {
|
|
401
|
+
const normalizedEvent = typeof eventValue === 'string' ? eventValue.toLowerCase() : eventValue;
|
|
402
|
+
const normalizedValue = typeof value === 'string' ? value.toLowerCase() : value;
|
|
403
|
+
if (normalizedEvent !== normalizedValue) return false;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return true;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
_getNestedValue(obj, path) {
|
|
410
|
+
if (path.startsWith('indexed.')) {
|
|
411
|
+
return obj.indexed?.[path.slice(8)];
|
|
412
|
+
}
|
|
413
|
+
if (path.startsWith('data.')) {
|
|
414
|
+
return obj.data?.[path.slice(5)];
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (obj.indexed?.[path] !== undefined) return obj.indexed[path];
|
|
418
|
+
if (obj.data?.[path] !== undefined) return obj.data[path];
|
|
419
|
+
return obj[path];
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export default IndexedDBAdapter;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// src/storage/IndexerSettings.js
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* User preferences for EventIndexer storage.
|
|
5
|
+
* Persisted in localStorage.
|
|
6
|
+
*/
|
|
7
|
+
const STORAGE_KEY = 'mw3_indexer_settings';
|
|
8
|
+
|
|
9
|
+
const IndexerSettings = {
|
|
10
|
+
/**
|
|
11
|
+
* Get all settings.
|
|
12
|
+
* @returns {Object}
|
|
13
|
+
*/
|
|
14
|
+
get() {
|
|
15
|
+
try {
|
|
16
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
|
17
|
+
return stored ? JSON.parse(stored) : this.getDefaults();
|
|
18
|
+
} catch {
|
|
19
|
+
return this.getDefaults();
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get default settings.
|
|
25
|
+
* @returns {Object}
|
|
26
|
+
*/
|
|
27
|
+
getDefaults() {
|
|
28
|
+
return {
|
|
29
|
+
storageEnabled: true, // Allow IndexedDB storage
|
|
30
|
+
maxStorageMB: 50, // Max storage in MB (0 = unlimited)
|
|
31
|
+
};
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Update settings.
|
|
36
|
+
* @param {Object} updates - Partial settings to update
|
|
37
|
+
*/
|
|
38
|
+
set(updates) {
|
|
39
|
+
const current = this.get();
|
|
40
|
+
const updated = { ...current, ...updates };
|
|
41
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(updated));
|
|
42
|
+
return updated;
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Check if IndexedDB storage is enabled.
|
|
47
|
+
* @returns {boolean}
|
|
48
|
+
*/
|
|
49
|
+
isStorageEnabled() {
|
|
50
|
+
return this.get().storageEnabled;
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Clear all indexer data from IndexedDB.
|
|
55
|
+
* @returns {Promise<void>}
|
|
56
|
+
*/
|
|
57
|
+
async clearAllData() {
|
|
58
|
+
// Find and delete all micro-web3 databases
|
|
59
|
+
if (typeof indexedDB === 'undefined') return;
|
|
60
|
+
|
|
61
|
+
const databases = await indexedDB.databases?.() || [];
|
|
62
|
+
const mw3Databases = databases.filter(db =>
|
|
63
|
+
db.name?.startsWith('micro-web3-events-') ||
|
|
64
|
+
db.name?.includes('colasseum-')
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
for (const db of mw3Databases) {
|
|
68
|
+
await new Promise((resolve, reject) => {
|
|
69
|
+
const request = indexedDB.deleteDatabase(db.name);
|
|
70
|
+
request.onsuccess = resolve;
|
|
71
|
+
request.onerror = reject;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Estimate current storage usage.
|
|
78
|
+
* @returns {Promise<{used: number, quota: number}>} Bytes
|
|
79
|
+
*/
|
|
80
|
+
async getStorageEstimate() {
|
|
81
|
+
if (navigator.storage?.estimate) {
|
|
82
|
+
return navigator.storage.estimate();
|
|
83
|
+
}
|
|
84
|
+
return { used: 0, quota: 0 };
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export default IndexerSettings;
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// src/storage/MemoryAdapter.js
|
|
2
|
+
|
|
3
|
+
import StorageAdapter from './StorageAdapter.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* In-memory storage adapter for EventIndexer.
|
|
7
|
+
* Used as fallback when IndexedDB unavailable (SSR, private browsing).
|
|
8
|
+
* No persistence across sessions.
|
|
9
|
+
*/
|
|
10
|
+
class MemoryAdapter extends StorageAdapter {
|
|
11
|
+
constructor() {
|
|
12
|
+
super();
|
|
13
|
+
this.stores = new Map(); // eventType -> Map<id, event>
|
|
14
|
+
this.syncState = null;
|
|
15
|
+
this.version = 0;
|
|
16
|
+
this.config = null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async initialize(config) {
|
|
20
|
+
this.config = config;
|
|
21
|
+
|
|
22
|
+
// Create store for each event type
|
|
23
|
+
for (const eventType of config.eventTypes) {
|
|
24
|
+
this.stores.set(eventType.name, new Map());
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Check version for migration
|
|
28
|
+
if (config.version > this.version) {
|
|
29
|
+
await this.clear();
|
|
30
|
+
this.version = config.version;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async putEvents(events) {
|
|
35
|
+
for (const event of events) {
|
|
36
|
+
const store = this.stores.get(event.type);
|
|
37
|
+
if (!store) {
|
|
38
|
+
// Lazily create store for unknown event types
|
|
39
|
+
this.stores.set(event.type, new Map());
|
|
40
|
+
}
|
|
41
|
+
this.stores.get(event.type).set(event.id, event);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async getEvent(type, id) {
|
|
46
|
+
const store = this.stores.get(type);
|
|
47
|
+
if (!store) return null;
|
|
48
|
+
return store.get(id) || null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async queryEvents(type, options = {}) {
|
|
52
|
+
const store = this.stores.get(type);
|
|
53
|
+
if (!store) {
|
|
54
|
+
return { events: [], total: 0, hasMore: false };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let events = Array.from(store.values());
|
|
58
|
+
|
|
59
|
+
// Apply where filters
|
|
60
|
+
if (options.where) {
|
|
61
|
+
events = events.filter(event => this._matchesWhere(event, options.where));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const total = events.length;
|
|
65
|
+
|
|
66
|
+
// Sort
|
|
67
|
+
const orderBy = options.orderBy || 'blockNumber';
|
|
68
|
+
const order = options.order || 'desc';
|
|
69
|
+
events.sort((a, b) => {
|
|
70
|
+
const aVal = this._getNestedValue(a, orderBy);
|
|
71
|
+
const bVal = this._getNestedValue(b, orderBy);
|
|
72
|
+
if (aVal < bVal) return order === 'asc' ? -1 : 1;
|
|
73
|
+
if (aVal > bVal) return order === 'asc' ? 1 : -1;
|
|
74
|
+
return 0;
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Pagination
|
|
78
|
+
const offset = options.offset || 0;
|
|
79
|
+
const limit = options.limit || 100;
|
|
80
|
+
const paged = events.slice(offset, offset + limit);
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
events: paged,
|
|
84
|
+
total,
|
|
85
|
+
hasMore: offset + limit < total
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async countEvents(type, where) {
|
|
90
|
+
const store = this.stores.get(type);
|
|
91
|
+
if (!store) return 0;
|
|
92
|
+
|
|
93
|
+
if (!where) return store.size;
|
|
94
|
+
|
|
95
|
+
let count = 0;
|
|
96
|
+
for (const event of store.values()) {
|
|
97
|
+
if (this._matchesWhere(event, where)) {
|
|
98
|
+
count++;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return count;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async deleteEvents(type, ids) {
|
|
105
|
+
const store = this.stores.get(type);
|
|
106
|
+
if (!store) return;
|
|
107
|
+
|
|
108
|
+
for (const id of ids) {
|
|
109
|
+
store.delete(id);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async deleteEventsAfterBlock(blockNumber) {
|
|
114
|
+
for (const store of this.stores.values()) {
|
|
115
|
+
for (const [id, event] of store.entries()) {
|
|
116
|
+
if (event.blockNumber > blockNumber) {
|
|
117
|
+
store.delete(id);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async getSyncState() {
|
|
124
|
+
return this.syncState;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async setSyncState(state) {
|
|
128
|
+
this.syncState = { ...state };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async getVersion() {
|
|
132
|
+
return this.version;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async setVersion(version) {
|
|
136
|
+
this.version = version;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async clear() {
|
|
140
|
+
for (const store of this.stores.values()) {
|
|
141
|
+
store.clear();
|
|
142
|
+
}
|
|
143
|
+
this.syncState = null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async close() {
|
|
147
|
+
// No-op for memory adapter
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Helper: check if event matches where clause
|
|
151
|
+
_matchesWhere(event, where) {
|
|
152
|
+
for (const [key, value] of Object.entries(where)) {
|
|
153
|
+
const eventValue = this._getNestedValue(event, key);
|
|
154
|
+
|
|
155
|
+
// Handle array values (OR condition)
|
|
156
|
+
if (Array.isArray(value)) {
|
|
157
|
+
if (!value.includes(eventValue)) return false;
|
|
158
|
+
}
|
|
159
|
+
// Handle comparison operators
|
|
160
|
+
else if (typeof value === 'object' && value !== null) {
|
|
161
|
+
if (value.$gt !== undefined && !(eventValue > value.$gt)) return false;
|
|
162
|
+
if (value.$gte !== undefined && !(eventValue >= value.$gte)) return false;
|
|
163
|
+
if (value.$lt !== undefined && !(eventValue < value.$lt)) return false;
|
|
164
|
+
if (value.$lte !== undefined && !(eventValue <= value.$lte)) return false;
|
|
165
|
+
if (value.$ne !== undefined && eventValue === value.$ne) return false;
|
|
166
|
+
}
|
|
167
|
+
// Direct equality (case-insensitive for addresses)
|
|
168
|
+
else {
|
|
169
|
+
const normalizedEvent = typeof eventValue === 'string' ? eventValue.toLowerCase() : eventValue;
|
|
170
|
+
const normalizedValue = typeof value === 'string' ? value.toLowerCase() : value;
|
|
171
|
+
if (normalizedEvent !== normalizedValue) return false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Helper: get nested value from object using dot notation
|
|
178
|
+
_getNestedValue(obj, path) {
|
|
179
|
+
// Check indexed params first, then data, then direct
|
|
180
|
+
if (path.startsWith('indexed.')) {
|
|
181
|
+
return obj.indexed?.[path.slice(8)];
|
|
182
|
+
}
|
|
183
|
+
if (path.startsWith('data.')) {
|
|
184
|
+
return obj.data?.[path.slice(5)];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Try indexed, then data, then direct property
|
|
188
|
+
if (obj.indexed?.[path] !== undefined) return obj.indexed[path];
|
|
189
|
+
if (obj.data?.[path] !== undefined) return obj.data[path];
|
|
190
|
+
return obj[path];
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export default MemoryAdapter;
|