@nitpicker/core 0.6.4 → 0.7.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/README.md +39 -1
- package/lib/core.d.ts +0 -1
- package/lib/core.js +0 -1
- package/lib/nitpicker.d.ts +18 -15
- package/lib/nitpicker.js +113 -87
- package/lib/page-analysis-worker.d.ts +7 -5
- package/lib/page-analysis-worker.js +4 -3
- package/lib/types.d.ts +12 -1
- package/lib/url-event-bus.d.ts +7 -6
- package/lib/url-event-bus.js +7 -6
- package/lib/worker/runner.d.ts +2 -2
- package/lib/worker/runner.js +2 -2
- package/lib/worker/types.d.ts +3 -3
- package/lib/worker/worker-pool.d.ts +82 -0
- package/lib/worker/worker-pool.js +278 -0
- package/lib/worker/worker.d.ts +21 -14
- package/lib/worker/worker.js +81 -41
- package/package.json +4 -4
- package/lib/find-nitpicker-modules-dir.d.ts +0 -12
- package/lib/find-nitpicker-modules-dir.js +0 -23
- package/lib/hooks/actions.d.ts +0 -9
- package/lib/hooks/actions.js +0 -9
- package/lib/hooks/child-process.d.ts +0 -1
- package/lib/hooks/child-process.js +0 -34
- package/lib/hooks/index.d.ts +0 -1
- package/lib/hooks/index.js +0 -1
- package/lib/hooks/runner.d.ts +0 -10
- package/lib/hooks/runner.js +0 -32
- package/lib/index.d.ts +0 -5
- package/lib/index.js +0 -5
- package/lib/utils.d.ts +0 -36
- package/lib/utils.js +0 -43
- package/lib/worker/run-in-worker.d.ts +0 -51
- package/lib/worker/run-in-worker.js +0 -125
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { Worker } from 'node:worker_threads';
|
|
2
|
+
/**
|
|
3
|
+
* Long-lived pool of Worker threads that share execution across many tasks.
|
|
4
|
+
*
|
|
5
|
+
* Replaces the previous "one worker per page" model where every analyzed page
|
|
6
|
+
* paid the cost of spawning a new V8 isolate, importing JSDOM, importing the
|
|
7
|
+
* plugin module, and tearing it all down. With the pool, each worker is
|
|
8
|
+
* created once at the start of an analyze run and processes tasks via
|
|
9
|
+
* message passing until the pool is terminated.
|
|
10
|
+
*
|
|
11
|
+
* ## Why this matters
|
|
12
|
+
*
|
|
13
|
+
* On a 750-page archive with `CONCURRENCY_LIMIT = 50`, the old model spawned
|
|
14
|
+
* 50 workers nearly simultaneously and repeated this every time a worker
|
|
15
|
+
* finished, producing recurring "boot waves" that drove peak memory above
|
|
16
|
+
* 20GB. The pool model performs N initial boots and then reuses those V8
|
|
17
|
+
* isolates for the rest of the run, eliminating the recurring cost entirely.
|
|
18
|
+
*
|
|
19
|
+
* ## Message protocol (Pool ↔ Worker)
|
|
20
|
+
*
|
|
21
|
+
* | Direction | `type` | Payload |
|
|
22
|
+
* |------------------|---------------|----------------------------------|
|
|
23
|
+
* | Pool → Worker | `'task'` | `{ taskId, data }` |
|
|
24
|
+
* | Pool → Worker | `'shutdown'` | (none) |
|
|
25
|
+
* | Worker → Pool | `'url'` | `{ taskId, url }` (forwarded) |
|
|
26
|
+
* | Worker → Pool | `'result'` | `{ taskId, result, error? }` |
|
|
27
|
+
*
|
|
28
|
+
* Each task carries a unique numeric `taskId` so that responses and URL
|
|
29
|
+
* notifications from a worker can be routed back to the correct caller.
|
|
30
|
+
*/
|
|
31
|
+
export class WorkerPool {
|
|
32
|
+
/** FIFO queue of slots ready to accept a task. */
|
|
33
|
+
#idle = [];
|
|
34
|
+
/** Tasks awaiting their result, keyed by taskId. */
|
|
35
|
+
#inFlight = new Map();
|
|
36
|
+
/** Monotonic task identifier source. */
|
|
37
|
+
#nextTaskId = 0;
|
|
38
|
+
/** FIFO queue of pending tasks waiting for an idle slot. */
|
|
39
|
+
#queue = [];
|
|
40
|
+
/** Once `terminate()` has been called, no new tasks may be submitted. */
|
|
41
|
+
#shuttingDown = false;
|
|
42
|
+
/** All worker slots, idle or busy. */
|
|
43
|
+
#slots = [];
|
|
44
|
+
/** Per-task data payload to send when a queued task is dispatched. */
|
|
45
|
+
#taskData = new Map();
|
|
46
|
+
/** Worker entry script path; reused when replacing crashed workers. */
|
|
47
|
+
#workerPath;
|
|
48
|
+
/**
|
|
49
|
+
* Constructs a pool with the given size and worker entry script.
|
|
50
|
+
* @param options - Pool size and worker entry script path.
|
|
51
|
+
*/
|
|
52
|
+
constructor(options) {
|
|
53
|
+
const size = Math.max(1, Math.floor(options.size));
|
|
54
|
+
this.#workerPath = options.workerPath;
|
|
55
|
+
for (let i = 0; i < size; i++) {
|
|
56
|
+
this.#spawn();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Submits a task to the pool. Resolves with the worker's `result` payload
|
|
61
|
+
* once a worker has finished processing it. The task may be queued if all
|
|
62
|
+
* workers are currently busy.
|
|
63
|
+
* @template I - Shape of the per-task `initialData` payload.
|
|
64
|
+
* @template R - Expected result type returned by the worker module.
|
|
65
|
+
* @param params - Task payload (file path, batch indices, emitter, data).
|
|
66
|
+
* @returns The worker's result.
|
|
67
|
+
*/
|
|
68
|
+
run(params) {
|
|
69
|
+
if (this.#shuttingDown) {
|
|
70
|
+
return Promise.reject(new Error('WorkerPool is shutting down'));
|
|
71
|
+
}
|
|
72
|
+
const taskId = this.#nextTaskId++;
|
|
73
|
+
const data = {
|
|
74
|
+
filePath: params.filePath,
|
|
75
|
+
num: params.num,
|
|
76
|
+
total: params.total,
|
|
77
|
+
...params.initialData,
|
|
78
|
+
};
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
const task = {
|
|
81
|
+
taskId,
|
|
82
|
+
emitter: params.emitter,
|
|
83
|
+
resolve: resolve,
|
|
84
|
+
reject,
|
|
85
|
+
};
|
|
86
|
+
this.#taskData.set(taskId, data);
|
|
87
|
+
const slot = this.#idle.shift();
|
|
88
|
+
if (slot) {
|
|
89
|
+
this.#dispatch(slot, task);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
this.#queue.push(task);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Rejects every queued or in-flight task, sends each worker a `shutdown`
|
|
97
|
+
* message, and awaits its exit. After this call returns, no further
|
|
98
|
+
* submissions are accepted.
|
|
99
|
+
*
|
|
100
|
+
* Pending tasks are rejected synchronously so callers waiting on
|
|
101
|
+
* `pool.run()` Promises do not hang. Workers handle the shutdown message
|
|
102
|
+
* by exiting cleanly; if that fails, {@link #shutdownSlot} forces
|
|
103
|
+
* termination after a 5 second timeout.
|
|
104
|
+
*/
|
|
105
|
+
async terminate() {
|
|
106
|
+
this.#shuttingDown = true;
|
|
107
|
+
const pendingError = new Error('WorkerPool terminated before task completed');
|
|
108
|
+
for (const task of this.#queue) {
|
|
109
|
+
task.reject(pendingError);
|
|
110
|
+
this.#taskData.delete(task.taskId);
|
|
111
|
+
}
|
|
112
|
+
this.#queue = [];
|
|
113
|
+
for (const task of this.#inFlight.values()) {
|
|
114
|
+
task.reject(pendingError);
|
|
115
|
+
}
|
|
116
|
+
this.#inFlight.clear();
|
|
117
|
+
await Promise.all(this.#slots.map((slot) => this.#shutdownSlot(slot)));
|
|
118
|
+
this.#slots = [];
|
|
119
|
+
this.#idle = [];
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Completes the current task on `slot` and dispatches the next queued task.
|
|
123
|
+
* @param slot
|
|
124
|
+
* @param taskId
|
|
125
|
+
* @param result
|
|
126
|
+
* @param error
|
|
127
|
+
*/
|
|
128
|
+
#completeTask(slot, taskId, result, error) {
|
|
129
|
+
const task = this.#inFlight.get(taskId);
|
|
130
|
+
this.#inFlight.delete(taskId);
|
|
131
|
+
slot.current = null;
|
|
132
|
+
if (task) {
|
|
133
|
+
if (error) {
|
|
134
|
+
task.reject(new Error(error));
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
task.resolve(result);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const next = this.#queue.shift();
|
|
141
|
+
if (next) {
|
|
142
|
+
this.#dispatch(slot, next);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
this.#idle.push(slot);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Sends a queued task to an idle slot and registers it as in-flight.
|
|
150
|
+
*
|
|
151
|
+
* If `postMessage` throws (e.g. the worker died between handoff and
|
|
152
|
+
* dispatch), the task is rejected immediately and the slot is left out
|
|
153
|
+
* of the idle pool so the worker's `error` event can replace it via
|
|
154
|
+
* {@link #onWorkerCrash}.
|
|
155
|
+
* @param slot - Pool slot that owns the worker.
|
|
156
|
+
* @param task - Pending task to dispatch.
|
|
157
|
+
*/
|
|
158
|
+
#dispatch(slot, task) {
|
|
159
|
+
const data = this.#taskData.get(task.taskId);
|
|
160
|
+
try {
|
|
161
|
+
slot.worker.postMessage({ type: 'task', taskId: task.taskId, data });
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
this.#taskData.delete(task.taskId);
|
|
165
|
+
task.reject(error instanceof Error ? error : new Error(String(error)));
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
slot.current = task;
|
|
169
|
+
this.#inFlight.set(task.taskId, task);
|
|
170
|
+
this.#taskData.delete(task.taskId);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Routes an incoming worker message to the matching pending task.
|
|
174
|
+
* @param slot
|
|
175
|
+
* @param message
|
|
176
|
+
*/
|
|
177
|
+
#onMessage(slot, message) {
|
|
178
|
+
if (!message || typeof message !== 'object') {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const msg = message;
|
|
182
|
+
if (msg.type === 'url' &&
|
|
183
|
+
typeof msg.taskId === 'number' &&
|
|
184
|
+
typeof msg.url === 'string') {
|
|
185
|
+
const task = this.#inFlight.get(msg.taskId);
|
|
186
|
+
if (task) {
|
|
187
|
+
void task.emitter.emit('url', msg.url);
|
|
188
|
+
}
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (msg.type === 'result' && typeof msg.taskId === 'number') {
|
|
192
|
+
this.#completeTask(slot, msg.taskId, msg.result, msg.error);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Handles an unrecoverable worker failure: rejects the current task,
|
|
198
|
+
* removes the dead slot, and spawns a replacement so the pool keeps its
|
|
199
|
+
* configured size. Queued tasks are picked up by the new slot when ready.
|
|
200
|
+
* @param slot
|
|
201
|
+
* @param error
|
|
202
|
+
*/
|
|
203
|
+
#onWorkerCrash(slot, error) {
|
|
204
|
+
const task = slot.current;
|
|
205
|
+
slot.current = null;
|
|
206
|
+
if (task) {
|
|
207
|
+
this.#inFlight.delete(task.taskId);
|
|
208
|
+
task.reject(error instanceof Error ? error : new Error(String(error)));
|
|
209
|
+
}
|
|
210
|
+
const slotIndex = this.#slots.indexOf(slot);
|
|
211
|
+
if (slotIndex !== -1) {
|
|
212
|
+
this.#slots.splice(slotIndex, 1);
|
|
213
|
+
}
|
|
214
|
+
const idleIndex = this.#idle.indexOf(slot);
|
|
215
|
+
if (idleIndex !== -1) {
|
|
216
|
+
this.#idle.splice(idleIndex, 1);
|
|
217
|
+
}
|
|
218
|
+
slot.worker.removeAllListeners();
|
|
219
|
+
void slot.worker.terminate();
|
|
220
|
+
if (this.#shuttingDown) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const replacement = this.#spawn();
|
|
224
|
+
const next = this.#queue.shift();
|
|
225
|
+
if (next) {
|
|
226
|
+
const idleIdx = this.#idle.indexOf(replacement);
|
|
227
|
+
if (idleIdx !== -1) {
|
|
228
|
+
this.#idle.splice(idleIdx, 1);
|
|
229
|
+
}
|
|
230
|
+
this.#dispatch(replacement, next);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Sends a shutdown message and waits for the worker to exit cleanly.
|
|
235
|
+
* @param slot
|
|
236
|
+
*/
|
|
237
|
+
#shutdownSlot(slot) {
|
|
238
|
+
return new Promise((resolve) => {
|
|
239
|
+
const finish = () => {
|
|
240
|
+
slot.worker.removeAllListeners();
|
|
241
|
+
resolve();
|
|
242
|
+
};
|
|
243
|
+
slot.worker.once('exit', finish);
|
|
244
|
+
slot.worker.once('error', finish);
|
|
245
|
+
try {
|
|
246
|
+
slot.worker.postMessage({ type: 'shutdown' });
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
void slot.worker.terminate().finally(finish);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
// Safety net in case the worker fails to acknowledge the shutdown.
|
|
253
|
+
setTimeout(() => {
|
|
254
|
+
void slot.worker.terminate().finally(finish);
|
|
255
|
+
}, 5000).unref();
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
/** Spawns a fresh worker, wires up its event handlers, and adds it to the pool. */
|
|
259
|
+
#spawn() {
|
|
260
|
+
const worker = new Worker(this.#workerPath);
|
|
261
|
+
const slot = { worker, current: null };
|
|
262
|
+
this.#slots.push(slot);
|
|
263
|
+
worker.on('message', (message) => {
|
|
264
|
+
this.#onMessage(slot, message);
|
|
265
|
+
});
|
|
266
|
+
worker.on('error', (error) => {
|
|
267
|
+
this.#onWorkerCrash(slot, error);
|
|
268
|
+
});
|
|
269
|
+
worker.on('messageerror', (error) => {
|
|
270
|
+
this.#onWorkerCrash(slot, error);
|
|
271
|
+
});
|
|
272
|
+
this.#idle.push(slot);
|
|
273
|
+
// Workers should not keep the event loop alive on their own;
|
|
274
|
+
// the pool's terminate() controls their lifetime.
|
|
275
|
+
worker.unref();
|
|
276
|
+
return slot;
|
|
277
|
+
}
|
|
278
|
+
}
|
package/lib/worker/worker.d.ts
CHANGED
|
@@ -1,26 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Worker thread entry point for plugin execution.
|
|
3
3
|
*
|
|
4
|
-
* This module runs inside a `new Worker(...)` created by
|
|
5
|
-
*
|
|
4
|
+
* This module runs inside a `new Worker(...)` created by the
|
|
5
|
+
* {@link ./worker-pool.ts!WorkerPool}. Unlike the previous one-shot model,
|
|
6
|
+
* each worker is long-lived and processes many tasks via a message loop until
|
|
7
|
+
* it receives a `shutdown` message.
|
|
6
8
|
*
|
|
7
|
-
* ## Message protocol (Worker
|
|
9
|
+
* ## Message protocol (Worker ↔ Pool)
|
|
8
10
|
*
|
|
9
|
-
* | `type`
|
|
10
|
-
*
|
|
11
|
-
* | `'
|
|
12
|
-
* |
|
|
11
|
+
* | Direction | `type` | Payload |
|
|
12
|
+
* |-----------------|---------------|----------------------------------|
|
|
13
|
+
* | Pool → Worker | `'task'` | `{ taskId, data }` |
|
|
14
|
+
* | Pool → Worker | `'shutdown'` | (none) |
|
|
15
|
+
* | Worker → Pool | `'url'` | `{ taskId, url }` |
|
|
16
|
+
* | Worker → Pool | `'result'` | `{ taskId, result, error? }` |
|
|
13
17
|
*
|
|
14
18
|
* ## Lifecycle
|
|
15
19
|
*
|
|
16
|
-
* 1.
|
|
17
|
-
* 2.
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
20
|
+
* 1. Listens on `parentPort` for `task` messages.
|
|
21
|
+
* 2. For each task, delegates to {@link ./runner.ts!runner} which dynamically
|
|
22
|
+
* `import()`s the module pointed to by `data.filePath`. Subsequent tasks
|
|
23
|
+
* that target the same module reuse the import cache, so the heavy boot
|
|
24
|
+
* cost is paid once per module per worker.
|
|
25
|
+
* 3. Posts a `result` message back to the pool and waits for the next task.
|
|
26
|
+
* 4. On `shutdown`, exits the process so the V8 isolate is fully released.
|
|
21
27
|
*
|
|
22
|
-
* The
|
|
23
|
-
*
|
|
28
|
+
* The `'url'` events emitted by plugins are forwarded with the active
|
|
29
|
+
* `taskId` so the pool can route them back to the correct caller's emitter.
|
|
30
|
+
* @see {@link ./worker-pool.ts!WorkerPool} for the main-thread counterpart
|
|
24
31
|
* @see {@link ./runner.ts!runner} for the actual module loading logic
|
|
25
32
|
* @module
|
|
26
33
|
*/
|
package/lib/worker/worker.js
CHANGED
|
@@ -1,68 +1,108 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Worker thread entry point for plugin execution.
|
|
3
3
|
*
|
|
4
|
-
* This module runs inside a `new Worker(...)` created by
|
|
5
|
-
*
|
|
4
|
+
* This module runs inside a `new Worker(...)` created by the
|
|
5
|
+
* {@link ./worker-pool.ts!WorkerPool}. Unlike the previous one-shot model,
|
|
6
|
+
* each worker is long-lived and processes many tasks via a message loop until
|
|
7
|
+
* it receives a `shutdown` message.
|
|
6
8
|
*
|
|
7
|
-
* ## Message protocol (Worker
|
|
9
|
+
* ## Message protocol (Worker ↔ Pool)
|
|
8
10
|
*
|
|
9
|
-
* | `type`
|
|
10
|
-
*
|
|
11
|
-
* | `'
|
|
12
|
-
* |
|
|
11
|
+
* | Direction | `type` | Payload |
|
|
12
|
+
* |-----------------|---------------|----------------------------------|
|
|
13
|
+
* | Pool → Worker | `'task'` | `{ taskId, data }` |
|
|
14
|
+
* | Pool → Worker | `'shutdown'` | (none) |
|
|
15
|
+
* | Worker → Pool | `'url'` | `{ taskId, url }` |
|
|
16
|
+
* | Worker → Pool | `'result'` | `{ taskId, result, error? }` |
|
|
13
17
|
*
|
|
14
18
|
* ## Lifecycle
|
|
15
19
|
*
|
|
16
|
-
* 1.
|
|
17
|
-
* 2.
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
20
|
+
* 1. Listens on `parentPort` for `task` messages.
|
|
21
|
+
* 2. For each task, delegates to {@link ./runner.ts!runner} which dynamically
|
|
22
|
+
* `import()`s the module pointed to by `data.filePath`. Subsequent tasks
|
|
23
|
+
* that target the same module reuse the import cache, so the heavy boot
|
|
24
|
+
* cost is paid once per module per worker.
|
|
25
|
+
* 3. Posts a `result` message back to the pool and waits for the next task.
|
|
26
|
+
* 4. On `shutdown`, exits the process so the V8 isolate is fully released.
|
|
21
27
|
*
|
|
22
|
-
* The
|
|
23
|
-
*
|
|
28
|
+
* The `'url'` events emitted by plugins are forwarded with the active
|
|
29
|
+
* `taskId` so the pool can route them back to the correct caller's emitter.
|
|
30
|
+
* @see {@link ./worker-pool.ts!WorkerPool} for the main-thread counterpart
|
|
24
31
|
* @see {@link ./runner.ts!runner} for the actual module loading logic
|
|
25
32
|
* @module
|
|
26
33
|
*/
|
|
27
|
-
import { parentPort
|
|
34
|
+
import { parentPort } from 'node:worker_threads';
|
|
28
35
|
import { UrlEventBus } from '../url-event-bus.js';
|
|
29
36
|
import { runner } from './runner.js';
|
|
30
|
-
|
|
37
|
+
/** Identifier for the task currently being processed; used to tag forwarded `'url'` events. */
|
|
38
|
+
let currentTaskId = null;
|
|
39
|
+
/** Long-lived event bus that forwards `'url'` events from plugins to the pool. */
|
|
31
40
|
const emitter = new UrlEventBus();
|
|
32
|
-
/**
|
|
33
|
-
* Forward URL discovery events from the plugin to the main thread.
|
|
34
|
-
* The main thread's {@link ../url-event-bus.ts!UrlEventBus} re-emits these
|
|
35
|
-
* so that the orchestrator can track discovered URLs.
|
|
36
|
-
*/
|
|
37
41
|
emitter.on('url', (url) => {
|
|
38
42
|
if (!parentPort) {
|
|
39
43
|
throw new Error('Use in worker thread');
|
|
40
44
|
}
|
|
45
|
+
if (currentTaskId === null) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
41
48
|
parentPort.postMessage({
|
|
42
49
|
type: 'url',
|
|
50
|
+
taskId: currentTaskId,
|
|
43
51
|
url,
|
|
44
52
|
});
|
|
45
53
|
});
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (!parentPort) {
|
|
49
|
-
throw new Error('Use in worker thread');
|
|
50
|
-
}
|
|
51
|
-
parentPort.postMessage({
|
|
52
|
-
type: 'finish',
|
|
53
|
-
result,
|
|
54
|
-
});
|
|
54
|
+
if (!parentPort) {
|
|
55
|
+
throw new Error('Use in worker thread');
|
|
55
56
|
}
|
|
56
|
-
|
|
57
|
-
if (!
|
|
58
|
-
|
|
57
|
+
parentPort.on('message', (message) => {
|
|
58
|
+
if (!message || typeof message !== 'object') {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const msg = message;
|
|
62
|
+
if (msg.type === 'shutdown') {
|
|
63
|
+
// Defer to the next tick so any in-flight `postMessage` from the most
|
|
64
|
+
// recent task has a chance to flush before the worker exits.
|
|
65
|
+
setImmediate(() => process.exit(0));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (msg.type === 'task' && typeof msg.taskId === 'number' && msg.data) {
|
|
69
|
+
void runTask(msg.taskId, msg.data);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
/**
|
|
73
|
+
* Executes a single task and posts the result back to the pool.
|
|
74
|
+
*
|
|
75
|
+
* Errors are caught and reported via the `error` field on the `result` message
|
|
76
|
+
* so the pool can reject the originating Promise without the worker crashing.
|
|
77
|
+
* @param taskId - Identifier echoed back so the pool can match the response.
|
|
78
|
+
* @param data - Payload from the pool, including `filePath` consumed by `runner()`.
|
|
79
|
+
*/
|
|
80
|
+
async function runTask(taskId, data) {
|
|
81
|
+
currentTaskId = taskId;
|
|
82
|
+
try {
|
|
83
|
+
const result = await runner(data, emitter);
|
|
84
|
+
if (!parentPort) {
|
|
85
|
+
throw new Error('Use in worker thread');
|
|
86
|
+
}
|
|
87
|
+
parentPort.postMessage({
|
|
88
|
+
type: 'result',
|
|
89
|
+
taskId,
|
|
90
|
+
result,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
if (!parentPort) {
|
|
95
|
+
throw new Error('Use in worker thread');
|
|
96
|
+
}
|
|
97
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
98
|
+
parentPort.postMessage({
|
|
99
|
+
type: 'result',
|
|
100
|
+
taskId,
|
|
101
|
+
result: null,
|
|
102
|
+
error: message,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
currentTaskId = null;
|
|
59
107
|
}
|
|
60
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
61
|
-
// eslint-disable-next-line no-console
|
|
62
|
-
console.error(`[worker] Fatal error: ${message}`);
|
|
63
|
-
parentPort.postMessage({
|
|
64
|
-
type: 'finish',
|
|
65
|
-
result: null,
|
|
66
|
-
error: message,
|
|
67
|
-
});
|
|
68
108
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nitpicker/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Plugin-based page analysis engine for Nitpicker",
|
|
5
5
|
"author": "D-ZERO",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -29,11 +29,11 @@
|
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@d-zero/dealer": "1.7.0",
|
|
31
31
|
"@d-zero/shared": "0.20.1",
|
|
32
|
-
"@nitpicker/crawler": "0.
|
|
33
|
-
"@nitpicker/types": "0.
|
|
32
|
+
"@nitpicker/crawler": "0.7.0",
|
|
33
|
+
"@nitpicker/types": "0.7.0",
|
|
34
34
|
"ansi-colors": "4.1.3",
|
|
35
35
|
"cosmiconfig": "9.0.0",
|
|
36
36
|
"jsdom": "28.1.0"
|
|
37
37
|
},
|
|
38
|
-
"gitHead": "
|
|
38
|
+
"gitHead": "3b4c33adf8af69a46a85faf8c04eddb5ab563972"
|
|
39
39
|
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Walks up from the current module's directory to locate the parent
|
|
3
|
-
* `@nitpicker` directory in the file system.
|
|
4
|
-
*
|
|
5
|
-
* This is used by plugin discovery and label resolution to find
|
|
6
|
-
* where `@nitpicker/*` packages are installed on disk. The traversal
|
|
7
|
-
* starts from `import.meta.url` of the calling module (i.e. this file,
|
|
8
|
-
* which lives inside `@nitpicker/core`) and walks up until it finds
|
|
9
|
-
* a directory named `@nitpicker`.
|
|
10
|
-
* @returns Absolute path to the `@nitpicker` directory, or `null` if not found.
|
|
11
|
-
*/
|
|
12
|
-
export declare function findNitpickerModulesDir(): string | null;
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
/**
|
|
3
|
-
* Walks up from the current module's directory to locate the parent
|
|
4
|
-
* `@nitpicker` directory in the file system.
|
|
5
|
-
*
|
|
6
|
-
* This is used by plugin discovery and label resolution to find
|
|
7
|
-
* where `@nitpicker/*` packages are installed on disk. The traversal
|
|
8
|
-
* starts from `import.meta.url` of the calling module (i.e. this file,
|
|
9
|
-
* which lives inside `@nitpicker/core`) and walks up until it finds
|
|
10
|
-
* a directory named `@nitpicker`.
|
|
11
|
-
* @returns Absolute path to the `@nitpicker` directory, or `null` if not found.
|
|
12
|
-
*/
|
|
13
|
-
export function findNitpickerModulesDir() {
|
|
14
|
-
const __filename = new URL(import.meta.url).pathname;
|
|
15
|
-
let dir = path.dirname(__filename);
|
|
16
|
-
while (dir !== path.dirname(dir)) {
|
|
17
|
-
if (path.basename(dir) === '@nitpicker') {
|
|
18
|
-
return dir;
|
|
19
|
-
}
|
|
20
|
-
dir = path.dirname(dir);
|
|
21
|
-
}
|
|
22
|
-
return null;
|
|
23
|
-
}
|
package/lib/hooks/actions.d.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { StartAction } from '../types.js';
|
|
2
|
-
declare const _default: {
|
|
3
|
-
audit: {
|
|
4
|
-
start: import("typescript-fsa").ActionCreator<StartAction>;
|
|
5
|
-
log: import("typescript-fsa").ActionCreator<string>;
|
|
6
|
-
finish: import("typescript-fsa").ActionCreator<void>;
|
|
7
|
-
};
|
|
8
|
-
};
|
|
9
|
-
export default _default;
|
package/lib/hooks/actions.js
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { actionCreatorFactory } from 'typescript-fsa';
|
|
2
|
-
const auditActionCreator = actionCreatorFactory('@@hook/audit');
|
|
3
|
-
export default {
|
|
4
|
-
audit: {
|
|
5
|
-
start: auditActionCreator('START'),
|
|
6
|
-
log: auditActionCreator('LOG'),
|
|
7
|
-
finish: auditActionCreator('FINISH'),
|
|
8
|
-
},
|
|
9
|
-
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
// Run on child_process
|
|
2
|
-
import { Archive } from '@nitpicker/crawler';
|
|
3
|
-
import { isType } from 'typescript-fsa';
|
|
4
|
-
import actions from './actions.js';
|
|
5
|
-
const crateTable = (headers, data, options) => ({
|
|
6
|
-
headers,
|
|
7
|
-
data,
|
|
8
|
-
options,
|
|
9
|
-
});
|
|
10
|
-
process.on('message', async (action) => {
|
|
11
|
-
if (isType(action, actions.audit.start)) {
|
|
12
|
-
const mod = await import(action.payload.hookModulePath);
|
|
13
|
-
const pluginHook = mod.default;
|
|
14
|
-
const hook = await pluginHook(action.payload.settings || {}, action.payload.configFilePath);
|
|
15
|
-
if (hook.audit) {
|
|
16
|
-
const archive = await Archive.connect(action.payload.archiveTempDir, action.payload.hookModuleName);
|
|
17
|
-
const pluginReport = await hook.audit(archive, (log) => {
|
|
18
|
-
if (process.send) {
|
|
19
|
-
process.send(actions.audit.log(log));
|
|
20
|
-
}
|
|
21
|
-
}, action.payload.configFilePath, crateTable);
|
|
22
|
-
if (pluginReport) {
|
|
23
|
-
const data = {
|
|
24
|
-
name: action.payload.hookModuleName,
|
|
25
|
-
...pluginReport,
|
|
26
|
-
};
|
|
27
|
-
await archive.setData('reports', data);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
if (process.send) {
|
|
31
|
-
process.send(actions.audit.finish());
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
});
|
package/lib/hooks/index.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { definePlugin } from './define-plugin.js';
|
package/lib/hooks/index.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { definePlugin } from './define-plugin.js';
|
package/lib/hooks/runner.d.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import type { StartAction } from '../types.js';
|
|
2
|
-
import type { ProcessLogger } from '@nitpicker/types';
|
|
3
|
-
import actions from './actions.js';
|
|
4
|
-
/**
|
|
5
|
-
*
|
|
6
|
-
* @param phase
|
|
7
|
-
* @param startActionParam
|
|
8
|
-
* @param log
|
|
9
|
-
*/
|
|
10
|
-
export declare function runner(phase: keyof typeof actions, startActionParam: StartAction, log: ProcessLogger): Promise<void>;
|
package/lib/hooks/runner.js
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import childProcess from 'node:child_process';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { isType } from 'typescript-fsa';
|
|
4
|
-
import actions from './actions.js';
|
|
5
|
-
const __filename = new URL(import.meta.url).pathname;
|
|
6
|
-
const __dirname = path.dirname(__filename);
|
|
7
|
-
/**
|
|
8
|
-
*
|
|
9
|
-
* @param phase
|
|
10
|
-
* @param startActionParam
|
|
11
|
-
* @param log
|
|
12
|
-
*/
|
|
13
|
-
export function runner(phase, startActionParam, log) {
|
|
14
|
-
log('Fork process');
|
|
15
|
-
const child = childProcess.fork(path.join(__dirname, 'child-process'));
|
|
16
|
-
const actionOnPhase = actions[phase];
|
|
17
|
-
const session = new Promise((resolve) => {
|
|
18
|
-
child.on('message', (action) => {
|
|
19
|
-
if (isType(action, actionOnPhase.log)) {
|
|
20
|
-
log(action.payload);
|
|
21
|
-
}
|
|
22
|
-
if (isType(action, actionOnPhase.finish)) {
|
|
23
|
-
child.disconnect();
|
|
24
|
-
log('Finished');
|
|
25
|
-
resolve();
|
|
26
|
-
}
|
|
27
|
-
});
|
|
28
|
-
});
|
|
29
|
-
child.send(actionOnPhase.start(startActionParam));
|
|
30
|
-
log('Started');
|
|
31
|
-
return session;
|
|
32
|
-
}
|
package/lib/index.d.ts
DELETED
package/lib/index.js
DELETED