@hydranium/core 1.0.0-next.266 → 1.0.0-next.268

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 (126) hide show
  1. package/lib/documents/ast-document-manager.d.ts.map +1 -1
  2. package/lib/documents/ast-document-manager.js +4 -3
  3. package/lib/documents/ast-document-manager.js.map +1 -1
  4. package/lib/documents/document-release-handler.d.ts +2 -2
  5. package/lib/documents/document-release-handler.d.ts.map +1 -1
  6. package/lib/documents/document-release-handler.js +14 -19
  7. package/lib/documents/document-release-handler.js.map +1 -1
  8. package/lib/documents/hydranium-text-documents.d.ts.map +1 -1
  9. package/lib/documents/hydranium-text-documents.js +7 -10
  10. package/lib/documents/hydranium-text-documents.js.map +1 -1
  11. package/lib/documents/language-client-shadow.d.ts.map +1 -1
  12. package/lib/documents/language-client-shadow.js +2 -1
  13. package/lib/documents/language-client-shadow.js.map +1 -1
  14. package/lib/documents/version-sync-service.d.ts +18 -14
  15. package/lib/documents/version-sync-service.d.ts.map +1 -1
  16. package/lib/documents/version-sync-service.js +15 -13
  17. package/lib/documents/version-sync-service.js.map +1 -1
  18. package/lib/langium/diagnostics/lsp-logger.d.ts.map +1 -1
  19. package/lib/langium/diagnostics/lsp-logger.js +2 -2
  20. package/lib/langium/diagnostics/lsp-logger.js.map +1 -1
  21. package/lib/langium/document-builder/document-builder.d.ts +2 -2
  22. package/lib/langium/document-builder/document-builder.d.ts.map +1 -1
  23. package/lib/langium/document-builder/document-builder.js +23 -10
  24. package/lib/langium/document-builder/document-builder.js.map +1 -1
  25. package/lib/langium/integrity/integrity-service.d.ts.map +1 -1
  26. package/lib/langium/integrity/integrity-service.js +5 -1
  27. package/lib/langium/integrity/integrity-service.js.map +1 -1
  28. package/lib/langium/model-service/client-session.d.ts.map +1 -1
  29. package/lib/langium/model-service/client-session.js +20 -3
  30. package/lib/langium/model-service/client-session.js.map +1 -1
  31. package/lib/langium/model-service/model-service.d.ts +4 -3
  32. package/lib/langium/model-service/model-service.d.ts.map +1 -1
  33. package/lib/langium/model-service/model-service.js +49 -19
  34. package/lib/langium/model-service/model-service.js.map +1 -1
  35. package/lib/langium/project/abstract-project-manager.d.ts.map +1 -1
  36. package/lib/langium/project/abstract-project-manager.js +4 -4
  37. package/lib/langium/project/abstract-project-manager.js.map +1 -1
  38. package/lib/langium/scope/hydranium-scope-computation.d.ts +6 -0
  39. package/lib/langium/scope/hydranium-scope-computation.d.ts.map +1 -1
  40. package/lib/langium/scope/hydranium-scope-computation.js +6 -10
  41. package/lib/langium/scope/hydranium-scope-computation.js.map +1 -1
  42. package/lib/langium/validation/document-validator.d.ts.map +1 -1
  43. package/lib/langium/validation/document-validator.js +5 -3
  44. package/lib/langium/validation/document-validator.js.map +1 -1
  45. package/lib/langium/workspace/hydranium-workspace-lock.d.ts +1 -1
  46. package/lib/langium/workspace/hydranium-workspace-lock.js +1 -1
  47. package/lib/langium/workspace/hydranium-workspace-lock.js.map +1 -1
  48. package/lib/langium/workspace/hydranium-workspace-manager.d.ts.map +1 -1
  49. package/lib/langium/workspace/hydranium-workspace-manager.js +4 -5
  50. package/lib/langium/workspace/hydranium-workspace-manager.js.map +1 -1
  51. package/lib/langium/workspace/persistent-file-system-provider.d.ts.map +1 -1
  52. package/lib/langium/workspace/persistent-file-system-provider.js +2 -1
  53. package/lib/langium/workspace/persistent-file-system-provider.js.map +1 -1
  54. package/lib/launcher/integrated-server.d.ts +3 -1
  55. package/lib/launcher/integrated-server.d.ts.map +1 -1
  56. package/lib/lsp/hydranium-document-update-handler.d.ts.map +1 -1
  57. package/lib/lsp/hydranium-document-update-handler.js +5 -5
  58. package/lib/lsp/hydranium-document-update-handler.js.map +1 -1
  59. package/lib/node/event-loop-monitor.d.ts.map +1 -1
  60. package/lib/node/event-loop-monitor.js +2 -1
  61. package/lib/node/event-loop-monitor.js.map +1 -1
  62. package/lib/node/log-preamble-node.d.ts +5 -3
  63. package/lib/node/log-preamble-node.d.ts.map +1 -1
  64. package/lib/node/log-preamble-node.js +13 -10
  65. package/lib/node/log-preamble-node.js.map +1 -1
  66. package/lib/node/memory-monitor.d.ts +2 -2
  67. package/lib/node/memory-monitor.d.ts.map +1 -1
  68. package/lib/node/memory-monitor.js +3 -5
  69. package/lib/node/memory-monitor.js.map +1 -1
  70. package/lib/node/node-file-system-provider.d.ts +5 -2
  71. package/lib/node/node-file-system-provider.d.ts.map +1 -1
  72. package/lib/node/node-file-system-provider.js +5 -2
  73. package/lib/node/node-file-system-provider.js.map +1 -1
  74. package/lib/node/server-diagnostics.js +1 -1
  75. package/lib/node/server-diagnostics.js.map +1 -1
  76. package/lib/node/socket-launcher.d.ts.map +1 -1
  77. package/lib/node/socket-launcher.js +4 -4
  78. package/lib/node/socket-launcher.js.map +1 -1
  79. package/lib/node/stdio-launcher.d.ts.map +1 -1
  80. package/lib/node/stdio-launcher.js +12 -3
  81. package/lib/node/stdio-launcher.js.map +1 -1
  82. package/lib/testing/playwright/server-log-capture.d.ts +7 -0
  83. package/lib/testing/playwright/server-log-capture.d.ts.map +1 -1
  84. package/lib/testing/playwright/server-log-capture.js +12 -0
  85. package/lib/testing/playwright/server-log-capture.js.map +1 -1
  86. package/lib/testing/playwright/server-log-fixture.d.ts +16 -0
  87. package/lib/testing/playwright/server-log-fixture.d.ts.map +1 -1
  88. package/lib/testing/playwright/server-log-fixture.js +25 -6
  89. package/lib/testing/playwright/server-log-fixture.js.map +1 -1
  90. package/lib/testing/playwright/server-log-region.d.ts +15 -0
  91. package/lib/testing/playwright/server-log-region.d.ts.map +1 -0
  92. package/lib/testing/playwright/server-log-region.js +30 -0
  93. package/lib/testing/playwright/server-log-region.js.map +1 -0
  94. package/lib/util/connection-liveness.d.ts.map +1 -1
  95. package/lib/util/connection-liveness.js +3 -2
  96. package/lib/util/connection-liveness.js.map +1 -1
  97. package/package.json +5 -5
  98. package/src/documents/ast-document-manager.ts +7 -3
  99. package/src/documents/document-release-handler.ts +15 -21
  100. package/src/documents/hydranium-text-documents.ts +13 -12
  101. package/src/documents/language-client-shadow.ts +2 -1
  102. package/src/documents/version-sync-service.ts +32 -24
  103. package/src/langium/diagnostics/lsp-logger.ts +2 -1
  104. package/src/langium/document-builder/document-builder.ts +30 -11
  105. package/src/langium/integrity/integrity-service.ts +4 -1
  106. package/src/langium/model-service/client-session.ts +20 -3
  107. package/src/langium/model-service/model-service.ts +55 -24
  108. package/src/langium/project/abstract-project-manager.ts +4 -4
  109. package/src/langium/scope/hydranium-scope-computation.ts +12 -12
  110. package/src/langium/validation/document-validator.ts +5 -3
  111. package/src/langium/workspace/hydranium-workspace-lock.ts +2 -2
  112. package/src/langium/workspace/hydranium-workspace-manager.ts +4 -5
  113. package/src/langium/workspace/persistent-file-system-provider.ts +2 -1
  114. package/src/launcher/integrated-server.ts +3 -1
  115. package/src/lsp/hydranium-document-update-handler.ts +15 -7
  116. package/src/node/event-loop-monitor.ts +2 -1
  117. package/src/node/log-preamble-node.ts +20 -10
  118. package/src/node/memory-monitor.ts +4 -5
  119. package/src/node/node-file-system-provider.ts +5 -2
  120. package/src/node/server-diagnostics.ts +1 -1
  121. package/src/node/socket-launcher.ts +4 -4
  122. package/src/node/stdio-launcher.ts +11 -3
  123. package/src/testing/playwright/server-log-capture.ts +14 -0
  124. package/src/testing/playwright/server-log-fixture.ts +46 -6
  125. package/src/testing/playwright/server-log-region.ts +32 -0
  126. package/src/util/connection-liveness.ts +4 -2
@@ -8,7 +8,7 @@
8
8
  ********************************************************************************/
9
9
 
10
10
  import { DocumentState, type LangiumDocument, type URI } from '@hydranium/langium';
11
- import { type Tracer, UNRECORDED_VERSION } from '@hydranium/protocol';
11
+ import { type Tracer, UNRECORDED_VERSION, describeError, describeErrorWithStack } from '@hydranium/protocol';
12
12
  import { Disposable, Emitter, type Event } from 'vscode-languageserver-protocol';
13
13
  import { type LogNameOptions } from '../langium/diagnostics/logger.js';
14
14
  import { type ServerSharedServices } from '../langium/module.js';
@@ -46,20 +46,21 @@ export interface VersionSyncService {
46
46
  /**
47
47
  * Build `uri` from a `WorkspaceLock` read, so the build queues behind a
48
48
  * running one rather than cancelling it. Requests made while one for `uri`
49
- * is pending share its next build. Resolves `true` once a build that carried
50
- * this request completed, or once a check before a build found it no longer
49
+ * is pending share its next build. Resolves once a build that carried this
50
+ * request completed, or once a check before a build found it no longer
51
51
  * needed or deferred. A build a later write cancelled does not count: the
52
- * request rides the next one. Resolves `false` once builds carrying this
53
- * request have failed more often than it may retry, so a caller waiting on
54
- * the build can stop. Never rejects.
52
+ * request rides the next one. Rejects with the last build's error once builds
53
+ * carrying this request have failed more often than it may retry, so a
54
+ * caller waiting on the build can stop; the caller ends that failure, and
55
+ * logs it if nothing else reports it.
55
56
  */
56
- requestRecoveryBuild(uri: URI, request: RecoveryBuildRequest): Promise<boolean>;
57
+ requestRecoveryBuild(uri: URI, request: RecoveryBuildRequest): Promise<void>;
57
58
  /**
58
59
  * Build `uri` unless it is {@link isSyncedTo synced to} `textVersion`, asked
59
60
  * again inside the lock. `undefined` when no build is needed, else the
60
- * {@link requestRecoveryBuild} answer.
61
+ * {@link requestRecoveryBuild} promise.
61
62
  */
62
- syncTo(uri: URI, textVersion: number): Promise<boolean> | undefined;
63
+ syncTo(uri: URI, textVersion: number): Promise<void> | undefined;
63
64
  /**
64
65
  * Whether `uri`'s root was parsed from text at `textVersion` or later; requests
65
66
  * nothing. A root with no recorded version or a placeholder counts as synced,
@@ -85,7 +86,10 @@ export interface VersionSyncService {
85
86
  export interface QueuedRecoveryBuild {
86
87
  readonly request: RecoveryBuildRequest;
87
88
  failures: number;
88
- readonly answer: (built: boolean) => void;
89
+ /** Settles the request once a build carried it or it was no longer needed. */
90
+ readonly resolve: () => void;
91
+ /** Settles the request once it has used its retries, with the last build's error. */
92
+ readonly reject: (error: unknown) => void;
89
93
  }
90
94
 
91
95
  /** The requests waiting on one URI's builds in {@link DefaultVersionSyncService}. */
@@ -102,7 +106,7 @@ export class DefaultVersionSyncService implements VersionSyncService {
102
106
  protected readonly recordedEmitter = new Emitter<LangiumDocument>();
103
107
  protected readonly deferredBuildChecks = new Set<(uri: URI) => boolean>();
104
108
  protected readonly pendingBuilds = new Map<string, PendingRecoveryBuilds>();
105
- /** Retries of each request after a failed build before it is answered `false`. */
109
+ /** Retries of each request after a failed build before it is rejected. */
106
110
  protected readonly maxBuildRetries: number = 1;
107
111
 
108
112
  constructor(
@@ -142,19 +146,22 @@ export class DefaultVersionSyncService implements VersionSyncService {
142
146
  }
143
147
  ledger.record(root, version, origin.text);
144
148
  }
145
- void this.syncTo(document.uri, store.version(uri));
149
+ // Nothing waits on this build, so its failure ends here.
150
+ this.syncTo(document.uri, store.version(uri))?.catch((error: unknown) =>
151
+ this.tracer.withUri(uri).error(`The build that would sync the root to its text failed. ${describeErrorWithStack(error)}`)
152
+ );
146
153
  this.recordedEmitter.fire(document);
147
154
  }
148
155
 
149
- syncTo(uri: URI, textVersion: number): Promise<boolean> | undefined {
156
+ syncTo(uri: URI, textVersion: number): Promise<void> | undefined {
150
157
  const behind = (): boolean => !this.isSyncedTo(uri, textVersion);
151
158
  return behind() ? this.requestRecoveryBuild(uri, { stillNeeded: behind }) : undefined;
152
159
  }
153
160
 
154
- requestRecoveryBuild(uri: URI, request: RecoveryBuildRequest): Promise<boolean> {
155
- return new Promise<boolean>(answer => {
161
+ requestRecoveryBuild(uri: URI, request: RecoveryBuildRequest): Promise<void> {
162
+ return new Promise<void>((resolve, reject) => {
156
163
  const key = uri.toString();
157
- const queued: QueuedRecoveryBuild = { request, failures: 0, answer };
164
+ const queued: QueuedRecoveryBuild = { request, failures: 0, resolve, reject };
158
165
  const pending = this.pendingBuilds.get(key);
159
166
  if (pending) {
160
167
  // Not dropped: the pending build may have read the text before this request's change.
@@ -189,8 +196,8 @@ export class DefaultVersionSyncService implements VersionSyncService {
189
196
 
190
197
  /**
191
198
  * Run `entry`'s requests, one build per batch, until none is queued, and
192
- * answer each. A request a failed build carried is queued again
193
- * {@link maxBuildRetries} times, then answered `false`; one a cancelled
199
+ * settle each. A request a failed build carried is queued again
200
+ * {@link maxBuildRetries} times, then rejected; one a cancelled
194
201
  * build carried is queued again without counting. The entry is dropped
195
202
  * in the same tick as the last check of the queue, so no request lands in
196
203
  * an entry nothing runs any more.
@@ -210,7 +217,7 @@ export class DefaultVersionSyncService implements VersionSyncService {
210
217
  await lock.read(() => {
211
218
  carried = entry.queued.splice(0);
212
219
  const needed = carried.filter(queued => this.isStillNeeded(uri, queued.request));
213
- carried.filter(queued => !needed.includes(queued)).forEach(queued => queued.answer(true));
220
+ carried.filter(queued => !needed.includes(queued)).forEach(queued => queued.resolve());
214
221
  carried = needed;
215
222
  if (needed.length > 0) {
216
223
  build = this.scheduleBatch(
@@ -227,18 +234,19 @@ export class DefaultVersionSyncService implements VersionSyncService {
227
234
  entry.queued.unshift(...carried);
228
235
  continue;
229
236
  }
230
- carried.forEach(queued => queued.answer(true));
237
+ carried.forEach(queued => queued.resolve());
231
238
  } catch (err: unknown) {
232
- const detail = err instanceof Error ? (err.stack ?? err.message) : String(err);
233
239
  const retried = carried.filter(queued => queued.failures++ < this.maxBuildRetries);
234
240
  const givenUp = carried.filter(queued => !retried.includes(queued));
235
241
  if (retried.length > 0) {
236
- tracer.warn(`Requested build failed; retrying. ${detail}`);
242
+ // The only line about this failure when the retry succeeds.
243
+ tracer.warn(`Requested build failed; retrying. ${describeErrorWithStack(err)}`);
237
244
  entry.queued.unshift(...retried);
238
245
  }
239
246
  if (givenUp.length > 0) {
240
- tracer.error(`Requested build failed again; giving up. ${detail}`);
241
- givenUp.forEach(queued => queued.answer(false));
247
+ // Each requester ends the failure, so the log line is theirs.
248
+ tracer.debug(`Requested build failed again; giving up. ${describeError(err)}`);
249
+ givenUp.forEach(queued => queued.reject(err));
242
250
  }
243
251
  }
244
252
  }
@@ -19,6 +19,7 @@ import {
19
19
  type LogThreshold,
20
20
  ObservableValue,
21
21
  type MaybeObservableValue,
22
+ describeErrorWithStack,
22
23
  parseLogLevel
23
24
  } from '@hydranium/protocol';
24
25
  import { type URI } from '@hydranium/langium';
@@ -308,7 +309,7 @@ function formatLogArg(value: unknown): string {
308
309
  return value;
309
310
  }
310
311
  if (value instanceof Error) {
311
- return value.stack ?? value.message;
312
+ return describeErrorWithStack(value);
312
313
  }
313
314
  if (typeof value === 'object') {
314
315
  try {
@@ -7,7 +7,14 @@
7
7
  * SPDX-License-Identifier: MIT
8
8
  ********************************************************************************/
9
9
 
10
- import { type Clock, type LogThreshold, type MaybeObservableValue, ObservableValue, type Tracer } from '@hydranium/protocol';
10
+ import {
11
+ type Clock,
12
+ type LogThreshold,
13
+ type MaybeObservableValue,
14
+ ObservableValue,
15
+ type Tracer,
16
+ describeErrorWithStack
17
+ } from '@hydranium/protocol';
11
18
  import {
12
19
  type AstNode,
13
20
  type BuildOptions,
@@ -356,7 +363,7 @@ export class HydraniumDocumentBuilder extends DefaultDocumentBuilder {
356
363
  try {
357
364
  listener(event);
358
365
  } catch (error: unknown) {
359
- this.tracer.error(`A build-ended listener failed: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}`);
366
+ this.tracer.error(`A build-ended listener failed: ${describeErrorWithStack(error)}`);
360
367
  }
361
368
  }
362
369
  }
@@ -488,12 +495,15 @@ export class HydraniumDocumentBuilder extends DefaultDocumentBuilder {
488
495
  // land while a busy workspace keeps cancelling builds.
489
496
  let stalledRequeues = 0;
490
497
  let lastRequeueState: DocumentState | undefined;
491
- const giveUp = (reason: string): void => {
498
+ let settled = false;
499
+ let cancelled = false;
500
+ const giveUp = (reason: string, cause?: unknown): void => {
492
501
  cleanup();
493
502
  reject(
494
503
  new Error(
495
504
  `Gave up building ${this.formatUri(uri)} to '${DocumentState[state]}' ` +
496
- `(at '${DocumentState[document.state]}'): ${reason}`
505
+ `(at '${DocumentState[document.state]}'): ${reason}`,
506
+ { cause }
497
507
  )
498
508
  );
499
509
  };
@@ -517,9 +527,16 @@ export class HydraniumDocumentBuilder extends DefaultDocumentBuilder {
517
527
  stalledRequeues = 0;
518
528
  lastRequeueState = document.state;
519
529
  }
520
- void this.requeueOrphaned(document, state, reason).then(built => {
521
- if (!built && rejectWhenStuck) {
522
- giveUp('its recovery build failed');
530
+ this.requeueOrphaned(document, state, reason).catch((error: unknown) => {
531
+ const line = `A recovery build of ${this.formatUri(uri)} failed. ${describeErrorWithStack(error)}`;
532
+ if (rejectWhenStuck && !settled) {
533
+ giveUp('its recovery build failed', error);
534
+ } else if (settled && !cancelled) {
535
+ // The wait ended with an outcome its caller reports, and the retry warned.
536
+ this.tracer.withUri(uri.toString()).debug(line);
537
+ } else {
538
+ // The waiter has gone, or waits on its cancellation token alone, so nothing else reports this.
539
+ this.tracer.withUri(uri.toString()).error(line);
523
540
  }
524
541
  });
525
542
  };
@@ -559,10 +576,12 @@ export class HydraniumDocumentBuilder extends DefaultDocumentBuilder {
559
576
  };
560
577
  const buildDisposable = this.onBuildEnded(buildEnded);
561
578
  const cancelDisposable = cancelToken.onCancellationRequested(() => {
579
+ cancelled = true;
562
580
  cleanup();
563
581
  reject(OperationCancelled);
564
582
  });
565
583
  const cleanup = (): void => {
584
+ settled = true;
566
585
  phaseDisposable.dispose();
567
586
  skipDisposable.dispose();
568
587
  buildDisposable.dispose();
@@ -628,11 +647,11 @@ export class HydraniumDocumentBuilder extends DefaultDocumentBuilder {
628
647
  * the build neither runs beside a locked one nor cancels the build a
629
648
  * re-queue is fired from, and skipped if a write that ran meanwhile carried
630
649
  * the document to `state`. The waiter resolves off the resulting phase
631
- * notification; the answer only tells it whether the build was given up.
650
+ * notification; the promise only tells it whether the build was given up.
632
651
  */
633
- protected requeueOrphaned(document: LangiumDocument, state: DocumentState, reason: string): Promise<boolean> {
652
+ protected requeueOrphaned(document: LangiumDocument, state: DocumentState, reason: string): Promise<void> {
634
653
  const tracer = this.tracer.withUri(document.uri.toString());
635
- tracer.info(`Re-queuing orphaned document (at '${DocumentState[document.state]}', needs '${DocumentState[state]}'): ${reason}`);
654
+ tracer.debug(`Re-queuing orphaned document (at '${DocumentState[document.state]}', needs '${DocumentState[state]}'): ${reason}`);
636
655
  return this.versionSyncService().requestRecoveryBuild(document.uri, {
637
656
  // Skipped on purpose: a waiting caller is answered sooner, at the cost
638
657
  // of a possible duplicate build once the debounce fires.
@@ -1391,7 +1410,7 @@ export class HydraniumDocumentBuilder extends DefaultDocumentBuilder {
1391
1410
  if (cancelledListeners > 0) {
1392
1411
  this.tracer
1393
1412
  .withUri(document.uri.toString())
1394
- .info(this.cancelledDocumentPhaseLine(document, state, cancelledListeners, listeners.length));
1413
+ .debug(this.cancelledDocumentPhaseLine(document, state, cancelledListeners, listeners.length));
1395
1414
  }
1396
1415
  if (totalMs >= this.slowPhaseMs.value) {
1397
1416
  this.log(this.slowDocumentPhaseLine(document, state, listeners, perListenerMs, totalMs));
@@ -213,8 +213,11 @@ export class DefaultIntegrityService<TRoot extends AstNode = AstNode> implements
213
213
  await interruptAndCheck(cancelToken);
214
214
  }
215
215
  const totalMs = Math.round(performance.now() - phaseStart);
216
- if (totalMs >= 25 || mutatedCount > 0) {
216
+ // A repair is the operator's to know of; the timing alone is diagnosis.
217
+ if (mutatedCount > 0) {
217
218
  this.tracer.info(`enforce ${DocumentState[phase]}: docs=${documents.length}, mutated=${mutatedCount} [${totalMs}ms]`);
219
+ } else if (totalMs >= 25) {
220
+ this.tracer.debug(`enforce ${DocumentState[phase]}: docs=${documents.length}, mutated=${mutatedCount} [${totalMs}ms]`);
218
221
  }
219
222
  // One line per rule (count + self-% + self-ms), sorted by cost — only when profiling.
220
223
  session?.report('debug');
@@ -17,9 +17,10 @@ import {
17
17
  ObservableValue,
18
18
  type TextVersion,
19
19
  type Tracer,
20
- type TransferElement
20
+ type TransferElement,
21
+ describeErrorWithStack
21
22
  } from '@hydranium/protocol';
22
- import { type AstNode, UriUtils } from '@hydranium/langium';
23
+ import { type AstNode, isOperationCancelled, UriUtils } from '@hydranium/langium';
23
24
  import { type CancellationToken, type Disposable, Emitter, type Event } from 'vscode-languageserver';
24
25
  import { type AstDocument, type SavedAstDocument } from '../../documents/ast-document-manager.js';
25
26
  import { DocumentNotOpenError, SessionClosedError } from '../../documents/client-session-errors.js';
@@ -410,7 +411,23 @@ export class DefaultClientSession<
410
411
  const applied = uris.map((uri, i) => this.services.workspace.AstDocumentManager.update(uri, texts[i], this.clientId));
411
412
  await Promise.all(applied);
412
413
  const state = this.services.workspace.DocumentBuilder.finalBuildState();
413
- return Promise.all(uris.map(uri => service.rebuild(uri, state, cancelToken)));
414
+ const rebuilt = await Promise.allSettled(uris.map(uri => service.rebuild(uri, state, cancelToken)));
415
+ const failures = rebuilt.flatMap((result, i) => (result.status === 'rejected' ? [{ uri: uris[i], reason: result.reason }] : []));
416
+ if (failures.length === 0) {
417
+ return rebuilt.flatMap(result => (result.status === 'fulfilled' ? [result.value] : []));
418
+ }
419
+ // The first is thrown as it is, so a typed failure keeps its type; one build
420
+ // usually carries the set and fails each with the same error. Any other
421
+ // failure reaches no one else, so it is logged here.
422
+ const [{ uri: firstUri, reason: firstReason }] = failures;
423
+ for (const { uri, reason } of failures.slice(1)) {
424
+ if (isOperationCancelled(reason)) {
425
+ this.tracer.with(uri).debug(`Rebuild of ${uri} cancelled beside that of ${firstUri}`);
426
+ } else if (reason !== firstReason) {
427
+ this.tracer.with(uri).error(`Rebuild of ${uri} failed beside that of ${firstUri}. ${describeErrorWithStack(reason)}`);
428
+ }
429
+ }
430
+ throw firstReason;
414
431
  }
415
432
 
416
433
  /**
@@ -18,9 +18,18 @@ import {
18
18
  type TransferElement,
19
19
  type TextVersion,
20
20
  type Tracer,
21
- UNRECORDED_VERSION
21
+ UNRECORDED_VERSION,
22
+ describeErrorWithStack
22
23
  } from '@hydranium/protocol';
23
- import { type AstNode, DocumentState, type LangiumDocument, OperationCancelled, UriUtils, type URI } from '@hydranium/langium';
24
+ import {
25
+ type AstNode,
26
+ DocumentState,
27
+ type LangiumDocument,
28
+ OperationCancelled,
29
+ isOperationCancelled,
30
+ UriUtils,
31
+ type URI
32
+ } from '@hydranium/langium';
24
33
  import { type AstDiagnostic } from '../validation/document-validator.js';
25
34
  import { type DocumentUriPolicy } from '../workspace/document-uri-policy.js';
26
35
  import { ReentrantWriteLockError, isInsideWriteLock, isWriteLockScopeInstalled } from '../workspace/write-lock-scope.js';
@@ -571,23 +580,37 @@ export class DefaultModelService<
571
580
  * given up. A `Parsed` phase listener cannot serve: a cancel right after the
572
581
  * parse skips it, and the resumed build does not parse again.
573
582
  */
574
- protected nextParseOrDeletion(
575
- uri: URI,
576
- cancelToken: CancellationToken = CancellationToken.None,
577
- build?: Promise<boolean>
578
- ): Promise<void> {
583
+ protected nextParseOrDeletion(uri: URI, cancelToken: CancellationToken = CancellationToken.None, build?: Promise<void>): Promise<void> {
579
584
  const builder = this.services.workspace.DocumentBuilder;
580
585
  return new Promise<void>((resolve, reject) => {
586
+ let ended = false;
587
+ let cancelled = false;
588
+ // Handled before any return, so a build given up after the wait ended is not an unhandled rejection.
589
+ build?.catch((error: unknown) => {
590
+ const line = `The build that would sync the root to its text failed. ${describeErrorWithStack(error)}`;
591
+ if (cancelled) {
592
+ // The waiter has gone, so nothing else reports this.
593
+ this.tracer.withUri(uri.toString()).error(line);
594
+ } else if (ended) {
595
+ // A parse or deletion came first: the waiter goes on and reports its own outcome, and the retry warned.
596
+ this.tracer.withUri(uri.toString()).debug(line);
597
+ } else {
598
+ done(() => reject(new Error(`The build that would sync ${uri.toString()} to its text failed`, { cause: error })));
599
+ }
600
+ });
581
601
  // A cancelled token's listener runs a macrotask later, after a build
582
602
  // that started meanwhile may have parsed.
583
603
  if (cancelToken.isCancellationRequested) {
604
+ ended = true;
605
+ cancelled = true;
584
606
  reject(OperationCancelled);
585
607
  return;
586
608
  }
587
609
  const done = (settle: () => void): void => {
610
+ ended = true;
588
611
  parsed.dispose();
589
612
  deleted.dispose();
590
- cancelled.dispose();
613
+ cancelling.dispose();
591
614
  settle();
592
615
  };
593
616
  const parsed = this.services.workspace.VersionSyncService.onDidRecordModel(document => {
@@ -600,11 +623,9 @@ export class DefaultModelService<
600
623
  done(resolve);
601
624
  }
602
625
  });
603
- const cancelled = cancelToken.onCancellationRequested(() => done(() => reject(OperationCancelled)));
604
- void build?.then(built => {
605
- if (!built) {
606
- done(() => reject(new Error(`The build that would sync ${uri.toString()} to its text failed`)));
607
- }
626
+ const cancelling = cancelToken.onCancellationRequested(() => {
627
+ cancelled = true;
628
+ done(() => reject(OperationCancelled));
608
629
  });
609
630
  });
610
631
  }
@@ -808,8 +829,9 @@ export class DefaultModelService<
808
829
  * so every language client reflects the latest content before a save returns.
809
830
  * No-op tail for headless adopters — `syncChains` is empty without an LSP
810
831
  * client. Bounded by {@link SAVE_SETTLE_TIMEOUT_MS} so a hung build, or an
811
- * applyEdit reverse-RPC deadlock, can't freeze the caller; on timeout or a
812
- * failed wait it logs a warning and returns rather than throwing.
832
+ * applyEdit reverse-RPC deadlock, can't freeze the caller; it returns rather
833
+ * than throwing, warning on timeout. A failed wait is logged at `error`, at
834
+ * `debug` when cancelled and at `warn` when the document is gone.
813
835
  *
814
836
  * Adopters with a save flow that must converge editor + disk before returning
815
837
  * (e.g. a dual form/code editor that would otherwise show a content-conflict
@@ -824,15 +846,26 @@ export class DefaultModelService<
824
846
  await this.waitForDocumentStateCanonical(canonical, IntegrityService.SettledState, cancelToken, { sync: true });
825
847
  await this.syncChains.get(key);
826
848
  })();
849
+ // The settle ends a failed wait, so it logs it, at one level whether the wait failed inside the bound or after it.
850
+ const reportFailedWait = (error: unknown, when: string): void => {
851
+ const tracer = this.tracer.withUri(key);
852
+ if (isOperationCancelled(error)) {
853
+ tracer.debug(`Save settle cancelled ${when}`);
854
+ } else if (!this.services.workspace.LangiumDocuments.hasDocument(UriUtils.toUri(canonical))) {
855
+ tracer.warn(`Save settle failed ${when}; the document is gone. ${describeErrorWithStack(error)}`);
856
+ } else {
857
+ tracer.error(`Save settle failed ${when}. ${describeErrorWithStack(error)}`);
858
+ }
859
+ };
827
860
  try {
828
861
  if ((await this.services.Clock.raceTimer(settled, SAVE_SETTLE_TIMEOUT_MS)) === TIMED_OUT) {
829
862
  this.tracer.withUri(key).warn(`Save settle exceeded ${SAVE_SETTLE_TIMEOUT_MS}ms — returning anyway`);
863
+ // The race has settled, so a later failure of the wait, such as a given-up sync build, reaches no one else.
864
+ void settled.catch((error: unknown) => reportFailedWait(error, 'after the timeout'));
830
865
  }
831
866
  } catch (err: unknown) {
832
- // A failed wait, such as a cancelled token or a document the builder
833
- // does not hold, gets its own line so it is not blamed on the bound.
834
- const detail = err instanceof Error ? (err.stack ?? err.message) : String(err);
835
- this.tracer.withUri(key).warn(`Save settle failed before the timeout — returning anyway. ${detail}`);
867
+ // Its own line, so the failure is not blamed on the bound.
868
+ reportFailedWait(err, 'before the timeout — returning anyway');
836
869
  }
837
870
  }
838
871
 
@@ -1123,7 +1156,7 @@ export class DefaultModelService<
1123
1156
  if (isClientGoneError(err)) {
1124
1157
  uriLogger.debug(`applyEdit to ${LANGUAGE_CLIENT_ID} skipped: client disconnected`);
1125
1158
  } else {
1126
- uriLogger.error(`applyEdit to ${LANGUAGE_CLIENT_ID} failed: ${err}`);
1159
+ uriLogger.error(`applyEdit to ${LANGUAGE_CLIENT_ID} failed: ${describeErrorWithStack(err)}`);
1127
1160
  }
1128
1161
  }
1129
1162
  }
@@ -1162,7 +1195,7 @@ export class DefaultModelService<
1162
1195
  try {
1163
1196
  listener(Object.freeze({ uris: built.map(document => this.uriPolicy.canonicalUri(document.uri.toString())), phase }));
1164
1197
  } catch (err: unknown) {
1165
- this.tracer.error(`onModelsBuilt listener threw: ${err instanceof Error ? (err.stack ?? err.message) : String(err)}`);
1198
+ this.tracer.error(`onModelsBuilt listener threw: ${describeErrorWithStack(err)}`);
1166
1199
  }
1167
1200
  });
1168
1201
  }
@@ -1267,9 +1300,7 @@ export class DefaultModelService<
1267
1300
  try {
1268
1301
  listener(event);
1269
1302
  } catch (err: unknown) {
1270
- this.tracer
1271
- .with(uri)
1272
- .error(`onModelUpdated listener threw: ${err instanceof Error ? (err.stack ?? err.message) : String(err)}`);
1303
+ this.tracer.with(uri).error(`onModelUpdated listener threw: ${describeErrorWithStack(err)}`);
1273
1304
  }
1274
1305
  }
1275
1306
  }
@@ -7,7 +7,7 @@
7
7
  * SPDX-License-Identifier: MIT
8
8
  ********************************************************************************/
9
9
 
10
- import { Deferred, type Project, type Tracer, UNQUALIFIED_PROJECT_REFERENCE } from '@hydranium/protocol';
10
+ import { Deferred, type Project, type Tracer, UNQUALIFIED_PROJECT_REFERENCE, describeErrorWithStack } from '@hydranium/protocol';
11
11
  import {
12
12
  type AstNode,
13
13
  AstUtils,
@@ -226,7 +226,7 @@ export abstract class AbstractProjectManager<TProject extends Project = Project>
226
226
  try {
227
227
  entries = await this.fileSystemProvider.readDirectory(folder);
228
228
  } catch (error) {
229
- this.tracer.warn(`Failed to read directory ${folder.toString()}: ${error}`);
229
+ this.tracer.warn(`Failed to read directory ${folder.toString()}: ${describeErrorWithStack(error)}`);
230
230
  return;
231
231
  }
232
232
  for (const entry of entries) {
@@ -395,7 +395,7 @@ export abstract class AbstractProjectManager<TProject extends Project = Project>
395
395
  }
396
396
  return project;
397
397
  } catch (error) {
398
- this.tracer.error(`Failed to parse ${this.conceptName()} descriptor at ${uri.toString()}: ${error}`);
398
+ this.tracer.error(`Failed to parse ${this.conceptName()} descriptor at ${uri.toString()}: ${describeErrorWithStack(error)}`);
399
399
  return undefined;
400
400
  }
401
401
  }
@@ -620,7 +620,7 @@ export abstract class AbstractProjectManager<TProject extends Project = Project>
620
620
  try {
621
621
  listener(event);
622
622
  } catch (error) {
623
- this.tracer.error(`Project change listener failed: ${error}`);
623
+ this.tracer.error(`Project change listener failed: ${describeErrorWithStack(error)}`);
624
624
  }
625
625
  }
626
626
  }
@@ -59,6 +59,10 @@ import { type HydraniumAstNodeDescriptionProvider } from './ast-node-description
59
59
  * duplicated walks.
60
60
  */
61
61
  export class HydraniumScopeComputation extends DefaultScopeComputation {
62
+ /** The framework's name provider, which its language module binds to the slot. */
63
+ declare protected readonly nameProvider: NameProvider;
64
+ /** The framework's description provider, which its language module binds to the slot. */
65
+ declare protected readonly descriptions: HydraniumAstNodeDescriptionProvider;
62
66
  protected readonly tracer: Tracer;
63
67
 
64
68
  constructor(
@@ -155,17 +159,15 @@ export class HydraniumScopeComputation extends DefaultScopeComputation {
155
159
  * workspace-visible rather than being filtered out everywhere.
156
160
  */
157
161
  protected exportProject(node: AstNode, exports: AstNodeDescription[], document: LangiumDocument): void {
158
- const nameProvider = this.nameProvider as NameProvider;
159
- const documentName = nameProvider.getDocumentQualifiedName(node);
162
+ const documentName = this.nameProvider.getDocumentQualifiedName(node);
160
163
  if (!documentName) {
161
164
  return;
162
165
  }
163
166
  const projectId = this.getProjectIdForDocument(document);
164
- const descriptions = this.descriptions as HydraniumAstNodeDescriptionProvider;
165
167
  exports.push(
166
168
  projectId !== undefined
167
- ? descriptions.createProject({ node, name: documentName, document, projectId })
168
- : descriptions.createUniversal({ node, name: documentName, document })
169
+ ? this.descriptions.createProject({ node, name: documentName, document, projectId })
170
+ : this.descriptions.createUniversal({ node, name: documentName, document })
169
171
  );
170
172
  }
171
173
 
@@ -182,16 +184,14 @@ export class HydraniumScopeComputation extends DefaultScopeComputation {
182
184
  * document-qualified name.
183
185
  */
184
186
  protected exportPublic(node: AstNode, exports: AstNodeDescription[], document: LangiumDocument): void {
185
- const nameProvider = this.nameProvider as NameProvider;
186
187
  const projectId = this.getProjectIdForDocument(document);
187
188
  if (projectId === undefined) {
188
189
  return;
189
190
  }
190
- const documentName = nameProvider.getDocumentQualifiedName(node);
191
- const projectName = nameProvider.getProjectQualifiedName(node);
191
+ const documentName = this.nameProvider.getDocumentQualifiedName(node);
192
+ const projectName = this.nameProvider.getProjectQualifiedName(node);
192
193
  if (projectName && projectName !== documentName) {
193
- const descriptions = this.descriptions as HydraniumAstNodeDescriptionProvider;
194
- exports.push(descriptions.createPublic({ node, name: projectName, document, projectId }));
194
+ exports.push(this.descriptions.createPublic({ node, name: projectName, document, projectId }));
195
195
  }
196
196
  }
197
197
 
@@ -239,9 +239,9 @@ export class HydraniumScopeComputation extends DefaultScopeComputation {
239
239
  if (!container) {
240
240
  return;
241
241
  }
242
- const name = (this.nameProvider as NameProvider).getOwnName(node);
242
+ const name = this.nameProvider.getOwnName(node);
243
243
  if (name) {
244
- symbols.add(container, (this.descriptions as HydraniumAstNodeDescriptionProvider).createLocal({ node, name, document }));
244
+ symbols.add(container, this.descriptions.createLocal({ node, name, document }));
245
245
  }
246
246
  }
247
247
  }
@@ -698,9 +698,11 @@ export class HydraniumDocumentValidator extends DefaultDocumentValidator {
698
698
  */
699
699
  protected buildElementPath<N extends AstNode>(node: N, info: DiagnosticInfo<N, string>): string {
700
700
  const directPath = this.astNodeLocator.getAstNodePath(node);
701
- const containerPath = directPath ? undefined : this.astNodeLocator.getAstNodePath(node.$container ?? ({ $type: 'Dummy' } as AstNode));
702
- const nodePath = directPath || containerPath;
703
- if (!nodePath) {
701
+ // The locator answers '' for the root, which is the root's path rather than a miss.
702
+ const isRoot = node.$container === undefined;
703
+ const containerPath = directPath || isRoot ? undefined : this.astNodeLocator.getAstNodePath(node.$container);
704
+ const nodePath = directPath || containerPath || '';
705
+ if (!nodePath && !isRoot) {
704
706
  this.tracer.warn('Cannot create diagnostic element path: Unable to determine AST node path.');
705
707
  return '';
706
708
  }
@@ -18,7 +18,7 @@ import { runInWriteLockScope } from './write-lock-scope.js';
18
18
  export interface HydraniumWorkspaceLockOptions extends LogNameOptions {
19
19
  /**
20
20
  * How long a write may keep running after a later write cancelled it before
21
- * the lock logs an error. Every write and read waits behind it meanwhile, so
21
+ * the lock logs a warning. Every write and read waits behind it meanwhile, so
22
22
  * one that never ends stops the server. Defaults to 10 s; `0` turns the
23
23
  * check off.
24
24
  */
@@ -109,7 +109,7 @@ export class HydraniumWorkspaceLock extends DefaultWorkspaceLock {
109
109
  }
110
110
 
111
111
  protected reportStalledWrite(): void {
112
- this.tracer.error(
112
+ this.tracer.warn(
113
113
  `A write is still running ${this.stalledWriteWarnMs} ms after it was cancelled, and every write and read waits behind it. ` +
114
114
  'Likely causes: a model-service write or rebuild from inside a build in a host with no write-lock scope tracker, ' +
115
115
  'which waits for the build it runs in, or a write action that ignores its cancellation token. ' +
@@ -7,7 +7,7 @@
7
7
  * SPDX-License-Identifier: MIT
8
8
  ********************************************************************************/
9
9
 
10
- import { Deferred, type Logger, type Tracer } from '@hydranium/protocol';
10
+ import { Deferred, type Logger, type Tracer, describeError, describeErrorWithStack } from '@hydranium/protocol';
11
11
  import {
12
12
  DefaultWorkspaceManager,
13
13
  DocumentState,
@@ -69,7 +69,7 @@ export function handleProcessUnhandledRejection(reason: unknown, logger: Logger)
69
69
  if (typeof reason === 'symbol' && String(reason) === 'Symbol(OperationCancelled)') {
70
70
  return;
71
71
  }
72
- logger.error('Unhandled promise rejection: ' + (reason instanceof Error ? (reason.stack ?? reason.message) : String(reason)));
72
+ logger.error('Unhandled promise rejection: ' + describeErrorWithStack(reason));
73
73
  }
74
74
 
75
75
  /**
@@ -507,11 +507,10 @@ export class HydraniumWorkspaceManager extends DefaultWorkspaceManager {
507
507
  await super.initializeWorkspace(folders, cancelToken);
508
508
  this.workspaceInitializedDeferred.resolve(undefined);
509
509
  } catch (error: unknown) {
510
- const message = error instanceof Error ? error.message : String(error);
511
510
  if (isOperationCancelled(error) || isConnectionGoneError(error)) {
512
- this.tracer.debug(`Initial workspace build did not complete: ${message}`);
511
+ this.tracer.debug(`Initial workspace build did not complete: ${describeError(error)}`);
513
512
  } else {
514
- this.tracer.error(`Initial workspace build failed: ${message}`);
513
+ this.tracer.error(`Initial workspace build failed: ${describeErrorWithStack(error)}`);
515
514
  }
516
515
  this.workspaceInitializedDeferred.reject(error);
517
516
  throw error;
@@ -8,6 +8,7 @@
8
8
  ********************************************************************************/
9
9
 
10
10
  import { URI } from '@hydranium/langium';
11
+ import { describeErrorWithStack } from '@hydranium/protocol';
11
12
  import { type WritableFileSystemProvider } from './file-system-provider.js';
12
13
  import { serverSharedFactory, type ServerSharedServicesMinimal } from '../shared-services.js';
13
14
  import { type FileSystemSeed, InMemoryFileSystemProvider, type InMemoryFileSystemOptions } from './in-memory-file-system-provider.js';
@@ -250,7 +251,7 @@ export class PersistentFileSystemProvider extends InMemoryFileSystemProvider {
250
251
  }
251
252
 
252
253
  protected reportStoreFailure(operation: string, uri: URI, error: unknown): void {
253
- this.tracer.error(`Failed to ${operation} ${uri.toString()}: ${error instanceof Error ? error.message : String(error)}`);
254
+ this.tracer.error(`Failed to ${operation} ${uri.toString()}: ${describeErrorWithStack(error)}`);
254
255
  }
255
256
  }
256
257