ast-mcp-server 0.6.0 → 0.7.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/CHANGELOG.md +38 -0
- package/README.md +47 -15
- package/SECURITY.md +31 -0
- package/dist/cli.js +0 -0
- package/dist/index.js +129 -10
- package/dist/server.js +21 -2
- package/dist/services/agent-setup.js +36 -117
- package/dist/services/agent-targets.js +122 -0
- package/dist/services/context-builder.js +52 -24
- package/dist/services/diagnostics.js +10 -5
- package/dist/services/file-fingerprints.js +104 -0
- package/dist/services/file-snapshot.js +8 -1
- package/dist/services/impact.js +334 -0
- package/dist/services/operations.js +343 -254
- package/dist/services/outline.js +94 -21
- package/dist/services/pagination.js +10 -4
- package/dist/services/project-operation-scheduler.js +475 -0
- package/dist/services/project-status.js +134 -52
- package/dist/services/project-watcher.js +230 -0
- package/dist/services/project.js +646 -66
- package/dist/services/public-errors.js +255 -0
- package/dist/services/references.js +7 -1
- package/dist/services/relationships.js +1198 -0
- package/dist/services/request-context.js +42 -0
- package/dist/services/runtime-activity.js +65 -0
- package/dist/services/runtime-logger.js +50 -0
- package/dist/services/runtime-policy.js +47 -0
- package/dist/services/setup-wizard.js +4 -7
- package/dist/services/shutdown.js +93 -0
- package/dist/services/symbol-index-limits.js +5 -0
- package/dist/services/symbol-index-order.js +13 -0
- package/dist/services/symbol-index-policy.js +91 -0
- package/dist/services/symbol-index-sqlite.js +850 -0
- package/dist/services/symbol-index.js +447 -0
- package/dist/services/symbols.js +403 -79
- package/dist/services/test-candidates.js +209 -0
- package/dist/services/workspace.js +18 -7
- package/dist/tools/apply_operation.js +8 -4
- package/dist/tools/explore.js +8 -6
- package/dist/tools/find_references.js +12 -13
- package/dist/tools/get_diagnostics.js +10 -7
- package/dist/tools/get_file.js +23 -11
- package/dist/tools/get_impact.js +134 -0
- package/dist/tools/get_operation_preview.js +7 -4
- package/dist/tools/get_outline.js +8 -6
- package/dist/tools/get_project_status.js +77 -8
- package/dist/tools/get_symbol_source.js +8 -6
- package/dist/tools/list_files.js +11 -6
- package/dist/tools/rename_symbol.js +6 -4
- package/dist/tools/replace_symbol_body.js +6 -4
- package/dist/tools/result.js +27 -17
- package/dist/tools/scaffold_class.js +6 -4
- package/dist/tools/search_symbols.js +15 -14
- package/docs/support.md +80 -0
- package/package.json +16 -5
- package/skills/structural-code-editing/SKILL.md +37 -10
package/dist/services/project.js
CHANGED
|
@@ -1,12 +1,184 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { Project } from "ts-morph";
|
|
4
|
-
import {
|
|
4
|
+
import { InMemorySymbolIndex, SYMBOL_INDEX_SCHEMA_VERSION, } from "./symbol-index.js";
|
|
5
|
+
import { addSymbolIndexRuntimeCount, createInitialSymbolIndexRuntimeObservability, readSymbolIndexPersistencePolicy, symbolIndexCachePath, symbolIndexPolicyKey, } from "./symbol-index-policy.js";
|
|
6
|
+
import { openSQLiteSymbolIndexStore, quarantineSQLiteSymbolIndexFile, } from "./symbol-index-sqlite.js";
|
|
7
|
+
import { findDeclaration, sourceFileIndexSymbols } from "./symbols.js";
|
|
8
|
+
import { createProjectWatcher, selectProjectWatchDirectories, } from "./project-watcher.js";
|
|
5
9
|
import { createConfigSnapshot, createWorkspaceSnapshot } from "./workspace.js";
|
|
6
|
-
import { createInitialProjectStatus, createProjectIdentity, projectStatusToProjection, transitionProjectStatus, } from "./project-status.js";
|
|
7
|
-
|
|
10
|
+
import { createInitialProjectStatus, createProjectIdentity, projectOperationQueueToProjection, projectStatusToProjection, transitionProjectStatus, } from "./project-status.js";
|
|
11
|
+
import { ProjectOperationScheduler, ProjectOperationSchedulerError, } from "./project-operation-scheduler.js";
|
|
12
|
+
import { isCooperativeInterruption, NO_REQUEST_CONTEXT, } from "./request-context.js";
|
|
13
|
+
import { readRuntimePolicy } from "./runtime-policy.js";
|
|
14
|
+
export class ProjectCapacityError extends Error {
|
|
15
|
+
code = "PROJECT_CAPACITY_EXCEEDED";
|
|
16
|
+
constructor() {
|
|
17
|
+
super("Project session capacity exceeded.");
|
|
18
|
+
this.name = "ProjectCapacityError";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
8
21
|
const projectSessions = new Map();
|
|
9
22
|
const projectRoots = new WeakMap();
|
|
23
|
+
let projectSessionAccessSequence = 0n;
|
|
24
|
+
let projectRuntimePolicy;
|
|
25
|
+
let projectRuntimeAdmission = "open";
|
|
26
|
+
function nextProjectSessionAccessSequence() {
|
|
27
|
+
projectSessionAccessSequence += 1n;
|
|
28
|
+
return projectSessionAccessSequence;
|
|
29
|
+
}
|
|
30
|
+
function getProjectRuntimePolicy() {
|
|
31
|
+
projectRuntimePolicy ??= readRuntimePolicy();
|
|
32
|
+
return projectRuntimePolicy;
|
|
33
|
+
}
|
|
34
|
+
function closePersistentSymbolIndex(session) {
|
|
35
|
+
session.persistentSymbolIndex?.close();
|
|
36
|
+
session.persistentSymbolIndex = undefined;
|
|
37
|
+
}
|
|
38
|
+
function symbolIndexFailureOperation(reason) {
|
|
39
|
+
if (reason === "corrupt_storage")
|
|
40
|
+
return "corruption";
|
|
41
|
+
if (reason === "migration_failed" || reason === "unsupported_schema")
|
|
42
|
+
return "migration";
|
|
43
|
+
if (reason === "read_failed" || reason === "sqlite_read_failed")
|
|
44
|
+
return "read_failure";
|
|
45
|
+
if (reason === "write_failed" || reason === "sqlite_write_failed" || reason === "contention") {
|
|
46
|
+
return "write_failure";
|
|
47
|
+
}
|
|
48
|
+
return "fallback";
|
|
49
|
+
}
|
|
50
|
+
function fallbackToMemory(session, reason) {
|
|
51
|
+
const persistentSymbolIndex = session.persistentSymbolIndex;
|
|
52
|
+
session.persistentSymbolIndex = undefined;
|
|
53
|
+
const boundedReason = reason.slice(0, 128);
|
|
54
|
+
const operation = symbolIndexFailureOperation(boundedReason);
|
|
55
|
+
const previous = session.context.symbolIndexObservability;
|
|
56
|
+
session.context = {
|
|
57
|
+
...session.context,
|
|
58
|
+
status: transitionProjectStatus(session.context.status, {
|
|
59
|
+
type: "index_failed",
|
|
60
|
+
error: boundedReason,
|
|
61
|
+
}),
|
|
62
|
+
symbolIndex: new InMemorySymbolIndex(),
|
|
63
|
+
symbolIndexBackend: "memory",
|
|
64
|
+
symbolIndexFallbackReason: boundedReason,
|
|
65
|
+
symbolIndexReady: false,
|
|
66
|
+
symbolIndexObservability: {
|
|
67
|
+
...session.context.symbolIndexObservability,
|
|
68
|
+
policy: session.symbolIndexPolicy.mode,
|
|
69
|
+
policy_reason: session.symbolIndexPolicy.reason,
|
|
70
|
+
backend: "memory",
|
|
71
|
+
state: "failed",
|
|
72
|
+
operation,
|
|
73
|
+
last_operation: operation,
|
|
74
|
+
rejected_entries: addSymbolIndexRuntimeCount(previous.rejected_entries),
|
|
75
|
+
fallback_count: addSymbolIndexRuntimeCount(previous.fallback_count),
|
|
76
|
+
migration_count: addSymbolIndexRuntimeCount(previous.migration_count, operation === "migration" ? 1 : 0),
|
|
77
|
+
corruption_count: addSymbolIndexRuntimeCount(previous.corruption_count, operation === "corruption" ? 1 : 0),
|
|
78
|
+
write_failure_count: addSymbolIndexRuntimeCount(previous.write_failure_count, operation === "write_failure" ? 1 : 0),
|
|
79
|
+
last_error: boundedReason,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
try {
|
|
83
|
+
persistentSymbolIndex?.close();
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// The compiler-backed memory context was already installed; cleanup is best effort.
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
export async function reportSymbolIndexFailure(projectRoot, reason) {
|
|
90
|
+
const session = projectSessions.get(resolveTsConfigPath(projectRoot));
|
|
91
|
+
if (!session)
|
|
92
|
+
return undefined;
|
|
93
|
+
const cachePath = symbolIndexCachePath(session.symbolIndexPolicy, session.context.status.project);
|
|
94
|
+
fallbackToMemory(session, reason);
|
|
95
|
+
if (cachePath && reason === "corrupt_storage") {
|
|
96
|
+
try {
|
|
97
|
+
await quarantineSQLiteSymbolIndexFile(cachePath);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// The compiler-backed memory fallback remains authoritative if quarantine fails.
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return session.context;
|
|
104
|
+
}
|
|
105
|
+
async function ensureSessionSymbolIndex(session, requestContext) {
|
|
106
|
+
requestContext.checkpoint();
|
|
107
|
+
const policy = readSymbolIndexPersistencePolicy();
|
|
108
|
+
if (symbolIndexPolicyKey(policy) !== symbolIndexPolicyKey(session.symbolIndexPolicy)) {
|
|
109
|
+
closePersistentSymbolIndex(session);
|
|
110
|
+
session.symbolIndexPolicy = policy;
|
|
111
|
+
session.context = {
|
|
112
|
+
...session.context,
|
|
113
|
+
status: transitionProjectStatus(session.context.status, { type: "index_disabled" }),
|
|
114
|
+
symbolIndex: new InMemorySymbolIndex(),
|
|
115
|
+
symbolIndexBackend: "memory",
|
|
116
|
+
symbolIndexFallbackReason: null,
|
|
117
|
+
symbolIndexReady: false,
|
|
118
|
+
symbolIndexObservability: createInitialSymbolIndexRuntimeObservability(policy),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
if (policy.backend === "memory")
|
|
122
|
+
return;
|
|
123
|
+
if (session.persistentSymbolIndex)
|
|
124
|
+
return;
|
|
125
|
+
const cachePath = symbolIndexCachePath(policy, session.context.status.project);
|
|
126
|
+
if (!cachePath)
|
|
127
|
+
return;
|
|
128
|
+
try {
|
|
129
|
+
requestContext.checkpoint();
|
|
130
|
+
const store = await openSQLiteSymbolIndexStore(cachePath, {
|
|
131
|
+
busyTimeoutMs: policy.busy_timeout_ms,
|
|
132
|
+
});
|
|
133
|
+
try {
|
|
134
|
+
requestContext.checkpoint();
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
store.close();
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
session.persistentSymbolIndex = store;
|
|
141
|
+
const migrationPerformed = store.migrationPerformed;
|
|
142
|
+
session.context = {
|
|
143
|
+
...session.context,
|
|
144
|
+
status: session.context.status.causes.includes("index_failure")
|
|
145
|
+
? transitionProjectStatus(session.context.status, { type: "index_recovered" })
|
|
146
|
+
: transitionProjectStatus(session.context.status, { type: "index_ready" }),
|
|
147
|
+
symbolIndex: store,
|
|
148
|
+
symbolIndexBackend: "sqlite",
|
|
149
|
+
symbolIndexFallbackReason: null,
|
|
150
|
+
symbolIndexReady: false,
|
|
151
|
+
symbolIndexObservability: {
|
|
152
|
+
...session.context.symbolIndexObservability,
|
|
153
|
+
policy: policy.mode,
|
|
154
|
+
policy_reason: policy.reason,
|
|
155
|
+
backend: "sqlite",
|
|
156
|
+
state: "rebuilding",
|
|
157
|
+
operation: "miss",
|
|
158
|
+
last_operation: migrationPerformed ? "migration" : "miss",
|
|
159
|
+
migration_count: addSymbolIndexRuntimeCount(session.context.symbolIndexObservability.migration_count, migrationPerformed ? 1 : 0),
|
|
160
|
+
last_error: null,
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
if (isCooperativeInterruption(error))
|
|
166
|
+
throw error;
|
|
167
|
+
const reason = error.code ?? "sqlite_open_failed";
|
|
168
|
+
if (reason === "corrupt_storage" ||
|
|
169
|
+
reason === "unsupported_schema" ||
|
|
170
|
+
reason === "migration_failed" ||
|
|
171
|
+
reason === "read_failed") {
|
|
172
|
+
try {
|
|
173
|
+
await quarantineSQLiteSymbolIndexFile(cachePath);
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
// Keep the memory fallback even when the corrupt artifact cannot be moved.
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
fallbackToMemory(session, String(reason));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
10
182
|
export function resolveTsConfigPath(projectRoot) {
|
|
11
183
|
const abs = path.resolve(projectRoot);
|
|
12
184
|
const stat = fs.existsSync(abs) ? fs.statSync(abs) : null;
|
|
@@ -27,70 +199,219 @@ function buildProjectContext(tsConfigFilePath) {
|
|
|
27
199
|
const projectRoot = path.dirname(tsConfigFilePath);
|
|
28
200
|
projectRoots.set(project, projectRoot);
|
|
29
201
|
const status = createInitialProjectStatus(createProjectIdentity({ projectRoot, configPath: tsConfigFilePath }), { sourceCount: project.getSourceFiles().length });
|
|
30
|
-
return {
|
|
202
|
+
return {
|
|
203
|
+
project,
|
|
204
|
+
projectRoot,
|
|
205
|
+
tsConfigFilePath,
|
|
206
|
+
status,
|
|
207
|
+
symbolIndex: new InMemorySymbolIndex(),
|
|
208
|
+
symbolIndexReady: false,
|
|
209
|
+
symbolIndexBackend: "memory",
|
|
210
|
+
symbolIndexFallbackReason: null,
|
|
211
|
+
symbolIndexObservability: createInitialSymbolIndexRuntimeObservability(),
|
|
212
|
+
};
|
|
31
213
|
}
|
|
32
214
|
export function createFreshProject(projectRoot) {
|
|
33
215
|
return buildProjectContext(resolveTsConfigPath(projectRoot));
|
|
34
216
|
}
|
|
35
|
-
function
|
|
36
|
-
|
|
217
|
+
function closeProjectSession(session) {
|
|
218
|
+
session.watcher.close();
|
|
219
|
+
closePersistentSymbolIndex(session);
|
|
220
|
+
}
|
|
221
|
+
function createSessionWatcher(getSession, projectRoot, directories) {
|
|
222
|
+
return createProjectWatcher({
|
|
223
|
+
projectRoot,
|
|
224
|
+
directories,
|
|
225
|
+
onChange: (files) => {
|
|
226
|
+
const session = getSession();
|
|
227
|
+
session.context = {
|
|
228
|
+
...session.context,
|
|
229
|
+
status: transitionProjectStatus(session.context.status, {
|
|
230
|
+
type: "source_changed",
|
|
231
|
+
files,
|
|
232
|
+
}),
|
|
233
|
+
};
|
|
234
|
+
},
|
|
235
|
+
onError: (error) => {
|
|
236
|
+
const session = getSession();
|
|
237
|
+
session.context = {
|
|
238
|
+
...session.context,
|
|
239
|
+
status: transitionProjectStatus(session.context.status, {
|
|
240
|
+
type: "watcher_failed",
|
|
241
|
+
error: error instanceof Error ? error.message : String(error),
|
|
242
|
+
}),
|
|
243
|
+
};
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
function sameDirectories(left, right) {
|
|
248
|
+
return (left.length === right.length && left.every((directory, index) => directory === right[index]));
|
|
249
|
+
}
|
|
250
|
+
function replaceSessionWatcher(session, directories) {
|
|
251
|
+
if (sameDirectories(session.watchDirectories, directories) &&
|
|
252
|
+
session.watcher.snapshot().state !== "failed") {
|
|
37
253
|
return;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
254
|
+
}
|
|
255
|
+
const previous = session.watcher;
|
|
256
|
+
const replacement = createSessionWatcher(() => session, session.context.projectRoot, directories);
|
|
257
|
+
session.watcher = replacement;
|
|
258
|
+
session.watchDirectories = directories;
|
|
259
|
+
replacement.start();
|
|
260
|
+
previous.close();
|
|
261
|
+
if (replacement.snapshot().state === "ready") {
|
|
262
|
+
session.context = {
|
|
263
|
+
...session.context,
|
|
264
|
+
status: transitionProjectStatus(session.context.status, { type: "watcher_recovered" }),
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
function sessionHasAdmittedOperations(session) {
|
|
269
|
+
const snapshot = session.scheduler.snapshot();
|
|
270
|
+
return snapshot.active_operations + snapshot.queued_operations > 0;
|
|
271
|
+
}
|
|
272
|
+
function ensureProjectSessionCapacity(sessionCapacity) {
|
|
273
|
+
while (projectSessions.size >= sessionCapacity) {
|
|
274
|
+
const candidate = [...projectSessions.entries()]
|
|
275
|
+
.filter(([, session]) => !sessionHasAdmittedOperations(session))
|
|
276
|
+
.sort((left, right) => left[1].lastAccessSequence < right[1].lastAccessSequence
|
|
277
|
+
? -1
|
|
278
|
+
: left[1].lastAccessSequence > right[1].lastAccessSequence
|
|
279
|
+
? 1
|
|
280
|
+
: 0)[0];
|
|
281
|
+
if (!candidate)
|
|
282
|
+
throw new ProjectCapacityError();
|
|
283
|
+
closeProjectSession(candidate[1]);
|
|
42
284
|
projectSessions.delete(candidate[0]);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
function assertProjectRuntimeAdmissionOpen() {
|
|
288
|
+
if (projectRuntimeAdmission === "closed") {
|
|
289
|
+
throw new ProjectOperationSchedulerError("SERVER_SHUTTING_DOWN");
|
|
290
|
+
}
|
|
43
291
|
}
|
|
44
|
-
function getOrCreateSession(projectRoot) {
|
|
292
|
+
function getOrCreateSession(projectRoot, requestContext = NO_REQUEST_CONTEXT) {
|
|
293
|
+
assertProjectRuntimeAdmissionOpen();
|
|
294
|
+
requestContext.checkpoint();
|
|
45
295
|
const tsConfigFilePath = resolveTsConfigPath(projectRoot);
|
|
46
296
|
const existing = projectSessions.get(tsConfigFilePath);
|
|
47
297
|
if (existing)
|
|
48
298
|
return existing;
|
|
49
|
-
|
|
299
|
+
const runtimePolicy = getProjectRuntimePolicy();
|
|
300
|
+
ensureProjectSessionCapacity(runtimePolicy.maxProjectSessions);
|
|
301
|
+
const symbolIndexPolicy = readSymbolIndexPersistencePolicy();
|
|
302
|
+
requestContext.checkpoint();
|
|
303
|
+
const baseContext = buildProjectContext(tsConfigFilePath);
|
|
304
|
+
const context = {
|
|
305
|
+
...baseContext,
|
|
306
|
+
symbolIndexObservability: createInitialSymbolIndexRuntimeObservability(symbolIndexPolicy),
|
|
307
|
+
};
|
|
308
|
+
const watchDirectories = selectProjectWatchDirectories(context.projectRoot, context.project.getSourceFiles().map((sourceFile) => sourceFile.getFilePath()));
|
|
309
|
+
const watcher = createSessionWatcher(() => session, context.projectRoot, watchDirectories);
|
|
50
310
|
const session = {
|
|
51
|
-
context
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
311
|
+
context,
|
|
312
|
+
watcher,
|
|
313
|
+
watchDirectories,
|
|
314
|
+
configDigest: (() => {
|
|
315
|
+
requestContext.checkpoint();
|
|
316
|
+
return createConfigSnapshot(tsConfigFilePath).digest;
|
|
317
|
+
})(),
|
|
318
|
+
fingerprints: new Map(),
|
|
319
|
+
compilerStateUntrusted: false,
|
|
320
|
+
scheduler: new ProjectOperationScheduler({
|
|
321
|
+
queueCapacity: runtimePolicy.maxQueuedOperationsPerProject,
|
|
322
|
+
queueWaitTimeoutMs: runtimePolicy.queueWaitTimeoutMs,
|
|
323
|
+
operationDeadlineMs: runtimePolicy.operationDeadlineMs,
|
|
324
|
+
}),
|
|
325
|
+
lastAccessSequence: nextProjectSessionAccessSequence(),
|
|
326
|
+
symbolIndexPolicy,
|
|
58
327
|
};
|
|
59
328
|
projectSessions.set(tsConfigFilePath, session);
|
|
329
|
+
session.watcher.start();
|
|
330
|
+
if (session.watcher.snapshot().state === "ready") {
|
|
331
|
+
session.context = {
|
|
332
|
+
...session.context,
|
|
333
|
+
status: transitionProjectStatus(session.context.status, { type: "watcher_recovered" }),
|
|
334
|
+
};
|
|
335
|
+
}
|
|
60
336
|
return session;
|
|
61
337
|
}
|
|
62
338
|
function getQueueStatus(session) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
339
|
+
const snapshot = session.scheduler.snapshot();
|
|
340
|
+
return projectOperationQueueToProjection(snapshot, snapshot.queue_capacity);
|
|
341
|
+
}
|
|
342
|
+
export function getProjectOperationQueueSnapshot(projectRoot) {
|
|
343
|
+
const session = projectSessions.get(resolveTsConfigPath(projectRoot));
|
|
344
|
+
if (!session)
|
|
345
|
+
throw new Error("Project session not found.");
|
|
346
|
+
return getQueueStatus(session);
|
|
68
347
|
}
|
|
69
|
-
|
|
348
|
+
export function getProjectSessionRegistrySnapshot() {
|
|
349
|
+
const activeSessions = [...projectSessions.values()].filter((session) => sessionHasAdmittedOperations(session)).length;
|
|
350
|
+
return Object.freeze({
|
|
351
|
+
session_count: projectSessions.size,
|
|
352
|
+
active_sessions: activeSessions,
|
|
353
|
+
idle_sessions: projectSessions.size - activeSessions,
|
|
354
|
+
session_capacity: getProjectRuntimePolicy().maxProjectSessions,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
async function synchronizeSession(session, requestContext) {
|
|
70
358
|
let syncFailureCause = "compiler_rebuild";
|
|
71
359
|
let currentConfigDigest;
|
|
360
|
+
let compilerStateMayHaveAdvanced = false;
|
|
72
361
|
try {
|
|
362
|
+
requestContext.checkpoint();
|
|
73
363
|
currentConfigDigest = createConfigSnapshot(session.context.tsConfigFilePath).digest;
|
|
74
|
-
|
|
75
|
-
|
|
364
|
+
const compilerConfigChanged = currentConfigDigest !== session.configDigest;
|
|
365
|
+
if (compilerConfigChanged || session.compilerStateUntrusted) {
|
|
366
|
+
let status = session.context.status;
|
|
367
|
+
if (compilerConfigChanged) {
|
|
368
|
+
status = transitionProjectStatus(status, { type: "config_changed" });
|
|
369
|
+
}
|
|
76
370
|
status = transitionProjectStatus(status, { type: "compiler_rebuild_started" });
|
|
371
|
+
const symbolIndexObservability = session.context.symbolIndexObservability;
|
|
372
|
+
closePersistentSymbolIndex(session);
|
|
77
373
|
const rebuilt = buildProjectContext(session.context.tsConfigFilePath);
|
|
78
|
-
session.context = { ...rebuilt, status };
|
|
374
|
+
session.context = { ...rebuilt, status, symbolIndexObservability };
|
|
375
|
+
session.compilerStateUntrusted = false;
|
|
79
376
|
}
|
|
80
377
|
const { project, tsConfigFilePath } = session.context;
|
|
81
378
|
syncFailureCause = "source_change";
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
379
|
+
requestContext.checkpoint();
|
|
380
|
+
compilerStateMayHaveAdvanced = true;
|
|
381
|
+
project.addSourceFilesFromTsConfig(tsConfigFilePath);
|
|
382
|
+
for (const sourceFile of project.getSourceFiles()) {
|
|
383
|
+
requestContext.checkpoint();
|
|
384
|
+
if (!fs.existsSync(sourceFile.getFilePath()))
|
|
385
|
+
project.removeSourceFile(sourceFile);
|
|
386
|
+
}
|
|
387
|
+
requestContext.checkpoint();
|
|
388
|
+
replaceSessionWatcher(session, selectProjectWatchDirectories(session.context.projectRoot, project.getSourceFiles().map((sourceFile) => sourceFile.getFilePath())));
|
|
389
|
+
requestContext.checkpoint();
|
|
390
|
+
const snapshot = createWorkspaceSnapshot(session.context, {
|
|
391
|
+
previousFingerprints: session.fingerprints,
|
|
392
|
+
verifyContentHash: true,
|
|
393
|
+
});
|
|
88
394
|
if (snapshot.configDigest !== currentConfigDigest) {
|
|
89
395
|
syncFailureCause = "config_change";
|
|
90
396
|
throw new Error("Project configuration changed during synchronization. Retry the operation.");
|
|
91
397
|
}
|
|
92
|
-
|
|
93
|
-
|
|
398
|
+
const changedPaths = new Set([
|
|
399
|
+
...snapshot.fingerprintChanges.added,
|
|
400
|
+
...snapshot.fingerprintChanges.changed,
|
|
401
|
+
]);
|
|
402
|
+
requestContext.checkpoint();
|
|
403
|
+
await Promise.all(project.getSourceFiles().map(async (sourceFile) => {
|
|
404
|
+
requestContext.checkpoint();
|
|
405
|
+
const absoluteFilePath = fs.realpathSync(sourceFile.getFilePath());
|
|
406
|
+
if (changedPaths.has(absoluteFilePath))
|
|
407
|
+
await sourceFile.refreshFromFileSystem();
|
|
408
|
+
}));
|
|
409
|
+
requestContext.checkpoint();
|
|
410
|
+
const verificationSnapshot = createWorkspaceSnapshot(session.context, {
|
|
411
|
+
previousFingerprints: snapshot.fingerprints,
|
|
412
|
+
verifyContentHash: true,
|
|
413
|
+
});
|
|
414
|
+
requestContext.checkpoint();
|
|
94
415
|
if (verificationSnapshot.configDigest !== currentConfigDigest) {
|
|
95
416
|
syncFailureCause = "config_change";
|
|
96
417
|
throw new Error("Project configuration changed during synchronization. Retry the operation.");
|
|
@@ -99,6 +420,165 @@ async function synchronizeSession(session) {
|
|
|
99
420
|
syncFailureCause = "source_change";
|
|
100
421
|
throw new Error("Project source changed during synchronization. Retry the operation.");
|
|
101
422
|
}
|
|
423
|
+
requestContext.checkpoint();
|
|
424
|
+
await ensureSessionSymbolIndex(session, requestContext);
|
|
425
|
+
const sourceFiles = session.context.project.getSourceFiles();
|
|
426
|
+
const currentIndexFiles = sourceFiles.map((sourceFile) => {
|
|
427
|
+
requestContext.checkpoint();
|
|
428
|
+
const absoluteFilePath = fs.realpathSync(sourceFile.getFilePath());
|
|
429
|
+
const fingerprint = verificationSnapshot.fingerprints.get(absoluteFilePath);
|
|
430
|
+
if (!fingerprint) {
|
|
431
|
+
throw new Error(`No verified fingerprint found for ${absoluteFilePath}.`);
|
|
432
|
+
}
|
|
433
|
+
return {
|
|
434
|
+
file_path: path
|
|
435
|
+
.relative(session.context.projectRoot, sourceFile.getFilePath())
|
|
436
|
+
.replaceAll(path.sep, "/"),
|
|
437
|
+
content_hash: fingerprint.content_hash,
|
|
438
|
+
};
|
|
439
|
+
});
|
|
440
|
+
let existingIndexEntries;
|
|
441
|
+
try {
|
|
442
|
+
requestContext.checkpoint();
|
|
443
|
+
existingIndexEntries = await session.context.symbolIndex.load(session.context.status.project, SYMBOL_INDEX_SCHEMA_VERSION);
|
|
444
|
+
requestContext.checkpoint();
|
|
445
|
+
}
|
|
446
|
+
catch (error) {
|
|
447
|
+
if (isCooperativeInterruption(error))
|
|
448
|
+
throw error;
|
|
449
|
+
if (session.context.symbolIndexBackend !== "sqlite")
|
|
450
|
+
throw error;
|
|
451
|
+
fallbackToMemory(session, error.code ?? "sqlite_read_failed");
|
|
452
|
+
requestContext.checkpoint();
|
|
453
|
+
existingIndexEntries = await session.context.symbolIndex.load(session.context.status.project, SYMBOL_INDEX_SCHEMA_VERSION);
|
|
454
|
+
requestContext.checkpoint();
|
|
455
|
+
}
|
|
456
|
+
if (session.context.symbolIndexBackend === "sqlite") {
|
|
457
|
+
const cacheWarm = existingIndexEntries.length > 0;
|
|
458
|
+
session.context = {
|
|
459
|
+
...session.context,
|
|
460
|
+
symbolIndexObservability: {
|
|
461
|
+
...session.context.symbolIndexObservability,
|
|
462
|
+
state: "rebuilding",
|
|
463
|
+
operation: cacheWarm ? "hit" : "miss",
|
|
464
|
+
last_operation: cacheWarm ? "hit" : "miss",
|
|
465
|
+
loaded_entries: addSymbolIndexRuntimeCount(session.context.symbolIndexObservability.loaded_entries, existingIndexEntries.length),
|
|
466
|
+
accepted_entries: addSymbolIndexRuntimeCount(session.context.symbolIndexObservability.accepted_entries, existingIndexEntries.length),
|
|
467
|
+
cache_hits: addSymbolIndexRuntimeCount(session.context.symbolIndexObservability.cache_hits, cacheWarm ? 1 : 0),
|
|
468
|
+
cache_misses: addSymbolIndexRuntimeCount(session.context.symbolIndexObservability.cache_misses, cacheWarm ? 0 : 1),
|
|
469
|
+
},
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
const existingByPath = new Map(existingIndexEntries.map((entry) => [entry.file_path, entry]));
|
|
473
|
+
const fingerprintChangedFiles = new Set([
|
|
474
|
+
...verificationSnapshot.fingerprintChanges.added,
|
|
475
|
+
...verificationSnapshot.fingerprintChanges.changed,
|
|
476
|
+
].map((filePath) => path.relative(session.context.projectRoot, filePath).replaceAll(path.sep, "/")));
|
|
477
|
+
const configChanged = existingIndexEntries.some((entry) => entry.config_digest !== verificationSnapshot.configDigest);
|
|
478
|
+
const filesToRebuild = new Set(currentIndexFiles
|
|
479
|
+
.filter((file) => {
|
|
480
|
+
const previous = existingByPath.get(file.file_path);
|
|
481
|
+
return (configChanged ||
|
|
482
|
+
fingerprintChangedFiles.has(file.file_path) ||
|
|
483
|
+
!previous ||
|
|
484
|
+
previous.content_hash !== file.content_hash ||
|
|
485
|
+
previous.config_digest !== verificationSnapshot.configDigest);
|
|
486
|
+
})
|
|
487
|
+
.map((file) => file.file_path));
|
|
488
|
+
const indexFiles = sourceFiles
|
|
489
|
+
.filter((sourceFile) => {
|
|
490
|
+
requestContext.checkpoint();
|
|
491
|
+
const filePath = path
|
|
492
|
+
.relative(session.context.projectRoot, sourceFile.getFilePath())
|
|
493
|
+
.replaceAll(path.sep, "/");
|
|
494
|
+
return filesToRebuild.has(filePath);
|
|
495
|
+
})
|
|
496
|
+
.map((sourceFile) => {
|
|
497
|
+
requestContext.checkpoint();
|
|
498
|
+
const absoluteFilePath = fs.realpathSync(sourceFile.getFilePath());
|
|
499
|
+
const fingerprint = verificationSnapshot.fingerprints.get(absoluteFilePath);
|
|
500
|
+
if (!fingerprint) {
|
|
501
|
+
throw new Error(`No verified fingerprint found for ${absoluteFilePath}.`);
|
|
502
|
+
}
|
|
503
|
+
return {
|
|
504
|
+
file_path: path
|
|
505
|
+
.relative(session.context.projectRoot, sourceFile.getFilePath())
|
|
506
|
+
.replaceAll(path.sep, "/"),
|
|
507
|
+
content_hash: fingerprint.content_hash,
|
|
508
|
+
symbols: sourceFileIndexSymbols(sourceFile, session.context.projectRoot),
|
|
509
|
+
};
|
|
510
|
+
});
|
|
511
|
+
try {
|
|
512
|
+
requestContext.checkpoint();
|
|
513
|
+
const refreshResult = await session.context.symbolIndex.refresh({
|
|
514
|
+
project: session.context.status.project,
|
|
515
|
+
config_digest: verificationSnapshot.configDigest,
|
|
516
|
+
current_files: currentIndexFiles,
|
|
517
|
+
files: indexFiles,
|
|
518
|
+
last_indexed_at: new Date().toISOString(),
|
|
519
|
+
});
|
|
520
|
+
requestContext.checkpoint();
|
|
521
|
+
await session.context.symbolIndex.flush();
|
|
522
|
+
requestContext.checkpoint();
|
|
523
|
+
session.context = {
|
|
524
|
+
...session.context,
|
|
525
|
+
symbolIndexReady: true,
|
|
526
|
+
symbolIndexObservability: session.context.symbolIndexBackend === "sqlite"
|
|
527
|
+
? {
|
|
528
|
+
...session.context.symbolIndexObservability,
|
|
529
|
+
state: "ready",
|
|
530
|
+
operation: refreshResult.rebuilt_files.length > 0 ? "rebuild" : "hit",
|
|
531
|
+
last_operation: refreshResult.rebuilt_files.length > 0 ? "rebuild" : "hit",
|
|
532
|
+
last_successful_persistence_at: new Date().toISOString(),
|
|
533
|
+
rebuilt_files: addSymbolIndexRuntimeCount(session.context.symbolIndexObservability.rebuilt_files, refreshResult.rebuilt_files.length),
|
|
534
|
+
reused_files: addSymbolIndexRuntimeCount(session.context.symbolIndexObservability.reused_files, refreshResult.reused_files.length),
|
|
535
|
+
removed_files: addSymbolIndexRuntimeCount(session.context.symbolIndexObservability.removed_files, refreshResult.removed_files.length),
|
|
536
|
+
}
|
|
537
|
+
: session.context.symbolIndexObservability,
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
catch (error) {
|
|
541
|
+
if (isCooperativeInterruption(error))
|
|
542
|
+
throw error;
|
|
543
|
+
if (session.context.symbolIndexBackend === "sqlite") {
|
|
544
|
+
fallbackToMemory(session, error.code ?? "sqlite_write_failed");
|
|
545
|
+
try {
|
|
546
|
+
const fallbackFiles = sourceFiles.map((sourceFile) => {
|
|
547
|
+
requestContext.checkpoint();
|
|
548
|
+
const absoluteFilePath = fs.realpathSync(sourceFile.getFilePath());
|
|
549
|
+
const fingerprint = verificationSnapshot.fingerprints.get(absoluteFilePath);
|
|
550
|
+
if (!fingerprint) {
|
|
551
|
+
throw new Error(`No verified fingerprint found for ${absoluteFilePath}.`);
|
|
552
|
+
}
|
|
553
|
+
return {
|
|
554
|
+
file_path: path
|
|
555
|
+
.relative(session.context.projectRoot, sourceFile.getFilePath())
|
|
556
|
+
.replaceAll(path.sep, "/"),
|
|
557
|
+
content_hash: fingerprint.content_hash,
|
|
558
|
+
symbols: sourceFileIndexSymbols(sourceFile, session.context.projectRoot),
|
|
559
|
+
};
|
|
560
|
+
});
|
|
561
|
+
requestContext.checkpoint();
|
|
562
|
+
await session.context.symbolIndex.refresh({
|
|
563
|
+
project: session.context.status.project,
|
|
564
|
+
config_digest: verificationSnapshot.configDigest,
|
|
565
|
+
current_files: currentIndexFiles,
|
|
566
|
+
files: fallbackFiles,
|
|
567
|
+
last_indexed_at: new Date().toISOString(),
|
|
568
|
+
});
|
|
569
|
+
requestContext.checkpoint();
|
|
570
|
+
session.context = { ...session.context, symbolIndexReady: true };
|
|
571
|
+
}
|
|
572
|
+
catch (fallbackError) {
|
|
573
|
+
if (isCooperativeInterruption(fallbackError))
|
|
574
|
+
throw fallbackError;
|
|
575
|
+
session.context = { ...session.context, symbolIndexReady: false };
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
else {
|
|
579
|
+
session.context = { ...session.context, symbolIndexReady: false };
|
|
580
|
+
}
|
|
581
|
+
}
|
|
102
582
|
const status = transitionProjectStatus(session.context.status, {
|
|
103
583
|
type: "sync_succeeded",
|
|
104
584
|
sourceCount: verificationSnapshot.sourceFileCount,
|
|
@@ -110,8 +590,14 @@ async function synchronizeSession(session) {
|
|
|
110
590
|
});
|
|
111
591
|
session.context = { ...session.context, status };
|
|
112
592
|
session.configDigest = verificationSnapshot.configDigest;
|
|
593
|
+
session.fingerprints = verificationSnapshot.fingerprints;
|
|
594
|
+
session.compilerStateUntrusted = false;
|
|
113
595
|
}
|
|
114
596
|
catch (error) {
|
|
597
|
+
if (compilerStateMayHaveAdvanced)
|
|
598
|
+
session.compilerStateUntrusted = true;
|
|
599
|
+
if (isCooperativeInterruption(error))
|
|
600
|
+
throw error;
|
|
115
601
|
const status = transitionProjectStatus(session.context.status, {
|
|
116
602
|
type: "sync_failed",
|
|
117
603
|
cause: syncFailureCause,
|
|
@@ -121,43 +607,66 @@ async function synchronizeSession(session) {
|
|
|
121
607
|
throw error;
|
|
122
608
|
}
|
|
123
609
|
}
|
|
124
|
-
async function runSessionWithSyncPolicy(session, operation, allowSynchronizationFailure) {
|
|
125
|
-
|
|
126
|
-
session.
|
|
127
|
-
|
|
128
|
-
const previous = session.queue;
|
|
129
|
-
session.queue = new Promise((resolve) => {
|
|
130
|
-
release = resolve;
|
|
131
|
-
});
|
|
132
|
-
await previous;
|
|
133
|
-
session.queuedOperations -= 1;
|
|
134
|
-
session.runningOperations += 1;
|
|
135
|
-
try {
|
|
610
|
+
async function runSessionWithSyncPolicy(session, operation, allowSynchronizationFailure, requestContext) {
|
|
611
|
+
requestContext.checkpoint();
|
|
612
|
+
return session.scheduler.run(async (operationContext) => {
|
|
613
|
+
operationContext.checkpoint();
|
|
136
614
|
try {
|
|
137
|
-
await synchronizeSession(session);
|
|
615
|
+
await synchronizeSession(session, operationContext);
|
|
138
616
|
}
|
|
139
617
|
catch (error) {
|
|
140
|
-
if (!allowSynchronizationFailure)
|
|
618
|
+
if (!allowSynchronizationFailure || isCooperativeInterruption(error))
|
|
141
619
|
throw error;
|
|
142
620
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
621
|
+
operationContext.checkpoint();
|
|
622
|
+
operationContext.markExecuting();
|
|
623
|
+
session.lastAccessSequence = nextProjectSessionAccessSequence();
|
|
624
|
+
return operation(session.context, operationContext);
|
|
625
|
+
}, { signal: requestContext.signal });
|
|
626
|
+
}
|
|
627
|
+
export async function withProject(projectRoot, operation, requestContext = NO_REQUEST_CONTEXT) {
|
|
628
|
+
requestContext.checkpoint();
|
|
629
|
+
return runSessionWithSyncPolicy(getOrCreateSession(projectRoot, requestContext), operation, false, requestContext);
|
|
151
630
|
}
|
|
152
|
-
export async function
|
|
153
|
-
|
|
631
|
+
export async function withProjectOperation(projectRoot, operation, requestContext = NO_REQUEST_CONTEXT) {
|
|
632
|
+
requestContext.checkpoint();
|
|
633
|
+
const session = getOrCreateSession(projectRoot, requestContext);
|
|
634
|
+
return session.scheduler.run((operationContext) => {
|
|
635
|
+
operationContext.checkpoint();
|
|
636
|
+
operationContext.markExecuting();
|
|
637
|
+
session.lastAccessSequence = nextProjectSessionAccessSequence();
|
|
638
|
+
return operation(operationContext);
|
|
639
|
+
}, { signal: requestContext.signal });
|
|
154
640
|
}
|
|
155
|
-
export async function getProjectStatus(projectRoot) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
641
|
+
export async function getProjectStatus(projectRoot, requestContext = NO_REQUEST_CONTEXT) {
|
|
642
|
+
requestContext.checkpoint();
|
|
643
|
+
const session = getOrCreateSession(projectRoot, requestContext);
|
|
644
|
+
return runSessionWithSyncPolicy(session, async (context, operationContext) => {
|
|
645
|
+
let effectiveContext = context;
|
|
646
|
+
let indexedCount = 0;
|
|
647
|
+
if (context.symbolIndexBackend === "sqlite" && context.symbolIndexReady) {
|
|
648
|
+
try {
|
|
649
|
+
operationContext.checkpoint();
|
|
650
|
+
indexedCount = (await context.symbolIndex.load(context.status.project, SYMBOL_INDEX_SCHEMA_VERSION)).length;
|
|
651
|
+
operationContext.checkpoint();
|
|
652
|
+
}
|
|
653
|
+
catch (error) {
|
|
654
|
+
if (isCooperativeInterruption(error))
|
|
655
|
+
throw error;
|
|
656
|
+
fallbackToMemory(session, error.code ?? "sqlite_read_failed");
|
|
657
|
+
effectiveContext = session.context;
|
|
658
|
+
indexedCount = 0;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
return {
|
|
662
|
+
...projectStatusToProjection(effectiveContext.status),
|
|
663
|
+
index: { state: effectiveContext.symbolIndexObservability.state },
|
|
664
|
+
indexed_count: indexedCount,
|
|
665
|
+
last_successful_index_at: effectiveContext.symbolIndexObservability.last_successful_persistence_at,
|
|
666
|
+
index_observability: { ...effectiveContext.symbolIndexObservability },
|
|
667
|
+
operation_queue: getQueueStatus(session),
|
|
668
|
+
};
|
|
669
|
+
}, true, requestContext);
|
|
161
670
|
}
|
|
162
671
|
// Compatibility API for internal scripts. MCP tools should use withProject().
|
|
163
672
|
export function getProject(projectRoot) {
|
|
@@ -165,10 +674,81 @@ export function getProject(projectRoot) {
|
|
|
165
674
|
}
|
|
166
675
|
export function invalidateProject(projectRoot) {
|
|
167
676
|
const tsConfigFilePath = resolveTsConfigPath(projectRoot);
|
|
677
|
+
const session = projectSessions.get(tsConfigFilePath);
|
|
678
|
+
if (session)
|
|
679
|
+
closeProjectSession(session);
|
|
680
|
+
projectSessions.delete(tsConfigFilePath);
|
|
681
|
+
}
|
|
682
|
+
export function invalidateProjectIfIdle(projectRoot, options = {}) {
|
|
683
|
+
const tsConfigFilePath = resolveTsConfigPath(projectRoot);
|
|
684
|
+
const session = projectSessions.get(tsConfigFilePath);
|
|
685
|
+
if (!session)
|
|
686
|
+
return true;
|
|
687
|
+
const queue = session.scheduler.snapshot();
|
|
688
|
+
if (queue.active_operations > 0 ||
|
|
689
|
+
queue.queued_operations > 0 ||
|
|
690
|
+
(options.preserveCancellationTelemetry === true && queue.cancelled_operations > 0)) {
|
|
691
|
+
return false;
|
|
692
|
+
}
|
|
693
|
+
closeProjectSession(session);
|
|
168
694
|
projectSessions.delete(tsConfigFilePath);
|
|
695
|
+
return true;
|
|
169
696
|
}
|
|
170
697
|
export function clearProjectSessions() {
|
|
698
|
+
for (const session of projectSessions.values()) {
|
|
699
|
+
closeProjectSession(session);
|
|
700
|
+
}
|
|
701
|
+
projectSessions.clear();
|
|
702
|
+
projectSessionAccessSequence = 0n;
|
|
703
|
+
projectRuntimePolicy = undefined;
|
|
704
|
+
projectRuntimeAdmission = "open";
|
|
705
|
+
}
|
|
706
|
+
export function prepareProjectRuntimeForStartup() {
|
|
707
|
+
if (projectSessions.size !== 0) {
|
|
708
|
+
throw new Error("Cannot start a project runtime while sessions remain open.");
|
|
709
|
+
}
|
|
710
|
+
projectRuntimeAdmission = "open";
|
|
711
|
+
}
|
|
712
|
+
export function getProjectRuntimeShutdownSnapshot() {
|
|
713
|
+
let activeOperations = 0;
|
|
714
|
+
let queuedOperations = 0;
|
|
715
|
+
let completionCriticalOperations = 0;
|
|
716
|
+
for (const session of projectSessions.values()) {
|
|
717
|
+
const snapshot = session.scheduler.shutdownSnapshot();
|
|
718
|
+
activeOperations += snapshot.active_operations;
|
|
719
|
+
queuedOperations += snapshot.queued_operations;
|
|
720
|
+
completionCriticalOperations += snapshot.completion_critical_operations;
|
|
721
|
+
}
|
|
722
|
+
return Object.freeze({
|
|
723
|
+
admission: projectRuntimeAdmission,
|
|
724
|
+
session_count: projectSessions.size,
|
|
725
|
+
active_operations: activeOperations,
|
|
726
|
+
queued_operations: queuedOperations,
|
|
727
|
+
completion_critical_operations: completionCriticalOperations,
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
export function beginProjectShutdown() {
|
|
731
|
+
projectRuntimeAdmission = "closed";
|
|
732
|
+
for (const session of projectSessions.values())
|
|
733
|
+
session.scheduler.beginShutdown();
|
|
734
|
+
return getProjectRuntimeShutdownSnapshot();
|
|
735
|
+
}
|
|
736
|
+
export async function waitForProjectOperationsToDrain() {
|
|
737
|
+
await Promise.all([...projectSessions.values()].map((session) => session.scheduler.waitForIdle()));
|
|
738
|
+
}
|
|
739
|
+
export async function waitForProjectCompletionCriticalOperationsToDrain() {
|
|
740
|
+
await Promise.all([...projectSessions.values()].map((session) => session.scheduler.waitForCompletionCriticalOperationsToDrain()));
|
|
741
|
+
}
|
|
742
|
+
export function closeDrainedProjectSessions() {
|
|
743
|
+
const snapshot = getProjectRuntimeShutdownSnapshot();
|
|
744
|
+
if (snapshot.active_operations !== 0 || snapshot.queued_operations !== 0) {
|
|
745
|
+
throw new Error("Cannot close project sessions while operations are active.");
|
|
746
|
+
}
|
|
747
|
+
for (const session of projectSessions.values())
|
|
748
|
+
closeProjectSession(session);
|
|
171
749
|
projectSessions.clear();
|
|
750
|
+
projectSessionAccessSequence = 0n;
|
|
751
|
+
projectRuntimePolicy = undefined;
|
|
172
752
|
}
|
|
173
753
|
export function getSourceFileOrThrow(project, filePath) {
|
|
174
754
|
const projectRoot = projectRoots.get(project) ?? process.cwd();
|