@hydranium/glsp-server 1.0.0-next.10

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 (208) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +97 -0
  3. package/lib/browser/index.d.ts +10 -0
  4. package/lib/browser/index.d.ts.map +1 -0
  5. package/lib/browser/index.js +15 -0
  6. package/lib/browser/index.js.map +1 -0
  7. package/lib/browser/start-glsp-server-in-worker.d.ts +121 -0
  8. package/lib/browser/start-glsp-server-in-worker.d.ts.map +1 -0
  9. package/lib/browser/start-glsp-server-in-worker.js +102 -0
  10. package/lib/browser/start-glsp-server-in-worker.js.map +1 -0
  11. package/lib/command/hydranium-glsp-recording-command.d.ts +132 -0
  12. package/lib/command/hydranium-glsp-recording-command.d.ts.map +1 -0
  13. package/lib/command/hydranium-glsp-recording-command.js +159 -0
  14. package/lib/command/hydranium-glsp-recording-command.js.map +1 -0
  15. package/lib/command/index.d.ts +10 -0
  16. package/lib/command/index.d.ts.map +1 -0
  17. package/lib/command/index.js +10 -0
  18. package/lib/command/index.js.map +1 -0
  19. package/lib/computed-bounds/hydranium-glsp-computed-bounds-action-handler.d.ts +61 -0
  20. package/lib/computed-bounds/hydranium-glsp-computed-bounds-action-handler.d.ts.map +1 -0
  21. package/lib/computed-bounds/hydranium-glsp-computed-bounds-action-handler.js +93 -0
  22. package/lib/computed-bounds/hydranium-glsp-computed-bounds-action-handler.js.map +1 -0
  23. package/lib/computed-bounds/index.d.ts +10 -0
  24. package/lib/computed-bounds/index.d.ts.map +1 -0
  25. package/lib/computed-bounds/index.js +10 -0
  26. package/lib/computed-bounds/index.js.map +1 -0
  27. package/lib/dispatcher/index.d.ts +10 -0
  28. package/lib/dispatcher/index.d.ts.map +1 -0
  29. package/lib/dispatcher/index.js +10 -0
  30. package/lib/dispatcher/index.js.map +1 -0
  31. package/lib/dispatcher/server-action-dispatcher.d.ts +52 -0
  32. package/lib/dispatcher/server-action-dispatcher.d.ts.map +1 -0
  33. package/lib/dispatcher/server-action-dispatcher.js +79 -0
  34. package/lib/dispatcher/server-action-dispatcher.js.map +1 -0
  35. package/lib/index.d.ts +19 -0
  36. package/lib/index.d.ts.map +1 -0
  37. package/lib/index.js +20 -0
  38. package/lib/index.js.map +1 -0
  39. package/lib/launcher/abstract-hydranium-glsp-diagram-module.d.ts +75 -0
  40. package/lib/launcher/abstract-hydranium-glsp-diagram-module.d.ts.map +1 -0
  41. package/lib/launcher/abstract-hydranium-glsp-diagram-module.js +100 -0
  42. package/lib/launcher/abstract-hydranium-glsp-diagram-module.js.map +1 -0
  43. package/lib/launcher/glsp-app-module.d.ts +87 -0
  44. package/lib/launcher/glsp-app-module.d.ts.map +1 -0
  45. package/lib/launcher/glsp-app-module.js +71 -0
  46. package/lib/launcher/glsp-app-module.js.map +1 -0
  47. package/lib/launcher/glsp-framework-overrides.d.ts +32 -0
  48. package/lib/launcher/glsp-framework-overrides.d.ts.map +1 -0
  49. package/lib/launcher/glsp-framework-overrides.js +49 -0
  50. package/lib/launcher/glsp-framework-overrides.js.map +1 -0
  51. package/lib/launcher/glsp-server-overrides.d.ts +39 -0
  52. package/lib/launcher/glsp-server-overrides.d.ts.map +1 -0
  53. package/lib/launcher/glsp-server-overrides.js +45 -0
  54. package/lib/launcher/glsp-server-overrides.js.map +1 -0
  55. package/lib/launcher/hydranium-glsp-server.d.ts +62 -0
  56. package/lib/launcher/hydranium-glsp-server.d.ts.map +1 -0
  57. package/lib/launcher/hydranium-glsp-server.js +101 -0
  58. package/lib/launcher/hydranium-glsp-server.js.map +1 -0
  59. package/lib/launcher/index.d.ts +12 -0
  60. package/lib/launcher/index.d.ts.map +1 -0
  61. package/lib/launcher/index.js +22 -0
  62. package/lib/launcher/index.js.map +1 -0
  63. package/lib/logging/glsp-client-logger.d.ts +110 -0
  64. package/lib/logging/glsp-client-logger.d.ts.map +1 -0
  65. package/lib/logging/glsp-client-logger.js +151 -0
  66. package/lib/logging/glsp-client-logger.js.map +1 -0
  67. package/lib/logging/index.d.ts +10 -0
  68. package/lib/logging/index.d.ts.map +1 -0
  69. package/lib/logging/index.js +10 -0
  70. package/lib/logging/index.js.map +1 -0
  71. package/lib/node/index.d.ts +10 -0
  72. package/lib/node/index.d.ts.map +1 -0
  73. package/lib/node/index.js +13 -0
  74. package/lib/node/index.js.map +1 -0
  75. package/lib/node/start-glsp-server.d.ts +122 -0
  76. package/lib/node/start-glsp-server.d.ts.map +1 -0
  77. package/lib/node/start-glsp-server.js +103 -0
  78. package/lib/node/start-glsp-server.js.map +1 -0
  79. package/lib/state/abstract-hydranium-glsp-state.d.ts +333 -0
  80. package/lib/state/abstract-hydranium-glsp-state.d.ts.map +1 -0
  81. package/lib/state/abstract-hydranium-glsp-state.js +451 -0
  82. package/lib/state/abstract-hydranium-glsp-state.js.map +1 -0
  83. package/lib/state/full-text-hydranium-glsp-state.d.ts +43 -0
  84. package/lib/state/full-text-hydranium-glsp-state.d.ts.map +1 -0
  85. package/lib/state/full-text-hydranium-glsp-state.js +60 -0
  86. package/lib/state/full-text-hydranium-glsp-state.js.map +1 -0
  87. package/lib/state/hydranium-glsp-index.d.ts +220 -0
  88. package/lib/state/hydranium-glsp-index.d.ts.map +1 -0
  89. package/lib/state/hydranium-glsp-index.js +320 -0
  90. package/lib/state/hydranium-glsp-index.js.map +1 -0
  91. package/lib/state/hydranium-shared-core-services.d.ts +90 -0
  92. package/lib/state/hydranium-shared-core-services.d.ts.map +1 -0
  93. package/lib/state/hydranium-shared-core-services.js +89 -0
  94. package/lib/state/hydranium-shared-core-services.js.map +1 -0
  95. package/lib/state/index.d.ts +17 -0
  96. package/lib/state/index.d.ts.map +1 -0
  97. package/lib/state/index.js +17 -0
  98. package/lib/state/index.js.map +1 -0
  99. package/lib/state/model-ready-timeout-error.d.ts +28 -0
  100. package/lib/state/model-ready-timeout-error.d.ts.map +1 -0
  101. package/lib/state/model-ready-timeout-error.js +36 -0
  102. package/lib/state/model-ready-timeout-error.js.map +1 -0
  103. package/lib/state/reconcile-source-model-write.d.ts +43 -0
  104. package/lib/state/reconcile-source-model-write.d.ts.map +1 -0
  105. package/lib/state/reconcile-source-model-write.js +47 -0
  106. package/lib/state/reconcile-source-model-write.js.map +1 -0
  107. package/lib/state/reconciling-multi-document-glsp-state.d.ts +164 -0
  108. package/lib/state/reconciling-multi-document-glsp-state.d.ts.map +1 -0
  109. package/lib/state/reconciling-multi-document-glsp-state.js +222 -0
  110. package/lib/state/reconciling-multi-document-glsp-state.js.map +1 -0
  111. package/lib/state/reconciling-transfer-hydranium-glsp-state.d.ts +91 -0
  112. package/lib/state/reconciling-transfer-hydranium-glsp-state.d.ts.map +1 -0
  113. package/lib/state/reconciling-transfer-hydranium-glsp-state.js +129 -0
  114. package/lib/state/reconciling-transfer-hydranium-glsp-state.js.map +1 -0
  115. package/lib/storage/hydranium-glsp-storage.d.ts +348 -0
  116. package/lib/storage/hydranium-glsp-storage.d.ts.map +1 -0
  117. package/lib/storage/hydranium-glsp-storage.js +634 -0
  118. package/lib/storage/hydranium-glsp-storage.js.map +1 -0
  119. package/lib/storage/index.d.ts +11 -0
  120. package/lib/storage/index.d.ts.map +1 -0
  121. package/lib/storage/index.js +11 -0
  122. package/lib/storage/index.js.map +1 -0
  123. package/lib/storage/save-conflict-policy.d.ts +56 -0
  124. package/lib/storage/save-conflict-policy.d.ts.map +1 -0
  125. package/lib/storage/save-conflict-policy.js +19 -0
  126. package/lib/storage/save-conflict-policy.js.map +1 -0
  127. package/lib/submission/hydranium-glsp-submission-handler.d.ts +109 -0
  128. package/lib/submission/hydranium-glsp-submission-handler.d.ts.map +1 -0
  129. package/lib/submission/hydranium-glsp-submission-handler.js +157 -0
  130. package/lib/submission/hydranium-glsp-submission-handler.js.map +1 -0
  131. package/lib/submission/index.d.ts +10 -0
  132. package/lib/submission/index.d.ts.map +1 -0
  133. package/lib/submission/index.js +10 -0
  134. package/lib/submission/index.js.map +1 -0
  135. package/lib/testing/glsp-harness.d.ts +170 -0
  136. package/lib/testing/glsp-harness.d.ts.map +1 -0
  137. package/lib/testing/glsp-harness.js +231 -0
  138. package/lib/testing/glsp-harness.js.map +1 -0
  139. package/lib/testing/index.d.ts +11 -0
  140. package/lib/testing/index.d.ts.map +1 -0
  141. package/lib/testing/index.js +13 -0
  142. package/lib/testing/index.js.map +1 -0
  143. package/lib/testing/make-noop-glsp-logger.d.ts +42 -0
  144. package/lib/testing/make-noop-glsp-logger.d.ts.map +1 -0
  145. package/lib/testing/make-noop-glsp-logger.js +47 -0
  146. package/lib/testing/make-noop-glsp-logger.js.map +1 -0
  147. package/lib/util/index.d.ts +10 -0
  148. package/lib/util/index.d.ts.map +1 -0
  149. package/lib/util/index.js +10 -0
  150. package/lib/util/index.js.map +1 -0
  151. package/lib/util/service-identifier.d.ts +46 -0
  152. package/lib/util/service-identifier.d.ts.map +1 -0
  153. package/lib/util/service-identifier.js +47 -0
  154. package/lib/util/service-identifier.js.map +1 -0
  155. package/lib/validation/diagnostic-markers.d.ts +48 -0
  156. package/lib/validation/diagnostic-markers.d.ts.map +1 -0
  157. package/lib/validation/diagnostic-markers.js +68 -0
  158. package/lib/validation/diagnostic-markers.js.map +1 -0
  159. package/lib/validation/hydranium-glsp-model-validator.d.ts +51 -0
  160. package/lib/validation/hydranium-glsp-model-validator.d.ts.map +1 -0
  161. package/lib/validation/hydranium-glsp-model-validator.js +118 -0
  162. package/lib/validation/hydranium-glsp-model-validator.js.map +1 -0
  163. package/lib/validation/index.d.ts +11 -0
  164. package/lib/validation/index.d.ts.map +1 -0
  165. package/lib/validation/index.js +11 -0
  166. package/lib/validation/index.js.map +1 -0
  167. package/package.json +111 -0
  168. package/src/browser/index.ts +15 -0
  169. package/src/browser/start-glsp-server-in-worker.ts +202 -0
  170. package/src/command/hydranium-glsp-recording-command.ts +183 -0
  171. package/src/command/index.ts +10 -0
  172. package/src/computed-bounds/hydranium-glsp-computed-bounds-action-handler.ts +103 -0
  173. package/src/computed-bounds/index.ts +10 -0
  174. package/src/dispatcher/index.ts +10 -0
  175. package/src/dispatcher/server-action-dispatcher.ts +72 -0
  176. package/src/index.ts +20 -0
  177. package/src/launcher/abstract-hydranium-glsp-diagram-module.ts +117 -0
  178. package/src/launcher/glsp-app-module.ts +106 -0
  179. package/src/launcher/glsp-framework-overrides.ts +51 -0
  180. package/src/launcher/glsp-server-overrides.ts +46 -0
  181. package/src/launcher/hydranium-glsp-server.ts +104 -0
  182. package/src/launcher/index.ts +22 -0
  183. package/src/logging/glsp-client-logger.ts +184 -0
  184. package/src/logging/index.ts +10 -0
  185. package/src/node/index.ts +13 -0
  186. package/src/node/start-glsp-server.ts +214 -0
  187. package/src/state/abstract-hydranium-glsp-state.ts +503 -0
  188. package/src/state/full-text-hydranium-glsp-state.ts +63 -0
  189. package/src/state/hydranium-glsp-index.ts +337 -0
  190. package/src/state/hydranium-shared-core-services.ts +92 -0
  191. package/src/state/index.ts +17 -0
  192. package/src/state/model-ready-timeout-error.ts +33 -0
  193. package/src/state/reconcile-source-model-write.ts +77 -0
  194. package/src/state/reconciling-multi-document-glsp-state.ts +248 -0
  195. package/src/state/reconciling-transfer-hydranium-glsp-state.ts +132 -0
  196. package/src/storage/hydranium-glsp-storage.ts +660 -0
  197. package/src/storage/index.ts +11 -0
  198. package/src/storage/save-conflict-policy.ts +52 -0
  199. package/src/submission/hydranium-glsp-submission-handler.ts +170 -0
  200. package/src/submission/index.ts +10 -0
  201. package/src/testing/glsp-harness.ts +422 -0
  202. package/src/testing/index.ts +14 -0
  203. package/src/testing/make-noop-glsp-logger.ts +66 -0
  204. package/src/util/index.ts +10 -0
  205. package/src/util/service-identifier.ts +49 -0
  206. package/src/validation/diagnostic-markers.ts +93 -0
  207. package/src/validation/hydranium-glsp-model-validator.ts +105 -0
  208. package/src/validation/index.ts +11 -0
@@ -0,0 +1,248 @@
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 JsonModelState } from '@eclipse-glsp/server';
11
+ import { injectable } from 'inversify';
12
+ import { type AstNode } from '@hydranium/langium';
13
+ import { type TransferElement } from '@hydranium/protocol';
14
+ import { AbstractHydraniumGlspState } from './abstract-hydranium-glsp-state.js';
15
+ import { reconcileSourceModelWrite } from './reconcile-source-model-write.js';
16
+
17
+ /**
18
+ * Source model spanning more than one document: the primary document's
19
+ * projection plus one per registered secondary, keyed by URI.
20
+ *
21
+ * Keyed by URI rather than held as an array so a recorded undo / redo patch
22
+ * still addresses the right document when the tracked set has changed since the
23
+ * patch was recorded — an array would re-apply by index and silently write the
24
+ * wrong file. The JSON-Pointer escaping that URI keys imply is the patch
25
+ * library's job, not the adopter's.
26
+ */
27
+ export interface MultiDocumentSourceModel<TPrimary extends TransferElement = TransferElement> {
28
+ /** Projection of the primary (diagram) document — the one `sourceUri` names. */
29
+ primary: TPrimary;
30
+ /** Projection of each registered secondary document, keyed by its URI. */
31
+ secondaries: Record<string, TransferElement>;
32
+ }
33
+
34
+ /**
35
+ * Editable GLSP state for a diagram whose edits span SEVERAL Langium documents —
36
+ * a diagram file plus the semantic file it references, say, where creating a node
37
+ * on the canvas writes both.
38
+ *
39
+ * The single-document `ReconcilingTransferHydraniumGlspState` cannot express
40
+ * that: its `sourceModel`, `persist` and `refetch` are all defined over
41
+ * `sourceUri` alone, so an adopter needing a second document had to override all
42
+ * three — i.e. the entire class. This subclass keeps the same three seams and the
43
+ * same conflict handling (both share {@link reconcileSourceModelWrite}) but
44
+ * defines them over the primary plus the secondary write set registered through
45
+ * {@link AbstractHydraniumGlspState.trackSecondaryDocument}.
46
+ *
47
+ * **Writes are NOT atomic, deliberately, and the window is real.** Each document
48
+ * goes through its own `ModelService.update`, so a failure after the first write
49
+ * has landed leaves the set inconsistent on disk. There is no multi-document
50
+ * transaction to lean on: `WorkspaceLock` serialises builds but does not roll
51
+ * back, and the document store commits per URI. An adopter that cannot accept the
52
+ * window should not span documents in one operation.
53
+ *
54
+ * **What makes the window tolerable is write ORDER, and the rule is
55
+ * referenced-before-referencing** — not "secondaries first" as such. Write the
56
+ * document whose content the others point AT before the documents that point at
57
+ * it, so a partial write leaves an unreferenced element (inert, and the direction
58
+ * an integrity rule can repair) rather than a reference to something that does
59
+ * not exist (a linking error). {@link persist} implements that as
60
+ * secondaries-then-primary, which is correct for the common shape where the
61
+ * primary is the DIAGRAM and the semantics it references are secondaries: the
62
+ * user opened the diagram, so it is also the document the conflict gate should
63
+ * guard. **An adopter whose primary is the referenced document — a semantic file
64
+ * as the diagram source, with layout in a secondary — has the ordering backwards
65
+ * and must override {@link persist} to write the primary first.** Overriding it
66
+ * is the supported route; the base order is a default for the common case, not an
67
+ * invariant of the class.
68
+ *
69
+ * **The conflict gate covers the primary only.** Secondaries are written without
70
+ * a based-on version, so a concurrent foreign edit to one is overwritten rather
71
+ * than reconciled. Gating them too would need a per-document reconcile whose
72
+ * outcomes can disagree (merge one, conflict another) with no way to un-write the
73
+ * merged one — the atomicity problem again, one layer up. The captured versions
74
+ * ARE available via {@link AbstractHydraniumGlspState.capturedVersionOf}, so an
75
+ * adopter that wants a coarser check can compare before writing.
76
+ */
77
+ @injectable()
78
+ export class ReconcilingMultiDocumentGlspState<TRoot extends AstNode, TPrimary extends TransferElement = TransferElement>
79
+ extends AbstractHydraniumGlspState<TRoot, MultiDocumentSourceModel<TPrimary>>
80
+ implements JsonModelState<MultiDocumentSourceModel<TPrimary>>
81
+ {
82
+ /**
83
+ * Last in-sync projection across the whole write set, captured on every
84
+ * {@link setSourceRoot}. Same role as the single-document baseline: the state a
85
+ * forward-write conflict reconciles the user's intent against.
86
+ */
87
+ protected baseline?: MultiDocumentSourceModel<TPrimary>;
88
+
89
+ /**
90
+ * Projection of the primary plus every registered secondary, in the framework
91
+ * encoder's `'grammar'` mode (authored state only — cross-references as
92
+ * `$refText`, computed / synthetic properties excluded), so `fast-json-patch`
93
+ * diffs per field across all of them at once.
94
+ *
95
+ * A secondary that is not currently loaded is OMITTED rather than represented
96
+ * as `undefined`: an absent key produces no patch operations for that
97
+ * document, whereas an explicit `undefined` would diff as a removal and
98
+ * persist as a deletion of content the state simply could not see.
99
+ */
100
+ get sourceModel(): MultiDocumentSourceModel<TPrimary> {
101
+ const secondaries: Record<string, TransferElement> = {};
102
+ for (const uri of this.secondaryUris) {
103
+ const projection = this.projectDocument(uri);
104
+ if (projection) {
105
+ secondaries[uri] = projection;
106
+ }
107
+ }
108
+ return { primary: this.projectRoot(this._sourceRoot), secondaries };
109
+ }
110
+
111
+ override setSourceRoot(uri: string, root: TRoot): void {
112
+ super.setSourceRoot(uri, root);
113
+ this.trackWriteSet(uri);
114
+ this.baseline = this.sourceModel;
115
+ }
116
+
117
+ /**
118
+ * Register the secondary documents that belong to the primary at `uri`, via
119
+ * {@link AbstractHydraniumGlspState.trackSecondaryDocument}. Called on every
120
+ * {@link setSourceRoot}, after the primary is captured (so `sourceUri` is
121
+ * current) and BEFORE the baseline is taken (so the baseline includes them).
122
+ * Default: no secondaries.
123
+ *
124
+ * This hook exists because that ordering is a trap an adopter would otherwise
125
+ * hit silently. Registering from an overridden `setSourceRoot` *after*
126
+ * `super.setSourceRoot(...)` runs too late — the baseline has already been
127
+ * captured without the secondaries, so the first conflict reconcile measures
128
+ * the user's intent against a baseline missing half the write set and the
129
+ * secondary edits look like foreign changes. Registering *before* the super
130
+ * call is too early for a URI derived from the new primary. Overriding this
131
+ * instead removes the choice.
132
+ */
133
+ protected trackWriteSet(_uri: string): void {
134
+ // No-op by default; adopters with secondaries override.
135
+ }
136
+
137
+ /**
138
+ * Persist the whole write set, then capture the resulting primary root. On a
139
+ * `ConflictError` from the primary, reconcile via the injected policy exactly
140
+ * as the single-document state does — the orchestration is shared.
141
+ */
142
+ async updateSourceModel(model: MultiDocumentSourceModel<TPrimary>, version?: number): Promise<void> {
143
+ return reconcileSourceModelWrite<MultiDocumentSourceModel<TPrimary>>(model, version, {
144
+ persist: async (candidate, baseVersion) => {
145
+ const { root } = await this.persist(candidate, baseVersion);
146
+ this.setSourceRoot(this._sourceUri, root);
147
+ },
148
+ refetch: () => this.refetch(),
149
+ baseline: this.baseline,
150
+ conflictResolver: this.conflictResolver,
151
+ logger: this.logger,
152
+ onConflictDropped: () => this.refreshSourceRoot()
153
+ });
154
+ }
155
+
156
+ /**
157
+ * Write hook — secondaries first (ungated), primary last (gated on
158
+ * `baseVersion`). The order is the failure-mode choice documented on the
159
+ * class, not incidental: override this when the primary is the document the
160
+ * others REFERENCE, since then this order writes the references first.
161
+ */
162
+ protected async persist(model: MultiDocumentSourceModel<TPrimary>, baseVersion?: number): Promise<{ root: TRoot }> {
163
+ for (const [uri, secondary] of Object.entries(model.secondaries)) {
164
+ if (this.hasChanged(this.baseline?.secondaries[uri], secondary)) {
165
+ await this.persistSecondary(uri, secondary);
166
+ }
167
+ }
168
+ if (!this.hasChanged(this.baseline?.primary, model.primary)) {
169
+ // Nothing to write, so nothing to gate either — return the root already
170
+ // captured rather than round-tripping the document for no reason.
171
+ return { root: this._sourceRoot };
172
+ }
173
+ return this.persistPrimary(model.primary, baseVersion);
174
+ }
175
+
176
+ /**
177
+ * Whether `candidate` differs from the baseline projection of the same
178
+ * document, and therefore needs writing.
179
+ *
180
+ * **Skipping unchanged documents is correctness, not an optimisation.** A
181
+ * write goes through `ModelService.update`, which re-serializes from the AST —
182
+ * so writing a document that did not change still rewrites its text, and a
183
+ * serializer is free to normalise formatting and cannot preserve comments.
184
+ * Persisting the whole write set unconditionally therefore means a pure layout
185
+ * drag reflows the semantic file and strips its comments, which is a data
186
+ * loss the user never asked for and would struggle to attribute.
187
+ *
188
+ * Compared by serialised form. Both sides come from the same encoder walking
189
+ * the same shape, so key order is stable and a string compare is sound here;
190
+ * it is also cheap enough to run per document per write.
191
+ */
192
+ protected hasChanged(baseline: object | undefined, candidate: object): boolean {
193
+ return baseline === undefined || JSON.stringify(baseline) !== JSON.stringify(candidate);
194
+ }
195
+
196
+ /** Write the primary document, opting into the conflict gate when `baseVersion` is given. */
197
+ protected async persistPrimary(model: TPrimary, baseVersion?: number): Promise<{ root: TRoot }> {
198
+ const document = await this.sharedServices.model.ModelService.update({
199
+ uri: this._sourceUri,
200
+ model,
201
+ clientId: this.clientId,
202
+ baseVersion
203
+ });
204
+ return document as unknown as { root: TRoot };
205
+ }
206
+
207
+ /**
208
+ * Write one secondary document. Ungated by design (see the class doc); override
209
+ * alongside {@link persistPrimary} when the round-trip differs per document
210
+ * role — a layout file and a semantic file need not share a serializer.
211
+ */
212
+ protected async persistSecondary(uri: string, model: TransferElement): Promise<void> {
213
+ await this.sharedServices.model.ModelService.update({ uri, model, clientId: this.clientId });
214
+ }
215
+
216
+ /**
217
+ * Refetch hook — the current settled projection across the write set, used by
218
+ * the conflict resolver to replay the user's intent against fresh state.
219
+ * Returns `undefined` when the PRIMARY cannot be read, since a reconcile
220
+ * without it has nothing to merge into; an unreadable secondary is omitted the
221
+ * same way {@link sourceModel} omits one.
222
+ */
223
+ protected async refetch(): Promise<MultiDocumentSourceModel<TPrimary> | undefined> {
224
+ const fresh = await this.sharedServices.model.ModelService.validated(this._sourceUri).catch(() => undefined);
225
+ if (!fresh) {
226
+ return undefined;
227
+ }
228
+ const secondaries: Record<string, TransferElement> = {};
229
+ for (const uri of this.secondaryUris) {
230
+ const settled = await this.sharedServices.model.ModelService.validated(uri).catch(() => undefined);
231
+ if (settled) {
232
+ secondaries[uri] = this.projectRoot(settled.root as unknown as AstNode);
233
+ }
234
+ }
235
+ return { primary: this.projectRoot(fresh.root as unknown as AstNode) as TPrimary, secondaries };
236
+ }
237
+
238
+ /** Project a currently-loaded document's root, or `undefined` when it is not loaded. */
239
+ protected projectDocument(uri: string): TransferElement | undefined {
240
+ const root = this.sharedServices.model.ModelService.getDocument(uri)?.parseResult?.value;
241
+ return root ? this.projectRoot(root) : undefined;
242
+ }
243
+
244
+ /** Grammar-mode transfer projection of one AST root. The single projection seam. */
245
+ protected projectRoot<T extends TransferElement = TransferElement>(root: AstNode): T {
246
+ return this.sharedServices.model.TransferEncoder.toTransfer(root, 'grammar') as unknown as T;
247
+ }
248
+ }
@@ -0,0 +1,132 @@
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 JsonModelState } from '@eclipse-glsp/server';
11
+ import { injectable } from 'inversify';
12
+ import { type AstNode } from '@hydranium/langium';
13
+ import { type TransferElement } from '@hydranium/protocol';
14
+ import { AbstractHydraniumGlspState } from './abstract-hydranium-glsp-state.js';
15
+ import { reconcileSourceModelWrite } from './reconcile-source-model-write.js';
16
+
17
+ /**
18
+ * Editable GLSP state for adopters whose source model is a **structured
19
+ * transfer-model projection** round-tripped through the language
20
+ * `ModelService`. Adds the forward-write
21
+ * reconcile machinery on top of the slim {@link AbstractHydraniumGlspState} base:
22
+ *
23
+ * - {@link sourceModel} — the persisted-shape projection of the current
24
+ * source root, produced by the framework
25
+ * `TransferEncoder` in `'grammar'` mode
26
+ * (grammar-declared properties only; computed / synthetic excluded). The
27
+ * field-level shape is what lets `fast-json-patch` diff per field, so undo /
28
+ * redo and forward-write reconcile act per field instead of clobbering the
29
+ * whole document.
30
+ * - {@link baseline} — the last in-sync projection, captured on every
31
+ * {@link setSourceRoot}. A forward-write conflict reconciles the user's
32
+ * intent (baseline → attempted) against the fresh server root.
33
+ * - {@link updateSourceModel} — the concrete reconcile template: persist,
34
+ * and on a `ConflictError` consult the injected `conflictResolver` and act
35
+ * on the merged / no-op / conflict / unavailable outcome.
36
+ * - {@link persist} / {@link refetch} — the only I/O seams. Defaults route
37
+ * through `ModelService.update` / `.validated` (reachable from the base via
38
+ * `sharedServices.model`); adopters whose document round-trip differs
39
+ * override these without touching the orchestration.
40
+ *
41
+ * Adopters whose source model is whole-document text (a bare `{ text }`
42
+ * source model) or that are read-only do NOT extend this class — they extend
43
+ * {@link AbstractHydraniumGlspState} directly and supply their own `updateSourceModel`
44
+ * (or throw). The conflict-resolution *policy* (reconcile vs force) is still
45
+ * the base's injected `conflictResolver`; this class owns the forward-write
46
+ * *orchestration* that consults it.
47
+ */
48
+ @injectable()
49
+ export class ReconcilingTransferHydraniumGlspState<TRoot extends AstNode, TSourceModel extends TransferElement>
50
+ extends AbstractHydraniumGlspState<TRoot, TSourceModel>
51
+ implements JsonModelState<TSourceModel>
52
+ {
53
+ /**
54
+ * Last in-sync source-model projection, captured on every
55
+ * {@link setSourceRoot} (initial load + post-update). The baseline a
56
+ * forward-write conflict reconciles against: it stays the pre-command
57
+ * state because operation handlers mutate `_sourceRoot` in place during
58
+ * `execute` while `setSourceRoot` only re-runs once the write commits.
59
+ */
60
+ protected baseline?: TSourceModel;
61
+
62
+ /**
63
+ * Persisted-shape projection of the current source root, consumed by GLSP's
64
+ * {@link JsonModelState} read side and recorded for field-level undo / redo.
65
+ * Produced by the framework `TransferEncoder` in `'grammar'` mode
66
+ * (cross-references → `$refText`, Langium internals + computed / synthetic
67
+ * properties excluded), so the diff reflects only authored state.
68
+ * Synchronous — the encoder walks the in-memory AST without serialising.
69
+ */
70
+ get sourceModel(): TSourceModel {
71
+ return this.sharedServices.model.TransferEncoder.toTransfer(this._sourceRoot, 'grammar') as unknown as TSourceModel;
72
+ }
73
+
74
+ override setSourceRoot(uri: string, root: TRoot): void {
75
+ super.setSourceRoot(uri, root);
76
+ this.baseline = this.sourceModel;
77
+ }
78
+
79
+ /**
80
+ * Persist `model` back to the document store, then capture the resulting
81
+ * AST root. On a `ConflictError` (the based-on version was superseded),
82
+ * reconcile the user's intent against the fresh server root via the bound
83
+ * `conflictResolver` and act on the outcome — one declarative policy
84
+ * (force = last-writer-wins, reconciling = field-level merge) shared with
85
+ * undo / redo.
86
+ */
87
+ async updateSourceModel(model: TSourceModel, version?: number): Promise<void> {
88
+ // Orchestration lives in `reconcileSourceModelWrite` so the multi-document
89
+ // state gets the identical conflict handling; this method supplies only
90
+ // the single-document meaning of persist / project.
91
+ return reconcileSourceModelWrite<TSourceModel>(model, version, {
92
+ persist: async (candidate, baseVersion) => {
93
+ const { root } = await this.persist(candidate, baseVersion);
94
+ this.setSourceRoot(this._sourceUri, root);
95
+ },
96
+ refetch: () => this.refetch(),
97
+ baseline: this.baseline,
98
+ conflictResolver: this.conflictResolver,
99
+ logger: this.logger,
100
+ onConflictDropped: () => this.refreshSourceRoot()
101
+ });
102
+ }
103
+
104
+ /**
105
+ * Persist hook — the only write-side I/O. Default routes the structured
106
+ * model through `ModelService.update` (serialize → reparse), opting into
107
+ * the `ConflictError` gate when `version` is given. Adopters whose document
108
+ * round-trip differs override this; the orchestration in
109
+ * {@link updateSourceModel} is unchanged.
110
+ */
111
+ protected async persist(model: TSourceModel, baseVersion?: number): Promise<{ root: TRoot }> {
112
+ const document = await this.sharedServices.model.ModelService.update({
113
+ uri: this._sourceUri,
114
+ model,
115
+ clientId: this.clientId,
116
+ baseVersion
117
+ });
118
+ return document as unknown as { root: TRoot };
119
+ }
120
+
121
+ /**
122
+ * Refetch hook — reads the current settled server root as a persisted-shape
123
+ * projection (or `undefined` when unavailable). The `conflictResolver`
124
+ * replays the user's intent against this fresh state. Default uses
125
+ * `ModelService.validated` + the framework encoder's `'grammar'` mode;
126
+ * adopters override alongside {@link persist}.
127
+ */
128
+ protected async refetch(): Promise<TSourceModel | undefined> {
129
+ const fresh = await this.sharedServices.model.ModelService.validated(this._sourceUri).catch(() => undefined);
130
+ return fresh ? (this.sharedServices.model.TransferEncoder.toTransfer(fresh.root, 'grammar') as unknown as TSourceModel) : undefined;
131
+ }
132
+ }