@sschepis/magazine 0.1.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/src/index.js ADDED
@@ -0,0 +1,571 @@
1
+ // magazine - a decentralized library for ethereum event logging
2
+ // magazine uses gun.js to store events in a decentralized manner
3
+ // events are stored in a graph structure, where each event is a node
4
+ // ethers.js is used to interact with the ethereum blockchain
5
+ // gun.js is used to store events in a decentralized manner
6
+ //
7
+ // Usage:
8
+ // 1. create a new magazine
9
+ // 2. sync history from the blockchain to the magazine
10
+ // 3. publish the magazine
11
+ // 4. subscribe to the magazine
12
+ // 5. read events from the magazine
13
+
14
+ // Import required libraries and components
15
+ import ConfigManager from './components/ConfigManager.js';
16
+ import NetworkManager from './components/NetworkManager.js';
17
+ import EventStore from './components/EventStore.js';
18
+ import EventSyncer from './components/EventSyncer.js';
19
+ import EventMetadata from './components/EventMetadata.js';
20
+ import DebugUtils from './components/DebugUtils.js';
21
+ import DataTransformer from './components/DataTransformer.js';
22
+ import Logger from './components/Logger.js';
23
+
24
+ // Define Magazine class
25
+ export default class Magazine {
26
+ constructor(config) {
27
+ // Initialize configuration manager and validate config
28
+ this.configManager = new ConfigManager(config);
29
+ const validatedConfig = this.configManager.getAll();
30
+
31
+ // Initialize logger
32
+ this.logger = new Logger({
33
+ context: 'Magazine',
34
+ level: validatedConfig.logLevel || 'info'
35
+ });
36
+
37
+ this.logger.info('Initializing Magazine', {
38
+ name: validatedConfig.name,
39
+ address: validatedConfig.address,
40
+ networkId: validatedConfig.networkId
41
+ });
42
+
43
+ // Initialize retry operation as a shared utility
44
+ this.retryOptions = this.configManager.getRetryOptions();
45
+
46
+ // Initialize modular components with dependency injection
47
+ this.networkManager = new NetworkManager(validatedConfig, this.retryOperation.bind(this));
48
+ this.eventStore = new EventStore(validatedConfig.gun, this.retryOperation.bind(this));
49
+ this.eventSyncer = new EventSyncer(
50
+ this.networkManager,
51
+ this.eventStore,
52
+ this.configManager,
53
+ this.retryOperation.bind(this)
54
+ );
55
+
56
+ // Initialize EventMetadata component
57
+ this.eventMetadata = new EventMetadata(this.networkManager, this.logger);
58
+
59
+ // Initialize DebugUtils component
60
+ this.debugUtils = new DebugUtils(this, this.logger);
61
+
62
+ // Initialize DataTransformer component
63
+ this.dataTransformer = new DataTransformer(
64
+ this.eventStore.getGunInstance(),
65
+ {
66
+ logLevel: validatedConfig.logLevel,
67
+ enableAutoTransform: validatedConfig.enableAutoTransform !== false
68
+ }
69
+ );
70
+
71
+ // Expose configuration for backward compatibility
72
+ this.name = validatedConfig.name;
73
+ this.address = validatedConfig.address;
74
+ this.networkId = validatedConfig.networkId;
75
+ this.abi = validatedConfig.abi;
76
+
77
+ this.logger.info('Magazine initialized successfully', {
78
+ name: this.name,
79
+ address: this.address,
80
+ networkId: this.networkId,
81
+ eventCount: Object.keys(this.networkManager.eventsByTopic).length,
82
+ componentCount: 6,
83
+ debugModeEnabled: this.debugUtils.isDebugMode(),
84
+ dataTransformerEnabled: true
85
+ });
86
+ }
87
+
88
+ // Get configuration manager for advanced configuration access
89
+ getConfigManager() {
90
+ return this.configManager;
91
+ }
92
+
93
+ // Get network manager for advanced network operations
94
+ getNetworkManager() {
95
+ return this.networkManager;
96
+ }
97
+
98
+ // Get event store for advanced storage operations
99
+ getEventStore() {
100
+ return this.eventStore;
101
+ }
102
+
103
+ // Get event syncer for advanced synchronization operations
104
+ getEventSyncer() {
105
+ return this.eventSyncer;
106
+ }
107
+
108
+ // Get logger for external logging operations
109
+ getLogger() {
110
+ return this.logger;
111
+ }
112
+
113
+ // Set log level dynamically
114
+ setLogLevel(level) {
115
+ this.logger.setLevel(level);
116
+ this.logger.info('Log level updated', { newLevel: level });
117
+ }
118
+
119
+ // Retry wrapper for async operations
120
+ async retryOperation(operation, context = '') {
121
+ const { maxRetries, retryDelay, timeout } = this.retryOptions;
122
+ let lastError;
123
+
124
+ this.logger.debug('Starting retry operation', { context, maxRetries, timeout });
125
+
126
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
127
+ try {
128
+ // Add timeout to the operation
129
+ const timeoutPromise = new Promise((_, reject) =>
130
+ setTimeout(() => reject(new Error(`Operation timeout: ${context}`)), timeout)
131
+ );
132
+
133
+ const result = await Promise.race([operation(), timeoutPromise]);
134
+
135
+ if (attempt > 1) {
136
+ this.logger.info('Retry operation succeeded', {
137
+ context,
138
+ attempt,
139
+ maxRetries
140
+ });
141
+ }
142
+
143
+ return result;
144
+ } catch (error) {
145
+ lastError = error;
146
+
147
+ this.logger.warn('Retry operation failed', {
148
+ context,
149
+ attempt,
150
+ maxRetries,
151
+ error: error.message
152
+ });
153
+
154
+ if (attempt < maxRetries) {
155
+ const delay = retryDelay * Math.pow(2, attempt - 1); // Exponential backoff
156
+ this.logger.debug('Retrying operation', {
157
+ context,
158
+ delay: `${delay}ms`,
159
+ nextAttempt: attempt + 1
160
+ });
161
+ await new Promise(resolve => setTimeout(resolve, delay));
162
+ } else {
163
+ this.logger.error('Retry operation exhausted', {
164
+ context,
165
+ totalAttempts: maxRetries,
166
+ finalError: error.message,
167
+ stack: error.stack
168
+ });
169
+ }
170
+ }
171
+ }
172
+
173
+ throw lastError;
174
+ }
175
+
176
+ // Start syncing events from the blockchain to the magazine
177
+ startSync() {
178
+ this.logger.info('Starting automatic sync');
179
+ this.stopSync();
180
+
181
+ this.syncInterval = setInterval(async () => {
182
+ try {
183
+ await this.syncEvents();
184
+ } catch (error) {
185
+ this.logger.error('Automatic sync error', {
186
+ error: error.message,
187
+ stack: error.stack
188
+ });
189
+ // Continue syncing despite errors
190
+ }
191
+ }, 5000);
192
+
193
+ // Initial sync
194
+ this.syncEvents().catch(error => {
195
+ this.logger.error('Initial sync error', {
196
+ error: error.message,
197
+ stack: error.stack
198
+ });
199
+ });
200
+
201
+ this.logger.info('Automatic sync started', { interval: '5000ms' });
202
+ }
203
+
204
+ /**
205
+ * Start real-time event streaming (requires WebSocket provider)
206
+ */
207
+ startRealtimeSync() {
208
+ return this.eventSyncer.startRealtimeSync();
209
+ }
210
+
211
+ /**
212
+ * Stop real-time event streaming
213
+ */
214
+ stopRealtimeSync() {
215
+ return this.eventSyncer.stopRealtimeSync();
216
+ }
217
+
218
+ /**
219
+ * Destroy the magazine instance, stopping sync and cleaning up listeners
220
+ */
221
+ destroy() {
222
+ this.stopSync();
223
+ this.stopRealtimeSync();
224
+ this.eventStore.destroy();
225
+ this.logger.info('Magazine instance destroyed');
226
+ }
227
+
228
+ // Stop syncing events from the blockchain to the magazine
229
+ stopSync() {
230
+ if (this.syncInterval) {
231
+ this.logger.info('Stopping automatic sync');
232
+ clearInterval(this.syncInterval);
233
+ this.syncInterval = undefined;
234
+ }
235
+ }
236
+
237
+ // Sync events from the blockchain to the magazine - delegates to EventSyncer
238
+ async syncEvents() {
239
+ return this.eventSyncer.syncEvents();
240
+ }
241
+
242
+ // Sync events from a specific block range
243
+ async syncEventsFromRange(fromBlock, toBlock) {
244
+ return this.eventSyncer.syncEventsFromRange(fromBlock, toBlock);
245
+ }
246
+
247
+ // Sync events from a specific transaction
248
+ async syncEventsFromTransaction(txHash) {
249
+ return this.eventSyncer.syncEventsFromTransaction(txHash);
250
+ }
251
+
252
+ // Get sync status information
253
+ async getSyncStatus() {
254
+ return this.eventSyncer.getSyncStatus();
255
+ }
256
+
257
+ // Get the start block number for syncing events - delegates to EventStore
258
+ async getStartBlockNumber() {
259
+ return this.eventStore.getStartBlockNumber();
260
+ }
261
+
262
+ // Set the start block number for syncing events - delegates to EventStore
263
+ async setStartBlockNumber(startBlockNumber) {
264
+ this.configManager.validateBlockNumber(startBlockNumber, 'startBlockNumber');
265
+ return this.eventStore.setStartBlockNumber(startBlockNumber);
266
+ }
267
+
268
+ // Get events by name with optional filtering
269
+ async getEvents(eventName, filter = {}) {
270
+ // Use the new filtering system
271
+ const result = await this.getAllEvents({
272
+ filter: { ...filter, eventName }
273
+ });
274
+ return result.data;
275
+ }
276
+
277
+ // Get all events with optional filtering and pagination
278
+ async getAllEvents(options = {}) {
279
+ return this.eventStore.getAllEvents(options);
280
+ }
281
+
282
+ /**
283
+ * Query events using fluent API
284
+ * @returns {Promise<Object>} Query builder
285
+ * @example
286
+ * const results = await magazine.query()
287
+ * .eventName('Transfer')
288
+ * .blockRange(1000, 2000)
289
+ * .where('value', { gte: 1000 })
290
+ * .orderBy('blockNumber', 'desc')
291
+ * .execute();
292
+ */
293
+ async query() {
294
+ return this.eventStore.query();
295
+ }
296
+
297
+ /**
298
+ * Get events by transaction hash
299
+ * @param {string} transactionHash - Transaction hash to search for
300
+ * @returns {Promise<Array>} Events from the transaction
301
+ */
302
+ async getEventsByTransactionHash(transactionHash) {
303
+ if (!transactionHash || typeof transactionHash !== 'string') {
304
+ throw new Error('Transaction hash must be a non-empty string');
305
+ }
306
+ return this.eventStore.getEventsByTransactionHash(transactionHash);
307
+ }
308
+
309
+ /**
310
+ * Get events by block number
311
+ * @param {number} blockNumber - Block number to search for
312
+ * @returns {Promise<Array>} Events from the block
313
+ */
314
+ async getEventsByBlockNumber(blockNumber) {
315
+ if (!Number.isInteger(blockNumber) || blockNumber < 0) {
316
+ throw new Error('Block number must be a non-negative integer');
317
+ }
318
+ return this.eventStore.getEventsByBlockNumber(blockNumber);
319
+ }
320
+
321
+ /**
322
+ * Create async iterator for events
323
+ * @param {Object} options - Iterator options
324
+ * @param {Object} options.filter - Filter criteria
325
+ * @param {Object} options.sort - Sort options
326
+ * @param {number} options.pageSize - Items per iteration batch
327
+ * @returns {AsyncIterator} Async iterator for events
328
+ * @example
329
+ * for await (const event of magazine.iterateEvents({ pageSize: 50 })) {
330
+ * console.log(event);
331
+ * }
332
+ */
333
+ async *iterateEvents(options = {}) {
334
+ yield* this.eventStore.iterateEvents(options);
335
+ }
336
+
337
+ // Get network information
338
+ async getNetworkInfo() {
339
+ return this.networkManager.getNetworkInfo();
340
+ }
341
+
342
+ // Validate network and contract
343
+ async validateNetwork() {
344
+ return this.networkManager.validateNetwork();
345
+ }
346
+
347
+ // Get latest block number
348
+ async getLatestBlockNumber() {
349
+ return this.networkManager.getLatestBlockNumber();
350
+ }
351
+
352
+ // Publish the magazine to the decentralized network
353
+ async publish(options = {}) {
354
+ const validatedOptions = this.configManager.validateOptions(options, ['alias', 'url']);
355
+
356
+ // Validate specific option values
357
+ if (validatedOptions.alias && (typeof validatedOptions.alias !== 'string' || validatedOptions.alias.trim().length === 0)) {
358
+ throw new Error('alias must be a non-empty string');
359
+ }
360
+
361
+ if (validatedOptions.url) {
362
+ if (typeof validatedOptions.url !== 'string') {
363
+ throw new Error('url must be a string');
364
+ }
365
+ try {
366
+ new URL(validatedOptions.url);
367
+ } catch {
368
+ throw new Error('url must be a valid URL');
369
+ }
370
+ }
371
+
372
+ const alias = validatedOptions.alias ? validatedOptions.alias : this.name.toLowerCase().replace(/\s/g, '-');
373
+ const url = validatedOptions.url ? validatedOptions.url : `https://gunjs.herokuapp.com/gun`;
374
+
375
+ const magazineData = {
376
+ name: this.name,
377
+ address: this.address,
378
+ networkId: this.networkId,
379
+ abi: this.abi,
380
+ };
381
+
382
+ return this.eventStore.publishMagazine(alias, magazineData, url);
383
+ }
384
+
385
+ // Subscribe to a magazine on the decentralized network
386
+ static async subscribe(magazineId, config) {
387
+ // Validate parameters
388
+ if (!magazineId || typeof magazineId !== 'string') {
389
+ throw new Error('magazineId must be a non-empty string');
390
+ }
391
+
392
+ if (!config || typeof config !== 'object') {
393
+ throw new Error('config must be an object');
394
+ }
395
+
396
+ // Validate required config fields for subscription
397
+ const requiredFields = ['gun'];
398
+ for (const field of requiredFields) {
399
+ if (!config[field]) {
400
+ throw new Error(`Config field '${field}' is required for subscription`);
401
+ }
402
+ }
403
+
404
+ // Create a temporary event store for subscription
405
+ const tempEventStore = new EventStore(config.gun, () => {});
406
+ return tempEventStore.subscribeMagazine(magazineId);
407
+ }
408
+
409
+ // Read events from the magazine - delegates to EventStore
410
+ on(eventName, callback) {
411
+ if (typeof callback !== 'function') {
412
+ throw new Error('callback must be a function');
413
+ }
414
+ const gunInstance = this.eventStore.getGunInstance();
415
+ const eventNode = gunInstance.get('events').get(eventName);
416
+ eventNode.map().on(callback);
417
+ }
418
+
419
+ /**
420
+ * Health check endpoint - performs system health checks
421
+ * @returns {Promise<Object>} Health check results
422
+ */
423
+ async healthCheck() {
424
+ return this.debugUtils.healthCheck();
425
+ }
426
+
427
+ /**
428
+ * Get performance metrics
429
+ * @returns {Promise<Object>} Performance metrics
430
+ */
431
+ async getPerformanceMetrics() {
432
+ return this.debugUtils.getPerformanceReport();
433
+ }
434
+
435
+ /**
436
+ * Export debug information
437
+ * @param {Object} options - Export options
438
+ * @returns {Promise<Object>} Debug information
439
+ */
440
+ async exportDebugInfo(options = {}) {
441
+ return this.debugUtils.exportDebugData();
442
+ }
443
+
444
+ /**
445
+ * Enable or disable debug mode
446
+ * @param {boolean} enabled - Whether to enable debug mode
447
+ */
448
+ setDebugMode(enabled) {
449
+ this.debugUtils.setDebugMode(enabled);
450
+ }
451
+
452
+ /**
453
+ * Check if debug mode is enabled
454
+ * @returns {boolean} Debug mode status
455
+ */
456
+ isDebugMode() {
457
+ return this.debugUtils.isDebugMode();
458
+ }
459
+
460
+ /**
461
+ * Get configuration schema documentation
462
+ * @returns {Object} Schema documentation
463
+ */
464
+ getConfigSchema() {
465
+ return this.configManager.getSchemaDocumentation();
466
+ }
467
+
468
+ /**
469
+ * Generate example configuration
470
+ * @param {Object} options - Generation options
471
+ * @returns {Object} Example configuration
472
+ */
473
+ static generateExampleConfig(options = {}) {
474
+ const configManager = new ConfigManager({
475
+ name: 'temp',
476
+ address: '0x0000000000000000000000000000000000000000',
477
+ abi: [],
478
+ provider: 'http://localhost:8545',
479
+ networkId: 1
480
+ });
481
+ return configManager.generateExampleConfig(options);
482
+ }
483
+
484
+ /**
485
+ * Validate configuration without creating an instance
486
+ * @static
487
+ * @param {Object} config - Configuration to validate
488
+ * @returns {Object} Validation result
489
+ */
490
+ static validateConfig(config) {
491
+ return ConfigManager.validateConfiguration(config);
492
+ }
493
+
494
+ /**
495
+ * Get compression statistics from the event store
496
+ * @returns {Promise<Object>} Compression stats
497
+ */
498
+ async getCompressionStats() {
499
+ return this.eventStore.getCompressionStats();
500
+ }
501
+
502
+ // === Data Transformation API ===
503
+
504
+ /**
505
+ * Create or get a data model for structured storage
506
+ * @param {string} name - Model name
507
+ * @param {Object} schema - Model schema definition
508
+ * @param {Object} options - Model options
509
+ * @returns {Object} DataModel instance
510
+ */
511
+ model(name, schema, options = {}) {
512
+ return this.dataTransformer.model(name, schema, options);
513
+ }
514
+
515
+ /**
516
+ * Register an event transformer that converts blockchain events to model data
517
+ * @param {string} eventName - Event name to transform
518
+ * @param {string} modelName - Target model name
519
+ * @param {Function} transformFn - Transformation function
520
+ */
521
+ registerTransformer(eventName, modelName, transformFn) {
522
+ return this.dataTransformer.registerTransformer(eventName, modelName, transformFn);
523
+ }
524
+
525
+ /**
526
+ * Get a registered data model
527
+ * @param {string} name - Model name
528
+ * @returns {Object} DataModel instance
529
+ */
530
+ getModel(name) {
531
+ return this.dataTransformer.getModel(name);
532
+ }
533
+
534
+ /**
535
+ * Transform an event using registered transformers
536
+ * @param {Object} event - Blockchain event to transform
537
+ * @returns {Promise<Object|null>} Transformed data or null
538
+ */
539
+ async transformEvent(event) {
540
+ return this.dataTransformer.transformEvent(event);
541
+ }
542
+
543
+ /**
544
+ * Get data transformation statistics
545
+ * @returns {Object} Transformation stats
546
+ */
547
+ getTransformationStats() {
548
+ return this.dataTransformer.getStats();
549
+ }
550
+
551
+ /**
552
+ * Get all registered models
553
+ * @returns {Map} Map of model name to DataModel instance
554
+ */
555
+ getModels() {
556
+ return this.dataTransformer.getModels();
557
+ }
558
+ }
559
+
560
+ // Named exports for advanced usage
561
+ export { default as ConfigManager } from './components/ConfigManager.js';
562
+ export { default as NetworkManager } from './components/NetworkManager.js';
563
+ export { default as EventStore } from './components/EventStore.js';
564
+ export { default as EventSyncer } from './components/EventSyncer.js';
565
+ export { default as EventMetadata } from './components/EventMetadata.js';
566
+ export { default as DebugUtils } from './components/DebugUtils.js';
567
+ export { default as DataTransformer, ValidationError, ModelError, DataModel, AggregationPipeline, SchemaMigration } from './components/DataTransformer.js';
568
+ export { default as EventFilter } from './components/EventFilter.js';
569
+ export { default as DataCompressor } from './components/DataCompressor.js';
570
+ export { default as Paginator, PaginationHelper } from './components/Paginator.js';
571
+ export { default as Logger } from './components/Logger.js';