@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,378 @@
1
+ /**
2
+ * DataCompressor - Handles data compression and decompression for storage optimization
3
+ */
4
+ import { gzip, ungzip } from 'pako';
5
+
6
+ const isNode = typeof process !== 'undefined' && process.versions != null && process.versions.node != null;
7
+
8
+ function toBase64(uint8Array) {
9
+ if (isNode) {
10
+ return Buffer.from(uint8Array).toString('base64');
11
+ }
12
+ return btoa(String.fromCharCode.apply(null, uint8Array));
13
+ }
14
+
15
+ function fromBase64(base64String) {
16
+ if (isNode) {
17
+ const buf = Buffer.from(base64String, 'base64');
18
+ return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
19
+ }
20
+ const binaryString = atob(base64String);
21
+ const bytes = new Uint8Array(binaryString.length);
22
+ for (let i = 0; i < binaryString.length; i++) {
23
+ bytes[i] = binaryString.charCodeAt(i);
24
+ }
25
+ return bytes;
26
+ }
27
+
28
+ export default class DataCompressor {
29
+ constructor(logger) {
30
+ this.logger = logger.child({ context: 'DataCompressor' });
31
+ this.logger.info('DataCompressor initialized');
32
+
33
+ // Compression strategies
34
+ this.strategies = {
35
+ none: {
36
+ compress: (data) => data,
37
+ decompress: (data) => data,
38
+ ratio: 1
39
+ },
40
+ json: {
41
+ compress: (data) => this._compressJSON(data),
42
+ decompress: (data) => this._decompressJSON(data),
43
+ ratio: 0.7 // Approximate
44
+ },
45
+ gzip: {
46
+ compress: (data) => this._compressGzip(data),
47
+ decompress: (data) => this._decompressGzip(data),
48
+ ratio: 0.3 // Approximate
49
+ },
50
+ optimized: {
51
+ compress: (data) => this._compressOptimized(data),
52
+ decompress: (data) => this._decompressOptimized(data),
53
+ ratio: 0.4 // Approximate
54
+ }
55
+ };
56
+
57
+ this.defaultStrategy = 'optimized';
58
+ }
59
+
60
+ /**
61
+ * Compress data using specified strategy
62
+ * @param {any} data - Data to compress
63
+ * @param {string} strategy - Compression strategy
64
+ * @returns {Object} Compressed data with metadata
65
+ */
66
+ compress(data, strategy = this.defaultStrategy) {
67
+ try {
68
+ const startTime = Date.now();
69
+ const originalSize = this._calculateSize(data);
70
+
71
+ if (!this.strategies[strategy]) {
72
+ this.logger.warn('Unknown compression strategy, using default', { strategy });
73
+ strategy = this.defaultStrategy;
74
+ }
75
+
76
+ const compressed = this.strategies[strategy].compress(data);
77
+ const compressedSize = this._calculateSize(compressed);
78
+ const compressionRatio = compressedSize / originalSize;
79
+ const duration = Date.now() - startTime;
80
+
81
+ const result = {
82
+ data: compressed,
83
+ metadata: {
84
+ strategy,
85
+ originalSize,
86
+ compressedSize,
87
+ compressionRatio: compressionRatio.toFixed(3),
88
+ spaceSaved: `${((1 - compressionRatio) * 100).toFixed(1)}%`,
89
+ compressionTime: duration,
90
+ timestamp: new Date().toISOString()
91
+ }
92
+ };
93
+
94
+ this.logger.debug('Data compressed', result.metadata);
95
+ return result;
96
+
97
+ } catch (error) {
98
+ this.logger.error('Compression failed', { strategy, error: error.message });
99
+ throw error;
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Decompress data
105
+ * @param {Object} compressedData - Compressed data with metadata
106
+ * @returns {any} Original data
107
+ */
108
+ decompress(compressedData) {
109
+ try {
110
+ if (!compressedData.metadata || !compressedData.metadata.strategy) {
111
+ this.logger.warn('No compression metadata found, assuming no compression');
112
+ return compressedData;
113
+ }
114
+
115
+ const { strategy } = compressedData.metadata;
116
+ const startTime = Date.now();
117
+
118
+ if (!this.strategies[strategy]) {
119
+ throw new Error(`Unknown compression strategy: ${strategy}`);
120
+ }
121
+
122
+ const decompressed = this.strategies[strategy].decompress(compressedData.data);
123
+ const duration = Date.now() - startTime;
124
+
125
+ this.logger.debug('Data decompressed', {
126
+ strategy,
127
+ decompressionTime: duration,
128
+ originalSize: compressedData.metadata.originalSize
129
+ });
130
+
131
+ return decompressed;
132
+
133
+ } catch (error) {
134
+ this.logger.error('Decompression failed', { error: error.message });
135
+ throw error;
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Compress events for storage
141
+ * @param {Array} events - Events to compress
142
+ * @param {Object} options - Compression options
143
+ * @returns {Object} Compressed events
144
+ */
145
+ compressEvents(events, options = {}) {
146
+ const { strategy = this.defaultStrategy, chunkSize = 100 } = options;
147
+
148
+ return this.logger.time('compressEvents', () => {
149
+ // Group events into chunks for better compression
150
+ const chunks = [];
151
+ for (let i = 0; i < events.length; i += chunkSize) {
152
+ chunks.push(events.slice(i, i + chunkSize));
153
+ }
154
+
155
+ // Compress each chunk
156
+ const compressedChunks = chunks.map((chunk, index) => {
157
+ const compressed = this.compress(chunk, strategy);
158
+ return {
159
+ ...compressed,
160
+ metadata: {
161
+ ...compressed.metadata,
162
+ chunkIndex: index,
163
+ eventCount: chunk.length
164
+ }
165
+ };
166
+ });
167
+
168
+ // Calculate total compression stats
169
+ const totalOriginal = compressedChunks.reduce((sum, c) => sum + c.metadata.originalSize, 0);
170
+ const totalCompressed = compressedChunks.reduce((sum, c) => sum + c.metadata.compressedSize, 0);
171
+
172
+ const result = {
173
+ chunks: compressedChunks,
174
+ metadata: {
175
+ totalEvents: events.length,
176
+ chunkCount: chunks.length,
177
+ strategy,
178
+ totalOriginalSize: totalOriginal,
179
+ totalCompressedSize: totalCompressed,
180
+ overallCompressionRatio: (totalCompressed / totalOriginal).toFixed(3),
181
+ spaceSaved: `${((1 - totalCompressed / totalOriginal) * 100).toFixed(1)}%`
182
+ }
183
+ };
184
+
185
+ this.logger.info('Events compressed', result.metadata);
186
+ return result;
187
+ });
188
+ }
189
+
190
+ /**
191
+ * Decompress events
192
+ * @param {Object} compressedEvents - Compressed events object
193
+ * @returns {Array} Original events
194
+ */
195
+ decompressEvents(compressedEvents) {
196
+ return this.logger.time('decompressEvents', () => {
197
+ if (!compressedEvents.chunks || !Array.isArray(compressedEvents.chunks)) {
198
+ throw new Error('Invalid compressed events format');
199
+ }
200
+
201
+ const events = [];
202
+
203
+ // Decompress each chunk
204
+ compressedEvents.chunks.forEach(chunk => {
205
+ const decompressedChunk = this.decompress(chunk);
206
+ events.push(...decompressedChunk);
207
+ });
208
+
209
+ this.logger.info('Events decompressed', {
210
+ totalEvents: events.length,
211
+ chunks: compressedEvents.chunks.length
212
+ });
213
+
214
+ return events;
215
+ });
216
+ }
217
+
218
+ /**
219
+ * JSON compression - removes whitespace and uses short keys
220
+ * @private
221
+ */
222
+ _compressJSON(data) {
223
+ // Create key mapping for common event fields
224
+ const keyMap = {
225
+ eventName: 'e',
226
+ blockNumber: 'b',
227
+ transactionHash: 't',
228
+ address: 'a',
229
+ args: 'r',
230
+ topics: 'p',
231
+ data: 'd',
232
+ logIndex: 'l',
233
+ transactionIndex: 'i',
234
+ blockHash: 'h',
235
+ removed: 'm'
236
+ };
237
+
238
+ const compressed = this._replaceKeys(data, keyMap);
239
+ return {
240
+ d: compressed,
241
+ k: keyMap
242
+ };
243
+ }
244
+
245
+ /**
246
+ * JSON decompression
247
+ * @private
248
+ */
249
+ _decompressJSON(compressed) {
250
+ if (!compressed.d || !compressed.k) {
251
+ return compressed;
252
+ }
253
+
254
+ // Reverse the key mapping
255
+ const reverseMap = {};
256
+ Object.entries(compressed.k).forEach(([k, v]) => {
257
+ reverseMap[v] = k;
258
+ });
259
+
260
+ return this._replaceKeys(compressed.d, reverseMap);
261
+ }
262
+
263
+ /**
264
+ * Gzip compression
265
+ * @private
266
+ */
267
+ _compressGzip(data) {
268
+ const jsonString = JSON.stringify(data);
269
+ const compressed = gzip(jsonString);
270
+ return toBase64(compressed);
271
+ }
272
+
273
+ /**
274
+ * Gzip decompression
275
+ * @private
276
+ */
277
+ _decompressGzip(compressed) {
278
+ const bytes = fromBase64(compressed);
279
+ const decompressed = ungzip(bytes);
280
+ const jsonString = new TextDecoder().decode(decompressed);
281
+ return JSON.parse(jsonString);
282
+ }
283
+
284
+ /**
285
+ * Optimized compression - combines JSON and gzip
286
+ * @private
287
+ */
288
+ _compressOptimized(data) {
289
+ // First apply JSON compression
290
+ const jsonCompressed = this._compressJSON(data);
291
+ // Then apply gzip
292
+ return this._compressGzip(jsonCompressed);
293
+ }
294
+
295
+ /**
296
+ * Optimized decompression
297
+ * @private
298
+ */
299
+ _decompressOptimized(compressed) {
300
+ // First decompress gzip
301
+ const gzipDecompressed = this._decompressGzip(compressed);
302
+ // Then decompress JSON
303
+ return this._decompressJSON(gzipDecompressed);
304
+ }
305
+
306
+ /**
307
+ * Replace keys in object recursively
308
+ * @private
309
+ */
310
+ _replaceKeys(obj, keyMap) {
311
+ if (Array.isArray(obj)) {
312
+ return obj.map(item => this._replaceKeys(item, keyMap));
313
+ } else if (obj !== null && typeof obj === 'object') {
314
+ const result = {};
315
+ Object.entries(obj).forEach(([key, value]) => {
316
+ const newKey = keyMap[key] || key;
317
+ result[newKey] = this._replaceKeys(value, keyMap);
318
+ });
319
+ return result;
320
+ }
321
+ return obj;
322
+ }
323
+
324
+ /**
325
+ * Calculate size of data in bytes
326
+ * @private
327
+ */
328
+ _calculateSize(data) {
329
+ const jsonString = JSON.stringify(data);
330
+ if (isNode) {
331
+ return Buffer.byteLength(jsonString, 'utf8');
332
+ }
333
+ return new Blob([jsonString]).size;
334
+ }
335
+
336
+ /**
337
+ * Analyze compression efficiency for different strategies
338
+ * @param {any} data - Data to analyze
339
+ * @returns {Object} Analysis results
340
+ */
341
+ analyzeCompressionStrategies(data) {
342
+ const results = {};
343
+
344
+ Object.keys(this.strategies).forEach(strategy => {
345
+ try {
346
+ const compressed = this.compress(data, strategy);
347
+ results[strategy] = {
348
+ originalSize: compressed.metadata.originalSize,
349
+ compressedSize: compressed.metadata.compressedSize,
350
+ compressionRatio: compressed.metadata.compressionRatio,
351
+ spaceSaved: compressed.metadata.spaceSaved,
352
+ compressionTime: compressed.metadata.compressionTime
353
+ };
354
+ } catch (error) {
355
+ results[strategy] = {
356
+ error: error.message
357
+ };
358
+ }
359
+ });
360
+
361
+ // Find best strategy
362
+ let bestStrategy = 'none';
363
+ let bestRatio = 1;
364
+
365
+ Object.entries(results).forEach(([strategy, result]) => {
366
+ if (!result.error && parseFloat(result.compressionRatio) < bestRatio) {
367
+ bestRatio = parseFloat(result.compressionRatio);
368
+ bestStrategy = strategy;
369
+ }
370
+ });
371
+
372
+ return {
373
+ strategies: results,
374
+ recommendation: bestStrategy,
375
+ bestCompressionRatio: bestRatio
376
+ };
377
+ }
378
+ }