@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,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;