@mui/internal-docs-infra 0.12.1-canary.43 → 0.12.1-canary.44

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.
@@ -1,4 +1,5 @@
1
1
  import type { CommandModule } from 'yargs';
2
+ import { Worker } from 'node:worker_threads';
2
3
  type Args = {
3
4
  paths?: string[];
4
5
  command?: string;
@@ -7,5 +8,7 @@ type Args = {
7
8
  perf?: boolean;
8
9
  notableMs?: number;
9
10
  };
11
+ export declare function shutdownWorker(worker: Worker, timeoutMs?: number): Promise<void>;
12
+ export declare function shutdownWorkers(workers: Worker[]): Promise<void>;
10
13
  declare const runValidate: CommandModule<{}, Args>;
11
14
  export default runValidate;
@@ -16,6 +16,51 @@ import { ensureDemoClients } from "./ensureDemoClients.mjs";
16
16
  import { ensureDemoPages } from "./ensureDemoPages.mjs";
17
17
  const completeMessage = message => `✓ ${chalk.green(message)}`;
18
18
  const functionName = 'Run Validate';
19
+ export async function shutdownWorker(worker, timeoutMs = 5000) {
20
+ if (worker.threadId === -1) {
21
+ return;
22
+ }
23
+ await new Promise(resolve => {
24
+ let handleMessage;
25
+ let settled = false;
26
+ let timeout;
27
+ const finish = () => {
28
+ if (settled) {
29
+ return;
30
+ }
31
+ settled = true;
32
+ if (timeout) {
33
+ clearTimeout(timeout);
34
+ }
35
+ worker.off('message', handleMessage);
36
+ worker.off('error', finish);
37
+ worker.off('exit', finish);
38
+ resolve();
39
+ };
40
+ handleMessage = result => {
41
+ if (result.type === 'shutdown') {
42
+ finish();
43
+ }
44
+ };
45
+ timeout = setTimeout(finish, timeoutMs);
46
+ worker.on('message', handleMessage);
47
+ worker.once('error', finish);
48
+ worker.once('exit', finish);
49
+
50
+ // The worker may have exited between the initial check and listener setup.
51
+ if (worker.threadId === -1) {
52
+ finish();
53
+ return;
54
+ }
55
+ worker.postMessage({
56
+ type: 'shutdown'
57
+ });
58
+ });
59
+ await worker.terminate();
60
+ }
61
+ export async function shutdownWorkers(workers) {
62
+ await Promise.all(workers.map(worker => shutdownWorker(worker)));
63
+ }
19
64
 
20
65
  /**
21
66
  * Recursively find all files matching a specific name in a directory
@@ -143,6 +188,9 @@ const runValidate = {
143
188
  let taskIdCounter = 0;
144
189
  for (const worker of workers) {
145
190
  worker.on('message', result => {
191
+ if (result.type === 'shutdown') {
192
+ return;
193
+ }
146
194
  const resolve = pendingResults.get(result.taskId);
147
195
  if (resolve) {
148
196
  pendingResults.delete(result.taskId);
@@ -407,11 +455,11 @@ const runValidate = {
407
455
  currentMark = pagesMark;
408
456
  }
409
457
  } finally {
410
- // Terminate worker pool
411
- await Promise.all(workers.map(w => w.terminate()));
458
+ // Let each validation worker release its types-server election lock before exit.
459
+ await shutdownWorkers(workers);
412
460
 
413
461
  // Terminate the types meta worker manager to allow the process to exit
414
- terminateWorkerManager();
462
+ await terminateWorkerManager();
415
463
  }
416
464
  if (observer) {
417
465
  // Flush any remaining performance entries before disconnecting
@@ -22,6 +22,9 @@ interface TypesTask {
22
22
  syncTypesOptions: Omit<SyncTypesOptions, 'typesMarkdownPath' | 'rootContext' | 'variants'>;
23
23
  rootContext: string;
24
24
  }
25
+ export interface ShutdownTask {
26
+ type: 'shutdown';
27
+ }
25
28
  interface SerializedPerfEntry {
26
29
  name: string;
27
30
  duration: number;
@@ -42,6 +45,9 @@ interface TypesResult {
42
45
  perfEntries?: SerializedPerfEntry[];
43
46
  error?: string;
44
47
  }
48
+ export interface ShutdownResult {
49
+ type: 'shutdown';
50
+ }
45
51
  export type ValidateTask = IndexTask | TypesTask;
46
52
  export type ValidateResult = IndexResult | TypesResult;
47
53
  export {};
@@ -8,6 +8,7 @@ import remarkMdx from 'remark-mdx';
8
8
  import { transformMarkdownMetadata } from "../pipeline/transformMarkdownMetadata/transformMarkdownMetadata.mjs";
9
9
  import { parseCreateFactoryCall } from "../pipeline/parseCreateFactoryCall/parseCreateFactoryCall.mjs";
10
10
  import { syncTypes } from "../pipeline/syncTypes/syncTypes.mjs";
11
+ import { terminateWorkerManager } from "../pipeline/loadServerTypesMeta/workerManager.mjs";
11
12
  function collectPerfEntries() {
12
13
  const entries = performance.getEntriesByType('measure').map(entry => ({
13
14
  name: entry.name,
@@ -22,7 +23,12 @@ if (parentPort) {
22
23
  let taskQueue = Promise.resolve();
23
24
  parentPort.on('message', task => {
24
25
  taskQueue = taskQueue.then(async () => {
25
- if (task.type === 'index') {
26
+ if (task.type === 'shutdown') {
27
+ await terminateWorkerManager();
28
+ parentPort.postMessage({
29
+ type: 'shutdown'
30
+ });
31
+ } else if (task.type === 'index') {
26
32
  try {
27
33
  const processor = unified().use(remarkParse).use(remarkMdx).use(transformMarkdownMetadata, {
28
34
  extractToIndex: task.processorOptions
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mui/internal-docs-infra",
3
- "version": "0.12.1-canary.43",
3
+ "version": "0.12.1-canary.44",
4
4
  "author": "MUI Team",
5
5
  "description": "MUI Infra - internal documentation creation tools.",
6
6
  "license": "MIT",
@@ -804,5 +804,5 @@
804
804
  "bin": {
805
805
  "docs-infra": "./cli/index.mjs"
806
806
  },
807
- "gitSha": "2a2c232ded2053934aa082b335952c525c6ea88c"
807
+ "gitSha": "428843a303d23c4c904fc740058e33e1a8b95c85"
808
808
  }
@@ -23,15 +23,6 @@ export declare function getLockPath(): string;
23
23
  * Ensure the socket directory exists
24
24
  */
25
25
  export declare function ensureSocketDir(): Promise<void>;
26
- /**
27
- * Wait for the IPC endpoint to become available.
28
- * On Unix: Polls the filesystem for the socket file to appear. We avoid
29
- * `fs.watch` here because on macOS it does not reliably fire events when a
30
- * unix domain socket file is created.
31
- * On Windows: Polls by attempting to connect to the named pipe.
32
- * @param timeoutMs - Timeout in milliseconds (default: 5000)
33
- */
34
- export declare function waitForSocketFile(timeoutMs?: number): Promise<void>;
35
26
  /**
36
27
  * Try to acquire the server lock using proper-lockfile
37
28
  * Returns true if successfully acquired (this worker should be server)
@@ -42,8 +33,7 @@ export declare function tryAcquireServerLock(): Promise<boolean>;
42
33
  */
43
34
  export declare function releaseServerLock(): Promise<void>;
44
35
  /**
45
- * Check if there's an existing worker socket file
46
- * Note: The socket server will clean up stale sockets on startup
36
+ * Check whether a types server is accepting connections, retrying for up to 500ms.
47
37
  */
48
38
  export declare function hasExistingWorker(): Promise<boolean>;
49
39
  /**
@@ -55,9 +45,10 @@ export declare class SocketClient {
55
45
  private pendingRequests;
56
46
  private buffer;
57
47
  /**
58
- * Connect to the worker socket with retry logic
48
+ * Connect to the worker socket, retrying while the server starts or named-pipe
49
+ * instances are temporarily busy.
59
50
  */
60
- connect(retryCount?: number, maxRetries?: number, retryDelay?: number): Promise<void>;
51
+ connect(timeoutMs?: number, retryDelay?: number): Promise<void>;
61
52
  /**
62
53
  * Attempt to connect to the socket
63
54
  */
@@ -9,7 +9,7 @@
9
9
  */
10
10
 
11
11
  import { connect } from 'node:net';
12
- import { mkdir, stat } from 'node:fs/promises';
12
+ import { mkdir } from 'node:fs/promises';
13
13
  import { createHash } from 'node:crypto';
14
14
  import { tmpdir } from 'node:os';
15
15
  import { join } from 'node:path';
@@ -92,22 +92,10 @@ export async function ensureSocketDir() {
92
92
  }
93
93
 
94
94
  /**
95
- * Check if a file exists
96
- */
97
- async function fileExists(path) {
98
- try {
99
- await stat(path);
100
- return true;
101
- } catch {
102
- return false;
103
- }
104
- }
105
-
106
- /**
107
- * Try to connect to a named pipe (Windows)
95
+ * Try to connect to an IPC endpoint.
108
96
  * @returns true if connection succeeded, false otherwise
109
97
  */
110
- function tryConnectToPipe(socketPath) {
98
+ function tryConnectToSocket(socketPath) {
111
99
  return new Promise(resolve => {
112
100
  const socket = connect(socketPath);
113
101
  socket.on('connect', () => {
@@ -130,42 +118,25 @@ function sleep(ms) {
130
118
  }
131
119
 
132
120
  /**
133
- * Wait for the IPC endpoint to become available.
134
- * On Unix: Polls the filesystem for the socket file to appear. We avoid
135
- * `fs.watch` here because on macOS it does not reliably fire events when a
136
- * unix domain socket file is created.
137
- * On Windows: Polls by attempting to connect to the named pipe.
138
- * @param timeoutMs - Timeout in milliseconds (default: 5000)
121
+ * Poll an IPC endpoint until it accepts a connection or the timeout expires.
139
122
  */
140
- export async function waitForSocketFile(timeoutMs = 5000) {
141
- const socketPath = getSocketPath();
123
+ async function waitForSocketConnection(socketPath, timeoutMs) {
142
124
  const pollInterval = 50;
143
125
  const startTime = Date.now();
144
- if (isWindows) {
145
- while (Date.now() - startTime < timeoutMs) {
146
- // eslint-disable-next-line no-await-in-loop
147
- if (await tryConnectToPipe(socketPath)) {
148
- return;
149
- }
150
- // eslint-disable-next-line no-await-in-loop
151
- await sleep(pollInterval);
152
- }
153
- throw new Error(`Named pipe did not become available within ${timeoutMs}ms`);
154
- }
155
-
156
- // Ensure the directory exists so the first stat doesn't fail spuriously
157
- await mkdir(getEffectiveSocketDir(), {
158
- recursive: true
159
- });
160
- while (Date.now() - startTime < timeoutMs) {
126
+ do {
161
127
  // eslint-disable-next-line no-await-in-loop
162
- if (await fileExists(socketPath)) {
163
- return;
128
+ if (await tryConnectToSocket(socketPath)) {
129
+ return true;
130
+ }
131
+ const remainingTime = timeoutMs - (Date.now() - startTime);
132
+ if (remainingTime <= 0) {
133
+ return false;
164
134
  }
135
+
165
136
  // eslint-disable-next-line no-await-in-loop
166
- await sleep(pollInterval);
167
- }
168
- throw new Error(`Socket file did not appear within ${timeoutMs}ms`);
137
+ await sleep(Math.min(pollInterval, remainingTime));
138
+ } while (Date.now() - startTime < timeoutMs);
139
+ return false;
169
140
  }
170
141
 
171
142
  // Store the release function globally so we can call it when needed
@@ -182,14 +153,27 @@ export async function tryAcquireServerLock() {
182
153
  await ensureSocketDir();
183
154
  try {
184
155
  // Try to acquire the lock with no retries (immediate check)
185
- // Stale locks will be detected after 3 seconds (server should start quickly)
156
+ // Allow enough time for a server worker to start even when validation workers
157
+ // saturate the CPU and delay the lock heartbeat.
186
158
  lockReleaseFunction = await lockfile.lock(lockPath, {
187
159
  retries: 0,
188
160
  // Don't retry, just check once
189
- stale: 3000,
190
- // Consider lock stale after 3 seconds
191
- realpath: false // Don't resolve symlinks (file doesn't need to exist)
161
+ stale: 30_000,
162
+ realpath: false,
163
+ // Don't resolve symlinks (file doesn't need to exist)
164
+ onCompromised: error => {
165
+ console.error('[SocketClient] Server lock compromised:', error);
166
+ lockReleaseFunction = null;
167
+ }
192
168
  });
169
+
170
+ // A worker that initialized earlier may have released the election lock after
171
+ // starting the server. Check the endpoint while holding the lock so late
172
+ // workers reuse that server instead of trying to listen on the same address.
173
+ if (await hasExistingWorker()) {
174
+ await releaseServerLock();
175
+ return false;
176
+ }
193
177
  return true;
194
178
  } catch (error) {
195
179
  // Lock is already held by another worker
@@ -205,10 +189,11 @@ export async function tryAcquireServerLock() {
205
189
  * Release the server lock
206
190
  */
207
191
  export async function releaseServerLock() {
208
- if (lockReleaseFunction) {
192
+ const release = lockReleaseFunction;
193
+ lockReleaseFunction = null;
194
+ if (release) {
209
195
  try {
210
- await lockReleaseFunction();
211
- lockReleaseFunction = null;
196
+ await release();
212
197
  } catch (error) {
213
198
  // Ignore errors during cleanup
214
199
  }
@@ -216,11 +201,12 @@ export async function releaseServerLock() {
216
201
  }
217
202
 
218
203
  /**
219
- * Check if there's an existing worker socket file
220
- * Note: The socket server will clean up stale sockets on startup
204
+ * Check whether a types server is accepting connections, retrying for up to 500ms.
221
205
  */
222
206
  export async function hasExistingWorker() {
223
- return fileExists(getSocketPath());
207
+ // The election lock may have been released immediately before the server
208
+ // becomes connectable. Retry briefly before deciding to start another server.
209
+ return waitForSocketConnection(getSocketPath(), 500);
224
210
  }
225
211
 
226
212
  /**
@@ -233,25 +219,25 @@ export class SocketClient {
233
219
  buffer = '';
234
220
 
235
221
  /**
236
- * Connect to the worker socket with retry logic
222
+ * Connect to the worker socket, retrying while the server starts or named-pipe
223
+ * instances are temporarily busy.
237
224
  */
238
- async connect(retryCount = 0, maxRetries = 10, retryDelay = 50) {
225
+ async connect(timeoutMs = 30_000, retryDelay = 50) {
239
226
  const socketPath = getSocketPath();
240
- try {
241
- await this.attemptConnect(socketPath);
242
- } catch (error) {
243
- // If we've exhausted retries, throw the error
244
- if (retryCount >= maxRetries - 1) {
245
- throw error;
227
+ const startTime = Date.now();
228
+ while (true) {
229
+ try {
230
+ // eslint-disable-next-line no-await-in-loop
231
+ await this.attemptConnect(socketPath);
232
+ return;
233
+ } catch (error) {
234
+ if (Date.now() - startTime >= timeoutMs) {
235
+ throw error;
236
+ }
246
237
  }
247
238
 
248
- // Wait before retrying
249
- await new Promise(resolve => {
250
- setTimeout(resolve, retryDelay);
251
- });
252
-
253
- // Recursive retry
254
- await this.connect(retryCount + 1, maxRetries, retryDelay);
239
+ // eslint-disable-next-line no-await-in-loop
240
+ await sleep(retryDelay);
255
241
  }
256
242
  }
257
243
 
@@ -4,7 +4,7 @@ import type { WorkerRequest, WorkerResponse } from "./worker.mjs";
4
4
  */
5
5
  export interface TypesProcessor {
6
6
  processTypes(request: WorkerRequest): Promise<WorkerResponse>;
7
- terminate(): void;
7
+ terminate(): Promise<void>;
8
8
  }
9
9
  export declare function getWorkerManager(): TypesProcessor;
10
- export declare function terminateWorkerManager(): void;
10
+ export declare function terminateWorkerManager(): Promise<void>;
@@ -5,7 +5,7 @@ import path from 'path';
5
5
  import { fileURLToPath } from 'url';
6
6
  // eslint-disable-next-line n/prefer-node-protocol
7
7
  import { isMainThread, Worker } from 'worker_threads';
8
- import { SocketClient, tryAcquireServerLock, releaseServerLock, waitForSocketFile } from "./socketClient.mjs";
8
+ import { SocketClient, tryAcquireServerLock, releaseServerLock } from "./socketClient.mjs";
9
9
 
10
10
  /**
11
11
  * Shared interface for types processing managers.
@@ -73,9 +73,9 @@ class TypesMetaWorkerManager {
73
73
  });
74
74
  });
75
75
  }
76
- terminate() {
76
+ async terminate() {
77
77
  if (this.worker) {
78
- this.worker.terminate();
78
+ await this.worker.terminate();
79
79
  this.worker = null;
80
80
  }
81
81
  this.pendingRequests.clear();
@@ -85,8 +85,7 @@ class TypesMetaWorkerManager {
85
85
  /**
86
86
  * Types processor for validate worker threads.
87
87
  * On first processTypes() call, races to acquire the server lock:
88
- * - Winner: releases the lock, spawns a bare worker (which acquires the lock
89
- * naturally and becomes the socket server via existing worker.ts logic)
88
+ * - Winner: holds the lock and spawns a bare worker that becomes the socket server
90
89
  * - Losers: skip spawning
91
90
  *
92
91
  * All workers then connect to the socket server as clients.
@@ -121,36 +120,43 @@ class WorkerThreadTypesProcessor {
121
120
  this.serverWorker.on('error', error => {
122
121
  console.error('[WorkerThreadTypesProcessor] Server worker error:', error);
123
122
  });
123
+ }
124
+ this.socketClient = new SocketClient();
125
+ try {
126
+ // Establish the persistent client connection directly. A separate probe
127
+ // can consume an available Windows named-pipe instance and make the real
128
+ // connection fail under high concurrency.
129
+ await this.socketClient.connect();
130
+ } catch (error) {
131
+ this.socketClient?.close();
132
+ this.socketClient = null;
124
133
  try {
125
- // Wait for the socket file to appear, then release the lock.
126
- await waitForSocketFile(30_000);
127
- } catch (error) {
128
- // Server worker crashed before creating the socket — release the lock
129
- // so another worker can become the server on a subsequent attempt.
130
- await releaseServerLock();
131
- throw error;
134
+ if (this.serverWorker) {
135
+ await this.serverWorker.terminate();
136
+ }
137
+ } finally {
138
+ this.serverWorker = null;
139
+ if (isServer) {
140
+ await releaseServerLock();
141
+ }
132
142
  }
133
- await releaseServerLock();
134
- } else {
135
- // Another worker is the server — wait for the socket file.
136
- await waitForSocketFile(30_000);
143
+ throw error;
137
144
  }
138
- this.socketClient = new SocketClient();
139
- await this.socketClient.connect();
140
145
  }
141
146
  async processTypes(request) {
142
147
  await this.ensureInit();
143
148
  return this.socketClient.sendRequest(request);
144
149
  }
145
- terminate() {
150
+ async terminate() {
146
151
  if (this.socketClient) {
147
152
  this.socketClient.close();
148
153
  this.socketClient = null;
149
154
  }
150
155
  if (this.serverWorker) {
151
- this.serverWorker.terminate();
156
+ await this.serverWorker.terminate();
152
157
  this.serverWorker = null;
153
158
  }
159
+ await releaseServerLock();
154
160
  }
155
161
  }
156
162
 
@@ -171,10 +177,10 @@ export function getWorkerManager() {
171
177
  }
172
178
  return processObj[WORKER_MANAGER_KEY];
173
179
  }
174
- export function terminateWorkerManager() {
180
+ export async function terminateWorkerManager() {
175
181
  const processObj = process;
176
182
  if (processObj[WORKER_MANAGER_KEY]) {
177
- processObj[WORKER_MANAGER_KEY].terminate();
183
+ await processObj[WORKER_MANAGER_KEY].terminate();
178
184
  processObj[WORKER_MANAGER_KEY] = undefined;
179
185
  }
180
186
  }