@hydranium/core 1.0.0-next.267 → 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 (121) 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/server-diagnostics.js +1 -1
  71. package/lib/node/server-diagnostics.js.map +1 -1
  72. package/lib/node/socket-launcher.d.ts.map +1 -1
  73. package/lib/node/socket-launcher.js +4 -4
  74. package/lib/node/socket-launcher.js.map +1 -1
  75. package/lib/node/stdio-launcher.d.ts.map +1 -1
  76. package/lib/node/stdio-launcher.js +12 -3
  77. package/lib/node/stdio-launcher.js.map +1 -1
  78. package/lib/testing/playwright/server-log-capture.d.ts +7 -0
  79. package/lib/testing/playwright/server-log-capture.d.ts.map +1 -1
  80. package/lib/testing/playwright/server-log-capture.js +12 -0
  81. package/lib/testing/playwright/server-log-capture.js.map +1 -1
  82. package/lib/testing/playwright/server-log-fixture.d.ts +16 -0
  83. package/lib/testing/playwright/server-log-fixture.d.ts.map +1 -1
  84. package/lib/testing/playwright/server-log-fixture.js +25 -6
  85. package/lib/testing/playwright/server-log-fixture.js.map +1 -1
  86. package/lib/testing/playwright/server-log-region.d.ts +15 -0
  87. package/lib/testing/playwright/server-log-region.d.ts.map +1 -0
  88. package/lib/testing/playwright/server-log-region.js +30 -0
  89. package/lib/testing/playwright/server-log-region.js.map +1 -0
  90. package/lib/util/connection-liveness.d.ts.map +1 -1
  91. package/lib/util/connection-liveness.js +3 -2
  92. package/lib/util/connection-liveness.js.map +1 -1
  93. package/package.json +5 -5
  94. package/src/documents/ast-document-manager.ts +7 -3
  95. package/src/documents/document-release-handler.ts +15 -21
  96. package/src/documents/hydranium-text-documents.ts +13 -12
  97. package/src/documents/language-client-shadow.ts +2 -1
  98. package/src/documents/version-sync-service.ts +32 -24
  99. package/src/langium/diagnostics/lsp-logger.ts +2 -1
  100. package/src/langium/document-builder/document-builder.ts +30 -11
  101. package/src/langium/integrity/integrity-service.ts +4 -1
  102. package/src/langium/model-service/client-session.ts +20 -3
  103. package/src/langium/model-service/model-service.ts +55 -24
  104. package/src/langium/project/abstract-project-manager.ts +4 -4
  105. package/src/langium/scope/hydranium-scope-computation.ts +12 -12
  106. package/src/langium/validation/document-validator.ts +5 -3
  107. package/src/langium/workspace/hydranium-workspace-lock.ts +2 -2
  108. package/src/langium/workspace/hydranium-workspace-manager.ts +4 -5
  109. package/src/langium/workspace/persistent-file-system-provider.ts +2 -1
  110. package/src/launcher/integrated-server.ts +3 -1
  111. package/src/lsp/hydranium-document-update-handler.ts +15 -7
  112. package/src/node/event-loop-monitor.ts +2 -1
  113. package/src/node/log-preamble-node.ts +20 -10
  114. package/src/node/memory-monitor.ts +4 -5
  115. package/src/node/server-diagnostics.ts +1 -1
  116. package/src/node/socket-launcher.ts +4 -4
  117. package/src/node/stdio-launcher.ts +11 -3
  118. package/src/testing/playwright/server-log-capture.ts +14 -0
  119. package/src/testing/playwright/server-log-fixture.ts +46 -6
  120. package/src/testing/playwright/server-log-region.ts +32 -0
  121. package/src/util/connection-liveness.ts +4 -2
@@ -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
 
@@ -20,7 +20,9 @@
20
20
  * primarily by tests waiting for tidy teardown.
21
21
  *
22
22
  * Either promise may reject if startup or shutdown encounters an error
23
- * the head can't recover from.
23
+ * the head can't recover from. A launcher given a logger logs its own startup
24
+ * failure, so a rejected `started` is control flow: the caller stops what
25
+ * depends on it without logging the failure again.
24
26
  */
25
27
  export interface IntegratedServer {
26
28
  readonly started: Promise<void>;
@@ -7,7 +7,15 @@
7
7
  * SPDX-License-Identifier: MIT
8
8
  ********************************************************************************/
9
9
 
10
- import { Debouncer, Deferred, type Logger, ObservableValue, type MaybeObservableValue, TIMED_OUT } from '@hydranium/protocol';
10
+ import {
11
+ Debouncer,
12
+ Deferred,
13
+ type Logger,
14
+ ObservableValue,
15
+ type MaybeObservableValue,
16
+ TIMED_OUT,
17
+ describeErrorWithStack
18
+ } from '@hydranium/protocol';
11
19
  import { URI, UriUtils } from '@hydranium/langium';
12
20
  import { DefaultDocumentUpdateHandler } from '@hydranium/langium/lsp';
13
21
  import { HYDRANIUM_BUILD_REASONS, type HydraniumDocumentBuilder } from '../langium/document-builder/document-builder.js';
@@ -221,7 +229,9 @@ export class HydraniumDocumentUpdateHandler extends DefaultDocumentUpdateHandler
221
229
  for (const change of filtered.changes) {
222
230
  this.textDocuments
223
231
  .reloadDiskBaseline(change.uri)
224
- .catch((err: unknown) => this.logger.error(`Disk baseline not reloaded for ${change.uri}. ${String(err)}`));
232
+ .catch((err: unknown) =>
233
+ this.logger.error(`Disk baseline not reloaded for ${change.uri}. ${describeErrorWithStack(err)}`)
234
+ );
225
235
  }
226
236
  // External file-system changes are authoritative — overwrite any
227
237
  // pending reason (typically empty here; could be `didChangeContent`
@@ -238,7 +248,7 @@ export class HydraniumDocumentUpdateHandler extends DefaultDocumentUpdateHandler
238
248
  // `unhandledRejection` listener, which reports neither the URIs nor
239
249
  // the fact that a watched-file rebuild was dropped — leaving the
240
250
  // workspace diverged from disk with no diagnostic and no retry.
241
- const detail = err instanceof Error ? (err.stack ?? err.message) : String(err);
251
+ const detail = describeErrorWithStack(err);
242
252
  const uris = params.changes.map(change => change.uri).join(', ');
243
253
  this.logger.error(`Watched-file update dropped for ${uris}. ${detail}`);
244
254
  });
@@ -293,9 +303,7 @@ export class HydraniumDocumentUpdateHandler extends DefaultDocumentUpdateHandler
293
303
  });
294
304
  } catch (err) {
295
305
  cap.dispose();
296
- this.logger.error(
297
- `willSaveWaitUntil for ${uri} answered without waiting. ${err instanceof Error ? (err.stack ?? err.message) : String(err)}`
298
- );
306
+ this.logger.error(`willSaveWaitUntil for ${uri} answered without waiting. ${describeErrorWithStack(err)}`);
299
307
  release();
300
308
  resolve([]);
301
309
  }
@@ -417,7 +425,7 @@ export class HydraniumDocumentUpdateHandler extends DefaultDocumentUpdateHandler
417
425
  if (isConnectionGoneError(err)) {
418
426
  return;
419
427
  }
420
- const detail = err instanceof Error ? (err.stack ?? err.message) : String(err);
428
+ const detail = describeErrorWithStack(err);
421
429
  this.logger.error(`Workspace initialization failed. Could not perform document update. ${detail}`);
422
430
  });
423
431
  }
@@ -8,6 +8,7 @@
8
8
  ********************************************************************************/
9
9
 
10
10
  import { monitorEventLoopDelay } from 'node:perf_hooks';
11
+ import { describeErrorWithStack } from '@hydranium/protocol';
11
12
  import { formatMemory } from '../langium/diagnostics/logger.js';
12
13
  import { type ServerSharedServicesMinimal } from '../langium/shared-services.js';
13
14
  import { formatServerState } from './server-state-snapshot.js';
@@ -74,7 +75,7 @@ export function startEventLoopMonitor(shared: ServerSharedServicesMinimal, optio
74
75
  try {
75
76
  onStallSnapshot(maxMs);
76
77
  } catch (error) {
77
- logger.warn(`Stall snapshot failed: ${error instanceof Error ? error.message : String(error)}`);
78
+ logger.warn(`Stall snapshot failed: ${describeErrorWithStack(error)}`);
78
79
  }
79
80
  }
80
81
  }
@@ -17,6 +17,7 @@ import {
17
17
  } from './event-loop-monitor.js';
18
18
  import { MEMORY_DELTA_BYTES } from '../langium/diagnostics/logger.js';
19
19
  import { DEFAULT_MEMORY_CRITICAL_THRESHOLD, DEFAULT_MEMORY_WARN_THRESHOLD } from './memory-monitor.js';
20
+ import { type ServerDiagnosticsOptions } from './server-diagnostics.js';
20
21
  import { type LogPreambleOptions } from '../langium/diagnostics/log-preamble.js';
21
22
 
22
23
  /**
@@ -37,19 +38,25 @@ export function nodeSystemInfoLines(): string[] {
37
38
  * Node monitor + memory-suffix convention lines for
38
39
  * {@link LogPreambleOptions.conventionLines}. These describe the `[EventLoop]`
39
40
  * / `[Memory]` monitors and the heap/rss suffix — all Node-only behaviour, so
40
- * they're omitted from the portable preamble.
41
+ * they're omitted from the portable preamble. Pass the options the monitors
42
+ * were started with, or the lines state the defaults.
41
43
  */
42
- export function nodeConventionLines(phaseDetailThresholdMs = 50): string[] {
44
+ export function nodeConventionLines(
45
+ phaseDetailThresholdMs = 50,
46
+ monitors: Pick<ServerDiagnosticsOptions, 'eventLoop' | 'memory'> = {}
47
+ ): string[] {
43
48
  const memoryDeltaMb = MEMORY_DELTA_BYTES / (1024 * 1024);
44
- const intervalSeconds = DEFAULT_EVENT_LOOP_INTERVAL_MS / 1000;
45
- const warnPercent = Math.round(DEFAULT_MEMORY_WARN_THRESHOLD * 100);
46
- const criticalPercent = Math.round(DEFAULT_MEMORY_CRITICAL_THRESHOLD * 100);
49
+ const thresholdMs = monitors.eventLoop?.thresholdMs ?? DEFAULT_EVENT_LOOP_THRESHOLD_MS;
50
+ const intervalSeconds = (monitors.eventLoop?.intervalMs ?? DEFAULT_EVENT_LOOP_INTERVAL_MS) / 1000;
51
+ const snapshotAboveMs = monitors.eventLoop?.snapshotAboveMs ?? DEFAULT_EVENT_LOOP_SNAPSHOT_ABOVE_MS;
52
+ const warnPercent = Math.round((monitors.memory?.warnThreshold ?? DEFAULT_MEMORY_WARN_THRESHOLD) * 100);
53
+ const criticalPercent = Math.round((monitors.memory?.criticalThreshold ?? DEFAULT_MEMORY_CRITICAL_THRESHOLD) * 100);
47
54
  return [
48
55
  `• Memory suffix: heap/rss included on [done] only when heap moved >${memoryDeltaMb}MB since last log,`,
49
56
  ` always included on rebuilds >=${phaseDetailThresholdMs}ms and on [EventLoop] stall warnings`,
50
- `• [EventLoop]: warns when a stall >=${DEFAULT_EVENT_LOOP_THRESHOLD_MS}ms is observed within a ${intervalSeconds}s window;`,
51
- ` stalls >=${DEFAULT_EVENT_LOOP_SNAPSHOT_ABOVE_MS}ms also trigger a [Process] server-state snapshot`,
52
- `• [Memory]: warns >=${warnPercent}% of V8 heap_size_limit, errors >=${criticalPercent}%, info on recovery`
57
+ `• [EventLoop]: warns when a stall >=${thresholdMs}ms is observed within a ${intervalSeconds}s window;`,
58
+ ` stalls >=${snapshotAboveMs}ms also trigger a [Process] server-state snapshot`,
59
+ `• [Memory]: warns >=${warnPercent}% of V8 heap_size_limit, again as imminent OOM >=${criticalPercent}%, info on recovery`
53
60
  ];
54
61
  }
55
62
 
@@ -58,6 +65,9 @@ export function nodeConventionLines(phaseDetailThresholdMs = 50): string[] {
58
65
  * the portable `logPreamble`'s options on a Node host:
59
66
  * `logPreamble(logger, { productName, version, ...nodeLogPreambleSections() })`.
60
67
  */
61
- export function nodeLogPreambleSections(phaseDetailThresholdMs = 50): Pick<LogPreambleOptions, 'systemInfoLines' | 'conventionLines'> {
62
- return { systemInfoLines: nodeSystemInfoLines(), conventionLines: nodeConventionLines(phaseDetailThresholdMs) };
68
+ export function nodeLogPreambleSections(
69
+ phaseDetailThresholdMs = 50,
70
+ monitors: Pick<ServerDiagnosticsOptions, 'eventLoop' | 'memory'> = {}
71
+ ): Pick<LogPreambleOptions, 'systemInfoLines' | 'conventionLines'> {
72
+ return { systemInfoLines: nodeSystemInfoLines(), conventionLines: nodeConventionLines(phaseDetailThresholdMs, monitors) };
63
73
  }