@minecraft/creator-tools
Version:
Minecraft Creator Tools command line and libraries.
289 lines (288 loc) • 9.98 kB
JavaScript
;
/**
* WorkerPool - Abstraction for parallel task execution
*
* This module provides:
* - IWorkerPool interface (from ICommandContext)
* - ThreadWorkerPool: Node.js worker_threads implementation
* - SingleThreadPool: Executes tasks via worker but one at a time
*
* Both pools use the same TaskWorker.js script for execution,
* ensuring consistent behavior between single and multi-threaded modes.
*
* The factory function createWorkerPool() selects the appropriate
* implementation based on thread count.
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.ThreadWorkerPool = exports.SingleThreadPool = void 0;
exports.createWorkerPool = createWorkerPool;
const worker_threads_1 = require("worker_threads");
const path = __importStar(require("path"));
/**
* Get the path to the TaskWorker.mjs script.
* The TaskWorker.mjs is in the same directory as the main CLI bundle.
*/
function getWorkerPath() {
return path.resolve(__dirname, "TaskWorker.mjs");
}
/**
* SingleThreadPool executes tasks one at a time using a single worker.
* This provides consistent behavior with ThreadWorkerPool but without parallelism.
*/
class SingleThreadPool {
concurrency = 1;
workerPath;
memoryLimitMb;
constructor(workerPath, memoryLimitMb = 16384) {
this.workerPath = workerPath || getWorkerPath();
this.memoryLimitMb = memoryLimitMb;
}
async execute(task) {
return executeInWorker(task, this.workerPath, this.memoryLimitMb);
}
async executeBatch(tasks, onProgress) {
const results = [];
for (let i = 0; i < tasks.length; i++) {
const result = await this.execute(tasks[i]);
results.push(result);
if (onProgress) {
onProgress(i + 1, tasks.length);
}
}
return results;
}
async shutdown() {
// Nothing to clean up - workers are created per-task
}
}
exports.SingleThreadPool = SingleThreadPool;
/**
* ThreadWorkerPool uses Node.js worker_threads for parallel execution.
*/
class ThreadWorkerPool {
concurrency;
workerPath;
memoryLimitMb;
activeWorkers = new Set();
isShutdown = false;
/**
* Create a thread worker pool.
* @param concurrency Maximum concurrent workers
* @param workerPath Path to the worker script (TaskWorker.js)
* @param memoryLimitMb Memory limit per worker in MB
*/
constructor(concurrency, workerPath, memoryLimitMb = 16384) {
this.concurrency = Math.max(1, Math.min(concurrency, 8));
this.workerPath = workerPath || getWorkerPath();
this.memoryLimitMb = memoryLimitMb;
}
async execute(task) {
if (this.isShutdown) {
return {
success: false,
error: "Worker pool has been shutdown",
};
}
return new Promise((resolve) => {
const worker = new worker_threads_1.Worker(this.workerPath, {
resourceLimits: {
maxOldGenerationSizeMb: this.memoryLimitMb,
},
});
this.activeWorkers.add(worker);
const cleanup = () => {
worker.removeAllListeners();
this.activeWorkers.delete(worker);
worker.terminate();
};
const onMessage = (result) => {
cleanup();
// Handle error strings returned from worker
if (typeof result === "string" && result.startsWith("Error")) {
resolve({
success: false,
error: result,
});
}
else {
resolve({
success: true,
result: result,
});
}
};
const onError = (error) => {
cleanup();
resolve({
success: false,
error: error.message,
stack: error.stack,
});
};
const onExit = (code) => {
cleanup();
if (code !== 0) {
resolve({
success: false,
error: `Worker exited with code ${code}`,
});
}
};
worker.on("message", onMessage);
worker.on("error", onError);
worker.on("exit", onExit);
// Post the task data to the worker
worker.postMessage({
task: task.taskType,
...task.args,
});
});
}
async executeBatch(tasks, onProgress) {
if (this.isShutdown) {
return tasks.map(() => ({
success: false,
error: "Worker pool has been shutdown",
}));
}
const results = new Array(tasks.length);
let currentIndex = 0;
let completedCount = 0;
const processNextBatch = async () => {
const batchPromises = [];
const batchIndices = [];
// Start up to concurrency tasks
for (let i = 0; i < this.concurrency && currentIndex < tasks.length; i++) {
const taskIndex = currentIndex++;
batchIndices.push(taskIndex);
const promise = this.execute(tasks[taskIndex]).then((result) => {
results[taskIndex] = result;
completedCount++;
if (onProgress) {
onProgress(completedCount, tasks.length);
}
});
batchPromises.push(promise);
}
await Promise.all(batchPromises);
};
// Process all tasks in batches
while (currentIndex < tasks.length) {
await processNextBatch();
}
return results;
}
async shutdown() {
this.isShutdown = true;
const terminatePromises = [];
for (const worker of this.activeWorkers) {
terminatePromises.push(worker.terminate());
}
await Promise.all(terminatePromises);
this.activeWorkers.clear();
}
}
exports.ThreadWorkerPool = ThreadWorkerPool;
/**
* Execute a single task in a worker and return the result.
* This is a helper used by both SingleThreadPool and ThreadWorkerPool.
*/
async function executeInWorker(task, workerPath, memoryLimitMb) {
return new Promise((resolve) => {
const worker = new worker_threads_1.Worker(workerPath, {
resourceLimits: {
maxOldGenerationSizeMb: memoryLimitMb,
},
});
const cleanup = () => {
worker.removeAllListeners();
worker.terminate();
};
const onMessage = (result) => {
cleanup();
// Handle error strings returned from worker
if (typeof result === "string" && result.startsWith("Error")) {
resolve({
success: false,
error: result,
});
}
else {
resolve({
success: true,
result: result,
});
}
};
const onError = (error) => {
cleanup();
resolve({
success: false,
error: error.message,
stack: error.stack,
});
};
const onExit = (code) => {
cleanup();
if (code !== 0) {
resolve({
success: false,
error: `Worker exited with code ${code}`,
});
}
};
worker.on("message", onMessage);
worker.on("error", onError);
worker.on("exit", onExit);
// Post the task data to the worker
// Convert IWorkerTask to the format expected by TaskWorker
worker.postMessage({
task: task.taskType,
...task.args,
});
});
}
/**
* Factory function to create the appropriate worker pool.
* @param threads Number of threads (1 = single-threaded, >1 = multi-threaded)
* @param workerPath Optional path to worker script (defaults to TaskWorker.js)
*/
function createWorkerPool(threads, workerPath) {
if (threads <= 1) {
return new SingleThreadPool(workerPath);
}
return new ThreadWorkerPool(threads, workerPath);
}