@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.
- package/CHANGELOG.md +14 -0
- package/README.md +12 -0
- package/dist/cjs/index.d.ts +20 -0
- package/dist/cjs/index.js +31 -0
- package/dist/cjs/lib/commonjs.cjs +17 -0
- package/dist/cjs/lib/commonjs.d.cts +4 -0
- package/dist/cjs/lib/format-date.d.ts +15 -0
- package/dist/cjs/lib/format-date.js +60 -0
- package/dist/cjs/lib/html-guard.d.ts +3 -0
- package/dist/cjs/lib/html-guard.js +119 -0
- package/dist/cjs/lib/html-to-text.d.ts +1 -0
- package/dist/cjs/lib/html-to-text.js +49 -0
- package/dist/cjs/lib/inline-html.d.ts +19 -0
- package/dist/cjs/lib/inline-html.js +150 -0
- package/dist/cjs/lib/inline-style.d.ts +2 -0
- package/dist/cjs/lib/inline-style.js +42 -0
- package/dist/cjs/lib/inline-text.d.ts +3 -0
- package/dist/cjs/lib/inline-text.js +111 -0
- package/dist/cjs/lib/juice-worker.d.ts +1 -0
- package/dist/cjs/lib/juice-worker.js +19 -0
- package/dist/cjs/lib/mime-html.d.ts +34 -0
- package/dist/cjs/lib/mime-html.js +496 -0
- package/dist/cjs/lib/text-to-html.d.ts +1 -0
- package/dist/cjs/lib/text-to-html.js +182 -0
- package/dist/cjs/lib/worker-pool.d.ts +61 -0
- package/dist/cjs/lib/worker-pool.js +229 -0
- package/dist/cjs/package.json +3 -0
- package/dist/esm/index.d.ts +20 -0
- package/dist/esm/index.js +13 -0
- package/dist/esm/lib/commonjs.cjs +17 -0
- package/dist/esm/lib/commonjs.d.cts +4 -0
- package/dist/esm/lib/format-date.d.ts +15 -0
- package/{lib → dist/esm/lib}/format-date.js +8 -16
- package/dist/esm/lib/html-guard.d.ts +3 -0
- package/{lib → dist/esm/lib}/html-guard.js +8 -24
- package/dist/esm/lib/html-to-text.d.ts +1 -0
- package/{lib → dist/esm/lib}/html-to-text.js +2 -14
- package/dist/esm/lib/inline-html.d.ts +19 -0
- package/{lib → dist/esm/lib}/inline-html.js +17 -55
- package/dist/esm/lib/inline-style.d.ts +2 -0
- package/dist/esm/lib/inline-style.js +39 -0
- package/dist/esm/lib/inline-text.d.ts +3 -0
- package/{lib → dist/esm/lib}/inline-text.js +8 -31
- package/dist/esm/lib/juice-worker.d.ts +1 -0
- package/{lib → dist/esm/lib}/juice-worker.js +7 -8
- package/dist/esm/lib/mime-html.d.ts +34 -0
- package/{lib → dist/esm/lib}/mime-html.js +80 -161
- package/dist/esm/lib/text-to-html.d.ts +1 -0
- package/{lib → dist/esm/lib}/text-to-html.js +35 -63
- package/dist/esm/lib/worker-pool.d.ts +61 -0
- package/{lib → dist/esm/lib}/worker-pool.js +18 -64
- package/dist/esm/package.json +3 -0
- package/package.json +67 -10
- package/.github/VULNERABILITY_REPORT.yml +0 -75
- package/.github/workflows/release.yaml +0 -38
- package/.github/workflows/test.yaml +0 -35
- package/.ncurc.js +0 -28
- package/.prettierignore +0 -14
- package/.prettierrc.js +0 -10
- package/.release-please-manifest.json +0 -3
- package/CLAUDE.md +0 -105
- package/benchmark/benchmark.js +0 -348
- package/benchmark/concurrency-benchmark.js +0 -176
- package/benchmark/quick-benchmark.js +0 -200
- package/benchmark/quick-concurrency.js +0 -64
- package/benchmark/real-world-benchmark.js +0 -257
- package/docs/PERFORMANCE.md +0 -262
- package/eslint.config.js +0 -71
- package/examples/failing.html +0 -617
- package/examples/fixtures/message-1.json +0 -74
- package/examples/inline-html.js +0 -24
- package/examples/inline-text.js +0 -24
- package/examples/mime-html.js +0 -284
- package/index.js +0 -22
- package/release-please-config.json +0 -10
- package/test/comprehensive.test.js +0 -358
- package/test/containment.test.js +0 -128
- package/test/edge-cases.test.js +0 -62
- package/test/hardening.test.js +0 -504
- package/test/html-to-text.test.js +0 -253
- package/test/inline-html.test.js +0 -230
- package/test/inline-text.test.js +0 -204
- package/test/mime-html-extra.test.js +0 -39
- package/test/mime-html.test.js +0 -302
- package/test/text-to-html-extra.test.js +0 -42
- package/test/worker-pool-extra.test.js +0 -132
- package/test/worker-pool-regression.test.js +0 -128
- 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
|
-
});
|
package/test/mime-html.test.js
DELETED
|
@@ -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><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
|
-
// 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
|
-
});
|