@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.
- package/lib/documents/ast-document-manager.d.ts.map +1 -1
- package/lib/documents/ast-document-manager.js +4 -3
- package/lib/documents/ast-document-manager.js.map +1 -1
- package/lib/documents/document-release-handler.d.ts +2 -2
- package/lib/documents/document-release-handler.d.ts.map +1 -1
- package/lib/documents/document-release-handler.js +14 -19
- package/lib/documents/document-release-handler.js.map +1 -1
- package/lib/documents/hydranium-text-documents.d.ts.map +1 -1
- package/lib/documents/hydranium-text-documents.js +7 -10
- package/lib/documents/hydranium-text-documents.js.map +1 -1
- package/lib/documents/language-client-shadow.d.ts.map +1 -1
- package/lib/documents/language-client-shadow.js +2 -1
- package/lib/documents/language-client-shadow.js.map +1 -1
- package/lib/documents/version-sync-service.d.ts +18 -14
- package/lib/documents/version-sync-service.d.ts.map +1 -1
- package/lib/documents/version-sync-service.js +15 -13
- package/lib/documents/version-sync-service.js.map +1 -1
- package/lib/langium/diagnostics/lsp-logger.d.ts.map +1 -1
- package/lib/langium/diagnostics/lsp-logger.js +2 -2
- package/lib/langium/diagnostics/lsp-logger.js.map +1 -1
- package/lib/langium/document-builder/document-builder.d.ts +2 -2
- package/lib/langium/document-builder/document-builder.d.ts.map +1 -1
- package/lib/langium/document-builder/document-builder.js +23 -10
- package/lib/langium/document-builder/document-builder.js.map +1 -1
- package/lib/langium/integrity/integrity-service.d.ts.map +1 -1
- package/lib/langium/integrity/integrity-service.js +5 -1
- package/lib/langium/integrity/integrity-service.js.map +1 -1
- package/lib/langium/model-service/client-session.d.ts.map +1 -1
- package/lib/langium/model-service/client-session.js +20 -3
- package/lib/langium/model-service/client-session.js.map +1 -1
- package/lib/langium/model-service/model-service.d.ts +4 -3
- package/lib/langium/model-service/model-service.d.ts.map +1 -1
- package/lib/langium/model-service/model-service.js +49 -19
- package/lib/langium/model-service/model-service.js.map +1 -1
- package/lib/langium/project/abstract-project-manager.d.ts.map +1 -1
- package/lib/langium/project/abstract-project-manager.js +4 -4
- package/lib/langium/project/abstract-project-manager.js.map +1 -1
- package/lib/langium/scope/hydranium-scope-computation.d.ts +6 -0
- package/lib/langium/scope/hydranium-scope-computation.d.ts.map +1 -1
- package/lib/langium/scope/hydranium-scope-computation.js +6 -10
- package/lib/langium/scope/hydranium-scope-computation.js.map +1 -1
- package/lib/langium/validation/document-validator.d.ts.map +1 -1
- package/lib/langium/validation/document-validator.js +5 -3
- package/lib/langium/validation/document-validator.js.map +1 -1
- package/lib/langium/workspace/hydranium-workspace-lock.d.ts +1 -1
- package/lib/langium/workspace/hydranium-workspace-lock.js +1 -1
- package/lib/langium/workspace/hydranium-workspace-lock.js.map +1 -1
- package/lib/langium/workspace/hydranium-workspace-manager.d.ts.map +1 -1
- package/lib/langium/workspace/hydranium-workspace-manager.js +4 -5
- package/lib/langium/workspace/hydranium-workspace-manager.js.map +1 -1
- package/lib/langium/workspace/persistent-file-system-provider.d.ts.map +1 -1
- package/lib/langium/workspace/persistent-file-system-provider.js +2 -1
- package/lib/langium/workspace/persistent-file-system-provider.js.map +1 -1
- package/lib/launcher/integrated-server.d.ts +3 -1
- package/lib/launcher/integrated-server.d.ts.map +1 -1
- package/lib/lsp/hydranium-document-update-handler.d.ts.map +1 -1
- package/lib/lsp/hydranium-document-update-handler.js +5 -5
- package/lib/lsp/hydranium-document-update-handler.js.map +1 -1
- package/lib/node/event-loop-monitor.d.ts.map +1 -1
- package/lib/node/event-loop-monitor.js +2 -1
- package/lib/node/event-loop-monitor.js.map +1 -1
- package/lib/node/log-preamble-node.d.ts +5 -3
- package/lib/node/log-preamble-node.d.ts.map +1 -1
- package/lib/node/log-preamble-node.js +13 -10
- package/lib/node/log-preamble-node.js.map +1 -1
- package/lib/node/memory-monitor.d.ts +2 -2
- package/lib/node/memory-monitor.d.ts.map +1 -1
- package/lib/node/memory-monitor.js +3 -5
- package/lib/node/memory-monitor.js.map +1 -1
- package/lib/node/node-file-system-provider.d.ts +5 -2
- package/lib/node/node-file-system-provider.d.ts.map +1 -1
- package/lib/node/node-file-system-provider.js +5 -2
- package/lib/node/node-file-system-provider.js.map +1 -1
- package/lib/node/server-diagnostics.js +1 -1
- package/lib/node/server-diagnostics.js.map +1 -1
- package/lib/node/socket-launcher.d.ts.map +1 -1
- package/lib/node/socket-launcher.js +4 -4
- package/lib/node/socket-launcher.js.map +1 -1
- package/lib/node/stdio-launcher.d.ts.map +1 -1
- package/lib/node/stdio-launcher.js +12 -3
- package/lib/node/stdio-launcher.js.map +1 -1
- package/lib/testing/playwright/server-log-capture.d.ts +7 -0
- package/lib/testing/playwright/server-log-capture.d.ts.map +1 -1
- package/lib/testing/playwright/server-log-capture.js +12 -0
- package/lib/testing/playwright/server-log-capture.js.map +1 -1
- package/lib/testing/playwright/server-log-fixture.d.ts +16 -0
- package/lib/testing/playwright/server-log-fixture.d.ts.map +1 -1
- package/lib/testing/playwright/server-log-fixture.js +25 -6
- package/lib/testing/playwright/server-log-fixture.js.map +1 -1
- package/lib/testing/playwright/server-log-region.d.ts +15 -0
- package/lib/testing/playwright/server-log-region.d.ts.map +1 -0
- package/lib/testing/playwright/server-log-region.js +30 -0
- package/lib/testing/playwright/server-log-region.js.map +1 -0
- package/lib/util/connection-liveness.d.ts.map +1 -1
- package/lib/util/connection-liveness.js +3 -2
- package/lib/util/connection-liveness.js.map +1 -1
- package/package.json +5 -5
- package/src/documents/ast-document-manager.ts +7 -3
- package/src/documents/document-release-handler.ts +15 -21
- package/src/documents/hydranium-text-documents.ts +13 -12
- package/src/documents/language-client-shadow.ts +2 -1
- package/src/documents/version-sync-service.ts +32 -24
- package/src/langium/diagnostics/lsp-logger.ts +2 -1
- package/src/langium/document-builder/document-builder.ts +30 -11
- package/src/langium/integrity/integrity-service.ts +4 -1
- package/src/langium/model-service/client-session.ts +20 -3
- package/src/langium/model-service/model-service.ts +55 -24
- package/src/langium/project/abstract-project-manager.ts +4 -4
- package/src/langium/scope/hydranium-scope-computation.ts +12 -12
- package/src/langium/validation/document-validator.ts +5 -3
- package/src/langium/workspace/hydranium-workspace-lock.ts +2 -2
- package/src/langium/workspace/hydranium-workspace-manager.ts +4 -5
- package/src/langium/workspace/persistent-file-system-provider.ts +2 -1
- package/src/launcher/integrated-server.ts +3 -1
- package/src/lsp/hydranium-document-update-handler.ts +15 -7
- package/src/node/event-loop-monitor.ts +2 -1
- package/src/node/log-preamble-node.ts +20 -10
- package/src/node/memory-monitor.ts +4 -5
- package/src/node/node-file-system-provider.ts +5 -2
- package/src/node/server-diagnostics.ts +1 -1
- package/src/node/socket-launcher.ts +4 -4
- package/src/node/stdio-launcher.ts +11 -3
- package/src/testing/playwright/server-log-capture.ts +14 -0
- package/src/testing/playwright/server-log-fixture.ts +46 -6
- package/src/testing/playwright/server-log-region.ts +32 -0
- 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
|
|
50
|
-
*
|
|
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.
|
|
53
|
-
* request have failed more often than it may retry, so a
|
|
54
|
-
* the build can stop
|
|
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<
|
|
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}
|
|
61
|
+
* {@link requestRecoveryBuild} promise.
|
|
61
62
|
*/
|
|
62
|
-
syncTo(uri: URI, textVersion: number): Promise<
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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<
|
|
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<
|
|
155
|
-
return new Promise<
|
|
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,
|
|
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
|
-
*
|
|
193
|
-
* {@link maxBuildRetries} times, then
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
241
|
-
|
|
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
|
|
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 {
|
|
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: ${
|
|
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
|
-
|
|
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
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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
|
|
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<
|
|
652
|
+
protected requeueOrphaned(document: LangiumDocument, state: DocumentState, reason: string): Promise<void> {
|
|
634
653
|
const tracer = this.tracer.withUri(document.uri.toString());
|
|
635
|
-
tracer.
|
|
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
|
-
.
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
604
|
-
|
|
605
|
-
|
|
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;
|
|
812
|
-
*
|
|
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
|
-
//
|
|
833
|
-
|
|
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: ${
|
|
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
|
|
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
|
-
|
|
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 =
|
|
242
|
+
const name = this.nameProvider.getOwnName(node);
|
|
243
243
|
if (name) {
|
|
244
|
-
symbols.add(container,
|
|
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
|
-
|
|
702
|
-
const
|
|
703
|
-
|
|
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
|
|
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.
|
|
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
|
|
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: ${
|
|
511
|
+
this.tracer.debug(`Initial workspace build did not complete: ${describeError(error)}`);
|
|
513
512
|
} else {
|
|
514
|
-
this.tracer.error(`Initial workspace build failed: ${
|
|
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()}: ${
|
|
254
|
+
this.tracer.error(`Failed to ${operation} ${uri.toString()}: ${describeErrorWithStack(error)}`);
|
|
254
255
|
}
|
|
255
256
|
}
|
|
256
257
|
|