multi-tasks 3.2.2 → 3.3.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 CHANGED
@@ -48,6 +48,7 @@ multiTasks({
48
48
  numberOfWorkers: 3, //how many workers are working in parallel
49
49
  //taskTimeout: 30000, //optional, in milliseconds, an overdue task is treated as failed with a timeout error
50
50
  //maxTaskRetries: 2, //optional, auto-retry a failed task (worker crash, processTask error, or timeout); retried tasks go back to the queue
51
+ //workerPriority: 'low', //optional, one of 'low' | 'below_normal' | 'normal' | 'above_normal' | 'high' (or a nice number -20~19); lower the OS scheduling priority of workers to keep the machine responsive during long batches
51
52
  //autoCloseAfterCompletion: true, //set true to exit the process when all tasks are done; keep it false (default) if you create new tasks dynamically
52
53
  shouldTerminate:(info)=>{
53
54
  //return true if you need to terminate the whole process
@@ -254,7 +255,7 @@ multiTasks.restart({ taskFolder: `/myworks/my_scan_tasks/my-task` }); //deletes
254
255
 
255
256
  ```
256
257
 
257
- Unlike `retry_fails` (which re-runs only the failed tasks), `restart` re-runs **all the initial tasks** — tasks created dynamically via `helper.createNewTasks` are not restored and will not be re-run. Fields you pass (e.g. `processTask`) override the snapshot and are written back into it; the same self-contained-function note as in Resuming applies. (**Breaking change:** the name `restart` was the old name of `retry_fails` in v2 and in the published v3.0–v3.1.x releases; this is a different API — calling it no longer retries failed tasks, it wipes the folder and re-runs everything.)
258
+ Unlike `retry_fails` (which re-runs only the failed tasks), `restart` re-runs **all the initial tasks** — tasks created dynamically via `helper.createNewTasks` are not restored and will not be re-run. Fields you pass (e.g. `processTask`) override the snapshot and are written back into it; the same self-contained-function note as in Resuming applies.
258
259
 
259
260
  ### Real-Time Monitoring
260
261
 
@@ -285,19 +286,20 @@ The API has three parts: the entry functions, the config options, and the `helpe
285
286
  **Config options:**
286
287
 
287
288
  - **`initialTasks`** — array of task objects, a function returning the array (or a Promise of it — called only once, in the master process; worker processes never call it; see [Performance: runs only once, in the master process](#performance-runs-only-once-in-the-master-process)), or a folder path string to resume from.
288
- - **`processTask(task, helper)`** — required; return a value or a Promise.
289
+ - **`processTask(task, helper)`** — required; return a result or a Promise with result. Results are stored as JSON in `results/succ/<subid>.json`; if a result cannot be saved because it is not JSON-serializable, the task still counts as succeeded, and a warning shows up in the command line and in monitor tools.
289
290
  - **`taskRootFolder`** — the task run folder (named by `taskId`) is created under this directory.
290
291
  - **`taskId`** — task folder name under `taskRootFolder`.
291
292
  - **`numberOfWorkers`** — how many worker processes run in parallel; a number, or a percentage string of CPU cores like `"50%"` (default).
292
293
  - **`taskTimeout`** — optional, milliseconds; an overdue task fails with a timeout error.
293
294
  - **`maxTaskRetries`** — optional, max times a failed task is auto-retried.
294
- - **`progressBar`** — `true` by default: shows a live single-line progress bar in the terminal (set `false` to disable); logs (including your own `console.*` calls) always go to `<taskFolder>/.sys/run.log`, and print to the console as well only when the bar is off or stdout is not a TTY.
295
+ - **`workerPriority`** — optional, OS scheduling priority for worker processes: `'low' | 'below_normal' | 'normal' | 'above_normal' | 'high'`, or a nice number (integer -20~19). Lower priority keeps the machine responsive while a long batch occupies all cores; invalid values throw before any task folder is created.
296
+ - **`progressBar`** — `true` by default: shows a live progress bar in the terminal; set `false` to fall back to plain log output.
295
297
  - **`autoCloseAfterCompletion`** — set `true` to kill workers and exit the process when no tasks are left; default is `false`, i.e. workers stay alive waiting for dynamically created tasks.
296
- - **`shouldTerminate(info)`** — return `true` to terminate the whole process.
298
+ - **`shouldTerminate(info)`** — return `true` to terminate the whole process. If it throws, the error is logged and the task is dispatched anyway (treated as "don't terminate").
297
299
  - **`setSysListener(event, payload, meta)`** — optional, handle custom system events (sent via `helper.emitSys`) on the master; `meta` has `fromWorkerId` and `fromWorkerPid`.
298
- - **`onFinish(report, helper)`** — called once after all workers exit (only when `autoCloseAfterCompletion` is `true`). `report` summarizes the run: `{ taskId, taskFolder, startTimestamp, endTimestamp, cost, total, succeeded, failed, failedTasks, workers }` — `cost` is the run duration in ms (a resumed run measures only the current round); `failedTasks` lists the failed tasks' subids; `workers` is `{ count, crashed }` — the configured worker count and how many workers crashed and were replaced. `helper` lets you walk every persisted task result:
299
- - **`helper.foreachSuccResult((count, task, result) => {})`** — iterate all succeeded results (`results/succ`); `count` is a 0-based index, `task` is the original task object (`null` if its task file is missing or unparseable), `result` is the value returned by `processTask` (`undefined` when the task returned nothing).
300
- - **`helper.foreachErrorResult((count, task, err) => {})`** — iterate all failed results (`results/errors`); `err` is the persisted error object as-is (`{ex}` / `{subTask, exception}` / `{type: 'timeout', ...}` / `{type: 'worker_crash', ...}`).
300
+ - **`onFinish(report, helper)`** — called once after all workers exit (only when `autoCloseAfterCompletion` is `true`). `report` summarizes the run: `{ taskId, taskFolder, startTimestamp, endTimestamp, cost, total, succeeded, failed, failedTasks, succButSaveFailed, workers }` — `cost` is the run duration in ms (a resumed run measures only the current round); `failedTasks` lists the failed tasks' subids; `succButSaveFailed` counts tasks that ran fine but whose results could not be JSON-serialized (details in `<taskFolder>/.sys/user_messages/`); `workers` is `{ count, crashed }` — the configured worker count and how many workers crashed and were replaced. `helper` lets you walk every persisted task result:
301
+ - **`helper.foreachSuccResult((count, task, result) => {})`** — iterate all succeeded results (`results/succ`); `count` is a 0-based index, `task` is the original task object (`null` if its task file is missing or unparseable), `result` is the value returned by `processTask` (`undefined` when the task returned nothing; a `{type: 'result_serialize_error', message}` placeholder when the returned value could not be JSON-serialized).
302
+ - **`helper.foreachErrorResult((count, task, err) => {})`** — iterate all failed results (`results/errors`); `err` is the persisted error object as-is (`{ex}` / `{subTask, exception}` / `{type: 'timeout', ...}` / `{type: 'worker_crash', ...}`). A non-`Error` rejection value that is not JSON-serializable is persisted as `{ex: {type: 'unserializable_error_value', ...}}`.
301
303
  - Iteration follows filesystem order (not sorted). `onFinish` runs synchronously at the very end and the master process exits shortly after it returns — don't `await` inside it.
302
304
 
303
305
  **Other options:**
@@ -313,6 +315,8 @@ The API has three parts: the entry functions, the config options, and the `helpe
313
315
 
314
316
  ### Changelog:
315
317
 
318
+ - 3.3.0 Unserializable results still succeed; shouldTerminate throwing is safe; progressBar counters added
319
+ - 3.2.3 Support workerPriority: set OS scheduling priority for worker processes
316
320
  - 3.2.2 initialTasks accepts a function (sync or async) that is called only once, in the master process
317
321
  - 3.2.1 Fix retry bug: user-defined taskCount/index no longer overwritten on task retries
318
322
  - 3.2.0 Add progressBar (on by default); console output diverted to run.log; new startup banner
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "multi-tasks",
3
- "version": "3.2.2",
3
+ "version": "3.3.0",
4
4
  "description": "Multi-process task scheduling based on Node.js cluster, with crash resume and failed-task restart support",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -2,7 +2,7 @@
2
2
  //Multithread 只调语义化方法(setup/workerStarted/taskAssigned/...), 不碰渲染细节。
3
3
  //纯 master 端运行时状态, 不进快照不进 IPC; 统计数据(getStats/getRemaining)由调用方注入, 本模块不依赖 TaskMgr。
4
4
  //console 劫持与 run.log 已与 progressBar 解耦(永久生效, 见 utils/runLog), 不再是本模块的职责。
5
- //单行进度条(progressView): worker 增删不体现在渲染上, 这里只维护 busyWorkers 计数 running。
5
+ //两行进度区(progressView): worker 增删不体现在渲染上, 这里只维护 busyWorkers 计数 running。
6
6
  const progressView = require('./progressView');
7
7
 
8
8
  let RENDER_INTERVAL = 100;//渲染节流: 高频小任务场景避免每次事件都 readdirSync(getFinalStats) 造成 O(N²) 盘扫
@@ -14,12 +14,34 @@ let getStats = null;//() => TaskMgr.getFinalStats(), setup 注入
14
14
  let getRemaining = null;//() => new 目录任务数, setup 注入
15
15
  let lastRenderAt = 0;
16
16
  let renderTimer = null;
17
+ let resizeStream = null;//resize 监听挂载的 stream, reset 时摘除防累积
18
+ let resizeListener = null;
19
+ let outStream = null;//渲染目标 stream, 供列宽变化检测(resize 事件缺失的终端靠它轮询)
20
+ let lastCols = 0;//上一帧使用的列宽, 0 = 尚未渲染
21
+ let resizeSettleTimer = null;
22
+
23
+ const RESIZE_SETTLE = 200;//列宽变化防抖: 拖动窗口会连续变化, 等稳定再重锚定重绘一次(防连续重锚定刷屏)
24
+
25
+ const currentCols = () => {
26
+ return (outStream && outStream.columns) || 80;
27
+ };
17
28
 
18
29
  const reset = () => {//master start 入口/同进程再跑一轮时调用: 清掉上一轮全部残留(含未发的节流帧)
19
30
  if (renderTimer) {
20
31
  clearTimeout(renderTimer);
21
32
  renderTimer = null;
22
33
  };
34
+ if (resizeSettleTimer) {
35
+ clearTimeout(resizeSettleTimer);
36
+ resizeSettleTimer = null;
37
+ };
38
+ if (resizeStream && resizeListener) {
39
+ resizeStream.removeListener('resize', resizeListener);
40
+ };
41
+ resizeStream = null;
42
+ resizeListener = null;
43
+ outStream = null;
44
+ lastCols = 0;
23
45
  view = null;
24
46
  progressOn = false;
25
47
  busyWorkers = {};
@@ -42,7 +64,17 @@ const setup = ({ config, stream, getStats: statsFn, getRemaining: remainingFn })
42
64
  progressOn = true;
43
65
  getStats = statsFn;
44
66
  getRemaining = remainingFn;
45
- view = progressView.create({ stream, barWidth: 30 });
67
+ view = progressView.create({ stream });//两行进度区: bar 行固定 36 列(见 progressView)
68
+ outStream = stream;
69
+ if (typeof stream.on === 'function') {//resize 事件仅作加速路径: 真正的检测在 render 时比对列宽(事件缺失的终端也自愈)
70
+ resizeStream = stream;
71
+ resizeListener = () => {
72
+ if (view) {
73
+ scheduleResizeSettle();
74
+ };
75
+ };
76
+ stream.on('resize', resizeListener);
77
+ };
46
78
  return true;
47
79
  };
48
80
 
@@ -51,18 +83,42 @@ const isOn = () => {
51
83
  };
52
84
 
53
85
  const doRender = () => {
54
- //完成数按盘上结果目录统计: 重试/超时/崩溃各种路径天然正确, 无需在协议里额外计数
86
+ //完成数按盘上结果目录统计: 重试/超时/崩溃各种路径天然正确, 无需在协议里额外计数;
87
+ //failed/succ-but-save-failed 同源实时挂在行尾, 全程可见
55
88
  let stats = getStats();
56
89
  let done = stats.succeeded + stats.failed;
57
90
  let running = Object.keys(busyWorkers).length;
58
91
  lastRenderAt = Date.now();
59
- view.render({ done, running, remaining: getRemaining() });
92
+ lastCols = currentCols();
93
+ view.render({ done, succ: stats.succeeded, running, remaining: getRemaining(), failed: stats.failed, succButSaveFailed: stats.succButSaveFailed || 0 });
94
+ };
95
+
96
+ const scheduleResizeSettle = () => {//列宽变化(事件或 render 时检测): 防抖等宽度稳定, 再重锚定 + 重绘一次
97
+ if (resizeSettleTimer) {
98
+ clearTimeout(resizeSettleTimer);
99
+ };
100
+ resizeSettleTimer = setTimeout(() => {
101
+ resizeSettleTimer = null;
102
+ if (view) {
103
+ view.reanchor();
104
+ doRender();
105
+ };
106
+ }, RESIZE_SETTLE);
107
+ resizeSettleTimer.unref();//不阻碍 master 退出
108
+ };
109
+
110
+ const colsChanged = () => {//列宽变了 = 旧帧已被终端重排, 占用行数不可信: 任何光标序列重绘必错位
111
+ return lastCols > 0 && currentCols() !== lastCols;
60
112
  };
61
113
 
62
114
  const render = () => {
63
115
  if (!view) {
64
116
  return;
65
117
  };
118
+ if (colsChanged()) {//原地重绘必错位, 走防抖重锚定
119
+ scheduleResizeSettle();
120
+ return;
121
+ };
66
122
  if (renderTimer) {
67
123
  return;//已挂着一个尾随渲染, 合并掉中间帧
68
124
  };
@@ -73,14 +129,19 @@ const render = () => {
73
129
  };
74
130
  renderTimer = setTimeout(() => {
75
131
  renderTimer = null;
76
- if (view) {
77
- doRender();
132
+ if (!view) {
133
+ return;
134
+ };
135
+ if (colsChanged()) {//拖动期间尾随帧触发: 用旧列宽光标序列重绘是满屏碎片根因, 转防抖重锚定
136
+ scheduleResizeSettle();
137
+ return;
78
138
  };
139
+ doRender();
79
140
  }, wait);
80
141
  renderTimer.unref();//不阻碍 master 退出(最后一帧可能因进程退出丢失, 可接受)
81
142
  };
82
143
 
83
- const workerStarted = (id, pid) => {//单行进度条不展示 worker 行: 仅保留语义入口, 无渲染动作
144
+ const workerStarted = (id, pid) => {//两行进度区不展示 worker 行: 仅保留语义入口, 无渲染动作
84
145
  };
85
146
 
86
147
  const taskAssigned = (id, subid) => {
@@ -99,9 +160,17 @@ const workerExited = (id) => {
99
160
  render();
100
161
  };
101
162
 
102
- const finish = () => {//收尾: 换行定格进度条行(master 退出前调用); 未开启为 no-op
163
+ const finish = () => {//收尾: 换行定格进度条行 + 输出计数器行(master 退出前调用); 未开启为 no-op
164
+ if (resizeSettleTimer) {//收尾后不再重锚定重绘: 防抖帧会落在计数器行之后
165
+ clearTimeout(resizeSettleTimer);
166
+ resizeSettleTimer = null;
167
+ };
103
168
  if (view) {
104
169
  view.end();
170
+ if (getStats) {
171
+ let stats = getStats();
172
+ view.writeLine(`[Tasks] succeeded=${stats.succeeded}, failed=${stats.failed}, succ-but-save-failed=${stats.succButSaveFailed || 0}`);
173
+ };
105
174
  };
106
175
  };
107
176
 
@@ -1,35 +1,102 @@
1
- //终端进度条渲染器(零依赖): 单行进度条, 每帧 \r 回行首 + 清行 + 重写, 不上移光标、不换行。
2
- //纯渲染, 不碰 cluster/fs。单行形态对"两帧之间有外部写入共享终端"鲁棒:
3
- //旧帧最多留一行滚动残留, 下一帧在当前行自愈(多行版靠 ESC[nA 数行, 外部写入挤偏光标会让旧帧永久滞留)。
4
- const ESC = '';
1
+ //终端进度条渲染器(零依赖): 两行进度区——上行 bar+百分比固定 36 列(约 18 个中文字符宽, 不随终端宽度变), 下行实时计数(动态宽, 封顶 cols-1)。
2
+ //bar 行短则拖动窗口时几乎不触发终端折行重排(reflow), ESC[1A 数行更可信(占满整行方案在 conpty 重排下
3
+ //反复错位, 用户拍板改短行)。首帧直接写两行(无光标控制); 后续帧 \r 回行首 + 清行 + ESC[1A 上移一行 + 清行 + 重写两行。
4
+ //resize 重锚定(reanchor)是抹除式: 终端重排(reflow)后旧帧占用行数不再固定为 2, 按记录的上帧
5
+ //两行可见长度 ÷ 当前列宽算出实际占用行数, 上移回旧帧首行 + ESC[0J 清到屏尾, 下一帧从锚点重写——
6
+ //不做换行定格: 定格会把重排碎片留在滚动区, 连续拖动即满屏碎片(用户否掉的旧方案)。
7
+ //已知取舍: 两帧之间若有外部写入共享终端, ESC[1A 数行会被挤偏, 旧帧可能滞留残留(用户确认接受)。
8
+ const ESC = '\x1B';
9
+ const fs = require('fs');
10
+ const RED = '\x1B[31m';
11
+ const GREEN = '\x1B[32m';
12
+ const YELLOW = '\x1B[33m';
13
+ const RESET_FG = '\x1B[39m';
5
14
 
6
- const create = ({ stream, barWidth }) => {
15
+ const BAR_OVERHEAD = 10;//'[' + '] ' + 6 位百分比 + '%'
16
+ const BAR_COLS = 36;//bar 行固定 36 列(约 18 个中文字符宽, 用户拍板, 不随终端宽度变): 行短则拖动窗口时几乎不触发折行重排; info 行保持动态宽(计数器全显)
17
+
18
+ //仅渲染帧上色(writeLine/run.log 永远无色): bar 完成段绿 / done 数绿 / succ 数绿 / running 数黄 / failed 整词红 / succ-but-save-failed 数黄,
19
+ //为 0 不着色减少噪音。必须在按列宽截断之后调用: 颜色码不占可见宽度, 也不会被截断拆开
20
+ const colorizeFrame = (line) => {
21
+ return line
22
+ .replace(/\[([█]+)/, (m, blocks) => `[${GREEN}${blocks}${RESET_FG}`)
23
+ .replace(/(\d+) failed/, (m, n) => (n === '0' ? m : `${RED}${n} failed${RESET_FG}`))
24
+ .replace(/(\d+) succ-but-save-failed/, (m, n) => (n === '0' ? m : `${YELLOW}${n}${RESET_FG} succ-but-save-failed`))
25
+ .replace(/(\d+) running/, (m, n) => (n === '0' ? m : `${YELLOW}${n}${RESET_FG} running`))
26
+ .replace(/(\d+) succ(?=,|$)/, (m, n) => (n === '0' ? m : `${GREEN}${n}${RESET_FG} succ`))//(?=,|$) 防误配 succ-but-save-failed 的词头
27
+ .replace(/(\d+)\/(\d+) done/, (m, done, total) => (done === '0' ? m : `${GREEN}${done}${RESET_FG}/${total} done`));
28
+ };
29
+
30
+ const create = ({ stream }) => {
7
31
  if (!stream) { throw new Error('progressView needs a stream'); };
8
- const width = barWidth || 30;
32
+ let frameCount = 0;
33
+ let ended = false;
34
+ let lastBarLen = 0;//上帧两行可见长度(截断后、上色前): resize 重排后算旧帧实际占用行数用
35
+ let lastInfoLen = 0;
36
+
37
+ //TTY 现场排查开关: MULTI_TASKS_PROGRESS_DEBUG=文件路径 时, 每帧字节流+当时列宽记成 JSON 行,
38
+ //供 test/terminalSim.js 回放比对真实终端行为(仅排查用, 平时为空不写盘)
39
+ const debugPath = process.env.MULTI_TASKS_PROGRESS_DEBUG;
40
+ const write = (s) => {
41
+ if (debugPath) {
42
+ fs.appendFileSync(debugPath, `${JSON.stringify({ at: Date.now(), cols: stream.columns || 80, data: s })}\n`);
43
+ };
44
+ stream.write(s);
45
+ };
9
46
 
10
- const buildBarLine = ({ done, running, remaining }) => {
47
+ const buildLines = ({ done, succ, running, remaining, failed, succButSaveFailed }) => {
48
+ //行比终端列宽还长会折行、光标定位错乱: 每行封顶 cols-1(截断后才上色);
49
+ //留 1 空列: 写满整列会触发终端满列换行(deferred wrap), resize 重排时 ESC[1A 数行失准;
50
+ //bar 行固定 BAR_COLS 列(窄终端整行截断, 行尾百分比会被截掉): 行短则拖动窗口时几乎不触发折行重排
51
+ const cols = stream.columns || 80;
52
+ const maxLineLen = Math.max(1, cols - 1);
53
+ const truncate = (line) => (line.length > maxLineLen ? line.slice(0, maxLineLen) : line);
11
54
  let total = done + running + remaining;
55
+ let width = BAR_COLS - BAR_OVERHEAD;//固定 26 格, 不随终端宽度变
12
56
  let filled = total > 0 ? Math.floor(width * done / total) : width;
13
57
  let bar = '█'.repeat(filled) + '░'.repeat(width - filled);
14
58
  let pct = (total > 0 ? 100 * done / total : 100).toFixed(2).padStart(6);//两位小数, 定宽防行长抖动
15
- return `[${bar}] ${pct}% ${done}/${total} done, ${running} running`;
59
+ //下行实时计数器(done/succ/running/failed/succ-but-save-failed), 与收尾计数器行同源(都出自 getFinalStats)
60
+ let barLine = `[${bar}] ${pct}%`;
61
+ let infoLine = `${done}/${total} done, ${succ || 0} succ, ${running} running, ${failed || 0} failed, ${succButSaveFailed || 0} succ-but-save-failed`;
62
+ return [truncate(barLine), truncate(infoLine)];
63
+ };
64
+
65
+ const render = (data) => {
66
+ let [barLine, infoLine] = buildLines(data);
67
+ lastBarLen = barLine.length;
68
+ lastInfoLen = infoLine.length;
69
+ let frame = `${colorizeFrame(barLine)}\n${colorizeFrame(infoLine)}`;
70
+ if (frameCount === 0) {
71
+ write(frame);
72
+ } else {
73
+ write(`\r${ESC}[2K${ESC}[1A${ESC}[2K${frame}`);
74
+ };
75
+ frameCount++;
76
+ };
77
+
78
+ const end = () => {//收尾换行: 进度区定格, 后续输出(shell 提示符等)落在下一行
79
+ ended = true;
80
+ write('\n');
16
81
  };
17
82
 
18
- const render = ({ done, running, remaining }) => {
19
- //行比终端列宽还长会折行、\r 回不到帧首: 按列宽截断
20
- let cols = stream.columns || 80;
21
- let line = buildBarLine({ done, running, remaining });
22
- if (line.length > cols) {
23
- line = line.slice(0, cols);
83
+ const reanchor = () => {//终端 resize 重排后旧帧行数不可信: 按上帧行长 ÷ 当前列宽算出实际占用行数,
84
+ //上移回旧帧首行并 ESC[0J 清到屏尾(抹除旧帧, 不留定格碎片), 下一帧从该锚点按首帧重写
85
+ if (ended || frameCount === 0) {
86
+ return;
24
87
  };
25
- stream.write(`\r${ESC}[2K${line}`);
88
+ const cols = stream.columns || 80;
89
+ const wrapRows = (len) => Math.max(1, Math.ceil(len / cols));
90
+ const rowsAbove = wrapRows(lastBarLen) + wrapRows(lastInfoLen) - 1;
91
+ write(`\r${ESC}[${rowsAbove}A${ESC}[0J`);
92
+ frameCount = 0;
26
93
  };
27
94
 
28
- const end = () => {//收尾换行: 进度条行定格, 后续输出(shell 提示符等)落在下一行
29
- stream.write('\n');
95
+ const writeLine = (text) => {//定格后整行输出(计数器行等): 纯文本一行, 不进 run.log(ASCII 契约之外的字符只走 render)
96
+ write(`${text}\n`);
30
97
  };
31
98
 
32
- return { render, end };
99
+ return { render, end, reanchor, writeLine };
33
100
  };
34
101
 
35
102
  module.exports = { create };
@@ -0,0 +1,37 @@
1
+ 'use strict';
2
+
3
+ const os = require('os');
4
+
5
+ const PRIORITY_NAMES = {
6
+ low: os.constants.priority.PRIORITY_LOW,
7
+ below_normal: os.constants.priority.PRIORITY_BELOW_NORMAL,
8
+ normal: os.constants.priority.PRIORITY_NORMAL,
9
+ above_normal: os.constants.priority.PRIORITY_ABOVE_NORMAL,
10
+ high: os.constants.priority.PRIORITY_HIGH,
11
+ };
12
+
13
+ // 解析 workerPriority 配置:
14
+ // - undefined -> 返回 undefined(不设置优先级, 行为不变);
15
+ // - string -> 枚举 'low'|'below_normal'|'normal'|'above_normal'|'high'(允许首尾空白),
16
+ // 映射 os.constants.priority 常量;
17
+ // - number -> 须为 -20~19 的整数(os.setPriority 原生 nice 范围), 原样返回;
18
+ // - 其余 -> 抛 Error。
19
+ const resolveWorkerPriority = (value) => {
20
+ if (typeof value === 'undefined') {
21
+ return undefined;
22
+ };
23
+ if (typeof value === 'string') {
24
+ const name = value.trim();
25
+ if (Object.prototype.hasOwnProperty.call(PRIORITY_NAMES, name)) {
26
+ return PRIORITY_NAMES[name];
27
+ };
28
+ };
29
+ if (typeof value === 'number') {
30
+ if (Number.isInteger(value) && value >= -20 && value <= 19) {
31
+ return value;
32
+ };
33
+ };
34
+ throw new Error(`Invalid workerPriority: ${JSON.stringify(value)}. Expected 'low' | 'below_normal' | 'normal' | 'above_normal' | 'high', or an integer between -20 and 19.`);
35
+ };
36
+
37
+ module.exports = { resolveWorkerPriority };