@postalsys/email-text-tools 2.4.26 → 2.6.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.
Files changed (88) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +12 -0
  3. package/dist/cjs/index.d.ts +20 -0
  4. package/dist/cjs/index.js +31 -0
  5. package/dist/cjs/lib/commonjs.cjs +17 -0
  6. package/dist/cjs/lib/commonjs.d.cts +4 -0
  7. package/dist/cjs/lib/format-date.d.ts +15 -0
  8. package/dist/cjs/lib/format-date.js +60 -0
  9. package/dist/cjs/lib/html-guard.d.ts +3 -0
  10. package/dist/cjs/lib/html-guard.js +119 -0
  11. package/dist/cjs/lib/html-to-text.d.ts +1 -0
  12. package/dist/cjs/lib/html-to-text.js +49 -0
  13. package/dist/cjs/lib/inline-html.d.ts +19 -0
  14. package/dist/cjs/lib/inline-html.js +150 -0
  15. package/dist/cjs/lib/inline-style.d.ts +2 -0
  16. package/dist/cjs/lib/inline-style.js +42 -0
  17. package/dist/cjs/lib/inline-text.d.ts +3 -0
  18. package/dist/cjs/lib/inline-text.js +111 -0
  19. package/dist/cjs/lib/juice-worker.d.ts +1 -0
  20. package/dist/cjs/lib/juice-worker.js +19 -0
  21. package/dist/cjs/lib/mime-html.d.ts +34 -0
  22. package/dist/cjs/lib/mime-html.js +496 -0
  23. package/dist/cjs/lib/text-to-html.d.ts +1 -0
  24. package/dist/cjs/lib/text-to-html.js +182 -0
  25. package/dist/cjs/lib/worker-pool.d.ts +61 -0
  26. package/dist/cjs/lib/worker-pool.js +229 -0
  27. package/dist/cjs/package.json +3 -0
  28. package/dist/esm/index.d.ts +20 -0
  29. package/dist/esm/index.js +13 -0
  30. package/dist/esm/lib/commonjs.cjs +17 -0
  31. package/dist/esm/lib/commonjs.d.cts +4 -0
  32. package/dist/esm/lib/format-date.d.ts +15 -0
  33. package/{lib → dist/esm/lib}/format-date.js +8 -16
  34. package/dist/esm/lib/html-guard.d.ts +3 -0
  35. package/{lib → dist/esm/lib}/html-guard.js +8 -24
  36. package/dist/esm/lib/html-to-text.d.ts +1 -0
  37. package/{lib → dist/esm/lib}/html-to-text.js +2 -14
  38. package/dist/esm/lib/inline-html.d.ts +19 -0
  39. package/{lib → dist/esm/lib}/inline-html.js +17 -55
  40. package/dist/esm/lib/inline-style.d.ts +2 -0
  41. package/dist/esm/lib/inline-style.js +39 -0
  42. package/dist/esm/lib/inline-text.d.ts +3 -0
  43. package/{lib → dist/esm/lib}/inline-text.js +8 -31
  44. package/dist/esm/lib/juice-worker.d.ts +1 -0
  45. package/{lib → dist/esm/lib}/juice-worker.js +7 -8
  46. package/dist/esm/lib/mime-html.d.ts +34 -0
  47. package/{lib → dist/esm/lib}/mime-html.js +80 -161
  48. package/dist/esm/lib/text-to-html.d.ts +1 -0
  49. package/{lib → dist/esm/lib}/text-to-html.js +35 -63
  50. package/dist/esm/lib/worker-pool.d.ts +61 -0
  51. package/{lib → dist/esm/lib}/worker-pool.js +18 -64
  52. package/dist/esm/package.json +3 -0
  53. package/package.json +67 -10
  54. package/.github/VULNERABILITY_REPORT.yml +0 -75
  55. package/.github/workflows/release.yaml +0 -38
  56. package/.github/workflows/test.yaml +0 -35
  57. package/.ncurc.js +0 -28
  58. package/.prettierignore +0 -14
  59. package/.prettierrc.js +0 -10
  60. package/.release-please-manifest.json +0 -3
  61. package/CLAUDE.md +0 -105
  62. package/benchmark/benchmark.js +0 -348
  63. package/benchmark/concurrency-benchmark.js +0 -176
  64. package/benchmark/quick-benchmark.js +0 -200
  65. package/benchmark/quick-concurrency.js +0 -64
  66. package/benchmark/real-world-benchmark.js +0 -257
  67. package/docs/PERFORMANCE.md +0 -262
  68. package/eslint.config.js +0 -71
  69. package/examples/failing.html +0 -617
  70. package/examples/fixtures/message-1.json +0 -74
  71. package/examples/inline-html.js +0 -24
  72. package/examples/inline-text.js +0 -24
  73. package/examples/mime-html.js +0 -284
  74. package/index.js +0 -22
  75. package/release-please-config.json +0 -10
  76. package/test/comprehensive.test.js +0 -358
  77. package/test/containment.test.js +0 -128
  78. package/test/edge-cases.test.js +0 -62
  79. package/test/hardening.test.js +0 -504
  80. package/test/html-to-text.test.js +0 -253
  81. package/test/inline-html.test.js +0 -230
  82. package/test/inline-text.test.js +0 -204
  83. package/test/mime-html-extra.test.js +0 -39
  84. package/test/mime-html.test.js +0 -302
  85. package/test/text-to-html-extra.test.js +0 -42
  86. package/test/worker-pool-extra.test.js +0 -132
  87. package/test/worker-pool-regression.test.js +0 -128
  88. package/test/worker-pool.test.js +0 -282
@@ -1,176 +0,0 @@
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 concurrencyBenchmark() {
9
- console.log('Concurrency Benchmark - Real-World HTML');
10
- console.log('='.repeat(80));
11
- console.log('Testing concurrent processing with problematic HTML\n');
12
-
13
- // Load the real-world HTML
14
- const realWorldHtml = fs.readFileSync(path.join(__dirname, '../examples/failing.html'), 'utf8');
15
-
16
- console.log(`HTML size: ${(realWorldHtml.length / 1024).toFixed(1)} KB`);
17
- console.log('Testing different concurrency levels...\n');
18
-
19
- const concurrencyLevels = [1, 5, 10, 20, 50];
20
- const totalRequests = 100;
21
-
22
- const results = [];
23
-
24
- for (const concurrency of concurrencyLevels) {
25
- console.log(`Testing with concurrency level: ${concurrency}`);
26
- console.log('-'.repeat(40));
27
-
28
- // Test sync (sequential only)
29
- if (concurrency === 1) {
30
- console.log(' Sync version (sequential)...');
31
- const syncStart = process.hrtime.bigint();
32
- for (let i = 0; i < totalRequests; i++) {
33
- mimeHtml.sync({ html: realWorldHtml });
34
- if ((i + 1) % 20 === 0) {
35
- process.stdout.write(` Progress: ${i + 1}/${totalRequests}\r`);
36
- }
37
- }
38
- const syncEnd = process.hrtime.bigint();
39
- const syncTime = Number(syncEnd - syncStart) / 1000000;
40
- process.stdout.write(` Progress: ${totalRequests}/${totalRequests} - Done!\n`);
41
-
42
- results.push({
43
- method: 'Sync (sequential)',
44
- concurrency: 1,
45
- totalRequests,
46
- totalTime: syncTime.toFixed(2),
47
- avgTime: (syncTime / totalRequests).toFixed(2),
48
- reqPerSec: (totalRequests / (syncTime / 1000)).toFixed(0)
49
- });
50
- }
51
-
52
- // Test async with pool
53
- console.log(' Async with pool...');
54
- const asyncPoolStart = process.hrtime.bigint();
55
- const poolPromises = [];
56
-
57
- for (let i = 0; i < totalRequests; i++) {
58
- poolPromises.push(
59
- mimeHtml.async(
60
- { html: realWorldHtml },
61
- {
62
- useWorkerPool: true,
63
- timeout: 5000,
64
- minWorkers: 2,
65
- maxWorkers: 4,
66
- logErrors: false
67
- }
68
- )
69
- );
70
-
71
- // Control concurrency
72
- if (poolPromises.length >= concurrency) {
73
- await Promise.race(poolPromises).then(() => {
74
- const index = poolPromises.findIndex(
75
- p => p.constructor.name !== 'Promise' || Promise.race([p, Promise.resolve('done')]).then(v => v === 'done')
76
- );
77
- if (index > -1) poolPromises.splice(index, 1);
78
- });
79
- }
80
-
81
- if ((i + 1) % 20 === 0) {
82
- process.stdout.write(` Progress: ${i + 1}/${totalRequests}\r`);
83
- }
84
- }
85
-
86
- // Wait for remaining promises
87
- await Promise.all(poolPromises);
88
- const asyncPoolEnd = process.hrtime.bigint();
89
- const asyncPoolTime = Number(asyncPoolEnd - asyncPoolStart) / 1000000;
90
- process.stdout.write(` Progress: ${totalRequests}/${totalRequests} - Done!\n`);
91
-
92
- results.push({
93
- method: 'Async (pool)',
94
- concurrency,
95
- totalRequests,
96
- totalTime: asyncPoolTime.toFixed(2),
97
- avgTime: (asyncPoolTime / totalRequests).toFixed(2),
98
- reqPerSec: (totalRequests / (asyncPoolTime / 1000)).toFixed(0)
99
- });
100
-
101
- // Show worker pool stats at peak concurrency
102
- if (concurrency === 20) {
103
- const stats = mimeHtml.getWorkerPoolStats();
104
- console.log(` Worker pool stats: ${stats.totalWorkers} workers, ${stats.busyWorkers} busy`);
105
- }
106
-
107
- console.log();
108
- }
109
-
110
- // Display results table
111
- console.log('='.repeat(80));
112
- console.log('RESULTS');
113
- console.log('='.repeat(80) + '\n');
114
-
115
- console.table(
116
- results.map(r => ({
117
- Method: r.method,
118
- Concurrency: r.concurrency,
119
- 'Total Time': `${r.totalTime}ms`,
120
- 'Avg per Request': `${r.avgTime}ms`,
121
- Throughput: `${r.reqPerSec} req/s`
122
- }))
123
- );
124
-
125
- // Analysis
126
- console.log('Performance Analysis:');
127
- console.log('-'.repeat(40));
128
-
129
- const syncResult = results.find(r => r.method === 'Sync (sequential)');
130
- const bestAsync = results.filter(r => r.method === 'Async (pool)').sort((a, b) => parseFloat(a.totalTime) - parseFloat(b.totalTime))[0];
131
-
132
- if (syncResult && bestAsync) {
133
- const speedup = (parseFloat(syncResult.totalTime) / parseFloat(bestAsync.totalTime)).toFixed(2);
134
- console.log(`• Best async configuration: Concurrency ${bestAsync.concurrency}`);
135
- console.log(`• Async is ${speedup}x faster than sync for batch processing`);
136
- console.log(`• Sync throughput: ${syncResult.reqPerSec} req/s`);
137
- console.log(`• Best async throughput: ${bestAsync.reqPerSec} req/s`);
138
- }
139
-
140
- // Throughput graph
141
- console.log('\nThroughput by Concurrency Level:');
142
- console.log('-'.repeat(40));
143
-
144
- const asyncResults = results.filter(r => r.method === 'Async (pool)');
145
- const maxThroughput = Math.max(...asyncResults.map(r => parseInt(r.reqPerSec, 10)));
146
-
147
- asyncResults.forEach(r => {
148
- const barLength = Math.round((parseInt(r.reqPerSec, 10) / maxThroughput) * 30);
149
- const bar = '█'.repeat(barLength);
150
- console.log(` ${String(r.concurrency).padStart(2)}: ${bar} ${r.reqPerSec} req/s`);
151
- });
152
-
153
- // Memory usage
154
- const memUsage = process.memoryUsage();
155
- console.log('\nFinal Memory Usage:');
156
- console.log('-'.repeat(40));
157
- console.log(`• RSS: ${(memUsage.rss / 1024 / 1024).toFixed(1)} MB`);
158
- console.log(`• Heap Used: ${(memUsage.heapUsed / 1024 / 1024).toFixed(1)} MB`);
159
-
160
- // Cleanup
161
- await mimeHtml.closeWorkerPool();
162
-
163
- console.log('\n' + '='.repeat(80));
164
- console.log('Summary:');
165
- console.log('• Async with worker pool provides significant speedup for concurrent processing');
166
- console.log('• Pre-filtering prevents timeouts even under high load');
167
- console.log('• Worker pool efficiently handles concurrent requests');
168
- console.log('• Optimal concurrency depends on CPU cores and workload');
169
- console.log('='.repeat(80));
170
- }
171
-
172
- // Run benchmark
173
- concurrencyBenchmark().catch(err => {
174
- console.error('Benchmark failed:', err);
175
- process.exit(1);
176
- });
@@ -1,200 +0,0 @@
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
- });
@@ -1,64 +0,0 @@
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);
@@ -1,257 +0,0 @@
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(await this.runTest('Sync', () => Promise.resolve(mimeHtml.sync({ html: realWorldHtml })), 50));
121
-
122
- // Test 2: Async with worker pool
123
- console.log('\nTest 2: Async with Worker Pool');
124
- console.log('-'.repeat(40));
125
- results.push(
126
- await this.runTest(
127
- 'Async (pool)',
128
- async () =>
129
- await mimeHtml.async(
130
- { html: realWorldHtml },
131
- {
132
- useWorkerPool: true,
133
- timeout: 5000,
134
- minWorkers: 2,
135
- maxWorkers: 4,
136
- logErrors: false
137
- }
138
- ),
139
- 50
140
- )
141
- );
142
-
143
- // Test 3: Async without pool (fewer iterations due to overhead)
144
- console.log('\nTest 3: Async without Worker Pool');
145
- console.log('-'.repeat(40));
146
- results.push(
147
- await this.runTest(
148
- 'Async (no pool)',
149
- async () =>
150
- await mimeHtml.async(
151
- { html: realWorldHtml },
152
- {
153
- useWorkerPool: false,
154
- timeout: 5000,
155
- logErrors: false
156
- }
157
- ),
158
- 10
159
- )
160
- );
161
-
162
- // Display results
163
- console.log('\n' + '='.repeat(80));
164
- console.log('RESULTS');
165
- console.log('='.repeat(80) + '\n');
166
-
167
- this.formatTable(results);
168
-
169
- // Performance comparison
170
- console.log('\nPerformance Analysis:');
171
- console.log('-'.repeat(40));
172
-
173
- const sync = results.find(r => r.name === 'Sync');
174
- const asyncPool = results.find(r => r.name === 'Async (pool)');
175
- const asyncNoPool = results.find(r => r.name === 'Async (no pool)');
176
-
177
- if (sync && asyncPool && sync.successful > 0 && asyncPool.successful > 0) {
178
- const overhead = (((parseFloat(asyncPool.avg) - parseFloat(sync.avg)) / parseFloat(sync.avg)) * 100).toFixed(1);
179
- if (overhead > 0) {
180
- console.log(`• Async with pool adds ${overhead}% overhead vs sync`);
181
- } else {
182
- console.log(`• Async with pool is ${Math.abs(overhead)}% faster than sync`);
183
- }
184
- }
185
-
186
- if (asyncPool && asyncNoPool && asyncPool.successful > 0 && asyncNoPool.successful > 0) {
187
- const poolAdvantage = (parseFloat(asyncNoPool.avg) / parseFloat(asyncPool.avg)).toFixed(1);
188
- console.log(`• Worker pool is ${poolAdvantage}x faster than creating workers per request`);
189
- }
190
-
191
- if (sync && sync.successful > 0) {
192
- console.log(`• Sync processes at ~${(1000 / parseFloat(sync.avg)).toFixed(0)} requests/second`);
193
- }
194
-
195
- if (asyncPool && asyncPool.successful > 0) {
196
- console.log(`• Async with pool processes at ~${(1000 / parseFloat(asyncPool.avg)).toFixed(0)} requests/second`);
197
- }
198
-
199
- // Check if any timeouts occurred
200
- const anyTimeouts = results.some(r => r.failed > 0);
201
- if (!anyTimeouts) {
202
- console.log('\n✅ No timeouts occurred - pre-filtering is working correctly!');
203
- } else {
204
- console.log('\n⚠️ Some timeouts occurred:');
205
- results
206
- .filter(r => r.failed > 0)
207
- .forEach(r => {
208
- console.log(` - ${r.name}: ${r.failed} failures`);
209
- });
210
- }
211
-
212
- // Worker pool stats
213
- const stats = mimeHtml.getWorkerPoolStats();
214
- if (stats) {
215
- console.log('\nWorker Pool Final Stats:');
216
- console.log('-'.repeat(40));
217
- console.log(`• Total Workers: ${stats.totalWorkers}`);
218
- console.log(`• Busy Workers: ${stats.busyWorkers}`);
219
- console.log(`• Free Workers: ${stats.freeWorkers}`);
220
- console.log(`• Queued Tasks: ${stats.queuedTasks}`);
221
- }
222
-
223
- // Memory usage
224
- const memUsage = process.memoryUsage();
225
- console.log('\nMemory Usage:');
226
- console.log('-'.repeat(40));
227
- console.log(`• RSS: ${(memUsage.rss / 1024 / 1024).toFixed(1)} MB`);
228
- console.log(`• Heap Used: ${(memUsage.heapUsed / 1024 / 1024).toFixed(1)} MB`);
229
- console.log(`• External: ${(memUsage.external / 1024 / 1024).toFixed(1)} MB`);
230
-
231
- // Cleanup
232
- await mimeHtml.closeWorkerPool();
233
-
234
- console.log('\n' + '='.repeat(80));
235
- console.log('Benchmark Complete!');
236
- console.log('='.repeat(80));
237
-
238
- // Summary
239
- console.log('\nKey Takeaways:');
240
- console.log('• Both versions successfully handle problematic CSS by pre-filtering');
241
- console.log('• No timeouts needed when selectors are filtered before processing');
242
- console.log('• Worker pool provides efficient concurrent processing');
243
- console.log('• Async timeout serves as safety net for unknown edge cases');
244
- }
245
- }
246
-
247
- // Run benchmark
248
- const benchmark = new RealWorldBenchmark();
249
- benchmark
250
- .runBenchmarks()
251
- .then(() => {
252
- process.exit(0);
253
- })
254
- .catch(err => {
255
- console.error('Benchmark failed:', err);
256
- process.exit(1);
257
- });