@hydranium/core 1.0.0-next.135 → 1.0.0-next.140
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 +96 -9
- package/lib/documents/ast-document-manager.d.ts.map +1 -1
- package/lib/documents/ast-document-manager.js +129 -20
- package/lib/documents/ast-document-manager.js.map +1 -1
- package/lib/langium/document-builder/document-builder.d.ts +65 -12
- package/lib/langium/document-builder/document-builder.d.ts.map +1 -1
- package/lib/langium/document-builder/document-builder.js +106 -24
- package/lib/langium/document-builder/document-builder.js.map +1 -1
- package/lib/langium/model-service/client-session.d.ts +9 -1
- package/lib/langium/model-service/client-session.d.ts.map +1 -1
- package/lib/langium/model-service/client-session.js +19 -3
- package/lib/langium/model-service/client-session.js.map +1 -1
- package/lib/langium/model-service/model-service.d.ts +16 -15
- package/lib/langium/model-service/model-service.d.ts.map +1 -1
- package/lib/langium/model-service/model-service.js +4 -5
- package/lib/langium/model-service/model-service.js.map +1 -1
- package/lib/langium/workspace/write-lock-scope.d.ts +3 -2
- package/lib/langium/workspace/write-lock-scope.d.ts.map +1 -1
- package/lib/langium/workspace/write-lock-scope.js +3 -2
- package/lib/langium/workspace/write-lock-scope.js.map +1 -1
- package/lib/testing/stub-ast-document-manager.d.ts +1 -1
- package/lib/testing/stub-ast-document-manager.d.ts.map +1 -1
- package/lib/testing/stub-ast-document-manager.js +9 -0
- package/lib/testing/stub-ast-document-manager.js.map +1 -1
- package/lib/testing/stub-document-builder.d.ts +7 -6
- package/lib/testing/stub-document-builder.d.ts.map +1 -1
- package/lib/testing/stub-document-builder.js +26 -0
- package/lib/testing/stub-document-builder.js.map +1 -1
- package/package.json +5 -5
- package/src/documents/ast-document-manager.ts +158 -25
- package/src/langium/document-builder/document-builder.ts +115 -23
- package/src/langium/model-service/client-session.ts +29 -4
- package/src/langium/model-service/model-service.ts +18 -17
- package/src/langium/workspace/write-lock-scope.ts +3 -2
- package/src/testing/stub-ast-document-manager.ts +21 -1
- package/src/testing/stub-document-builder.ts +36 -9
|
@@ -87,9 +87,11 @@ export interface ModelServiceOptions extends LogNameOptions {
|
|
|
87
87
|
* {@link ReentrantWriteLockError} rather than hanging, wherever a host
|
|
88
88
|
* installs a write-lock scope tracker (`@hydranium/core/node` does at entry
|
|
89
89
|
* load; see {@link isInsideWriteLock}). Setting `false` is the escape hatch
|
|
90
|
-
* for an adopter whose reentrant shape is unavoidable
|
|
91
|
-
*
|
|
92
|
-
*
|
|
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.
|
|
93
95
|
*
|
|
94
96
|
* Accepts a {@link MaybeObservableValue} so it can be bound to a setting and
|
|
95
97
|
* flipped without a restart.
|
|
@@ -134,15 +136,15 @@ export interface ModelServiceOptions extends LogNameOptions {
|
|
|
134
136
|
* **Read-latest supersession**
|
|
135
137
|
*
|
|
136
138
|
* A session's default `update` and `save` use Langium's per-URI
|
|
137
|
-
* `DocumentBuilder.waitUntil` to wait for the integrity-settled
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
* event. Adopters wanting strict
|
|
142
|
-
* with vN's snapshot specifically, log
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
* safety.
|
|
139
|
+
* `DocumentBuilder.waitUntil` to wait for `Validated` (the integrity-settled
|
|
140
|
+
* landmark {@link IntegrityService.SettledState} when rebuilds do not
|
|
141
|
+
* validate), then read the post-build state. Concurrent in-process callers
|
|
142
|
+
* on the same URI all see the latest post-build snapshot — none deadlock
|
|
143
|
+
* waiting for a specific version's settled event. Adopters wanting strict
|
|
144
|
+
* version-matched semantics (resolve with vN's snapshot specifically, log
|
|
145
|
+
* "vN superseded by vM at vN+1") override
|
|
146
|
+
* `DefaultClientSession.updateDocument` to attach a phase-listener with
|
|
147
|
+
* explicit version checks; the framework default doesn't need it for safety.
|
|
146
148
|
*
|
|
147
149
|
* **Returns AST snapshots, not wire envelopes**
|
|
148
150
|
*
|
|
@@ -546,8 +548,8 @@ export class DefaultModelService<
|
|
|
546
548
|
// writing back through this facade — would cancel its own enclosing
|
|
547
549
|
// build and then likely stall in the phase wait below. The check is
|
|
548
550
|
// gated on `serializeBuilds` deliberately, because acquiring the lock IS
|
|
549
|
-
// the hazard: with serialisation off
|
|
550
|
-
//
|
|
551
|
+
// the hazard: with serialisation off the build takes no lock, which
|
|
552
|
+
// makes the existing opt-out the guard's opt-out too.
|
|
551
553
|
// Detection needs async-context propagation, so it is inert until a host
|
|
552
554
|
// installs a tracker (`@hydranium/core/node` does) — see
|
|
553
555
|
// `isInsideWriteLock`.
|
|
@@ -976,9 +978,8 @@ export class DefaultModelService<
|
|
|
976
978
|
* `HydraniumTextDocuments` — single listener registration shared with
|
|
977
979
|
* any direct `HydraniumTextDocuments.onUpdate` subscriber, so the same
|
|
978
980
|
* underlying `DocumentBuilder.onDocumentPhase` listener serves both
|
|
979
|
-
* call paths.
|
|
980
|
-
*
|
|
981
|
-
* `lastUpdate` snapshot (see `AstDocumentManager.onUpdate`).
|
|
981
|
+
* call paths. The event's reason, source and cause are
|
|
982
|
+
* {@link AstDocumentManager.attributeUpdate}'s.
|
|
982
983
|
*/
|
|
983
984
|
onModelUpdated(uri: string, listener: (event: AstDocumentUpdatedEvent<TAst, TDiagnostic>) => void): Disposable {
|
|
984
985
|
return this.services.workspace.AstDocumentManager.onUpdate(uri, listener as never);
|
|
@@ -72,8 +72,9 @@ export function isInsideWriteLock(): boolean {
|
|
|
72
72
|
* Named rather than a bare `Error` because the remedy is specific and worth
|
|
73
73
|
* pointing at: either move the write out of the build (integrity rules that
|
|
74
74
|
* persist repairs should write through `FileSystemProvider.writeFile`, which
|
|
75
|
-
* takes no lock), or set `ModelServiceOptions.serializeBuilds: false
|
|
76
|
-
*
|
|
75
|
+
* takes no lock), or set `ModelServiceOptions.serializeBuilds: false`, which
|
|
76
|
+
* keeps the facade's build from cancelling the holder but not its wait from
|
|
77
|
+
* stalling on a document that needs a re-queue.
|
|
77
78
|
*/
|
|
78
79
|
export class ReentrantWriteLockError extends Error {
|
|
79
80
|
constructor(readonly uri: string) {
|
|
@@ -14,8 +14,10 @@ import {
|
|
|
14
14
|
AstDocument,
|
|
15
15
|
type AstDocumentManager,
|
|
16
16
|
type AstDocumentUpdatedEvent,
|
|
17
|
+
type UpdateAttribution,
|
|
17
18
|
type WritableFileSystemProvider
|
|
18
19
|
} from '../documents/ast-document-manager.js';
|
|
20
|
+
import { UNKNOWN_CLIENT_ID } from '../documents/client-ids.js';
|
|
19
21
|
import type { CloseModelArgs, OpenModelArgs } from '@hydranium/protocol';
|
|
20
22
|
import type { StubHydraniumTextDocuments } from './stub-hydranium-text-documents.js';
|
|
21
23
|
import type { StubLangiumDocuments } from './stub-langium-documents.js';
|
|
@@ -48,7 +50,17 @@ import type { StubLangiumDocuments } from './stub-langium-documents.js';
|
|
|
48
50
|
*/
|
|
49
51
|
export interface StubAstDocumentManager<TAst extends AstNode, TDiagnostic extends AstDiagnostic = AstDiagnostic> extends Pick<
|
|
50
52
|
AstDocumentManager<TAst, TDiagnostic>,
|
|
51
|
-
|
|
53
|
+
| 'open'
|
|
54
|
+
| 'close'
|
|
55
|
+
| 'isOpen'
|
|
56
|
+
| 'update'
|
|
57
|
+
| 'save'
|
|
58
|
+
| 'onUpdate'
|
|
59
|
+
| 'onSave'
|
|
60
|
+
| 'onClientClosed'
|
|
61
|
+
| 'getAuthor'
|
|
62
|
+
| 'getDocument'
|
|
63
|
+
| 'attributeUpdate'
|
|
52
64
|
> {
|
|
53
65
|
readonly openClients: Map<string, Set<string>>;
|
|
54
66
|
|
|
@@ -201,6 +213,14 @@ export function makeStubAstDocumentManager<TAst extends AstNode, TDiagnostic ext
|
|
|
201
213
|
getAuthor(): string {
|
|
202
214
|
return notSupported('getAuthor');
|
|
203
215
|
},
|
|
216
|
+
// Reports every event as a change by the version's author: the stub
|
|
217
|
+
// tree runs no builds, so there is no delivery to tell a rebuild by. A
|
|
218
|
+
// test of the rebuilt branch replaces this method or uses the real
|
|
219
|
+
// manager.
|
|
220
|
+
attributeUpdate(document: LangiumDocument): UpdateAttribution {
|
|
221
|
+
const author = textDocuments.getAuthor(document.textDocument.uri, document.textDocument.version) ?? UNKNOWN_CLIENT_ID;
|
|
222
|
+
return { reason: 'changed', sourceClientId: author, causedBy: author };
|
|
223
|
+
},
|
|
204
224
|
// Canonicalizing document gateway. The stub has no canonicalizer, so it looks
|
|
205
225
|
// the document up in the seeded `StubLangiumDocuments` by its given URI (tests
|
|
206
226
|
// that need real symlink canonicalization wire the real AstDocumentManager).
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
type URI
|
|
18
18
|
} from '@hydranium/langium';
|
|
19
19
|
import { type CancellationToken, Disposable } from 'vscode-languageserver';
|
|
20
|
+
import { type HydraniumDocumentBuilder } from '../langium/document-builder/document-builder.js';
|
|
20
21
|
|
|
21
22
|
/** Recorded call to a stubbed {@link DocumentBuilder} method. */
|
|
22
23
|
export interface RecordedBuilderCall<TArgs extends unknown[]> {
|
|
@@ -46,8 +47,7 @@ export interface StubWaitUntilGate {
|
|
|
46
47
|
|
|
47
48
|
/**
|
|
48
49
|
* Stub for Langium's {@link DocumentBuilder}. Implements the slice production
|
|
49
|
-
* code reads
|
|
50
|
-
* `onDocumentPhase` / `onUpdate`) plus test-only helpers:
|
|
50
|
+
* code reads on the framework's test paths, plus test-only helpers:
|
|
51
51
|
*
|
|
52
52
|
* - {@link firePhase} — synchronously dispatch the registered phase
|
|
53
53
|
* listener(s) for a document, simulating a build completing.
|
|
@@ -71,10 +71,10 @@ export interface StubWaitUntilGate {
|
|
|
71
71
|
* stub claims but doesn't meaningfully implement) fails with a clear
|
|
72
72
|
* message instead of "undefined is not a function".
|
|
73
73
|
*/
|
|
74
|
-
export interface StubDocumentBuilder
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
> {
|
|
74
|
+
export interface StubDocumentBuilder
|
|
75
|
+
extends
|
|
76
|
+
Pick<DocumentBuilder, 'update' | 'onDocumentPhase' | 'onUpdate' | 'build' | 'onBuildPhase' | 'resetToState' | 'updateBuildOptions'>,
|
|
77
|
+
Pick<HydraniumDocumentBuilder, 'onDocumentPhaseDelivered'> {
|
|
78
78
|
/**
|
|
79
79
|
* Single-overload stub of {@link DocumentBuilder.waitUntil}. Real has
|
|
80
80
|
* two overloads (`(state, cancelToken?): Promise<void>` and
|
|
@@ -88,9 +88,10 @@ export interface StubDocumentBuilder extends Pick<
|
|
|
88
88
|
readonly waitUntilCalls: ReadonlyArray<RecordedBuilderCall<[DocumentState, URI | undefined]>>;
|
|
89
89
|
/**
|
|
90
90
|
* Synchronously fire the phase listener(s) registered for `state` with
|
|
91
|
-
* `document
|
|
92
|
-
*
|
|
93
|
-
*
|
|
91
|
+
* `document`, then, unless `cancelToken` is cancelled, the
|
|
92
|
+
* `onDocumentPhaseDelivered` listeners. `cancelToken` defaults to a
|
|
93
|
+
* non-cancelled token; pass a cancelled token to simulate a build preempted
|
|
94
|
+
* by a concurrent write lock.
|
|
94
95
|
*/
|
|
95
96
|
firePhase(state: DocumentState, document: LangiumDocument, cancelToken?: CancellationToken): void;
|
|
96
97
|
/** Synchronously fire every registered `onUpdate` listener. */
|
|
@@ -140,6 +141,7 @@ function reraise(result: unknown): void {
|
|
|
140
141
|
*/
|
|
141
142
|
export function makeStubDocumentBuilder(): StubDocumentBuilder {
|
|
142
143
|
const phaseListeners = new Map<DocumentState, DocumentPhaseListener[]>();
|
|
144
|
+
const deliveredListeners = new Map<DocumentState, Array<(document: LangiumDocument, version: number) => void>>();
|
|
143
145
|
const buildPhaseListeners = new Map<DocumentState, DocumentBuildListener[]>();
|
|
144
146
|
const onUpdateListeners: DocumentUpdateListener[] = [];
|
|
145
147
|
const gates: Array<{ take(release: () => void): void }> = [];
|
|
@@ -150,6 +152,9 @@ export function makeStubDocumentBuilder(): StubDocumentBuilder {
|
|
|
150
152
|
throw new Error(`StubDocumentBuilder.${method} is not implemented; wire a real DocumentBuilder if your test needs it.`);
|
|
151
153
|
};
|
|
152
154
|
const stub: StubDocumentBuilder = {
|
|
155
|
+
// Langium's own default, so a session write waits for `Validated` here
|
|
156
|
+
// as it does against the real builder.
|
|
157
|
+
updateBuildOptions: { validation: { categories: ['built-in', 'fast'] } },
|
|
153
158
|
get updateCalls() {
|
|
154
159
|
return updateCalls;
|
|
155
160
|
},
|
|
@@ -195,9 +200,30 @@ export function makeStubDocumentBuilder(): StubDocumentBuilder {
|
|
|
195
200
|
onCancellationRequested: () => Disposable.create(() => undefined)
|
|
196
201
|
} as CancellationToken);
|
|
197
202
|
const listeners = phaseListeners.get(state) ?? [];
|
|
203
|
+
const delivered = deliveredListeners.get(state) ?? [];
|
|
204
|
+
// Taken before the listeners, as the real builder does: one of them
|
|
205
|
+
// may stand in for a write that moves the version on. Read only when
|
|
206
|
+
// someone is told, since many fakes carry no text document.
|
|
207
|
+
const version = delivered.length > 0 ? document.textDocument.version : 0;
|
|
198
208
|
for (const listener of listeners) {
|
|
199
209
|
reraise(listener(document, token));
|
|
200
210
|
}
|
|
211
|
+
if (!token.isCancellationRequested) {
|
|
212
|
+
for (const listener of delivered) {
|
|
213
|
+
listener(document, version);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
onDocumentPhaseDelivered(state: DocumentState, listener: (document: LangiumDocument, version: number) => void) {
|
|
218
|
+
const list = deliveredListeners.get(state) ?? [];
|
|
219
|
+
list.push(listener);
|
|
220
|
+
deliveredListeners.set(state, list);
|
|
221
|
+
return Disposable.create(() => {
|
|
222
|
+
const idx = list.indexOf(listener);
|
|
223
|
+
if (idx >= 0) {
|
|
224
|
+
list.splice(idx, 1);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
201
227
|
},
|
|
202
228
|
fireOnUpdate(changed: URI[], deleted: URI[]) {
|
|
203
229
|
for (const listener of onUpdateListeners.slice()) {
|
|
@@ -261,6 +287,7 @@ export function makeStubDocumentBuilder(): StubDocumentBuilder {
|
|
|
261
287
|
},
|
|
262
288
|
reset() {
|
|
263
289
|
phaseListeners.clear();
|
|
290
|
+
deliveredListeners.clear();
|
|
264
291
|
onUpdateListeners.length = 0;
|
|
265
292
|
gates.length = 0;
|
|
266
293
|
updateCalls.length = 0;
|