@mrclrchtr/supi-code-intelligence 4.9.0 → 4.10.0

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.
Files changed (28) hide show
  1. package/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  2. package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  3. package/node_modules/@mrclrchtr/supi-core/src/debug-timing.ts +34 -21
  4. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  5. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  6. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug-timing.ts +34 -21
  7. package/node_modules/@mrclrchtr/supi-lsp/package.json +3 -3
  8. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-timing.ts +155 -0
  9. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostics.ts +70 -34
  10. package/node_modules/@mrclrchtr/supi-lsp/src/client/transport.ts +104 -5
  11. package/node_modules/@mrclrchtr/supi-tree-sitter/README.md +10 -0
  12. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  13. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  14. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/debug-timing.ts +34 -21
  15. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/debug/web-tree-sitter.wasm +0 -0
  16. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/debug/web-tree-sitter.wasm.map +4 -4
  17. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/package.json +1 -1
  18. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.wasm +0 -0
  19. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.wasm.map +4 -4
  20. package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +4 -3
  21. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/kotlin/tree-sitter-kotlin.wasm.json +1 -1
  22. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/sql/tree-sitter-sql.wasm.json +1 -1
  23. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/runtime.ts +106 -2
  24. package/package.json +5 -5
  25. package/src/analysis/refactor/apply.ts +14 -19
  26. package/src/analysis/refactor/position.ts +47 -0
  27. package/src/analysis/refactor/safety.ts +18 -10
  28. package/src/analysis/search/ast-scan-timing.ts +0 -2
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-code-runtime",
3
- "version": "4.9.0",
3
+ "version": "4.10.0",
4
4
  "description": "Shared workspace context and capability contracts for code intelligence",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-core",
3
- "version": "4.9.0",
3
+ "version": "4.10.0",
4
4
  "description": "Shared settings, configuration, reporting, and session infrastructure",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -46,13 +46,21 @@ export interface DebugTimer {
46
46
  * names are accumulated. Event data reserves the `timing` field. When Debug is
47
47
  * disabled at start, this returns a no-op timer and does not read the clock.
48
48
  * Pass a factory to `finish()` to avoid event-data construction when disabled.
49
+ * Clock, event-construction, and registry failures are isolated from the
50
+ * measured operation and make the timer a no-op.
49
51
  */
50
52
  export function startDebugTimer(options: DebugTimerOptions = {}): DebugTimer {
51
53
  if (!isDebugRegistryEnabled()) return DISABLED_DEBUG_TIMER;
52
54
  const now = options.now ?? performance.now.bind(performance);
53
- const startedAt = now();
55
+ let startedAt: number;
56
+ try {
57
+ startedAt = now();
58
+ } catch {
59
+ return DISABLED_DEBUG_TIMER;
60
+ }
54
61
  let previousAt = startedAt;
55
62
  let finished = false;
63
+ let failed = false;
56
64
  const phases = new Map<string, number>();
57
65
 
58
66
  const markAt = (phase: string, current: number): void => {
@@ -65,30 +73,35 @@ export function startDebugTimer(options: DebugTimerOptions = {}): DebugTimer {
65
73
  return {
66
74
  enabled: true,
67
75
  mark(phase) {
68
- if (finished) return;
69
- markAt(phase, now());
76
+ if (finished || failed) return;
77
+ try {
78
+ markAt(phase, now());
79
+ } catch {
80
+ failed = true;
81
+ }
70
82
  },
71
83
  finish(input, finalPhase) {
72
- if (finished) return null;
73
- if (!isDebugRegistryEnabled()) {
74
- finished = true;
84
+ if (finished || failed) return null;
85
+ finished = true;
86
+ try {
87
+ if (!isDebugRegistryEnabled()) return null;
88
+ const completedAt = now();
89
+ if (finalPhase) markAt(finalPhase, completedAt);
90
+ const phasesMs = Object.fromEntries(
91
+ [...phases.entries()].map(([name, value]) => [name, duration(value)]),
92
+ );
93
+ const timing: DebugTiming = {
94
+ durationMs: duration(completedAt - startedAt),
95
+ phasesMs,
96
+ };
97
+ const eventInput = typeof input === "function" ? input() : input;
98
+ return recordDebugEvent({
99
+ ...eventInput,
100
+ data: { ...eventInput.data, timing },
101
+ });
102
+ } catch {
75
103
  return null;
76
104
  }
77
- const completedAt = now();
78
- if (finalPhase) markAt(finalPhase, completedAt);
79
- finished = true;
80
- const phasesMs = Object.fromEntries(
81
- [...phases.entries()].map(([name, value]) => [name, duration(value)]),
82
- );
83
- const timing: DebugTiming = {
84
- durationMs: duration(completedAt - startedAt),
85
- phasesMs,
86
- };
87
- const eventInput = typeof input === "function" ? input() : input;
88
- return recordDebugEvent({
89
- ...eventInput,
90
- data: { ...eventInput.data, timing },
91
- });
92
105
  },
93
106
  };
94
107
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-code-runtime",
3
- "version": "4.9.0",
3
+ "version": "4.10.0",
4
4
  "description": "Shared workspace context and capability contracts for code intelligence",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-core",
3
- "version": "4.9.0",
3
+ "version": "4.10.0",
4
4
  "description": "Shared settings, configuration, reporting, and session infrastructure",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -46,13 +46,21 @@ export interface DebugTimer {
46
46
  * names are accumulated. Event data reserves the `timing` field. When Debug is
47
47
  * disabled at start, this returns a no-op timer and does not read the clock.
48
48
  * Pass a factory to `finish()` to avoid event-data construction when disabled.
49
+ * Clock, event-construction, and registry failures are isolated from the
50
+ * measured operation and make the timer a no-op.
49
51
  */
50
52
  export function startDebugTimer(options: DebugTimerOptions = {}): DebugTimer {
51
53
  if (!isDebugRegistryEnabled()) return DISABLED_DEBUG_TIMER;
52
54
  const now = options.now ?? performance.now.bind(performance);
53
- const startedAt = now();
55
+ let startedAt: number;
56
+ try {
57
+ startedAt = now();
58
+ } catch {
59
+ return DISABLED_DEBUG_TIMER;
60
+ }
54
61
  let previousAt = startedAt;
55
62
  let finished = false;
63
+ let failed = false;
56
64
  const phases = new Map<string, number>();
57
65
 
58
66
  const markAt = (phase: string, current: number): void => {
@@ -65,30 +73,35 @@ export function startDebugTimer(options: DebugTimerOptions = {}): DebugTimer {
65
73
  return {
66
74
  enabled: true,
67
75
  mark(phase) {
68
- if (finished) return;
69
- markAt(phase, now());
76
+ if (finished || failed) return;
77
+ try {
78
+ markAt(phase, now());
79
+ } catch {
80
+ failed = true;
81
+ }
70
82
  },
71
83
  finish(input, finalPhase) {
72
- if (finished) return null;
73
- if (!isDebugRegistryEnabled()) {
74
- finished = true;
84
+ if (finished || failed) return null;
85
+ finished = true;
86
+ try {
87
+ if (!isDebugRegistryEnabled()) return null;
88
+ const completedAt = now();
89
+ if (finalPhase) markAt(finalPhase, completedAt);
90
+ const phasesMs = Object.fromEntries(
91
+ [...phases.entries()].map(([name, value]) => [name, duration(value)]),
92
+ );
93
+ const timing: DebugTiming = {
94
+ durationMs: duration(completedAt - startedAt),
95
+ phasesMs,
96
+ };
97
+ const eventInput = typeof input === "function" ? input() : input;
98
+ return recordDebugEvent({
99
+ ...eventInput,
100
+ data: { ...eventInput.data, timing },
101
+ });
102
+ } catch {
75
103
  return null;
76
104
  }
77
- const completedAt = now();
78
- if (finalPhase) markAt(finalPhase, completedAt);
79
- finished = true;
80
- const phasesMs = Object.fromEntries(
81
- [...phases.entries()].map(([name, value]) => [name, duration(value)]),
82
- );
83
- const timing: DebugTiming = {
84
- durationMs: duration(completedAt - startedAt),
85
- phasesMs,
86
- };
87
- const eventInput = typeof input === "function" ? input() : input;
88
- return recordDebugEvent({
89
- ...eventInput,
90
- data: { ...eventInput.data, timing },
91
- });
92
105
  },
93
106
  };
94
107
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-lsp",
3
- "version": "4.9.0",
3
+ "version": "4.10.0",
4
4
  "description": "Language Server Protocol runtime for SuPi code intelligence",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -37,8 +37,8 @@
37
37
  "vscode-jsonrpc": "^9.0.0",
38
38
  "vscode-languageserver-protocol": "^3.17.5",
39
39
  "vscode-languageserver-types": "^3.17.5",
40
- "@mrclrchtr/supi-core": "4.9.0",
41
- "@mrclrchtr/supi-code-runtime": "4.9.0"
40
+ "@mrclrchtr/supi-code-runtime": "4.10.0",
41
+ "@mrclrchtr/supi-core": "4.10.0"
42
42
  },
43
43
  "bundledDependencies": [
44
44
  "@mrclrchtr/supi-code-runtime",
@@ -0,0 +1,155 @@
1
+ import { startDebugTimer } from "@mrclrchtr/supi-core/debug";
2
+
3
+ type DiagnosticCollection = "fallback" | "none" | "pull" | "push";
4
+ type DiagnosticFreshness = "not-observed" | "observed";
5
+ type DiagnosticOutcome = "completed" | "skipped" | "timed-out";
6
+ type DiagnosticPullOutcome = "completed" | "failed" | "not-supported" | "not-used" | "timed-out";
7
+ type DiagnosticPushOutcome = "not-used" | "published" | "released" | "settled" | "timed-out";
8
+ type DiagnosticSettleOutcome = "not-used" | "published" | "quiet" | "released" | "timed-out";
9
+ type DiagnosticTimingOperation = "refresh-open" | "sync-file";
10
+
11
+ /** Result of waiting for a quiet push-diagnostic window. */
12
+ export interface DiagnosticSettleResult {
13
+ readonly outcome: "quiet" | "timed-out";
14
+ readonly freshness: DiagnosticFreshness;
15
+ }
16
+
17
+ /** Result of waiting for one file's push diagnostics. */
18
+ export type DiagnosticPushWaitOutcome = "published" | "released" | "timed-out";
19
+
20
+ interface DiagnosticTimingData {
21
+ readonly collection: DiagnosticCollection;
22
+ readonly documentCount: number;
23
+ readonly fallback: boolean;
24
+ readonly freshness: DiagnosticFreshness;
25
+ readonly outcome: DiagnosticOutcome;
26
+ readonly pull: DiagnosticPullOutcome;
27
+ readonly push: DiagnosticPushOutcome;
28
+ readonly settle: DiagnosticSettleOutcome;
29
+ readonly timedOut: boolean;
30
+ }
31
+
32
+ /** Internal pull failure that retains only whether a timeout occurred. */
33
+ export class DiagnosticPullError extends Error {
34
+ constructor(readonly timedOut: boolean) {
35
+ super("pull diagnostics incomplete");
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Record one diagnostic operation without document identifiers or diagnostic text.
41
+ *
42
+ * The observer owns result classification so diagnostic control flow does not
43
+ * duplicate the event shape.
44
+ */
45
+ export class DiagnosticObserver {
46
+ readonly #timer = startDebugTimer();
47
+ #pull: "failed" | "not-supported" | "timed-out";
48
+
49
+ constructor(
50
+ readonly operation: DiagnosticTimingOperation,
51
+ readonly supportsPull: boolean,
52
+ ) {
53
+ this.#pull = supportsPull ? "failed" : "not-supported";
54
+ }
55
+
56
+ synchronized(): void {
57
+ this.#timer.mark("synchronize");
58
+ }
59
+
60
+ skipped(documentCount: number): void {
61
+ this.#finish({
62
+ collection: "none",
63
+ documentCount,
64
+ fallback: false,
65
+ freshness: "not-observed",
66
+ outcome: "skipped",
67
+ pull: "not-used",
68
+ push: "not-used",
69
+ settle: "not-used",
70
+ timedOut: false,
71
+ });
72
+ }
73
+
74
+ pullCompleted(documentCount: number): void {
75
+ this.#finish(
76
+ {
77
+ collection: "pull",
78
+ documentCount,
79
+ fallback: false,
80
+ freshness: "observed",
81
+ outcome: "completed",
82
+ pull: "completed",
83
+ push: "not-used",
84
+ settle: "not-used",
85
+ timedOut: false,
86
+ },
87
+ "pull",
88
+ );
89
+ }
90
+
91
+ pullFailed(error: unknown): void {
92
+ this.#pull = isDiagnosticTimeout(error) ? "timed-out" : "failed";
93
+ this.#timer.mark("pull");
94
+ }
95
+
96
+ pullTimedOut(): void {
97
+ this.#pull = "timed-out";
98
+ this.#timer.mark("pull");
99
+ }
100
+
101
+ pushSettled(documentCount: number, settle: DiagnosticSettleResult): void {
102
+ const timedOut = settle.outcome === "timed-out";
103
+ this.#finish(
104
+ {
105
+ collection: this.supportsPull ? "fallback" : "push",
106
+ documentCount,
107
+ fallback: this.supportsPull,
108
+ freshness: settle.freshness,
109
+ outcome: timedOut ? "timed-out" : "completed",
110
+ pull: this.#pull,
111
+ push: timedOut ? "timed-out" : "settled",
112
+ settle: settle.outcome,
113
+ timedOut: timedOut || this.#pull === "timed-out",
114
+ },
115
+ "push-settle",
116
+ );
117
+ }
118
+
119
+ pushWaitCompleted(documentCount: number, push: DiagnosticPushWaitOutcome): void {
120
+ const timedOut = push === "timed-out";
121
+ this.#finish(
122
+ {
123
+ collection: this.supportsPull ? "fallback" : "push",
124
+ documentCount,
125
+ fallback: this.supportsPull,
126
+ freshness: push === "published" ? "observed" : "not-observed",
127
+ outcome: timedOut ? "timed-out" : "completed",
128
+ pull: this.#pull,
129
+ push,
130
+ settle: push,
131
+ timedOut: timedOut || this.#pull === "timed-out",
132
+ },
133
+ "push-settle",
134
+ );
135
+ }
136
+
137
+ #finish(data: DiagnosticTimingData, finalPhase?: "pull" | "push-settle" | "synchronize"): void {
138
+ this.#timer.finish(
139
+ () => ({
140
+ source: "lsp",
141
+ level: "debug",
142
+ category: "diagnostics.timing",
143
+ message: `LSP diagnostic ${this.operation} ${data.outcome}`,
144
+ data: { operation: this.operation, ...data },
145
+ }),
146
+ finalPhase,
147
+ );
148
+ }
149
+ }
150
+
151
+ /** Return whether a diagnostic failure represents a timeout without retaining its message. */
152
+ export function isDiagnosticTimeout(error: unknown): boolean {
153
+ if (error instanceof DiagnosticPullError) return error.timedOut;
154
+ return error instanceof Error && /\btimed? ?out\b|\btimeout\b/i.test(error.message);
155
+ }
@@ -10,10 +10,18 @@ import type {
10
10
  VersionedTextDocumentIdentifier,
11
11
  } from "../config/types.ts";
12
12
  import { detectLanguageId, fileToUri, uriToFile } from "../utils.ts";
13
+ import {
14
+ DiagnosticObserver,
15
+ DiagnosticPullError,
16
+ type DiagnosticPushWaitOutcome,
17
+ type DiagnosticSettleResult,
18
+ isDiagnosticTimeout,
19
+ } from "./client-diagnostic-timing.ts";
13
20
 
14
21
  const DIAGNOSTIC_WAIT_MS = 3_000;
15
22
 
16
23
  type OpenDocument = { version: number };
24
+ type DiagnosticWaiter = (outcome: "published" | "released") => void;
17
25
 
18
26
  type DiagnosticCacheEntry = {
19
27
  diagnostics: Diagnostic[];
@@ -48,7 +56,7 @@ interface ClientDiagnosticsHost {
48
56
  export class ClientDiagnostics {
49
57
  readonly #openDocs = new Map<string, OpenDocument>();
50
58
  readonly #diagnosticStore = new Map<string, DiagnosticCacheEntry>();
51
- readonly #diagnosticWaiters = new Map<string, Array<() => void>>();
59
+ readonly #diagnosticWaiters = new Map<string, DiagnosticWaiter[]>();
52
60
 
53
61
  constructor(private readonly host: ClientDiagnosticsHost) {}
54
62
 
@@ -160,53 +168,72 @@ export class ClientDiagnostics {
160
168
  receivedAt: Date.now(),
161
169
  version: params.version ?? undefined,
162
170
  });
163
- this.#releaseDiagnosticWaiters(params.uri);
171
+ this.#releaseDiagnosticWaiters(params.uri, "published");
164
172
  }
165
173
 
166
174
  /** Re-read open documents, then collect pull diagnostics or wait for push diagnostics. */
167
175
  async refreshOpenDiagnostics(
168
176
  options: { maxWaitMs?: number; quietMs?: number } = {},
169
177
  ): Promise<void> {
170
- if (!this.host.isOperational()) return;
178
+ const supportsPull = this.host.supportsPullDiagnostics();
179
+ const observer = new DiagnosticObserver("refresh-open", supportsPull);
180
+ if (!this.host.isOperational()) {
181
+ observer.skipped(0);
182
+ return;
183
+ }
171
184
 
172
185
  const maxWaitMs = options.maxWaitMs ?? 3_000;
173
186
  const quietMs = options.quietMs ?? 200;
174
187
  const syncStart = Date.now();
175
-
176
188
  this.#resyncOpenDocuments();
177
- if (this.#openDocs.size === 0) return;
189
+ observer.synchronized();
190
+ const documentCount = this.#openDocs.size;
191
+ if (documentCount === 0) {
192
+ observer.skipped(0);
193
+ return;
194
+ }
178
195
 
179
- if (this.host.supportsPullDiagnostics()) {
196
+ if (supportsPull) {
180
197
  try {
181
198
  await this.#pullDiagnosticsForOpenDocuments(syncStart, maxWaitMs);
199
+ observer.pullCompleted(documentCount);
182
200
  return;
183
- } catch {
184
- // Pull diagnostics failed. Wait for push diagnostics instead.
201
+ } catch (error) {
202
+ observer.pullFailed(error);
185
203
  }
186
204
  }
187
205
 
188
- await this.#waitForDiagnosticSettle(syncStart, maxWaitMs, quietMs);
206
+ const settle = await this.#waitForDiagnosticSettle(syncStart, maxWaitMs, quietMs);
207
+ observer.pushSettled(documentCount, settle);
189
208
  }
190
209
 
191
210
  /** Sync one file and return its diagnostics after pull or push collection. */
192
211
  async syncAndWaitForDiagnostics(filePath: string, content: string): Promise<Diagnostic[]> {
212
+ const supportsPull = this.host.supportsPullDiagnostics();
213
+ const observer = new DiagnosticObserver("sync-file", supportsPull);
193
214
  const uri = fileToUri(filePath);
194
215
  const syncStart = Date.now();
195
216
  this.didChange(filePath, content);
217
+ observer.synchronized();
196
218
 
197
- if (this.host.supportsPullDiagnostics()) {
219
+ if (supportsPull) {
198
220
  const remaining = DIAGNOSTIC_WAIT_MS - (Date.now() - syncStart);
199
- if (remaining > 0) {
200
- try {
201
- const pulled = await this.#pullDiagnosticsForUri(uri, remaining);
202
- if (pulled) return this.getDiagnostics(filePath);
203
- } catch {
204
- // Pull diagnostics failed. Wait for push diagnostics instead.
205
- }
221
+ try {
222
+ if (remaining <= 0) observer.pullTimedOut();
223
+ else if (await this.#pullDiagnosticsForUri(uri, remaining)) {
224
+ observer.pullCompleted(1);
225
+ return this.getDiagnostics(filePath);
226
+ } else observer.pullFailed(undefined);
227
+ } catch (error) {
228
+ observer.pullFailed(error);
206
229
  }
207
230
  }
208
231
 
209
- await this.#waitForDiagnostics(uri, Math.max(0, DIAGNOSTIC_WAIT_MS - (Date.now() - syncStart)));
232
+ const push = await this.#waitForDiagnostics(
233
+ uri,
234
+ Math.max(0, DIAGNOSTIC_WAIT_MS - (Date.now() - syncStart)),
235
+ );
236
+ observer.pushWaitCompleted(1, push);
210
237
  return this.getDiagnostics(filePath);
211
238
  }
212
239
 
@@ -240,9 +267,9 @@ export class ClientDiagnostics {
240
267
  );
241
268
 
242
269
  const anySuccess = results.some((result) => result.status === "fulfilled" && result.value);
243
- const hadFailure = results.some((result) => result.status === "rejected");
244
- if ((hadFailure || !anySuccess) && uris.length > 0) {
245
- throw new Error("pull diagnostics incomplete");
270
+ const failures = results.filter((result) => result.status === "rejected");
271
+ if ((failures.length > 0 || !anySuccess) && uris.length > 0) {
272
+ throw new DiagnosticPullError(failures.some((result) => isDiagnosticTimeout(result.reason)));
246
273
  }
247
274
  }
248
275
 
@@ -280,16 +307,25 @@ export class ClientDiagnostics {
280
307
  syncStart: number,
281
308
  maxWaitMs: number,
282
309
  quietMs: number,
283
- ): Promise<void> {
310
+ ): Promise<DiagnosticSettleResult> {
284
311
  const deadline = syncStart + maxWaitMs;
285
312
  while (Date.now() < deadline) {
286
- const lastReceived = this.#lastDiagnosticReceivedAfter(syncStart) || syncStart;
287
- const elapsed = Date.now() - lastReceived;
288
- if (elapsed >= quietMs) return;
313
+ const observedAt = this.#lastDiagnosticReceivedAfter(syncStart);
314
+ const elapsed = Date.now() - (observedAt || syncStart);
315
+ if (elapsed >= quietMs) {
316
+ return {
317
+ outcome: "quiet",
318
+ freshness: observedAt > 0 ? "observed" : "not-observed",
319
+ };
320
+ }
289
321
  await new Promise((resolve) =>
290
322
  setTimeout(resolve, Math.min(quietMs - elapsed, deadline - Date.now(), 50)),
291
323
  );
292
324
  }
325
+ return {
326
+ outcome: "timed-out",
327
+ freshness: this.#lastDiagnosticReceivedAfter(syncStart) > 0 ? "observed" : "not-observed",
328
+ };
293
329
  }
294
330
 
295
331
  #lastDiagnosticReceivedAfter(afterTime: number): number {
@@ -319,18 +355,18 @@ export class ClientDiagnostics {
319
355
  this.#releaseDiagnosticWaiters(uri);
320
356
  }
321
357
 
322
- #waitForDiagnostics(uri: string, timeoutMs: number): Promise<void> {
323
- if (timeoutMs <= 0) return Promise.resolve();
358
+ #waitForDiagnostics(uri: string, timeoutMs: number): Promise<DiagnosticPushWaitOutcome> {
359
+ if (timeoutMs <= 0) return Promise.resolve("timed-out");
324
360
 
325
- return new Promise<void>((resolve) => {
326
- const waiter = () => {
361
+ return new Promise<DiagnosticPushWaitOutcome>((resolve) => {
362
+ const waiter: DiagnosticWaiter = (outcome) => {
327
363
  clearTimeout(timer);
328
364
  this.#removeDiagnosticWaiter(uri, waiter);
329
- resolve();
365
+ resolve(outcome);
330
366
  };
331
367
  const timer = setTimeout(() => {
332
368
  this.#removeDiagnosticWaiter(uri, waiter);
333
- resolve();
369
+ resolve("timed-out");
334
370
  }, timeoutMs);
335
371
  const waiters = this.#diagnosticWaiters.get(uri) ?? [];
336
372
  waiters.push(waiter);
@@ -338,7 +374,7 @@ export class ClientDiagnostics {
338
374
  });
339
375
  }
340
376
 
341
- #removeDiagnosticWaiter(uri: string, waiter: () => void): void {
377
+ #removeDiagnosticWaiter(uri: string, waiter: DiagnosticWaiter): void {
342
378
  const waiters = this.#diagnosticWaiters.get(uri);
343
379
  if (!waiters) return;
344
380
  const next = waiters.filter((entry) => entry !== waiter);
@@ -352,10 +388,10 @@ export class ClientDiagnostics {
352
388
  }
353
389
  }
354
390
 
355
- #releaseDiagnosticWaiters(uri: string): void {
391
+ #releaseDiagnosticWaiters(uri: string, outcome: "published" | "released" = "released"): void {
356
392
  const waiters = this.#diagnosticWaiters.get(uri);
357
393
  if (!waiters) return;
358
394
  this.#diagnosticWaiters.delete(uri);
359
- for (const waiter of waiters) waiter();
395
+ for (const waiter of waiters) waiter(outcome);
360
396
  }
361
397
  }