@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 +93 -10
- package/package.json +6 -7
- package/scripts/ensure-better-sqlite3.mjs +1 -1
- package/src/core/codeIndex.js +68 -41
- package/src/core/indexBuildWorkerPool.js +186 -0
- package/src/core/indexWatcher.js +58 -36
- package/src/core/transports/HybridStdioServerTransport.js +16 -4
- package/src/core/unityConnection.js +78 -51
- package/src/core/workers/indexBuildWorker.js +362 -0
- package/src/handlers/script/CodeIndexBuildToolHandler.js +5 -0
- package/src/handlers/script/CodeIndexStatusToolHandler.js +21 -59
- package/src/handlers/script/ScriptRefsFindToolHandler.js +52 -1
- package/src/handlers/script/ScriptSearchToolHandler.js +1 -1
- package/src/handlers/script/ScriptSymbolFindToolHandler.js +64 -65
- package/src/lsp/LspRpcClient.js +9 -2
- package/src/lsp/LspRpcClientSingleton.js +8 -0
- package/src/core/codeIndexDb.js +0 -112
- package/src/core/sqliteFallback.js +0 -82
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,
|
|
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
|
|
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**:
|
|
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.
|
|
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/
|
|
24
|
-
"test:ci:coverage": "c8 --reporter=lcov --reporter=text node --test tests/unit/core/codeIndex.test.js tests/unit/core/
|
|
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/
|
|
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": "^
|
|
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
|
|
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,
|
package/src/core/codeIndex.js
CHANGED
|
@@ -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
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|