@postalsys/email-text-tools 2.4.26 → 2.5.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 (84) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +10 -0
  3. package/dist/cjs/index.d.ts +18 -0
  4. package/dist/cjs/index.js +28 -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-text.d.ts +3 -0
  16. package/dist/cjs/lib/inline-text.js +111 -0
  17. package/dist/cjs/lib/juice-worker.d.ts +1 -0
  18. package/dist/cjs/lib/juice-worker.js +19 -0
  19. package/dist/cjs/lib/mime-html.d.ts +34 -0
  20. package/dist/cjs/lib/mime-html.js +529 -0
  21. package/dist/cjs/lib/text-to-html.d.ts +1 -0
  22. package/dist/cjs/lib/text-to-html.js +182 -0
  23. package/dist/cjs/lib/worker-pool.d.ts +61 -0
  24. package/dist/cjs/lib/worker-pool.js +229 -0
  25. package/dist/cjs/package.json +3 -0
  26. package/dist/esm/index.d.ts +18 -0
  27. package/dist/esm/index.js +12 -0
  28. package/dist/esm/lib/commonjs.cjs +17 -0
  29. package/dist/esm/lib/commonjs.d.cts +4 -0
  30. package/dist/esm/lib/format-date.d.ts +15 -0
  31. package/{lib → dist/esm/lib}/format-date.js +8 -16
  32. package/dist/esm/lib/html-guard.d.ts +3 -0
  33. package/{lib → dist/esm/lib}/html-guard.js +8 -24
  34. package/dist/esm/lib/html-to-text.d.ts +1 -0
  35. package/{lib → dist/esm/lib}/html-to-text.js +2 -14
  36. package/dist/esm/lib/inline-html.d.ts +19 -0
  37. package/{lib → dist/esm/lib}/inline-html.js +17 -55
  38. package/dist/esm/lib/inline-text.d.ts +3 -0
  39. package/{lib → dist/esm/lib}/inline-text.js +8 -31
  40. package/dist/esm/lib/juice-worker.d.ts +1 -0
  41. package/{lib → dist/esm/lib}/juice-worker.js +7 -8
  42. package/dist/esm/lib/mime-html.d.ts +34 -0
  43. package/{lib → dist/esm/lib}/mime-html.js +79 -127
  44. package/dist/esm/lib/text-to-html.d.ts +1 -0
  45. package/{lib → dist/esm/lib}/text-to-html.js +35 -63
  46. package/dist/esm/lib/worker-pool.d.ts +61 -0
  47. package/{lib → dist/esm/lib}/worker-pool.js +18 -64
  48. package/dist/esm/package.json +3 -0
  49. package/package.json +67 -10
  50. package/.github/VULNERABILITY_REPORT.yml +0 -75
  51. package/.github/workflows/release.yaml +0 -38
  52. package/.github/workflows/test.yaml +0 -35
  53. package/.ncurc.js +0 -28
  54. package/.prettierignore +0 -14
  55. package/.prettierrc.js +0 -10
  56. package/.release-please-manifest.json +0 -3
  57. package/CLAUDE.md +0 -105
  58. package/benchmark/benchmark.js +0 -348
  59. package/benchmark/concurrency-benchmark.js +0 -176
  60. package/benchmark/quick-benchmark.js +0 -200
  61. package/benchmark/quick-concurrency.js +0 -64
  62. package/benchmark/real-world-benchmark.js +0 -257
  63. package/docs/PERFORMANCE.md +0 -262
  64. package/eslint.config.js +0 -71
  65. package/examples/failing.html +0 -617
  66. package/examples/fixtures/message-1.json +0 -74
  67. package/examples/inline-html.js +0 -24
  68. package/examples/inline-text.js +0 -24
  69. package/examples/mime-html.js +0 -284
  70. package/index.js +0 -22
  71. package/release-please-config.json +0 -10
  72. package/test/comprehensive.test.js +0 -358
  73. package/test/containment.test.js +0 -128
  74. package/test/edge-cases.test.js +0 -62
  75. package/test/hardening.test.js +0 -504
  76. package/test/html-to-text.test.js +0 -253
  77. package/test/inline-html.test.js +0 -230
  78. package/test/inline-text.test.js +0 -204
  79. package/test/mime-html-extra.test.js +0 -39
  80. package/test/mime-html.test.js +0 -302
  81. package/test/text-to-html-extra.test.js +0 -42
  82. package/test/worker-pool-extra.test.js +0 -132
  83. package/test/worker-pool-regression.test.js +0 -128
  84. package/test/worker-pool.test.js +0 -282
@@ -1,39 +0,0 @@
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
-
7
- describe('mime-html additional coverage', () => {
8
- after(async () => {
9
- await mimeHtml.closeWorkerPool();
10
- });
11
-
12
- describe('getWorkerPoolStats', () => {
13
- it('should return null when no pool is initialized', async () => {
14
- await mimeHtml.closeWorkerPool();
15
- const stats = mimeHtml.getWorkerPoolStats();
16
- assert.equal(stats, null);
17
- });
18
- });
19
-
20
- describe('pool fallback to single worker', () => {
21
- it('should still return result when pool times out and falls back', async () => {
22
- await mimeHtml.closeWorkerPool();
23
-
24
- // Use timeout: 1 to force pool timeout, triggering fallback to single worker
25
- // Both may time out, but fallbackOnError defaults to true so processing continues
26
- const result = await mimeHtml.async(
27
- // a style sheet, so the juice worker actually runs
28
- { html: '<style>p { color: red; }</style><p>Fallback test</p>' },
29
- {
30
- timeout: 1,
31
- useWorkerPool: true
32
- }
33
- );
34
-
35
- // Should still get a result (with or without inlined CSS)
36
- assert.ok(result.includes('Fallback test'));
37
- });
38
- });
39
- });
@@ -1,302 +0,0 @@
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>&lt;script&gt; &amp; &quot;quotes&quot;</p>'
146
- };
147
- const result = mimeHtml.sync(input);
148
-
149
- assert.ok(result.includes('&lt;'));
150
- assert.ok(result.includes('&amp;'));
151
- assert.ok(result.includes('&quot;') || 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
- // juice, and with it the pool, only runs when there is a style sheet to inline
178
- html: '<style>p { color: red; }</style><p>Pool test</p>'
179
- };
180
-
181
- const promises = [];
182
- for (let i = 0; i < 5; i++) {
183
- promises.push(mimeHtml.async(input));
184
- }
185
-
186
- const results = await Promise.all(promises);
187
- assert.equal(results.length, 5);
188
- results.forEach(result => {
189
- assert.ok(result.includes('Pool test'));
190
- });
191
-
192
- const stats = mimeHtml.getWorkerPoolStats();
193
- assert.ok(stats);
194
- assert.ok(stats.totalWorkers >= 2);
195
- });
196
-
197
- it('should work without worker pool when disabled', async () => {
198
- const input = {
199
- html: '<p>No pool test</p>'
200
- };
201
- const result = await mimeHtml.async(input, { useWorkerPool: false });
202
-
203
- assert.ok(result.includes('No pool test'));
204
- });
205
-
206
- it('should handle errors with fallback', async () => {
207
- const input = {
208
- html: '<p>Error test</p>'
209
- };
210
-
211
- // Even with errors, should return result with fallback
212
- const result = await mimeHtml.async(input, {
213
- fallbackOnError: true,
214
- logErrors: false
215
- });
216
-
217
- assert.ok(typeof result === 'string');
218
- });
219
-
220
- it('should throw errors when fallback disabled', async () => {
221
- // Create intentionally problematic scenario
222
- const input = {
223
- html: null // This will cause issues
224
- };
225
-
226
- try {
227
- await mimeHtml.async(input, {
228
- fallbackOnError: false,
229
- logErrors: false
230
- });
231
- assert.fail('Should have thrown an error');
232
- } catch (err) {
233
- assert.ok(err);
234
- }
235
- });
236
- });
237
-
238
- describe('backward compatibility', () => {
239
- it('should maintain default export compatibility', () => {
240
- const input = {
241
- html: '<p>Compatibility test</p>'
242
- };
243
-
244
- // Default export should work
245
- const result = mimeHtml(input);
246
- assert.ok(result.includes('Compatibility test'));
247
- });
248
-
249
- it('should have sync and async methods available', () => {
250
- assert.equal(typeof mimeHtml.sync, 'function');
251
- assert.equal(typeof mimeHtml.async, 'function');
252
- });
253
-
254
- it('should have worker pool management methods', () => {
255
- assert.equal(typeof mimeHtml.closeWorkerPool, 'function');
256
- assert.equal(typeof mimeHtml.getWorkerPoolStats, 'function');
257
- });
258
- });
259
-
260
- describe('concurrent processing', () => {
261
- it('should handle multiple concurrent requests', async () => {
262
- const htmls = [];
263
- for (let i = 0; i < 10; i++) {
264
- htmls.push({
265
- html: `<p>Request ${i}</p>`
266
- });
267
- }
268
-
269
- const promises = htmls.map((input, i) =>
270
- mimeHtml.async(input).then(result => {
271
- assert.ok(result.includes(`Request ${i}`));
272
- return i;
273
- })
274
- );
275
-
276
- const results = await Promise.all(promises);
277
- assert.equal(results.length, 10);
278
- assert.deepEqual(results, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
279
- });
280
-
281
- it('should handle mixed content types concurrently', async () => {
282
- const inputs = [
283
- { html: '<p>HTML content</p>' },
284
- { text: 'Plain text content' },
285
- { html: '<div>Another HTML</div>' },
286
- { text: 'More text\nWith newlines' }
287
- ];
288
-
289
- const results = await Promise.all(inputs.map(input => mimeHtml.async(input)));
290
-
291
- assert.ok(results[0].includes('HTML content'));
292
- assert.ok(results[1].includes('Plain text content'));
293
- assert.ok(results[2].includes('Another HTML'));
294
- assert.ok(results[3].includes('More text'));
295
- });
296
- });
297
-
298
- // Clean up worker pool after all tests
299
- after(async () => {
300
- await mimeHtml.closeWorkerPool();
301
- });
302
- });
@@ -1,42 +0,0 @@
1
- 'use strict';
2
-
3
- const { describe, it } = require('node:test');
4
- const assert = require('node:assert/strict');
5
- const textToHtml = require('../lib/text-to-html');
6
-
7
- describe('text-to-html additional coverage', () => {
8
- describe('trailing quoted lines', () => {
9
- it('should handle text ending with quoted lines', () => {
10
- const result = textToHtml('Normal line\n> Quoted at end\n> Still quoted');
11
- assert.ok(result.includes('<blockquote'));
12
- assert.ok(result.includes('Quoted at end'));
13
- assert.ok(result.includes('Still quoted'));
14
- assert.ok(result.includes('Normal line'));
15
- });
16
- });
17
-
18
- describe('horizontal rule detection', () => {
19
- it('should convert --- to <hr /> when followed by non-empty line', () => {
20
- const result = textToHtml('Before\n---\nAfter');
21
- assert.ok(result.includes('<hr />'));
22
- assert.ok(result.includes('Before'));
23
- assert.ok(result.includes('After'));
24
- });
25
-
26
- it('should convert --- to <hr /> when it is the last line', () => {
27
- const result = textToHtml('Before\n---');
28
- assert.ok(result.includes('<hr />'));
29
- assert.ok(result.includes('Before'));
30
- });
31
-
32
- it('should convert ___ to <hr />', () => {
33
- const result = textToHtml('Before\n___\nAfter');
34
- assert.ok(result.includes('<hr />'));
35
- });
36
-
37
- it('should convert --- to <hr /> when followed by empty line', () => {
38
- const result = textToHtml('Before\n---\n');
39
- assert.ok(result.includes('<hr />'));
40
- });
41
- });
42
- });
@@ -1,132 +0,0 @@
1
- 'use strict';
2
-
3
- const { describe, it, before, after, afterEach } = require('node:test');
4
- const assert = require('node:assert/strict');
5
- const path = require('path');
6
- const fs = require('fs');
7
- const WorkerPool = require('../lib/worker-pool');
8
-
9
- const crashWorkerPath = path.join(__dirname, 'crash-test-worker.js');
10
- const crashWorkerCode = `
11
- 'use strict';
12
- const { parentPort } = require('worker_threads');
13
- parentPort.on('message', (data) => {
14
- if (data === 'crash') {
15
- setTimeout(() => { throw new Error('intentional crash'); }, 0);
16
- } else if (data === 'slow') {
17
- setTimeout(() => {
18
- parentPort.postMessage({ success: true, result: 'slow: ' + data });
19
- }, 50);
20
- } else {
21
- parentPort.postMessage({ success: true, result: 'ok: ' + data });
22
- }
23
- });
24
- `;
25
-
26
- describe('WorkerPool additional coverage', () => {
27
- let pool;
28
-
29
- before(() => {
30
- fs.writeFileSync(crashWorkerPath, crashWorkerCode);
31
- });
32
-
33
- after(() => {
34
- try {
35
- fs.unlinkSync(crashWorkerPath);
36
- } catch (_err) {
37
- // ignore cleanup errors
38
- }
39
- });
40
-
41
- afterEach(async () => {
42
- if (pool) {
43
- await pool.close();
44
- pool = null;
45
- }
46
- });
47
-
48
- describe('worker error handling', () => {
49
- it('should replace a crashed worker and remain functional', async () => {
50
- pool = new WorkerPool({
51
- workerPath: crashWorkerPath,
52
- minWorkers: 2,
53
- maxWorkers: 3
54
- });
55
-
56
- // Verify initial state
57
- let stats = pool.getStats();
58
- assert.equal(stats.totalWorkers, 2);
59
-
60
- // Trigger a crash
61
- try {
62
- await pool.process('crash');
63
- assert.fail('Should have rejected');
64
- } catch (err) {
65
- assert.ok(err);
66
- }
67
-
68
- // Wait for replacement worker to be created
69
- await new Promise(r => setTimeout(r, 300));
70
-
71
- // Pool should have replaced the crashed worker
72
- stats = pool.getStats();
73
- assert.ok(stats.totalWorkers >= 2, `Expected >= 2 workers but got ${stats.totalWorkers}`);
74
-
75
- // Pool should still be functional
76
- const result = await pool.process('hello');
77
- assert.equal(result, 'ok: hello');
78
- });
79
- });
80
-
81
- describe('idle timeout', () => {
82
- it('should terminate idle worker above minimum via _setupIdleTimeout', async () => {
83
- pool = new WorkerPool({
84
- workerPath: crashWorkerPath,
85
- minWorkers: 1,
86
- maxWorkers: 3,
87
- idleTimeout: 200
88
- });
89
-
90
- assert.equal(pool.getStats().totalWorkers, 1);
91
-
92
- // Manually create an extra worker above minWorkers (without a task).
93
- // This triggers _setupIdleTimeout in _createWorker.
94
- pool._createWorker();
95
-
96
- assert.equal(pool.getStats().totalWorkers, 2);
97
-
98
- // Wait for idle timeout to fire and remove the extra worker
99
- await new Promise(r => setTimeout(r, 400));
100
-
101
- assert.equal(pool.getStats().totalWorkers, 1);
102
- });
103
- });
104
-
105
- describe('close with active idle timeouts', () => {
106
- it('should close cleanly when workers have pending idle timeouts', async () => {
107
- pool = new WorkerPool({
108
- workerPath: crashWorkerPath,
109
- minWorkers: 1,
110
- maxWorkers: 3,
111
- idleTimeout: 10000
112
- });
113
-
114
- // Scale up
115
- const promises = [];
116
- for (let i = 0; i < 3; i++) {
117
- promises.push(pool.process(`task-${i}`));
118
- }
119
- await Promise.all(promises);
120
-
121
- // Workers above min should have idle timeouts set
122
- let stats = pool.getStats();
123
- assert.ok(stats.totalWorkers > 1);
124
-
125
- // Close immediately before idle timeout fires
126
- await pool.close();
127
- pool = null;
128
-
129
- // Should not throw or leak timers
130
- });
131
- });
132
- });
@@ -1,128 +0,0 @@
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
- });