@hydranium/core 1.0.0-next.187 → 1.0.0-next.188
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 +26 -44
- package/lib/documents/ast-document-manager.d.ts.map +1 -1
- package/lib/documents/ast-document-manager.js +20 -50
- package/lib/documents/ast-document-manager.js.map +1 -1
- package/lib/documents/hydranium-text-documents.d.ts +87 -30
- package/lib/documents/hydranium-text-documents.d.ts.map +1 -1
- package/lib/documents/hydranium-text-documents.js +201 -37
- package/lib/documents/hydranium-text-documents.js.map +1 -1
- package/lib/documents/index.d.ts +2 -0
- package/lib/documents/index.d.ts.map +1 -1
- package/lib/documents/index.js +2 -0
- package/lib/documents/index.js.map +1 -1
- package/lib/documents/model-ledger.d.ts +47 -0
- package/lib/documents/model-ledger.d.ts.map +1 -0
- package/lib/documents/model-ledger.js +30 -0
- package/lib/documents/model-ledger.js.map +1 -0
- package/lib/documents/version-sync-service.d.ts +134 -0
- package/lib/documents/version-sync-service.d.ts.map +1 -0
- package/lib/documents/version-sync-service.js +171 -0
- package/lib/documents/version-sync-service.js.map +1 -0
- package/lib/langium/bootstrap.d.ts.map +1 -1
- package/lib/langium/bootstrap.js +16 -0
- package/lib/langium/bootstrap.js.map +1 -1
- package/lib/langium/document-builder/document-builder.d.ts +31 -25
- package/lib/langium/document-builder/document-builder.d.ts.map +1 -1
- package/lib/langium/document-builder/document-builder.js +43 -39
- 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 +6 -15
- package/lib/langium/integrity/integrity-service.js.map +1 -1
- package/lib/langium/model-service/client-session.d.ts +14 -10
- package/lib/langium/model-service/client-session.d.ts.map +1 -1
- package/lib/langium/model-service/client-session.js +18 -15
- package/lib/langium/model-service/client-session.js.map +1 -1
- package/lib/langium/model-service/model-service.d.ts +91 -41
- package/lib/langium/model-service/model-service.d.ts.map +1 -1
- package/lib/langium/model-service/model-service.js +127 -50
- package/lib/langium/model-service/model-service.js.map +1 -1
- package/lib/langium/module.d.ts +6 -0
- package/lib/langium/module.d.ts.map +1 -1
- package/lib/langium/module.js +5 -1
- package/lib/langium/module.js.map +1 -1
- package/lib/langium/residency/cst-residency-service.d.ts.map +1 -1
- package/lib/langium/residency/cst-residency-service.js +10 -1
- package/lib/langium/residency/cst-residency-service.js.map +1 -1
- package/lib/langium/shared-services.d.ts +6 -2
- package/lib/langium/shared-services.d.ts.map +1 -1
- package/lib/langium/shared-services.js.map +1 -1
- package/lib/langium/transfer/transfer-encoder.d.ts +19 -10
- package/lib/langium/transfer/transfer-encoder.d.ts.map +1 -1
- package/lib/langium/transfer/transfer-encoder.js +8 -7
- package/lib/langium/transfer/transfer-encoder.js.map +1 -1
- package/lib/langium/workspace/hydranium-langium-document-factory.d.ts +31 -1
- package/lib/langium/workspace/hydranium-langium-document-factory.d.ts.map +1 -1
- package/lib/langium/workspace/hydranium-langium-document-factory.js +54 -0
- package/lib/langium/workspace/hydranium-langium-document-factory.js.map +1 -1
- package/lib/langium/workspace/langium-documents.d.ts +7 -0
- package/lib/langium/workspace/langium-documents.d.ts.map +1 -1
- package/lib/langium/workspace/langium-documents.js +10 -0
- package/lib/langium/workspace/langium-documents.js.map +1 -1
- package/lib/lsp/hydranium-document-update-handler.d.ts.map +1 -1
- package/lib/lsp/hydranium-document-update-handler.js +1 -0
- package/lib/lsp/hydranium-document-update-handler.js.map +1 -1
- package/lib/testing/make-test-services.d.ts +7 -0
- package/lib/testing/make-test-services.d.ts.map +1 -1
- package/lib/testing/make-test-services.js +10 -2
- package/lib/testing/make-test-services.js.map +1 -1
- package/lib/testing/stub-ast-document-manager.d.ts +3 -2
- package/lib/testing/stub-ast-document-manager.d.ts.map +1 -1
- package/lib/testing/stub-ast-document-manager.js +7 -1
- package/lib/testing/stub-ast-document-manager.js.map +1 -1
- package/lib/testing/stub-hydranium-text-documents.d.ts +2 -2
- package/lib/testing/stub-hydranium-text-documents.d.ts.map +1 -1
- package/lib/testing/stub-hydranium-text-documents.js +6 -1
- package/lib/testing/stub-hydranium-text-documents.js.map +1 -1
- package/package.json +5 -5
- package/src/documents/ast-document-manager.ts +45 -77
- package/src/documents/hydranium-text-documents.ts +225 -46
- package/src/documents/index.ts +2 -0
- package/src/documents/model-ledger.ts +66 -0
- package/src/documents/version-sync-service.ts +267 -0
- package/src/langium/bootstrap.ts +16 -0
- package/src/langium/document-builder/document-builder.ts +76 -37
- package/src/langium/integrity/integrity-service.ts +6 -15
- package/src/langium/model-service/client-session.ts +29 -21
- package/src/langium/model-service/model-service.ts +182 -64
- package/src/langium/module.ts +9 -1
- package/src/langium/residency/cst-residency-service.ts +10 -1
- package/src/langium/shared-services.ts +6 -2
- package/src/langium/transfer/transfer-encoder.ts +25 -18
- package/src/langium/workspace/hydranium-langium-document-factory.ts +83 -0
- package/src/langium/workspace/langium-documents.ts +11 -0
- package/src/lsp/hydranium-document-update-handler.ts +3 -0
- package/src/testing/make-test-services.ts +15 -2
- package/src/testing/stub-ast-document-manager.ts +10 -2
- package/src/testing/stub-hydranium-text-documents.ts +8 -3
|
@@ -8,13 +8,14 @@
|
|
|
8
8
|
********************************************************************************/
|
|
9
9
|
|
|
10
10
|
import {
|
|
11
|
-
type
|
|
11
|
+
type BaseVersion,
|
|
12
12
|
ConflictError,
|
|
13
|
-
|
|
13
|
+
isModelVersion,
|
|
14
14
|
Logger,
|
|
15
15
|
type MaybeObservableValue,
|
|
16
16
|
type MaybePromise,
|
|
17
17
|
ObservableValue,
|
|
18
|
+
type TextVersion,
|
|
18
19
|
type Tracer,
|
|
19
20
|
type TransferElement
|
|
20
21
|
} from '@hydranium/protocol';
|
|
@@ -35,11 +36,11 @@ export interface ClientSessionWriteArgs<TTransfer> {
|
|
|
35
36
|
/** The whole structured model root, or its serialised textual form. */
|
|
36
37
|
model: TTransfer | string;
|
|
37
38
|
/**
|
|
38
|
-
* The version the write was authored against, or `'
|
|
39
|
+
* The version the write was authored against, or `'any'` to write
|
|
39
40
|
* unconditionally. Required, so an ungated write is a decision the caller
|
|
40
41
|
* makes rather than a field it forgot.
|
|
41
42
|
*/
|
|
42
|
-
|
|
43
|
+
baseVersion: BaseVersion;
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
/** What a session's `updateAll` writes, all or none. */
|
|
@@ -89,8 +90,12 @@ export interface ClientSession<
|
|
|
89
90
|
* disk on the first `save`. Fails when the file exists, any client has the
|
|
90
91
|
* URI open, or the URI waits out the revert grace, and of two creates of one
|
|
91
92
|
* URI at most one succeeds.
|
|
93
|
+
*
|
|
94
|
+
* Resolves with the version the created document took, which a write based
|
|
95
|
+
* on it names: once this resolves, the store's version may already be
|
|
96
|
+
* another client's write.
|
|
92
97
|
*/
|
|
93
|
-
create(uri: string, text: string): Promise<
|
|
98
|
+
create(uri: string, text: string): Promise<TextVersion>;
|
|
94
99
|
/**
|
|
95
100
|
* Write `args.model` into a document this session has open. Resolves to the
|
|
96
101
|
* rebuilt document once it is validated, in whichever build carried the
|
|
@@ -221,7 +226,7 @@ export class DefaultClientSession<
|
|
|
221
226
|
: undefined;
|
|
222
227
|
}
|
|
223
228
|
|
|
224
|
-
create(uri: string, text: string): Promise<
|
|
229
|
+
create(uri: string, text: string): Promise<TextVersion> {
|
|
225
230
|
this.assertLive();
|
|
226
231
|
return this.createDocument(uri, text);
|
|
227
232
|
}
|
|
@@ -279,19 +284,19 @@ export class DefaultClientSession<
|
|
|
279
284
|
}
|
|
280
285
|
|
|
281
286
|
/**
|
|
282
|
-
* Throw `ConflictError` when `
|
|
287
|
+
* Throw `ConflictError` when `baseVersion` names a version other than
|
|
283
288
|
* `currentVersion`. A write calls it at the door and again in the
|
|
284
289
|
* synchronous step that applies the text, and has to call it synchronously
|
|
285
290
|
* there: a check separated from the apply by an await lets two writes based
|
|
286
291
|
* on one version both apply.
|
|
287
292
|
*/
|
|
288
|
-
protected
|
|
289
|
-
if (
|
|
293
|
+
protected assertBaseVersion(uri: string, baseVersion: BaseVersion, currentVersion: number): void {
|
|
294
|
+
if (isModelVersion(baseVersion) && currentVersion !== baseVersion) {
|
|
290
295
|
// Distinct from the post-build "superseded" debug line of an update: this
|
|
291
|
-
// is a
|
|
296
|
+
// is a stale-base-version rejection (the write never applies), not two
|
|
292
297
|
// writes racing.
|
|
293
|
-
this.tracer.debug(`Conflict on ${uri}:
|
|
294
|
-
throw new ConflictError(uri,
|
|
298
|
+
this.tracer.debug(`Conflict on ${uri}: stale base, model v${baseVersion} / text v${currentVersion}`);
|
|
299
|
+
throw new ConflictError(uri, baseVersion, currentVersion);
|
|
295
300
|
}
|
|
296
301
|
}
|
|
297
302
|
|
|
@@ -299,7 +304,7 @@ export class DefaultClientSession<
|
|
|
299
304
|
* Serialise `args.model`, apply it to the store under this session's id,
|
|
300
305
|
* rebuild, and resolve to the rebuilt document.
|
|
301
306
|
*
|
|
302
|
-
* The open check and the `
|
|
307
|
+
* The open check and the `baseVersion` gate run at the door, so a write that
|
|
303
308
|
* cannot land is refused before any adopter serialiser runs, and again in
|
|
304
309
|
* the synchronous step that applies the text: made only before the
|
|
305
310
|
* serialiser's await, a close during it lets the write land on a document
|
|
@@ -321,11 +326,12 @@ export class DefaultClientSession<
|
|
|
321
326
|
const run = async <T>(stage: string, fn: () => MaybePromise<T>): Promise<T> => (profile ? profile.scope(stage, fn) : fn());
|
|
322
327
|
const textDocuments = this.services.workspace.TextDocuments;
|
|
323
328
|
this.assertOpen(uri);
|
|
324
|
-
this.
|
|
329
|
+
this.assertBaseVersion(uri, args.baseVersion, textDocuments.version(uri));
|
|
330
|
+
service.assertCanBuild(uri);
|
|
325
331
|
const text = await run('serialize', () => service.modelToText(uri, args.model, cancelToken));
|
|
326
332
|
const appliedVersion = await run('apply', () => {
|
|
327
333
|
this.assertOpen(uri);
|
|
328
|
-
this.
|
|
334
|
+
this.assertBaseVersion(uri, args.baseVersion, textDocuments.version(uri));
|
|
329
335
|
return this.services.workspace.AstDocumentManager.update(uri, text, this.clientId);
|
|
330
336
|
});
|
|
331
337
|
// Through the public `rebuild`, so an override of it stays in the path.
|
|
@@ -352,7 +358,7 @@ export class DefaultClientSession<
|
|
|
352
358
|
|
|
353
359
|
/**
|
|
354
360
|
* Write every document of `args.updates`, all or none: every model is
|
|
355
|
-
* serialised first, then every open check and `
|
|
361
|
+
* serialised first, then every open check and `baseVersion` gate runs and every
|
|
356
362
|
* text is applied in one synchronous step. Checked at the door as well, so
|
|
357
363
|
* a stale set is refused before any adopter serialiser runs.
|
|
358
364
|
*
|
|
@@ -374,7 +380,8 @@ export class DefaultClientSession<
|
|
|
374
380
|
const check = (): void =>
|
|
375
381
|
updates.forEach((update, i) => {
|
|
376
382
|
this.assertOpen(uris[i]);
|
|
377
|
-
this.
|
|
383
|
+
this.assertBaseVersion(uris[i], update.baseVersion, textDocuments.version(uris[i]));
|
|
384
|
+
service.assertCanBuild(uris[i]);
|
|
378
385
|
});
|
|
379
386
|
check();
|
|
380
387
|
const texts: string[] = [];
|
|
@@ -455,7 +462,7 @@ export class DefaultClientSession<
|
|
|
455
462
|
return this.services.workspace.DocumentUriPolicy.canonicalUri(uri);
|
|
456
463
|
}
|
|
457
464
|
|
|
458
|
-
protected async createDocument(uri: string, text: string): Promise<
|
|
465
|
+
protected async createDocument(uri: string, text: string): Promise<TextVersion> {
|
|
459
466
|
if (await this.services.workspace.FileSystemProvider.exists(UriUtils.toUri(uri))) {
|
|
460
467
|
throw new Error(`Cannot create ${uri}: the file exists`);
|
|
461
468
|
}
|
|
@@ -473,10 +480,10 @@ export class DefaultClientSession<
|
|
|
473
480
|
// document holds `text`; the client list afterwards cannot, since a
|
|
474
481
|
// client that registered first may have closed again by then.
|
|
475
482
|
const key = this.canonicalKey(uri);
|
|
476
|
-
let created
|
|
483
|
+
let created: TextVersion | undefined;
|
|
477
484
|
const listener = this.services.workspace.TextDocuments.onDidOpen(event => {
|
|
478
485
|
if (event.clientId === this.clientId && event.document.uri === key) {
|
|
479
|
-
created =
|
|
486
|
+
created = event.document.version;
|
|
480
487
|
}
|
|
481
488
|
});
|
|
482
489
|
try {
|
|
@@ -485,10 +492,11 @@ export class DefaultClientSession<
|
|
|
485
492
|
listener.dispose();
|
|
486
493
|
}
|
|
487
494
|
await this.rejectIfEnded(uri, false);
|
|
488
|
-
if (
|
|
495
|
+
if (created === undefined) {
|
|
489
496
|
await this.closeDocument(uri);
|
|
490
497
|
throw new Error(`Cannot create ${uri}: it is open in a client`);
|
|
491
498
|
}
|
|
499
|
+
return created;
|
|
492
500
|
}
|
|
493
501
|
|
|
494
502
|
protected async runWithOpen<T>(uri: string, fn: () => MaybePromise<T>): Promise<T> {
|
|
@@ -15,13 +15,15 @@ import {
|
|
|
15
15
|
ObservableValue,
|
|
16
16
|
TIMED_OUT,
|
|
17
17
|
type TransferElement,
|
|
18
|
-
type
|
|
18
|
+
type TextVersion,
|
|
19
|
+
type Tracer,
|
|
20
|
+
UNRECORDED_VERSION
|
|
19
21
|
} from '@hydranium/protocol';
|
|
20
|
-
import { type AstNode, DocumentState, type LangiumDocument, UriUtils, type URI } from '@hydranium/langium';
|
|
22
|
+
import { type AstNode, DocumentState, type LangiumDocument, OperationCancelled, UriUtils, type URI } from '@hydranium/langium';
|
|
21
23
|
import { type AstDiagnostic } from '../validation/document-validator.js';
|
|
22
24
|
import { type DocumentUriPolicy } from '../workspace/document-uri-policy.js';
|
|
23
25
|
import { ReentrantWriteLockError, isInsideWriteLock, isWriteLockScopeInstalled } from '../workspace/write-lock-scope.js';
|
|
24
|
-
import {
|
|
26
|
+
import { CancellationToken, type Disposable } from 'vscode-languageserver';
|
|
25
27
|
import { AstDocument, type AstDocumentSavedEvent, type AstDocumentUpdatedEvent } from '../../documents/ast-document-manager.js';
|
|
26
28
|
import { isConnectionGoneError } from '../../util/connection-liveness.js';
|
|
27
29
|
import { type LogNameOptions } from '../diagnostics/logger.js';
|
|
@@ -86,6 +88,17 @@ export interface ModelServiceOptions extends LogNameOptions {
|
|
|
86
88
|
readonly allowReentrantBuilds?: MaybeObservableValue<boolean>;
|
|
87
89
|
}
|
|
88
90
|
|
|
91
|
+
/** Options for a {@link DefaultModelService} wait on one document. */
|
|
92
|
+
export interface SyncedWaitOptions {
|
|
93
|
+
/**
|
|
94
|
+
* Build a root parsed from text older than the store's at the call, and
|
|
95
|
+
* reject once that build, or one the builder re-queues, is given up. Without
|
|
96
|
+
* it, such a root is left to other builds, and a wait they never end stays
|
|
97
|
+
* pending until its token is cancelled.
|
|
98
|
+
*/
|
|
99
|
+
readonly sync?: boolean;
|
|
100
|
+
}
|
|
101
|
+
|
|
89
102
|
/**
|
|
90
103
|
* The seam every non-LSP head talks to: the data server, the GLSP head and an
|
|
91
104
|
* adopter's own services reach documents through this slot rather than through
|
|
@@ -151,11 +164,22 @@ export interface ModelServiceOptions extends LogNameOptions {
|
|
|
151
164
|
* by URI happens here so consumers can subscribe per-document without
|
|
152
165
|
* implementing the URI gate at each callsite.
|
|
153
166
|
*
|
|
154
|
-
* **Two families
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
167
|
+
* **Two families.** `waitFor*` only waits: it rejects for a URI with no
|
|
168
|
+
* document, and for a root behind the store's text it waits for whatever
|
|
169
|
+
* builds it next, so it never ends if nothing does. `ensureDocumentState` and
|
|
170
|
+
* the phase shorthands over it build a missing document, and build a root
|
|
171
|
+
* behind its text, rejecting when that build fails twice. Both re-queue a
|
|
172
|
+
* document the builder's last build left short of the state.
|
|
173
|
+
*
|
|
174
|
+
* Both resolve at the state with a root parsed from text no older than the
|
|
175
|
+
* store's version at the call; inside the write lock, at the state alone, since
|
|
176
|
+
* the build that would sync the document to that version cannot start until
|
|
177
|
+
* it is released.
|
|
178
|
+
*
|
|
179
|
+
* Do not await one inside a build phase listener: a browser host installs no
|
|
180
|
+
* write-lock scope, so the wait cannot tell it is inside a build and waits for
|
|
181
|
+
* one that cannot start. Nor inside a `WorkspaceLock.read`: the lock starts no
|
|
182
|
+
* write, the build included, while a read runs.
|
|
159
183
|
*
|
|
160
184
|
* **Diagnostics are typed `never` below `Validated`.** Validation is the last
|
|
161
185
|
* phase, so at any earlier landmark the array either is not yet computed or
|
|
@@ -171,8 +195,8 @@ export interface ModelServiceOptions extends LogNameOptions {
|
|
|
171
195
|
* The waits resolve at or ABOVE their target, so an already-validated document
|
|
172
196
|
* does carry usable diagnostics and the `never` over-forbids there. That
|
|
173
197
|
* direction is the safe one: the alternative permits stale reads silently. A
|
|
174
|
-
* member taking a phase as a PARAMETER
|
|
175
|
-
*
|
|
198
|
+
* member taking a phase as a PARAMETER returns `TDiagnostic`, and leaves them
|
|
199
|
+
* absent for a phase below `Validated`.
|
|
176
200
|
*
|
|
177
201
|
* **Generic parameters.**
|
|
178
202
|
* - `TAst` — the AST root type each consumer expects on the returned
|
|
@@ -203,14 +227,20 @@ export interface ModelService<
|
|
|
203
227
|
*/
|
|
204
228
|
readonly ready: Promise<void>;
|
|
205
229
|
|
|
206
|
-
//
|
|
230
|
+
// Wait only; a root behind its text is left to another build.
|
|
207
231
|
waitForDocumentState(uri: string, state: DocumentState, cancelToken?: CancellationToken): Promise<AstDocument<TAst, TDiagnostic>>;
|
|
208
232
|
waitForDocumentSettled(uri: string, cancelToken?: CancellationToken): Promise<AstDocument<TAst, never>>;
|
|
209
233
|
waitForBuilderState(state: DocumentState, cancelToken?: CancellationToken): Promise<void>;
|
|
210
234
|
|
|
211
|
-
//
|
|
235
|
+
// Build what is missing or behind its text, then wait.
|
|
212
236
|
ensureDocumentState(uri: string, state?: DocumentState, cancelToken?: CancellationToken): Promise<AstDocument<TAst, TDiagnostic>>;
|
|
213
237
|
rebuild(uri: string, state?: DocumentState, cancelToken?: CancellationToken): Promise<AstDocument<TAst, TDiagnostic>>;
|
|
238
|
+
/**
|
|
239
|
+
* Throw where {@link rebuild} would reject as reentrant. A write calls it
|
|
240
|
+
* before applying its text: a text applied and then refused stays applied
|
|
241
|
+
* with no build to follow it.
|
|
242
|
+
*/
|
|
243
|
+
assertCanBuild(uri: string): void;
|
|
214
244
|
parsed(uri: string, cancelToken?: CancellationToken): Promise<AstDocument<TAst, never>>;
|
|
215
245
|
linked(uri: string, cancelToken?: CancellationToken): Promise<AstDocument<TAst, never>>;
|
|
216
246
|
settled(uri: string, cancelToken?: CancellationToken): Promise<AstDocument<TAst, never>>;
|
|
@@ -379,9 +409,12 @@ export class DefaultModelService<
|
|
|
379
409
|
// ============================================================
|
|
380
410
|
|
|
381
411
|
/**
|
|
382
|
-
* Wait for the document at `uri` to reach `state
|
|
383
|
-
*
|
|
384
|
-
*
|
|
412
|
+
* Wait for the document at `uri` to reach `state` with a root no older than
|
|
413
|
+
* the store's text at the call. Rejects if `uri` is not in the document
|
|
414
|
+
* registry. A root behind its text is not built: the wait lasts until
|
|
415
|
+
* something else builds it, so use {@link ensureDocumentState} where
|
|
416
|
+
* nothing may. Builds that keep failing leave the wait pending, to its
|
|
417
|
+
* cancellation token.
|
|
385
418
|
*
|
|
386
419
|
* Wrapped in a debug-level timing log via {@link Tracer.time} so
|
|
387
420
|
* slow per-URI waits surface in build telemetry; the URI is
|
|
@@ -395,15 +428,12 @@ export class DefaultModelService<
|
|
|
395
428
|
* Wait for the document at `uri` to reach the integrity-settled landmark
|
|
396
429
|
* ({@link IntegrityService.SettledState}) — the earliest phase at which the
|
|
397
430
|
* AST + serialised text are stable post-integrity. Convenience wrapper over
|
|
398
|
-
* {@link waitForDocumentState}
|
|
399
|
-
* case (e.g. settling a save). Pure wait — does not trigger a build.
|
|
431
|
+
* {@link waitForDocumentState}, and like it builds nothing behind its text.
|
|
400
432
|
*/
|
|
401
433
|
async waitForDocumentSettled(uri: string, cancelToken?: CancellationToken): Promise<AstDocument<TAst, never>> {
|
|
402
|
-
return
|
|
403
|
-
this.uriPolicy.canonicalUri(uri),
|
|
404
|
-
|
|
405
|
-
cancelToken
|
|
406
|
-
)) as AstDocument<TAst, never>;
|
|
434
|
+
return this.withoutDiagnostics(
|
|
435
|
+
await this.waitForDocumentStateCanonical(this.uriPolicy.canonicalUri(uri), IntegrityService.SettledState, cancelToken)
|
|
436
|
+
);
|
|
407
437
|
}
|
|
408
438
|
|
|
409
439
|
/**
|
|
@@ -415,21 +445,108 @@ export class DefaultModelService<
|
|
|
415
445
|
* the identity once instead of re-running the (filesystem-touching) `realpath`
|
|
416
446
|
* at every wait. The `CanonicalUri` parameter type enforces that — a raw
|
|
417
447
|
* `string` cannot be passed without minting through the URI policy.
|
|
448
|
+
*
|
|
449
|
+
* With `options.sync`, a root behind the store's text is built, as in
|
|
450
|
+
* {@link ensureDocumentState}; without it, the root is left to other builds,
|
|
451
|
+
* as in {@link waitForDocumentState}. Below `Validated` the envelope has no
|
|
452
|
+
* diagnostics: see {@link withoutDiagnostics}.
|
|
418
453
|
*/
|
|
419
454
|
protected async waitForDocumentStateCanonical(
|
|
420
455
|
uri: CanonicalUri,
|
|
421
456
|
state: DocumentState,
|
|
422
|
-
cancelToken?: CancellationToken
|
|
457
|
+
cancelToken?: CancellationToken,
|
|
458
|
+
options: SyncedWaitOptions = {}
|
|
423
459
|
): Promise<AstDocument<TAst, TDiagnostic>> {
|
|
424
460
|
const documentUri = UriUtils.toUri(uri);
|
|
461
|
+
// Read at call time: a later edit must not extend this wait.
|
|
462
|
+
const textVersion = isInsideWriteLock() ? undefined : this.services.workspace.TextDocuments.textState(uri)?.version;
|
|
425
463
|
await this.tracer
|
|
426
464
|
.withUri(uri)
|
|
427
465
|
.time(
|
|
428
466
|
`Wait for document state '${DocumentState[state]}'`,
|
|
429
|
-
() => this.
|
|
467
|
+
() => this.waitUntilSynced(documentUri, state, textVersion, cancelToken, options),
|
|
430
468
|
'debug'
|
|
431
469
|
);
|
|
432
|
-
|
|
470
|
+
const document = this.toAstDocument(documentUri);
|
|
471
|
+
return state >= DocumentState.Validated ? document : this.withoutDiagnostics(document);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Wait for `state` with a root parsed from text at `textVersion` or later. A
|
|
476
|
+
* root no factory recorded counts as synced, since no build records it.
|
|
477
|
+
* With `options.sync`, a root still behind is built through
|
|
478
|
+
* `VersionSyncService.syncTo`, and the wait rejects once that build, or one
|
|
479
|
+
* the builder re-queues to reach `state`, is given up. Without it, the wait
|
|
480
|
+
* lasts until another build parses the text, and stays pending when the
|
|
481
|
+
* builder gives up re-queuing the document.
|
|
482
|
+
*/
|
|
483
|
+
protected async waitUntilSynced(
|
|
484
|
+
uri: URI,
|
|
485
|
+
state: DocumentState,
|
|
486
|
+
textVersion: TextVersion | undefined,
|
|
487
|
+
cancelToken?: CancellationToken,
|
|
488
|
+
options: SyncedWaitOptions = {}
|
|
489
|
+
): Promise<void> {
|
|
490
|
+
const builder = this.services.workspace.DocumentBuilder;
|
|
491
|
+
const sync = this.services.workspace.VersionSyncService;
|
|
492
|
+
const waitOptions = { rejectWhenStuck: options.sync === true };
|
|
493
|
+
await builder.waitUntil(state, uri, cancelToken, waitOptions);
|
|
494
|
+
if (textVersion === undefined) {
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
while (!sync.isSyncedTo(uri, textVersion)) {
|
|
498
|
+
const build = options.sync === true ? sync.syncTo(uri, textVersion) : undefined;
|
|
499
|
+
this.tracer
|
|
500
|
+
.withUri(uri.toString())
|
|
501
|
+
.debug(`waiting for ${uri.toString()} to reach ${DocumentState[state]} at v${textVersion} or later`);
|
|
502
|
+
await this.nextParseOrDeletion(uri, cancelToken, build);
|
|
503
|
+
await builder.waitUntil(state, uri, cancelToken, waitOptions);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Resolves once a parse of `uri` has been recorded or a build has deleted
|
|
509
|
+
* it, rejecting with `OperationCancelled` when `cancelToken` is cancelled
|
|
510
|
+
* first, and with an error when `build`, the build requested for it, is
|
|
511
|
+
* given up. A `Parsed` phase listener cannot serve: a cancel right after the
|
|
512
|
+
* parse skips it, and the resumed build does not parse again.
|
|
513
|
+
*/
|
|
514
|
+
protected nextParseOrDeletion(
|
|
515
|
+
uri: URI,
|
|
516
|
+
cancelToken: CancellationToken = CancellationToken.None,
|
|
517
|
+
build?: Promise<boolean>
|
|
518
|
+
): Promise<void> {
|
|
519
|
+
const builder = this.services.workspace.DocumentBuilder;
|
|
520
|
+
return new Promise<void>((resolve, reject) => {
|
|
521
|
+
// A cancelled token's listener runs a macrotask later, after a build
|
|
522
|
+
// that started meanwhile may have parsed.
|
|
523
|
+
if (cancelToken.isCancellationRequested) {
|
|
524
|
+
reject(OperationCancelled);
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
const done = (settle: () => void): void => {
|
|
528
|
+
parsed.dispose();
|
|
529
|
+
deleted.dispose();
|
|
530
|
+
cancelled.dispose();
|
|
531
|
+
settle();
|
|
532
|
+
};
|
|
533
|
+
const parsed = this.services.workspace.VersionSyncService.onDidRecordModel(document => {
|
|
534
|
+
if (UriUtils.equals(document.uri, uri)) {
|
|
535
|
+
done(resolve);
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
const deleted = builder.onUpdate((_changed, deletedUris) => {
|
|
539
|
+
if (deletedUris.some(deletedUri => UriUtils.equals(deletedUri, uri))) {
|
|
540
|
+
done(resolve);
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
const cancelled = cancelToken.onCancellationRequested(() => done(() => reject(OperationCancelled)));
|
|
544
|
+
void build?.then(built => {
|
|
545
|
+
if (!built) {
|
|
546
|
+
done(() => reject(new Error(`The build that would sync ${uri.toString()} to its text failed`)));
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
});
|
|
433
550
|
}
|
|
434
551
|
|
|
435
552
|
/**
|
|
@@ -523,28 +640,28 @@ export class DefaultModelService<
|
|
|
523
640
|
// A caller already inside the lock would cancel its own build and stall in
|
|
524
641
|
// the wait below; `allowReentrantBuilds` builds it unlocked, and without a
|
|
525
642
|
// tracker it cannot be told apart from any other caller.
|
|
526
|
-
|
|
527
|
-
if (this.allowReentrantBuilds.value && (inside || !isWriteLockScopeInstalled())) {
|
|
643
|
+
if (this.allowReentrantBuilds.value && (isInsideWriteLock() || !isWriteLockScopeInstalled())) {
|
|
528
644
|
await this.services.workspace.DocumentBuilder.update([documentUri], [], cancelToken);
|
|
529
645
|
} else {
|
|
530
|
-
|
|
531
|
-
throw new ReentrantWriteLockError(uri);
|
|
532
|
-
}
|
|
646
|
+
this.assertCanBuild(uri);
|
|
533
647
|
// The build runs on the lock's own token, which a later write cancels;
|
|
534
648
|
// the caller's token governs only the phase wait below.
|
|
535
649
|
await this.services.workspace.DocumentBuilder.scheduleUpdate([documentUri], []);
|
|
536
650
|
}
|
|
537
|
-
return this.waitForDocumentStateCanonical(uri, state ?? IntegrityService.SettledState, cancelToken);
|
|
651
|
+
return this.waitForDocumentStateCanonical(uri, state ?? IntegrityService.SettledState, cancelToken, { sync: true });
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
assertCanBuild(uri: string): void {
|
|
655
|
+
if (isInsideWriteLock() && !this.allowReentrantBuilds.value) {
|
|
656
|
+
throw new ReentrantWriteLockError(uri);
|
|
657
|
+
}
|
|
538
658
|
}
|
|
539
659
|
|
|
540
660
|
/**
|
|
541
661
|
* Per-state typed convenience methods. Each ensures the document at
|
|
542
662
|
* `uri` reaches the named phase and returns the AST envelope with
|
|
543
|
-
* the narrowest accurate diagnostics type for that phase
|
|
544
|
-
*
|
|
545
|
-
* registry) just waits via {@link waitForDocumentState}; cold path
|
|
546
|
-
* triggers a build via {@link rebuild}. Consumers do not need to
|
|
547
|
-
* know which path was taken.
|
|
663
|
+
* the narrowest accurate diagnostics type for that phase, building as
|
|
664
|
+
* {@link ensureDocumentState} does.
|
|
548
665
|
*
|
|
549
666
|
* Phase invariants encoded in the return type:
|
|
550
667
|
* - `parsed` / `linked` / `settled` / `indexed` return
|
|
@@ -590,18 +707,13 @@ export class DefaultModelService<
|
|
|
590
707
|
/**
|
|
591
708
|
* Ensure the document at `uri` reaches `state` (or the integrity-settled
|
|
592
709
|
* landmark {@link IntegrityService.SettledState} if omitted) and return its
|
|
593
|
-
* AST envelope.
|
|
594
|
-
*
|
|
595
|
-
*
|
|
596
|
-
*
|
|
597
|
-
*
|
|
598
|
-
*
|
|
599
|
-
*
|
|
600
|
-
* document. Also pairs with {@link waitForDocumentState} — the verb
|
|
601
|
-
* difference (`ensure` vs `waitFor`) signals the side-effect
|
|
602
|
-
* difference. The per-state convenience methods (`parsed` / `linked`
|
|
603
|
-
* / `settled` / `indexed` / `validated`) all delegate here with an
|
|
604
|
-
* explicit phase.
|
|
710
|
+
* AST envelope, with a root no older than the store's text at the call.
|
|
711
|
+
* A document not in `LangiumDocuments` is built via {@link rebuild}; a
|
|
712
|
+
* loaded one whose root is behind its text is built through
|
|
713
|
+
* `VersionSyncService.syncTo`. Until `state` is reached, the call rejects
|
|
714
|
+
* once that build, or one the builder re-queues, has failed twice, or the
|
|
715
|
+
* builder stops re-queuing the document. A loaded, current document is only
|
|
716
|
+
* awaited.
|
|
605
717
|
*/
|
|
606
718
|
async ensureDocumentState(uri: string, state?: DocumentState, cancelToken?: CancellationToken): Promise<AstDocument<TAst, TDiagnostic>> {
|
|
607
719
|
return this.ensureDocumentStateCanonical(this.uriPolicy.canonicalUri(uri), state, cancelToken);
|
|
@@ -624,13 +736,14 @@ export class DefaultModelService<
|
|
|
624
736
|
): Promise<AstDocument<TAst, TDiagnostic>> {
|
|
625
737
|
const target = state ?? IntegrityService.SettledState;
|
|
626
738
|
if (this.services.workspace.LangiumDocuments.hasDocument(UriUtils.toUri(uri))) {
|
|
627
|
-
return this.waitForDocumentStateCanonical(uri, target, cancelToken);
|
|
739
|
+
return this.waitForDocumentStateCanonical(uri, target, cancelToken, { sync: true });
|
|
628
740
|
}
|
|
629
741
|
return this.rebuild(uri, target, cancelToken);
|
|
630
742
|
}
|
|
631
743
|
|
|
632
744
|
/**
|
|
633
|
-
* Wait for the document at `uri` to reach the integrity-settled landmark
|
|
745
|
+
* Wait for the document at `uri` to reach the integrity-settled landmark,
|
|
746
|
+
* building a root behind its text as {@link ensureDocumentState} does, and
|
|
634
747
|
* drain any in-flight write-path applyEdit sync chain (see {@link syncChains})
|
|
635
748
|
* so every language client reflects the latest content before a save returns.
|
|
636
749
|
* No-op tail for headless adopters — `syncChains` is empty without an LSP
|
|
@@ -648,7 +761,7 @@ export class DefaultModelService<
|
|
|
648
761
|
// One race over both waits, so the bound is one deadline for the pair;
|
|
649
762
|
// a race per wait would let the save take twice the bound.
|
|
650
763
|
const settled = (async (): Promise<void> => {
|
|
651
|
-
await this.waitForDocumentStateCanonical(canonical, IntegrityService.SettledState, cancelToken);
|
|
764
|
+
await this.waitForDocumentStateCanonical(canonical, IntegrityService.SettledState, cancelToken, { sync: true });
|
|
652
765
|
await this.syncChains.get(key);
|
|
653
766
|
})();
|
|
654
767
|
try {
|
|
@@ -673,7 +786,9 @@ export class DefaultModelService<
|
|
|
673
786
|
|
|
674
787
|
/**
|
|
675
788
|
* Snapshot of `uri` as it stands RIGHT NOW — the synchronous sibling of the
|
|
676
|
-
* phase reads, which all wait. `undefined` when no document is registered
|
|
789
|
+
* phase reads, which all wait. `undefined` when no document is registered,
|
|
790
|
+
* or only the builder's placeholder for one its build has not parsed yet:
|
|
791
|
+
* that root is no parse of any text, and its version gates no write.
|
|
677
792
|
*
|
|
678
793
|
* **This is what a writer wants, and {@link getDocument} is not.** The
|
|
679
794
|
* envelope's `version` is copied by value at projection time, so it cannot
|
|
@@ -681,18 +796,18 @@ export class DefaultModelService<
|
|
|
681
796
|
* whatever the server is at *now*, which is the number an optimistic gate is
|
|
682
797
|
* about to compare it against.
|
|
683
798
|
*
|
|
684
|
-
* Diagnostics only from a document that has reached `Validated`, and
|
|
685
|
-
*
|
|
799
|
+
* Diagnostics only from a document that has reached `Validated`, and absent
|
|
800
|
+
* otherwise. Unlike the phase reads this one names no phase, so
|
|
686
801
|
* the state it finds is the only thing that can say whether the array
|
|
687
802
|
* describes the content being handed back or whatever an earlier build left.
|
|
688
803
|
* A caller that needs them unconditionally waits, via {@link validated}.
|
|
689
804
|
*/
|
|
690
805
|
snapshot(uri: string): AstDocument<TAst, TDiagnostic> | undefined {
|
|
691
806
|
const document = this.getDocument(uri);
|
|
692
|
-
if (!document) {
|
|
807
|
+
if (!document || this.services.workspace.ModelLedger.isPlaceholder(document.parseResult.value)) {
|
|
693
808
|
return undefined;
|
|
694
809
|
}
|
|
695
|
-
const envelope = AstDocument
|
|
810
|
+
const envelope = this.services.workspace.AstDocumentManager.toAstDocument(document) as AstDocument<TAst, TDiagnostic>;
|
|
696
811
|
return document.state >= DocumentState.Validated ? envelope : this.withoutDiagnostics(envelope);
|
|
697
812
|
}
|
|
698
813
|
|
|
@@ -705,7 +820,7 @@ export class DefaultModelService<
|
|
|
705
820
|
* still resolves to the one document the build keys by its real path. Returns
|
|
706
821
|
* `undefined` if no document is registered for `uri`.
|
|
707
822
|
*
|
|
708
|
-
* **Live, so do not take a
|
|
823
|
+
* **Live, so do not take a base version off it.** `textDocument` is the
|
|
709
824
|
* store's own object rather than a copy, so `.version` read here answers for
|
|
710
825
|
* the moment of the READ, not the moment of the earlier content — pass it to
|
|
711
826
|
* a write and the server compares its current version against itself, the
|
|
@@ -1037,21 +1152,23 @@ export class DefaultModelService<
|
|
|
1037
1152
|
|
|
1038
1153
|
/**
|
|
1039
1154
|
* Build an {@link AstDocument} envelope from the current
|
|
1040
|
-
* {@link LangiumDocument} state via
|
|
1041
|
-
* projection. Returns an empty envelope (built via
|
|
1155
|
+
* {@link LangiumDocument} state via `AstDocumentManager.toAstDocument`,
|
|
1156
|
+
* the projection events use too. Returns an empty envelope (built via
|
|
1042
1157
|
* {@link AstDocument.create}) when the document is absent from the
|
|
1043
|
-
* registry — adopters that prefer to throw override on their subclass.
|
|
1158
|
+
* registry — adopters that prefer to throw override on their subclass. Its
|
|
1159
|
+
* version is {@link UNRECORDED_VERSION}, so a write based on it conflicts.
|
|
1044
1160
|
*/
|
|
1045
1161
|
protected toAstDocument(uri: URI): AstDocument<TAst, TDiagnostic> {
|
|
1046
1162
|
const document = this.services.workspace.LangiumDocuments.getDocument(uri);
|
|
1047
1163
|
return document
|
|
1048
|
-
? AstDocument
|
|
1049
|
-
: AstDocument.create<TAst, TDiagnostic>(uri.toString(),
|
|
1164
|
+
? (this.services.workspace.AstDocumentManager.toAstDocument(document) as AstDocument<TAst, TDiagnostic>)
|
|
1165
|
+
: AstDocument.create<TAst, TDiagnostic>(uri.toString(), UNRECORDED_VERSION, undefined as unknown as TAst);
|
|
1050
1166
|
}
|
|
1051
1167
|
|
|
1052
1168
|
/**
|
|
1053
1169
|
* The same envelope with no diagnostics, for a read that names a phase below
|
|
1054
|
-
* `Validated`.
|
|
1170
|
+
* `Validated`. Absent rather than `[]`, which says the document was validated
|
|
1171
|
+
* and found clean.
|
|
1055
1172
|
*
|
|
1056
1173
|
* Langium fills `LangiumDocument.diagnostics` from inside `validateDocument`
|
|
1057
1174
|
* and from nowhere else, so below that phase the array holds whatever an
|
|
@@ -1065,6 +1182,7 @@ export class DefaultModelService<
|
|
|
1065
1182
|
* one it also keeps.
|
|
1066
1183
|
*/
|
|
1067
1184
|
protected withoutDiagnostics(document: AstDocument<TAst, TDiagnostic>): AstDocument<TAst, never> {
|
|
1068
|
-
|
|
1185
|
+
const { diagnostics: _diagnostics, ...rest } = document;
|
|
1186
|
+
return rest;
|
|
1069
1187
|
}
|
|
1070
1188
|
}
|
package/src/langium/module.ts
CHANGED
|
@@ -37,6 +37,8 @@ import { HydraniumTextDocuments } from '../documents/hydranium-text-documents.js
|
|
|
37
37
|
import { DefaultAstDocumentManager, type AstDocumentManager, type WritableFileSystemProvider } from '../documents/ast-document-manager.js';
|
|
38
38
|
import { DefaultSelfSaveRegistry, type SelfSaveRegistry } from '../documents/self-save-registry.js';
|
|
39
39
|
import { DefaultFileSystemTaskQueue, type FileSystemTaskQueue } from '../documents/file-system-task-queue.js';
|
|
40
|
+
import { DefaultModelLedger, type ModelLedger } from '../documents/model-ledger.js';
|
|
41
|
+
import { DefaultVersionSyncService, type VersionSyncService } from '../documents/version-sync-service.js';
|
|
40
42
|
import {
|
|
41
43
|
DefaultEmptyFileSystemProvider,
|
|
42
44
|
DefaultFileSystemProviderRegistry,
|
|
@@ -279,6 +281,10 @@ export interface ServerAddedSharedServices<
|
|
|
279
281
|
* strategy (`shed-closed-when-idle`) to enable shedding.
|
|
280
282
|
*/
|
|
281
283
|
CstResidencyService: CstResidencyService;
|
|
284
|
+
/** The version of the text each root was parsed from. */
|
|
285
|
+
ModelLedger: ModelLedger;
|
|
286
|
+
/** Reconciles each produced root with the store, and owns every build that syncs a root to its text or recovers one. */
|
|
287
|
+
VersionSyncService: VersionSyncService;
|
|
282
288
|
};
|
|
283
289
|
/**
|
|
284
290
|
* In-process workspace facade ({@link ModelService}) + AST→transfer
|
|
@@ -553,7 +559,9 @@ export function createServerSharedModule(
|
|
|
553
559
|
BuildPhasePassService: services => new DefaultBuildPhasePassService(services),
|
|
554
560
|
// Eagerly constructed, so its `Validated` pass registers before the
|
|
555
561
|
// first build.
|
|
556
|
-
CstResidencyService: services => new DefaultCstResidencyService(services)
|
|
562
|
+
CstResidencyService: services => new DefaultCstResidencyService(services),
|
|
563
|
+
ModelLedger: () => new DefaultModelLedger(),
|
|
564
|
+
VersionSyncService: services => new DefaultVersionSyncService(services)
|
|
557
565
|
},
|
|
558
566
|
model: {
|
|
559
567
|
// Generic walker — adopters with a typed `$type → wire shape` overlay
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* SPDX-License-Identifier: MIT
|
|
8
8
|
********************************************************************************/
|
|
9
9
|
|
|
10
|
-
import { asMutable, type Clock, Debouncer, Format, type Tracer } from '@hydranium/protocol';
|
|
10
|
+
import { asMutable, type Clock, Debouncer, Format, type Tracer, UNRECORDED_VERSION } from '@hydranium/protocol';
|
|
11
11
|
import {
|
|
12
12
|
type AstNode,
|
|
13
13
|
AstUtils,
|
|
@@ -405,6 +405,15 @@ export class DefaultCstResidencyService implements CstResidencyService {
|
|
|
405
405
|
if (root === undefined || root.$cstNode === undefined) {
|
|
406
406
|
return 0;
|
|
407
407
|
}
|
|
408
|
+
// Readers of the parsed text, the `'text-diagnostics'` fingerprint among
|
|
409
|
+
// them, otherwise see none once the CST is gone. Recorded in the ledger
|
|
410
|
+
// directly: no root is produced, and reconciling it with the store would
|
|
411
|
+
// step a closed document's sequence for a root behind its text.
|
|
412
|
+
const ledger = this.services.workspace.ModelLedger;
|
|
413
|
+
const version = ledger.versionOf(root);
|
|
414
|
+
if (version !== UNRECORDED_VERSION) {
|
|
415
|
+
ledger.record(root, version, ledger.textOf(root));
|
|
416
|
+
}
|
|
408
417
|
let cleared = 0;
|
|
409
418
|
for (const node of AstUtils.streamAst(root)) {
|
|
410
419
|
if (node.$cstNode) {
|