@postalsys/email-text-tools 2.1.16 → 2.3.3
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/.github/workflows/release.yaml +17 -17
- package/.ncurc.js +1 -2
- package/.prettierignore +14 -0
- package/.release-please-manifest.json +3 -0
- package/CHANGELOG.md +81 -31
- package/benchmark/benchmark.js +358 -0
- package/benchmark/concurrency-benchmark.js +176 -0
- package/benchmark/quick-benchmark.js +200 -0
- package/benchmark/quick-concurrency.js +64 -0
- package/benchmark/real-world-benchmark.js +267 -0
- package/docs/PERFORMANCE.md +262 -0
- package/eslint.config.js +52 -0
- package/examples/failing.html +617 -0
- package/examples/mime-html.js +262 -31
- package/lib/juice-worker.js +13 -0
- package/lib/mime-html.js +243 -13
- package/lib/text-to-html.js +1 -1
- package/lib/worker-pool.js +270 -0
- package/package.json +16 -8
- package/release-please-config.json +9 -0
- package/test/comprehensive.test.js +347 -0
- package/test/edge-cases.test.js +62 -0
- package/test/mime-html.test.js +327 -0
- package/test/worker-pool-regression.test.js +128 -0
- package/test/worker-pool.test.js +282 -0
- package/processed.txt +0 -699
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { Worker } = require('worker_threads');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { EventEmitter } = require('events');
|
|
6
|
+
|
|
7
|
+
class WorkerPool extends EventEmitter {
|
|
8
|
+
constructor(options = {}) {
|
|
9
|
+
super();
|
|
10
|
+
|
|
11
|
+
this.workerPath = options.workerPath || path.join(__dirname, 'juice-worker.js');
|
|
12
|
+
this.minWorkers = options.minWorkers || 2;
|
|
13
|
+
this.maxWorkers = options.maxWorkers || 4;
|
|
14
|
+
this.maxQueueSize = options.maxQueueSize || 100;
|
|
15
|
+
this.workerTimeout = options.workerTimeout || 10000;
|
|
16
|
+
this.idleTimeout = options.idleTimeout || 30000;
|
|
17
|
+
|
|
18
|
+
this.workers = [];
|
|
19
|
+
this.freeWorkers = [];
|
|
20
|
+
this.queue = [];
|
|
21
|
+
this.closing = false;
|
|
22
|
+
|
|
23
|
+
// Initialize minimum number of workers
|
|
24
|
+
this._initWorkers();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
_initWorkers() {
|
|
28
|
+
for (let i = 0; i < this.minWorkers; i++) {
|
|
29
|
+
this._createWorker();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
_createWorker() {
|
|
34
|
+
if (this.closing) return null;
|
|
35
|
+
|
|
36
|
+
const worker = new Worker(this.workerPath);
|
|
37
|
+
const workerInfo = {
|
|
38
|
+
worker,
|
|
39
|
+
busy: false,
|
|
40
|
+
lastUsed: Date.now(),
|
|
41
|
+
timeoutHandle: null,
|
|
42
|
+
currentTask: null
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
worker.on('message', msg => {
|
|
46
|
+
if (workerInfo.currentTask) {
|
|
47
|
+
const { resolve, reject, timeoutHandle } = workerInfo.currentTask;
|
|
48
|
+
clearTimeout(timeoutHandle);
|
|
49
|
+
|
|
50
|
+
if (msg.success) {
|
|
51
|
+
resolve(msg.result);
|
|
52
|
+
} else {
|
|
53
|
+
reject(new Error(msg.error));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
workerInfo.currentTask = null;
|
|
57
|
+
workerInfo.busy = false;
|
|
58
|
+
workerInfo.lastUsed = Date.now();
|
|
59
|
+
|
|
60
|
+
// Add worker back to free list if not already there
|
|
61
|
+
if (!this.freeWorkers.includes(workerInfo)) {
|
|
62
|
+
this.freeWorkers.push(workerInfo);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Process next item in queue
|
|
66
|
+
this._processQueue();
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
worker.on('error', err => {
|
|
71
|
+
if (workerInfo.currentTask) {
|
|
72
|
+
const { reject, timeoutHandle } = workerInfo.currentTask;
|
|
73
|
+
clearTimeout(timeoutHandle);
|
|
74
|
+
reject(err);
|
|
75
|
+
workerInfo.currentTask = null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Remove failed worker
|
|
79
|
+
const index = this.workers.indexOf(workerInfo);
|
|
80
|
+
if (index > -1) {
|
|
81
|
+
this.workers.splice(index, 1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const freeIndex = this.freeWorkers.indexOf(workerInfo);
|
|
85
|
+
if (freeIndex > -1) {
|
|
86
|
+
this.freeWorkers.splice(freeIndex, 1);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Create replacement if below minimum
|
|
90
|
+
if (this.workers.length < this.minWorkers && !this.closing) {
|
|
91
|
+
this._createWorker();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Process queue with remaining workers
|
|
95
|
+
this._processQueue();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
worker.on('exit', () => {
|
|
99
|
+
// Clean up worker references
|
|
100
|
+
const index = this.workers.indexOf(workerInfo);
|
|
101
|
+
if (index > -1) {
|
|
102
|
+
this.workers.splice(index, 1);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const freeIndex = this.freeWorkers.indexOf(workerInfo);
|
|
106
|
+
if (freeIndex > -1) {
|
|
107
|
+
this.freeWorkers.splice(freeIndex, 1);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
this.workers.push(workerInfo);
|
|
112
|
+
this.freeWorkers.push(workerInfo);
|
|
113
|
+
|
|
114
|
+
// Set up idle timeout for workers above minimum
|
|
115
|
+
if (this.workers.length > this.minWorkers) {
|
|
116
|
+
this._setupIdleTimeout(workerInfo);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return workerInfo;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
_setupIdleTimeout(workerInfo) {
|
|
123
|
+
if (workerInfo.timeoutHandle) {
|
|
124
|
+
clearTimeout(workerInfo.timeoutHandle);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
workerInfo.timeoutHandle = setTimeout(() => {
|
|
128
|
+
if (!workerInfo.busy && this.workers.length > this.minWorkers) {
|
|
129
|
+
this._removeWorker(workerInfo);
|
|
130
|
+
}
|
|
131
|
+
}, this.idleTimeout);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
_removeWorker(workerInfo) {
|
|
135
|
+
const index = this.workers.indexOf(workerInfo);
|
|
136
|
+
if (index > -1) {
|
|
137
|
+
this.workers.splice(index, 1);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const freeIndex = this.freeWorkers.indexOf(workerInfo);
|
|
141
|
+
if (freeIndex > -1) {
|
|
142
|
+
this.freeWorkers.splice(freeIndex, 1);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (workerInfo.timeoutHandle) {
|
|
146
|
+
clearTimeout(workerInfo.timeoutHandle);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
workerInfo.worker.terminate();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async process(data, options = {}) {
|
|
153
|
+
if (this.closing) {
|
|
154
|
+
throw new Error('Worker pool is closing');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const timeout = options.timeout || this.workerTimeout;
|
|
158
|
+
|
|
159
|
+
return new Promise((resolve, reject) => {
|
|
160
|
+
const task = { data, resolve, reject, timeout };
|
|
161
|
+
|
|
162
|
+
// Try to get a free worker
|
|
163
|
+
const workerInfo = this.freeWorkers.shift();
|
|
164
|
+
|
|
165
|
+
if (workerInfo) {
|
|
166
|
+
this._executeTask(workerInfo, task);
|
|
167
|
+
} else if (this.workers.length < this.maxWorkers) {
|
|
168
|
+
// Create new worker if below max
|
|
169
|
+
const newWorker = this._createWorker();
|
|
170
|
+
if (newWorker) {
|
|
171
|
+
this.freeWorkers.shift(); // Remove from free list
|
|
172
|
+
this._executeTask(newWorker, task);
|
|
173
|
+
}
|
|
174
|
+
} else if (this.queue.length < this.maxQueueSize) {
|
|
175
|
+
// Queue the task
|
|
176
|
+
this.queue.push(task);
|
|
177
|
+
} else {
|
|
178
|
+
reject(new Error('Worker pool queue is full'));
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
_executeTask(workerInfo, task) {
|
|
184
|
+
workerInfo.busy = true;
|
|
185
|
+
workerInfo.lastUsed = Date.now();
|
|
186
|
+
|
|
187
|
+
// Clear idle timeout while working
|
|
188
|
+
if (workerInfo.timeoutHandle) {
|
|
189
|
+
clearTimeout(workerInfo.timeoutHandle);
|
|
190
|
+
workerInfo.timeoutHandle = null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Set up task timeout
|
|
194
|
+
const timeoutHandle = setTimeout(() => {
|
|
195
|
+
task.reject(new Error(`Worker task timed out after ${task.timeout}ms`));
|
|
196
|
+
workerInfo.currentTask = null;
|
|
197
|
+
workerInfo.busy = false;
|
|
198
|
+
|
|
199
|
+
// Terminate and replace the worker
|
|
200
|
+
this._removeWorker(workerInfo);
|
|
201
|
+
if (this.workers.length < this.minWorkers && !this.closing) {
|
|
202
|
+
this._createWorker();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
this._processQueue();
|
|
206
|
+
}, task.timeout);
|
|
207
|
+
|
|
208
|
+
workerInfo.currentTask = { ...task, timeoutHandle };
|
|
209
|
+
workerInfo.worker.postMessage(task.data);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
_processQueue() {
|
|
213
|
+
if (this.queue.length === 0) {
|
|
214
|
+
// Mark free workers and set up idle timeouts
|
|
215
|
+
this.workers.forEach(workerInfo => {
|
|
216
|
+
if (!workerInfo.busy && !this.freeWorkers.includes(workerInfo)) {
|
|
217
|
+
this.freeWorkers.push(workerInfo);
|
|
218
|
+
if (this.workers.length > this.minWorkers) {
|
|
219
|
+
this._setupIdleTimeout(workerInfo);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const workerInfo = this.freeWorkers.shift();
|
|
227
|
+
if (workerInfo) {
|
|
228
|
+
const task = this.queue.shift();
|
|
229
|
+
if (task) {
|
|
230
|
+
this._executeTask(workerInfo, task);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async close() {
|
|
236
|
+
this.closing = true;
|
|
237
|
+
|
|
238
|
+
// Clear the queue
|
|
239
|
+
while (this.queue.length > 0) {
|
|
240
|
+
const task = this.queue.shift();
|
|
241
|
+
task.reject(new Error('Worker pool is closing'));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Terminate all workers
|
|
245
|
+
const terminatePromises = this.workers.map(workerInfo => {
|
|
246
|
+
if (workerInfo.timeoutHandle) {
|
|
247
|
+
clearTimeout(workerInfo.timeoutHandle);
|
|
248
|
+
}
|
|
249
|
+
return workerInfo.worker.terminate();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
await Promise.all(terminatePromises);
|
|
253
|
+
|
|
254
|
+
this.workers = [];
|
|
255
|
+
this.freeWorkers = [];
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
getStats() {
|
|
259
|
+
return {
|
|
260
|
+
totalWorkers: this.workers.length,
|
|
261
|
+
busyWorkers: this.workers.filter(w => w.busy).length,
|
|
262
|
+
freeWorkers: this.freeWorkers.length,
|
|
263
|
+
queuedTasks: this.queue.length,
|
|
264
|
+
minWorkers: this.minWorkers,
|
|
265
|
+
maxWorkers: this.maxWorkers
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
module.exports = WorkerPool;
|
package/package.json
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@postalsys/email-text-tools",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.3",
|
|
4
4
|
"description": "Email text tools",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"test": "
|
|
7
|
+
"test": "node --test test/*.test.js",
|
|
8
|
+
"test:coverage": "node --test --experimental-test-coverage test/*.test.js",
|
|
9
|
+
"test:watch": "node --test --watch test/*.test.js",
|
|
10
|
+
"lint": "eslint .",
|
|
11
|
+
"lint:fix": "eslint . --fix",
|
|
12
|
+
"format": "prettier --write .",
|
|
13
|
+
"format:check": "prettier --check .",
|
|
8
14
|
"update": "rm -rf node_modules package-lock.json && ncu -u && npm install"
|
|
9
15
|
},
|
|
10
16
|
"repository": {
|
|
@@ -19,18 +25,20 @@
|
|
|
19
25
|
},
|
|
20
26
|
"homepage": "https://github.com/postalsys/email-text-tools#readme",
|
|
21
27
|
"devDependencies": {
|
|
22
|
-
"eslint
|
|
23
|
-
"eslint
|
|
28
|
+
"@eslint/js": "9.38.0",
|
|
29
|
+
"eslint": "9.38.0",
|
|
30
|
+
"eslint-config-prettier": "10.1.8",
|
|
31
|
+
"prettier": "3.6.2"
|
|
24
32
|
},
|
|
25
33
|
"dependencies": {
|
|
26
|
-
"dompurify": "3.
|
|
34
|
+
"dompurify": "3.3.0",
|
|
27
35
|
"he": "1.2.0",
|
|
28
36
|
"html-to-text": "9.0.5",
|
|
29
|
-
"jsdom": "
|
|
30
|
-
"juice": "11.0.
|
|
37
|
+
"jsdom": "27.0.1",
|
|
38
|
+
"juice": "11.0.3",
|
|
31
39
|
"linkify-it": "5.0.0",
|
|
32
40
|
"moment-timezone": "0.6.0",
|
|
33
41
|
"node-html-parser": "7.0.1",
|
|
34
|
-
"tlds": "1.
|
|
42
|
+
"tlds": "1.261.0"
|
|
35
43
|
}
|
|
36
44
|
}
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { describe, it, before: _before, after } = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const mimeHtml = require('../lib/mime-html');
|
|
6
|
+
const textToHtml = require('../lib/text-to-html');
|
|
7
|
+
const _fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const { Worker } = require('worker_threads');
|
|
10
|
+
|
|
11
|
+
describe('Comprehensive Test Suite', () => {
|
|
12
|
+
describe('text-to-html conversion', () => {
|
|
13
|
+
it('should convert plain text to HTML', () => {
|
|
14
|
+
const text = 'Hello World\nThis is a test';
|
|
15
|
+
const html = textToHtml(text);
|
|
16
|
+
|
|
17
|
+
assert.ok(html.includes('Hello World'));
|
|
18
|
+
assert.ok(html.includes('<br'));
|
|
19
|
+
assert.ok(html.includes('This is a test'));
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should handle quoted text', () => {
|
|
23
|
+
const text = 'Normal text\n> Quoted text\n> More quoted\nNormal again';
|
|
24
|
+
const html = textToHtml(text);
|
|
25
|
+
|
|
26
|
+
assert.ok(html.includes('blockquote'));
|
|
27
|
+
assert.ok(html.includes('Quoted text'));
|
|
28
|
+
assert.ok(html.includes('Normal again'));
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should linkify URLs', () => {
|
|
32
|
+
const text = 'Visit https://example.com for more info';
|
|
33
|
+
const html = textToHtml(text);
|
|
34
|
+
|
|
35
|
+
assert.ok(html.includes('<a'));
|
|
36
|
+
assert.ok(html.includes('href='));
|
|
37
|
+
assert.ok(html.includes('https://example.com'));
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should handle email addresses', () => {
|
|
41
|
+
const text = 'Contact me at test@example.com';
|
|
42
|
+
const html = textToHtml(text);
|
|
43
|
+
|
|
44
|
+
assert.ok(html.includes('mailto:'));
|
|
45
|
+
assert.ok(html.includes('test@example.com'));
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should escape HTML entities', () => {
|
|
49
|
+
const text = 'Test <script>alert("xss")</script> & special chars';
|
|
50
|
+
const html = textToHtml(text);
|
|
51
|
+
|
|
52
|
+
assert.ok(!html.includes('<script>'));
|
|
53
|
+
assert.ok(html.includes('<script'));
|
|
54
|
+
assert.ok(html.includes('&'));
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should handle empty text', () => {
|
|
58
|
+
const html = textToHtml('');
|
|
59
|
+
assert.ok(typeof html === 'string');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should handle null/undefined', () => {
|
|
63
|
+
const html1 = textToHtml(null);
|
|
64
|
+
const html2 = textToHtml(undefined);
|
|
65
|
+
const html3 = textToHtml('');
|
|
66
|
+
|
|
67
|
+
// textToHtml returns minimal HTML for empty input
|
|
68
|
+
assert.ok(typeof html1 === 'string');
|
|
69
|
+
assert.ok(typeof html2 === 'string');
|
|
70
|
+
assert.ok(typeof html3 === 'string');
|
|
71
|
+
// Should return consistent output for all empty inputs
|
|
72
|
+
assert.equal(html1, html2);
|
|
73
|
+
assert.equal(html2, html3);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('juice-worker.js', () => {
|
|
78
|
+
it('should process HTML in worker thread', async () => {
|
|
79
|
+
const worker = new Worker(path.join(__dirname, '../lib/juice-worker.js'));
|
|
80
|
+
|
|
81
|
+
const html = '<style>p { color: red; }</style><p>Test</p>';
|
|
82
|
+
|
|
83
|
+
const result = await new Promise((resolve, reject) => {
|
|
84
|
+
worker.on('message', msg => {
|
|
85
|
+
worker.terminate();
|
|
86
|
+
if (msg.success) {
|
|
87
|
+
resolve(msg.result);
|
|
88
|
+
} else {
|
|
89
|
+
reject(new Error(msg.error));
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
worker.on('error', reject);
|
|
94
|
+
worker.postMessage(html);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
assert.ok(result.includes('style='));
|
|
98
|
+
assert.ok(result.includes('Test'));
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should handle worker errors gracefully', async () => {
|
|
102
|
+
const worker = new Worker(path.join(__dirname, '../lib/juice-worker.js'));
|
|
103
|
+
|
|
104
|
+
// Send invalid data that should cause an error
|
|
105
|
+
const invalidData = null;
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
await new Promise((resolve, reject) => {
|
|
109
|
+
worker.on('message', msg => {
|
|
110
|
+
worker.terminate();
|
|
111
|
+
if (msg.success) {
|
|
112
|
+
resolve(msg.result);
|
|
113
|
+
} else {
|
|
114
|
+
reject(new Error(msg.error));
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
worker.on('error', reject);
|
|
119
|
+
worker.postMessage(invalidData);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// If we get here without error, that's also OK
|
|
123
|
+
assert.ok(true);
|
|
124
|
+
} catch (err) {
|
|
125
|
+
// Expected to error
|
|
126
|
+
assert.ok(err);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe('mime-html with text input', () => {
|
|
132
|
+
it('should properly convert text to HTML with default styles', () => {
|
|
133
|
+
const input = {
|
|
134
|
+
text: 'Hello\nWorld\n\nThis is a test'
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const result = mimeHtml.sync(input);
|
|
138
|
+
|
|
139
|
+
assert.ok(result.includes('Hello'));
|
|
140
|
+
assert.ok(result.includes('World'));
|
|
141
|
+
assert.ok(result.includes('font-family')); // Default styles applied
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should handle quoted text in text mode', () => {
|
|
145
|
+
const input = {
|
|
146
|
+
text: 'Reply:\n> Original message\n> Second line\n\nMy response'
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const result = mimeHtml.sync(input);
|
|
150
|
+
|
|
151
|
+
assert.ok(result.includes('blockquote'));
|
|
152
|
+
assert.ok(result.includes('Original message'));
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe('error recovery and edge cases', () => {
|
|
157
|
+
it('should handle circular references in DOM', () => {
|
|
158
|
+
// This tests the DOM walking logic
|
|
159
|
+
const input = {
|
|
160
|
+
html: '<div><p>Test</p></div>'
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const result = mimeHtml.sync(input);
|
|
164
|
+
assert.ok(result.includes('Test'));
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should handle very long attribute values', () => {
|
|
168
|
+
const longValue = 'x'.repeat(10000);
|
|
169
|
+
const input = {
|
|
170
|
+
html: `<div data-test="${longValue}">Content</div>`
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const result = mimeHtml.sync(input);
|
|
174
|
+
assert.ok(result.includes('Content'));
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should handle style tags in body', () => {
|
|
178
|
+
const input = {
|
|
179
|
+
html: `
|
|
180
|
+
<body>
|
|
181
|
+
<style>p { color: red; }</style>
|
|
182
|
+
<p>Test</p>
|
|
183
|
+
<style>div { color: blue; }</style>
|
|
184
|
+
<div>More</div>
|
|
185
|
+
</body>
|
|
186
|
+
`
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const result = mimeHtml.sync(input);
|
|
190
|
+
assert.ok(result.includes('Test'));
|
|
191
|
+
assert.ok(result.includes('More'));
|
|
192
|
+
// Style tags in body should be removed
|
|
193
|
+
assert.ok(!result.includes('<style'));
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('should handle CDATA sections', () => {
|
|
197
|
+
const input = {
|
|
198
|
+
html: '<div><![CDATA[Some text]]></div>'
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const result = mimeHtml.sync(input);
|
|
202
|
+
assert.ok(typeof result === 'string');
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should handle XML namespaces', () => {
|
|
206
|
+
const input = {
|
|
207
|
+
html: '<html xmlns="http://www.w3.org/1999/xhtml"><body><p>Test</p></body></html>'
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const result = mimeHtml.sync(input);
|
|
211
|
+
assert.ok(result.includes('Test'));
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe('async version configuration', () => {
|
|
216
|
+
it('should respect timeout configuration', async () => {
|
|
217
|
+
const input = {
|
|
218
|
+
html: '<p>Quick test</p>'
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const start = Date.now();
|
|
222
|
+
const result = await mimeHtml.async(input, {
|
|
223
|
+
timeout: 100,
|
|
224
|
+
useWorkerPool: false
|
|
225
|
+
});
|
|
226
|
+
const elapsed = Date.now() - start;
|
|
227
|
+
|
|
228
|
+
assert.ok(result.includes('Quick test'));
|
|
229
|
+
// Should complete without timing out
|
|
230
|
+
assert.ok(elapsed < 200);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('should respect logErrors configuration', async () => {
|
|
234
|
+
const input = {
|
|
235
|
+
html: null
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// Capture console.warn
|
|
239
|
+
const originalWarn = console.warn;
|
|
240
|
+
let warnCalled = false;
|
|
241
|
+
console.warn = () => {
|
|
242
|
+
warnCalled = true;
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
await mimeHtml.async(input, {
|
|
246
|
+
logErrors: false,
|
|
247
|
+
fallbackOnError: true
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
console.warn = originalWarn;
|
|
251
|
+
assert.ok(!warnCalled, 'Should not log errors when logErrors is false');
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('should respect fallbackOnError configuration', async () => {
|
|
255
|
+
const input = {
|
|
256
|
+
html: '<p>Test</p>'
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
// With fallback
|
|
260
|
+
const result1 = await mimeHtml.async(input, {
|
|
261
|
+
fallbackOnError: true,
|
|
262
|
+
logErrors: false
|
|
263
|
+
});
|
|
264
|
+
assert.ok(typeof result1 === 'string');
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
describe('worker pool configuration', () => {
|
|
269
|
+
it('should respect min/max worker settings', async () => {
|
|
270
|
+
const input = {
|
|
271
|
+
html: '<p>Pool config test</p>'
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
// First close any existing pool
|
|
275
|
+
await mimeHtml.closeWorkerPool();
|
|
276
|
+
|
|
277
|
+
// Process with custom pool settings
|
|
278
|
+
await mimeHtml.async(input, {
|
|
279
|
+
useWorkerPool: true,
|
|
280
|
+
minWorkers: 1,
|
|
281
|
+
maxWorkers: 2
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const stats = mimeHtml.getWorkerPoolStats();
|
|
285
|
+
assert.ok(stats);
|
|
286
|
+
assert.equal(stats.minWorkers, 1);
|
|
287
|
+
assert.equal(stats.maxWorkers, 2);
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
describe('concurrent stress test', () => {
|
|
292
|
+
it('should handle rapid concurrent requests without deadlock', async () => {
|
|
293
|
+
const promises = [];
|
|
294
|
+
|
|
295
|
+
// Fire off 50 concurrent requests rapidly
|
|
296
|
+
for (let i = 0; i < 50; i++) {
|
|
297
|
+
promises.push(
|
|
298
|
+
mimeHtml.async(
|
|
299
|
+
{
|
|
300
|
+
html: `<p>Stress test ${i}</p>`
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
useWorkerPool: true,
|
|
304
|
+
timeout: 5000
|
|
305
|
+
}
|
|
306
|
+
)
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const results = await Promise.all(promises);
|
|
311
|
+
|
|
312
|
+
assert.equal(results.length, 50);
|
|
313
|
+
results.forEach((result, i) => {
|
|
314
|
+
assert.ok(result.includes(`Stress test ${i}`));
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
describe('memory leak prevention', () => {
|
|
320
|
+
it('should clean up workers after timeout', async () => {
|
|
321
|
+
const input = {
|
|
322
|
+
html: '<p>Timeout test</p>'
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
try {
|
|
326
|
+
// This should timeout and clean up the worker
|
|
327
|
+
await mimeHtml.async(input, {
|
|
328
|
+
timeout: 1,
|
|
329
|
+
useWorkerPool: false,
|
|
330
|
+
fallbackOnError: false,
|
|
331
|
+
logErrors: false
|
|
332
|
+
});
|
|
333
|
+
} catch (err) {
|
|
334
|
+
assert.ok(err.message.includes('timed out'));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Worker should be cleaned up, no leak
|
|
338
|
+
const memAfter = process.memoryUsage();
|
|
339
|
+
assert.ok(memAfter.heapUsed < 500 * 1024 * 1024); // Less than 500MB
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
// Clean up
|
|
344
|
+
after(async () => {
|
|
345
|
+
await mimeHtml.closeWorkerPool();
|
|
346
|
+
});
|
|
347
|
+
});
|