@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,399 @@
1
+ // src/services/EventIndexer.js
2
+
3
+ import { selectAdapter } from '../storage/index.js';
4
+ import { QueryEngine, SyncEngine, EntityResolver, Patterns } from '../indexer/index.js';
5
+ import { ethers } from 'ethers';
6
+
7
+ /**
8
+ * Error types for EventIndexer.
9
+ */
10
+ class EventIndexerError extends Error {
11
+ constructor(message, code, cause) {
12
+ super(message);
13
+ this.name = 'EventIndexerError';
14
+ this.code = code;
15
+ this.cause = cause;
16
+ }
17
+ }
18
+
19
+ const ErrorCodes = {
20
+ NOT_INITIALIZED: 'NOT_INITIALIZED',
21
+ STORAGE_ERROR: 'STORAGE_ERROR',
22
+ SYNC_ERROR: 'SYNC_ERROR',
23
+ QUERY_ERROR: 'QUERY_ERROR',
24
+ INVALID_CONFIG: 'INVALID_CONFIG',
25
+ PROVIDER_ERROR: 'PROVIDER_ERROR',
26
+ REORG_DETECTED: 'REORG_DETECTED'
27
+ };
28
+
29
+ /**
30
+ * EventIndexer - Client-side event indexing for dApps.
31
+ *
32
+ * Provides three levels of abstraction:
33
+ * - Events: Raw indexed events with filtering
34
+ * - Entities: Domain objects derived from events
35
+ * - Patterns: Pre-built solutions (activity feeds, leaderboards)
36
+ */
37
+ class EventIndexer {
38
+ /**
39
+ * Create EventIndexer instance.
40
+ * @param {EventBus} eventBus - microact EventBus instance
41
+ * @param {Object} options - Options
42
+ * @param {BlockchainService} options.blockchainService - For provider access (optional)
43
+ */
44
+ constructor(eventBus, options = {}) {
45
+ if (!eventBus) {
46
+ throw new EventIndexerError('EventBus is required', ErrorCodes.INVALID_CONFIG);
47
+ }
48
+
49
+ this.eventBus = eventBus;
50
+ this.blockchainService = options.blockchainService;
51
+
52
+ // Internal components (initialized in initialize())
53
+ this.storage = null;
54
+ this.queryEngine = null;
55
+ this.syncEngine = null;
56
+ this.entityResolver = null;
57
+ this.patternsAPI = null;
58
+ this.contract = null;
59
+
60
+ this.initialized = false;
61
+ this.config = null;
62
+ }
63
+
64
+ /**
65
+ * Initialize the EventIndexer.
66
+ * @param {EventIndexerConfig} config - Configuration
67
+ */
68
+ async initialize(config) {
69
+ this._validateConfig(config);
70
+ this.config = config;
71
+
72
+ try {
73
+ // Get provider
74
+ const provider = this._getProvider(config);
75
+
76
+ // Get chain ID
77
+ const network = await provider.getNetwork();
78
+ const chainId = Number(network.chainId);
79
+
80
+ // Extract event types from ABI
81
+ const eventTypes = this._extractEventTypes(config.contract.abi);
82
+
83
+ // Initialize storage
84
+ this.storage = selectAdapter(config.persistence);
85
+ await this.storage.initialize({
86
+ dbName: config.persistence?.dbName || `micro-web3-events-${chainId}`,
87
+ version: config.persistence?.version || 1,
88
+ eventTypes
89
+ });
90
+
91
+ // Create contract instance
92
+ this.contract = new ethers.Contract(
93
+ config.contract.address,
94
+ config.contract.abi,
95
+ provider
96
+ );
97
+
98
+ // Detect deploy block if not provided
99
+ const deployBlock = config.contract.deployBlock || await this._detectDeployBlock(provider);
100
+
101
+ // Initialize components
102
+ this.queryEngine = new QueryEngine(this.storage, this.eventBus);
103
+
104
+ this.syncEngine = new SyncEngine(
105
+ this.storage,
106
+ this.queryEngine,
107
+ this.eventBus,
108
+ config.sync || {}
109
+ );
110
+ await this.syncEngine.initialize({
111
+ provider,
112
+ contract: this.contract,
113
+ eventTypes,
114
+ deployBlock,
115
+ chainId
116
+ });
117
+
118
+ // Initialize entity resolver if entities defined
119
+ if (config.entities) {
120
+ this.entityResolver = new EntityResolver(this.queryEngine, config.entities);
121
+ }
122
+
123
+ // Initialize patterns
124
+ this.patternsAPI = new Patterns(this.queryEngine, this.entityResolver);
125
+
126
+ this.initialized = true;
127
+ this.eventBus.emit('indexer:initialized', {});
128
+
129
+ // Start syncing
130
+ await this.syncEngine.start();
131
+
132
+ } catch (error) {
133
+ this.eventBus.emit('indexer:error', {
134
+ code: ErrorCodes.STORAGE_ERROR,
135
+ message: `Initialization failed: ${error.message}`,
136
+ cause: error,
137
+ recoverable: false
138
+ });
139
+ throw new EventIndexerError(
140
+ `Failed to initialize EventIndexer: ${error.message}`,
141
+ ErrorCodes.STORAGE_ERROR,
142
+ error
143
+ );
144
+ }
145
+ }
146
+
147
+ _validateConfig(config) {
148
+ if (!config.contract) {
149
+ throw new EventIndexerError('contract config is required', ErrorCodes.INVALID_CONFIG);
150
+ }
151
+ if (!config.contract.address) {
152
+ throw new EventIndexerError('contract.address is required', ErrorCodes.INVALID_CONFIG);
153
+ }
154
+ if (!config.contract.abi || !Array.isArray(config.contract.abi)) {
155
+ throw new EventIndexerError('contract.abi is required and must be an array', ErrorCodes.INVALID_CONFIG);
156
+ }
157
+ if (!config.provider && !this.blockchainService) {
158
+ throw new EventIndexerError(
159
+ 'Either provider in config or blockchainService in constructor is required',
160
+ ErrorCodes.INVALID_CONFIG
161
+ );
162
+ }
163
+ }
164
+
165
+ _getProvider(config) {
166
+ if (config.provider) {
167
+ return config.provider;
168
+ }
169
+ if (this.blockchainService?.getProvider) {
170
+ return this.blockchainService.getProvider();
171
+ }
172
+ if (this.blockchainService?.provider) {
173
+ return this.blockchainService.provider;
174
+ }
175
+ throw new EventIndexerError('No provider available', ErrorCodes.PROVIDER_ERROR);
176
+ }
177
+
178
+ _extractEventTypes(abi) {
179
+ const eventTypes = [];
180
+
181
+ for (const item of abi) {
182
+ if (item.type === 'event') {
183
+ const indexedParams = item.inputs
184
+ .filter(input => input.indexed)
185
+ .map(input => input.name);
186
+
187
+ eventTypes.push({
188
+ name: item.name,
189
+ inputs: item.inputs,
190
+ indexedParams
191
+ });
192
+ }
193
+ }
194
+
195
+ return eventTypes;
196
+ }
197
+
198
+ async _detectDeployBlock(provider) {
199
+ // Default to 0 if we can't detect
200
+ // In production, you'd want to either require this or use a heuristic
201
+ console.warn('[EventIndexer] No deployBlock specified, starting from block 0');
202
+ return 0;
203
+ }
204
+
205
+ /**
206
+ * Events API - Level 1 (Low-level)
207
+ * Direct access to raw indexed events.
208
+ */
209
+ get events() {
210
+ this._checkInitialized();
211
+ return {
212
+ query: (eventName, options) => this.queryEngine.query(eventName, options),
213
+ get: (eventName, id) => this.queryEngine.get(eventName, id),
214
+ subscribe: (eventNames, callback) => this.queryEngine.subscribe(eventNames, callback),
215
+ count: (eventName, where) => this.queryEngine.count(eventName, where)
216
+ };
217
+ }
218
+
219
+ /**
220
+ * Entities API - Level 2 (Domain-level)
221
+ * Access to domain objects derived from events.
222
+ * Returns proxy for entity.EntityName access pattern.
223
+ */
224
+ get entities() {
225
+ this._checkInitialized();
226
+
227
+ if (!this.entityResolver) {
228
+ console.warn('[EventIndexer] No entities configured');
229
+ return {};
230
+ }
231
+
232
+ return new Proxy({}, {
233
+ get: (target, prop) => {
234
+ return this.entityResolver.getEntity(prop);
235
+ }
236
+ });
237
+ }
238
+
239
+ /**
240
+ * Patterns API - Level 3 (Zero-config)
241
+ * Pre-built solutions for common needs.
242
+ */
243
+ get patterns() {
244
+ this._checkInitialized();
245
+ return this.patternsAPI;
246
+ }
247
+
248
+ /**
249
+ * Sync API - Control sync behavior.
250
+ */
251
+ get sync() {
252
+ this._checkInitialized();
253
+ return {
254
+ getStatus: () => this.syncEngine.getStatus(),
255
+ resync: (fromBlock) => this.syncEngine.resync(fromBlock),
256
+ clear: () => this.storage.clear(),
257
+ pause: () => this.syncEngine.pause(),
258
+ resume: () => this.syncEngine.resume()
259
+ };
260
+ }
261
+
262
+ _checkInitialized() {
263
+ if (!this.initialized) {
264
+ throw new EventIndexerError(
265
+ 'EventIndexer not initialized. Call initialize() first.',
266
+ ErrorCodes.NOT_INITIALIZED
267
+ );
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Cleanup and close connections.
273
+ */
274
+ async destroy() {
275
+ if (this.syncEngine) {
276
+ await this.syncEngine.stop();
277
+ }
278
+ if (this.storage) {
279
+ await this.storage.close();
280
+ }
281
+ this.initialized = false;
282
+ }
283
+
284
+ /**
285
+ * Install useIndexer hook on Component class.
286
+ * Call this once after importing your Component class.
287
+ *
288
+ * @param {typeof Component} ComponentClass - The Component class to extend
289
+ *
290
+ * @example
291
+ * import { Component } from '@monygroupcorp/microact';
292
+ * import { EventIndexer } from '@monygroupcorp/micro-web3';
293
+ * EventIndexer.installHook(Component);
294
+ */
295
+ static installHook(ComponentClass) {
296
+ if (ComponentClass.prototype.useIndexer) {
297
+ console.warn('[EventIndexer] useIndexer hook already installed');
298
+ return;
299
+ }
300
+
301
+ /**
302
+ * Reactive query hook for EventIndexer.
303
+ * Auto-updates when indexed events change.
304
+ *
305
+ * @param {EntityQueryable|EventsAPI} queryable - What to query (entity or events)
306
+ * @param {Object|Function} where - Filter conditions (can be reactive functions)
307
+ * @param {Object} options - Hook options
308
+ * @param {Function} options.onUpdate - Called when results change
309
+ * @param {Function} options.onError - Called on query error
310
+ * @returns {Proxy} Proxy that returns current results
311
+ */
312
+ ComponentClass.prototype.useIndexer = function(queryable, where = {}, options = {}) {
313
+ const self = this;
314
+ let result = null;
315
+ let isLoading = true;
316
+ let unsubscribe = null;
317
+
318
+ // Resolve reactive where values
319
+ const resolveWhere = () => {
320
+ const resolved = {};
321
+ for (const [key, value] of Object.entries(where)) {
322
+ resolved[key] = typeof value === 'function' ? value() : value;
323
+ }
324
+ return resolved;
325
+ };
326
+
327
+ // Execute query
328
+ const executeQuery = async () => {
329
+ try {
330
+ const whereValues = resolveWhere();
331
+
332
+ if (queryable.query) {
333
+ // Entity or events queryable
334
+ const queryResult = await queryable.query(whereValues);
335
+ result = Array.isArray(queryResult) ? queryResult : queryResult.events;
336
+ } else {
337
+ result = [];
338
+ }
339
+
340
+ isLoading = false;
341
+
342
+ if (options.onUpdate) {
343
+ options.onUpdate(result);
344
+ }
345
+ } catch (error) {
346
+ isLoading = false;
347
+ console.error('[useIndexer] Query error:', error);
348
+ if (options.onError) {
349
+ options.onError(error);
350
+ }
351
+ }
352
+ };
353
+
354
+ // Initial query
355
+ executeQuery();
356
+
357
+ // Subscribe to changes
358
+ if (queryable.subscribe) {
359
+ unsubscribe = queryable.subscribe(resolveWhere(), () => {
360
+ executeQuery();
361
+ });
362
+ }
363
+
364
+ // Register cleanup
365
+ if (this.registerCleanup) {
366
+ this.registerCleanup(() => {
367
+ if (unsubscribe) unsubscribe();
368
+ });
369
+ }
370
+
371
+ // Return proxy for array-like access
372
+ return new Proxy([], {
373
+ get(target, prop) {
374
+ if (prop === 'length') return result?.length || 0;
375
+ if (prop === 'isLoading') return isLoading;
376
+ if (prop === Symbol.iterator) {
377
+ return function* () {
378
+ if (result) yield* result;
379
+ };
380
+ }
381
+ if (typeof prop === 'string' && !isNaN(prop)) {
382
+ return result?.[parseInt(prop)];
383
+ }
384
+ // Array methods
385
+ if (result && typeof result[prop] === 'function') {
386
+ return result[prop].bind(result);
387
+ }
388
+ return result?.[prop];
389
+ }
390
+ });
391
+ };
392
+ }
393
+ }
394
+
395
+ // Export error types for external use
396
+ EventIndexer.EventIndexerError = EventIndexerError;
397
+ EventIndexer.ErrorCodes = ErrorCodes;
398
+
399
+ export default EventIndexer;