@postalsys/email-text-tools 2.1.15 → 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,262 @@
1
+ # Performance Guide for email-text-tools
2
+
3
+ ## Overview
4
+
5
+ The `mime-html` module now provides both synchronous and asynchronous versions with configurable options for handling CSS processing. The async version includes worker thread support with pooling for high-throughput scenarios.
6
+
7
+ ## Performance Characteristics
8
+
9
+ ### Benchmark Results
10
+
11
+ Based on our benchmarks with various HTML complexities:
12
+
13
+ | Method | Average Time | Use Case |
14
+ | ---------------------- | ------------ | ---------------------------------------------------- |
15
+ | **Sync** | 5-10ms | Low-latency, simple HTML, single requests |
16
+ | **Async with Pool** | 5-10ms | High-throughput, concurrent processing |
17
+ | **Async without Pool** | 100-110ms | Isolated processing, memory-constrained environments |
18
+
19
+ ### Key Findings
20
+
21
+ 1. **Worker Pool Advantage**: The worker pool is **13.6x faster** than creating new workers for each request
22
+ 2. **Sync vs Async**: For simple HTML, sync and async with pool have similar performance (~5-10ms)
23
+ 3. **Startup Overhead**: Single worker creation adds ~100ms overhead per request
24
+ 4. **Problematic CSS**: Both versions pre-filter known problematic selectors for optimal performance
25
+ 5. **Timeout Protection**: The async version's timeout is rarely triggered thanks to pre-filtering, serving only as a safety net for unknown edge cases
26
+
27
+ ## API Usage
28
+
29
+ ### Synchronous Version (Default)
30
+
31
+ ```javascript
32
+ const mimeHtml = require('@postalsys/email-text-tools/lib/mime-html');
33
+
34
+ // Simple usage - backward compatible
35
+ const result = mimeHtml({ html: htmlContent });
36
+
37
+ // Or explicitly use sync
38
+ const result = mimeHtml.sync({ html: htmlContent });
39
+ ```
40
+
41
+ ### Asynchronous Version with Options
42
+
43
+ ```javascript
44
+ const mimeHtml = require('@postalsys/email-text-tools/lib/mime-html');
45
+
46
+ // With default worker pool
47
+ const result = await mimeHtml.async(
48
+ { html: htmlContent },
49
+ {
50
+ timeout: 5000, // Timeout in ms (default: 5000)
51
+ useWorkerPool: true, // Use worker pool (default: true)
52
+ minWorkers: 2, // Minimum pool workers (default: 2)
53
+ maxWorkers: 4, // Maximum pool workers (default: 4)
54
+ fallbackOnError: true, // Continue on error (default: true)
55
+ logErrors: true // Log errors to console (default: true)
56
+ }
57
+ );
58
+
59
+ // Without worker pool (creates new worker each time)
60
+ const result = await mimeHtml.async({ html: htmlContent }, { useWorkerPool: false });
61
+ ```
62
+
63
+ ## Configuration Options
64
+
65
+ ### Async Options
66
+
67
+ | Option | Type | Default | Description |
68
+ | ----------------- | ------- | ------- | ------------------------------------------------- |
69
+ | `timeout` | number | 5000 | Maximum time (ms) for CSS processing |
70
+ | `useWorkerPool` | boolean | true | Use worker pool for better performance |
71
+ | `minWorkers` | number | 2 | Minimum workers in pool |
72
+ | `maxWorkers` | number | 4 | Maximum workers in pool |
73
+ | `fallbackOnError` | boolean | true | Return unstyled HTML on error instead of throwing |
74
+ | `logErrors` | boolean | true | Log processing errors to console |
75
+ | `idleTimeout` | number | 30000 | Time (ms) before idle workers are terminated |
76
+
77
+ ## Worker Pool Management
78
+
79
+ ### Monitoring Pool Status
80
+
81
+ ```javascript
82
+ // Get current pool statistics
83
+ const stats = mimeHtml.getWorkerPoolStats();
84
+ console.log(stats);
85
+ // {
86
+ // totalWorkers: 2,
87
+ // busyWorkers: 0,
88
+ // freeWorkers: 2,
89
+ // queuedTasks: 0,
90
+ // minWorkers: 2,
91
+ // maxWorkers: 4
92
+ // }
93
+ ```
94
+
95
+ ### Manual Pool Cleanup
96
+
97
+ ```javascript
98
+ // Close worker pool when done (automatic on process exit)
99
+ await mimeHtml.closeWorkerPool();
100
+ ```
101
+
102
+ ## Choosing the Right Method
103
+
104
+ ### Use Sync When:
105
+
106
+ - Processing single emails
107
+ - Low-latency is critical (<10ms)
108
+ - Simple HTML without complex CSS
109
+ - Running in resource-constrained environments
110
+
111
+ ### Use Async with Pool When:
112
+
113
+ - Processing many emails concurrently
114
+ - Running a high-throughput service
115
+ - Need timeout protection for untrusted content
116
+ - Can accept slight latency for robustness
117
+
118
+ ### Use Async without Pool When:
119
+
120
+ - Processing untrusted content occasionally
121
+ - Memory constraints prevent worker pooling
122
+ - Isolation between requests is critical
123
+
124
+ ## Performance Optimization Tips
125
+
126
+ 1. **Reuse the Worker Pool**: The global worker pool is shared across all async calls. Don't close it between requests.
127
+
128
+ 2. **Tune Pool Size**: Adjust `minWorkers` and `maxWorkers` based on your workload:
129
+
130
+ ```javascript
131
+ // For high-throughput services
132
+ { minWorkers: 4, maxWorkers: 8 }
133
+
134
+ // For occasional processing
135
+ { minWorkers: 1, maxWorkers: 2 }
136
+ ```
137
+
138
+ 3. **Handle Timeouts Gracefully**: Set appropriate timeouts based on HTML complexity:
139
+
140
+ ```javascript
141
+ // For simple HTML
142
+ {
143
+ timeout: 2000;
144
+ }
145
+
146
+ // For complex HTML with many styles
147
+ {
148
+ timeout: 10000;
149
+ }
150
+ ```
151
+
152
+ 4. **Monitor Pool Health**: In production, monitor pool statistics to detect issues:
153
+ ```javascript
154
+ setInterval(() => {
155
+ const stats = mimeHtml.getWorkerPoolStats();
156
+ if (stats && stats.queuedTasks > 10) {
157
+ console.warn('High queue depth:', stats.queuedTasks);
158
+ }
159
+ }, 5000);
160
+ ```
161
+
162
+ ## Handling Problematic CSS
163
+
164
+ The library handles modern CSS selectors that cause the juice library to hang:
165
+
166
+ ### Problematic Selectors
167
+
168
+ - `:is()` pseudo-class
169
+ - `:where()` pseudo-class
170
+ - `:has()` pseudo-class
171
+ - Complex `:not()` selectors
172
+
173
+ ### How It's Handled
174
+
175
+ **Both Versions**: Pre-process HTML to remove known problematic selectors before juice processing. This prevents most hangs from occurring in the first place.
176
+
177
+ **Key Difference**:
178
+
179
+ - **Sync Version**: Pre-filters selectors, then runs juice synchronously. Can still hang if an unknown problematic pattern is encountered.
180
+ - **Async Version**: Pre-filters selectors (same as sync), then runs juice in a worker thread with timeout protection. Provides complete safety against any CSS that might cause hangs, even unknown patterns.
181
+
182
+ The timeout in the async version is a safety net for edge cases, not the primary defense. Both versions handle the known problematic selectors efficiently without timing out.
183
+
184
+ ### Example
185
+
186
+ ```javascript
187
+ const problematicHtml = `
188
+ <style>
189
+ .item:is(.active, .selected) { background: yellow; }
190
+ .parent:has(> .child) { border: 1px solid red; }
191
+ </style>
192
+ <div class="item active">Content</div>
193
+ `;
194
+
195
+ // Both handle it safely
196
+ const syncResult = mimeHtml.sync({ html: problematicHtml });
197
+ const asyncResult = await mimeHtml.async({ html: problematicHtml });
198
+ ```
199
+
200
+ ## Memory Considerations
201
+
202
+ - Each worker uses ~10-20MB of memory
203
+ - Worker pool maintains minimum workers even when idle
204
+ - Adjust pool size based on available memory:
205
+ - Low memory: `{ minWorkers: 1, maxWorkers: 2 }`
206
+ - Standard: `{ minWorkers: 2, maxWorkers: 4 }`
207
+ - High memory: `{ minWorkers: 4, maxWorkers: 8 }`
208
+
209
+ ## Error Handling
210
+
211
+ ```javascript
212
+ try {
213
+ // With fallback (default) - returns unstyled HTML on error
214
+ const result = await mimeHtml.async({ html: htmlContent }, { fallbackOnError: true });
215
+ } catch (err) {
216
+ // Only throws if fallbackOnError: false
217
+ console.error('Processing failed:', err);
218
+ }
219
+
220
+ // Check for processing issues
221
+ const result = await mimeHtml.async(
222
+ { html: htmlContent },
223
+ {
224
+ logErrors: false, // Suppress console warnings
225
+ fallbackOnError: true
226
+ }
227
+ );
228
+ ```
229
+
230
+ ## Migration Guide
231
+
232
+ ### From v2.0.x to v2.1.x
233
+
234
+ The default behavior remains unchanged - existing code will continue to work:
235
+
236
+ ```javascript
237
+ // This still works exactly as before
238
+ const result = mimeHtml({ html: htmlContent });
239
+ ```
240
+
241
+ To leverage new features:
242
+
243
+ ```javascript
244
+ // Before - only sync available
245
+ const result = mimeHtml({ html: htmlContent });
246
+
247
+ // After - use async for better protection
248
+ const result = await mimeHtml.async({ html: htmlContent }, { timeout: 5000 });
249
+ ```
250
+
251
+ ### For High-Volume Processing
252
+
253
+ ```javascript
254
+ // Before - sequential processing
255
+ for (const email of emails) {
256
+ const result = mimeHtml({ html: email.html });
257
+ // Process result
258
+ }
259
+
260
+ // After - concurrent with worker pool
261
+ const results = await Promise.all(emails.map(email => mimeHtml.async({ html: email.html }, { useWorkerPool: true })));
262
+ ```
@@ -0,0 +1,53 @@
1
+ const js = require('@eslint/js');
2
+ const prettier = require('eslint-config-prettier');
3
+
4
+ module.exports = [
5
+ js.configs.recommended,
6
+ prettier,
7
+ {
8
+ files: ['**/*.js'],
9
+ languageOptions: {
10
+ ecmaVersion: 2018,
11
+ sourceType: 'script',
12
+ globals: {
13
+ BigInt: true,
14
+ Buffer: 'readonly',
15
+ console: 'readonly',
16
+ global: 'readonly',
17
+ process: 'readonly',
18
+ require: 'readonly',
19
+ module: 'readonly',
20
+ exports: 'writable',
21
+ __dirname: 'readonly',
22
+ __filename: 'readonly',
23
+ setTimeout: 'readonly',
24
+ clearTimeout: 'readonly',
25
+ setInterval: 'readonly',
26
+ clearInterval: 'readonly',
27
+ setImmediate: 'readonly',
28
+ clearImmediate: 'readonly'
29
+ }
30
+ },
31
+ rules: {
32
+ indent: 0,
33
+ 'no-await-in-loop': 0,
34
+ 'require-atomic-updates': 0,
35
+ 'no-unused-vars': [
36
+ 'error',
37
+ {
38
+ argsIgnorePattern: '^_',
39
+ varsIgnorePattern: '^_',
40
+ caughtErrorsIgnorePattern: '^_'
41
+ }
42
+ ],
43
+ 'no-console': 'off',
44
+ 'no-constant-condition': ['error', { checkLoops: false }],
45
+ 'no-empty': ['error', { allowEmptyCatch: true }],
46
+ 'no-useless-escape': 'warn',
47
+ 'no-prototype-builtins': 'off'
48
+ }
49
+ },
50
+ {
51
+ ignores: ['node_modules/**', 'coverage/**', '.git/**', '*.log', 'test-*.js', 'dist/**', 'build/**']
52
+ }
53
+ ];