@mui/internal-docs-infra
Version:
MUI Infra - internal documentation creation tools.
191 lines (187 loc) • 6.45 kB
JavaScript
// webpack does not like node: imports
// eslint-disable-next-line n/prefer-node-protocol
import path from 'path';
// eslint-disable-next-line n/prefer-node-protocol
import { fileURLToPath } from 'url';
// eslint-disable-next-line n/prefer-node-protocol
import { isMainThread, Worker } from 'worker_threads';
import { SocketClient, tryAcquireServerLock, releaseServerLock, waitForSocketFile } from "./socketClient.mjs";
/**
* Shared interface for types processing managers.
*/
/**
* Spawns a dedicated worker thread for TypeScript type processing.
* Used from the main thread (webpack/Next.js) where the nested worker runs
* processTypes and handles socket server/client election internally.
*/
class TypesMetaWorkerManager {
worker = null;
pendingRequests = new Map();
requestId = 0;
constructor(socketDir) {
// Worker file must be compiled JS, not TS
// Use import.meta.url to get current directory in ESM
const currentDir = path.dirname(fileURLToPath(import.meta.url));
this.workerPath = path.join(currentDir, 'worker.mjs');
this.socketDir = socketDir;
}
ensureWorker() {
if (!this.worker) {
this.worker = new Worker(this.workerPath, {
workerData: this.socketDir ? {
socketDir: this.socketDir
} : undefined
});
this.worker.on('message', response => {
const {
requestId,
...rest
} = response;
if (requestId !== undefined && this.pendingRequests.has(requestId)) {
const resolve = this.pendingRequests.get(requestId);
this.pendingRequests.delete(requestId);
resolve(rest);
}
});
this.worker.on('error', error => {
console.error('[TypesMetaWorker] Worker error:', error);
// Reject all pending requests
this.pendingRequests.forEach(resolve => {
resolve({
success: false,
error: `Worker error: ${error.message}`
});
});
this.pendingRequests.clear();
this.worker = null;
});
this.worker.on('exit', code => {
if (code !== 0) {
console.error(`[TypesMetaWorker] Worker stopped with exit code ${code}`);
}
this.worker = null;
});
}
return this.worker;
}
async processTypes(request) {
const worker = this.ensureWorker();
const currentRequestId = this.requestId;
this.requestId += 1;
return new Promise(resolve => {
this.pendingRequests.set(currentRequestId, resolve);
worker.postMessage({
...request,
requestId: currentRequestId
});
});
}
terminate() {
if (this.worker) {
this.worker.terminate();
this.worker = null;
}
this.pendingRequests.clear();
}
}
/**
* Types processor for validate worker threads.
* On first processTypes() call, races to acquire the server lock:
* - Winner: releases the lock, spawns a bare worker (which acquires the lock
* naturally and becomes the socket server via existing worker.ts logic)
* - Losers: skip spawning
*
* All workers then connect to the socket server as clients.
* Result: N validate workers + 1 types server worker = N+1 threads total.
*/
class WorkerThreadTypesProcessor {
initPromise = null;
socketClient = null;
serverWorker = null;
constructor(socketDir) {
this.socketDir = socketDir;
}
ensureInit() {
if (!this.initPromise) {
this.initPromise = this.init().catch(error => {
// Reset so the next processTypes() call can retry
this.initPromise = null;
throw error;
});
}
return this.initPromise;
}
async init() {
const isServer = await tryAcquireServerLock(this.socketDir);
if (isServer) {
// We won the lock — spawn the bare worker which will become a socket server.
// Keep the lock held so no other worker tries to spawn a second server.
const currentDir = path.dirname(fileURLToPath(import.meta.url));
const workerPath = path.join(currentDir, 'worker.mjs');
this.serverWorker = new Worker(workerPath, {
workerData: {
isServer: true,
...(this.socketDir && {
socketDir: this.socketDir
})
}
});
this.serverWorker.on('error', error => {
console.error('[WorkerThreadTypesProcessor] Server worker error:', error);
});
try {
// Wait for the socket file to appear, then release the lock.
await waitForSocketFile(this.socketDir, 30_000);
} catch (error) {
// Server worker crashed before creating the socket — release the lock
// so another worker can become the server on a subsequent attempt.
await releaseServerLock();
throw error;
}
await releaseServerLock();
} else {
// Another worker is the server — wait for the socket file.
await waitForSocketFile(this.socketDir, 30_000);
}
this.socketClient = new SocketClient(this.socketDir);
await this.socketClient.connect();
}
async processTypes(request) {
await this.ensureInit();
return this.socketClient.sendRequest(request);
}
terminate() {
if (this.socketClient) {
this.socketClient.close();
this.socketClient = null;
}
if (this.serverWorker) {
this.serverWorker.terminate();
this.serverWorker = null;
}
}
}
// Use process global to ensure singleton persists across all Turbopack module contexts
// In Turbopack dev mode, each compilation can have separate globalThis contexts,
// but they all share the same Node.js process object
const WORKER_MANAGER_KEY = Symbol.for('@mui/docs-infra/types-meta-worker-manager');
export function getWorkerManager(socketDir) {
const processObj = process;
if (!processObj[WORKER_MANAGER_KEY]) {
if (isMainThread) {
// Main thread (webpack/Next.js): spawn a worker that does lock election internally
processObj[WORKER_MANAGER_KEY] = new TypesMetaWorkerManager(socketDir);
} else {
// Worker thread (validate workers): do lock election here to avoid nested workers
processObj[WORKER_MANAGER_KEY] = new WorkerThreadTypesProcessor(socketDir);
}
}
return processObj[WORKER_MANAGER_KEY];
}
export function terminateWorkerManager() {
const processObj = process;
if (processObj[WORKER_MANAGER_KEY]) {
processObj[WORKER_MANAGER_KEY].terminate();
processObj[WORKER_MANAGER_KEY] = undefined;
}
}