@nitpicker/core 0.24.0 → 0.25.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/README.md +165 -35
  2. package/lib/assert-u32-id.d.ts +15 -0
  3. package/lib/assert-u32-id.js +22 -0
  4. package/lib/check-native-executor-config.d.ts +21 -0
  5. package/lib/check-native-executor-config.js +24 -0
  6. package/lib/compute-alias-keys.d.ts +28 -0
  7. package/lib/compute-alias-keys.js +30 -0
  8. package/lib/compute-body-hash.d.ts +22 -0
  9. package/lib/compute-body-hash.js +25 -0
  10. package/lib/compute-url-sort-ranks.d.ts +28 -0
  11. package/lib/compute-url-sort-ranks.js +49 -0
  12. package/lib/create-isolated-cluster-run.d.ts +35 -0
  13. package/lib/create-isolated-cluster-run.js +52 -0
  14. package/lib/create-template-classification-run.d.ts +49 -0
  15. package/lib/create-template-classification-run.js +386 -0
  16. package/lib/group-alias-candidates.d.ts +40 -0
  17. package/lib/group-alias-candidates.js +104 -0
  18. package/lib/load-native-binding.d.ts +26 -0
  19. package/lib/load-native-binding.js +152 -0
  20. package/lib/match-js-license-comment-signal.d.ts +19 -0
  21. package/lib/match-js-license-comment-signal.js +21 -0
  22. package/lib/match-selector-html.d.ts +41 -0
  23. package/lib/match-selector-html.js +42 -0
  24. package/lib/match-selector-snapshots.d.ts +40 -0
  25. package/lib/match-selector-snapshots.js +57 -0
  26. package/lib/match-structural-technology-signals.d.ts +23 -0
  27. package/lib/match-structural-technology-signals.js +25 -0
  28. package/lib/max-decoded-snapshot-bytes.d.ts +22 -0
  29. package/lib/max-decoded-snapshot-bytes.js +35 -0
  30. package/lib/native-binding.d.ts +9 -0
  31. package/lib/native-binding.js +9 -0
  32. package/lib/pack-stored-snapshots.d.ts +23 -0
  33. package/lib/pack-stored-snapshots.js +53 -0
  34. package/lib/run-with-signal.d.ts +27 -0
  35. package/lib/run-with-signal.js +54 -0
  36. package/lib/search-html-document.d.ts +32 -0
  37. package/lib/search-html-document.js +31 -0
  38. package/lib/search-html-snapshots.d.ts +46 -0
  39. package/lib/search-html-snapshots.js +54 -0
  40. package/lib/template-block-sample-size.d.ts +13 -0
  41. package/lib/template-block-sample-size.js +14 -0
  42. package/lib/template-exact-page-limit.d.ts +18 -0
  43. package/lib/template-exact-page-limit.js +19 -0
  44. package/lib/template-memory-budget.d.ts +16 -0
  45. package/lib/template-memory-budget.js +17 -0
  46. package/lib/to-native-selector-plan.d.ts +16 -0
  47. package/lib/to-native-selector-plan.js +40 -0
  48. package/lib/to-u32-ids.d.ts +14 -0
  49. package/lib/to-u32-ids.js +22 -0
  50. package/lib/types.d.ts +736 -248
  51. package/lib/validate-search-pattern.d.ts +25 -0
  52. package/lib/validate-search-pattern.js +27 -0
  53. package/package.json +83 -16
  54. package/lib/core.d.ts +0 -4
  55. package/lib/core.js +0 -4
  56. package/lib/discover-analyze-plugins.d.ts +0 -14
  57. package/lib/discover-analyze-plugins.js +0 -32
  58. package/lib/get-table-cache-root.d.ts +0 -18
  59. package/lib/get-table-cache-root.js +0 -22
  60. package/lib/hooks/define-plugin.d.ts +0 -68
  61. package/lib/hooks/define-plugin.js +0 -69
  62. package/lib/import-modules.d.ts +0 -24
  63. package/lib/import-modules.js +0 -38
  64. package/lib/load-plugin-settings.d.ts +0 -44
  65. package/lib/load-plugin-settings.js +0 -125
  66. package/lib/nitpicker.d.ts +0 -150
  67. package/lib/nitpicker.js +0 -437
  68. package/lib/page-analysis-worker.d.ts +0 -58
  69. package/lib/page-analysis-worker.js +0 -68
  70. package/lib/read-plugin-labels.d.ts +0 -15
  71. package/lib/read-plugin-labels.js +0 -30
  72. package/lib/table.d.ts +0 -75
  73. package/lib/table.js +0 -132
  74. package/lib/url-event-bus.d.ts +0 -33
  75. package/lib/url-event-bus.js +0 -21
  76. package/lib/worker/runner.d.ts +0 -25
  77. package/lib/worker/runner.js +0 -31
  78. package/lib/worker/types.d.ts +0 -23
  79. package/lib/worker/types.js +0 -1
  80. package/lib/worker/worker-pool.d.ts +0 -82
  81. package/lib/worker/worker-pool.js +0 -278
  82. package/lib/worker/worker.d.ts +0 -34
  83. package/lib/worker/worker.js +0 -108
@@ -1,278 +0,0 @@
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
- }
@@ -1,34 +0,0 @@
1
- /**
2
- * Worker thread entry point for plugin execution.
3
- *
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.
8
- *
9
- * ## Message protocol (Worker ↔ Pool)
10
- *
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? }` |
17
- *
18
- * ## Lifecycle
19
- *
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.
27
- *
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
31
- * @see {@link ./runner.ts!runner} for the actual module loading logic
32
- * @module
33
- */
34
- export {};
@@ -1,108 +0,0 @@
1
- /**
2
- * Worker thread entry point for plugin execution.
3
- *
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.
8
- *
9
- * ## Message protocol (Worker ↔ Pool)
10
- *
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? }` |
17
- *
18
- * ## Lifecycle
19
- *
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.
27
- *
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
31
- * @see {@link ./runner.ts!runner} for the actual module loading logic
32
- * @module
33
- */
34
- import { parentPort } from 'node:worker_threads';
35
- import { UrlEventBus } from '../url-event-bus.js';
36
- import { runner } from './runner.js';
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. */
40
- const emitter = new UrlEventBus();
41
- emitter.on('url', (url) => {
42
- if (!parentPort) {
43
- throw new Error('Use in worker thread');
44
- }
45
- if (currentTaskId === null) {
46
- return;
47
- }
48
- parentPort.postMessage({
49
- type: 'url',
50
- taskId: currentTaskId,
51
- url,
52
- });
53
- });
54
- if (!parentPort) {
55
- throw new Error('Use in worker thread');
56
- }
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;
107
- }
108
- }