@hydranium/core 1.0.0-next.36 → 1.0.0-next.38

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 (95) hide show
  1. package/lib/documents/ast-document-manager.d.ts +42 -5
  2. package/lib/documents/ast-document-manager.d.ts.map +1 -1
  3. package/lib/documents/ast-document-manager.js +3 -7
  4. package/lib/documents/ast-document-manager.js.map +1 -1
  5. package/lib/documents/hydranium-text-documents.d.ts.map +1 -1
  6. package/lib/documents/hydranium-text-documents.js +29 -8
  7. package/lib/documents/hydranium-text-documents.js.map +1 -1
  8. package/lib/documents/language-client-text-shadow.d.ts +24 -1
  9. package/lib/documents/language-client-text-shadow.d.ts.map +1 -1
  10. package/lib/documents/language-client-text-shadow.js +36 -2
  11. package/lib/documents/language-client-text-shadow.js.map +1 -1
  12. package/lib/documents/self-save-registry.d.ts +37 -6
  13. package/lib/documents/self-save-registry.d.ts.map +1 -1
  14. package/lib/documents/self-save-registry.js +28 -15
  15. package/lib/documents/self-save-registry.js.map +1 -1
  16. package/lib/index.d.ts +1 -0
  17. package/lib/index.d.ts.map +1 -1
  18. package/lib/index.js +4 -0
  19. package/lib/index.js.map +1 -1
  20. package/lib/langium/bootstrap.js +8 -8
  21. package/lib/langium/bootstrap.js.map +1 -1
  22. package/lib/langium/document-builder/build-pipeline-integration.d.ts +13 -1
  23. package/lib/langium/document-builder/build-pipeline-integration.d.ts.map +1 -1
  24. package/lib/langium/document-builder/build-pipeline-integration.js +1 -1
  25. package/lib/langium/document-builder/build-pipeline-integration.js.map +1 -1
  26. package/lib/langium/document-builder/document-builder.d.ts +2 -2
  27. package/lib/langium/document-builder/document-builder.d.ts.map +1 -1
  28. package/lib/langium/document-builder/document-builder.js.map +1 -1
  29. package/lib/langium/integration-services.d.ts +1 -1
  30. package/lib/langium/model-service/model-service.d.ts +64 -5
  31. package/lib/langium/model-service/model-service.d.ts.map +1 -1
  32. package/lib/langium/model-service/model-service.js +5 -73
  33. package/lib/langium/model-service/model-service.js.map +1 -1
  34. package/lib/langium/module.d.ts +58 -16
  35. package/lib/langium/module.d.ts.map +1 -1
  36. package/lib/langium/module.js +16 -16
  37. package/lib/langium/module.js.map +1 -1
  38. package/lib/langium/residency/cst-residency-service.d.ts +30 -8
  39. package/lib/langium/residency/cst-residency-service.d.ts.map +1 -1
  40. package/lib/langium/residency/cst-residency-service.js +13 -56
  41. package/lib/langium/residency/cst-residency-service.js.map +1 -1
  42. package/lib/langium/shared-services.d.ts +3 -3
  43. package/lib/langium/shared-services.d.ts.map +1 -1
  44. package/lib/langium/transfer/transfer-encoder.d.ts +29 -1
  45. package/lib/langium/transfer/transfer-encoder.d.ts.map +1 -1
  46. package/lib/langium/transfer/transfer-encoder.js +1 -1
  47. package/lib/langium/transfer/transfer-encoder.js.map +1 -1
  48. package/lib/locale/server-locale.d.ts +15 -1
  49. package/lib/locale/server-locale.d.ts.map +1 -1
  50. package/lib/locale/server-locale.js +1 -1
  51. package/lib/locale/server-locale.js.map +1 -1
  52. package/lib/lsp/hydranium-document-update-handler.js +1 -1
  53. package/lib/lsp/hydranium-document-update-handler.js.map +1 -1
  54. package/lib/messages/renderer.d.ts +23 -4
  55. package/lib/messages/renderer.d.ts.map +1 -1
  56. package/lib/messages/renderer.js +1 -1
  57. package/lib/messages/renderer.js.map +1 -1
  58. package/lib/testing/make-noop-shared-services.d.ts +4 -4
  59. package/lib/testing/make-noop-shared-services.d.ts.map +1 -1
  60. package/lib/testing/make-noop-shared-services.js +4 -4
  61. package/lib/testing/make-noop-shared-services.js.map +1 -1
  62. package/lib/testing/make-test-services.d.ts +9 -9
  63. package/lib/testing/make-test-services.d.ts.map +1 -1
  64. package/lib/testing/make-test-services.js +6 -6
  65. package/lib/testing/make-test-services.js.map +1 -1
  66. package/lib/testing/stub-ast-document-manager.d.ts +3 -4
  67. package/lib/testing/stub-ast-document-manager.d.ts.map +1 -1
  68. package/lib/testing/stub-ast-document-manager.js +2 -5
  69. package/lib/testing/stub-ast-document-manager.js.map +1 -1
  70. package/lib/testing/stub-model-service.d.ts +1 -1
  71. package/lib/testing/stub-model-service.d.ts.map +1 -1
  72. package/lib/testing/stub-model-service.js +2 -2
  73. package/lib/testing/stub-model-service.js.map +1 -1
  74. package/package.json +5 -5
  75. package/src/documents/ast-document-manager.ts +49 -9
  76. package/src/documents/hydranium-text-documents.ts +29 -8
  77. package/src/documents/language-client-text-shadow.ts +38 -2
  78. package/src/documents/self-save-registry.ts +52 -13
  79. package/src/index.ts +4 -0
  80. package/src/langium/bootstrap.ts +8 -8
  81. package/src/langium/document-builder/build-pipeline-integration.ts +14 -1
  82. package/src/langium/document-builder/document-builder.ts +2 -2
  83. package/src/langium/integration-services.ts +1 -1
  84. package/src/langium/model-service/model-service.ts +78 -7
  85. package/src/langium/module.ts +71 -24
  86. package/src/langium/residency/cst-residency-service.ts +35 -11
  87. package/src/langium/shared-services.ts +3 -3
  88. package/src/langium/transfer/transfer-encoder.ts +32 -1
  89. package/src/locale/server-locale.ts +16 -1
  90. package/src/lsp/hydranium-document-update-handler.ts +1 -1
  91. package/src/messages/renderer.ts +24 -4
  92. package/src/testing/make-noop-shared-services.ts +6 -6
  93. package/src/testing/make-test-services.ts +15 -15
  94. package/src/testing/stub-ast-document-manager.ts +5 -10
  95. package/src/testing/stub-model-service.ts +2 -2
@@ -200,7 +200,74 @@ export interface ModelServiceOptions extends LogNameOptions {
200
200
  * args. Constrained to {@link TransferElement}. Defaults to the
201
201
  * structural base.
202
202
  */
203
- export class ModelService<
203
+ /**
204
+ * The seam every non-LSP head talks to: the data server, the GLSP head and an
205
+ * adopter's own services reach documents through this slot rather than through
206
+ * the workspace stores.
207
+ *
208
+ * **Two families, and the distinction matters more than the names suggest.**
209
+ * `waitFor*` is a pure wait — it never triggers a build, so a caller waiting on
210
+ * a document no build has touched waits until something else builds it.
211
+ * `ensureDocumentState` and the phase shorthands over it *dispatch*: warm
212
+ * documents are awaited, cold ones are built.
213
+ *
214
+ * **Diagnostics are typed `never` below `Validated`.** Validation is the last
215
+ * phase, so at any earlier landmark the array either is not yet computed or
216
+ * still holds the previous build's, and reading it would take stale results
217
+ * for fresh ones. A caller that needs diagnostics asks for {@link validated}.
218
+ *
219
+ * What the `never` buys, exactly: reading a field off an element is a compile
220
+ * error, and nothing can be appended. It does NOT stop a caller assigning an
221
+ * element to a typed variable, because `never` is assignable to everything — so
222
+ * this is a guard against reaching for diagnostics by accident, not a seal
223
+ * against doing it deliberately.
224
+ *
225
+ * The waits resolve at or ABOVE their target, so an already-validated document
226
+ * does carry usable diagnostics and the `never` over-forbids there. That
227
+ * direction is the safe one: the alternative permits stale reads silently. A
228
+ * member taking a phase as a PARAMETER cannot judge statically and so returns
229
+ * `TDiagnostic`, leaving the choice to the caller.
230
+ */
231
+ export interface ModelService<TAst extends AstNode, TDiagnostic = TransferDiagnostic, TTransfer extends TransferElement = TransferElement> {
232
+ /**
233
+ * Resolves once the workspace has been initialised and its first build has
234
+ * completed — the gate every read should wait behind, since a document
235
+ * queried before it may be unbuilt and reach no phase.
236
+ *
237
+ * A property rather than a method, matching `ProjectManager.ready` and
238
+ * Langium's `WorkspaceManager.ready`. An implementation needing a stricter
239
+ * gate supplies a Promise that awaits its own concern as well.
240
+ */
241
+ readonly ready: Promise<void>;
242
+
243
+ // Pure waits — never trigger a build.
244
+ waitForDocumentState(uri: string, state: DocumentState, cancelToken?: CancellationToken): Promise<AstDocument<TAst, TDiagnostic>>;
245
+ waitForDocumentSettled(uri: string, cancelToken?: CancellationToken): Promise<AstDocument<TAst, never>>;
246
+ waitForBuilderState(state: DocumentState, cancelToken?: CancellationToken): Promise<void>;
247
+
248
+ // Wait if warm, build if cold.
249
+ ensureDocumentState(uri: string, state?: DocumentState, cancelToken?: CancellationToken): Promise<AstDocument<TAst, TDiagnostic>>;
250
+ rebuild(uri: string, state?: DocumentState, cancelToken?: CancellationToken): Promise<AstDocument<TAst, TDiagnostic>>;
251
+ parsed(uri: string, cancelToken?: CancellationToken): Promise<AstDocument<TAst, never>>;
252
+ linked(uri: string, cancelToken?: CancellationToken): Promise<AstDocument<TAst, never>>;
253
+ settled(uri: string, cancelToken?: CancellationToken): Promise<AstDocument<TAst, never>>;
254
+ indexed(uri: string, cancelToken?: CancellationToken): Promise<AstDocument<TAst, never>>;
255
+ validated(uri: string, cancelToken?: CancellationToken): Promise<AstDocument<TAst, TDiagnostic>>;
256
+
257
+ update(args: TransferUpdateArgs<TTransfer>, cancelToken?: CancellationToken): Promise<AstDocument<TAst, TDiagnostic>>;
258
+ save(args: TransferSaveArgs<TTransfer>, cancelToken?: CancellationToken): Promise<AstDocument<TAst, TDiagnostic>>;
259
+
260
+ open(args: OpenModelArgs): Promise<Disposable>;
261
+ close(args: CloseModelArgs): Promise<void>;
262
+ isOpen(uri: string): boolean;
263
+ getDocument(uri: string): LangiumDocument | undefined;
264
+
265
+ onModelUpdated(uri: string, listener: (event: AstDocumentUpdatedEvent<TAst, TDiagnostic>) => void): Disposable;
266
+ onModelSaved(uri: string, listener: (event: AstDocumentSavedEvent<TAst, TDiagnostic>) => void): Disposable;
267
+ onClientClosed(uri: string, clientId: string, listener: () => void): Disposable;
268
+ }
269
+
270
+ export class DefaultModelService<
204
271
  TAst extends AstNode,
205
272
  TDiagnostic = TransferDiagnostic,
206
273
  /**
@@ -213,7 +280,7 @@ export class ModelService<
213
280
  * integrity service) that pass AST roots remain compatible.
214
281
  */
215
282
  TTransfer extends TransferElement = TransferElement
216
- > {
283
+ > implements ModelService<TAst, TDiagnostic, TTransfer> {
217
284
  protected readonly tracer: Tracer;
218
285
  /**
219
286
  * The single document-identity seam (`services.workspace.DocumentUriPolicy`),
@@ -347,8 +414,12 @@ export class ModelService<
347
414
  * {@link waitForDocumentState} for the common "wait until content is stable"
348
415
  * case (e.g. settling a save). Pure wait — does not trigger a build.
349
416
  */
350
- async waitForDocumentSettled(uri: string, cancelToken?: CancellationToken): Promise<AstDocument<TAst, TDiagnostic>> {
351
- return this.waitForDocumentStateCanonical(this.uriPolicy.canonicalUri(uri), IntegrityService.SettledState, cancelToken);
417
+ async waitForDocumentSettled(uri: string, cancelToken?: CancellationToken): Promise<AstDocument<TAst, never>> {
418
+ return (await this.waitForDocumentStateCanonical(
419
+ this.uriPolicy.canonicalUri(uri),
420
+ IntegrityService.SettledState,
421
+ cancelToken
422
+ )) as AstDocument<TAst, never>;
352
423
  }
353
424
 
354
425
  /**
@@ -863,17 +934,17 @@ export class ModelService<
863
934
  *
864
935
  * The signal is "a known client other than the language client authored this
865
936
  * version **and** the URI was in the last build's changed set
866
- * (`isDirectChange`)". A framework-internal rebuild reports no author
937
+ * (`isTriggeringEdit`)". A framework-internal rebuild reports no author
867
938
  * (`getAuthor` → `undefined`), so it fails `hasKnownAuthor` without comparing
868
939
  * against a sentinel. This is NOT redundant with content/registration — it
869
940
  * distinguishes "client edited" from "framework rebuilt", which neither the
870
- * shadow nor `isDirectChange` alone can.
941
+ * shadow nor `isTriggeringEdit` alone can.
871
942
  */
872
943
  protected isNonLanguageClientEdit(document: LangiumDocument): boolean {
873
944
  const documents = this.services.workspace.AstDocumentManager;
874
945
  const author = documents.getAuthor(document);
875
946
  const hasKnownAuthor = !!author && author !== LANGUAGE_CLIENT_ID;
876
- return hasKnownAuthor && documents.isDirectChange(document.textDocument.uri);
947
+ return hasKnownAuthor && documents.isTriggeringEdit(document.textDocument.uri);
877
948
  }
878
949
 
879
950
  /**
@@ -7,23 +7,23 @@
7
7
  * SPDX-License-Identifier: MIT
8
8
  ********************************************************************************/
9
9
 
10
- import { type Clock, type Logger, type Project, type Tracer, type TransferDiagnostic, NoopLogger, SystemClock } from '@hydranium/protocol';
11
- import { ServerLocale } from '../locale/server-locale.js';
12
- import { ServerMessageRenderer } from '../messages/renderer.js';
10
+ import { type Clock, type Logger, type Project, type Tracer, NoopLogger, SystemClock } from '@hydranium/protocol';
11
+ import { DefaultServerLocale, type ServerLocale } from '../locale/server-locale.js';
12
+ import { DefaultMessageRenderer, type MessageRenderer } from '../messages/renderer.js';
13
13
  import { ServerTracer } from './diagnostics/server-tracer.js';
14
14
  import { HydraniumLangiumProfiler } from './diagnostics/hydranium-langium-profiler.js';
15
15
  import { type AstNode, type Module } from '@hydranium/langium';
16
16
  import { type DefaultSharedModuleContext, type LangiumSharedServices, type PartialLangiumSharedServices } from '@hydranium/langium/lsp';
17
17
  import { type TextDocument } from 'vscode-languageserver-textdocument';
18
- import { ModelService } from './model-service/model-service.js';
18
+ import { DefaultModelService, type ModelService } from './model-service/model-service.js';
19
19
  import { type ProjectManager } from './project/project-manager.js';
20
20
  import { SingleProjectManager } from './project/single-project-manager.js';
21
- import { TransferEncoder } from './transfer/transfer-encoder.js';
21
+ import { DefaultTransferEncoder, type TransferEncoder } from './transfer/transfer-encoder.js';
22
22
  import { HydraniumDocumentBuilder } from './document-builder/document-builder.js';
23
- import { BuildPipelineIntegration } from './document-builder/build-pipeline-integration.js';
23
+ import { DefaultBuildPipelineIntegration, type BuildPipelineIntegration } from './document-builder/build-pipeline-integration.js';
24
24
  import { type BuildPhasePassContribution } from './build-phase-pass/build-phase-pass.js';
25
25
  import { type BuildPhasePassService, DefaultBuildPhasePassService } from './build-phase-pass/build-phase-pass-service.js';
26
- import { CstResidencyService } from './residency/index.js';
26
+ import { DefaultCstResidencyService, type CstResidencyService } from './residency/index.js';
27
27
  import { HydraniumIndexManager } from './workspace/index-manager.js';
28
28
  import { HydraniumWorkspaceManager } from './workspace/hydranium-workspace-manager.js';
29
29
  import { HydraniumWorkspaceLock } from './workspace/hydranium-workspace-lock.js';
@@ -32,8 +32,8 @@ import { type HydraniumDocumentRegistry, HydraniumLangiumDocuments } from './wor
32
32
  import { type AdditionalDocumentContribution } from './workspace/additional-document-contribution.js';
33
33
  import { DefaultDocumentUriPolicy, type DocumentUriPolicy } from './workspace/document-uri-policy.js';
34
34
  import { HydraniumTextDocuments } from '../documents/hydranium-text-documents.js';
35
- import { AstDocumentManager, type WritableFileSystemProvider } from '../documents/ast-document-manager.js';
36
- import { SelfSaveRegistry } from '../documents/self-save-registry.js';
35
+ import { DefaultAstDocumentManager, type AstDocumentManager, type WritableFileSystemProvider } from '../documents/ast-document-manager.js';
36
+ import { DefaultSelfSaveRegistry, type SelfSaveRegistry } from '../documents/self-save-registry.js';
37
37
  import { DefaultEmptyFileSystemProvider } from './workspace/file-system-provider.js';
38
38
  import { type ServerLanguageServices } from './language-module.js';
39
39
  import { ExtendedServiceRegistry } from './service-registry.js';
@@ -105,7 +105,7 @@ export interface ServerAddedSharedServices<TProject extends Project = Project> {
105
105
  * returns every sentence unchanged. Adopters subclass and override
106
106
  * `translationsFor`.
107
107
  */
108
- MessageRenderer: ServerMessageRenderer;
108
+ MessageRenderer: MessageRenderer;
109
109
  /**
110
110
  * The locale an init handed the server, for whoever renders in it. Held
111
111
  * apart from the renderer so replacing the renderer cannot drop locale
@@ -255,14 +255,22 @@ export interface ServerAddedSharedServices<TProject extends Project = Project> {
255
255
  * heads read both slots from shared services rather than constructor
256
256
  * arguments.
257
257
  *
258
- * Slot types use the upper bounds (`unknown` for encoder map / facade
259
- * generics, {@link TransferDiagnostic} for the encoder's wire-diagnostic
260
- * shape) so adopter subclasses with narrower typed overlays satisfy
261
- * the slot via output-position covariance `TDiagnostic` appears
262
- * only in encoder return positions.
258
+ * `TransferEncoder` is the INTERFACE, not `DefaultTransferEncoder` an
259
+ * adopter can therefore REPLACE this declaration (see
260
+ * {@link WithServiceOverrides}) rather than intersect with it, which is what
261
+ * keeps slot resolution independent of the order a services type is written
262
+ * in. `ModelService` is still a class and does not yet have that property.
263
+ *
264
+ * A class in a slot costs two things, both measured. Its `protected` members
265
+ * join every assignability check and are compared NOMINALLY, so a subclass
266
+ * declared against a second physical copy of this package cannot satisfy it.
267
+ * And where the class is generic over a map reached through `keyof`, that
268
+ * parameter is measured INVARIANT — instantiations then relate only when
269
+ * their arguments are mutually assignable, which no adopter map is with the
270
+ * framework's. Neither survives on an interface.
263
271
  */
264
272
  model: {
265
- TransferEncoder: TransferEncoder<unknown, TransferDiagnostic>;
273
+ TransferEncoder: TransferEncoder;
266
274
  ModelService: ModelService<AstNode, unknown>;
267
275
  };
268
276
  /**
@@ -306,6 +314,45 @@ export type ServerSharedServices<TProject extends Project = Project> = Omit<Lang
306
314
  workspace: Omit<LangiumSharedServices['workspace'], 'TextDocuments' | 'DocumentBuilder'>;
307
315
  } & ServerAddedSharedServices<TProject>;
308
316
 
317
+ /**
318
+ * The service-tree namespaces — the keys whose value groups further slots
319
+ * rather than being a slot itself.
320
+ *
321
+ * Consumed by {@link WithServiceOverrides} to know where to merge one level
322
+ * deeper. **Add a namespace to {@link ServerAddedSharedServices} and you must
323
+ * add it here**, and the failure of forgetting is silent in the direction that
324
+ * matters: an unlisted namespace is treated as a leaf, so an adopter overriding
325
+ * one slot inside it replaces the WHOLE namespace and loses the sibling slots
326
+ * with no diagnostic. The reverse mistake — listing a leaf — fails loudly.
327
+ */
328
+ type ServiceNamespace = 'lsp' | 'workspace' | 'model';
329
+
330
+ /**
331
+ * Compose an adopter's service tree so its declarations REPLACE the framework's
332
+ * rather than intersecting with them.
333
+ *
334
+ * Langium composes services by intersection, which accumulates: two
335
+ * declarations of one slot survive as an overload set, and which one a call
336
+ * resolves to depends on the order the intersection was written in — silently,
337
+ * with no diagnostic at the point a reorder changes it. There is no override
338
+ * operator for intersections, so the framework's declaration has to be removed
339
+ * before the adopter's is added. This does that, one level deep for each
340
+ * {@link ServiceNamespace}, so a narrowed slot replaces its framework twin
341
+ * while its siblings survive.
342
+ *
343
+ * Only slots typed as INTERFACES can be replaced this way. A class-typed slot
344
+ * drags its `protected` members into the assignability check — compared
345
+ * nominally — so an adopter subclass does not satisfy it and the framework's
346
+ * declaration cannot be dropped.
347
+ */
348
+ export type WithServiceOverrides<TBase, TOverrides> = Omit<TBase, keyof TOverrides> & {
349
+ [K in keyof TOverrides]: K extends ServiceNamespace
350
+ ? K extends keyof TBase
351
+ ? Omit<TBase[K], keyof TOverrides[K]> & TOverrides[K]
352
+ : TOverrides[K]
353
+ : TOverrides[K];
354
+ };
355
+
309
356
  /**
310
357
  * Construction context for {@link createServerSharedModule}.
311
358
  *
@@ -354,8 +401,8 @@ export function createServerSharedModule(
354
401
  ServiceRegistry: services => new ExtendedServiceRegistry<ServerLanguageServices>(services),
355
402
  Logger: () => new NoopLogger(),
356
403
  Tracer: services => new ServerTracer(services.Logger, services.Clock),
357
- ServerLocale: services => new ServerLocale(services),
358
- MessageRenderer: services => new ServerMessageRenderer(services),
404
+ ServerLocale: services => new DefaultServerLocale(services),
405
+ MessageRenderer: services => new DefaultMessageRenderer(services),
359
406
  // Bind Langium's per-grammar-rule / per-`$type` parse/link/validate
360
407
  // profiler (the data the framework's own `Tracer`/`ProfileSession` passes
361
408
  // cannot produce), routed through our `Logger` and debug-gated. At the
@@ -430,7 +477,7 @@ export function createServerSharedModule(
430
477
  // scope tracker — `@hydranium/core/node` does.
431
478
  WorkspaceLock: () => new HydraniumWorkspaceLock(),
432
479
  ProjectManager: services => new SingleProjectManager(services),
433
- SelfSaveRegistry: services => new SelfSaveRegistry(services),
480
+ SelfSaveRegistry: services => new DefaultSelfSaveRegistry(services),
434
481
  DocumentUriPolicy: () => new DefaultDocumentUriPolicy(),
435
482
  // Writable filesystem with two paths:
436
483
  // - Adopter passed `context.fileSystemProvider` and it returned a
@@ -454,20 +501,20 @@ export function createServerSharedModule(
454
501
  }
455
502
  return new DefaultEmptyFileSystemProvider(services);
456
503
  },
457
- AstDocumentManager: services => new AstDocumentManager(services),
504
+ AstDocumentManager: services => new DefaultAstDocumentManager(services),
458
505
  // Eagerly constructed, so its build-phase listeners attach before the
459
506
  // first build.
460
- BuildPipelineIntegration: services => new BuildPipelineIntegration(services),
507
+ BuildPipelineIntegration: services => new DefaultBuildPipelineIntegration(services),
461
508
  BuildPhasePassService: services => new DefaultBuildPhasePassService(services),
462
509
  // Eagerly constructed, so its `Validated` pass registers before the
463
510
  // first build.
464
- CstResidencyService: services => new CstResidencyService(services)
511
+ CstResidencyService: services => new DefaultCstResidencyService(services)
465
512
  },
466
513
  model: {
467
514
  // Generic walker — adopters with a typed `$type → wire shape` overlay
468
515
  // rebind this slot with a subclass.
469
- TransferEncoder: services => new TransferEncoder(services),
470
- ModelService: services => new ModelService(services)
516
+ TransferEncoder: services => new DefaultTransferEncoder(services),
517
+ ModelService: services => new DefaultModelService(services)
471
518
  },
472
519
  // Empty default so `services.buildPhasePasses` always resolves (Langium
473
520
  // throws on access to an unbound slot). The framework's own integrity /
@@ -24,9 +24,13 @@ import { type ServerSharedServicesMinimal } from '../shared-services.js';
24
24
  /**
25
25
  * Default priority of the CST-residency pass. Deliberately very high so it runs
26
26
  * *last* among `Validated` build-phase passes (the registry iterates priority
27
- * ascending): every other reaction to the build — an adopter's own derived-state
28
- * pass included — settles while the CST is still guaranteed resident, before the
29
- * residency policy arms the shed.
27
+ * ascending).
28
+ *
29
+ * The ordering is defensive, not load-bearing: the pass only arms or cancels an
30
+ * idle timer, so every pass at `Validated` reads a resident CST whichever order
31
+ * they run in, and lowering it changes nothing observable while that holds. It
32
+ * becomes load-bearing the moment the shed stops being deferred — a pass that
33
+ * frees the CST synchronously has to run after every reader of it.
30
34
  */
31
35
  export const CST_RESIDENCY_PASS_PRIORITY = 1_000_000;
32
36
 
@@ -227,7 +231,27 @@ function rehydrateCst(document: LangiumDocument, factory: LangiumDocumentFactory
227
231
  * `NameProvider.getNameNode` chokepoint, the comment provider, and
228
232
  * `HydraniumLangiumDocuments.getOrCreateDocument`.
229
233
  */
230
- export class CstResidencyService {
234
+ /**
235
+ * Keeps a document's CST available to the readers that need one, shedding it
236
+ * for documents that have gone idle and re-grafting on demand.
237
+ */
238
+ export interface CstResidencyService {
239
+ /**
240
+ * Ensure `document` has a CST, re-parsing its retained text if it was shed.
241
+ * A resident document is left exactly as it is, so this is safe to call on
242
+ * a hot path and safe to call twice.
243
+ */
244
+ rehydrate(document: LangiumDocument): void;
245
+
246
+ /**
247
+ * Node-level convenience over {@link rehydrate}, cheap enough for an
248
+ * unconditional call: a node whose `$cstNode` is already set returns
249
+ * without resolving its document.
250
+ */
251
+ rehydrateNode(node: AstNode): void;
252
+ }
253
+
254
+ export class DefaultCstResidencyService implements CstResidencyService {
231
255
  protected readonly strategy: CstResidencyStrategy;
232
256
  /** Bytes-per-node for the optional reclaim estimate in traces; `0` disables it. */
233
257
  protected readonly estimatedBytesPerShedNode: number;
@@ -277,8 +301,8 @@ export class CstResidencyService {
277
301
  * until navigation latency on shed files is actually observed to hurt. This
278
302
  * method is where that hook goes.
279
303
  */
280
- rehydrate(document: LangiumDocument): boolean {
281
- return rehydrateCst(document, this.services.workspace.LangiumDocumentFactory);
304
+ rehydrate(document: LangiumDocument): void {
305
+ rehydrateCst(document, this.services.workspace.LangiumDocumentFactory);
282
306
  }
283
307
 
284
308
  /**
@@ -287,11 +311,11 @@ export class CstResidencyService {
287
311
  * `true` without even resolving the document, so hot read paths
288
312
  * (`getNameNode`, comment lookup) call this unconditionally.
289
313
  */
290
- rehydrateNode(node: AstNode): boolean {
314
+ rehydrateNode(node: AstNode): void {
291
315
  if (node.$cstNode !== undefined) {
292
- return true;
316
+ return;
293
317
  }
294
- return this.rehydrate(AstUtils.getDocument(node));
318
+ this.rehydrate(AstUtils.getDocument(node));
295
319
  }
296
320
 
297
321
  /**
@@ -353,8 +377,8 @@ export class CstResidencyService {
353
377
  this.shedTotal += this.shed(document);
354
378
  }
355
379
 
356
- /** Cancel and drop every idle timer for clean shutdown and deterministic tests. */
357
- dispose(): void {
380
+ /** Cancel every armed shed, so a document that has gone idle keeps its CST. */
381
+ cancelPendingShed(): void {
358
382
  for (const timer of this.idleTimers.values()) {
359
383
  timer.dispose();
360
384
  }
@@ -10,7 +10,7 @@
10
10
  import type { Clock, Logger, Project, Tracer } from '@hydranium/protocol';
11
11
  import type { LangiumSharedCoreServices } from '@hydranium/langium';
12
12
  import type { ServerLocale } from '../locale/server-locale.js';
13
- import type { ServerMessageRenderer } from '../messages/renderer.js';
13
+ import type { MessageRenderer } from '../messages/renderer.js';
14
14
  import type { SelfSaveRegistry } from '../documents/self-save-registry.js';
15
15
  import type { WritableFileSystemProvider } from '../documents/ast-document-manager.js';
16
16
  import type { HydraniumDocumentRegistry } from './workspace/langium-documents.js';
@@ -31,7 +31,7 @@ import type { AdditionalDocumentContribution } from './workspace/additional-docu
31
31
  * {@link Clock}, {@link Logger} and {@link Tracer} top-level slots (injectable
32
32
  * time source, emission-only logger, and the measure-and-emit tracer composed
33
33
  * from the other two), the {@link AdditionalDocumentContribution} group, the
34
- * {@link ServerMessageRenderer} and the {@link ServerLocale} it renders in,
34
+ * {@link MessageRenderer} and the {@link ServerLocale} it renders in,
35
35
  * an {@link ExtendedServiceRegistry}, and on `workspace` a writable file-system
36
36
  * provider plus {@link HydraniumWorkspaceManager}, {@link ProjectManager},
37
37
  * {@link SelfSaveRegistry}, {@link BuildPipelineIntegration},
@@ -66,7 +66,7 @@ export interface ServerSharedServicesMinimal<TProject extends Project = Project>
66
66
  * shared tier with no workspace dependency, because all three heads'
67
67
  * messages pass through it.
68
68
  */
69
- MessageRenderer: ServerMessageRenderer;
69
+ MessageRenderer: MessageRenderer;
70
70
  /** The locale an init handed the server, for whoever renders in it. */
71
71
  ServerLocale: ServerLocale;
72
72
  /**
@@ -80,6 +80,37 @@ export interface TransferEnvelopeSource<TDiagnostic> {
80
80
  readonly diagnostics: readonly (TDiagnostic | Diagnostic)[];
81
81
  }
82
82
 
83
+ /**
84
+ * The AST→transfer contract a DI slot holds, implemented by
85
+ * {@link DefaultTransferEncoder}.
86
+ *
87
+ * **This is an interface and not the class, and that is load-bearing.** A class
88
+ * in a slot drags its `protected` members into every assignability check, which
89
+ * TypeScript compares NOMINALLY — so the slot type could not be satisfied by a
90
+ * subclass declared against a second physical copy of this package, and an
91
+ * adopter narrowing the slot had to intersect with the framework's declaration
92
+ * rather than replace it, leaving method resolution dependent on declaration
93
+ * order. An interface has no protected members and is compared structurally, so
94
+ * neither applies.
95
+ *
96
+ * **The transfer returns are the structural base ({@link TransferElement}), not
97
+ * the per-grammar overlay.** The overlay cannot parameterise this contract:
98
+ * `TransferTypeFor` reaches `keyof TTransferMap`, which makes that parameter
99
+ * measured-INVARIANT, and invariant parameters relate only when their arguments
100
+ * are mutually assignable — so a map-parameterised contract admits no adopter
101
+ * instantiation at all. Heads read this contract and do not know adopter maps;
102
+ * an adopter recovers the precise type by declaring its own slot, which it can
103
+ * now do by replacement.
104
+ */
105
+ export interface TransferEncoder<TDiagnostic extends TransferDiagnostic = TransferDiagnostic> {
106
+ toTransfer<T extends AstNode>(ast: T, mode?: TransferMode): TransferElement;
107
+ toTransferDocument(langiumDocument: LangiumDocument): TransferDocument<TransferElement, TDiagnostic>;
108
+ astDocumentToTransferDocument<TAst extends AstNode>(
109
+ document: AstDocument<TAst, TDiagnostic | TransferLspDiagnostic>
110
+ ): TransferDocument<TransferElement, TDiagnostic>;
111
+ toTransferDiagnostic(diagnostic: TransferLspDiagnostic): TDiagnostic;
112
+ }
113
+
83
114
  /**
84
115
  * One-directional structural mapper from a Langium AST into the wire
85
116
  * transfer-model shape ({@link TransferElement}-based JSON).
@@ -124,7 +155,7 @@ export interface TransferEnvelopeSource<TDiagnostic> {
124
155
  * - {@link toTransferDiagnostic} — project a {@link TransferLspDiagnostic}
125
156
  * (the framework validator's output) to {@link TDiagnostic}.
126
157
  */
127
- export class TransferEncoder<
158
+ export class DefaultTransferEncoder<
128
159
  // The map is intentionally unconstrained — adopters supply typed
129
160
  // overlays whose specific-key shapes don't structurally satisfy a
130
161
  // `Record<string, TransferElement>` index signature. `TransferTypeFor`
@@ -14,6 +14,21 @@ import type { ServerSharedServicesMinimal } from '../langium/shared-services.js'
14
14
  /** Construction options for {@link ServerLocale}. */
15
15
  export type ServerLocaleOptions = LogNameOptions;
16
16
 
17
+ /**
18
+ * The locale contract the `ServerLocale` slot holds, implemented by
19
+ * {@link DefaultServerLocale}.
20
+ *
21
+ * An interface rather than the class, so the slot is compared STRUCTURALLY —
22
+ * a class-typed slot carries its `protected` members into every assignability
23
+ * check, compared nominally, which makes it unsatisfiable across two physical
24
+ * copies of this package and unreplaceable by an adopter's own declaration.
25
+ */
26
+ export interface ServerLocale {
27
+ /** The locale, or `undefined` when no init supplied one — the framework's English. */
28
+ readonly value: string | undefined;
29
+ accept(locale: string): void;
30
+ }
31
+
17
32
  /**
18
33
  * The locale the server was handed at init, for whoever needs to render in the
19
34
  * reading user's language. Held apart from the message renderer that reads it,
@@ -23,7 +38,7 @@ export type ServerLocaleOptions = LogNameOptions;
23
38
  * read the current value wraps this itself — nothing does today, and both real
24
39
  * hosts respawn the server on a display-language switch.
25
40
  */
26
- export class ServerLocale {
41
+ export class DefaultServerLocale implements ServerLocale {
27
42
  protected readonly tracer: Tracer;
28
43
  protected current: string | undefined;
29
44
 
@@ -257,7 +257,7 @@ export class HydraniumDocumentUpdateHandler extends DefaultDocumentUpdateHandler
257
257
  }
258
258
  const uri = UriUtils.toUri(change.uri);
259
259
  const mtimeMs = await this.fileSystemProvider.mtimeMs?.(uri);
260
- if (mtimeMs !== undefined && this.selfSaveRegistry.matches(uri.fsPath, mtimeMs)) {
260
+ if (mtimeMs !== undefined && this.selfSaveRegistry.isRegistered(uri.fsPath, mtimeMs)) {
261
261
  return undefined;
262
262
  }
263
263
  return change;
@@ -25,8 +25,28 @@ import type { ServerSharedServicesMinimal } from '../langium/shared-services.js'
25
25
  import type { ServerLocale } from '../locale/server-locale.js';
26
26
  import { resolvedFromDiagnostic } from './carriers.js';
27
27
 
28
- /** Construction options for {@link ServerMessageRenderer}. */
29
- export type ServerMessageRendererOptions = LogNameOptions;
28
+ /** Construction options for {@link DefaultMessageRenderer}. */
29
+ export type MessageRendererOptions = LogNameOptions;
30
+
31
+ /**
32
+ * The render contract the `MessageRenderer` slot holds, implemented by
33
+ * {@link DefaultMessageRenderer}.
34
+ *
35
+ * An interface rather than the class, so the slot is compared STRUCTURALLY. A
36
+ * class-typed slot carries its `protected` members into every assignability
37
+ * check and TypeScript compares those nominally — which makes the slot
38
+ * unsatisfiable by a subclass declared against a second physical copy of this
39
+ * package, the state a pre-publish `yalc` / `file:` install produces. It also
40
+ * lets an adopter REPLACE this declaration rather than intersect with it, so
41
+ * slot resolution does not depend on the order a services type is written in.
42
+ *
43
+ * Every method carries a no-throw contract; see the implementation.
44
+ */
45
+ export interface MessageRenderer {
46
+ renderDiagnostic(diagnostic: Diagnostic): string;
47
+ renderError(error: ResponseError<unknown>): string;
48
+ renderMessage<S extends string>(message: MessageDefinition<S>, ...params: ParamsArg<S>): string;
49
+ }
30
50
 
31
51
  /**
32
52
  * Renders every user-facing message the server sends, in the locale the server
@@ -41,7 +61,7 @@ export type ServerMessageRendererOptions = LogNameOptions;
41
61
  * single `render(text)` would force an adopter to match English prose, which
42
62
  * breaks on the first Langium reword.
43
63
  */
44
- export class ServerMessageRenderer {
64
+ export class DefaultMessageRenderer implements MessageRenderer {
45
65
  protected readonly tracer: Tracer;
46
66
  protected readonly serverLocale: ServerLocale;
47
67
  /**
@@ -58,7 +78,7 @@ export class ServerMessageRenderer {
58
78
  */
59
79
  protected readonly catalogues = new SimpleCache<string | undefined, Record<string, string> | undefined>();
60
80
 
61
- constructor(services: ServerSharedServicesMinimal, options: ServerMessageRendererOptions = {}) {
81
+ constructor(services: ServerSharedServicesMinimal, options: MessageRendererOptions = {}) {
62
82
  this.serverLocale = services.ServerLocale;
63
83
  this.tracer = services.Tracer.for(options.logName ?? 'MessageRenderer').trace('instantiated');
64
84
  }
@@ -9,8 +9,8 @@
9
9
 
10
10
  import { type Clock, DefaultTracer, type Logger, NoopLogger, SystemClock, type Tracer } from '@hydranium/protocol';
11
11
  import type { ServerSharedServicesMinimal } from '../langium/shared-services.js';
12
- import { ServerLocale } from '../locale/server-locale.js';
13
- import { ServerMessageRenderer } from '../messages/renderer.js';
12
+ import { DefaultServerLocale, type ServerLocale } from '../locale/server-locale.js';
13
+ import { DefaultMessageRenderer, type MessageRenderer } from '../messages/renderer.js';
14
14
 
15
15
  /**
16
16
  * Overrides for {@link makeNoopSharedServices}. The three observability slots
@@ -35,14 +35,14 @@ export interface NoopSharedServicesOverrides {
35
35
  ServerLocale?: (services: ServerSharedServicesMinimal) => ServerLocale;
36
36
  /**
37
37
  * Factory for the `MessageRenderer` slot. Default: the real
38
- * {@link ServerMessageRenderer}, whose no-catalogue behaviour is a
38
+ * {@link DefaultMessageRenderer}, whose no-catalogue behaviour is a
39
39
  * pass-through — so the default is the framework's own behaviour, not a stub.
40
40
  *
41
41
  * A factory rather than an instance, because a renderer reads the tree it is
42
42
  * bound into. Both slots are resolved lazily, so an override is honoured
43
43
  * however late the caller reads them.
44
44
  */
45
- MessageRenderer?: (services: ServerSharedServicesMinimal) => ServerMessageRenderer;
45
+ MessageRenderer?: (services: ServerSharedServicesMinimal) => MessageRenderer;
46
46
  /** Per-slot `workspace` overrides. Slots left out resolve to `undefined`. */
47
47
  workspace?: Record<string, unknown>;
48
48
  /** Any other minimal slot (`ServiceRegistry`, `AstReflection`, `additionalDocuments`, …). */
@@ -103,8 +103,8 @@ export function makeNoopSharedServices<T extends ServerSharedServicesMinimal = S
103
103
  // both services emit an `instantiated` trace, so building them eagerly puts
104
104
  // two lines into the capture of every test that passes a capturing logger
105
105
  // and asserts on emptiness.
106
- defineLazySlot(services, 'ServerLocale', () => serverLocale?.(services) ?? new ServerLocale(services));
107
- defineLazySlot(services, 'MessageRenderer', () => messageRenderer?.(services) ?? new ServerMessageRenderer(services));
106
+ defineLazySlot(services, 'ServerLocale', () => serverLocale?.(services) ?? new DefaultServerLocale(services));
107
+ defineLazySlot(services, 'MessageRenderer', () => messageRenderer?.(services) ?? new DefaultMessageRenderer(services));
108
108
  return services;
109
109
  }
110
110