@hydranium/glsp-server 1.0.0-next.31 → 1.0.0-next.313

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 (217) hide show
  1. package/README.md +80 -88
  2. package/lib/browser/start-glsp-server-in-worker.d.ts +43 -42
  3. package/lib/browser/start-glsp-server-in-worker.d.ts.map +1 -1
  4. package/lib/browser/start-glsp-server-in-worker.js +62 -13
  5. package/lib/browser/start-glsp-server-in-worker.js.map +1 -1
  6. package/lib/command/diagram-edit-dropped-error.d.ts +31 -0
  7. package/lib/command/diagram-edit-dropped-error.d.ts.map +1 -0
  8. package/lib/command/diagram-edit-dropped-error.js +48 -0
  9. package/lib/command/diagram-edit-dropped-error.js.map +1 -0
  10. package/lib/command/hydranium-glsp-command-stack.d.ts +33 -0
  11. package/lib/command/hydranium-glsp-command-stack.d.ts.map +1 -0
  12. package/lib/command/hydranium-glsp-command-stack.js +63 -0
  13. package/lib/command/hydranium-glsp-command-stack.js.map +1 -0
  14. package/lib/command/hydranium-glsp-operation-action-handler.d.ts +64 -0
  15. package/lib/command/hydranium-glsp-operation-action-handler.d.ts.map +1 -0
  16. package/lib/command/hydranium-glsp-operation-action-handler.js +142 -0
  17. package/lib/command/hydranium-glsp-operation-action-handler.js.map +1 -0
  18. package/lib/command/hydranium-glsp-operation-command.d.ts +250 -0
  19. package/lib/command/hydranium-glsp-operation-command.d.ts.map +1 -0
  20. package/lib/command/hydranium-glsp-operation-command.js +491 -0
  21. package/lib/command/hydranium-glsp-operation-command.js.map +1 -0
  22. package/lib/command/hydranium-glsp-recording-command.d.ts +26 -87
  23. package/lib/command/hydranium-glsp-recording-command.d.ts.map +1 -1
  24. package/lib/command/hydranium-glsp-recording-command.js +40 -117
  25. package/lib/command/hydranium-glsp-recording-command.js.map +1 -1
  26. package/lib/command/hydranium-glsp-undo-redo-action-handler.d.ts +23 -0
  27. package/lib/command/hydranium-glsp-undo-redo-action-handler.d.ts.map +1 -0
  28. package/lib/command/hydranium-glsp-undo-redo-action-handler.js +41 -0
  29. package/lib/command/hydranium-glsp-undo-redo-action-handler.js.map +1 -0
  30. package/lib/command/index.d.ts +5 -0
  31. package/lib/command/index.d.ts.map +1 -1
  32. package/lib/command/index.js +5 -0
  33. package/lib/command/index.js.map +1 -1
  34. package/lib/computed-bounds/hydranium-glsp-computed-bounds-action-handler.d.ts +1 -1
  35. package/lib/computed-bounds/hydranium-glsp-computed-bounds-action-handler.d.ts.map +1 -1
  36. package/lib/computed-bounds/hydranium-glsp-computed-bounds-action-handler.js +2 -2
  37. package/lib/computed-bounds/hydranium-glsp-computed-bounds-action-handler.js.map +1 -1
  38. package/lib/dispatcher/server-action-dispatcher.d.ts +25 -24
  39. package/lib/dispatcher/server-action-dispatcher.d.ts.map +1 -1
  40. package/lib/dispatcher/server-action-dispatcher.js +38 -26
  41. package/lib/dispatcher/server-action-dispatcher.js.map +1 -1
  42. package/lib/index.d.ts +1 -0
  43. package/lib/index.d.ts.map +1 -1
  44. package/lib/index.js +1 -0
  45. package/lib/index.js.map +1 -1
  46. package/lib/launcher/abstract-hydranium-glsp-diagram-module.d.ts +35 -59
  47. package/lib/launcher/abstract-hydranium-glsp-diagram-module.d.ts.map +1 -1
  48. package/lib/launcher/abstract-hydranium-glsp-diagram-module.js +60 -71
  49. package/lib/launcher/abstract-hydranium-glsp-diagram-module.js.map +1 -1
  50. package/lib/launcher/drop-notifications-to-gone-client.d.ts +23 -0
  51. package/lib/launcher/drop-notifications-to-gone-client.d.ts.map +1 -0
  52. package/lib/launcher/drop-notifications-to-gone-client.js +43 -0
  53. package/lib/launcher/drop-notifications-to-gone-client.js.map +1 -0
  54. package/lib/launcher/glsp-app-module.d.ts +1 -2
  55. package/lib/launcher/glsp-app-module.d.ts.map +1 -1
  56. package/lib/launcher/glsp-app-module.js.map +1 -1
  57. package/lib/launcher/glsp-connection-logger.d.ts +19 -0
  58. package/lib/launcher/glsp-connection-logger.d.ts.map +1 -0
  59. package/lib/launcher/glsp-connection-logger.js +24 -0
  60. package/lib/launcher/glsp-connection-logger.js.map +1 -0
  61. package/lib/launcher/glsp-server-overrides.d.ts +10 -8
  62. package/lib/launcher/glsp-server-overrides.d.ts.map +1 -1
  63. package/lib/launcher/glsp-server-overrides.js +25 -11
  64. package/lib/launcher/glsp-server-overrides.js.map +1 -1
  65. package/lib/launcher/hydranium-glsp-server.d.ts +60 -15
  66. package/lib/launcher/hydranium-glsp-server.d.ts.map +1 -1
  67. package/lib/launcher/hydranium-glsp-server.js +145 -22
  68. package/lib/launcher/hydranium-glsp-server.js.map +1 -1
  69. package/lib/launcher/index.d.ts +2 -0
  70. package/lib/launcher/index.d.ts.map +1 -1
  71. package/lib/launcher/index.js +5 -4
  72. package/lib/launcher/index.js.map +1 -1
  73. package/lib/logging/glsp-client-logger.d.ts +10 -6
  74. package/lib/logging/glsp-client-logger.d.ts.map +1 -1
  75. package/lib/logging/glsp-client-logger.js +14 -7
  76. package/lib/logging/glsp-client-logger.js.map +1 -1
  77. package/lib/messages/index.d.ts +11 -6
  78. package/lib/messages/index.d.ts.map +1 -1
  79. package/lib/messages/index.js +11 -6
  80. package/lib/messages/index.js.map +1 -1
  81. package/lib/node/start-glsp-server.d.ts +50 -15
  82. package/lib/node/start-glsp-server.d.ts.map +1 -1
  83. package/lib/node/start-glsp-server.js +97 -30
  84. package/lib/node/start-glsp-server.js.map +1 -1
  85. package/lib/state/abstract-hydranium-glsp-state.d.ts +326 -102
  86. package/lib/state/abstract-hydranium-glsp-state.d.ts.map +1 -1
  87. package/lib/state/abstract-hydranium-glsp-state.js +529 -139
  88. package/lib/state/abstract-hydranium-glsp-state.js.map +1 -1
  89. package/lib/state/diagram-status.d.ts +38 -0
  90. package/lib/state/diagram-status.d.ts.map +1 -0
  91. package/lib/state/diagram-status.js +27 -0
  92. package/lib/state/diagram-status.js.map +1 -0
  93. package/lib/state/full-text-hydranium-glsp-state.d.ts +22 -9
  94. package/lib/state/full-text-hydranium-glsp-state.d.ts.map +1 -1
  95. package/lib/state/full-text-hydranium-glsp-state.js +45 -18
  96. package/lib/state/full-text-hydranium-glsp-state.js.map +1 -1
  97. package/lib/state/hydranium-glsp-index.d.ts +40 -29
  98. package/lib/state/hydranium-glsp-index.d.ts.map +1 -1
  99. package/lib/state/hydranium-glsp-index.js +82 -45
  100. package/lib/state/hydranium-glsp-index.js.map +1 -1
  101. package/lib/state/hydranium-shared-core-services.d.ts +16 -29
  102. package/lib/state/hydranium-shared-core-services.d.ts.map +1 -1
  103. package/lib/state/hydranium-shared-core-services.js +15 -28
  104. package/lib/state/hydranium-shared-core-services.js.map +1 -1
  105. package/lib/state/index.d.ts +1 -0
  106. package/lib/state/index.d.ts.map +1 -1
  107. package/lib/state/index.js +1 -0
  108. package/lib/state/index.js.map +1 -1
  109. package/lib/state/model-ready-timeout-error.d.ts.map +1 -1
  110. package/lib/state/model-ready-timeout-error.js.map +1 -1
  111. package/lib/state/reconcile-source-model-write.d.ts +13 -27
  112. package/lib/state/reconcile-source-model-write.d.ts.map +1 -1
  113. package/lib/state/reconcile-source-model-write.js +44 -32
  114. package/lib/state/reconcile-source-model-write.js.map +1 -1
  115. package/lib/state/reconciling-multi-document-glsp-state.d.ts +125 -69
  116. package/lib/state/reconciling-multi-document-glsp-state.d.ts.map +1 -1
  117. package/lib/state/reconciling-multi-document-glsp-state.js +242 -100
  118. package/lib/state/reconciling-multi-document-glsp-state.js.map +1 -1
  119. package/lib/state/reconciling-transfer-hydranium-glsp-state.d.ts +36 -28
  120. package/lib/state/reconciling-transfer-hydranium-glsp-state.d.ts.map +1 -1
  121. package/lib/state/reconciling-transfer-hydranium-glsp-state.js +59 -42
  122. package/lib/state/reconciling-transfer-hydranium-glsp-state.js.map +1 -1
  123. package/lib/status/diagram-status-reporter.d.ts +44 -0
  124. package/lib/status/diagram-status-reporter.d.ts.map +1 -0
  125. package/lib/status/diagram-status-reporter.js +88 -0
  126. package/lib/status/diagram-status-reporter.js.map +1 -0
  127. package/lib/status/hydranium-glsp-request-model-action-handler.d.ts +27 -0
  128. package/lib/status/hydranium-glsp-request-model-action-handler.d.ts.map +1 -0
  129. package/lib/status/hydranium-glsp-request-model-action-handler.js +52 -0
  130. package/lib/status/hydranium-glsp-request-model-action-handler.js.map +1 -0
  131. package/lib/status/hydranium-glsp-set-edit-mode-action-handler.d.ts +27 -0
  132. package/lib/status/hydranium-glsp-set-edit-mode-action-handler.d.ts.map +1 -0
  133. package/lib/status/hydranium-glsp-set-edit-mode-action-handler.js +53 -0
  134. package/lib/status/hydranium-glsp-set-edit-mode-action-handler.js.map +1 -0
  135. package/lib/status/index.d.ts +12 -0
  136. package/lib/status/index.d.ts.map +1 -0
  137. package/lib/status/index.js +12 -0
  138. package/lib/status/index.js.map +1 -0
  139. package/lib/storage/hydranium-glsp-request-save-model-action-handler.d.ts +30 -0
  140. package/lib/storage/hydranium-glsp-request-save-model-action-handler.d.ts.map +1 -0
  141. package/lib/storage/hydranium-glsp-request-save-model-action-handler.js +47 -0
  142. package/lib/storage/hydranium-glsp-request-save-model-action-handler.js.map +1 -0
  143. package/lib/storage/hydranium-glsp-storage.d.ts +324 -143
  144. package/lib/storage/hydranium-glsp-storage.d.ts.map +1 -1
  145. package/lib/storage/hydranium-glsp-storage.js +569 -213
  146. package/lib/storage/hydranium-glsp-storage.js.map +1 -1
  147. package/lib/storage/index.d.ts +2 -1
  148. package/lib/storage/index.d.ts.map +1 -1
  149. package/lib/storage/index.js +2 -1
  150. package/lib/storage/index.js.map +1 -1
  151. package/lib/storage/save-delivery-policy.d.ts +51 -0
  152. package/lib/storage/save-delivery-policy.d.ts.map +1 -0
  153. package/lib/storage/{save-conflict-policy.js → save-delivery-policy.js} +7 -8
  154. package/lib/storage/save-delivery-policy.js.map +1 -0
  155. package/lib/submission/hydranium-glsp-submission-handler.d.ts +44 -4
  156. package/lib/submission/hydranium-glsp-submission-handler.d.ts.map +1 -1
  157. package/lib/submission/hydranium-glsp-submission-handler.js +82 -6
  158. package/lib/submission/hydranium-glsp-submission-handler.js.map +1 -1
  159. package/lib/testing/glsp-harness.d.ts +34 -7
  160. package/lib/testing/glsp-harness.d.ts.map +1 -1
  161. package/lib/testing/glsp-harness.js +47 -19
  162. package/lib/testing/glsp-harness.js.map +1 -1
  163. package/lib/validation/diagnostic-markers.d.ts +3 -3
  164. package/lib/validation/diagnostic-markers.d.ts.map +1 -1
  165. package/lib/validation/diagnostic-markers.js +1 -1
  166. package/lib/validation/diagnostic-markers.js.map +1 -1
  167. package/lib/validation/hydranium-glsp-model-validator.d.ts +1 -2
  168. package/lib/validation/hydranium-glsp-model-validator.d.ts.map +1 -1
  169. package/lib/validation/hydranium-glsp-model-validator.js +1 -1
  170. package/lib/validation/hydranium-glsp-model-validator.js.map +1 -1
  171. package/package.json +31 -52
  172. package/src/browser/start-glsp-server-in-worker.ts +79 -51
  173. package/src/command/diagram-edit-dropped-error.ts +51 -0
  174. package/src/command/hydranium-glsp-command-stack.ts +47 -0
  175. package/src/command/hydranium-glsp-operation-action-handler.ts +141 -0
  176. package/src/command/hydranium-glsp-operation-command.ts +602 -0
  177. package/src/command/hydranium-glsp-recording-command.ts +40 -121
  178. package/src/command/hydranium-glsp-undo-redo-action-handler.ts +29 -0
  179. package/src/command/index.ts +10 -0
  180. package/src/computed-bounds/hydranium-glsp-computed-bounds-action-handler.ts +2 -2
  181. package/src/dispatcher/server-action-dispatcher.ts +39 -31
  182. package/src/index.ts +1 -0
  183. package/src/launcher/abstract-hydranium-glsp-diagram-module.ts +79 -91
  184. package/src/launcher/drop-notifications-to-gone-client.ts +47 -0
  185. package/src/launcher/glsp-app-module.ts +1 -2
  186. package/src/launcher/glsp-connection-logger.ts +27 -0
  187. package/src/launcher/glsp-server-overrides.ts +27 -11
  188. package/src/launcher/hydranium-glsp-server.ts +153 -23
  189. package/src/launcher/index.ts +5 -4
  190. package/src/logging/glsp-client-logger.ts +16 -9
  191. package/src/messages/index.ts +16 -6
  192. package/src/node/start-glsp-server.ts +133 -40
  193. package/src/state/abstract-hydranium-glsp-state.ts +609 -154
  194. package/src/state/diagram-status.ts +41 -0
  195. package/src/state/full-text-hydranium-glsp-state.ts +46 -18
  196. package/src/state/hydranium-glsp-index.ts +85 -41
  197. package/src/state/hydranium-shared-core-services.ts +16 -29
  198. package/src/state/index.ts +1 -0
  199. package/src/state/reconcile-source-model-write.ts +51 -53
  200. package/src/state/reconciling-multi-document-glsp-state.ts +276 -102
  201. package/src/state/reconciling-transfer-hydranium-glsp-state.ts +67 -45
  202. package/src/status/diagram-status-reporter.ts +106 -0
  203. package/src/status/hydranium-glsp-request-model-action-handler.ts +43 -0
  204. package/src/status/hydranium-glsp-set-edit-mode-action-handler.ts +38 -0
  205. package/src/status/index.ts +12 -0
  206. package/src/storage/hydranium-glsp-request-save-model-action-handler.ts +40 -0
  207. package/src/storage/hydranium-glsp-storage.ts +647 -223
  208. package/src/storage/index.ts +2 -1
  209. package/src/storage/save-delivery-policy.ts +49 -0
  210. package/src/submission/hydranium-glsp-submission-handler.ts +91 -5
  211. package/src/testing/glsp-harness.ts +67 -25
  212. package/src/validation/diagnostic-markers.ts +3 -3
  213. package/src/validation/hydranium-glsp-model-validator.ts +4 -5
  214. package/lib/storage/save-conflict-policy.d.ts +0 -56
  215. package/lib/storage/save-conflict-policy.d.ts.map +0 -1
  216. package/lib/storage/save-conflict-policy.js.map +0 -1
  217. package/src/storage/save-conflict-policy.ts +0 -52
@@ -0,0 +1,41 @@
1
+ /********************************************************************************
2
+ * Copyright (c) 2026 CrossBreeze, EclipseSource and others.
3
+ *
4
+ * This program and the accompanying materials are made available under the
5
+ * terms of the MIT License which is available in the project root.
6
+ *
7
+ * SPDX-License-Identifier: MIT
8
+ ********************************************************************************/
9
+
10
+ import type { ProposalString, SeverityLevel } from '@eclipse-glsp/server';
11
+
12
+ /**
13
+ * Why a diagram is in a state its client has to show: the key an entry is set
14
+ * and withdrawn under in {@link AbstractHydraniumGlspState.setStatus}.
15
+ *
16
+ * Open, as GLSP's `EditMode` is: an adopter keys its own statuses with any
17
+ * other string. Each writer withdraws only its own key, so no writer clears a
18
+ * status another one still holds.
19
+ */
20
+ export const DiagramStatus = {
21
+ /** The document does not parse, so the canvas refuses edits until it does. */
22
+ PARSE_ERROR: 'parse-error',
23
+ /** Live validation of the model is running. */
24
+ VALIDATION: 'validation',
25
+ /** The source model is loading. */
26
+ MODEL_LOAD: 'model-load',
27
+ /** The client asked for the diagram to be read-only. */
28
+ CLIENT_REQUEST: 'client-request'
29
+ } as const;
30
+
31
+ export type DiagramStatus = ProposalString<(typeof DiagramStatus)[keyof typeof DiagramStatus]>;
32
+
33
+ /** What an active {@link DiagramStatus} does to the diagram. */
34
+ export interface DiagramStatusEntry {
35
+ /** Shown in the client's status overlay; omitted when the status has nothing to say. */
36
+ readonly message?: string;
37
+ /** Picks which message is shown when several statuses carry one; the most recently set wins a tie. */
38
+ readonly severity?: Exclude<SeverityLevel, 'NONE' | 'OK'>;
39
+ /** Blocks editing while the status is active. */
40
+ readonly readonly?: boolean;
41
+ }
@@ -11,7 +11,7 @@ import { type JsonModelState } from '@eclipse-glsp/server';
11
11
  import { injectable } from 'inversify';
12
12
  import { type AstNode } from '@hydranium/langium';
13
13
  import { URI } from '@hydranium/langium';
14
- import { isPromiseLike, type MaybePromise } from '@hydranium/protocol';
14
+ import { type BaseVersion, isConflictError, isPromiseLike, type MaybePromise } from '@hydranium/protocol';
15
15
  import { AbstractHydraniumGlspState } from './abstract-hydranium-glsp-state.js';
16
16
 
17
17
  /** Whole-document-text source model: the serialised text of the source root. */
@@ -30,15 +30,17 @@ export interface FullTextSourceModel {
30
30
  * nothing beyond narrowing `TRoot` and their index:
31
31
  * - {@link sourceModel} serialises the source root through the per-URI
32
32
  * `serializer.Serializer` reached via `sharedServices.ServiceRegistry`
33
- * (so multi-grammar workspaces route to the right serializer). `MaybePromise`
34
- * because serialization may be async; the sync fast path is preserved.
35
- * - {@link updateSourceModel} pushes the text back through
36
- * `ModelService.update` (which accepts a raw text payload) and captures the
37
- * re-parsed root.
33
+ * (so multi-grammar workspaces route to the right serializer), keeping the
34
+ * document's comments. `MaybePromise` because serialization may be async;
35
+ * the sync fast path is preserved.
36
+ * - {@link updateSourceModel} pushes the text back through the diagram's
37
+ * session (`update` accepts a raw text payload) and captures the re-parsed
38
+ * root, gated on the caller's `baseVersion`; it throws without a session.
38
39
  *
39
- * No `baseline` / conflict reconcile: a whole-document model has exactly one
40
+ * No `base` / conflict reconcile: a whole-document model has exactly one
40
41
  * field, so every concurrent edit is a same-field collision and undo / redo
41
- * degrade to drop-on-divergence. Adopters that need field-level undo use
42
+ * degrade to drop-on-divergence, unless the text already holds what they
43
+ * write. Adopters that need field-level undo use
42
44
  * `ReconcilingTransferHydraniumGlspState` instead.
43
45
  */
44
46
  @injectable()
@@ -46,18 +48,44 @@ export class FullTextHydraniumGlspState<TRoot extends AstNode>
46
48
  extends AbstractHydraniumGlspState<TRoot, FullTextSourceModel>
47
49
  implements JsonModelState<FullTextSourceModel>
48
50
  {
51
+ /**
52
+ * The source root serialised, with the comments of the built document the
53
+ * captured root was parsed from re-attached through the language's trivia
54
+ * service, as a structured write's text gets them. The serializer emits
55
+ * none, so without them every write, undo and redo would delete them.
56
+ */
49
57
  get sourceModel(): MaybePromise<FullTextSourceModel> {
50
- const serializer = this.sharedServices.ServiceRegistry.getServices(URI.parse(this._sourceUri)).serializer.Serializer;
51
- const text = serializer.serializeAst(this._sourceRoot);
52
- return isPromiseLike(text) ? text.then(value => ({ text: value })) : { text };
58
+ const uri = URI.parse(this._sourceUri);
59
+ const services = this.sharedServices.ServiceRegistry.getServices(uri);
60
+ const trivia = services.trivia?.TriviaService;
61
+ const document = this._sourceRoot.$document;
62
+ const extracted = trivia !== undefined && document !== undefined ? trivia.extract(document) : undefined;
63
+ const withTrivia = (text: string): FullTextSourceModel => ({
64
+ text: trivia !== undefined && extracted !== undefined ? trivia.apply(text, extracted, uri) : text
65
+ });
66
+ const text = services.serializer.Serializer.serializeAst(this.sourceRoot);
67
+ return isPromiseLike(text) ? text.then(withTrivia) : withTrivia(text);
53
68
  }
54
69
 
55
- async updateSourceModel(model: FullTextSourceModel): Promise<void> {
56
- const document = await this.sharedServices.model.ModelService.update({
57
- uri: this._sourceUri,
58
- model: model.text,
59
- clientId: this.clientId
60
- });
61
- this.setSourceRoot(this._sourceUri, document.root as TRoot);
70
+ /**
71
+ * A write that conflicts while the store already holds `model.text`, as
72
+ * when an undo's side effect brought the text to the undo's result, is
73
+ * taken as applied; any other conflict is thrown.
74
+ */
75
+ async updateSourceModel(model: FullTextSourceModel, baseVersion: BaseVersion = this.baseVersion): Promise<void> {
76
+ let document: { root: AstNode };
77
+ try {
78
+ document = await this.requireModelSession().update({ uri: this._sourceUri, model: model.text, baseVersion });
79
+ } catch (error: unknown) {
80
+ if (isConflictError(error) && this.sharedServices.workspace.TextDocuments.get(this._sourceUri)?.getText() === model.text) {
81
+ this.logger.debug(
82
+ `updateSourceModel already applied (model v${error.baseVersion} / text v${error.actualVersion}); the document already holds the diagram edit`
83
+ );
84
+ this.captureBuiltRoot();
85
+ return;
86
+ }
87
+ throw error;
88
+ }
89
+ this.captureWrittenRoot(document.root as TRoot);
62
90
  }
63
91
  }
@@ -8,10 +8,11 @@
8
8
  ********************************************************************************/
9
9
 
10
10
  import { type GModelElement, GModelIndex } from '@eclipse-glsp/server';
11
- import { inject, injectable, optional } from 'inversify';
11
+ import { inject, injectable } from 'inversify';
12
12
  import { type AstNode, AstUtils, type URI } from '@hydranium/langium';
13
- import * as uuid from 'uuid';
14
13
  import { type ElementKeyProvider, type ServerLanguageServices, type ServerSharedServices } from '@hydranium/core';
14
+ import { randomUuid } from '@hydranium/protocol';
15
+ import { workingUriOfCopy } from '../command/hydranium-glsp-operation-command.js';
15
16
  import { HydraniumTypes } from './hydranium-shared-core-services.js';
16
17
 
17
18
  /**
@@ -46,34 +47,24 @@ export class HydraniumGlspIndex extends GModelIndex {
46
47
 
47
48
  protected idToSemanticNode = new Map<string, AstNode>();
48
49
 
50
+ /**
51
+ * The entries {@link indexSemanticElement} added outside the walk, by id.
52
+ * Kept across {@link reindexSemanticElements}, which walks containment
53
+ * only and so cannot rebuild them, and moved with {@link remapSemanticAliases}.
54
+ */
55
+ protected semanticAliases = new Map<string, AstNode>();
56
+
57
+ /** Whether {@link reindexSemanticElements} is walking, so an entry it adds is not an alias. */
58
+ protected walking = false;
59
+
49
60
  /**
50
61
  * The language services of the diagram's OWN document, captured from the
51
62
  * source root in {@link indexSourceRoot}. Used only as the fallback for a
52
63
  * node that routes nowhere — a synthetic or freshly-built node with no
53
64
  * `$document` yet, which a GModel factory legitimately hands us mid-edit.
54
- *
55
- * Captured rather than injected because it is the language the loaded
56
- * document ACTUALLY routes to, which is not necessarily the one the diagram
57
- * module declared — precisely the drift
58
- * `AbstractHydraniumGlspState.checkDeclaredLanguage` warns about. (Not
59
- * a cost argument: {@link HydraniumTypes}.DiagramLanguage is bound
60
- * `inSingletonScope`, so injecting it would resolve once per session too.)
61
65
  */
62
66
  protected diagramLanguage?: ServerLanguageServices;
63
67
 
64
- /**
65
- * The grammar the diagram module declared, as the last resort behind
66
- * {@link diagramLanguage}.
67
- *
68
- * Order-independent where the captured one is not: GLSP builds every
69
- * operation handler at `InitializeClientSession`, so a document-less node
70
- * can reach {@link createId} before the first {@link indexSourceRoot} — and
71
- * with no language at all that mints an unstable `fallback_<uuid>` where a
72
- * stable positional key was available. `@optional()` so a harness that binds
73
- * no diagram module still resolves.
74
- */
75
- @inject(HydraniumTypes.DiagramLanguage) @optional() protected readonly declaredLanguage?: ServerLanguageServices;
76
-
77
68
  /**
78
69
  * Reverse map: the *stable id* of the AST element a GModel element
79
70
  * represents → the GModel id(s) that render it. Multi-valued because one
@@ -140,8 +131,8 @@ export class HydraniumGlspIndex extends GModelIndex {
140
131
 
141
132
  /**
142
133
  * The {@link ElementKeyProvider} of the language owning `node`'s document,
143
- * falling back to the language the diagram document routes to and then to
144
- * the one its module declared, for a node that routes nowhere.
134
+ * falling back to the language the diagram document routes to for a node
135
+ * that routes nowhere.
145
136
  *
146
137
  * **Resolved per node on the cross-document entry points.** This index is
147
138
  * deliberately cross-document — {@link registerElementId} and
@@ -155,13 +146,24 @@ export class HydraniumGlspIndex extends GModelIndex {
155
146
  * {@link indexSourceRoot} needs no per-node lookup and does not pay for one.
156
147
  */
157
148
  protected elementKeyProviderFor(node?: AstNode): ElementKeyProvider | undefined {
158
- const language = this.sharedServices.ServiceRegistry.getServicesFor(node) ?? this.diagramLanguage ?? this.declaredLanguage;
159
- return language?.references.ElementKeyProvider;
149
+ return this.languageServicesFor(node)?.references.ElementKeyProvider;
150
+ }
151
+
152
+ /**
153
+ * The language services owning `node`, falling back to the diagram's
154
+ * language for a node that routes nowhere. A per-language lookup an index
155
+ * subclass adds goes through this, so an operation's copy of a node from
156
+ * another document reaches that document's language, as its built node does.
157
+ */
158
+ languageServicesFor(node?: AstNode): ServerLanguageServices | undefined {
159
+ // A copy node has no `$document`; it routes by its working root's URI.
160
+ const route = node === undefined ? undefined : workingUriOfCopy(node);
161
+ return this.sharedServices.ServiceRegistry.getServicesFor(route ?? node) ?? this.diagramLanguage;
160
162
  }
161
163
 
162
164
  /** Return the GModel id assigned to `node`, falling back to a fresh `fallback_<uuid>`. */
163
165
  createId(node?: AstNode): string {
164
- return this.findId(node, () => 'fallback_' + uuid.v4());
166
+ return this.findId(node, () => 'fallback_' + randomUuid());
165
167
  }
166
168
 
167
169
  /**
@@ -186,14 +188,31 @@ export class HydraniumGlspIndex extends GModelIndex {
186
188
  }
187
189
 
188
190
  /**
189
- * Walk the source root and index every reachable AST node with a resolvable
190
- * id.
191
+ * Index `root` as a new source root: forget what {@link registerElementId}
192
+ * and {@link indexSemanticElement} recorded, seed the diagram document as
193
+ * rendered, and index the nodes through {@link reindexSemanticElements}.
194
+ */
195
+ indexSourceRoot(root: AstNode, uri?: URI | string): void {
196
+ this.semanticAliases.clear();
197
+ this.elementToIds.clear();
198
+ this.renderedDocUris.clear();
199
+ this.addRenderedDocUri(root);
200
+ this.reindexSemanticElements(root, uri);
201
+ }
202
+
203
+ /**
204
+ * Index the nodes of `root` by id, keeping what {@link registerElementId}
205
+ * and, outside the walk, {@link indexSemanticElement} recorded. For a root
206
+ * carrying the same ids as the one indexed, such as a working copy: those
207
+ * are made only when the GModel is built, and clearing them leaves
208
+ * diagnostics with no marker and an alias resolving to nothing until the
209
+ * next build. An alias still names its node; move it with
210
+ * {@link remapSemanticAliases}.
191
211
  *
192
- * `uri` is the diagram document's URI, supplied by
193
- * `AbstractHydraniumGlspState.setSourceRoot`, and is used only to
194
- * resolve the diagram's language when `root` carries no `$document` —
195
- * always the case for a synthesised root, and the more authoritative signal
196
- * either way since it is what the caller declared it was loading.
212
+ * `uri` is the diagram document's URI and is used only to resolve the
213
+ * diagram's language when `root` carries no `$document` — always the case
214
+ * for a synthesised root or a working copy, and the more authoritative
215
+ * signal either way since it is what the caller declared it was loading.
197
216
  *
198
217
  * The walk resolves the key provider ONCE and threads it: `streamAllContents`
199
218
  * is containment-only, so every node it yields has `root` as its root node
@@ -202,15 +221,37 @@ export class HydraniumGlspIndex extends GModelIndex {
202
221
  * languageId, file name, extension — for an answer already in hand, on every
203
222
  * diagram open and every source-root refresh.
204
223
  */
205
- indexSourceRoot(root: AstNode, uri?: URI | string): void {
224
+ reindexSemanticElements(root: AstNode, uri?: URI | string): void {
206
225
  this.idToSemanticNode.clear();
207
- this.elementToIds.clear();
208
- this.renderedDocUris.clear();
209
226
  const registry = this.sharedServices.ServiceRegistry;
210
227
  this.diagramLanguage = registry.getServicesFor(root) ?? registry.getServicesFor(uri);
211
- this.addRenderedDocUri(root);
212
228
  const keyProvider = this.elementKeyProviderFor(root);
213
- AstUtils.streamAllContents(root).forEach(node => this.indexAstNode(node, keyProvider));
229
+ this.walking = true;
230
+ try {
231
+ AstUtils.streamAllContents(root).forEach(node => this.indexAstNode(node, keyProvider));
232
+ } finally {
233
+ this.walking = false;
234
+ }
235
+ for (const [id, node] of this.semanticAliases) {
236
+ this.idToSemanticNode.set(id, node);
237
+ }
238
+ }
239
+
240
+ /**
241
+ * Point every alias {@link indexSemanticElement} added at `map(node)`, or
242
+ * drop it where that is `undefined`: the node it named is gone.
243
+ */
244
+ remapSemanticAliases(map: (node: AstNode) => AstNode | undefined): void {
245
+ for (const [id, node] of [...this.semanticAliases]) {
246
+ const mapped = map(node);
247
+ if (mapped === undefined) {
248
+ this.semanticAliases.delete(id);
249
+ this.idToSemanticNode.delete(id);
250
+ } else {
251
+ this.semanticAliases.set(id, mapped);
252
+ this.idToSemanticNode.set(id, mapped);
253
+ }
254
+ }
214
255
  }
215
256
 
216
257
  /**
@@ -242,7 +283,7 @@ export class HydraniumGlspIndex extends GModelIndex {
242
283
 
243
284
  /** Record `node`'s document uri (if it has one) as contributing rendered elements. */
244
285
  protected addRenderedDocUri(node: AstNode): void {
245
- const uri = AstUtils.findRootNode(node).$document?.uri.toString();
286
+ const uri = AstUtils.findRootNode(node).$document?.uri.toString() ?? workingUriOfCopy(node);
246
287
  if (uri !== undefined) {
247
288
  this.renderedDocUris.add(uri);
248
289
  }
@@ -266,6 +307,9 @@ export class HydraniumGlspIndex extends GModelIndex {
266
307
  /** Insert an AST node under `id` directly — for adopters that need to seed entries outside the standard walk. */
267
308
  indexSemanticElement<T extends AstNode>(id: string, element: T): void {
268
309
  this.idToSemanticNode.set(id, element);
310
+ if (!this.walking) {
311
+ this.semanticAliases.set(id, element);
312
+ }
269
313
  }
270
314
 
271
315
  /** Look up the AST node previously indexed under `id`. */
@@ -329,7 +373,7 @@ export class HydraniumGlspIndex extends GModelIndex {
329
373
  */
330
374
  protected override doIndex(element: GModelElement): void {
331
375
  if (this.idToElement.has(element.id)) {
332
- this.sharedServices.Logger.for('Index').error('Duplicate element id in graph: ' + element.id);
376
+ this.sharedServices.Logger.for('Index').warn('Skipping duplicate element id in graph: ' + element.id);
333
377
  return;
334
378
  }
335
379
  super.doIndex(element);
@@ -7,7 +7,7 @@
7
7
  * SPDX-License-Identifier: MIT
8
8
  ********************************************************************************/
9
9
 
10
- import type { ServerLanguageServices, ServerSharedServices } from '@hydranium/core';
10
+ import type { ServerSharedServices } from '@hydranium/core';
11
11
  import type { ConflictResolver, Tracer } from '@hydranium/protocol';
12
12
  import { serviceIdentifier } from '../util/service-identifier.js';
13
13
 
@@ -19,37 +19,23 @@ import { serviceIdentifier } from '../util/service-identifier.js';
19
19
  * framework hands around via plain constructor arguments.
20
20
  *
21
21
  * Every token here is a service whose value is **fixed for the container tier
22
- * that binds it** — process-wide shared services, a per-session conflict
23
- * policy, the grammar a diagram type edits. That is the whole membership rule,
24
- * and it is why the registry is short: see below for what deliberately does
25
- * NOT get a token.
22
+ * that binds it**: the shared services and the conflict policy for the
23
+ * process, the tracer for each request. That is the whole membership rule, and
24
+ * it is why the registry is short: see below for what deliberately does NOT
25
+ * get a token.
26
26
  *
27
27
  * Grouped under one object (rather than top-level `Hydranium<Role>` consts)
28
28
  * so the token identity is decoupled from the implementation class name.
29
29
  * Mirrors GLSP's own `TYPES` registry idiom.
30
30
  *
31
- * Bound by the adopter from their services trees — the app-tier ones directly,
32
- * the session-tier language by declaring a grammar on the diagram module.
33
- * Inject with the real service type as the field type.
34
- *
35
- * **Split by tier, because GLSP has no language tier.** GLSP's containers are
36
- * app (per process) -> server (per connection) -> session (per open diagram);
37
- * Langium's are shared (per process) -> language (per grammar, routed by URI).
38
- * Only the outermost pair line up. A per-language service therefore CANNOT be
39
- * bound at the app tier — that container is built before any document exists,
40
- * so it can only ever hold one grammar's services. The tokens below are split
41
- * accordingly:
31
+ * `SharedCoreServices` and `ConflictResolver` are bound by
32
+ * `HydraniumGlspAppModule`, `Tracer` by the launchers. Inject each with the
33
+ * real service type as the field type.
42
34
  *
43
35
  * - **SharedCoreServices** — the framework's Langium-style shared services
44
36
  * tree ({@link ServerSharedServices}); the services-tree root that state /
45
37
  * index classes inject. Bound once at the app tier. Carries
46
38
  * `ServiceRegistry`, which is how everything below reaches a language.
47
- * - **DiagramLanguage** — the {@link ServerLanguageServices} of the grammar a
48
- * diagram type edits, bound at the SESSION tier by
49
- * `AbstractHydraniumGlspDiagramModule` from its declared `declareLanguage()`.
50
- * Static (a diagram type has one grammar), so it is safe for the eagerly
51
- * constructed handlers GLSP builds at `InitializeClientSession`. Read it
52
- * through `AbstractHydraniumGlspState.diagramLanguage`.
53
39
  * - **ConflictResolver** — the policy the GLSP state consults when a write
54
40
  * (forward-write, undo, redo, save) races a foreign edit.
55
41
  * `HydraniumGlspAppModule` binds it to `options.conflictResolver`
@@ -62,8 +48,14 @@ import { serviceIdentifier } from '../util/service-identifier.js';
62
48
  * component — no manual `for(...)`. The non-injectable recording command,
63
49
  * which can't inject, borrows the state's `tracer` getter instead.
64
50
  *
65
- * **No per-language SERVICE has a token — only the language does.** There is
66
- * no `ElementKeyProvider`, `NameProvider`, `ScopeProvider` or
51
+ * **No language and no per-language service has a token, because GLSP has no
52
+ * language tier.** GLSP's containers are app (per process) -> server (per
53
+ * connection) -> session (per open diagram); Langium's are shared (per
54
+ * process) -> language (per grammar, routed by URI). Only the outermost pair
55
+ * line up. The app container is built before any document exists, and a
56
+ * session's handlers before its document is loaded, so neither knows the
57
+ * diagram's language: the one its loaded document routes to. Nor is there an
58
+ * `ElementKeyProvider`, `NameProvider`, `ScopeProvider` or
67
59
  * `CandidateProvider` symbol, because every one of them is keyed by a document
68
60
  * that the container cannot know: the target node's, for naming and keying;
69
61
  * the one a reference is WRITTEN in, for scope and candidates. A GLSP
@@ -79,14 +71,9 @@ import { serviceIdentifier } from '../util/service-identifier.js';
79
71
  * reference written on the canvas, `modelState.languageServicesFor(node)` for
80
72
  * anything reached through a reference. `ServiceRegistry.getServicesFor(target)`
81
73
  * serves the same purpose where no model state is at hand.
82
- *
83
- * The app-tier tokens are bound by `HydraniumGlspAppModule`, the
84
- * session-tier one by `AbstractHydraniumGlspDiagramModule`. A multi-grammar
85
- * adopter declares a language per diagram module and needs nothing else.
86
74
  */
87
75
  export const HydraniumTypes = {
88
76
  SharedCoreServices: serviceIdentifier<ServerSharedServices>('HydraniumSharedCoreServices'),
89
- DiagramLanguage: serviceIdentifier<ServerLanguageServices>('HydraniumDiagramLanguage'),
90
77
  ConflictResolver: serviceIdentifier<ConflictResolver>('HydraniumConflictResolver'),
91
78
  Tracer: serviceIdentifier<Tracer>('HydraniumTracer')
92
79
  } as const;
@@ -15,3 +15,4 @@ export * from './reconciling-multi-document-glsp-state.js';
15
15
  export * from './full-text-hydranium-glsp-state.js';
16
16
  export * from './hydranium-shared-core-services.js';
17
17
  export * from './model-ready-timeout-error.js';
18
+ export * from './diagram-status.js';
@@ -7,71 +7,69 @@
7
7
  * SPDX-License-Identifier: MIT
8
8
  ********************************************************************************/
9
9
 
10
- import { type ConflictResolver, type Logger, isConflictError } from '@hydranium/protocol';
10
+ import { type BaseVersion, type Logger, type ReconcileWriteHooks, reconcileWrite } from '@hydranium/protocol';
11
11
 
12
- /**
13
- * The I/O and policy a {@link reconcileSourceModelWrite} call needs. Deliberately
14
- * a plain record rather than a base class: the states that use it differ in what
15
- * "persist" and "project" mean but not at all in how a conflict is handled, and
16
- * pulling the orchestration out is what keeps that second half from being
17
- * written once per state.
18
- */
19
- export interface SourceModelWriteHooks<TModel> {
20
- /**
21
- * Write `model` and capture the resulting state. Called with a `baseVersion`
22
- * on the first attempt (opting into the `ConflictError` gate) and without one
23
- * on the merged / forced retries, where the point is to land the write.
24
- */
25
- persist(model: TModel, baseVersion?: number): Promise<void>;
26
- /** Current server-side projection, or `undefined` when unavailable. */
27
- refetch(): Promise<TModel | undefined>;
28
- /** Last in-sync projection the user's intent is measured against. */
29
- readonly baseline: TModel | undefined;
30
- readonly conflictResolver: ConflictResolver;
12
+ /** The I/O and policy a {@link reconcileSourceModelWrite} call needs. */
13
+ export interface SourceModelWriteHooks<TModel> extends Omit<ReconcileWriteHooks<TModel>, 'onUnavailable'> {
31
14
  readonly logger: Logger;
32
15
  /** Invoked when the edit is dropped, so the caller can resync its own state. */
33
16
  onConflictDropped(): void;
17
+ /** Invoked when the document already held the edit, so the caller can take its built root. */
18
+ onAlreadyApplied(): void;
34
19
  }
35
20
 
36
21
  /**
37
- * Persist `model`, and on a `ConflictError` reconcile the user's intent against
38
- * the current server state via the injected policy and act on the outcome.
22
+ * {@link reconcileWrite} for a diagram edit: logs the outcome it ends, and
23
+ * resyncs through `onConflictDropped` when the edit is dropped, through
24
+ * `onAlreadyApplied` when the document already held it.
39
25
  *
40
- * One declarative policy shared by forward-write, undo and redo: force =
41
- * last-writer-wins, reconciling = field-level merge. A non-conflict error is
42
- * re-thrown untouched — this function only knows how to handle the specific
43
- * failure of "the based-on version was superseded".
26
+ * A conflict whose refetch is unavailable, the current text not readable to
27
+ * reconcile against, throws the `ConflictError` the write raised: forcing the
28
+ * edit would overwrite text the diagram never saw. An operation then rolls
29
+ * back and an undo or redo fails, as for any write that throws.
44
30
  */
45
31
  export async function reconcileSourceModelWrite<TModel extends object>(
46
32
  model: TModel,
47
- version: number | undefined,
33
+ baseVersion: BaseVersion,
48
34
  hooks: SourceModelWriteHooks<TModel>
49
35
  ): Promise<void> {
50
- try {
51
- await hooks.persist(model, version);
52
- } catch (err) {
53
- if (!isConflictError(err)) {
54
- throw err;
55
- }
56
- const outcome = await hooks.conflictResolver.resolve(hooks.baseline ?? model, model, () => hooks.refetch());
57
- switch (outcome.status) {
58
- case 'merged':
59
- await hooks.persist(outcome.merged);
60
- return;
61
- case 'no-op':
62
- hooks.logger.debug(`updateSourceModel no-op (v${err.expected} → v${err.actual}); already in sync`);
63
- return;
64
- case 'conflict':
65
- hooks.logger.warn(
66
- `updateSourceModel conflict (v${err.expected} → v${err.actual}); dropping the diagram edit — ` +
67
- 'a foreign writer changed the same field'
68
- );
69
- hooks.onConflictDropped();
70
- return;
71
- case 'unavailable':
72
- hooks.logger.warn(`updateSourceModel refetch unavailable (v${err.expected} → v${err.actual}); forcing without version`);
73
- await hooks.persist(model);
74
- return;
75
- }
36
+ const outcome = await reconcileWrite(model, baseVersion, {
37
+ persist: (candidate, candidateBaseVersion) => hooks.persist(candidate, candidateBaseVersion),
38
+ refetch: () => hooks.refetch(),
39
+ base: hooks.base,
40
+ conflictResolver: hooks.conflictResolver,
41
+ maxWrites: hooks.maxWrites
42
+ });
43
+ if (outcome.status === 'persisted') {
44
+ return;
45
+ }
46
+ const { conflict, writes } = outcome;
47
+ const versions = `model v${conflict.baseVersion} / text v${conflict.actualVersion}`;
48
+ if (outcome.status === 'unavailable') {
49
+ // The conflict carries both versions to whoever ends the failure, which logs it; why it was not reconciled is diagnosis.
50
+ hooks.logger.debug(`updateSourceModel refetch unavailable (${versions}); the diagram edit fails rather than overwrite unread text`);
51
+ throw conflict;
52
+ }
53
+ switch (outcome.status) {
54
+ case 'merged':
55
+ hooks.logger.debug(`updateSourceModel merged (${versions}); landed on write ${writes}`);
56
+ return;
57
+ case 'already-applied':
58
+ hooks.logger.debug(`updateSourceModel already applied (${versions}); the document already holds the diagram edit`);
59
+ hooks.onAlreadyApplied();
60
+ return;
61
+ case 'unchanged':
62
+ hooks.logger.debug(`updateSourceModel no-op (${versions}); the diagram is behind, its update catches it up`);
63
+ return;
64
+ case 'conflict':
65
+ hooks.logger.warn(`updateSourceModel conflict (${versions}); dropping the diagram edit — a foreign writer changed the same field`);
66
+ hooks.onConflictDropped();
67
+ return;
68
+ case 'dropped':
69
+ hooks.logger.warn(
70
+ `updateSourceModel still conflicting after ${writes} ${writes === 1 ? 'write' : 'writes'} (${versions}); dropping the diagram edit`
71
+ );
72
+ hooks.onConflictDropped();
73
+ return;
76
74
  }
77
75
  }