@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.
- package/CHANGELOG.md +7 -0
- package/README.md +10 -0
- package/dist/cjs/index.d.ts +18 -0
- package/dist/cjs/index.js +28 -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-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 +529 -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 +18 -0
- package/dist/esm/index.js +12 -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-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 +79 -127
- 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,28 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const linkify = linkifyIt()
|
|
8
|
-
.tlds(tlds) // Reload with full tlds list
|
|
1
|
+
import LinkifyIt from 'linkify-it';
|
|
2
|
+
import he from 'he';
|
|
3
|
+
import { TLDS } from './commonjs.cjs';
|
|
4
|
+
const linkify = new LinkifyIt()
|
|
5
|
+
.tlds(TLDS) // Reload with full tlds list
|
|
9
6
|
.tlds('onion', true) // Add unofficial `.onion` domain
|
|
10
7
|
.add('git:', 'http:') // Add `git:` protocol as "alias"
|
|
11
8
|
.set({ fuzzyIP: true });
|
|
12
|
-
|
|
13
9
|
// Quote levels deeper than this are kept as literal "> " text. Every level costs a few stack
|
|
14
10
|
// frames here and a nested <blockquote> later, so hostile input with thousands of leading ">"
|
|
15
11
|
// used to overflow the stack.
|
|
16
12
|
const MAX_QUOTE_DEPTH = 100;
|
|
17
|
-
|
|
18
13
|
function generateQuotedTree(text) {
|
|
19
14
|
let lines = text.split(/\r?\n/);
|
|
20
|
-
|
|
21
15
|
let tree = {
|
|
22
16
|
type: 'text',
|
|
23
17
|
children: []
|
|
24
18
|
};
|
|
25
|
-
|
|
26
19
|
let createNode = (parent, type) => {
|
|
27
20
|
let node = {
|
|
28
21
|
parent,
|
|
@@ -33,7 +26,6 @@ function generateQuotedTree(text) {
|
|
|
33
26
|
parent.children.push(node);
|
|
34
27
|
return node;
|
|
35
28
|
};
|
|
36
|
-
|
|
37
29
|
let walkNode = (curNode, lines, depth) => {
|
|
38
30
|
for (let line of lines) {
|
|
39
31
|
if (depth < MAX_QUOTE_DEPTH && /^>/.test(line)) {
|
|
@@ -41,41 +33,34 @@ function generateQuotedTree(text) {
|
|
|
41
33
|
curNode = createNode(curNode.parent, 'quote');
|
|
42
34
|
}
|
|
43
35
|
curNode.lines.push(line.replace(/^> ?/, ''));
|
|
44
|
-
}
|
|
36
|
+
}
|
|
37
|
+
else if (curNode.type === 'quote') {
|
|
45
38
|
// process child
|
|
46
|
-
|
|
47
39
|
let quoteNode = curNode;
|
|
48
|
-
|
|
49
40
|
let childNode = createNode(curNode, 'text');
|
|
50
41
|
walkNode(childNode, quoteNode.lines, depth + 1);
|
|
51
|
-
|
|
52
42
|
curNode = createNode(curNode.parent, 'text');
|
|
53
43
|
curNode.lines.push(line);
|
|
54
|
-
}
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
55
46
|
curNode.lines.push(line);
|
|
56
47
|
}
|
|
57
48
|
}
|
|
58
|
-
|
|
59
49
|
if (curNode.type === 'quote') {
|
|
60
50
|
// process child
|
|
61
|
-
|
|
62
51
|
let quoteNode = curNode;
|
|
63
|
-
|
|
64
52
|
let childNode = createNode(curNode, 'text');
|
|
65
53
|
walkNode(childNode, quoteNode.lines, depth + 1);
|
|
66
54
|
}
|
|
67
55
|
};
|
|
68
|
-
|
|
69
56
|
let rootNode = createNode(tree, 'text');
|
|
70
57
|
walkNode(rootNode, lines, 0);
|
|
71
|
-
|
|
72
58
|
return tree;
|
|
73
59
|
}
|
|
74
|
-
|
|
75
60
|
/**
|
|
76
61
|
* Return HTML escaped text with clickable links
|
|
77
|
-
* @param
|
|
78
|
-
* @returns
|
|
62
|
+
* @param text Text content to HTML escape
|
|
63
|
+
* @returns HTML escaped content
|
|
79
64
|
*/
|
|
80
65
|
function linkifyAndHtmlEscape(text) {
|
|
81
66
|
try {
|
|
@@ -94,56 +79,55 @@ function linkifyAndHtmlEscape(text) {
|
|
|
94
79
|
parts.push(Object.assign({ type: 'link' }, link));
|
|
95
80
|
cursor = link.lastIndex;
|
|
96
81
|
}
|
|
97
|
-
|
|
98
82
|
if (cursor < text.length) {
|
|
99
83
|
parts.push({
|
|
100
84
|
type: 'text',
|
|
101
|
-
content: text.
|
|
85
|
+
content: text.slice(cursor)
|
|
102
86
|
});
|
|
103
87
|
}
|
|
104
|
-
|
|
105
88
|
return parts
|
|
106
89
|
.map(part => {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
case 'link':
|
|
113
|
-
// URL with html escaped text content and URL
|
|
114
|
-
return `<a href="${he.encode(part.url, { useNamedReferences: true })}">${he.encode(part.text, {
|
|
115
|
-
useNamedReferences: true
|
|
116
|
-
})}</a>`;
|
|
90
|
+
switch (part.type) {
|
|
91
|
+
case 'text': {
|
|
92
|
+
// normal text, escape HTML
|
|
93
|
+
return he.encode(part.content, { useNamedReferences: true });
|
|
117
94
|
}
|
|
118
|
-
|
|
119
|
-
|
|
95
|
+
case 'link':
|
|
96
|
+
// URL with html escaped text content and URL
|
|
97
|
+
return `<a href="${he.encode(part.url, { useNamedReferences: true })}">${he.encode(part.text, {
|
|
98
|
+
useNamedReferences: true
|
|
99
|
+
})}</a>`;
|
|
100
|
+
}
|
|
101
|
+
return '';
|
|
102
|
+
})
|
|
120
103
|
.join('');
|
|
121
104
|
}
|
|
122
|
-
}
|
|
105
|
+
}
|
|
106
|
+
catch (_err) {
|
|
123
107
|
// ignore?
|
|
124
108
|
}
|
|
125
|
-
|
|
126
109
|
// No links or exception, so HTML escape everything
|
|
127
110
|
return he.encode(text, { useNamedReferences: true });
|
|
128
111
|
}
|
|
129
|
-
|
|
130
112
|
function textLinesToHtml(lines) {
|
|
131
113
|
let textparts = ['<p>'];
|
|
132
114
|
for (let i = 0; i < lines.length; i++) {
|
|
133
115
|
let line = lines[i];
|
|
134
|
-
|
|
135
116
|
if (!line.trim()) {
|
|
136
117
|
if (i && lines[i - 1].trim()) {
|
|
137
118
|
textparts.push('</p><p>');
|
|
138
119
|
}
|
|
139
|
-
}
|
|
120
|
+
}
|
|
121
|
+
else if (i < lines.length - 1 && lines[i + 1].trim()) {
|
|
140
122
|
let escaped = linkifyAndHtmlEscape(line);
|
|
141
123
|
if (/^[ \t]*[-_]{2,}[ \t]*$/.test(escaped)) {
|
|
142
124
|
textparts.push(`<hr />`);
|
|
143
|
-
}
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
144
127
|
textparts.push(`${escaped}<br />`);
|
|
145
128
|
}
|
|
146
|
-
}
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
147
131
|
let escaped = linkifyAndHtmlEscape(line);
|
|
148
132
|
if (/^[ \t]*[-_]{2,}[ \t]*$/.test(escaped)) {
|
|
149
133
|
escaped = `<hr />`;
|
|
@@ -151,19 +135,15 @@ function textLinesToHtml(lines) {
|
|
|
151
135
|
textparts.push(escaped);
|
|
152
136
|
}
|
|
153
137
|
}
|
|
154
|
-
|
|
155
138
|
return textparts.join('\n') + '</p>';
|
|
156
139
|
}
|
|
157
|
-
|
|
158
|
-
function textToHtml(text) {
|
|
140
|
+
export default function textToHtml(text) {
|
|
159
141
|
text = (text || '').toString();
|
|
160
142
|
let tree = generateQuotedTree(text);
|
|
161
|
-
|
|
162
143
|
let blockQuoteCounter = 0;
|
|
163
144
|
function processQuotedNode(curNode, level) {
|
|
164
145
|
let entries = [];
|
|
165
146
|
level = (level || 0) + 1;
|
|
166
|
-
|
|
167
147
|
if (curNode.type === 'text') {
|
|
168
148
|
for (let child of curNode.children) {
|
|
169
149
|
let content = processQuotedNode(child);
|
|
@@ -171,32 +151,24 @@ function textToHtml(text) {
|
|
|
171
151
|
entries.push(content);
|
|
172
152
|
}
|
|
173
153
|
}
|
|
174
|
-
|
|
175
154
|
if (curNode.lines && curNode.lines.length) {
|
|
176
155
|
entries.push(textLinesToHtml(curNode.lines));
|
|
177
156
|
}
|
|
178
157
|
}
|
|
179
|
-
|
|
180
158
|
if (curNode.type === 'quote') {
|
|
181
159
|
for (let child of curNode.children) {
|
|
182
160
|
let content = processQuotedNode(child, level);
|
|
183
161
|
if (typeof content === 'string') {
|
|
184
162
|
let id = ++blockQuoteCounter;
|
|
185
|
-
entries.push(
|
|
186
|
-
`<blockquote type="cite" class="ee-block-${level}"><!-- blocquote ${id} start-->${content}</blockquote><!-- blocquote ${id} end-->`
|
|
187
|
-
);
|
|
163
|
+
entries.push(`<blockquote type="cite" class="ee-block-${level}"><!-- blocquote ${id} start-->${content}</blockquote><!-- blocquote ${id} end-->`);
|
|
188
164
|
}
|
|
189
165
|
}
|
|
190
166
|
}
|
|
191
|
-
|
|
192
167
|
if (entries.length) {
|
|
193
168
|
return entries.join('\n');
|
|
194
169
|
}
|
|
170
|
+
return undefined;
|
|
195
171
|
}
|
|
196
|
-
|
|
197
172
|
let generatedHtml = processQuotedNode(tree);
|
|
198
|
-
|
|
199
173
|
return generatedHtml;
|
|
200
174
|
}
|
|
201
|
-
|
|
202
|
-
module.exports = textToHtml;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { Worker } from 'node:worker_threads';
|
|
3
|
+
export interface WorkerPoolOptions {
|
|
4
|
+
/** Worker script, defaults to the juice worker */
|
|
5
|
+
workerPath?: string | URL | undefined;
|
|
6
|
+
minWorkers?: number | undefined;
|
|
7
|
+
maxWorkers?: number | undefined;
|
|
8
|
+
maxQueueSize?: number | undefined;
|
|
9
|
+
/** Default task timeout in milliseconds */
|
|
10
|
+
workerTimeout?: number | undefined;
|
|
11
|
+
/** How long a worker above the minimum may stay idle, in milliseconds */
|
|
12
|
+
idleTimeout?: number | undefined;
|
|
13
|
+
}
|
|
14
|
+
export interface WorkerPoolStats {
|
|
15
|
+
totalWorkers: number;
|
|
16
|
+
busyWorkers: number;
|
|
17
|
+
freeWorkers: number;
|
|
18
|
+
queuedTasks: number;
|
|
19
|
+
minWorkers: number;
|
|
20
|
+
maxWorkers: number;
|
|
21
|
+
}
|
|
22
|
+
interface Task {
|
|
23
|
+
data: unknown;
|
|
24
|
+
resolve: (result: any) => void;
|
|
25
|
+
reject: (err: Error) => void;
|
|
26
|
+
timeout: number;
|
|
27
|
+
}
|
|
28
|
+
interface WorkerInfo {
|
|
29
|
+
worker: Worker;
|
|
30
|
+
busy: boolean;
|
|
31
|
+
lastUsed: number;
|
|
32
|
+
timeoutHandle: NodeJS.Timeout | null;
|
|
33
|
+
currentTask: (Task & {
|
|
34
|
+
timeoutHandle: NodeJS.Timeout;
|
|
35
|
+
}) | null;
|
|
36
|
+
}
|
|
37
|
+
export default class WorkerPool extends EventEmitter {
|
|
38
|
+
workerPath: string | URL;
|
|
39
|
+
minWorkers: number;
|
|
40
|
+
maxWorkers: number;
|
|
41
|
+
maxQueueSize: number;
|
|
42
|
+
workerTimeout: number;
|
|
43
|
+
idleTimeout: number;
|
|
44
|
+
workers: WorkerInfo[];
|
|
45
|
+
freeWorkers: WorkerInfo[];
|
|
46
|
+
queue: Task[];
|
|
47
|
+
closing: boolean;
|
|
48
|
+
constructor(options?: WorkerPoolOptions);
|
|
49
|
+
_initWorkers(): void;
|
|
50
|
+
_createWorker(): WorkerInfo | null;
|
|
51
|
+
_setupIdleTimeout(workerInfo: WorkerInfo): void;
|
|
52
|
+
_removeWorker(workerInfo: WorkerInfo): void;
|
|
53
|
+
process(data: unknown, options?: {
|
|
54
|
+
timeout?: number | undefined;
|
|
55
|
+
}): Promise<any>;
|
|
56
|
+
_executeTask(workerInfo: WorkerInfo, task: Task): void;
|
|
57
|
+
_processQueue(): void;
|
|
58
|
+
close(): Promise<void>;
|
|
59
|
+
getStats(): WorkerPoolStats;
|
|
60
|
+
}
|
|
61
|
+
export {};
|
|
@@ -1,38 +1,30 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const { EventEmitter } = require('events');
|
|
6
|
-
|
|
7
|
-
class WorkerPool extends EventEmitter {
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { Worker } from 'node:worker_threads';
|
|
3
|
+
import { JUICE_WORKER_PATH } from './commonjs.cjs';
|
|
4
|
+
export default class WorkerPool extends EventEmitter {
|
|
8
5
|
constructor(options = {}) {
|
|
9
6
|
super();
|
|
10
|
-
|
|
11
|
-
this.workerPath = options.workerPath || path.join(__dirname, 'juice-worker.js');
|
|
7
|
+
this.workerPath = options.workerPath || JUICE_WORKER_PATH;
|
|
12
8
|
this.minWorkers = options.minWorkers || 2;
|
|
13
9
|
this.maxWorkers = options.maxWorkers || 4;
|
|
14
10
|
this.maxQueueSize = options.maxQueueSize || 100;
|
|
15
11
|
this.workerTimeout = options.workerTimeout || 10000;
|
|
16
12
|
this.idleTimeout = options.idleTimeout || 30000;
|
|
17
|
-
|
|
18
13
|
this.workers = [];
|
|
19
14
|
this.freeWorkers = [];
|
|
20
15
|
this.queue = [];
|
|
21
16
|
this.closing = false;
|
|
22
|
-
|
|
23
17
|
// Initialize minimum number of workers
|
|
24
18
|
this._initWorkers();
|
|
25
19
|
}
|
|
26
|
-
|
|
27
20
|
_initWorkers() {
|
|
28
21
|
for (let i = 0; i < this.minWorkers; i++) {
|
|
29
22
|
this._createWorker();
|
|
30
23
|
}
|
|
31
24
|
}
|
|
32
|
-
|
|
33
25
|
_createWorker() {
|
|
34
|
-
if (this.closing)
|
|
35
|
-
|
|
26
|
+
if (this.closing)
|
|
27
|
+
return null;
|
|
36
28
|
const worker = new Worker(this.workerPath);
|
|
37
29
|
const workerInfo = {
|
|
38
30
|
worker,
|
|
@@ -41,174 +33,145 @@ class WorkerPool extends EventEmitter {
|
|
|
41
33
|
timeoutHandle: null,
|
|
42
34
|
currentTask: null
|
|
43
35
|
};
|
|
44
|
-
|
|
45
|
-
worker.on('message', msg => {
|
|
36
|
+
worker.on('message', (msg) => {
|
|
46
37
|
if (workerInfo.currentTask) {
|
|
47
38
|
const { resolve, reject, timeoutHandle } = workerInfo.currentTask;
|
|
48
39
|
clearTimeout(timeoutHandle);
|
|
49
|
-
|
|
50
40
|
if (msg.success) {
|
|
51
41
|
resolve(msg.result);
|
|
52
|
-
}
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
53
44
|
reject(new Error(msg.error));
|
|
54
45
|
}
|
|
55
|
-
|
|
56
46
|
workerInfo.currentTask = null;
|
|
57
47
|
workerInfo.busy = false;
|
|
58
48
|
workerInfo.lastUsed = Date.now();
|
|
59
|
-
|
|
60
49
|
// Add worker back to free list if not already there
|
|
61
50
|
if (!this.freeWorkers.includes(workerInfo)) {
|
|
62
51
|
this.freeWorkers.push(workerInfo);
|
|
63
52
|
}
|
|
64
|
-
|
|
65
53
|
// Process next item in queue
|
|
66
54
|
this._processQueue();
|
|
67
55
|
}
|
|
68
56
|
});
|
|
69
|
-
|
|
70
|
-
worker.on('error', err => {
|
|
57
|
+
worker.on('error', (err) => {
|
|
71
58
|
if (workerInfo.currentTask) {
|
|
72
59
|
const { reject, timeoutHandle } = workerInfo.currentTask;
|
|
73
60
|
clearTimeout(timeoutHandle);
|
|
74
61
|
reject(err);
|
|
75
62
|
workerInfo.currentTask = null;
|
|
76
63
|
}
|
|
77
|
-
|
|
78
64
|
// Remove failed worker
|
|
79
65
|
const index = this.workers.indexOf(workerInfo);
|
|
80
66
|
if (index > -1) {
|
|
81
67
|
this.workers.splice(index, 1);
|
|
82
68
|
}
|
|
83
|
-
|
|
84
69
|
const freeIndex = this.freeWorkers.indexOf(workerInfo);
|
|
85
70
|
if (freeIndex > -1) {
|
|
86
71
|
this.freeWorkers.splice(freeIndex, 1);
|
|
87
72
|
}
|
|
88
|
-
|
|
89
73
|
// Create replacement if below minimum
|
|
90
74
|
if (this.workers.length < this.minWorkers && !this.closing) {
|
|
91
75
|
this._createWorker();
|
|
92
76
|
}
|
|
93
|
-
|
|
94
77
|
// Process queue with remaining workers
|
|
95
78
|
this._processQueue();
|
|
96
79
|
});
|
|
97
|
-
|
|
98
80
|
worker.on('exit', () => {
|
|
99
81
|
// Clean up worker references
|
|
100
82
|
const index = this.workers.indexOf(workerInfo);
|
|
101
83
|
if (index > -1) {
|
|
102
84
|
this.workers.splice(index, 1);
|
|
103
85
|
}
|
|
104
|
-
|
|
105
86
|
const freeIndex = this.freeWorkers.indexOf(workerInfo);
|
|
106
87
|
if (freeIndex > -1) {
|
|
107
88
|
this.freeWorkers.splice(freeIndex, 1);
|
|
108
89
|
}
|
|
109
90
|
});
|
|
110
|
-
|
|
111
91
|
this.workers.push(workerInfo);
|
|
112
92
|
this.freeWorkers.push(workerInfo);
|
|
113
|
-
|
|
114
93
|
// Set up idle timeout for workers above minimum
|
|
115
94
|
if (this.workers.length > this.minWorkers) {
|
|
116
95
|
this._setupIdleTimeout(workerInfo);
|
|
117
96
|
}
|
|
118
|
-
|
|
119
97
|
return workerInfo;
|
|
120
98
|
}
|
|
121
|
-
|
|
122
99
|
_setupIdleTimeout(workerInfo) {
|
|
123
100
|
if (workerInfo.timeoutHandle) {
|
|
124
101
|
clearTimeout(workerInfo.timeoutHandle);
|
|
125
102
|
}
|
|
126
|
-
|
|
127
103
|
workerInfo.timeoutHandle = setTimeout(() => {
|
|
128
104
|
if (!workerInfo.busy && this.workers.length > this.minWorkers) {
|
|
129
105
|
this._removeWorker(workerInfo);
|
|
130
106
|
}
|
|
131
107
|
}, this.idleTimeout);
|
|
132
108
|
}
|
|
133
|
-
|
|
134
109
|
_removeWorker(workerInfo) {
|
|
135
110
|
const index = this.workers.indexOf(workerInfo);
|
|
136
111
|
if (index > -1) {
|
|
137
112
|
this.workers.splice(index, 1);
|
|
138
113
|
}
|
|
139
|
-
|
|
140
114
|
const freeIndex = this.freeWorkers.indexOf(workerInfo);
|
|
141
115
|
if (freeIndex > -1) {
|
|
142
116
|
this.freeWorkers.splice(freeIndex, 1);
|
|
143
117
|
}
|
|
144
|
-
|
|
145
118
|
if (workerInfo.timeoutHandle) {
|
|
146
119
|
clearTimeout(workerInfo.timeoutHandle);
|
|
147
120
|
}
|
|
148
|
-
|
|
149
|
-
workerInfo.worker.terminate();
|
|
121
|
+
void workerInfo.worker.terminate();
|
|
150
122
|
}
|
|
151
|
-
|
|
152
123
|
async process(data, options = {}) {
|
|
153
124
|
if (this.closing) {
|
|
154
125
|
throw new Error('Worker pool is closing');
|
|
155
126
|
}
|
|
156
|
-
|
|
157
127
|
const timeout = options.timeout || this.workerTimeout;
|
|
158
|
-
|
|
159
128
|
return new Promise((resolve, reject) => {
|
|
160
129
|
const task = { data, resolve, reject, timeout };
|
|
161
|
-
|
|
162
130
|
// Try to get a free worker
|
|
163
131
|
const workerInfo = this.freeWorkers.shift();
|
|
164
|
-
|
|
165
132
|
if (workerInfo) {
|
|
166
133
|
this._executeTask(workerInfo, task);
|
|
167
|
-
}
|
|
134
|
+
}
|
|
135
|
+
else if (this.workers.length < this.maxWorkers) {
|
|
168
136
|
// Create new worker if below max
|
|
169
137
|
const newWorker = this._createWorker();
|
|
170
138
|
if (newWorker) {
|
|
171
139
|
this.freeWorkers.shift(); // Remove from free list
|
|
172
140
|
this._executeTask(newWorker, task);
|
|
173
141
|
}
|
|
174
|
-
}
|
|
142
|
+
}
|
|
143
|
+
else if (this.queue.length < this.maxQueueSize) {
|
|
175
144
|
// Queue the task
|
|
176
145
|
this.queue.push(task);
|
|
177
|
-
}
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
178
148
|
reject(new Error('Worker pool queue is full'));
|
|
179
149
|
}
|
|
180
150
|
});
|
|
181
151
|
}
|
|
182
|
-
|
|
183
152
|
_executeTask(workerInfo, task) {
|
|
184
153
|
workerInfo.busy = true;
|
|
185
154
|
workerInfo.lastUsed = Date.now();
|
|
186
|
-
|
|
187
155
|
// Clear idle timeout while working
|
|
188
156
|
if (workerInfo.timeoutHandle) {
|
|
189
157
|
clearTimeout(workerInfo.timeoutHandle);
|
|
190
158
|
workerInfo.timeoutHandle = null;
|
|
191
159
|
}
|
|
192
|
-
|
|
193
160
|
// Set up task timeout
|
|
194
161
|
const timeoutHandle = setTimeout(() => {
|
|
195
162
|
task.reject(new Error(`Worker task timed out after ${task.timeout}ms`));
|
|
196
163
|
workerInfo.currentTask = null;
|
|
197
164
|
workerInfo.busy = false;
|
|
198
|
-
|
|
199
165
|
// Terminate and replace the worker
|
|
200
166
|
this._removeWorker(workerInfo);
|
|
201
167
|
if (this.workers.length < this.minWorkers && !this.closing) {
|
|
202
168
|
this._createWorker();
|
|
203
169
|
}
|
|
204
|
-
|
|
205
170
|
this._processQueue();
|
|
206
171
|
}, task.timeout);
|
|
207
|
-
|
|
208
172
|
workerInfo.currentTask = { ...task, timeoutHandle };
|
|
209
173
|
workerInfo.worker.postMessage(task.data);
|
|
210
174
|
}
|
|
211
|
-
|
|
212
175
|
_processQueue() {
|
|
213
176
|
if (this.queue.length === 0) {
|
|
214
177
|
// Mark free workers and set up idle timeouts
|
|
@@ -222,7 +185,6 @@ class WorkerPool extends EventEmitter {
|
|
|
222
185
|
});
|
|
223
186
|
return;
|
|
224
187
|
}
|
|
225
|
-
|
|
226
188
|
const workerInfo = this.freeWorkers.shift();
|
|
227
189
|
if (workerInfo) {
|
|
228
190
|
const task = this.queue.shift();
|
|
@@ -231,16 +193,13 @@ class WorkerPool extends EventEmitter {
|
|
|
231
193
|
}
|
|
232
194
|
}
|
|
233
195
|
}
|
|
234
|
-
|
|
235
196
|
async close() {
|
|
236
197
|
this.closing = true;
|
|
237
|
-
|
|
238
198
|
// Clear the queue
|
|
239
199
|
while (this.queue.length > 0) {
|
|
240
200
|
const task = this.queue.shift();
|
|
241
201
|
task.reject(new Error('Worker pool is closing'));
|
|
242
202
|
}
|
|
243
|
-
|
|
244
203
|
// Terminate all workers
|
|
245
204
|
const terminatePromises = this.workers.map(workerInfo => {
|
|
246
205
|
if (workerInfo.timeoutHandle) {
|
|
@@ -248,13 +207,10 @@ class WorkerPool extends EventEmitter {
|
|
|
248
207
|
}
|
|
249
208
|
return workerInfo.worker.terminate();
|
|
250
209
|
});
|
|
251
|
-
|
|
252
210
|
await Promise.all(terminatePromises);
|
|
253
|
-
|
|
254
211
|
this.workers = [];
|
|
255
212
|
this.freeWorkers = [];
|
|
256
213
|
}
|
|
257
|
-
|
|
258
214
|
getStats() {
|
|
259
215
|
return {
|
|
260
216
|
totalWorkers: this.workers.length,
|
|
@@ -266,5 +222,3 @@ class WorkerPool extends EventEmitter {
|
|
|
266
222
|
};
|
|
267
223
|
}
|
|
268
224
|
}
|
|
269
|
-
|
|
270
|
-
module.exports = WorkerPool;
|
package/package.json
CHANGED
|
@@ -1,18 +1,69 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@postalsys/email-text-tools",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"description": "Email text tools",
|
|
5
|
-
"
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/cjs/index.js",
|
|
7
|
+
"types": "./dist/cjs/index.d.ts",
|
|
8
|
+
"typesVersions": {
|
|
9
|
+
"*": {
|
|
10
|
+
"lib/*": [
|
|
11
|
+
"dist/cjs/lib/*"
|
|
12
|
+
],
|
|
13
|
+
"index": [
|
|
14
|
+
"dist/cjs/index.d.ts"
|
|
15
|
+
],
|
|
16
|
+
"index.js": [
|
|
17
|
+
"dist/cjs/index.d.ts"
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"import": "./dist/esm/index.js",
|
|
24
|
+
"require": "./dist/cjs/index.js"
|
|
25
|
+
},
|
|
26
|
+
"./index": {
|
|
27
|
+
"import": "./dist/esm/index.js",
|
|
28
|
+
"require": "./dist/cjs/index.js"
|
|
29
|
+
},
|
|
30
|
+
"./index.js": {
|
|
31
|
+
"import": "./dist/esm/index.js",
|
|
32
|
+
"require": "./dist/cjs/index.js"
|
|
33
|
+
},
|
|
34
|
+
"./lib/*.js": {
|
|
35
|
+
"import": "./dist/esm/lib/*.js",
|
|
36
|
+
"require": "./dist/cjs/lib/*.js"
|
|
37
|
+
},
|
|
38
|
+
"./lib/*": {
|
|
39
|
+
"import": "./dist/esm/lib/*.js",
|
|
40
|
+
"require": "./dist/cjs/lib/*.js"
|
|
41
|
+
},
|
|
42
|
+
"./package.json": "./package.json"
|
|
43
|
+
},
|
|
44
|
+
"files": [
|
|
45
|
+
"dist",
|
|
46
|
+
"README.md",
|
|
47
|
+
"LICENSE",
|
|
48
|
+
"CHANGELOG.md",
|
|
49
|
+
"SECURITY.md"
|
|
50
|
+
],
|
|
6
51
|
"scripts": {
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"
|
|
52
|
+
"build": "node scripts/build.js",
|
|
53
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
54
|
+
"pretest": "npm run build",
|
|
55
|
+
"test": "node --test test/*.test.cjs test/package/*.test.cjs test/package/*.test.mjs",
|
|
56
|
+
"pretest:coverage": "npm run build",
|
|
57
|
+
"test:coverage": "node --test --experimental-test-coverage test/*.test.cjs",
|
|
58
|
+
"test:watch": "node --test --watch test/*.test.cjs",
|
|
59
|
+
"pretest:cjs": "npm run build",
|
|
60
|
+
"test:cjs": "NODE_OPTIONS=--no-experimental-require-module node --test test/*.test.cjs test/package/*.test.cjs",
|
|
61
|
+
"lint": "npm run typecheck && eslint .",
|
|
11
62
|
"lint:fix": "eslint . --fix",
|
|
12
63
|
"format": "prettier --write .",
|
|
13
64
|
"format:check": "prettier --check .",
|
|
14
|
-
"
|
|
15
|
-
"
|
|
65
|
+
"prepare": "npm run build",
|
|
66
|
+
"update": "rm -rf node_modules package-lock.json && ncu -u && npm install"
|
|
16
67
|
},
|
|
17
68
|
"repository": {
|
|
18
69
|
"type": "git",
|
|
@@ -29,11 +80,17 @@
|
|
|
29
80
|
},
|
|
30
81
|
"homepage": "https://github.com/postalsys/email-text-tools#readme",
|
|
31
82
|
"devDependencies": {
|
|
32
|
-
"@
|
|
83
|
+
"@types/he": "1.2.3",
|
|
84
|
+
"@types/html-to-text": "9.0.4",
|
|
85
|
+
"@types/jsdom": "27.0.0",
|
|
86
|
+
"@types/linkify-it": "5.0.0",
|
|
87
|
+
"@types/node": "20.19.43",
|
|
33
88
|
"eslint": "10.13.0",
|
|
34
89
|
"eslint-config-prettier": "10.1.8",
|
|
35
90
|
"globals": "17.13.0",
|
|
36
|
-
"prettier": "3.9.10"
|
|
91
|
+
"prettier": "3.9.10",
|
|
92
|
+
"typescript": "6.0.3",
|
|
93
|
+
"typescript-eslint": "8.71.2"
|
|
37
94
|
},
|
|
38
95
|
"dependencies": {
|
|
39
96
|
"@postalsys/email-content": "0.1.17",
|