@hydranium/core 1.0.0-next.169 → 1.0.0-next.173
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/langium/document-builder/document-builder.d.ts +8 -7
- package/lib/langium/document-builder/document-builder.d.ts.map +1 -1
- package/lib/langium/document-builder/document-builder.js +9 -8
- package/lib/langium/document-builder/document-builder.js.map +1 -1
- package/lib/langium/integrity/integrity-service.d.ts +8 -7
- package/lib/langium/integrity/integrity-service.d.ts.map +1 -1
- package/lib/langium/integrity/integrity-service.js +20 -12
- package/lib/langium/integrity/integrity-service.js.map +1 -1
- package/lib/langium/model-service/model-service.d.ts +27 -40
- package/lib/langium/model-service/model-service.d.ts.map +1 -1
- package/lib/langium/model-service/model-service.js +16 -25
- package/lib/langium/model-service/model-service.js.map +1 -1
- package/lib/langium/module.d.ts +1 -1
- package/lib/langium/module.js +1 -1
- package/lib/langium/module.js.map +1 -1
- package/lib/langium/workspace/hydranium-workspace-lock.d.ts +31 -2
- package/lib/langium/workspace/hydranium-workspace-lock.d.ts.map +1 -1
- package/lib/langium/workspace/hydranium-workspace-lock.js +41 -2
- package/lib/langium/workspace/hydranium-workspace-lock.js.map +1 -1
- package/lib/langium/workspace/write-lock-scope.d.ts +19 -10
- package/lib/langium/workspace/write-lock-scope.d.ts.map +1 -1
- package/lib/langium/workspace/write-lock-scope.js +11 -4
- package/lib/langium/workspace/write-lock-scope.js.map +1 -1
- package/lib/testing/make-test-services.d.ts +1 -1
- package/lib/testing/make-test-services.js +4 -4
- package/lib/testing/make-test-services.js.map +1 -1
- package/lib/testing/stub-model-service.d.ts +1 -1
- package/lib/testing/stub-model-service.js +1 -1
- package/package.json +5 -5
- package/src/langium/document-builder/document-builder.ts +9 -8
- package/src/langium/integrity/integrity-service.ts +27 -13
- package/src/langium/model-service/model-service.ts +35 -57
- package/src/langium/module.ts +2 -2
- package/src/langium/workspace/hydranium-workspace-lock.ts +66 -3
- package/src/langium/workspace/write-lock-scope.ts +23 -11
- package/src/testing/make-test-services.ts +5 -5
- package/src/testing/stub-model-service.ts +1 -1
|
@@ -8,8 +8,16 @@
|
|
|
8
8
|
********************************************************************************/
|
|
9
9
|
|
|
10
10
|
import { isPromiseLike, Logger, type ProfileSession, type Tracer } from '@hydranium/protocol';
|
|
11
|
-
import {
|
|
12
|
-
|
|
11
|
+
import {
|
|
12
|
+
type AstNode,
|
|
13
|
+
AstUtils,
|
|
14
|
+
DocumentState,
|
|
15
|
+
interruptAndCheck,
|
|
16
|
+
isOperationCancelled,
|
|
17
|
+
type LangiumDocument,
|
|
18
|
+
UriUtils
|
|
19
|
+
} from '@hydranium/langium';
|
|
20
|
+
import { CancellationToken, type Disposable } from 'vscode-languageserver';
|
|
13
21
|
import { type HydraniumDocumentBuilder } from '../document-builder/document-builder.js';
|
|
14
22
|
import { type TextDocument } from 'vscode-languageserver-textdocument';
|
|
15
23
|
import { type WritableFileSystemProvider } from '../../documents/ast-document-manager.js';
|
|
@@ -200,7 +208,8 @@ export class DefaultIntegrityService<TRoot extends AstNode = AstNode> implements
|
|
|
200
208
|
const changed = await this.enforceIntegrity(document, phase, cancelToken, session);
|
|
201
209
|
if (changed) {
|
|
202
210
|
mutatedCount++;
|
|
203
|
-
|
|
211
|
+
// Not cancellable: see `resyncDocument`.
|
|
212
|
+
await this.resyncDocument(document, CancellationToken.None);
|
|
204
213
|
}
|
|
205
214
|
await interruptAndCheck(cancelToken);
|
|
206
215
|
}
|
|
@@ -261,8 +270,10 @@ export class DefaultIntegrityService<TRoot extends AstNode = AstNode> implements
|
|
|
261
270
|
* full rule sweep) — coarser than per (node × rule) to avoid a microtask
|
|
262
271
|
* per inner iteration, finer than per-document so a long-running rule set
|
|
263
272
|
* on a large file can still interrupt mid-stream. Throws
|
|
264
|
-
* `OperationCancelled` when the token is cancelled
|
|
265
|
-
* pipeline absorbs the throw at the lock-holder.
|
|
273
|
+
* `OperationCancelled` when the token is cancelled before any rule mutated
|
|
274
|
+
* the AST; Langium's build pipeline absorbs the throw at the lock-holder.
|
|
275
|
+
* After a mutation it stops the sweep and returns `true` instead, so the
|
|
276
|
+
* caller can resync what was mutated before the cancellation unwinds.
|
|
266
277
|
*/
|
|
267
278
|
async enforceIntegrity(
|
|
268
279
|
document: LangiumDocument,
|
|
@@ -296,7 +307,14 @@ export class DefaultIntegrityService<TRoot extends AstNode = AstNode> implements
|
|
|
296
307
|
}
|
|
297
308
|
}
|
|
298
309
|
if (cancelToken !== undefined) {
|
|
299
|
-
|
|
310
|
+
try {
|
|
311
|
+
await interruptAndCheck(cancelToken);
|
|
312
|
+
} catch (err: unknown) {
|
|
313
|
+
if (changed && isOperationCancelled(err)) {
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
throw err;
|
|
317
|
+
}
|
|
300
318
|
}
|
|
301
319
|
}
|
|
302
320
|
return changed;
|
|
@@ -306,15 +324,11 @@ export class DefaultIntegrityService<TRoot extends AstNode = AstNode> implements
|
|
|
306
324
|
* Re-serialise the document text after AST mutations, propagate corrections,
|
|
307
325
|
* and re-parse at Parsed phase (safe because linking hasn't occurred yet).
|
|
308
326
|
*
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
*
|
|
312
|
-
* a natural interruption point.
|
|
327
|
+
* Runs to the end whatever `cancelToken` says: the AST is already mutated,
|
|
328
|
+
* and skipping the resync leaves it against unchanged text that the next
|
|
329
|
+
* build does not re-parse. `cancelToken` reaches only the re-parse.
|
|
313
330
|
*/
|
|
314
331
|
protected async resyncDocument(document: LangiumDocument, cancelToken?: CancellationToken): Promise<void> {
|
|
315
|
-
if (cancelToken?.isCancellationRequested) {
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
332
|
const root = document.parseResult.value as TRoot;
|
|
319
333
|
// Extracted while `document` still holds the text being replaced — the
|
|
320
334
|
// resync that follows overwrites it with `newText`. Absent on a test
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
import { type AstNode, DocumentState, type LangiumDocument, UriUtils, type URI } from '@hydranium/langium';
|
|
21
21
|
import { type AstDiagnostic } from '../validation/document-validator.js';
|
|
22
22
|
import { type DocumentUriPolicy } from '../workspace/document-uri-policy.js';
|
|
23
|
-
import { ReentrantWriteLockError, isInsideWriteLock } from '../workspace/write-lock-scope.js';
|
|
23
|
+
import { ReentrantWriteLockError, isInsideWriteLock, isWriteLockScopeInstalled } from '../workspace/write-lock-scope.js';
|
|
24
24
|
import { type CancellationToken, type Disposable } from 'vscode-languageserver';
|
|
25
25
|
import { AstDocument, type AstDocumentSavedEvent, type AstDocumentUpdatedEvent } from '../../documents/ast-document-manager.js';
|
|
26
26
|
import { isConnectionGoneError } from '../../util/connection-liveness.js';
|
|
@@ -58,45 +58,32 @@ const SAVE_SETTLE_TIMEOUT_MS = 10_000;
|
|
|
58
58
|
*/
|
|
59
59
|
export interface ModelServiceOptions extends LogNameOptions {
|
|
60
60
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
61
|
+
* Let the facade's build run without the workspace write lock when its
|
|
62
|
+
* caller already holds it: an integrity rule or build-phase pass that
|
|
63
|
+
* writes back through a session's `update` / `save`, or calls `rebuild`.
|
|
64
|
+
* Default `false`.
|
|
64
65
|
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
66
|
+
* `WorkspaceLock` is not reentrant. Taking it from inside a holder cancels
|
|
67
|
+
* that holder and then waits for it to end, while the holder waits for this
|
|
68
|
+
* call, so neither completes. On `false` that call fails with
|
|
69
|
+
* {@link ReentrantWriteLockError} instead, wherever a host installs a
|
|
70
|
+
* write-lock scope tracker (`@hydranium/core/node` does at entry load); on
|
|
71
|
+
* `true` its build takes no lock. The wait after that build can still hang,
|
|
72
|
+
* because a document no build will carry is re-queued through the lock, and
|
|
73
|
+
* the re-queue waits for the holder to end.
|
|
72
74
|
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* **What `false` buys.** `WorkspaceLock` is not reentrant. A caller that
|
|
82
|
-
* reaches a session's `update` / `save` or `rebuild` while already holding
|
|
83
|
-
* the write lock — an integrity rule or build-phase pass that writes
|
|
84
|
-
* back — deadlocks: acquiring the lock cancels the running holder, and the new
|
|
85
|
-
* acquisition then waits for that holder to release while the holder waits
|
|
86
|
-
* for this call. On `true` that shape is DETECTED and rejected with
|
|
87
|
-
* {@link ReentrantWriteLockError} rather than hanging, wherever a host
|
|
88
|
-
* installs a write-lock scope tracker (`@hydranium/core/node` does at entry
|
|
89
|
-
* load; see {@link isInsideWriteLock}). Setting `false` is the escape hatch
|
|
90
|
-
* for an adopter whose reentrant shape is unavoidable: the facade's own
|
|
91
|
-
* build then takes no lock, so it cannot cancel the holder it runs inside.
|
|
92
|
-
* The wait after it can still hang there, because a document no build will
|
|
93
|
-
* carry is re-queued through the lock, and that re-queue waits for the
|
|
94
|
-
* holder to end. Prefer `false` over serialised builds only in that case.
|
|
75
|
+
* Every other facade build takes the lock on either value. Unlocked, it can
|
|
76
|
+
* overlap another build of the same document: both run a full validation
|
|
77
|
+
* pass that Langium appends, and a caller can be handed a version that an
|
|
78
|
+
* integrity repair still running in the other build then moves past. Without
|
|
79
|
+
* a tracker the two cases cannot be told apart (see
|
|
80
|
+
* {@link isWriteLockScopeInstalled}), so `true` builds without the lock every
|
|
81
|
+
* time and accepts both.
|
|
95
82
|
*
|
|
96
83
|
* Accepts a {@link MaybeObservableValue} so it can be bound to a setting and
|
|
97
84
|
* flipped without a restart.
|
|
98
85
|
*/
|
|
99
|
-
readonly
|
|
86
|
+
readonly allowReentrantBuilds?: MaybeObservableValue<boolean>;
|
|
100
87
|
}
|
|
101
88
|
|
|
102
89
|
/**
|
|
@@ -289,8 +276,8 @@ export class DefaultModelService<
|
|
|
289
276
|
* (see {@link DocumentUriPolicy}).
|
|
290
277
|
*/
|
|
291
278
|
protected readonly uriPolicy: DocumentUriPolicy;
|
|
292
|
-
/** See {@link ModelServiceOptions.
|
|
293
|
-
protected readonly
|
|
279
|
+
/** See {@link ModelServiceOptions.allowReentrantBuilds}. Defaults to `false`. */
|
|
280
|
+
protected readonly allowReentrantBuilds: ObservableValue<boolean>;
|
|
294
281
|
|
|
295
282
|
/**
|
|
296
283
|
* Per-URI applyEdit coalescing: at most one in-flight `applyEditToLanguageClient`
|
|
@@ -351,7 +338,7 @@ export class DefaultModelService<
|
|
|
351
338
|
) {
|
|
352
339
|
this.tracer = services.Tracer.for(options.logName ?? 'ModelService').trace('instantiated');
|
|
353
340
|
this.uriPolicy = services.workspace.DocumentUriPolicy;
|
|
354
|
-
this.
|
|
341
|
+
this.allowReentrantBuilds = ObservableValue.from(options.allowReentrantBuilds ?? false);
|
|
355
342
|
// Optional-chain so a harness that binds no WorkspaceManager awaits
|
|
356
343
|
// `undefined` and resolves immediately; production hosts always have it
|
|
357
344
|
// bound.
|
|
@@ -483,9 +470,9 @@ export class DefaultModelService<
|
|
|
483
470
|
* where that build carries it. Langium alone does not coalesce concurrent
|
|
484
471
|
* builds of one URI: two unserialised builds each run a full validation
|
|
485
472
|
* pass and Langium appends the second set onto the first, duplicating every
|
|
486
|
-
* diagnostic.
|
|
487
|
-
* {@link ModelServiceOptions.
|
|
488
|
-
* non-reentrancy hazard
|
|
473
|
+
* diagnostic. A caller that already holds the lock builds without it only
|
|
474
|
+
* under {@link ModelServiceOptions.allowReentrantBuilds} — see there for the
|
|
475
|
+
* non-reentrancy hazard.
|
|
489
476
|
*
|
|
490
477
|
* An override calls the base before it awaits. Under an LSP connection the
|
|
491
478
|
* store's change event has already asked the update handler to build a
|
|
@@ -533,28 +520,19 @@ export class DefaultModelService<
|
|
|
533
520
|
// meant for category-partitioned passes). The user-visible result is every
|
|
534
521
|
// diagnostic duplicated, plus double the validation work per write.
|
|
535
522
|
//
|
|
536
|
-
//
|
|
537
|
-
// the
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
// gated on `serializeBuilds` deliberately, because acquiring the lock IS
|
|
545
|
-
// the hazard: with serialisation off the build takes no lock, which
|
|
546
|
-
// makes the existing opt-out the guard's opt-out too.
|
|
547
|
-
// Detection needs async-context propagation, so it is inert until a host
|
|
548
|
-
// installs a tracker (`@hydranium/core/node` does) — see
|
|
549
|
-
// `isInsideWriteLock`.
|
|
550
|
-
if (isInsideWriteLock()) {
|
|
523
|
+
// A caller already inside the lock would cancel its own build and stall in
|
|
524
|
+
// the wait below; `allowReentrantBuilds` builds it unlocked, and without a
|
|
525
|
+
// tracker it cannot be told apart from any other caller.
|
|
526
|
+
const inside = isInsideWriteLock();
|
|
527
|
+
if (this.allowReentrantBuilds.value && (inside || !isWriteLockScopeInstalled())) {
|
|
528
|
+
await this.services.workspace.DocumentBuilder.update([documentUri], [], cancelToken);
|
|
529
|
+
} else {
|
|
530
|
+
if (inside) {
|
|
551
531
|
throw new ReentrantWriteLockError(uri);
|
|
552
532
|
}
|
|
553
533
|
// The build runs on the lock's own token, which a later write cancels;
|
|
554
534
|
// the caller's token governs only the phase wait below.
|
|
555
535
|
await this.services.workspace.DocumentBuilder.scheduleUpdate([documentUri], []);
|
|
556
|
-
} else {
|
|
557
|
-
await this.services.workspace.DocumentBuilder.update([documentUri], [], cancelToken);
|
|
558
536
|
}
|
|
559
537
|
return this.waitForDocumentStateCanonical(uri, state ?? IntegrityService.SettledState, cancelToken);
|
|
560
538
|
}
|
package/src/langium/module.ts
CHANGED
|
@@ -169,7 +169,7 @@ export interface ServerAddedSharedServices<
|
|
|
169
169
|
* `ModelService.rebuild` build through this slot's
|
|
170
170
|
* `scheduleUpdate`, so narrowing here is a load-bearing contract:
|
|
171
171
|
* a builder without it fails every LSP-driven build, and every
|
|
172
|
-
* session write
|
|
172
|
+
* session write that takes the lock, at run time.
|
|
173
173
|
*/
|
|
174
174
|
/* override */ DocumentBuilder: HydraniumDocumentBuilder;
|
|
175
175
|
/**
|
|
@@ -530,7 +530,7 @@ export function createServerSharedModule(
|
|
|
530
530
|
DocumentBuilder: services => new HydraniumDocumentBuilder(services),
|
|
531
531
|
// The write-lock scope this marks is inert unless a host installs a
|
|
532
532
|
// scope tracker — `@hydranium/core/node` does.
|
|
533
|
-
WorkspaceLock:
|
|
533
|
+
WorkspaceLock: services => new HydraniumWorkspaceLock(services),
|
|
534
534
|
ProjectManager: services => new SingleProjectManager(services),
|
|
535
535
|
SelfSaveRegistry: services => new DefaultSelfSaveRegistry(services),
|
|
536
536
|
DocumentUriPolicy: () => new DefaultDocumentUriPolicy(),
|
|
@@ -7,10 +7,31 @@
|
|
|
7
7
|
* SPDX-License-Identifier: MIT
|
|
8
8
|
********************************************************************************/
|
|
9
9
|
|
|
10
|
+
import { type Clock, type Tracer } from '@hydranium/protocol';
|
|
10
11
|
import { DefaultWorkspaceLock, type MaybePromise } from '@hydranium/langium';
|
|
11
|
-
import { type CancellationToken } from 'vscode-languageserver-protocol';
|
|
12
|
+
import { type CancellationToken, type Disposable } from 'vscode-languageserver-protocol';
|
|
13
|
+
import { type LogNameOptions } from '../diagnostics/logger.js';
|
|
14
|
+
import { type ServerSharedServicesMinimal } from '../shared-services.js';
|
|
12
15
|
import { runInWriteLockScope } from './write-lock-scope.js';
|
|
13
16
|
|
|
17
|
+
/** Constructor options for {@link HydraniumWorkspaceLock}. */
|
|
18
|
+
export interface HydraniumWorkspaceLockOptions extends LogNameOptions {
|
|
19
|
+
/**
|
|
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
|
|
22
|
+
* one that never ends stops the server. Defaults to 10 s; `0` turns the
|
|
23
|
+
* check off.
|
|
24
|
+
*/
|
|
25
|
+
readonly stalledWriteWarnMs?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const DEFAULT_STALLED_WRITE_WARN_MS = 10_000;
|
|
29
|
+
|
|
30
|
+
/** The write action a {@link HydraniumWorkspaceLock} is running, with the timer its cancellation started. */
|
|
31
|
+
export interface RunningWrite {
|
|
32
|
+
stallTimer?: Disposable;
|
|
33
|
+
}
|
|
34
|
+
|
|
14
35
|
/**
|
|
15
36
|
* Langium's {@link DefaultWorkspaceLock} with the write action marked as a
|
|
16
37
|
* write-lock scope, so anything it reaches can be asked whether the lock is
|
|
@@ -31,6 +52,17 @@ import { runInWriteLockScope } from './write-lock-scope.js';
|
|
|
31
52
|
export class HydraniumWorkspaceLock extends DefaultWorkspaceLock {
|
|
32
53
|
/** Backs {@link writeCancellations}. */
|
|
33
54
|
protected cancelledWriteCount = 0;
|
|
55
|
+
protected readonly clock: Clock;
|
|
56
|
+
protected readonly tracer: Tracer;
|
|
57
|
+
protected readonly stalledWriteWarnMs: number;
|
|
58
|
+
protected runningWrite?: RunningWrite;
|
|
59
|
+
|
|
60
|
+
constructor(services: Pick<ServerSharedServicesMinimal, 'Clock' | 'Tracer'>, options: HydraniumWorkspaceLockOptions = {}) {
|
|
61
|
+
super();
|
|
62
|
+
this.clock = services.Clock;
|
|
63
|
+
this.tracer = services.Tracer.for(options.logName ?? 'WorkspaceLock');
|
|
64
|
+
this.stalledWriteWarnMs = options.stalledWriteWarnMs ?? DEFAULT_STALLED_WRITE_WARN_MS;
|
|
65
|
+
}
|
|
34
66
|
|
|
35
67
|
/**
|
|
36
68
|
* How many calls have cancelled the lock's latest write: every `write`,
|
|
@@ -45,12 +77,43 @@ export class HydraniumWorkspaceLock extends DefaultWorkspaceLock {
|
|
|
45
77
|
}
|
|
46
78
|
|
|
47
79
|
override write(action: (token: CancellationToken) => MaybePromise<void>): Promise<void> {
|
|
48
|
-
return super.write(token => runInWriteLockScope(() => action
|
|
80
|
+
return super.write(token => runInWriteLockScope(() => this.runWrite(action, token)));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Run `action` as {@link runningWrite}, so a cancellation while it runs can time it. */
|
|
84
|
+
protected async runWrite(action: (token: CancellationToken) => MaybePromise<void>, token: CancellationToken): Promise<void> {
|
|
85
|
+
const running: RunningWrite = {};
|
|
86
|
+
this.runningWrite = running;
|
|
87
|
+
try {
|
|
88
|
+
await action(token);
|
|
89
|
+
} finally {
|
|
90
|
+
running.stallTimer?.dispose();
|
|
91
|
+
if (this.runningWrite === running) {
|
|
92
|
+
this.runningWrite = undefined;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
49
95
|
}
|
|
50
96
|
|
|
51
|
-
/**
|
|
97
|
+
/**
|
|
98
|
+
* Counted here rather than in {@link write}, because Langium's `write` cancels
|
|
99
|
+
* through this method too. A write running when this is called has been
|
|
100
|
+
* cancelled, by this call or an earlier one, so its timer starts here.
|
|
101
|
+
*/
|
|
52
102
|
override cancelWrite(): void {
|
|
53
103
|
this.cancelledWriteCount++;
|
|
104
|
+
const running = this.runningWrite;
|
|
105
|
+
if (running && !running.stallTimer && this.stalledWriteWarnMs > 0) {
|
|
106
|
+
running.stallTimer = this.clock.setTimer(() => this.reportStalledWrite(), this.stalledWriteWarnMs);
|
|
107
|
+
}
|
|
54
108
|
super.cancelWrite();
|
|
55
109
|
}
|
|
110
|
+
|
|
111
|
+
protected reportStalledWrite(): void {
|
|
112
|
+
this.tracer.error(
|
|
113
|
+
`A write is still running ${this.stalledWriteWarnMs} ms after it was cancelled, and every write and read waits behind it. ` +
|
|
114
|
+
'Likely causes: a model-service write or rebuild from inside a build in a host with no write-lock scope tracker, ' +
|
|
115
|
+
'which waits for the build it runs in, or a write action that ignores its cancellation token. ' +
|
|
116
|
+
'See https://github.com/eclipse-emfcloud/hydranium/issues/245.'
|
|
117
|
+
);
|
|
118
|
+
}
|
|
56
119
|
}
|
|
@@ -22,13 +22,17 @@
|
|
|
22
22
|
* Answering it needs async-context propagation, which has no browser-neutral
|
|
23
23
|
* implementation: Node's `AsyncLocalStorage` is a `node:async_hooks` import (so
|
|
24
24
|
* it cannot appear on the portable `.` entry, which `check:neutral` gates), and
|
|
25
|
-
* TC39's `AsyncContext` is unshipped.
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
25
|
+
* TC39's `AsyncContext` is unshipped. Userland polyfills lose the context at a
|
|
26
|
+
* native `await` unless every awaited path, Langium's included, is transpiled.
|
|
27
|
+
* Hence the same seam shape the log file-tee uses: this module declares the
|
|
28
|
+
* contract and defaults to inert, and `@hydranium/core/node` installs a real
|
|
29
|
+
* implementation at entry load. A browser bundle therefore gets NO detection
|
|
30
|
+
* rather than a broken approximation, which is the right degradation — the
|
|
31
|
+
* reentrant shape is a server-side build concern, and a false negative costs
|
|
32
|
+
* the stall, which `HydraniumWorkspaceLock` reports once the stalled write
|
|
33
|
+
* outlives its cancellation, while a false positive would reject a legitimate
|
|
34
|
+
* write. Detection without a tracker is tracked in
|
|
35
|
+
* https://github.com/eclipse-emfcloud/hydranium/issues/245.
|
|
32
36
|
*/
|
|
33
37
|
export interface WriteLockScope {
|
|
34
38
|
/** Run `fn` marked as executing inside a write-lock holder. */
|
|
@@ -64,6 +68,14 @@ export function isInsideWriteLock(): boolean {
|
|
|
64
68
|
return writeLockScope?.isInside() ?? false;
|
|
65
69
|
}
|
|
66
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Whether a write-lock scope tracker is installed, which is what makes a
|
|
73
|
+
* `false` from {@link isInsideWriteLock} mean "outside" rather than "unknown".
|
|
74
|
+
*/
|
|
75
|
+
export function isWriteLockScopeInstalled(): boolean {
|
|
76
|
+
return writeLockScope !== undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
67
79
|
/**
|
|
68
80
|
* Thrown when the model facade is reached from inside a workspace write-lock
|
|
69
81
|
* holder — typically an integrity rule or build-phase pass writing through
|
|
@@ -72,9 +84,9 @@ export function isInsideWriteLock(): boolean {
|
|
|
72
84
|
* Named rather than a bare `Error` because the remedy is specific and worth
|
|
73
85
|
* pointing at: either move the write out of the build (integrity rules that
|
|
74
86
|
* persist repairs should write through `FileSystemProvider.writeFile`, which
|
|
75
|
-
* takes no lock), or set `ModelServiceOptions.
|
|
76
|
-
* keeps the facade's build from cancelling the holder but not its wait
|
|
77
|
-
* stalling on a document that needs a re-queue.
|
|
87
|
+
* takes no lock), or set `ModelServiceOptions.allowReentrantBuilds: true`,
|
|
88
|
+
* which keeps the facade's build from cancelling the holder but not its wait
|
|
89
|
+
* from stalling on a document that needs a re-queue.
|
|
78
90
|
*/
|
|
79
91
|
export class ReentrantWriteLockError extends Error {
|
|
80
92
|
constructor(readonly uri: string) {
|
|
@@ -82,7 +94,7 @@ export class ReentrantWriteLockError extends Error {
|
|
|
82
94
|
`Model facade reached for '${uri}' from inside a workspace write-lock holder. ` +
|
|
83
95
|
'Acquiring the write lock would cancel the enclosing build and may stall this call. ' +
|
|
84
96
|
'Write through FileSystemProvider.writeFile from a build-phase pass, or set ' +
|
|
85
|
-
'ModelServiceOptions.
|
|
97
|
+
'ModelServiceOptions.allowReentrantBuilds: true to build without the lock from inside it.'
|
|
86
98
|
);
|
|
87
99
|
this.name = 'ReentrantWriteLockError';
|
|
88
100
|
}
|
|
@@ -218,7 +218,7 @@ export interface MakeTestServicesOptions<
|
|
|
218
218
|
modelService?: (services: ServerSharedServices<TProject>) => ModelService<TAst, TDiagnostic, TTransfer>;
|
|
219
219
|
/**
|
|
220
220
|
* Framework {@link ModelServiceOptions} for the DEFAULT stub service, so a
|
|
221
|
-
* test can reach an option-gated path (`
|
|
221
|
+
* test can reach an option-gated path (`allowReentrantBuilds`) without supplying
|
|
222
222
|
* a whole {@link modelService} factory. Ignored when `modelService` is
|
|
223
223
|
* given — that factory owns its own construction.
|
|
224
224
|
*/
|
|
@@ -312,15 +312,15 @@ export function makeTestServices<
|
|
|
312
312
|
): TestServicesBundle<TAst, TDiagnostic, TTransfer, TProject, TTransferDiagnostic> {
|
|
313
313
|
const documents = makeStubLangiumDocuments<TAst, TDiagnostic>(options.seedDocuments);
|
|
314
314
|
const textDocuments = makeStubHydraniumTextDocuments();
|
|
315
|
-
const
|
|
315
|
+
const logger = options.logger ?? new NoopLogger();
|
|
316
|
+
const clock = options.clock ?? new SystemClock();
|
|
317
|
+
const tracer = new DefaultTracer(logger, clock);
|
|
318
|
+
const workspaceLock = new HydraniumWorkspaceLock({ Clock: clock, Tracer: tracer });
|
|
316
319
|
const documentBuilder = makeStubDocumentBuilder(workspaceLock);
|
|
317
320
|
const selfSaveRegistry = makeStubSelfSaveRegistry();
|
|
318
321
|
const fileSystem = makeStubWritableFileSystem(selfSaveRegistry);
|
|
319
322
|
const projectManager = makeStubProjectManager<TProject>(options.seedProjects);
|
|
320
323
|
const astDocumentManager = makeStubAstDocumentManager<TAst, TDiagnostic>(textDocuments, fileSystem, documents);
|
|
321
|
-
const logger = options.logger ?? new NoopLogger();
|
|
322
|
-
const clock = options.clock ?? new SystemClock();
|
|
323
|
-
const tracer = new DefaultTracer(logger, clock);
|
|
324
324
|
const documentUriPolicy = options.documentUriPolicy ?? new DefaultDocumentUriPolicy();
|
|
325
325
|
const serviceRegistry = options.languages
|
|
326
326
|
? makeStubServiceRegistry(options.languages, { openLanguageIds: options.openLanguageIds })
|
|
@@ -53,7 +53,7 @@ export type StubModelService<
|
|
|
53
53
|
* `makeTestServices` wires this stub automatically.
|
|
54
54
|
*
|
|
55
55
|
* `options` are the framework {@link ModelServiceOptions}, forwarded verbatim, so
|
|
56
|
-
* a test can exercise an option-gated code path (`
|
|
56
|
+
* a test can exercise an option-gated code path (`allowReentrantBuilds`) without
|
|
57
57
|
* hand-rolling a subclass just to reach the constructor.
|
|
58
58
|
*
|
|
59
59
|
* Adopters that need richer override behaviour — the framework's own extension
|