@postalsys/email-text-tools 2.1.16 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -32
- 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 +53 -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 -7
- 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,282 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { describe, it, before, after, beforeEach, afterEach } = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const WorkerPool = require('../lib/worker-pool');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
// Create a simple test worker
|
|
9
|
+
const testWorkerCode = `
|
|
10
|
+
const { parentPort } = require('worker_threads');
|
|
11
|
+
|
|
12
|
+
parentPort.on('message', (data) => {
|
|
13
|
+
// Simulate some processing
|
|
14
|
+
setTimeout(() => {
|
|
15
|
+
if (data === 'error') {
|
|
16
|
+
parentPort.postMessage({ success: false, error: 'Test error' });
|
|
17
|
+
} else if (data === 'slow') {
|
|
18
|
+
// Simulate slow processing
|
|
19
|
+
setTimeout(() => {
|
|
20
|
+
parentPort.postMessage({ success: true, result: 'slow result' });
|
|
21
|
+
}, 1000);
|
|
22
|
+
} else {
|
|
23
|
+
parentPort.postMessage({ success: true, result: 'processed: ' + data });
|
|
24
|
+
}
|
|
25
|
+
}, 10);
|
|
26
|
+
});
|
|
27
|
+
`;
|
|
28
|
+
|
|
29
|
+
describe('WorkerPool', () => {
|
|
30
|
+
let pool;
|
|
31
|
+
|
|
32
|
+
before(async () => {
|
|
33
|
+
// Create test worker file
|
|
34
|
+
const fs = require('fs');
|
|
35
|
+
const workerPath = path.join(__dirname, 'test-worker.js');
|
|
36
|
+
fs.writeFileSync(workerPath, testWorkerCode);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
after(async () => {
|
|
40
|
+
// Clean up test worker file
|
|
41
|
+
const fs = require('fs');
|
|
42
|
+
const workerPath = path.join(__dirname, 'test-worker.js');
|
|
43
|
+
try {
|
|
44
|
+
fs.unlinkSync(workerPath);
|
|
45
|
+
} catch (_err) {
|
|
46
|
+
// Ignore cleanup errors
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
afterEach(async () => {
|
|
51
|
+
// Close pool after each test
|
|
52
|
+
if (pool) {
|
|
53
|
+
await pool.close();
|
|
54
|
+
pool = null;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('initialization', () => {
|
|
59
|
+
it('should create pool with default options', () => {
|
|
60
|
+
pool = new WorkerPool({
|
|
61
|
+
workerPath: path.join(__dirname, 'test-worker.js')
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const stats = pool.getStats();
|
|
65
|
+
assert.equal(stats.minWorkers, 2);
|
|
66
|
+
assert.equal(stats.maxWorkers, 4);
|
|
67
|
+
assert.equal(stats.totalWorkers, 2);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should create pool with custom options', () => {
|
|
71
|
+
pool = new WorkerPool({
|
|
72
|
+
workerPath: path.join(__dirname, 'test-worker.js'),
|
|
73
|
+
minWorkers: 1,
|
|
74
|
+
maxWorkers: 3,
|
|
75
|
+
workerTimeout: 500,
|
|
76
|
+
maxQueueSize: 50
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const stats = pool.getStats();
|
|
80
|
+
assert.equal(stats.minWorkers, 1);
|
|
81
|
+
assert.equal(stats.maxWorkers, 3);
|
|
82
|
+
assert.equal(stats.totalWorkers, 1);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('task processing', () => {
|
|
87
|
+
beforeEach(() => {
|
|
88
|
+
pool = new WorkerPool({
|
|
89
|
+
workerPath: path.join(__dirname, 'test-worker.js'),
|
|
90
|
+
minWorkers: 2,
|
|
91
|
+
maxWorkers: 4
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should process a single task', async () => {
|
|
96
|
+
const result = await pool.process('test data');
|
|
97
|
+
assert.equal(result, 'processed: test data');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should process multiple tasks sequentially', async () => {
|
|
101
|
+
const result1 = await pool.process('task 1');
|
|
102
|
+
const result2 = await pool.process('task 2');
|
|
103
|
+
const result3 = await pool.process('task 3');
|
|
104
|
+
|
|
105
|
+
assert.equal(result1, 'processed: task 1');
|
|
106
|
+
assert.equal(result2, 'processed: task 2');
|
|
107
|
+
assert.equal(result3, 'processed: task 3');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should process multiple tasks concurrently', async () => {
|
|
111
|
+
const promises = [];
|
|
112
|
+
for (let i = 0; i < 10; i++) {
|
|
113
|
+
promises.push(pool.process(`task ${i}`));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const results = await Promise.all(promises);
|
|
117
|
+
|
|
118
|
+
assert.equal(results.length, 10);
|
|
119
|
+
results.forEach((result, i) => {
|
|
120
|
+
assert.equal(result, `processed: task ${i}`);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should handle worker errors', async () => {
|
|
125
|
+
try {
|
|
126
|
+
await pool.process('error');
|
|
127
|
+
assert.fail('Should have thrown an error');
|
|
128
|
+
} catch (err) {
|
|
129
|
+
assert.equal(err.message, 'Test error');
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should timeout long-running tasks', async () => {
|
|
134
|
+
try {
|
|
135
|
+
await pool.process('slow', { timeout: 100 });
|
|
136
|
+
assert.fail('Should have timed out');
|
|
137
|
+
} catch (err) {
|
|
138
|
+
assert.ok(err.message.includes('timed out'));
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe('worker management', () => {
|
|
144
|
+
beforeEach(() => {
|
|
145
|
+
pool = new WorkerPool({
|
|
146
|
+
workerPath: path.join(__dirname, 'test-worker.js'),
|
|
147
|
+
minWorkers: 2,
|
|
148
|
+
maxWorkers: 4
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('should scale up workers under load', async () => {
|
|
153
|
+
const initialStats = pool.getStats();
|
|
154
|
+
assert.equal(initialStats.totalWorkers, 2);
|
|
155
|
+
|
|
156
|
+
// Create enough concurrent tasks to trigger scaling
|
|
157
|
+
const promises = [];
|
|
158
|
+
for (let i = 0; i < 6; i++) {
|
|
159
|
+
promises.push(pool.process(`task ${i}`));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Check stats while processing
|
|
163
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
164
|
+
const midStats = pool.getStats();
|
|
165
|
+
assert.ok(midStats.totalWorkers > 2);
|
|
166
|
+
assert.ok(midStats.totalWorkers <= 4);
|
|
167
|
+
|
|
168
|
+
await Promise.all(promises);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should reuse workers efficiently', async () => {
|
|
172
|
+
const stats1 = pool.getStats();
|
|
173
|
+
const _initialWorkers = stats1.totalWorkers;
|
|
174
|
+
|
|
175
|
+
// Process tasks that complete quickly
|
|
176
|
+
const promises = [];
|
|
177
|
+
for (let i = 0; i < 20; i++) {
|
|
178
|
+
promises.push(pool.process(`quick ${i}`));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
await Promise.all(promises);
|
|
182
|
+
|
|
183
|
+
const stats2 = pool.getStats();
|
|
184
|
+
// Should not have created too many workers
|
|
185
|
+
assert.ok(stats2.totalWorkers <= 4);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should handle queue when at max workers', async () => {
|
|
189
|
+
// Fill up all workers with slow tasks
|
|
190
|
+
const slowPromises = [];
|
|
191
|
+
for (let i = 0; i < 4; i++) {
|
|
192
|
+
slowPromises.push(pool.process('slow'));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// These should queue
|
|
196
|
+
const queuedPromises = [];
|
|
197
|
+
for (let i = 0; i < 3; i++) {
|
|
198
|
+
queuedPromises.push(pool.process(`queued ${i}`));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Check that tasks are queued
|
|
202
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
203
|
+
const stats = pool.getStats();
|
|
204
|
+
assert.ok(stats.queuedTasks > 0);
|
|
205
|
+
|
|
206
|
+
// Wait for all to complete
|
|
207
|
+
await Promise.all([...slowPromises, ...queuedPromises]);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe('pool lifecycle', () => {
|
|
212
|
+
it('should close pool gracefully', async () => {
|
|
213
|
+
pool = new WorkerPool({
|
|
214
|
+
workerPath: path.join(__dirname, 'test-worker.js')
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Process some tasks
|
|
218
|
+
await pool.process('test 1');
|
|
219
|
+
await pool.process('test 2');
|
|
220
|
+
|
|
221
|
+
// Close pool
|
|
222
|
+
await pool.close();
|
|
223
|
+
|
|
224
|
+
// Should not be able to process after closing
|
|
225
|
+
try {
|
|
226
|
+
await pool.process('after close');
|
|
227
|
+
assert.fail('Should not process after closing');
|
|
228
|
+
} catch (err) {
|
|
229
|
+
assert.ok(err.message.includes('closing'));
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('should handle closing with pending tasks', async () => {
|
|
234
|
+
pool = new WorkerPool({
|
|
235
|
+
workerPath: path.join(__dirname, 'test-worker.js')
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// Start some tasks but don't wait
|
|
239
|
+
const promises = [];
|
|
240
|
+
for (let i = 0; i < 5; i++) {
|
|
241
|
+
promises.push(pool.process(`task ${i}`).catch(() => {}));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Close immediately
|
|
245
|
+
await pool.close();
|
|
246
|
+
|
|
247
|
+
// Some tasks might fail, but close should complete
|
|
248
|
+
assert.ok(true, 'Pool closed successfully');
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
describe('statistics', () => {
|
|
253
|
+
it('should provide accurate statistics', async () => {
|
|
254
|
+
pool = new WorkerPool({
|
|
255
|
+
workerPath: path.join(__dirname, 'test-worker.js'),
|
|
256
|
+
minWorkers: 2,
|
|
257
|
+
maxWorkers: 4
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const stats1 = pool.getStats();
|
|
261
|
+
assert.equal(stats1.totalWorkers, 2);
|
|
262
|
+
assert.equal(stats1.busyWorkers, 0);
|
|
263
|
+
assert.equal(stats1.freeWorkers, 2);
|
|
264
|
+
assert.equal(stats1.queuedTasks, 0);
|
|
265
|
+
|
|
266
|
+
// Start processing
|
|
267
|
+
const promise = pool.process('test');
|
|
268
|
+
|
|
269
|
+
// Check stats while processing
|
|
270
|
+
await new Promise(resolve => setTimeout(resolve, 5));
|
|
271
|
+
const stats2 = pool.getStats();
|
|
272
|
+
assert.ok(stats2.busyWorkers > 0);
|
|
273
|
+
|
|
274
|
+
await promise;
|
|
275
|
+
|
|
276
|
+
// Check stats after completion
|
|
277
|
+
const stats3 = pool.getStats();
|
|
278
|
+
assert.equal(stats3.busyWorkers, 0);
|
|
279
|
+
assert.equal(stats3.freeWorkers, stats3.totalWorkers);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
});
|