@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.
Files changed (36) hide show
  1. package/lib/documents/ast-document-manager.d.ts +96 -9
  2. package/lib/documents/ast-document-manager.d.ts.map +1 -1
  3. package/lib/documents/ast-document-manager.js +129 -20
  4. package/lib/documents/ast-document-manager.js.map +1 -1
  5. package/lib/langium/document-builder/document-builder.d.ts +65 -12
  6. package/lib/langium/document-builder/document-builder.d.ts.map +1 -1
  7. package/lib/langium/document-builder/document-builder.js +106 -24
  8. package/lib/langium/document-builder/document-builder.js.map +1 -1
  9. package/lib/langium/model-service/client-session.d.ts +9 -1
  10. package/lib/langium/model-service/client-session.d.ts.map +1 -1
  11. package/lib/langium/model-service/client-session.js +19 -3
  12. package/lib/langium/model-service/client-session.js.map +1 -1
  13. package/lib/langium/model-service/model-service.d.ts +16 -15
  14. package/lib/langium/model-service/model-service.d.ts.map +1 -1
  15. package/lib/langium/model-service/model-service.js +4 -5
  16. package/lib/langium/model-service/model-service.js.map +1 -1
  17. package/lib/langium/workspace/write-lock-scope.d.ts +3 -2
  18. package/lib/langium/workspace/write-lock-scope.d.ts.map +1 -1
  19. package/lib/langium/workspace/write-lock-scope.js +3 -2
  20. package/lib/langium/workspace/write-lock-scope.js.map +1 -1
  21. package/lib/testing/stub-ast-document-manager.d.ts +1 -1
  22. package/lib/testing/stub-ast-document-manager.d.ts.map +1 -1
  23. package/lib/testing/stub-ast-document-manager.js +9 -0
  24. package/lib/testing/stub-ast-document-manager.js.map +1 -1
  25. package/lib/testing/stub-document-builder.d.ts +7 -6
  26. package/lib/testing/stub-document-builder.d.ts.map +1 -1
  27. package/lib/testing/stub-document-builder.js +26 -0
  28. package/lib/testing/stub-document-builder.js.map +1 -1
  29. package/package.json +5 -5
  30. package/src/documents/ast-document-manager.ts +158 -25
  31. package/src/langium/document-builder/document-builder.ts +115 -23
  32. package/src/langium/model-service/client-session.ts +29 -4
  33. package/src/langium/model-service/model-service.ts +18 -17
  34. package/src/langium/workspace/write-lock-scope.ts +3 -2
  35. package/src/testing/stub-ast-document-manager.ts +21 -1
  36. 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 — nothing acquires the
91
- * lock then, so there is nothing to be reentrant about; prefer it over
92
- * unserialised builds only in that case.
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 landmark
138
- * ({@link IntegrityService.SettledState}), then read the post-build state. Concurrent
139
- * in-process callers on the same URI all see the latest post-build
140
- * snapshot — none deadlock waiting for a specific version's settled
141
- * event. Adopters wanting strict version-matched semantics (resolve
142
- * with vN's snapshot specifically, log "vN superseded by vM at vN+1")
143
- * override `DefaultClientSession.updateDocument` to attach a phase-listener
144
- * with explicit version checks; the framework default doesn't need it for
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 there is nothing to be reentrant
550
- // about, which makes the existing opt-out the guard's opt-out too.
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. `sourceClientId` is resolved from the multi-client
980
- * author history; reason discrimination follows the manager's own
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` to accept
76
- * unserialised builds in exchange for the reentrancy.
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
- 'open' | 'close' | 'isOpen' | 'update' | 'save' | 'onUpdate' | 'onSave' | 'onClientClosed' | 'getAuthor' | 'getDocument'
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 from on the framework's test paths (`update` / `waitUntil` /
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 extends Pick<
75
- DocumentBuilder,
76
- 'update' | 'onDocumentPhase' | 'onUpdate' | 'build' | 'onBuildPhase' | 'resetToState'
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`. `cancelToken` defaults to a non-cancelled token; pass a
92
- * cancelled token to simulate a build preempted by a concurrent write
93
- * lock.
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;