@akiojin/unity-mcp-server 2.41.8 → 2.42.1

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
@@ -307,12 +307,102 @@ Add to your `claude_desktop_config.json`:
307
307
  - `includeTestResults` (bool, default `false`): attaches the latest exported `.unity/test-results/*.json` summary to the response when a test run has completed.
308
308
  - `includeFileContent` (bool, default `false`): when combined with `includeTestResults`, also returns the JSON file contents as a string so agents can parse the detailed per-test data without reading the file directly.
309
309
 
310
+ ## Architecture
311
+
312
+ ### Native Dependencies
313
+
314
+ Unity MCP Server uses platform-specific native binaries for performance-critical operations:
315
+
316
+ | Component | Purpose | Distribution | Size per platform |
317
+ |-----------|---------|--------------|-------------------|
318
+ | **better-sqlite3** | Code index database | Bundled in npm package | ~3-5 MB |
319
+ | **csharp-lsp** | C# symbol analysis | Downloaded on first use | ~80 MB |
320
+
321
+ #### Why better-sqlite3 over JavaScript-based SQLite?
322
+
323
+ We chose **better-sqlite3** (native C binding) instead of JavaScript-based alternatives like **sql.js** for the following reasons:
324
+
325
+ | Aspect | better-sqlite3 (native) | sql.js (WASM/JS) |
326
+ |--------|------------------------|------------------|
327
+ | **Performance** | 10-100x faster for bulk operations | Slower due to WASM overhead |
328
+ | **Memory** | Efficient native memory management | Higher memory usage, GC pressure |
329
+ | **Synchronous API** | Native sync operations, ideal for MCP | Async-only, adds complexity |
330
+ | **Startup time** | Instant module load | ~100-500ms WASM initialization |
331
+ | **Database size** | Handles large indexes efficiently | Performance degrades with size |
332
+
333
+ The code index database may contain tens of thousands of symbols from Unity projects. Native SQLite provides the performance characteristics needed for responsive symbol searches and incremental updates.
334
+
335
+ **Trade-off**: Requires platform-specific binaries (bundled for all supported platforms), while sql.js would work everywhere but with unacceptable performance for our use case.
336
+
337
+ #### Why Different Distribution Methods?
338
+
339
+ **better-sqlite3 (bundled)**:
340
+
341
+ - Small size (~3-5 MB per platform)
342
+ - Prevents MCP server initialization timeout during `npm install` compilation
343
+ - Code index features disabled with clear error message if native binding fails
344
+
345
+ **csharp-lsp (downloaded)**:
346
+
347
+ - Large size (~80 MB per platform, ~480 MB for all 6 platforms)
348
+ - Too large to bundle in npm package (would increase package size 100x)
349
+ - Downloaded from GitHub Release on first use of script editing tools
350
+
351
+ #### Supported Platforms
352
+
353
+ | Platform | better-sqlite3 | csharp-lsp |
354
+ |----------|---------------|------------|
355
+ | Linux x64 | ✅ Bundled | ✅ Downloaded (~79 MB) |
356
+ | Linux arm64 | ✅ Bundled | ✅ Downloaded (~86 MB) |
357
+ | macOS x64 | ✅ Bundled | ✅ Downloaded (~80 MB) |
358
+ | macOS arm64 (Apple Silicon) | ✅ Bundled | ✅ Downloaded (~86 MB) |
359
+ | Windows x64 | ✅ Bundled | ✅ Downloaded (~80 MB) |
360
+ | Windows arm64 | ✅ Bundled | ✅ Downloaded (~85 MB) |
361
+
362
+ #### Fallback Behavior
363
+
364
+ - **better-sqlite3**: No fallback; code index features disabled with clear error if native binding unavailable
365
+ - **csharp-lsp**: No fallback; script editing features require the native binary
366
+
367
+ #### Storage Locations
368
+
369
+ - **better-sqlite3**: `<package>/prebuilt/better-sqlite3/<platform>/`
370
+ - **csharp-lsp**: `~/.unity/tools/csharp-lsp/<rid>/`
371
+ - **Code index database**: `<workspace>/.unity/cache/code-index/code-index.db`
372
+
310
373
  ## Requirements
311
374
 
312
375
  - **Unity**: 2020.3 LTS or newer (Unity 6 supported)
313
376
  - **Node.js**: 18.0.0 or newer
314
377
  - **MCP Client**: Claude Desktop, Cursor, or compatible client
315
378
 
379
+ ## Performance Benchmark
380
+
381
+ Response time comparison between Code Index tools and standard Claude Code tools (with DB index built, `watch: true` enabled):
382
+
383
+ | Operation | unity-mcp-server | Time | Standard Tool | Time | Result |
384
+ |-----------|------------------|------|---------------|------|--------|
385
+ | File Read | `script_read` | **instant** | `Read` | **instant** | EQUAL |
386
+ | Symbol List | `script_symbols_get` | **instant** | N/A | N/A | PASS |
387
+ | Symbol Find | `script_symbol_find` | **instant** | `Grep` | **instant** | EQUAL |
388
+ | Text Search | `script_search` | **instant** | `Grep` | **instant** | EQUAL |
389
+ | Reference Find | `script_refs_find` | **instant** | `Grep` | **instant** | EQUAL |
390
+ | Index Status | `code_index_status` | **instant** | N/A | N/A | PASS |
391
+
392
+ **Note**: Performance above requires DB index to be built. Run `code_index_build` before first use.
393
+
394
+ ### Worker Thread Implementation
395
+
396
+ Since v2.41.x, background index builds run in Worker Threads, so MCP tools are never blocked even with `watch: true`:
397
+
398
+ | Scenario | Before Worker Thread | After Worker Thread |
399
+ |----------|---------------------|---------------------|
400
+ | `system_ping` | **60+ seconds block** | **instant** |
401
+ | `code_index_status` | **60+ seconds block** | **instant** |
402
+ | Any MCP tool | timeout during build | **instant** |
403
+
404
+ See [`docs/benchmark.md`](../docs/benchmark.md) for detailed benchmark results.
405
+
316
406
  ## Troubleshooting
317
407
 
318
408
  ### Connection Issues
@@ -338,23 +428,16 @@ If you encounter errors related to `better-sqlite3` during installation or start
338
428
 
339
429
  **Symptom**: Installation fails with `node-gyp` errors, or startup shows "Could not locate the bindings file."
340
430
 
341
- **Cause**: The package includes prebuilt native binaries for supported platforms (Linux/macOS/Windows × x64/arm64 × Node 18/20/22). If your platform isn't supported or the prebuilt fails to load, the system falls back to WASM.
342
-
343
- **Solution 1 - Use WASM fallback (recommended for unsupported platforms)**:
344
-
345
- ```bash
346
- # Skip native build and use sql.js WASM fallback
347
- UNITY_MCP_SKIP_NATIVE_BUILD=1 npm install @akiojin/unity-mcp-server
348
- ```
431
+ **Cause**: The package includes prebuilt native binaries for supported platforms (Linux/macOS/Windows × x64/arm64 × Node 18/20/22). If your platform isn't supported or the prebuilt fails to load, code index features will be disabled.
349
432
 
350
- **Solution 2 - Force native rebuild**:
433
+ **Solution - Force native rebuild**:
351
434
 
352
435
  ```bash
353
436
  # Force rebuild from source (requires build tools)
354
437
  UNITY_MCP_FORCE_NATIVE=1 npm install @akiojin/unity-mcp-server
355
438
  ```
356
439
 
357
- **Note**: WASM fallback is fully functional but may have slightly slower performance for large codebases. Code index features work normally in either mode.
440
+ **Note**: If native binding is unavailable, code index features (`code_index_build`, `code_index_status`, `script_symbol_find`, etc.) will return clear error messages indicating the feature is disabled. Other MCP server features continue to work normally.
358
441
 
359
442
  ### MCP Client Shows "Capabilities: none"
360
443
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akiojin/unity-mcp-server",
3
- "version": "2.41.8",
3
+ "version": "2.42.1",
4
4
  "description": "MCP server and Unity Editor bridge — enables AI assistants to control Unity for AI-assisted workflows",
5
5
  "type": "module",
6
6
  "main": "src/core/server.js",
@@ -20,8 +20,8 @@
20
20
  "test:watch": "node --watch --test tests/unit/**/*.test.js",
21
21
  "test:watch:all": "node --watch --test tests/**/*.test.js",
22
22
  "test:performance": "node --test tests/performance/*.test.js",
23
- "test:ci": "CI=true NODE_ENV=test node --test tests/unit/core/codeIndex.test.js tests/unit/core/codeIndexDb.test.js tests/unit/core/config.test.js tests/unit/core/indexWatcher.test.js tests/unit/core/projectInfo.test.js tests/unit/core/server.test.js tests/unit/handlers/script/CodeIndexStatusToolHandler.test.js tests/unit/scripts/ensure-better-sqlite3.test.js",
24
- "test:ci:coverage": "c8 --reporter=lcov --reporter=text node --test tests/unit/core/codeIndex.test.js tests/unit/core/codeIndexDb.test.js tests/unit/core/config.test.js tests/unit/core/indexWatcher.test.js tests/unit/core/projectInfo.test.js tests/unit/core/server.test.js tests/unit/handlers/script/CodeIndexStatusToolHandler.test.js",
23
+ "test:ci": "CI=true NODE_ENV=test node --test tests/unit/core/codeIndex.test.js tests/unit/core/config.test.js tests/unit/core/indexWatcher.test.js tests/unit/core/projectInfo.test.js tests/unit/core/server.test.js tests/unit/handlers/script/CodeIndexStatusToolHandler.test.js tests/unit/scripts/ensure-better-sqlite3.test.js",
24
+ "test:ci:coverage": "c8 --reporter=lcov --reporter=text node --test tests/unit/core/codeIndex.test.js tests/unit/core/config.test.js tests/unit/core/indexWatcher.test.js tests/unit/core/projectInfo.test.js tests/unit/core/server.test.js tests/unit/handlers/script/CodeIndexStatusToolHandler.test.js",
25
25
  "test:ci:all": "c8 --reporter=lcov node --test tests/unit/**/*.test.js",
26
26
  "simulate:code-index": "node scripts/simulate-code-index-status.mjs",
27
27
  "test:verbose": "VERBOSE_TEST=true node --test tests/**/*.test.js",
@@ -30,7 +30,7 @@
30
30
  "prebuilt:manifest": "node scripts/generate-prebuilt-manifest.mjs",
31
31
  "prepublishOnly": "npm run test:ci && npm run prebuilt:manifest",
32
32
  "postinstall": "node scripts/ensure-better-sqlite3.mjs && chmod +x bin/unity-mcp-server || true",
33
- "test:ci:unity": "timeout 60 node --test tests/unit/core/codeIndex.test.js tests/unit/core/codeIndexDb.test.js tests/unit/core/config.test.js tests/unit/core/indexWatcher.test.js tests/unit/core/projectInfo.test.js tests/unit/core/server.test.js || exit 0",
33
+ "test:ci:unity": "timeout 60 node --test tests/unit/core/codeIndex.test.js tests/unit/core/config.test.js tests/unit/core/indexWatcher.test.js tests/unit/core/projectInfo.test.js tests/unit/core/server.test.js || exit 0",
34
34
  "test:unity": "node tests/run-unity-integration.mjs",
35
35
  "test:nounity": "npm run test:integration",
36
36
  "test:ci:integration": "CI=true NODE_ENV=test node --test tests/integration/code-index-background.test.js"
@@ -50,10 +50,9 @@
50
50
  "author": "Akio Jinsenji <akio-jinsenji@cloud-creative-studios.com>",
51
51
  "license": "MIT",
52
52
  "dependencies": {
53
- "@modelcontextprotocol/sdk": "^0.6.1",
53
+ "@modelcontextprotocol/sdk": "^1.24.0",
54
54
  "better-sqlite3": "^9.4.3",
55
- "find-up": "^6.3.0",
56
- "sql.js": "^1.13.0"
55
+ "find-up": "^6.3.0"
57
56
  },
58
57
  "engines": {
59
58
  "node": ">=18 <23"
@@ -66,7 +66,7 @@ export function ensureBetterSqlite3(options = {}) {
66
66
  bindingPath = DEFAULT_BINDING_PATH,
67
67
  prebuiltRoot = DEFAULT_PREBUILT_ROOT,
68
68
  pkgRoot = PKG_ROOT,
69
- skipNative = process.env.UNITY_MCP_SKIP_NATIVE_BUILD !== '0',
69
+ skipNative = process.env.UNITY_MCP_SKIP_NATIVE_BUILD === '1',
70
70
  forceNative = process.env.UNITY_MCP_FORCE_NATIVE === '1',
71
71
  skipLegacyFlag = Boolean(process.env.SKIP_SQLITE_REBUILD),
72
72
  log = console.log,
@@ -2,7 +2,6 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { ProjectInfoProvider } from './projectInfo.js';
4
4
  import { logger } from './config.js';
5
- import { createSqliteFallback } from './sqliteFallback.js';
6
5
 
7
6
  // Shared driver availability state across CodeIndex instances
8
7
  const driverStatus = {
@@ -11,17 +10,23 @@ const driverStatus = {
11
10
  logged: false
12
11
  };
13
12
 
13
+ // Shared DB connections (singleton pattern for concurrent access)
14
+ const sharedConnections = {
15
+ writeDb: null,
16
+ readDb: null,
17
+ dbPath: null,
18
+ schemaInitialized: false
19
+ };
20
+
14
21
  export class CodeIndex {
15
22
  constructor(unityConnection) {
16
23
  this.unityConnection = unityConnection;
17
24
  this.projectInfo = new ProjectInfoProvider(unityConnection);
18
- this.db = null;
25
+ this.db = null; // legacy reference, points to writeDb
19
26
  this.dbPath = null;
20
27
  this.disabled = false; // set true if better-sqlite3 is unavailable
21
28
  this.disableReason = null;
22
29
  this._Database = null;
23
- this._openFallback = null;
24
- this._persistFallback = null;
25
30
  }
26
31
 
27
32
  async _ensureDriver() {
@@ -39,21 +44,13 @@ export class CodeIndex {
39
44
  driverStatus.error = null;
40
45
  return true;
41
46
  } catch (e) {
42
- // Try wasm fallback (sql.js) before giving up
43
- try {
44
- this._openFallback = createSqliteFallback;
45
- driverStatus.available = true;
46
- driverStatus.error = null;
47
- logger?.info?.('[index] falling back to sql.js (WASM) for code index');
48
- return true;
49
- } catch (fallbackError) {
50
- this.disabled = true;
51
- this.disableReason = `better-sqlite3 unavailable: ${e?.message || e}. Fallback failed: ${fallbackError?.message || fallbackError}`;
52
- driverStatus.available = false;
53
- driverStatus.error = this.disableReason;
54
- this._logDisable(this.disableReason);
55
- return false;
56
- }
47
+ // No fallback - fail fast with clear error
48
+ this.disabled = true;
49
+ this.disableReason = `better-sqlite3 native binding unavailable: ${e?.message || e}. Code index features are disabled.`;
50
+ driverStatus.available = false;
51
+ driverStatus.error = this.disableReason;
52
+ this._logDisable(this.disableReason);
53
+ return false;
57
54
  }
58
55
  }
59
56
 
@@ -66,12 +63,23 @@ export class CodeIndex {
66
63
  fs.mkdirSync(dir, { recursive: true });
67
64
  const dbPath = path.join(dir, 'code-index.db');
68
65
  this.dbPath = dbPath;
66
+
67
+ // Use shared connections for all CodeIndex instances
68
+ if (sharedConnections.writeDb && sharedConnections.dbPath === dbPath) {
69
+ this.db = sharedConnections.writeDb;
70
+ return this.db;
71
+ }
72
+
69
73
  try {
70
74
  if (this._Database) {
71
- this.db = new this._Database(dbPath);
72
- } else if (this._openFallback) {
73
- this.db = await this._openFallback(dbPath);
74
- this._persistFallback = this.db.persist;
75
+ // Create write connection (default)
76
+ sharedConnections.writeDb = new this._Database(dbPath);
77
+ sharedConnections.dbPath = dbPath;
78
+ this.db = sharedConnections.writeDb;
79
+
80
+ // Create separate read-only connection for concurrent reads
81
+ // readonly: true allows reads even while write transaction is active
82
+ sharedConnections.readDb = new this._Database(dbPath, { readonly: true });
75
83
  } else {
76
84
  throw new Error('No database driver available');
77
85
  }
@@ -87,6 +95,14 @@ export class CodeIndex {
87
95
  return this.db;
88
96
  }
89
97
 
98
+ /**
99
+ * Get read-only DB connection for queries (non-blocking during writes)
100
+ * Falls back to write connection if read connection unavailable
101
+ */
102
+ _getReadDb() {
103
+ return sharedConnections.readDb || this.db;
104
+ }
105
+
90
106
  _logDisable(reason) {
91
107
  if (driverStatus.logged) return;
92
108
  driverStatus.logged = true;
@@ -100,6 +116,7 @@ export class CodeIndex {
100
116
  const db = this.db;
101
117
  db.exec(`
102
118
  PRAGMA journal_mode=WAL;
119
+ PRAGMA busy_timeout=5000;
103
120
  CREATE TABLE IF NOT EXISTS meta (
104
121
  key TEXT PRIMARY KEY,
105
122
  value TEXT
@@ -122,13 +139,14 @@ export class CodeIndex {
122
139
  CREATE INDEX IF NOT EXISTS idx_symbols_kind ON symbols(kind);
123
140
  CREATE INDEX IF NOT EXISTS idx_symbols_path ON symbols(path);
124
141
  `);
125
- if (this._persistFallback) this._persistFallback();
126
142
  }
127
143
 
128
144
  async isReady() {
129
145
  const db = await this.open();
130
146
  if (!db) return false;
131
- const row = db.prepare('SELECT COUNT(*) AS c FROM symbols').get();
147
+ // Use read-only connection for non-blocking check
148
+ const readDb = this._getReadDb();
149
+ const row = readDb.prepare('SELECT COUNT(*) AS c FROM symbols').get();
132
150
  return (row?.c || 0) > 0;
133
151
  }
134
152
 
@@ -158,7 +176,6 @@ export class CodeIndex {
158
176
  );
159
177
  });
160
178
  tx(symbols || []);
161
- await this._flushFallback();
162
179
  return { total: symbols?.length || 0 };
163
180
  }
164
181
 
@@ -166,7 +183,9 @@ export class CodeIndex {
166
183
  async getFiles() {
167
184
  const db = await this.open();
168
185
  if (!db) return new Map();
169
- const rows = db.prepare('SELECT path, sig FROM files').all();
186
+ // Use read-only connection for non-blocking read
187
+ const readDb = this._getReadDb();
188
+ const rows = readDb.prepare('SELECT path, sig FROM files').all();
170
189
  const map = new Map();
171
190
  for (const r of rows) map.set(String(r.path), String(r.sig || ''));
172
191
  return map;
@@ -180,7 +199,6 @@ export class CodeIndex {
180
199
  sig || '',
181
200
  new Date().toISOString()
182
201
  );
183
- await this._flushFallback();
184
202
  }
185
203
 
186
204
  async removeFile(pathStr) {
@@ -191,7 +209,6 @@ export class CodeIndex {
191
209
  db.prepare('DELETE FROM files WHERE path = ?').run(p);
192
210
  });
193
211
  tx(pathStr);
194
- await this._flushFallback();
195
212
  }
196
213
 
197
214
  async replaceSymbolsForPath(pathStr, rows) {
@@ -218,12 +235,13 @@ export class CodeIndex {
218
235
  );
219
236
  });
220
237
  tx(pathStr, rows || []);
221
- await this._flushFallback();
222
238
  }
223
239
 
224
240
  async querySymbols({ name, kind, scope = 'all', exact = false }) {
225
241
  const db = await this.open();
226
242
  if (!db) return [];
243
+ // Use read-only connection for non-blocking query
244
+ const readDb = this._getReadDb();
227
245
  let sql = 'SELECT path,name,kind,container,namespace,line,column FROM symbols WHERE 1=1';
228
246
  const params = {};
229
247
  if (name) {
@@ -239,7 +257,7 @@ export class CodeIndex {
239
257
  sql += ' AND kind = @kind';
240
258
  params.kind = kind;
241
259
  }
242
- const rows = db.prepare(sql).all(params);
260
+ const rows = readDb.prepare(sql).all(params);
243
261
  // Apply path-based scope filter in JS (simpler than CASE in SQL)
244
262
  const filtered = rows.filter(r => {
245
263
  const p = String(r.path || '').replace(/\\\\/g, '/');
@@ -263,19 +281,13 @@ export class CodeIndex {
263
281
  async getStats() {
264
282
  const db = await this.open();
265
283
  if (!db) return { total: 0, lastIndexedAt: null };
266
- const total = db.prepare('SELECT COUNT(*) AS c FROM symbols').get().c || 0;
284
+ // Use read-only connection for non-blocking stats
285
+ const readDb = this._getReadDb();
286
+ const total = readDb.prepare('SELECT COUNT(*) AS c FROM symbols').get().c || 0;
267
287
  const last =
268
- db.prepare("SELECT value AS v FROM meta WHERE key = 'lastIndexedAt'").get()?.v || null;
288
+ readDb.prepare("SELECT value AS v FROM meta WHERE key = 'lastIndexedAt'").get()?.v || null;
269
289
  return { total, lastIndexedAt: last };
270
290
  }
271
-
272
- async _flushFallback() {
273
- if (typeof this._persistFallback === 'function') {
274
- try {
275
- await this._persistFallback();
276
- } catch {}
277
- }
278
- }
279
291
  }
280
292
 
281
293
  // Test-only helper to reset cached driver status between runs
@@ -283,4 +295,19 @@ export function __resetCodeIndexDriverStatusForTest() {
283
295
  driverStatus.available = null;
284
296
  driverStatus.error = null;
285
297
  driverStatus.logged = false;
298
+ // Also reset shared connections
299
+ if (sharedConnections.writeDb) {
300
+ try {
301
+ sharedConnections.writeDb.close();
302
+ } catch {}
303
+ }
304
+ if (sharedConnections.readDb) {
305
+ try {
306
+ sharedConnections.readDb.close();
307
+ } catch {}
308
+ }
309
+ sharedConnections.writeDb = null;
310
+ sharedConnections.readDb = null;
311
+ sharedConnections.dbPath = null;
312
+ sharedConnections.schemaInitialized = false;
286
313
  }
@@ -0,0 +1,186 @@
1
+ import { Worker } from 'worker_threads';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { logger } from './config.js';
5
+ import { JobManager } from './jobManager.js';
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+
10
+ /**
11
+ * Worker Thread pool for non-blocking code index builds.
12
+ *
13
+ * This class manages Worker Threads that execute index builds in a separate
14
+ * thread, preventing the main Node.js event loop from being blocked by
15
+ * better-sqlite3's synchronous database operations.
16
+ *
17
+ * Requirements:
18
+ * - FR-056: Execute index build in Worker Thread
19
+ * - FR-057: Maintain <1s ping response during builds
20
+ * - FR-058: Propagate Worker Thread errors to main thread
21
+ * - FR-059: Async progress notification from Worker Thread
22
+ */
23
+ export class IndexBuildWorkerPool {
24
+ constructor() {
25
+ this.worker = null;
26
+ this.jobManager = JobManager.getInstance();
27
+ this.progressCallbacks = [];
28
+ this.currentBuildId = null;
29
+ }
30
+
31
+ /**
32
+ * Subscribe to progress updates from Worker Thread
33
+ * @param {Function} callback - Called with progress object {processed, total, rate}
34
+ */
35
+ onProgress(callback) {
36
+ this.progressCallbacks.push(callback);
37
+ }
38
+
39
+ /**
40
+ * Execute index build in Worker Thread
41
+ * @param {Object} options - Build options
42
+ * @param {string} options.projectRoot - Unity project root path
43
+ * @param {string} options.dbPath - Database file path
44
+ * @param {number} [options.concurrency=1] - Worker concurrency (kept low to avoid blocking)
45
+ * @param {number} [options.throttleMs=0] - Delay between files for testing
46
+ * @returns {Promise<Object>} Build result
47
+ */
48
+ async executeBuild(options) {
49
+ if (this.worker) {
50
+ throw new Error('Build already in progress');
51
+ }
52
+
53
+ return new Promise((resolve, reject) => {
54
+ const workerPath = path.join(__dirname, 'workers', 'indexBuildWorker.js');
55
+
56
+ try {
57
+ this.worker = new Worker(workerPath, {
58
+ workerData: {
59
+ ...options,
60
+ concurrency: options.concurrency || 1
61
+ }
62
+ });
63
+
64
+ this.worker.on('message', message => {
65
+ if (message.type === 'progress') {
66
+ // Notify progress callbacks
67
+ this.progressCallbacks.forEach(cb => {
68
+ try {
69
+ cb(message.data);
70
+ } catch (e) {
71
+ logger.warn(`[worker-pool] Progress callback error: ${e.message}`);
72
+ }
73
+ });
74
+
75
+ // Update JobManager if job is tracked
76
+ if (this.currentBuildId) {
77
+ const job = this.jobManager.get(this.currentBuildId);
78
+ if (job) {
79
+ job.progress = message.data;
80
+ }
81
+ }
82
+ } else if (message.type === 'complete') {
83
+ this._cleanup();
84
+ resolve(message.data);
85
+ } else if (message.type === 'error') {
86
+ this._cleanup();
87
+ reject(new Error(message.error));
88
+ } else if (message.type === 'log') {
89
+ // Forward logs from worker
90
+ const level = message.level || 'info';
91
+ if (logger[level]) {
92
+ logger[level](message.message);
93
+ }
94
+ }
95
+ });
96
+
97
+ this.worker.on('error', error => {
98
+ logger.error(`[worker-pool] Worker error: ${error.message}`);
99
+ this._cleanup();
100
+ reject(error);
101
+ });
102
+
103
+ this.worker.on('exit', code => {
104
+ if (code !== 0 && this.worker) {
105
+ logger.warn(`[worker-pool] Worker exited with code ${code}`);
106
+ this._cleanup();
107
+ reject(new Error(`Worker exited with code ${code}`));
108
+ }
109
+ });
110
+ } catch (error) {
111
+ this._cleanup();
112
+ reject(error);
113
+ }
114
+ });
115
+ }
116
+
117
+ /**
118
+ * Execute build with JobManager integration
119
+ * @param {string} jobId - Job ID for tracking
120
+ * @param {Object} options - Build options
121
+ * @returns {Promise<Object>} Build result
122
+ */
123
+ async executeBuildWithJob(jobId, options) {
124
+ this.currentBuildId = jobId;
125
+
126
+ try {
127
+ const result = await this.executeBuild(options);
128
+ this.currentBuildId = null;
129
+ return result;
130
+ } catch (error) {
131
+ this.currentBuildId = null;
132
+ throw error;
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Terminate the worker thread
138
+ */
139
+ terminate() {
140
+ if (this.worker) {
141
+ this.worker.terminate();
142
+ this._cleanup();
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Check if a build is currently running
148
+ * @returns {boolean}
149
+ */
150
+ isRunning() {
151
+ return this.worker !== null;
152
+ }
153
+
154
+ /**
155
+ * Clean up worker reference
156
+ * @private
157
+ */
158
+ _cleanup() {
159
+ this.worker = null;
160
+ this.currentBuildId = null;
161
+ }
162
+ }
163
+
164
+ // Singleton instance for shared use
165
+ let instance = null;
166
+
167
+ /**
168
+ * Get singleton instance of IndexBuildWorkerPool
169
+ * @returns {IndexBuildWorkerPool}
170
+ */
171
+ export function getWorkerPool() {
172
+ if (!instance) {
173
+ instance = new IndexBuildWorkerPool();
174
+ }
175
+ return instance;
176
+ }
177
+
178
+ /**
179
+ * Reset singleton (for testing)
180
+ */
181
+ export function __resetWorkerPoolForTest() {
182
+ if (instance) {
183
+ instance.terminate();
184
+ instance = null;
185
+ }
186
+ }