@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/LICENSE +21 -0
- package/README.md +167 -0
- package/dist/magazine.cjs.js +5 -0
- package/dist/magazine.cjs.js.map +1 -0
- package/dist/magazine.es.js +4149 -0
- package/dist/magazine.es.js.map +1 -0
- package/dist/magazine.umd.js +5 -0
- package/dist/magazine.umd.js.map +1 -0
- package/package.json +74 -0
- package/src/components/ConfigManager.js +202 -0
- package/src/components/ConfigSchema.js +506 -0
- package/src/components/DataCompressor.js +378 -0
- package/src/components/DataTransformer.js +1172 -0
- package/src/components/DebugUtils.js +657 -0
- package/src/components/EventFilter.js +474 -0
- package/src/components/EventMetadata.js +273 -0
- package/src/components/EventStore.js +443 -0
- package/src/components/EventSyncer.js +353 -0
- package/src/components/Logger.js +299 -0
- package/src/components/NetworkManager.js +461 -0
- package/src/components/Paginator.js +343 -0
- package/src/index.js +571 -0
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
import Gun from 'gun';
|
|
2
|
+
import 'gun/lib/load.js';
|
|
3
|
+
import 'gun/lib/open.js';
|
|
4
|
+
import Logger from './Logger.js';
|
|
5
|
+
import EventFilter from './EventFilter.js';
|
|
6
|
+
import Paginator from './Paginator.js';
|
|
7
|
+
import DataCompressor from './DataCompressor.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* EventStore - Handles Gun.js data storage operations
|
|
11
|
+
*/
|
|
12
|
+
export default class EventStore {
|
|
13
|
+
constructor(gunConfig, retryOperation, networkManager = null) {
|
|
14
|
+
this.gun = Gun(gunConfig);
|
|
15
|
+
this.retryOperation = retryOperation;
|
|
16
|
+
this.networkManager = networkManager;
|
|
17
|
+
this.logger = new Logger({
|
|
18
|
+
context: 'EventStore',
|
|
19
|
+
level: gunConfig?.logLevel || 'info'
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// Initialize components
|
|
23
|
+
this.eventFilter = new EventFilter(this.logger);
|
|
24
|
+
this.paginator = new Paginator(this.logger);
|
|
25
|
+
this.dataCompressor = new DataCompressor(this.logger);
|
|
26
|
+
|
|
27
|
+
// Compression settings
|
|
28
|
+
this.compressionEnabled = gunConfig?.compression?.enabled ?? true;
|
|
29
|
+
this.compressionStrategy = gunConfig?.compression?.strategy ?? 'optimized';
|
|
30
|
+
|
|
31
|
+
// Track active Gun listeners for cleanup
|
|
32
|
+
this._activeListeners = [];
|
|
33
|
+
|
|
34
|
+
this.logger.info('EventStore initialized', {
|
|
35
|
+
gunConfig: this._sanitizeGunConfig(gunConfig),
|
|
36
|
+
compressionEnabled: this.compressionEnabled,
|
|
37
|
+
compressionStrategy: this.compressionStrategy
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Sanitize Gun config for logging
|
|
42
|
+
_sanitizeGunConfig(config) {
|
|
43
|
+
if (!config || typeof config !== 'object') return config;
|
|
44
|
+
// Remove potentially sensitive information
|
|
45
|
+
const sanitized = { ...config };
|
|
46
|
+
delete sanitized.peers;
|
|
47
|
+
delete sanitized.localStorage;
|
|
48
|
+
return sanitized;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Get the start block number for syncing events from the blockchain
|
|
52
|
+
async getStartBlockNumber() {
|
|
53
|
+
return this.retryOperation(async () => {
|
|
54
|
+
return new Promise((resolve) => {
|
|
55
|
+
this.gun.get('config').get('startBlockNumber').once((data) => {
|
|
56
|
+
resolve(data || 1);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
}, 'Getting start block number');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Set the start block number for syncing events from the blockchain
|
|
63
|
+
async setStartBlockNumber(startBlockNumber) {
|
|
64
|
+
return this.retryOperation(async () => {
|
|
65
|
+
return new Promise((resolve, reject) => {
|
|
66
|
+
this.gun.get('config').get('startBlockNumber').put(startBlockNumber, (ack) => {
|
|
67
|
+
if (ack.err) {
|
|
68
|
+
reject(new Error(ack.err));
|
|
69
|
+
} else {
|
|
70
|
+
resolve(ack);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
}, `Setting start block number to ${startBlockNumber}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Store an event in the decentralized database
|
|
78
|
+
async storeEvent(eventName, eventKey, eventData) {
|
|
79
|
+
return this.logger.time('storeEvent', async () => {
|
|
80
|
+
this.logger.logDatabaseActivity('storeEvent', {
|
|
81
|
+
eventName,
|
|
82
|
+
eventKey,
|
|
83
|
+
blockNumber: eventData.blockNumber,
|
|
84
|
+
transactionHash: eventData.transactionHash
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Compress data if enabled
|
|
88
|
+
let dataToStore = eventData;
|
|
89
|
+
if (this.compressionEnabled) {
|
|
90
|
+
const compressed = this.dataCompressor.compress(eventData, this.compressionStrategy);
|
|
91
|
+
dataToStore = {
|
|
92
|
+
_compressed: true,
|
|
93
|
+
_data: compressed.data,
|
|
94
|
+
_metadata: compressed.metadata
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return this.retryOperation(
|
|
99
|
+
() => new Promise(resolve => {
|
|
100
|
+
this.gun.get('events').get(eventName).get(eventKey).put(dataToStore, resolve);
|
|
101
|
+
}),
|
|
102
|
+
`Storing event ${eventName}`
|
|
103
|
+
);
|
|
104
|
+
}, { eventName, eventKey });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Get events by name with optional filtering
|
|
108
|
+
async getEvents(eventName, filter = {}) {
|
|
109
|
+
return this.logger.time('getEvents', async () => {
|
|
110
|
+
this.logger.logDatabaseActivity('getEvents', {
|
|
111
|
+
eventName,
|
|
112
|
+
filter: Object.keys(filter)
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
return this.retryOperation(async () => {
|
|
116
|
+
return new Promise((resolve) => {
|
|
117
|
+
let resolved = false;
|
|
118
|
+
const node = this.gun.get('events').get(eventName);
|
|
119
|
+
|
|
120
|
+
const timeout = setTimeout(() => {
|
|
121
|
+
if (!resolved) {
|
|
122
|
+
resolved = true;
|
|
123
|
+
this.logger.debug('getEvents timed out, returning empty', { eventName });
|
|
124
|
+
resolve([]);
|
|
125
|
+
}
|
|
126
|
+
}, 3000);
|
|
127
|
+
|
|
128
|
+
node.open((data) => {
|
|
129
|
+
if (resolved) return;
|
|
130
|
+
resolved = true;
|
|
131
|
+
clearTimeout(timeout);
|
|
132
|
+
|
|
133
|
+
const events = [];
|
|
134
|
+
if (data && typeof data === 'object') {
|
|
135
|
+
Object.entries(data).forEach(([key, value]) => {
|
|
136
|
+
if (key === '_' || !value || typeof value !== 'object') return;
|
|
137
|
+
if (this._matchesFilter(value, filter)) {
|
|
138
|
+
events.push({ key, ...value });
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
this.logger.debug('Events retrieved', {
|
|
144
|
+
eventName,
|
|
145
|
+
eventCount: events.length,
|
|
146
|
+
filterApplied: Object.keys(filter).length > 0
|
|
147
|
+
});
|
|
148
|
+
resolve(events);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
}, `Getting events for ${eventName}`);
|
|
152
|
+
}, { eventName });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Get all events with optional filtering and pagination
|
|
156
|
+
async getAllEvents(options = {}) {
|
|
157
|
+
return this.logger.time('getAllEvents', async () => {
|
|
158
|
+
const { filter = {}, pagination = {}, sort = {} } = options;
|
|
159
|
+
|
|
160
|
+
// Get all events from Gun
|
|
161
|
+
const allEventsMap = await this._fetchAllEventsMap();
|
|
162
|
+
|
|
163
|
+
// Flatten events into array
|
|
164
|
+
const allEvents = [];
|
|
165
|
+
Object.entries(allEventsMap).forEach(([eventName, events]) => {
|
|
166
|
+
events.forEach(event => {
|
|
167
|
+
allEvents.push({
|
|
168
|
+
...event,
|
|
169
|
+
eventName
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Apply filters
|
|
175
|
+
let filteredEvents = allEvents;
|
|
176
|
+
if (Object.keys(filter).length > 0) {
|
|
177
|
+
const filterCriteria = this.eventFilter.buildFilter(filter);
|
|
178
|
+
filteredEvents = this.eventFilter.applyFilter(allEvents, filterCriteria);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Apply sorting
|
|
182
|
+
if (sort.by) {
|
|
183
|
+
filteredEvents = this.eventFilter.sortEvents(filteredEvents, sort);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Apply pagination
|
|
187
|
+
if (pagination.page || pagination.pageSize) {
|
|
188
|
+
const paginatedResult = this.paginator.paginate(filteredEvents, pagination);
|
|
189
|
+
this.logger.debug('Query completed', {
|
|
190
|
+
totalEvents: allEvents.length,
|
|
191
|
+
filteredEvents: filteredEvents.length,
|
|
192
|
+
returnedEvents: paginatedResult.data.length
|
|
193
|
+
});
|
|
194
|
+
return paginatedResult;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Return unpaginated results
|
|
198
|
+
return {
|
|
199
|
+
data: filteredEvents,
|
|
200
|
+
meta: {
|
|
201
|
+
totalItems: filteredEvents.length
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Internal method to fetch all events as a map
|
|
208
|
+
async _fetchAllEventsMap() {
|
|
209
|
+
return this.retryOperation(async () => {
|
|
210
|
+
return new Promise((resolve) => {
|
|
211
|
+
let resolved = false;
|
|
212
|
+
const node = this.gun.get('events');
|
|
213
|
+
|
|
214
|
+
const timeout = setTimeout(() => {
|
|
215
|
+
if (!resolved) {
|
|
216
|
+
resolved = true;
|
|
217
|
+
this.logger.debug('_fetchAllEventsMap timed out, returning empty');
|
|
218
|
+
resolve({});
|
|
219
|
+
}
|
|
220
|
+
}, 5000);
|
|
221
|
+
|
|
222
|
+
node.open((data) => {
|
|
223
|
+
if (resolved) return;
|
|
224
|
+
resolved = true;
|
|
225
|
+
clearTimeout(timeout);
|
|
226
|
+
|
|
227
|
+
const allEvents = {};
|
|
228
|
+
if (data && typeof data === 'object') {
|
|
229
|
+
Object.entries(data).forEach(([eventName, eventGroup]) => {
|
|
230
|
+
if (eventName === '_' || !eventGroup || typeof eventGroup !== 'object') return;
|
|
231
|
+
allEvents[eventName] = [];
|
|
232
|
+
Object.entries(eventGroup).forEach(([key, eventData]) => {
|
|
233
|
+
if (key === '_' || !eventData || typeof eventData !== 'object') return;
|
|
234
|
+
let decompressed = eventData;
|
|
235
|
+
if (eventData._compressed && eventData._data) {
|
|
236
|
+
try {
|
|
237
|
+
decompressed = this.dataCompressor.decompress({
|
|
238
|
+
data: eventData._data,
|
|
239
|
+
metadata: eventData._metadata
|
|
240
|
+
});
|
|
241
|
+
} catch (error) {
|
|
242
|
+
this.logger.error('Failed to decompress event', {
|
|
243
|
+
eventName,
|
|
244
|
+
key,
|
|
245
|
+
error: error.message
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
allEvents[eventName].push({ key, ...decompressed });
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
resolve(allEvents);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
}, 'Getting all events');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Publish magazine metadata to the decentralized network
|
|
260
|
+
async publishMagazine(alias, magazineData, url) {
|
|
261
|
+
return this.logger.time('publishMagazine', async () => {
|
|
262
|
+
this.logger.info('Publishing magazine', {
|
|
263
|
+
alias,
|
|
264
|
+
url,
|
|
265
|
+
magazineData: {
|
|
266
|
+
name: magazineData.name,
|
|
267
|
+
address: magazineData.address,
|
|
268
|
+
networkId: magazineData.networkId
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
return this.retryOperation(async () => {
|
|
273
|
+
const magazineNode = this.gun.back(-1).get(alias);
|
|
274
|
+
|
|
275
|
+
const magazineId = await new Promise((resolve, reject) => {
|
|
276
|
+
magazineNode.put(magazineData, (ack) => {
|
|
277
|
+
if (ack.err) {
|
|
278
|
+
this.logger.error('Failed to publish magazine metadata', {
|
|
279
|
+
alias,
|
|
280
|
+
error: ack.err
|
|
281
|
+
});
|
|
282
|
+
reject(new Error(ack.err));
|
|
283
|
+
} else {
|
|
284
|
+
this.logger.debug('Magazine metadata published', { alias });
|
|
285
|
+
resolve(ack);
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
await new Promise((resolve, reject) => {
|
|
291
|
+
magazineNode.get('url').put(url, (ack) => {
|
|
292
|
+
if (ack.err) {
|
|
293
|
+
this.logger.error('Failed to publish magazine URL', {
|
|
294
|
+
alias,
|
|
295
|
+
url,
|
|
296
|
+
error: ack.err
|
|
297
|
+
});
|
|
298
|
+
reject(new Error(ack.err));
|
|
299
|
+
} else {
|
|
300
|
+
this.logger.debug('Magazine URL published', { alias, url });
|
|
301
|
+
resolve(ack);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
this.logger.info('Magazine published successfully', { alias, url });
|
|
307
|
+
return magazineId;
|
|
308
|
+
}, 'Publishing magazine');
|
|
309
|
+
}, { alias });
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Subscribe to a magazine on the decentralized network
|
|
313
|
+
async subscribeMagazine(magazineId) {
|
|
314
|
+
return this.retryOperation(async () => {
|
|
315
|
+
return new Promise((resolve, reject) => {
|
|
316
|
+
this.gun.get(magazineId).once((data, key) => {
|
|
317
|
+
if (data) {
|
|
318
|
+
resolve(data);
|
|
319
|
+
} else {
|
|
320
|
+
reject(new Error(`Magazine with ID ${magazineId} not found`));
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
}, `Subscribing to magazine ${magazineId}`);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Helper method to check if event data matches filter criteria
|
|
328
|
+
_matchesFilter(data, filter) {
|
|
329
|
+
if (!filter || Object.keys(filter).length === 0) {
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
for (const [key, value] of Object.entries(filter)) {
|
|
334
|
+
if (data[key] !== value) {
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Query events using fluent API
|
|
343
|
+
async query() {
|
|
344
|
+
const allEventsMap = await this._fetchAllEventsMap();
|
|
345
|
+
|
|
346
|
+
// Flatten events
|
|
347
|
+
const allEvents = [];
|
|
348
|
+
Object.entries(allEventsMap).forEach(([eventName, events]) => {
|
|
349
|
+
events.forEach(event => {
|
|
350
|
+
allEvents.push({
|
|
351
|
+
...event,
|
|
352
|
+
eventName
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
return this.eventFilter.query(allEvents);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Get events by transaction hash
|
|
361
|
+
async getEventsByTransactionHash(transactionHash) {
|
|
362
|
+
return this.logger.time('getEventsByTransactionHash', async () => {
|
|
363
|
+
const result = await this.getAllEvents({
|
|
364
|
+
filter: { transactionHash }
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
this.logger.debug('Found events by transaction hash', {
|
|
368
|
+
transactionHash,
|
|
369
|
+
count: result.data.length
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
return result.data;
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Get events by block number
|
|
377
|
+
async getEventsByBlockNumber(blockNumber) {
|
|
378
|
+
return this.logger.time('getEventsByBlockNumber', async () => {
|
|
379
|
+
const result = await this.getAllEvents({
|
|
380
|
+
filter: {
|
|
381
|
+
fromBlock: blockNumber,
|
|
382
|
+
toBlock: blockNumber
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
this.logger.debug('Found events by block number', {
|
|
387
|
+
blockNumber,
|
|
388
|
+
count: result.data.length
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
return result.data;
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Create async iterator for events
|
|
396
|
+
async *iterateEvents(options = {}) {
|
|
397
|
+
const { filter = {}, sort = {}, pageSize = 100 } = options;
|
|
398
|
+
|
|
399
|
+
// Get filtered and sorted events
|
|
400
|
+
const result = await this.getAllEvents({ filter, sort });
|
|
401
|
+
const events = result.data;
|
|
402
|
+
|
|
403
|
+
// Use paginator's iterator
|
|
404
|
+
yield* this.paginator.iterate(events, { pageSize });
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Get compression statistics
|
|
408
|
+
async getCompressionStats() {
|
|
409
|
+
const allEventsMap = await this._fetchAllEventsMap();
|
|
410
|
+
let totalOriginal = 0;
|
|
411
|
+
let totalCompressed = 0;
|
|
412
|
+
let compressedCount = 0;
|
|
413
|
+
|
|
414
|
+
Object.values(allEventsMap).forEach(events => {
|
|
415
|
+
events.forEach(event => {
|
|
416
|
+
if (event._compressed && event._metadata) {
|
|
417
|
+
totalOriginal += event._metadata.originalSize || 0;
|
|
418
|
+
totalCompressed += event._metadata.compressedSize || 0;
|
|
419
|
+
compressedCount++;
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
return {
|
|
425
|
+
compressedEvents: compressedCount,
|
|
426
|
+
totalOriginalSize: totalOriginal,
|
|
427
|
+
totalCompressedSize: totalCompressed,
|
|
428
|
+
compressionRatio: totalOriginal > 0 ? (totalCompressed / totalOriginal).toFixed(3) : '1.000',
|
|
429
|
+
spaceSaved: totalOriginal > 0 ? `${((1 - totalCompressed / totalOriginal) * 100).toFixed(1)}%` : '0%'
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Clean up all active listeners
|
|
434
|
+
destroy() {
|
|
435
|
+
this._activeListeners = [];
|
|
436
|
+
this.logger.info('EventStore destroyed, listeners cleaned up');
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Get Gun instance for advanced operations
|
|
440
|
+
getGunInstance() {
|
|
441
|
+
return this.gun;
|
|
442
|
+
}
|
|
443
|
+
}
|