@hydranium/core 1.0.0-next.140 → 1.0.0-next.148
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/hydranium-text-documents.d.ts.map +1 -1
- package/lib/documents/hydranium-text-documents.js +3 -13
- package/lib/documents/hydranium-text-documents.js.map +1 -1
- package/lib/langium/document-builder/document-builder.d.ts +140 -13
- package/lib/langium/document-builder/document-builder.d.ts.map +1 -1
- package/lib/langium/document-builder/document-builder.js +218 -24
- package/lib/langium/document-builder/document-builder.js.map +1 -1
- package/lib/langium/model-service/model-service.d.ts +25 -28
- package/lib/langium/model-service/model-service.d.ts.map +1 -1
- package/lib/langium/model-service/model-service.js +25 -30
- package/lib/langium/model-service/model-service.js.map +1 -1
- package/lib/langium/module.d.ts +5 -6
- package/lib/langium/module.d.ts.map +1 -1
- package/lib/langium/module.js.map +1 -1
- package/lib/langium/workspace/hydranium-workspace-lock.d.ts +13 -0
- package/lib/langium/workspace/hydranium-workspace-lock.d.ts.map +1 -1
- package/lib/langium/workspace/hydranium-workspace-lock.js +18 -0
- package/lib/langium/workspace/hydranium-workspace-lock.js.map +1 -1
- package/lib/lsp/hydranium-document-update-handler.d.ts +24 -21
- package/lib/lsp/hydranium-document-update-handler.d.ts.map +1 -1
- package/lib/lsp/hydranium-document-update-handler.js +27 -28
- package/lib/lsp/hydranium-document-update-handler.js.map +1 -1
- package/lib/testing/make-test-services.d.ts.map +1 -1
- package/lib/testing/make-test-services.js +3 -2
- package/lib/testing/make-test-services.js.map +1 -1
- package/lib/testing/stub-document-builder.d.ts +12 -6
- package/lib/testing/stub-document-builder.d.ts.map +1 -1
- package/lib/testing/stub-document-builder.js +9 -1
- package/lib/testing/stub-document-builder.js.map +1 -1
- package/package.json +5 -5
- package/src/documents/hydranium-text-documents.ts +3 -12
- package/src/langium/document-builder/document-builder.ts +253 -28
- package/src/langium/model-service/model-service.ts +34 -41
- package/src/langium/module.ts +5 -6
- package/src/langium/workspace/hydranium-workspace-lock.ts +21 -0
- package/src/lsp/hydranium-document-update-handler.ts +28 -31
- package/src/testing/make-test-services.ts +3 -2
- package/src/testing/stub-document-builder.ts +16 -6
|
@@ -58,17 +58,17 @@ const SAVE_SETTLE_TIMEOUT_MS = 10_000;
|
|
|
58
58
|
*/
|
|
59
59
|
export interface ModelServiceOptions extends LogNameOptions {
|
|
60
60
|
/**
|
|
61
|
-
* Serialise the facade's own build under the workspace WRITE lock,
|
|
62
|
-
*
|
|
63
|
-
* `true`.
|
|
61
|
+
* Serialise the facade's own build under the workspace WRITE lock, through
|
|
62
|
+
* `HydraniumDocumentBuilder.scheduleUpdate` as the LSP update handler
|
|
63
|
+
* builds. Default `true`.
|
|
64
64
|
*
|
|
65
65
|
* **Why it defaults on.** Unlocked, the facade's build races the LSP bridge's
|
|
66
66
|
* build of the same URI — both are legitimate (the bridge exists only under a
|
|
67
67
|
* `Connection`, so the facade stands in for it headless), but nothing
|
|
68
68
|
* serialises them, so both run a full validation pass and Langium appends the
|
|
69
69
|
* second onto the first. Every diagnostic is then duplicated, and the
|
|
70
|
-
* duplication compounds per rebuild. Serialised,
|
|
71
|
-
* build
|
|
70
|
+
* duplication compounds per rebuild. Serialised, the second request shares
|
|
71
|
+
* the first build where it carries both, so the redundant work goes too.
|
|
72
72
|
*
|
|
73
73
|
* **What `false` costs.** The facade's build is then unserialised and can run
|
|
74
74
|
* concurrently with the bridge's build of the same URI, so the affected
|
|
@@ -165,10 +165,10 @@ export interface ModelServiceOptions extends LogNameOptions {
|
|
|
165
165
|
* implementing the URI gate at each callsite.
|
|
166
166
|
*
|
|
167
167
|
* **Two families, and the distinction matters more than the names suggest.**
|
|
168
|
-
* `waitFor*`
|
|
169
|
-
* a document no build
|
|
170
|
-
* `ensureDocumentState` and the phase shorthands over it
|
|
171
|
-
* documents are awaited, cold ones are built.
|
|
168
|
+
* `waitFor*` requests no build of its own: a wait on a URI with no document
|
|
169
|
+
* rejects, and one on a document no build will carry is re-queued by the
|
|
170
|
+
* builder's wait. `ensureDocumentState` and the phase shorthands over it
|
|
171
|
+
* *dispatch*: warm documents are awaited, cold ones are built.
|
|
172
172
|
*
|
|
173
173
|
* **Diagnostics are typed `never` below `Validated`.** Validation is the last
|
|
174
174
|
* phase, so at any earlier landmark the array either is not yet computed or
|
|
@@ -397,8 +397,7 @@ export class DefaultModelService<
|
|
|
397
397
|
/**
|
|
398
398
|
* Wait for the document at `uri` to reach `state`. Pure wait — does
|
|
399
399
|
* not trigger a build. If `uri` is not yet in the document registry
|
|
400
|
-
* the call
|
|
401
|
-
* pipeline; for the cold-start case use {@link rebuild} instead.
|
|
400
|
+
* the call rejects; for the cold-start case use {@link rebuild} instead.
|
|
402
401
|
*
|
|
403
402
|
* Wrapped in a debug-level timing log via {@link Tracer.time} so
|
|
404
403
|
* slow per-URI waits surface in build telemetry; the URI is
|
|
@@ -470,24 +469,23 @@ export class DefaultModelService<
|
|
|
470
469
|
* Force a fresh build of the document at `uri` and wait for it to
|
|
471
470
|
* reach `state` (or the integrity-settled landmark
|
|
472
471
|
* {@link IntegrityService.SettledState} if omitted).
|
|
473
|
-
* Always
|
|
474
|
-
*
|
|
475
|
-
* you want to re-process from scratch.
|
|
472
|
+
* Always builds the document, whether or not it is already in the
|
|
473
|
+
* registry, in a build of its own or in one already scheduled that carries
|
|
474
|
+
* it — call this when you want to re-process from scratch.
|
|
476
475
|
*
|
|
477
|
-
* The facade is responsible for
|
|
478
|
-
* directly because Langium's `DefaultDocumentUpdateHandler.didChangeContent`
|
|
476
|
+
* The facade is responsible for requesting the build itself because Langium's `DefaultDocumentUpdateHandler.didChangeContent`
|
|
479
477
|
* (the standard text-document → builder bridge) only runs under an
|
|
480
478
|
* LSP `Connection`. Running headless the bridge never fires; the
|
|
481
479
|
* facade stands in for it on its own update path.
|
|
482
480
|
*
|
|
483
481
|
* Coexistence with an LSP head running on the same `DocumentBuilder` is
|
|
484
|
-
* fine
|
|
485
|
-
*
|
|
486
|
-
*
|
|
487
|
-
*
|
|
488
|
-
*
|
|
489
|
-
*
|
|
490
|
-
* and Langium appends the second set onto the first, duplicating every
|
|
482
|
+
* fine: the LSP head builds from the LSP-driven event and the facade from
|
|
483
|
+
* its own RPC-driven event — both legitimate — and both go through
|
|
484
|
+
* `HydraniumDocumentBuilder.scheduleUpdate`, which serialises them under
|
|
485
|
+
* the workspace WRITE lock and lets the second share the first's build
|
|
486
|
+
* where that build carries it. Langium alone does not coalesce concurrent
|
|
487
|
+
* builds of one URI: two unserialised builds each run a full validation
|
|
488
|
+
* pass and Langium appends the second set onto the first, duplicating every
|
|
491
489
|
* diagnostic. Opt out via
|
|
492
490
|
* {@link ModelServiceOptions.serializeBuilds} — see there for the
|
|
493
491
|
* non-reentrancy hazard that is the reason the opt-out exists.
|
|
@@ -498,9 +496,8 @@ export class DefaultModelService<
|
|
|
498
496
|
* base's request start a build of its own, so the write is built and
|
|
499
497
|
* delivered twice.
|
|
500
498
|
*
|
|
501
|
-
*
|
|
502
|
-
*
|
|
503
|
-
* on their subclass.
|
|
499
|
+
* Rejects when the build leaves no document for `uri`, as for a URI with
|
|
500
|
+
* neither a file nor text: the wait after it has nothing to wait on.
|
|
504
501
|
*
|
|
505
502
|
* Consumers wanting "give me this doc at state X, building only if
|
|
506
503
|
* needed" — use the per-state methods ({@link parsed} / {@link linked}
|
|
@@ -512,8 +509,8 @@ export class DefaultModelService<
|
|
|
512
509
|
|
|
513
510
|
/**
|
|
514
511
|
* Internal build core operating on an already-{@link CanonicalUri canonical}
|
|
515
|
-
* URI — the build counterpart to {@link waitForDocumentStateCanonical}.
|
|
516
|
-
*
|
|
512
|
+
* URI — the build counterpart to {@link waitForDocumentStateCanonical}.
|
|
513
|
+
* Requests the build, then waits via the canonical wait core, so the
|
|
517
514
|
* identity is resolved once at the public door and neither sink re-runs the
|
|
518
515
|
* `realpath`. (`DocumentBuilder.update` still resolves each URI internally for
|
|
519
516
|
* directory flattening — that is the build's own existence-aware resolution,
|
|
@@ -525,15 +522,15 @@ export class DefaultModelService<
|
|
|
525
522
|
cancelToken?: CancellationToken
|
|
526
523
|
): Promise<AstDocument<TAst, TDiagnostic>> {
|
|
527
524
|
const documentUri = UriUtils.toUri(uri);
|
|
528
|
-
// Runs under the workspace WRITE lock,
|
|
529
|
-
// `
|
|
530
|
-
//
|
|
525
|
+
// Runs under the workspace WRITE lock, through the builder's
|
|
526
|
+
// `scheduleUpdate` like the LSP update handler's build, so the build the
|
|
527
|
+
// store's change event already scheduled for this write carries this
|
|
528
|
+
// request too rather than being cancelled by it.
|
|
531
529
|
//
|
|
532
530
|
// Unlocked, this build races the LSP bridge's build of the same URI: both
|
|
533
531
|
// are legitimate (the bridge only exists under a `Connection`, so the
|
|
534
|
-
// facade stands in for it headless), and
|
|
535
|
-
//
|
|
536
|
-
// two, so both reach `Validated`, and because each computes its missing
|
|
532
|
+
// facade stands in for it headless), and nothing serialises the two, so
|
|
533
|
+
// both reach `Validated`, and because each computes its missing
|
|
537
534
|
// validation categories before the other has recorded its own, both run a
|
|
538
535
|
// FULL pass and Langium appends the second onto the first (its append is
|
|
539
536
|
// meant for category-partitioned passes). The user-visible result is every
|
|
@@ -556,13 +553,9 @@ export class DefaultModelService<
|
|
|
556
553
|
if (isInsideWriteLock()) {
|
|
557
554
|
throw new ReentrantWriteLockError(uri);
|
|
558
555
|
}
|
|
559
|
-
// The lock's
|
|
560
|
-
//
|
|
561
|
-
|
|
562
|
-
// token still governs the phase wait below.
|
|
563
|
-
await this.services.workspace.WorkspaceLock.write(lockToken =>
|
|
564
|
-
this.services.workspace.DocumentBuilder.update([documentUri], [], lockToken)
|
|
565
|
-
);
|
|
556
|
+
// The build runs on the lock's own token, which a later write cancels;
|
|
557
|
+
// the caller's token governs only the phase wait below.
|
|
558
|
+
await this.services.workspace.DocumentBuilder.scheduleUpdate([documentUri], []);
|
|
566
559
|
} else {
|
|
567
560
|
await this.services.workspace.DocumentBuilder.update([documentUri], [], cancelToken);
|
|
568
561
|
}
|
package/src/langium/module.ts
CHANGED
|
@@ -160,12 +160,11 @@ export interface ServerAddedSharedServices<
|
|
|
160
160
|
* the `awaitDocumentState` bug-fix, framework-wired phase logs,
|
|
161
161
|
* and the `markNextReason` observability primitive without
|
|
162
162
|
* binding the slot explicitly. The
|
|
163
|
-
* `HydraniumDocumentUpdateHandler`'s `dispatch`
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
* compile-time target.
|
|
163
|
+
* `HydraniumDocumentUpdateHandler`'s `dispatch` and
|
|
164
|
+
* `ModelService.rebuild` build through this slot's
|
|
165
|
+
* `scheduleUpdate`, so narrowing here is a load-bearing contract:
|
|
166
|
+
* a builder without it fails every LSP-driven build, and every
|
|
167
|
+
* session write while `serializeBuilds` is on, at run time.
|
|
169
168
|
*/
|
|
170
169
|
/* override */ DocumentBuilder: HydraniumDocumentBuilder;
|
|
171
170
|
/**
|
|
@@ -29,7 +29,28 @@ import { runInWriteLockScope } from './write-lock-scope.js';
|
|
|
29
29
|
* would turn ordinary read-then-write sequences into false positives.
|
|
30
30
|
*/
|
|
31
31
|
export class HydraniumWorkspaceLock extends DefaultWorkspaceLock {
|
|
32
|
+
/** Backs {@link writeCancellations}. */
|
|
33
|
+
protected cancelledWriteCount = 0;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* How many calls have cancelled the lock's latest write: every `write`,
|
|
37
|
+
* which cancels the write before it, and every `cancelWrite`. A caller that
|
|
38
|
+
* reads this right after queuing a write, and reads the same number later,
|
|
39
|
+
* holds a write nothing has cancelled, whether it is still queued or
|
|
40
|
+
* already running. Its token answers that only once the write runs, since
|
|
41
|
+
* a queued write has not been handed it yet.
|
|
42
|
+
*/
|
|
43
|
+
get writeCancellations(): number {
|
|
44
|
+
return this.cancelledWriteCount;
|
|
45
|
+
}
|
|
46
|
+
|
|
32
47
|
override write(action: (token: CancellationToken) => MaybePromise<void>): Promise<void> {
|
|
33
48
|
return super.write(token => runInWriteLockScope(() => action(token)));
|
|
34
49
|
}
|
|
50
|
+
|
|
51
|
+
/** Counted here rather than in {@link write}, because Langium's `write` cancels through this method too. */
|
|
52
|
+
override cancelWrite(): void {
|
|
53
|
+
this.cancelledWriteCount++;
|
|
54
|
+
super.cancelWrite();
|
|
55
|
+
}
|
|
35
56
|
}
|
|
@@ -81,10 +81,11 @@ export interface HydraniumDocumentUpdateHandlerOptions {
|
|
|
81
81
|
* 3. **Build-reason stamping** — the LSP event overrides
|
|
82
82
|
* (`didOpenDocument`, `didChangeContent`, `didChangeWatchedFiles`)
|
|
83
83
|
* populate {@link nextReason} with a canonical reason string.
|
|
84
|
-
* {@link fireDocumentUpdate} captures the reason; {@link dispatch}
|
|
85
|
-
* it
|
|
86
|
-
*
|
|
87
|
-
* that
|
|
84
|
+
* {@link fireDocumentUpdate} captures the reason; {@link dispatch} hands
|
|
85
|
+
* it to {@link HydraniumDocumentBuilder.scheduleUpdate}, which stages it
|
|
86
|
+
* through {@link HydraniumDocumentBuilder.markNextReason} when the build
|
|
87
|
+
* runs, so a subclass that formats build logs can tag its line with the
|
|
88
|
+
* event that caused the build without per-adopter wiring to capture it.
|
|
88
89
|
* 4. **Editor save gate** ({@link willSaveDocumentWaitUntil}) — the editor
|
|
89
90
|
* writes the file itself, so the server's disk writes of that document
|
|
90
91
|
* are ordered around it: the editor's save waits for the ones already
|
|
@@ -102,13 +103,12 @@ export interface HydraniumDocumentUpdateHandlerOptions {
|
|
|
102
103
|
* Adopters with their own handler subclass should extend this class
|
|
103
104
|
* (not Langium's `DefaultDocumentUpdateHandler`) so all of them are
|
|
104
105
|
* preserved. The {@link dispatch} hook is the canonical override
|
|
105
|
-
* point for adopters that need to stamp additional per-build metadata
|
|
106
|
-
* inside the workspace-lock callback.
|
|
106
|
+
* point for adopters that need to stamp additional per-build metadata.
|
|
107
107
|
*/
|
|
108
108
|
export class HydraniumDocumentUpdateHandler extends DefaultDocumentUpdateHandler {
|
|
109
109
|
/**
|
|
110
110
|
* Narrow Langium's `DefaultDocumentUpdateHandler.documentBuilder` typing to
|
|
111
|
-
* the framework subclass: `
|
|
111
|
+
* the framework subclass: `scheduleUpdate` is the only method this handler
|
|
112
112
|
* reaches for beyond the Langium interface.
|
|
113
113
|
*/
|
|
114
114
|
declare protected readonly documentBuilder: HydraniumDocumentBuilder;
|
|
@@ -140,9 +140,9 @@ export class HydraniumDocumentUpdateHandler extends DefaultDocumentUpdateHandler
|
|
|
140
140
|
protected nextReason?: string;
|
|
141
141
|
/**
|
|
142
142
|
* Build-reason captured at `fireDocumentUpdate` time, drained by
|
|
143
|
-
* {@link dispatch} into
|
|
144
|
-
* batches
|
|
145
|
-
*
|
|
143
|
+
* {@link dispatch} into the scheduled build. Survives across debounced
|
|
144
|
+
* batches: `fireDocumentUpdate` replaces it with each event's reason, so a
|
|
145
|
+
* coalesced burst carries its latest.
|
|
146
146
|
*/
|
|
147
147
|
protected pendingReason?: string;
|
|
148
148
|
|
|
@@ -342,15 +342,13 @@ export class HydraniumDocumentUpdateHandler extends DefaultDocumentUpdateHandler
|
|
|
342
342
|
* flush immediately (`immediateFlush` or `debounceMs <= 0`) or restart
|
|
343
343
|
* the trailing-edge timer. Subclasses that need to stamp per-build
|
|
344
344
|
* metadata can override and inspect the merged sets — but the canonical
|
|
345
|
-
* override point for the
|
|
346
|
-
* not this method.
|
|
345
|
+
* override point for the build is {@link dispatch}, not this method.
|
|
347
346
|
*/
|
|
348
347
|
protected override fireDocumentUpdate(changed: URI[], deleted: URI[]): void {
|
|
349
348
|
// Capture the reason set by the LSP event handler immediately before
|
|
350
|
-
// we merge into the pending sets.
|
|
351
|
-
//
|
|
352
|
-
//
|
|
353
|
-
// because subsequent stamps see the field already non-undefined).
|
|
349
|
+
// we merge into the pending sets. Each event stamps its own reason, so
|
|
350
|
+
// a coalesced burst carries its latest; `?? pendingReason` keeps the
|
|
351
|
+
// earlier one only for a call no event stamped.
|
|
354
352
|
this.pendingReason = this.nextReason ?? this.pendingReason;
|
|
355
353
|
this.nextReason = undefined;
|
|
356
354
|
for (const uri of changed) {
|
|
@@ -388,26 +386,25 @@ export class HydraniumDocumentUpdateHandler extends DefaultDocumentUpdateHandler
|
|
|
388
386
|
}
|
|
389
387
|
|
|
390
388
|
/**
|
|
391
|
-
*
|
|
392
|
-
*
|
|
393
|
-
*
|
|
394
|
-
*
|
|
395
|
-
*
|
|
396
|
-
*
|
|
397
|
-
*
|
|
398
|
-
* `
|
|
399
|
-
*
|
|
389
|
+
* Build the merged sets once the workspace is ready, through
|
|
390
|
+
* {@link HydraniumDocumentBuilder.scheduleUpdate} with the pending reason,
|
|
391
|
+
* so the build carries the LSP event that triggered it. A session's write
|
|
392
|
+
* fires the store's change event before the session rebuilds the document
|
|
393
|
+
* itself, and the scheduled build is what the two share.
|
|
394
|
+
*
|
|
395
|
+
* Adopters override to stamp additional per-build metadata: read the reason
|
|
396
|
+
* via `this.pendingReason` and call `super.dispatch(...)`; the build may
|
|
397
|
+
* be shared with other requests, so metadata stamped for it describes
|
|
398
|
+
* theirs too. An override that needs code inside the write action takes
|
|
399
|
+
* `workspaceLock.write` itself, as Langium's `fireDocumentUpdate` does, and
|
|
400
|
+
* stages the reason through `markNextReason` there; its build then cancels
|
|
401
|
+
* a scheduled one it coincides with rather than sharing it.
|
|
400
402
|
*/
|
|
401
403
|
protected dispatch(changed: URI[], deleted: URI[]): void {
|
|
402
404
|
const reason = this.pendingReason;
|
|
403
405
|
this.pendingReason = undefined;
|
|
404
406
|
this.workspaceManager.ready
|
|
405
|
-
.then(() =>
|
|
406
|
-
this.workspaceLock.write(token => {
|
|
407
|
-
this.documentBuilder.markNextReason(reason);
|
|
408
|
-
return this.documentBuilder.update(changed, deleted, token);
|
|
409
|
-
})
|
|
410
|
-
)
|
|
407
|
+
.then(() => this.documentBuilder.scheduleUpdate(changed, deleted, reason))
|
|
411
408
|
.catch(err => {
|
|
412
409
|
// A deferred rebuild can finish after the LSP peer has gone away.
|
|
413
410
|
// Unless the head was started through `startLanguageServer`, whose
|
|
@@ -312,7 +312,8 @@ 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 workspaceLock = new HydraniumWorkspaceLock();
|
|
316
|
+
const documentBuilder = makeStubDocumentBuilder(workspaceLock);
|
|
316
317
|
const selfSaveRegistry = makeStubSelfSaveRegistry();
|
|
317
318
|
const fileSystem = makeStubWritableFileSystem(selfSaveRegistry);
|
|
318
319
|
const projectManager = makeStubProjectManager<TProject>(options.seedProjects);
|
|
@@ -366,7 +367,7 @@ export function makeTestServices<
|
|
|
366
367
|
// Spread for the same reason as `ServiceRegistry` above — the slot must
|
|
367
368
|
// be genuinely ABSENT, not present-and-undefined, when no index was seeded.
|
|
368
369
|
...(indexManager ? { IndexManager: indexManager } : {}),
|
|
369
|
-
WorkspaceLock:
|
|
370
|
+
WorkspaceLock: workspaceLock,
|
|
370
371
|
FileSystemTaskQueue: new DefaultFileSystemTaskQueue({ workspace: { DocumentUriPolicy: documentUriPolicy } })
|
|
371
372
|
},
|
|
372
373
|
model: {} as TestSharedServices<TAst, TDiagnostic, TTransfer, TProject, TTransferDiagnostic>['model'],
|
|
@@ -14,7 +14,8 @@ import {
|
|
|
14
14
|
type DocumentState,
|
|
15
15
|
type DocumentUpdateListener,
|
|
16
16
|
type LangiumDocument,
|
|
17
|
-
type URI
|
|
17
|
+
type URI,
|
|
18
|
+
type WorkspaceLock
|
|
18
19
|
} from '@hydranium/langium';
|
|
19
20
|
import { type CancellationToken, Disposable } from 'vscode-languageserver';
|
|
20
21
|
import { type HydraniumDocumentBuilder } from '../langium/document-builder/document-builder.js';
|
|
@@ -65,16 +66,17 @@ export interface StubWaitUntilGate {
|
|
|
65
66
|
* Picks the methods the framework reads from {@link DocumentBuilder} on
|
|
66
67
|
* the slot path — the compiler enforces those signatures stay aligned.
|
|
67
68
|
* `update`, `onDocumentPhase`, `onUpdate`, and `waitUntil` carry stubbed
|
|
68
|
-
* implementations
|
|
69
|
-
*
|
|
70
|
-
*
|
|
69
|
+
* implementations, and `scheduleUpdate` calls `update` in one lock write per
|
|
70
|
+
* call (see {@link makeStubDocumentBuilder}); `build`, `onBuildPhase`, and
|
|
71
|
+
* `resetToState` are implemented as loud-failing `notSupported` throwers
|
|
72
|
+
* so production code that goes through the cast at the bind site (i.e. accesses methods the
|
|
71
73
|
* stub claims but doesn't meaningfully implement) fails with a clear
|
|
72
74
|
* message instead of "undefined is not a function".
|
|
73
75
|
*/
|
|
74
76
|
export interface StubDocumentBuilder
|
|
75
77
|
extends
|
|
76
78
|
Pick<DocumentBuilder, 'update' | 'onDocumentPhase' | 'onUpdate' | 'build' | 'onBuildPhase' | 'resetToState' | 'updateBuildOptions'>,
|
|
77
|
-
Pick<HydraniumDocumentBuilder, 'onDocumentPhaseDelivered'> {
|
|
79
|
+
Pick<HydraniumDocumentBuilder, 'onDocumentPhaseDelivered' | 'scheduleUpdate'> {
|
|
78
80
|
/**
|
|
79
81
|
* Single-overload stub of {@link DocumentBuilder.waitUntil}. Real has
|
|
80
82
|
* two overloads (`(state, cancelToken?): Promise<void>` and
|
|
@@ -138,8 +140,13 @@ function reraise(result: unknown): void {
|
|
|
138
140
|
* want to observe rebuilt state should `set` the new root on the document
|
|
139
141
|
* registry and then call `firePhase` to trigger the framework's post-build
|
|
140
142
|
* read path.
|
|
143
|
+
*
|
|
144
|
+
* `workspaceLock` is the lock `scheduleUpdate` writes under, without the real
|
|
145
|
+
* builder's sharing: pass the services tree's own, so a caller's lock ordering
|
|
146
|
+
* and reentrancy behave as against the real builder. Without one,
|
|
147
|
+
* `scheduleUpdate` records the update unlocked.
|
|
141
148
|
*/
|
|
142
|
-
export function makeStubDocumentBuilder(): StubDocumentBuilder {
|
|
149
|
+
export function makeStubDocumentBuilder(workspaceLock?: WorkspaceLock): StubDocumentBuilder {
|
|
143
150
|
const phaseListeners = new Map<DocumentState, DocumentPhaseListener[]>();
|
|
144
151
|
const deliveredListeners = new Map<DocumentState, Array<(document: LangiumDocument, version: number) => void>>();
|
|
145
152
|
const buildPhaseListeners = new Map<DocumentState, DocumentBuildListener[]>();
|
|
@@ -164,6 +171,9 @@ export function makeStubDocumentBuilder(): StubDocumentBuilder {
|
|
|
164
171
|
async update(changed: URI[], deleted: URI[]) {
|
|
165
172
|
updateCalls.push({ args: [changed, deleted] });
|
|
166
173
|
},
|
|
174
|
+
scheduleUpdate(changed: URI[], deleted: URI[]) {
|
|
175
|
+
return workspaceLock ? workspaceLock.write(() => stub.update(changed, deleted)) : stub.update(changed, deleted);
|
|
176
|
+
},
|
|
167
177
|
async waitUntil(state: DocumentState, uri?: URI) {
|
|
168
178
|
waitUntilCalls.push({ args: [state, uri] });
|
|
169
179
|
const gate = gates.shift();
|