@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,62 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { describe, it, after } = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const mimeHtml = require('../lib/mime-html');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
describe('Edge Cases', () => {
|
|
10
|
+
describe('large HTML', () => {
|
|
11
|
+
it('should handle large HTML', async () => {
|
|
12
|
+
const largeContent = '<p>' + 'x'.repeat(10000) + '</p>';
|
|
13
|
+
const result = await mimeHtml.async({ html: largeContent });
|
|
14
|
+
assert.ok(result.length > 0);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('special characters', () => {
|
|
19
|
+
it('should handle UTF-8', () => {
|
|
20
|
+
const result = mimeHtml.sync({ html: '<p>Hello 世界</p>' });
|
|
21
|
+
assert.ok(result.includes('Hello'));
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should handle entities', () => {
|
|
25
|
+
const result = mimeHtml.sync({ html: '<p><>&</p>' });
|
|
26
|
+
assert.ok(result.includes('<') || result.includes('<'));
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('problematic CSS', () => {
|
|
31
|
+
it('should handle modern selectors', () => {
|
|
32
|
+
const html = `
|
|
33
|
+
<style>
|
|
34
|
+
.test:is(.a) { color: red; }
|
|
35
|
+
</style>
|
|
36
|
+
<p class="test a">Test</p>
|
|
37
|
+
`;
|
|
38
|
+
const result = mimeHtml.sync({ html });
|
|
39
|
+
assert.ok(result.includes('Test'));
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('real-world HTML', () => {
|
|
44
|
+
it('should process failing.html quickly', async () => {
|
|
45
|
+
const failingPath = path.join(__dirname, '../examples/failing.html');
|
|
46
|
+
if (fs.existsSync(failingPath)) {
|
|
47
|
+
const html = fs.readFileSync(failingPath, 'utf8');
|
|
48
|
+
|
|
49
|
+
const start = Date.now();
|
|
50
|
+
const result = await mimeHtml.async({ html });
|
|
51
|
+
const elapsed = Date.now() - start;
|
|
52
|
+
|
|
53
|
+
assert.ok(result.length > 0);
|
|
54
|
+
assert.ok(elapsed < 2000, `Took ${elapsed}ms`);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
after(async () => {
|
|
60
|
+
await mimeHtml.closeWorkerPool();
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { describe, it, before: _before, after, beforeEach: _beforeEach, afterEach: _afterEach } = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const mimeHtml = require('../lib/mime-html');
|
|
6
|
+
|
|
7
|
+
describe('mime-html', () => {
|
|
8
|
+
describe('sync version', () => {
|
|
9
|
+
it('should process simple HTML', () => {
|
|
10
|
+
const input = {
|
|
11
|
+
html: '<html><body><p>Hello World</p></body></html>'
|
|
12
|
+
};
|
|
13
|
+
const result = mimeHtml.sync(input);
|
|
14
|
+
|
|
15
|
+
assert.ok(result.includes('Hello World'));
|
|
16
|
+
assert.ok(result.includes('<div'));
|
|
17
|
+
assert.ok(result.includes('</div>'));
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should convert text to HTML when no HTML provided', () => {
|
|
21
|
+
const input = {
|
|
22
|
+
text: 'Hello World\nThis is a test'
|
|
23
|
+
};
|
|
24
|
+
const result = mimeHtml.sync(input);
|
|
25
|
+
|
|
26
|
+
assert.ok(result.includes('Hello World'));
|
|
27
|
+
assert.ok(result.includes('This is a test'));
|
|
28
|
+
assert.ok(result.includes('<br'));
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should handle HTML with CSS styles', () => {
|
|
32
|
+
const input = {
|
|
33
|
+
html: `
|
|
34
|
+
<html>
|
|
35
|
+
<head>
|
|
36
|
+
<style>
|
|
37
|
+
p { color: red; }
|
|
38
|
+
.test { font-weight: bold; }
|
|
39
|
+
</style>
|
|
40
|
+
</head>
|
|
41
|
+
<body>
|
|
42
|
+
<p class="test">Styled text</p>
|
|
43
|
+
</body>
|
|
44
|
+
</html>
|
|
45
|
+
`
|
|
46
|
+
};
|
|
47
|
+
const result = mimeHtml.sync(input);
|
|
48
|
+
|
|
49
|
+
assert.ok(result.includes('Styled text'));
|
|
50
|
+
assert.ok(result.includes('style='));
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should sanitize dangerous HTML', () => {
|
|
54
|
+
const input = {
|
|
55
|
+
html: `
|
|
56
|
+
<html>
|
|
57
|
+
<body>
|
|
58
|
+
<p>Safe content</p>
|
|
59
|
+
<script>alert('XSS')</script>
|
|
60
|
+
<img src=x onerror="alert('XSS')">
|
|
61
|
+
</body>
|
|
62
|
+
</html>
|
|
63
|
+
`
|
|
64
|
+
};
|
|
65
|
+
const result = mimeHtml.sync(input);
|
|
66
|
+
|
|
67
|
+
assert.ok(result.includes('Safe content'));
|
|
68
|
+
assert.ok(!result.includes('<script'));
|
|
69
|
+
assert.ok(!result.includes('onerror'));
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should add target="_blank" to links', () => {
|
|
73
|
+
const input = {
|
|
74
|
+
html: '<a href="https://example.com">Link</a>'
|
|
75
|
+
};
|
|
76
|
+
const result = mimeHtml.sync(input);
|
|
77
|
+
|
|
78
|
+
assert.ok(result.includes('target="_blank"'));
|
|
79
|
+
assert.ok(result.includes('Link'));
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should remove problematic CSS selectors', () => {
|
|
83
|
+
const input = {
|
|
84
|
+
html: `
|
|
85
|
+
<html>
|
|
86
|
+
<head>
|
|
87
|
+
<style>
|
|
88
|
+
.item:is(.active, .selected) { background: yellow; }
|
|
89
|
+
.element:where(.primary) { color: blue; }
|
|
90
|
+
.parent:has(> .child) { border: 1px solid red; }
|
|
91
|
+
p { color: green; }
|
|
92
|
+
</style>
|
|
93
|
+
</head>
|
|
94
|
+
<body>
|
|
95
|
+
<p class="item active">Test</p>
|
|
96
|
+
</body>
|
|
97
|
+
</html>
|
|
98
|
+
`
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// Should complete without hanging
|
|
102
|
+
const result = mimeHtml.sync(input);
|
|
103
|
+
assert.ok(result.includes('Test'));
|
|
104
|
+
assert.ok(result.includes('color'));
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should remove forbidden style properties', () => {
|
|
108
|
+
const input = {
|
|
109
|
+
html: `
|
|
110
|
+
<html>
|
|
111
|
+
<body style="position: fixed; width: 100%; height: 100%;">
|
|
112
|
+
<div style="position: absolute; top: 0;">Content</div>
|
|
113
|
+
</body>
|
|
114
|
+
</html>
|
|
115
|
+
`
|
|
116
|
+
};
|
|
117
|
+
const result = mimeHtml.sync(input);
|
|
118
|
+
|
|
119
|
+
assert.ok(result.includes('Content'));
|
|
120
|
+
assert.ok(!result.includes('position'));
|
|
121
|
+
assert.ok(!result.includes('width'));
|
|
122
|
+
assert.ok(!result.includes('height'));
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should handle empty input gracefully', () => {
|
|
126
|
+
const input = { html: '' };
|
|
127
|
+
const result = mimeHtml.sync(input);
|
|
128
|
+
|
|
129
|
+
assert.ok(typeof result === 'string');
|
|
130
|
+
assert.ok(result.includes('<div'));
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should handle malformed HTML', () => {
|
|
134
|
+
const input = {
|
|
135
|
+
html: '<p>Unclosed paragraph <div>Nested content</p></div>'
|
|
136
|
+
};
|
|
137
|
+
const result = mimeHtml.sync(input);
|
|
138
|
+
|
|
139
|
+
assert.ok(result.includes('Unclosed paragraph'));
|
|
140
|
+
assert.ok(result.includes('Nested content'));
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should preserve HTML entities', () => {
|
|
144
|
+
const input = {
|
|
145
|
+
html: '<p><script> & "quotes"</p>'
|
|
146
|
+
};
|
|
147
|
+
const result = mimeHtml.sync(input);
|
|
148
|
+
|
|
149
|
+
assert.ok(result.includes('<'));
|
|
150
|
+
assert.ok(result.includes('&'));
|
|
151
|
+
assert.ok(result.includes('"') || result.includes('"'));
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe('async version', () => {
|
|
156
|
+
it('should process simple HTML', async () => {
|
|
157
|
+
const input = {
|
|
158
|
+
html: '<html><body><p>Hello Async</p></body></html>'
|
|
159
|
+
};
|
|
160
|
+
const result = await mimeHtml.async(input);
|
|
161
|
+
|
|
162
|
+
assert.ok(result.includes('Hello Async'));
|
|
163
|
+
assert.ok(result.includes('<div'));
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should handle timeout option', async () => {
|
|
167
|
+
const input = {
|
|
168
|
+
html: '<p>Quick test</p>'
|
|
169
|
+
};
|
|
170
|
+
const result = await mimeHtml.async(input, { timeout: 1000 });
|
|
171
|
+
|
|
172
|
+
assert.ok(result.includes('Quick test'));
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('should use worker pool by default', async () => {
|
|
176
|
+
const input = {
|
|
177
|
+
html: '<p>Pool test</p>'
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const promises = [];
|
|
181
|
+
for (let i = 0; i < 5; i++) {
|
|
182
|
+
promises.push(mimeHtml.async(input));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const results = await Promise.all(promises);
|
|
186
|
+
assert.equal(results.length, 5);
|
|
187
|
+
results.forEach(result => {
|
|
188
|
+
assert.ok(result.includes('Pool test'));
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const stats = mimeHtml.getWorkerPoolStats();
|
|
192
|
+
assert.ok(stats);
|
|
193
|
+
assert.ok(stats.totalWorkers >= 2);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('should work without worker pool when disabled', async () => {
|
|
197
|
+
const input = {
|
|
198
|
+
html: '<p>No pool test</p>'
|
|
199
|
+
};
|
|
200
|
+
const result = await mimeHtml.async(input, { useWorkerPool: false });
|
|
201
|
+
|
|
202
|
+
assert.ok(result.includes('No pool test'));
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should handle problematic CSS with pre-filtering', async () => {
|
|
206
|
+
const input = {
|
|
207
|
+
html: `
|
|
208
|
+
<html>
|
|
209
|
+
<head>
|
|
210
|
+
<style>
|
|
211
|
+
.test:is(.a, .b) { color: red; }
|
|
212
|
+
.item:where(.x) { color: blue; }
|
|
213
|
+
.parent:has(.child) { border: 1px solid; }
|
|
214
|
+
</style>
|
|
215
|
+
</head>
|
|
216
|
+
<body>
|
|
217
|
+
<p class="test a">Async test</p>
|
|
218
|
+
</body>
|
|
219
|
+
</html>
|
|
220
|
+
`
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const start = Date.now();
|
|
224
|
+
const result = await mimeHtml.async(input, { timeout: 2000 });
|
|
225
|
+
const elapsed = Date.now() - start;
|
|
226
|
+
|
|
227
|
+
assert.ok(result.includes('Async test'));
|
|
228
|
+
assert.ok(elapsed < 1000, 'Should complete quickly with pre-filtering');
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('should handle errors with fallback', async () => {
|
|
232
|
+
const input = {
|
|
233
|
+
html: '<p>Error test</p>'
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
// Even with errors, should return result with fallback
|
|
237
|
+
const result = await mimeHtml.async(input, {
|
|
238
|
+
fallbackOnError: true,
|
|
239
|
+
logErrors: false
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
assert.ok(typeof result === 'string');
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('should throw errors when fallback disabled', async () => {
|
|
246
|
+
// Create intentionally problematic scenario
|
|
247
|
+
const input = {
|
|
248
|
+
html: null // This will cause issues
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
await mimeHtml.async(input, {
|
|
253
|
+
fallbackOnError: false,
|
|
254
|
+
logErrors: false
|
|
255
|
+
});
|
|
256
|
+
assert.fail('Should have thrown an error');
|
|
257
|
+
} catch (err) {
|
|
258
|
+
assert.ok(err);
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
describe('backward compatibility', () => {
|
|
264
|
+
it('should maintain default export compatibility', () => {
|
|
265
|
+
const input = {
|
|
266
|
+
html: '<p>Compatibility test</p>'
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
// Default export should work
|
|
270
|
+
const result = mimeHtml(input);
|
|
271
|
+
assert.ok(result.includes('Compatibility test'));
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('should have sync and async methods available', () => {
|
|
275
|
+
assert.equal(typeof mimeHtml.sync, 'function');
|
|
276
|
+
assert.equal(typeof mimeHtml.async, 'function');
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('should have worker pool management methods', () => {
|
|
280
|
+
assert.equal(typeof mimeHtml.closeWorkerPool, 'function');
|
|
281
|
+
assert.equal(typeof mimeHtml.getWorkerPoolStats, 'function');
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
describe('concurrent processing', () => {
|
|
286
|
+
it('should handle multiple concurrent requests', async () => {
|
|
287
|
+
const htmls = [];
|
|
288
|
+
for (let i = 0; i < 10; i++) {
|
|
289
|
+
htmls.push({
|
|
290
|
+
html: `<p>Request ${i}</p>`
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const promises = htmls.map((input, i) =>
|
|
295
|
+
mimeHtml.async(input).then(result => {
|
|
296
|
+
assert.ok(result.includes(`Request ${i}`));
|
|
297
|
+
return i;
|
|
298
|
+
})
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
const results = await Promise.all(promises);
|
|
302
|
+
assert.equal(results.length, 10);
|
|
303
|
+
assert.deepEqual(results, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('should handle mixed content types concurrently', async () => {
|
|
307
|
+
const inputs = [
|
|
308
|
+
{ html: '<p>HTML content</p>' },
|
|
309
|
+
{ text: 'Plain text content' },
|
|
310
|
+
{ html: '<div>Another HTML</div>' },
|
|
311
|
+
{ text: 'More text\nWith newlines' }
|
|
312
|
+
];
|
|
313
|
+
|
|
314
|
+
const results = await Promise.all(inputs.map(input => mimeHtml.async(input)));
|
|
315
|
+
|
|
316
|
+
assert.ok(results[0].includes('HTML content'));
|
|
317
|
+
assert.ok(results[1].includes('Plain text content'));
|
|
318
|
+
assert.ok(results[2].includes('Another HTML'));
|
|
319
|
+
assert.ok(results[3].includes('More text'));
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// Clean up worker pool after all tests
|
|
324
|
+
after(async () => {
|
|
325
|
+
await mimeHtml.closeWorkerPool();
|
|
326
|
+
});
|
|
327
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { describe, it, after: _after } = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const WorkerPool = require('../lib/worker-pool');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
// Test for the worker pool free list regression fix
|
|
9
|
+
describe('Worker Pool Regression Tests', () => {
|
|
10
|
+
describe('free workers list management', () => {
|
|
11
|
+
it('should properly return workers to free list after completion', async () => {
|
|
12
|
+
// Create a simple test worker
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const workerPath = path.join(__dirname, 'test-regression-worker.js');
|
|
15
|
+
const workerCode = `
|
|
16
|
+
const { parentPort } = require('worker_threads');
|
|
17
|
+
parentPort.on('message', (data) => {
|
|
18
|
+
setTimeout(() => {
|
|
19
|
+
parentPort.postMessage({ success: true, result: data });
|
|
20
|
+
}, 10);
|
|
21
|
+
});
|
|
22
|
+
`;
|
|
23
|
+
fs.writeFileSync(workerPath, workerCode);
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const pool = new WorkerPool({
|
|
27
|
+
workerPath,
|
|
28
|
+
minWorkers: 2,
|
|
29
|
+
maxWorkers: 4
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Initial state check
|
|
33
|
+
let stats = pool.getStats();
|
|
34
|
+
assert.equal(stats.totalWorkers, 2);
|
|
35
|
+
assert.equal(stats.freeWorkers, 2);
|
|
36
|
+
assert.equal(stats.busyWorkers, 0);
|
|
37
|
+
|
|
38
|
+
// Process multiple tasks concurrently
|
|
39
|
+
const promises = [];
|
|
40
|
+
for (let i = 0; i < 10; i++) {
|
|
41
|
+
promises.push(pool.process(`task ${i}`));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Wait for all to complete
|
|
45
|
+
const results = await Promise.all(promises);
|
|
46
|
+
|
|
47
|
+
// All tasks should complete successfully
|
|
48
|
+
assert.equal(results.length, 10);
|
|
49
|
+
results.forEach((result, i) => {
|
|
50
|
+
assert.equal(result, `task ${i}`);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// After completion, all workers should be free
|
|
54
|
+
stats = pool.getStats();
|
|
55
|
+
assert.equal(stats.busyWorkers, 0);
|
|
56
|
+
assert.equal(stats.freeWorkers, stats.totalWorkers);
|
|
57
|
+
|
|
58
|
+
// Process another batch to ensure workers are reusable
|
|
59
|
+
const secondBatch = [];
|
|
60
|
+
for (let i = 0; i < 5; i++) {
|
|
61
|
+
secondBatch.push(pool.process(`second ${i}`));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const secondResults = await Promise.all(secondBatch);
|
|
65
|
+
assert.equal(secondResults.length, 5);
|
|
66
|
+
|
|
67
|
+
// Workers should still be properly managed
|
|
68
|
+
stats = pool.getStats();
|
|
69
|
+
assert.equal(stats.busyWorkers, 0);
|
|
70
|
+
assert.equal(stats.freeWorkers, stats.totalWorkers);
|
|
71
|
+
|
|
72
|
+
await pool.close();
|
|
73
|
+
} finally {
|
|
74
|
+
// Clean up test worker
|
|
75
|
+
const fs = require('fs');
|
|
76
|
+
try {
|
|
77
|
+
fs.unlinkSync(workerPath);
|
|
78
|
+
} catch (_err) {
|
|
79
|
+
// Ignore cleanup errors
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should not have duplicate workers in free list', async () => {
|
|
85
|
+
const fs = require('fs');
|
|
86
|
+
const workerPath = path.join(__dirname, 'test-dup-worker.js');
|
|
87
|
+
const workerCode = `
|
|
88
|
+
const { parentPort } = require('worker_threads');
|
|
89
|
+
parentPort.on('message', (data) => {
|
|
90
|
+
setTimeout(() => {
|
|
91
|
+
parentPort.postMessage({ success: true, result: data });
|
|
92
|
+
}, 5);
|
|
93
|
+
});
|
|
94
|
+
`;
|
|
95
|
+
fs.writeFileSync(workerPath, workerCode);
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const pool = new WorkerPool({
|
|
99
|
+
workerPath,
|
|
100
|
+
minWorkers: 1,
|
|
101
|
+
maxWorkers: 2
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Process tasks to trigger worker management
|
|
105
|
+
const results = await Promise.all([pool.process('A'), pool.process('B'), pool.process('C')]);
|
|
106
|
+
|
|
107
|
+
assert.equal(results.length, 3);
|
|
108
|
+
|
|
109
|
+
// Check that free workers count matches actual workers
|
|
110
|
+
const stats = pool.getStats();
|
|
111
|
+
assert.ok(stats.freeWorkers <= stats.totalWorkers);
|
|
112
|
+
assert.equal(stats.busyWorkers, 0);
|
|
113
|
+
|
|
114
|
+
// The sum should always equal total
|
|
115
|
+
assert.equal(stats.busyWorkers + stats.freeWorkers, stats.totalWorkers);
|
|
116
|
+
|
|
117
|
+
await pool.close();
|
|
118
|
+
} finally {
|
|
119
|
+
const fs = require('fs');
|
|
120
|
+
try {
|
|
121
|
+
fs.unlinkSync(workerPath);
|
|
122
|
+
} catch (_err) {
|
|
123
|
+
// Ignore
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
});
|