@mui/internal-docs-infra
Version:
MUI Infra - internal documentation creation tools.
188 lines (177 loc) • 5.61 kB
JavaScript
/**
* Socket server for the first TypeScript language service worker
*
* The first worker to start creates a socket server that other workers can connect to.
* This allows sharing a single TypeScript language service across all Next.js worker processes.
*
* On Unix systems, this uses Unix domain sockets.
* On Windows, this uses named pipes (which Node.js net module supports transparently).
*/
import { createServer } from 'node:net';
import { unlink, stat } from 'node:fs/promises';
import { getSocketPath, ensureSocketDir } from "./socketClient.mjs";
const isWindows = process.platform === 'win32';
/**
* Check if a file exists
*/
async function fileExists(path) {
try {
await stat(path);
return true;
} catch {
return false;
}
}
/**
* Socket server that handles requests from other workers
*/
export class SocketServer {
connections = new Set();
/**
* Request queue to serialize processTypes calls.
* The TypeScript language service is a singleton that uses setRootFiles()
* on each call — concurrent requests would thrash the program cache.
*/
requestQueue = Promise.resolve();
constructor(requestHandler, socketPath, server) {
this.socketPath = socketPath;
this.requestHandler = requestHandler;
this.server = server;
}
/**
* Create and initialize a socket server
*/
static async create(requestHandler, socketDir) {
const socketPath = getSocketPath(socketDir);
// Ensure the directory exists (only needed for Unix sockets)
if (!isWindows) {
// Unix domain sockets have a max path length (sun_path field in sockaddr_un):
// Linux: 108 bytes, macOS/BSD: 104 bytes
const maxSocketPath = process.platform === 'darwin' ? 104 : 108;
if (Buffer.byteLength(socketPath) >= maxSocketPath) {
throw new Error(`Socket path exceeds the maximum length of ${maxSocketPath} bytes ` + `for this platform (${Buffer.byteLength(socketPath)} bytes): ${socketPath}. ` + `Use a shorter socketDir path or avoid deeply nesting your project directory.`);
}
await ensureSocketDir(socketDir);
// Clean up existing socket file if it exists (might be stale from previous build)
if (await fileExists(socketPath)) {
try {
await unlink(socketPath);
} catch {
// Ignore cleanup errors
}
}
}
const server = createServer();
const instance = new SocketServer(requestHandler, socketPath, server);
server.on('connection', socket => {
instance.handleConnection(socket);
});
server.on('error', error => {
console.error('[SocketServer] Server error:', error);
});
return instance;
}
/**
* Start the socket server
*/
start() {
return new Promise((resolve, reject) => {
this.server.listen(this.socketPath, () => {
resolve();
});
this.server.on('error', reject);
});
}
/**
* Handle incoming client connection
*/
handleConnection(socket) {
this.connections.add(socket);
let buffer = '';
socket.on('data', data => {
buffer += data.toString();
// Process complete messages (delimited by newlines)
const messages = buffer.split('\n');
buffer = messages.pop() || '';
for (const messageStr of messages) {
if (!messageStr.trim()) {
continue;
}
try {
const message = JSON.parse(messageStr);
// Enqueue message for serialized processing
this.handleMessage(socket, message);
} catch (error) {
console.error('[SocketServer] Failed to parse message:', error);
this.sendResponse(socket, {
id: 'unknown',
type: 'error',
data: {
error: 'Invalid message format'
}
});
}
}
});
socket.on('end', () => {
this.connections.delete(socket);
});
socket.on('error', error => {
console.error('[SocketServer] Socket error:', error);
this.connections.delete(socket);
});
}
/**
* Handle incoming message from client.
* Requests are serialized through a queue to prevent concurrent processTypes
* calls from thrashing the singleton TypeScript language service.
*/
handleMessage(socket, message) {
if (message.type === 'process-types' && message.data) {
this.requestQueue = this.requestQueue.then(async () => {
try {
const result = await this.requestHandler(message.data);
this.sendResponse(socket, {
id: message.id,
type: 'success',
data: result
});
} catch (error) {
console.error('[SocketServer] Error handling message:', error);
this.sendResponse(socket, {
id: message.id,
type: 'error',
data: {
error: error instanceof Error ? error.message : String(error)
}
});
}
}).catch(error => {
console.error('[SocketServer] Unexpected queue error:', error);
});
}
}
/**
* Send response to client
*/
sendResponse(socket, response) {
socket.write(`${JSON.stringify(response)}\n`);
}
/**
* Shutdown the server
*/
shutdown() {
// Close all connections
this.connections.forEach(socket => {
socket.end();
});
this.server.close(() => {
// Clean up socket file asynchronously (only for Unix sockets)
if (!isWindows) {
unlink(this.socketPath).catch(() => {
// Ignore cleanup errors
});
}
});
}
}