@postalsys/email-text-tools 2.1.16 → 2.2.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,200 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const mimeHtml = require('../lib/mime-html');
5
+
6
+ // Quick performance comparison
7
+ async function quickBenchmark() {
8
+ const testHtml = `
9
+ <!DOCTYPE html>
10
+ <html>
11
+ <head>
12
+ <style>
13
+ body { font-family: Arial, sans-serif; }
14
+ .container { max-width: 600px; margin: 0 auto; padding: 20px; }
15
+ h1 { color: #333; }
16
+ p { line-height: 1.6; }
17
+ .button { background: #007bff; color: white; padding: 10px 20px; }
18
+ </style>
19
+ </head>
20
+ <body>
21
+ <div class="container">
22
+ <h1>Test Email</h1>
23
+ <p>This is a test email with moderate complexity.</p>
24
+ <a href="#" class="button">Click Here</a>
25
+ </div>
26
+ </body>
27
+ </html>
28
+ `;
29
+
30
+ console.log('Quick Performance Comparison');
31
+ console.log('============================\n');
32
+
33
+ const iterations = 50;
34
+
35
+ // Test Sync
36
+ console.log('Testing Sync version...');
37
+ const syncTimes = [];
38
+ for (let i = 0; i < iterations; i++) {
39
+ const start = process.hrtime.bigint();
40
+ mimeHtml.sync({ html: testHtml });
41
+ const end = process.hrtime.bigint();
42
+ syncTimes.push(Number(end - start) / 1000000);
43
+ }
44
+
45
+ // Test Async with Pool
46
+ console.log('Testing Async with worker pool...');
47
+ const asyncPoolTimes = [];
48
+ for (let i = 0; i < iterations; i++) {
49
+ const start = process.hrtime.bigint();
50
+ await mimeHtml.async(
51
+ { html: testHtml },
52
+ {
53
+ useWorkerPool: true,
54
+ timeout: 5000
55
+ }
56
+ );
57
+ const end = process.hrtime.bigint();
58
+ asyncPoolTimes.push(Number(end - start) / 1000000);
59
+ }
60
+
61
+ // Test Async without Pool (just first 5 to avoid long wait)
62
+ console.log('Testing Async without pool (5 iterations only)...');
63
+ const asyncNoPoolTimes = [];
64
+ for (let i = 0; i < 5; i++) {
65
+ const start = process.hrtime.bigint();
66
+ await mimeHtml.async(
67
+ { html: testHtml },
68
+ {
69
+ useWorkerPool: false,
70
+ timeout: 5000
71
+ }
72
+ );
73
+ const end = process.hrtime.bigint();
74
+ asyncNoPoolTimes.push(Number(end - start) / 1000000);
75
+ }
76
+
77
+ // Calculate stats
78
+ const getStats = times => {
79
+ const avg = times.reduce((a, b) => a + b, 0) / times.length;
80
+ times.sort((a, b) => a - b);
81
+ const median = times[Math.floor(times.length / 2)];
82
+ const min = times[0];
83
+ const max = times[times.length - 1];
84
+ return { avg, median, min, max };
85
+ };
86
+
87
+ const syncStats = getStats(syncTimes);
88
+ const asyncPoolStats = getStats(asyncPoolTimes);
89
+ const asyncNoPoolStats = getStats(asyncNoPoolTimes);
90
+
91
+ // Display results
92
+ console.log('\nResults (all times in milliseconds):');
93
+ console.log('=====================================\n');
94
+
95
+ console.log('Sync Version:');
96
+ console.log(` Average: ${syncStats.avg.toFixed(2)} ms`);
97
+ console.log(` Median: ${syncStats.median.toFixed(2)} ms`);
98
+ console.log(` Min: ${syncStats.min.toFixed(2)} ms`);
99
+ console.log(` Max: ${syncStats.max.toFixed(2)} ms`);
100
+
101
+ console.log('\nAsync with Worker Pool:');
102
+ console.log(` Average: ${asyncPoolStats.avg.toFixed(2)} ms`);
103
+ console.log(` Median: ${asyncPoolStats.median.toFixed(2)} ms`);
104
+ console.log(` Min: ${asyncPoolStats.min.toFixed(2)} ms`);
105
+ console.log(` Max: ${asyncPoolStats.max.toFixed(2)} ms`);
106
+
107
+ console.log('\nAsync without Pool (single worker per call):');
108
+ console.log(` Average: ${asyncNoPoolStats.avg.toFixed(2)} ms`);
109
+ console.log(` Median: ${asyncNoPoolStats.median.toFixed(2)} ms`);
110
+ console.log(` Min: ${asyncNoPoolStats.min.toFixed(2)} ms`);
111
+ console.log(` Max: ${asyncNoPoolStats.max.toFixed(2)} ms`);
112
+
113
+ console.log('\nPerformance Summary:');
114
+ console.log('====================');
115
+
116
+ if (asyncPoolStats.avg < syncStats.avg) {
117
+ const speedup = (syncStats.avg / asyncPoolStats.avg).toFixed(2);
118
+ console.log(`✓ Async with pool is ${speedup}x faster than sync`);
119
+ } else {
120
+ const slowdown = (asyncPoolStats.avg / syncStats.avg).toFixed(2);
121
+ console.log(`✗ Async with pool is ${slowdown}x slower than sync`);
122
+ }
123
+
124
+ const poolVsNoPool = (asyncNoPoolStats.avg / asyncPoolStats.avg).toFixed(1);
125
+ console.log(`✓ Worker pool is ${poolVsNoPool}x faster than single workers`);
126
+
127
+ // Worker pool stats
128
+ const poolStats = mimeHtml.getWorkerPoolStats();
129
+ if (poolStats) {
130
+ console.log('\nWorker Pool Statistics:');
131
+ console.log('======================');
132
+ console.log(` Total Workers: ${poolStats.totalWorkers}`);
133
+ console.log(` Busy Workers: ${poolStats.busyWorkers}`);
134
+ console.log(` Free Workers: ${poolStats.freeWorkers}`);
135
+ console.log(` Queued Tasks: ${poolStats.queuedTasks}`);
136
+ console.log(` Min Workers: ${poolStats.minWorkers}`);
137
+ console.log(` Max Workers: ${poolStats.maxWorkers}`);
138
+ }
139
+
140
+ // Test with problematic CSS
141
+ console.log('\nTesting Problematic CSS Handling:');
142
+ console.log('=================================');
143
+
144
+ const problematicHtml = `
145
+ <!DOCTYPE html>
146
+ <html>
147
+ <head>
148
+ <style>
149
+ /* These selectors would cause juice to hang */
150
+ .item:is(.active, .selected) { background: yellow; }
151
+ .element:where(.primary) { color: blue; }
152
+ .parent:has(> .child) { border: 1px solid red; }
153
+ </style>
154
+ </head>
155
+ <body>
156
+ <div class="item active">Active Item</div>
157
+ <div class="parent"><div class="child">Child</div></div>
158
+ </body>
159
+ </html>
160
+ `;
161
+
162
+ try {
163
+ console.log(' Sync version... ');
164
+ const syncStart = process.hrtime.bigint();
165
+ const _syncResult = mimeHtml.sync({ html: problematicHtml });
166
+ const syncEnd = process.hrtime.bigint();
167
+ const syncTime = Number(syncEnd - syncStart) / 1000000;
168
+ console.log(` ✓ Success (${syncTime.toFixed(2)} ms) - problematic selectors removed`);
169
+ } catch (err) {
170
+ console.log(` ✗ Failed: ${err.message}`);
171
+ }
172
+
173
+ try {
174
+ console.log(' Async version...');
175
+ const asyncStart = process.hrtime.bigint();
176
+ const _asyncResult = await mimeHtml.async(
177
+ { html: problematicHtml },
178
+ {
179
+ timeout: 2000,
180
+ logErrors: false
181
+ }
182
+ );
183
+ const asyncEnd = process.hrtime.bigint();
184
+ const asyncTime = Number(asyncEnd - asyncStart) / 1000000;
185
+ console.log(` ✓ Success (${asyncTime.toFixed(2)} ms) - handled with timeout protection`);
186
+ } catch (err) {
187
+ console.log(` ✗ Failed: ${err.message}`);
188
+ }
189
+
190
+ // Clean up
191
+ await mimeHtml.closeWorkerPool();
192
+
193
+ console.log('\n✓ Benchmark complete!');
194
+ }
195
+
196
+ // Run the benchmark
197
+ quickBenchmark().catch(err => {
198
+ console.error('Benchmark failed:', err);
199
+ process.exit(1);
200
+ });
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const mimeHtml = require('../lib/mime-html');
7
+
8
+ async function quickConcurrencyTest() {
9
+ console.log('Quick Concurrency Test - Real-World HTML');
10
+ console.log('='.repeat(60));
11
+
12
+ const realWorldHtml = fs.readFileSync(path.join(__dirname, '../examples/failing.html'), 'utf8');
13
+
14
+ console.log(`Testing with ${(realWorldHtml.length / 1024).toFixed(1)} KB of problematic HTML\n`);
15
+
16
+ const iterations = 30;
17
+
18
+ // Test 1: Sequential Sync
19
+ console.log(`Test 1: Sequential Sync (${iterations} requests)`);
20
+ const syncStart = process.hrtime.bigint();
21
+ for (let i = 0; i < iterations; i++) {
22
+ mimeHtml.sync({ html: realWorldHtml });
23
+ }
24
+ const syncTime = Number(process.hrtime.bigint() - syncStart) / 1000000;
25
+ console.log(` Time: ${syncTime.toFixed(2)}ms`);
26
+ console.log(` Throughput: ${(iterations / (syncTime / 1000)).toFixed(0)} req/s\n`);
27
+
28
+ // Test 2: Concurrent Async with Pool (concurrency = 10)
29
+ console.log(`Test 2: Concurrent Async with Pool (${iterations} requests, concurrency=10)`);
30
+ const asyncStart = process.hrtime.bigint();
31
+ const promises = [];
32
+ for (let i = 0; i < iterations; i++) {
33
+ promises.push(
34
+ mimeHtml.async(
35
+ { html: realWorldHtml },
36
+ {
37
+ useWorkerPool: true,
38
+ timeout: 5000,
39
+ logErrors: false
40
+ }
41
+ )
42
+ );
43
+ }
44
+ await Promise.all(promises);
45
+ const asyncTime = Number(process.hrtime.bigint() - asyncStart) / 1000000;
46
+ console.log(` Time: ${asyncTime.toFixed(2)}ms`);
47
+ console.log(` Throughput: ${(iterations / (asyncTime / 1000)).toFixed(0)} req/s\n`);
48
+
49
+ // Show speedup
50
+ const speedup = (syncTime / asyncTime).toFixed(2);
51
+ console.log('='.repeat(60));
52
+ console.log('Results:');
53
+ console.log(`• Concurrent processing is ${speedup}x faster than sequential`);
54
+ console.log(`• No timeouts occurred (pre-filtering working correctly)`);
55
+
56
+ const stats = mimeHtml.getWorkerPoolStats();
57
+ if (stats) {
58
+ console.log(`• Worker pool used ${stats.totalWorkers} workers efficiently`);
59
+ }
60
+
61
+ await mimeHtml.closeWorkerPool();
62
+ }
63
+
64
+ quickConcurrencyTest().catch(console.error);
@@ -0,0 +1,267 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const mimeHtml = require('../lib/mime-html');
7
+
8
+ class RealWorldBenchmark {
9
+ constructor() {
10
+ this.results = [];
11
+ }
12
+
13
+ async runTest(name, fn, iterations = 50) {
14
+ const times = [];
15
+ const errors = [];
16
+
17
+ // Warmup
18
+ console.log(` Warming up ${name}...`);
19
+ for (let i = 0; i < 3; i++) {
20
+ try {
21
+ await fn();
22
+ } catch (_err) {
23
+ // Ignore warmup errors
24
+ }
25
+ }
26
+
27
+ // Actual test
28
+ console.log(` Running ${name} (${iterations} iterations)...`);
29
+ for (let i = 0; i < iterations; i++) {
30
+ if (i % 10 === 0) {
31
+ process.stdout.write(` Progress: ${i}/${iterations}\r`);
32
+ }
33
+ const start = process.hrtime.bigint();
34
+ try {
35
+ await fn();
36
+ const end = process.hrtime.bigint();
37
+ times.push(Number(end - start) / 1000000); // Convert to milliseconds
38
+ } catch (err) {
39
+ errors.push(err.message);
40
+ }
41
+ }
42
+ process.stdout.write(` Progress: ${iterations}/${iterations} - Done!\n`);
43
+
44
+ if (times.length > 0) {
45
+ times.sort((a, b) => a - b);
46
+ const avg = times.reduce((a, b) => a + b, 0) / times.length;
47
+ const median = times[Math.floor(times.length / 2)];
48
+ const min = times[0];
49
+ const max = times[times.length - 1];
50
+ const p95 = times[Math.floor(times.length * 0.95)];
51
+ const p99 = times[Math.floor(times.length * 0.99)];
52
+
53
+ return {
54
+ name,
55
+ iterations,
56
+ successful: times.length,
57
+ failed: errors.length,
58
+ avg: avg.toFixed(2),
59
+ median: median.toFixed(2),
60
+ min: min.toFixed(2),
61
+ max: max.toFixed(2),
62
+ p95: p95.toFixed(2),
63
+ p99: p99.toFixed(2),
64
+ errors: errors.length > 0 ? errors[0] : null
65
+ };
66
+ } else {
67
+ return {
68
+ name,
69
+ iterations,
70
+ successful: 0,
71
+ failed: errors.length,
72
+ errors: errors[0] || 'All iterations failed'
73
+ };
74
+ }
75
+ }
76
+
77
+ formatTable(results) {
78
+ const table = [];
79
+ results.forEach(test => {
80
+ if (test.successful > 0) {
81
+ table.push({
82
+ Method: test.name,
83
+ Success: `${test.successful}/${test.iterations}`,
84
+ Avg: `${test.avg}ms`,
85
+ Median: `${test.median}ms`,
86
+ Min: `${test.min}ms`,
87
+ Max: `${test.max}ms`,
88
+ P95: `${test.p95}ms`,
89
+ P99: `${test.p99}ms`
90
+ });
91
+ } else {
92
+ table.push({
93
+ Method: test.name,
94
+ Success: `${test.successful}/${test.iterations}`,
95
+ Error: test.errors
96
+ });
97
+ }
98
+ });
99
+
100
+ console.table(table);
101
+ }
102
+
103
+ async runBenchmarks() {
104
+ console.log('Real-World HTML Benchmark');
105
+ console.log('='.repeat(80));
106
+ console.log('Testing with actual problematic HTML that contains :is(), :where(), :has() selectors');
107
+ console.log('Both sync and async versions now pre-filter these selectors\n');
108
+
109
+ // Load the real-world HTML
110
+ const realWorldHtml = fs.readFileSync(path.join(__dirname, '../examples/failing.html'), 'utf8');
111
+
112
+ console.log(`HTML size: ${(realWorldHtml.length / 1024).toFixed(1)} KB`);
113
+ console.log(`Contains modern CSS selectors that would cause juice to hang\n`);
114
+
115
+ const results = [];
116
+
117
+ // Test 1: Sync version
118
+ console.log('Test 1: Sync Version');
119
+ console.log('-'.repeat(40));
120
+ results.push(
121
+ await this.runTest(
122
+ 'Sync',
123
+ () => {
124
+ return Promise.resolve(mimeHtml.sync({ html: realWorldHtml }));
125
+ },
126
+ 50
127
+ )
128
+ );
129
+
130
+ // Test 2: Async with worker pool
131
+ console.log('\nTest 2: Async with Worker Pool');
132
+ console.log('-'.repeat(40));
133
+ results.push(
134
+ await this.runTest(
135
+ 'Async (pool)',
136
+ async () => {
137
+ return await mimeHtml.async(
138
+ { html: realWorldHtml },
139
+ {
140
+ useWorkerPool: true,
141
+ timeout: 5000,
142
+ minWorkers: 2,
143
+ maxWorkers: 4,
144
+ logErrors: false
145
+ }
146
+ );
147
+ },
148
+ 50
149
+ )
150
+ );
151
+
152
+ // Test 3: Async without pool (fewer iterations due to overhead)
153
+ console.log('\nTest 3: Async without Worker Pool');
154
+ console.log('-'.repeat(40));
155
+ results.push(
156
+ await this.runTest(
157
+ 'Async (no pool)',
158
+ async () => {
159
+ return await mimeHtml.async(
160
+ { html: realWorldHtml },
161
+ {
162
+ useWorkerPool: false,
163
+ timeout: 5000,
164
+ logErrors: false
165
+ }
166
+ );
167
+ },
168
+ 10
169
+ )
170
+ );
171
+
172
+ // Display results
173
+ console.log('\n' + '='.repeat(80));
174
+ console.log('RESULTS');
175
+ console.log('='.repeat(80) + '\n');
176
+
177
+ this.formatTable(results);
178
+
179
+ // Performance comparison
180
+ console.log('\nPerformance Analysis:');
181
+ console.log('-'.repeat(40));
182
+
183
+ const sync = results.find(r => r.name === 'Sync');
184
+ const asyncPool = results.find(r => r.name === 'Async (pool)');
185
+ const asyncNoPool = results.find(r => r.name === 'Async (no pool)');
186
+
187
+ if (sync && asyncPool && sync.successful > 0 && asyncPool.successful > 0) {
188
+ const overhead = (((parseFloat(asyncPool.avg) - parseFloat(sync.avg)) / parseFloat(sync.avg)) * 100).toFixed(1);
189
+ if (overhead > 0) {
190
+ console.log(`• Async with pool adds ${overhead}% overhead vs sync`);
191
+ } else {
192
+ console.log(`• Async with pool is ${Math.abs(overhead)}% faster than sync`);
193
+ }
194
+ }
195
+
196
+ if (asyncPool && asyncNoPool && asyncPool.successful > 0 && asyncNoPool.successful > 0) {
197
+ const poolAdvantage = (parseFloat(asyncNoPool.avg) / parseFloat(asyncPool.avg)).toFixed(1);
198
+ console.log(`• Worker pool is ${poolAdvantage}x faster than creating workers per request`);
199
+ }
200
+
201
+ if (sync && sync.successful > 0) {
202
+ console.log(`• Sync processes at ~${(1000 / parseFloat(sync.avg)).toFixed(0)} requests/second`);
203
+ }
204
+
205
+ if (asyncPool && asyncPool.successful > 0) {
206
+ console.log(`• Async with pool processes at ~${(1000 / parseFloat(asyncPool.avg)).toFixed(0)} requests/second`);
207
+ }
208
+
209
+ // Check if any timeouts occurred
210
+ const anyTimeouts = results.some(r => r.failed > 0);
211
+ if (!anyTimeouts) {
212
+ console.log('\n✅ No timeouts occurred - pre-filtering is working correctly!');
213
+ } else {
214
+ console.log('\n⚠️ Some timeouts occurred:');
215
+ results
216
+ .filter(r => r.failed > 0)
217
+ .forEach(r => {
218
+ console.log(` - ${r.name}: ${r.failed} failures`);
219
+ });
220
+ }
221
+
222
+ // Worker pool stats
223
+ const stats = mimeHtml.getWorkerPoolStats();
224
+ if (stats) {
225
+ console.log('\nWorker Pool Final Stats:');
226
+ console.log('-'.repeat(40));
227
+ console.log(`• Total Workers: ${stats.totalWorkers}`);
228
+ console.log(`• Busy Workers: ${stats.busyWorkers}`);
229
+ console.log(`• Free Workers: ${stats.freeWorkers}`);
230
+ console.log(`• Queued Tasks: ${stats.queuedTasks}`);
231
+ }
232
+
233
+ // Memory usage
234
+ const memUsage = process.memoryUsage();
235
+ console.log('\nMemory Usage:');
236
+ console.log('-'.repeat(40));
237
+ console.log(`• RSS: ${(memUsage.rss / 1024 / 1024).toFixed(1)} MB`);
238
+ console.log(`• Heap Used: ${(memUsage.heapUsed / 1024 / 1024).toFixed(1)} MB`);
239
+ console.log(`• External: ${(memUsage.external / 1024 / 1024).toFixed(1)} MB`);
240
+
241
+ // Cleanup
242
+ await mimeHtml.closeWorkerPool();
243
+
244
+ console.log('\n' + '='.repeat(80));
245
+ console.log('Benchmark Complete!');
246
+ console.log('='.repeat(80));
247
+
248
+ // Summary
249
+ console.log('\nKey Takeaways:');
250
+ console.log('• Both versions successfully handle problematic CSS by pre-filtering');
251
+ console.log('• No timeouts needed when selectors are filtered before processing');
252
+ console.log('• Worker pool provides efficient concurrent processing');
253
+ console.log('• Async timeout serves as safety net for unknown edge cases');
254
+ }
255
+ }
256
+
257
+ // Run benchmark
258
+ const benchmark = new RealWorldBenchmark();
259
+ benchmark
260
+ .runBenchmarks()
261
+ .then(() => {
262
+ process.exit(0);
263
+ })
264
+ .catch(err => {
265
+ console.error('Benchmark failed:', err);
266
+ process.exit(1);
267
+ });