@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.
@@ -0,0 +1,353 @@
1
+ import Logger from './Logger.js';
2
+
3
+ /**
4
+ * EventSyncer - Handles blockchain event synchronization logic
5
+ */
6
+ export default class EventSyncer {
7
+ constructor(networkManager, eventStore, configManager, retryOperation) {
8
+ this.networkManager = networkManager;
9
+ this.eventStore = eventStore;
10
+ this.configManager = configManager;
11
+ this.retryOperation = retryOperation;
12
+
13
+ this.logger = new Logger({
14
+ context: 'EventSyncer',
15
+ level: configManager.get('logLevel') || 'info'
16
+ });
17
+
18
+ // Configuration for batch processing
19
+ this.batchSize = 1000; // Process events in batches of 1000 blocks
20
+ this.lastSyncTime = 0;
21
+
22
+ this.logger.info('EventSyncer initialized', {
23
+ batchSize: this.batchSize
24
+ });
25
+ }
26
+
27
+ // Sync events from the blockchain to the magazine
28
+ async syncEvents() {
29
+ if (this._syncing) {
30
+ this.logger.warn('Sync already in progress, skipping');
31
+ return 0;
32
+ }
33
+ this._setSyncing(true);
34
+ try {
35
+ return await this.logger.time('syncEvents', async () => {
36
+ return this.retryOperation(async () => {
37
+ // Get the starting block number and latest block number
38
+ const startBlockNumber = await this.eventStore.getStartBlockNumber();
39
+ const latestBlockNumber = await this.networkManager.getLatestBlockNumber();
40
+
41
+ if (startBlockNumber > latestBlockNumber) {
42
+ this.logger.info('Already up to date', {
43
+ startBlockNumber,
44
+ latestBlockNumber
45
+ });
46
+ return;
47
+ }
48
+
49
+ this.logger.info('Starting event synchronization', {
50
+ startBlockNumber,
51
+ latestBlockNumber,
52
+ totalBlocks: latestBlockNumber - startBlockNumber + 1
53
+ });
54
+
55
+ // Process events in batches to avoid overwhelming the provider
56
+ let currentStart = startBlockNumber;
57
+ let totalEventsProcessed = 0;
58
+
59
+ while (currentStart <= latestBlockNumber) {
60
+ const currentEnd = Math.min(currentStart + this.batchSize - 1, latestBlockNumber);
61
+
62
+ const batchEvents = await this._processBatch(currentStart, currentEnd);
63
+ totalEventsProcessed += batchEvents;
64
+ currentStart = currentEnd + 1;
65
+ }
66
+
67
+ // Update the start block number for next sync
68
+ await this.eventStore.setStartBlockNumber(latestBlockNumber + 1);
69
+
70
+ this.lastSyncTime = Date.now();
71
+
72
+ this.logger.info('Event synchronization completed', {
73
+ totalEventsProcessed,
74
+ newStartBlockNumber: latestBlockNumber + 1
75
+ });
76
+
77
+ return totalEventsProcessed;
78
+ }, 'Event synchronization');
79
+ });
80
+ } finally {
81
+ this._setSyncing(false);
82
+ }
83
+ }
84
+
85
+ // Process a batch of blocks for events
86
+ async _processBatch(fromBlock, toBlock) {
87
+ return this.logger.time('processBatch', async () => {
88
+ this.logger.debug('Processing batch', { fromBlock, toBlock });
89
+
90
+ const logs = await this.networkManager.getLogs(fromBlock, toBlock);
91
+ let eventsStored = 0;
92
+
93
+ // Process each log
94
+ for (const log of logs) {
95
+ const parsedEvent = this.networkManager.parseLog(log);
96
+
97
+ if (parsedEvent) {
98
+ try {
99
+ // Store event with unique key to prevent duplicates
100
+ const eventKey = this.networkManager.createEventKey(log);
101
+ await this.eventStore.storeEvent(
102
+ parsedEvent.eventName,
103
+ eventKey,
104
+ parsedEvent.eventData
105
+ );
106
+ eventsStored++;
107
+
108
+ this.logger.trace('Event stored', {
109
+ eventName: parsedEvent.eventName,
110
+ eventKey,
111
+ blockNumber: log.blockNumber
112
+ });
113
+ } catch (error) {
114
+ this.logger.warn('Failed to store event', {
115
+ eventName: parsedEvent.eventName,
116
+ blockNumber: log.blockNumber,
117
+ transactionHash: log.transactionHash,
118
+ error: error.message
119
+ });
120
+ }
121
+ }
122
+ }
123
+
124
+ this.logger.debug('Batch processing completed', {
125
+ fromBlock,
126
+ toBlock,
127
+ logsFound: logs.length,
128
+ eventsStored
129
+ });
130
+
131
+ return eventsStored;
132
+ }, { fromBlock, toBlock });
133
+ }
134
+
135
+ // Sync events from a specific block range
136
+ async syncEventsFromRange(fromBlock, toBlock) {
137
+ this.configManager.validateBlockNumber(fromBlock, 'fromBlock');
138
+ this.configManager.validateBlockNumber(toBlock, 'toBlock');
139
+
140
+ if (fromBlock > toBlock) {
141
+ throw new Error('fromBlock cannot be greater than toBlock');
142
+ }
143
+
144
+ if (this._syncing) {
145
+ this.logger.warn('Sync already in progress, skipping range sync');
146
+ return 0;
147
+ }
148
+ this._setSyncing(true);
149
+ try {
150
+ return await this.logger.time('syncEventsFromRange', async () => {
151
+ return this.retryOperation(async () => {
152
+ this.logger.info('Syncing events from specific range', {
153
+ fromBlock,
154
+ toBlock,
155
+ totalBlocks: toBlock - fromBlock + 1
156
+ });
157
+
158
+ // Process the range in batches
159
+ let currentStart = fromBlock;
160
+ let totalEventsProcessed = 0;
161
+
162
+ while (currentStart <= toBlock) {
163
+ const currentEnd = Math.min(currentStart + this.batchSize - 1, toBlock);
164
+ const batchEvents = await this._processBatch(currentStart, currentEnd);
165
+ totalEventsProcessed += batchEvents;
166
+ currentStart = currentEnd + 1;
167
+ }
168
+
169
+ this.lastSyncTime = Date.now();
170
+
171
+ this.logger.info('Range synchronization completed', {
172
+ fromBlock,
173
+ toBlock,
174
+ totalEventsProcessed
175
+ });
176
+
177
+ return totalEventsProcessed;
178
+ }, `Event synchronization for range ${fromBlock}-${toBlock}`);
179
+ }, { fromBlock, toBlock });
180
+ } finally {
181
+ this._setSyncing(false);
182
+ }
183
+ }
184
+
185
+ // Sync events from a specific transaction
186
+ async syncEventsFromTransaction(txHash) {
187
+ if (!txHash || typeof txHash !== 'string') {
188
+ throw new Error('Transaction hash must be a non-empty string');
189
+ }
190
+
191
+ return this.retryOperation(async () => {
192
+ const receipt = await this.networkManager.getTransactionReceipt(txHash);
193
+ if (!receipt) {
194
+ throw new Error(`Transaction ${txHash} not found`);
195
+ }
196
+
197
+ const contractAddress = this.configManager.get('address').toLowerCase();
198
+ const relevantLogs = receipt.logs.filter(
199
+ log => log.address.toLowerCase() === contractAddress
200
+ );
201
+
202
+ for (const log of relevantLogs) {
203
+ const parsedEvent = this.networkManager.parseLog(log);
204
+
205
+ if (parsedEvent) {
206
+ const eventKey = this.networkManager.createEventKey(log);
207
+ await this.eventStore.storeEvent(
208
+ parsedEvent.eventName,
209
+ eventKey,
210
+ parsedEvent.eventData
211
+ );
212
+ }
213
+ }
214
+
215
+ this.lastSyncTime = Date.now();
216
+ return relevantLogs.length;
217
+ }, `Syncing events from transaction ${txHash}`);
218
+ }
219
+
220
+ // Get sync status information
221
+ async getSyncStatus() {
222
+ return this.retryOperation(async () => {
223
+ const startBlockNumber = await this.eventStore.getStartBlockNumber();
224
+ const latestBlockNumber = await this.networkManager.getLatestBlockNumber();
225
+ const networkInfo = await this.networkManager.getNetworkInfo();
226
+
227
+ return {
228
+ nextSyncBlock: startBlockNumber,
229
+ latestNetworkBlock: latestBlockNumber,
230
+ blocksBehind: Math.max(0, latestBlockNumber - startBlockNumber + 1),
231
+ networkInfo,
232
+ isUpToDate: startBlockNumber > latestBlockNumber
233
+ };
234
+ }, 'Getting sync status');
235
+ }
236
+
237
+ // Validate network and contract before syncing
238
+ async validateBeforeSync() {
239
+ await this.networkManager.validateNetwork();
240
+ await this.networkManager.validateContract();
241
+ return true;
242
+ }
243
+
244
+ // Set batch size for processing (useful for performance tuning)
245
+ setBatchSize(size) {
246
+ if (!Number.isInteger(size) || size < 1) {
247
+ throw new Error('Batch size must be a positive integer');
248
+ }
249
+ this.batchSize = size;
250
+ }
251
+
252
+ // Get current batch size
253
+ getBatchSize() {
254
+ return this.batchSize;
255
+ }
256
+
257
+ // Reset sync position to a specific block
258
+ async resetSyncPosition(blockNumber) {
259
+ this.configManager.validateBlockNumber(blockNumber, 'blockNumber');
260
+ await this.eventStore.setStartBlockNumber(blockNumber);
261
+ }
262
+
263
+ // Get events count by name
264
+ async getEventCount(eventName) {
265
+ const events = await this.eventStore.getEvents(eventName);
266
+ return events.length;
267
+ }
268
+
269
+ /**
270
+ * Start real-time event streaming via WebSocket or EventEmitter provider.
271
+ */
272
+ startRealtimeSync() {
273
+ const provider = this.networkManager.getProvider();
274
+
275
+ if (!provider || typeof provider.on !== 'function') {
276
+ throw new Error(
277
+ 'Real-time sync requires a provider that supports event subscriptions ' +
278
+ '(e.g., WebSocketProvider). The current provider does not have an .on() method.'
279
+ );
280
+ }
281
+
282
+ if (this._realtimeListener) {
283
+ this.logger.warn('Real-time sync is already running, call stopRealtimeSync() first');
284
+ return;
285
+ }
286
+
287
+ const contractAddress = this.configManager.get('address');
288
+ const filter = { address: contractAddress };
289
+
290
+ this.logger.info('Starting real-time event sync', {
291
+ contractAddress,
292
+ providerType: provider.constructor.name
293
+ });
294
+
295
+ const listener = async (log) => {
296
+ try {
297
+ const parsedEvent = this.networkManager.parseLog(log);
298
+ if (parsedEvent) {
299
+ const eventKey = this.networkManager.createEventKey(log);
300
+ await this.eventStore.storeEvent(
301
+ parsedEvent.eventName,
302
+ eventKey,
303
+ parsedEvent.eventData
304
+ );
305
+ this.logger.debug('Real-time event stored', {
306
+ eventName: parsedEvent.eventName,
307
+ eventKey,
308
+ blockNumber: log.blockNumber
309
+ });
310
+ }
311
+ } catch (error) {
312
+ this.logger.warn('Failed to process real-time event', {
313
+ blockNumber: log.blockNumber,
314
+ error: error.message
315
+ });
316
+ }
317
+ };
318
+
319
+ provider.on(filter, listener);
320
+ this._realtimeListener = listener;
321
+ this._realtimeFilter = filter;
322
+ this.logger.info('Real-time event sync started');
323
+ }
324
+
325
+ /**
326
+ * Stop real-time event streaming and clean up the listener.
327
+ */
328
+ stopRealtimeSync() {
329
+ if (!this._realtimeListener) {
330
+ this.logger.debug('No real-time sync listener to stop');
331
+ return;
332
+ }
333
+
334
+ const provider = this.networkManager.getProvider();
335
+ if (provider && typeof provider.off === 'function') {
336
+ provider.off(this._realtimeFilter, this._realtimeListener);
337
+ this.logger.info('Real-time event sync stopped');
338
+ }
339
+
340
+ this._realtimeListener = null;
341
+ this._realtimeFilter = null;
342
+ }
343
+
344
+ // Check if syncer is busy (for preventing concurrent syncs)
345
+ isSyncing() {
346
+ return this._syncing || false;
347
+ }
348
+
349
+ // Set syncing state
350
+ _setSyncing(state) {
351
+ this._syncing = state;
352
+ }
353
+ }
@@ -0,0 +1,299 @@
1
+ /**
2
+ * Logger - Structured logging system with configurable levels
3
+ */
4
+ export default class Logger {
5
+ constructor(config = {}) {
6
+ this.level = config.level || 'info';
7
+ this.context = config.context || 'Magazine';
8
+ this.enableConsole = config.enableConsole !== false;
9
+ this.enableFile = config.enableFile || false;
10
+ this.filePath = config.filePath || './magazine.log';
11
+
12
+ // File logging state (lazily initialized)
13
+ this._fsModule = null;
14
+ this._fsAvailable = null; // null = not yet checked, true/false after init
15
+
16
+ // Log levels with numeric values for comparison
17
+ this.levels = {
18
+ error: 0,
19
+ warn: 1,
20
+ info: 2,
21
+ debug: 3,
22
+ trace: 4
23
+ };
24
+
25
+ this.currentLevelValue = this.levels[this.level.toLowerCase()] || this.levels.info;
26
+
27
+ // Initialize file logging if enabled
28
+ if (this.enableFile) {
29
+ this._initFileLogging();
30
+ }
31
+ }
32
+
33
+ // Create a formatted log entry
34
+ _formatLog(level, message, data = {}) {
35
+ const timestamp = new Date().toISOString();
36
+ const logEntry = {
37
+ timestamp,
38
+ level: level.toUpperCase(),
39
+ context: this.context,
40
+ message,
41
+ ...data
42
+ };
43
+
44
+ return {
45
+ formatted: `[${timestamp}] ${level.toUpperCase()} [${this.context}] ${message}${
46
+ Object.keys(data).length > 0 ? ' ' + JSON.stringify(data, this._getCircularReplacer()) : ''
47
+ }`,
48
+ structured: logEntry
49
+ };
50
+ }
51
+
52
+ // Get a replacer function to handle circular references in JSON.stringify
53
+ _getCircularReplacer() {
54
+ const seen = new WeakSet();
55
+ return (key, value) => {
56
+ if (typeof value === "object" && value !== null) {
57
+ if (seen.has(value)) {
58
+ return "[Circular]";
59
+ }
60
+ seen.add(value);
61
+ }
62
+ return value;
63
+ };
64
+ }
65
+
66
+ // Check if current level allows logging at specified level
67
+ _shouldLog(level) {
68
+ const levelValue = this.levels[level.toLowerCase()];
69
+ return levelValue !== undefined && levelValue <= this.currentLevelValue;
70
+ }
71
+
72
+ // Write log to console
73
+ _writeToConsole(level, formatted) {
74
+ if (!this.enableConsole) return;
75
+
76
+ switch (level.toLowerCase()) {
77
+ case 'error':
78
+ console.error(formatted);
79
+ break;
80
+ case 'warn':
81
+ console.warn(formatted);
82
+ break;
83
+ case 'debug':
84
+ case 'trace':
85
+ console.debug(formatted);
86
+ break;
87
+ default:
88
+ console.log(formatted);
89
+ }
90
+ }
91
+
92
+ // Initialize file system module for file logging
93
+ _initFileLogging() {
94
+ if (this._fsAvailable !== null) return; // already initialized
95
+
96
+ try {
97
+ // Use dynamic import to support both Node.js and browser environments.
98
+ // import() returns a promise, so we kick it off here and store the result
99
+ // once resolved. File writes before init completes are silently skipped.
100
+ const fsPromise = import('fs');
101
+ const pathPromise = import('path');
102
+
103
+ Promise.all([fsPromise, pathPromise]).then(([fsModule, pathModule]) => {
104
+ this._fsModule = fsModule.default || fsModule;
105
+ this._pathModule = pathModule.default || pathModule;
106
+ this._fsAvailable = true;
107
+ }).catch(() => {
108
+ this._fsAvailable = false;
109
+ });
110
+ } catch (_err) {
111
+ // Static import() not available (older environments) - disable file logging
112
+ this._fsAvailable = false;
113
+ }
114
+ }
115
+
116
+ // Write log entry as a JSON line to the configured file
117
+ _writeToFile(structured) {
118
+ if (!this.enableFile) return;
119
+ if (this._fsAvailable === false) return;
120
+ if (!this._fsModule) return; // fs not yet loaded (async init still pending)
121
+
122
+ try {
123
+ const line = JSON.stringify(structured, this._getCircularReplacer()) + '\n';
124
+ this._fsModule.appendFileSync(this.filePath, line, 'utf8');
125
+ } catch (_err) {
126
+ // File logging failures must not crash the application
127
+ }
128
+ }
129
+
130
+ // Rotate log file if it exceeds maxSizeBytes. Keeps 1 rotated backup.
131
+ rotateLogs(maxSizeBytes = 10 * 1024 * 1024) {
132
+ if (!this._fsModule || this._fsAvailable === false) return;
133
+
134
+ try {
135
+ if (!this._fsModule.existsSync(this.filePath)) return;
136
+
137
+ const stats = this._fsModule.statSync(this.filePath);
138
+ if (stats.size < maxSizeBytes) return;
139
+
140
+ const backupPath = this.filePath + '.1';
141
+
142
+ // Remove old backup if it exists, then rename current to backup
143
+ if (this._fsModule.existsSync(backupPath)) {
144
+ this._fsModule.unlinkSync(backupPath);
145
+ }
146
+ this._fsModule.renameSync(this.filePath, backupPath);
147
+
148
+ // Start a fresh log file
149
+ this._fsModule.writeFileSync(this.filePath, '', 'utf8');
150
+ } catch (_err) {
151
+ // Rotation failures must not crash the application
152
+ }
153
+ }
154
+
155
+ // Core logging method
156
+ _log(level, message, data = {}) {
157
+ if (!this._shouldLog(level)) return;
158
+
159
+ const { formatted, structured } = this._formatLog(level, message, data);
160
+
161
+ this._writeToConsole(level, formatted);
162
+ this._writeToFile(structured);
163
+
164
+ return structured;
165
+ }
166
+
167
+ // Logging level methods
168
+ error(message, data = {}) {
169
+ return this._log('error', message, data);
170
+ }
171
+
172
+ warn(message, data = {}) {
173
+ return this._log('warn', message, data);
174
+ }
175
+
176
+ info(message, data = {}) {
177
+ return this._log('info', message, data);
178
+ }
179
+
180
+ debug(message, data = {}) {
181
+ return this._log('debug', message, data);
182
+ }
183
+
184
+ trace(message, data = {}) {
185
+ return this._log('trace', message, data);
186
+ }
187
+
188
+ // Performance logging - measure function execution time
189
+ async time(operation, fn, context = {}) {
190
+ const startTime = Date.now();
191
+ const startMessage = `Starting operation: ${operation}`;
192
+
193
+ this.debug(startMessage, context);
194
+
195
+ try {
196
+ const result = await fn();
197
+ const duration = Date.now() - startTime;
198
+
199
+ this.info(`Completed operation: ${operation}`, {
200
+ ...context,
201
+ duration: `${duration}ms`,
202
+ success: true
203
+ });
204
+
205
+ return result;
206
+ } catch (error) {
207
+ const duration = Date.now() - startTime;
208
+
209
+ this.error(`Failed operation: ${operation}`, {
210
+ ...context,
211
+ duration: `${duration}ms`,
212
+ success: false,
213
+ error: error?.message || error?.toString() || 'Unknown error',
214
+ stack: error?.stack
215
+ });
216
+
217
+ throw error;
218
+ }
219
+ }
220
+
221
+ // Create a child logger with additional context
222
+ child(additionalContext = {}) {
223
+ return new Logger({
224
+ level: this.level,
225
+ context: this.context,
226
+ enableConsole: this.enableConsole,
227
+ enableFile: this.enableFile,
228
+ filePath: this.filePath,
229
+ ...additionalContext
230
+ });
231
+ }
232
+
233
+ // Change log level dynamically
234
+ setLevel(level) {
235
+ if (this.levels[level.toLowerCase()] !== undefined) {
236
+ this.level = level.toLowerCase();
237
+ this.currentLevelValue = this.levels[this.level];
238
+ this.info(`Log level changed to: ${level.toUpperCase()}`);
239
+ } else {
240
+ this.warn(`Invalid log level: ${level}. Valid levels: ${Object.keys(this.levels).join(', ')}`);
241
+ }
242
+ }
243
+
244
+ // Get current configuration
245
+ getConfig() {
246
+ return {
247
+ level: this.level,
248
+ context: this.context,
249
+ enableConsole: this.enableConsole,
250
+ enableFile: this.enableFile,
251
+ filePath: this.filePath
252
+ };
253
+ }
254
+
255
+ // Log system information
256
+ logSystemInfo(info = {}) {
257
+ this.info('System Information', {
258
+ nodeVersion: process.version,
259
+ platform: process.platform,
260
+ arch: process.arch,
261
+ memoryUsage: process.memoryUsage(),
262
+ uptime: process.uptime(),
263
+ ...info
264
+ });
265
+ }
266
+
267
+ // Log network request/response
268
+ logNetworkActivity(type, details = {}) {
269
+ this.debug(`Network ${type}`, {
270
+ type,
271
+ timestamp: Date.now(),
272
+ ...details
273
+ });
274
+ }
275
+
276
+ // Log blockchain interaction
277
+ logBlockchainActivity(operation, details = {}) {
278
+ this.info(`Blockchain: ${operation}`, {
279
+ operation,
280
+ timestamp: Date.now(),
281
+ ...details
282
+ });
283
+ }
284
+
285
+ // Log Gun.js database activity
286
+ logDatabaseActivity(operation, details = {}) {
287
+ this.debug(`Database: ${operation}`, {
288
+ operation,
289
+ timestamp: Date.now(),
290
+ ...details
291
+ });
292
+ }
293
+ }
294
+
295
+ // Export a default logger instance
296
+ export const defaultLogger = new Logger({
297
+ level: 'info',
298
+ context: 'Magazine'
299
+ });