@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,183 @@
1
+ /********************************************************************************
2
+ * Copyright (c) 2023-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 AnyObject, type JsonModelState, JsonRecordingCommand, type MaybePromise } from '@eclipse-glsp/server';
11
+ import { type AstNode } from '@hydranium/langium';
12
+ import { type AbstractHydraniumGlspState } from '../state/abstract-hydranium-glsp-state.js';
13
+
14
+ /**
15
+ * Source-model state shape consumed by {@link HydraniumGlspRecordingCommand}.
16
+ * The intersection ties together GLSP's read-side
17
+ * {@link JsonModelState.sourceModel} getter with the framework's typed
18
+ * `logger` and `sourceUri` accessors (used for time-labelled logging) and
19
+ * the lifted `updateSourceModel` abstract from
20
+ * {@link AbstractHydraniumGlspState.updateSourceModel}. Adopter states satisfy both
21
+ * naturally by implementing `JsonModelState<TSourceModel>` on
22
+ * their framework-state subclass.
23
+ */
24
+ export type HydraniumGlspRecordingState<TSourceModel extends AnyObject> = AbstractHydraniumGlspState<AstNode, TSourceModel> &
25
+ JsonModelState<TSourceModel>;
26
+
27
+ /**
28
+ * GLSP {@link JsonRecordingCommand} base shared by all hydranium adopters.
29
+ * Adds two things on top of GLSP's class:
30
+ *
31
+ * 1. A **time-labelled execute wrapper** so log lines record how long a
32
+ * user-facing operation takes end-to-end — including the `postChange`
33
+ * source-model update that re-parses the document. Otherwise the diff +
34
+ * patch + persist work happens silently and is hard to attribute when
35
+ * something is slow.
36
+ * 2. A typed `state` field that exposes `updateSourceModel` for the
37
+ * `postChange` bridge AND the framework `logger` for time-labelled
38
+ * logging. GLSP's base class only types `modelState: JsonModelState`,
39
+ * which lacks the framework's logger surface.
40
+ *
41
+ * **Optional undo/redo bridge hooks.** An adopter command can supply
42
+ * `undoAction` / `redoAction` callbacks invoked alongside the patch apply, to
43
+ * re-emit a side-effect the patch itself does not carry. Provided as optional
44
+ * constructor args so adopters get that composition surface without
45
+ * subclassing. Operation handlers that don't need bridge callbacks omit them.
46
+ *
47
+ * **postChange semantics.** GLSP's `JsonRecordingCommand.postChange` calls
48
+ * `modelState.updateSourceModel(newModel)` with the model only. The
49
+ * framework lift overrides `postChange` so it also threads the based-on
50
+ * version captured at command start — letting the downstream
51
+ * `ModelService.update` opt into the `ConflictError` gate. Undo / redo
52
+ * postChange calls run with version `undefined` (no gating): the user
53
+ * authored against the recorded patch, not against a specific server
54
+ * version, so re-applying it should succeed regardless of intervening
55
+ * edits. The recorded patch itself encodes the semantic intent.
56
+ */
57
+ export class HydraniumGlspRecordingCommand<TSourceModel extends AnyObject> extends JsonRecordingCommand<TSourceModel> {
58
+ declare protected modelState: HydraniumGlspRecordingState<TSourceModel>;
59
+
60
+ /**
61
+ * Based-on version captured at {@link execute} start; threaded into
62
+ * {@link postChange} so {@link AbstractHydraniumGlspState.updateSourceModel}
63
+ * sees the version the user authored against. Cleared after `execute`
64
+ * returns so undo / redo paths fall through to `updateSourceModel`
65
+ * without a version (no gating).
66
+ */
67
+ protected activeVersion?: number;
68
+
69
+ /**
70
+ * Source-model snapshots captured at {@link execute} — the state the
71
+ * command left ({@link afterSnapshot}) and the state it started from
72
+ * ({@link beforeSnapshot}). {@link undo} / {@link redo} reconcile the
73
+ * recorded transition against the *current* source model via the state's
74
+ * {@link AbstractHydraniumGlspState.conflictResolver}, so under the default
75
+ * reconciling policy a concurrent edit to another field survives and a
76
+ * same-field collision is dropped rather than clobbered.
77
+ */
78
+ protected beforeSnapshot?: TSourceModel;
79
+ protected afterSnapshot?: TSourceModel;
80
+
81
+ constructor(
82
+ modelState: HydraniumGlspRecordingState<TSourceModel>,
83
+ /** Human-readable label for the operation; appears in the timing-pair log line. */
84
+ protected readonly label: string,
85
+ doExecute: () => MaybePromise<void>,
86
+ /** Optional bridge invoked alongside undo patch application; for adopters
87
+ * that need to re-emit a side-effect the patch does not carry. */
88
+ protected readonly undoAction?: () => MaybePromise<void>,
89
+ /** Optional bridge invoked alongside redo patch application. */
90
+ protected readonly redoAction?: () => MaybePromise<void>
91
+ ) {
92
+ super(modelState, doExecute);
93
+ }
94
+
95
+ /**
96
+ * Time-label the full execute path: pre-state snapshot + `doExecute` +
97
+ * post-state snapshot + patch-derive + `postChange` (which writes back
98
+ * via {@link AbstractHydraniumGlspState.updateSourceModel}). The label combines
99
+ * the operation name with the source-uri-stamped logger.
100
+ *
101
+ * Captures {@link AbstractHydraniumGlspState.version} at start so {@link postChange}
102
+ * can thread the based-on version into `updateSourceModel`. The capture
103
+ * is `try`/`finally`-scoped so undo / redo paths invoked later do not
104
+ * see a stale captured version.
105
+ */
106
+ override async execute(): Promise<void> {
107
+ const logger = this.modelState.logger.for('HydraniumGlspRecordingCommand');
108
+ this.activeVersion = this.modelState.version;
109
+ logger.debug(`Executing '${this.label}' (based-on doc.version=v${this.activeVersion})`);
110
+ this.beforeSnapshot = this.deepClone(await this.getJsonObject());
111
+ try {
112
+ await this.modelState.tracer.for('HydraniumGlspRecordingCommand').time(`Execute command '${this.label}'`, () => super.execute());
113
+ } finally {
114
+ this.activeVersion = undefined;
115
+ }
116
+ this.afterSnapshot = this.deepClone(await this.getJsonObject());
117
+ }
118
+
119
+ /**
120
+ * Override of GLSP's {@link JsonRecordingCommand.postChange} so the
121
+ * call to {@link AbstractHydraniumGlspState.updateSourceModel} threads the
122
+ * captured based-on version alongside the new model.
123
+ *
124
+ * During {@link execute} the version is the snapshot captured at
125
+ * command start; during {@link undo} / {@link redo} it is `undefined`
126
+ * — replaying a recorded patch does not author against a specific
127
+ * server version, so no gate applies.
128
+ */
129
+ protected override postChange(newModel: TSourceModel): MaybePromise<void> {
130
+ return this.modelState.updateSourceModel(newModel, this.activeVersion);
131
+ }
132
+
133
+ /**
134
+ * Reconcile the recorded `after → before` transition against the
135
+ * *current* source model via the state's
136
+ * {@link AbstractHydraniumGlspState.conflictResolver}, invoke the optional adopter
137
+ * `undoAction`, and persist via `postChange`. Under the default reconciling
138
+ * policy the revert is replayed guarded by `test` ops, so a concurrent edit
139
+ * to another field is preserved (merged) and a same-field collision is
140
+ * dropped rather than clobbered; a force policy clobbers (last-writer-wins).
141
+ * On anything but a clean merge the undo is a logged no-op — the side-effect
142
+ * bridge and `postChange` only run on a merged result, preserving a
143
+ * "bridge before commit" order.
144
+ */
145
+ override async undo(): Promise<void> {
146
+ if (!this.undoPatch || this.beforeSnapshot === undefined || this.afterSnapshot === undefined) {
147
+ return;
148
+ }
149
+ const logger = this.modelState.logger.for('HydraniumGlspRecordingCommand');
150
+ logger.debug(`Undoing '${this.label}' at doc.version=v${this.modelState.version}`);
151
+ const outcome = await this.modelState.conflictResolver.resolve(this.afterSnapshot, this.beforeSnapshot, async () =>
152
+ this.getJsonObject()
153
+ );
154
+ if (outcome.status !== 'merged') {
155
+ logger.warn(`Undo '${this.label}' skipped (${outcome.status}); a foreign edit changed a shared field since the command ran`);
156
+ return;
157
+ }
158
+ await this.undoAction?.();
159
+ await this.postChange?.(outcome.merged);
160
+ }
161
+
162
+ /**
163
+ * Reconcile the recorded `before → after` transition against the current
164
+ * source model, invoke the optional adopter `redoAction`, and persist via
165
+ * `postChange`. Same guarded merge / drop semantics as {@link undo}.
166
+ */
167
+ override async redo(): Promise<void> {
168
+ if (!this.redoPatch || this.beforeSnapshot === undefined || this.afterSnapshot === undefined) {
169
+ return;
170
+ }
171
+ const logger = this.modelState.logger.for('HydraniumGlspRecordingCommand');
172
+ logger.debug(`Redoing '${this.label}' at doc.version=v${this.modelState.version}`);
173
+ const outcome = await this.modelState.conflictResolver.resolve(this.beforeSnapshot, this.afterSnapshot, async () =>
174
+ this.getJsonObject()
175
+ );
176
+ if (outcome.status !== 'merged') {
177
+ logger.warn(`Redo '${this.label}' skipped (${outcome.status}); a foreign edit changed a shared field since the undo`);
178
+ return;
179
+ }
180
+ await this.redoAction?.();
181
+ await this.postChange?.(outcome.merged);
182
+ }
183
+ }
@@ -0,0 +1,10 @@
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
+ export * from './hydranium-glsp-recording-command.js';
@@ -0,0 +1,103 @@
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 {
11
+ type Action,
12
+ type ComputedBoundsAction,
13
+ ComputedBoundsActionHandler,
14
+ type GModelRoot,
15
+ type MaybePromise
16
+ } from '@eclipse-glsp/server';
17
+ import { type AstNode } from '@hydranium/langium';
18
+ import { injectable } from 'inversify';
19
+ import { type AbstractHydraniumGlspState } from '../state/abstract-hydranium-glsp-state.js';
20
+ import { type HydraniumGlspSubmissionHandler } from '../submission/hydranium-glsp-submission-handler.js';
21
+
22
+ /**
23
+ * GLSP {@link ComputedBoundsActionHandler} base shared by all hydranium
24
+ * adopters. Adds handshake-aware revision-mismatch logging on top of the
25
+ * upstream handler.
26
+ *
27
+ * **Why the lift.** GLSP's base silently returns `[]` on a revision
28
+ * mismatch — benign for stale post-drag bounds (the user moved an element,
29
+ * the server-side GModel got re-rendered before the client's pre-move
30
+ * `computedBounds` arrived; dropping the stale frame is correct). But
31
+ * catastrophic during the initial `requestModel → setModel` handshake: if the
32
+ * very first `computedBounds` arrives with a revision the server has already
33
+ * bumped, the subsequent `SetModelAction` response never fires and the diagram
34
+ * stays stuck at "model loading…". Adopters need this initial-handshake case
35
+ * surfaced as a warning so the failure mode is visible in logs.
36
+ *
37
+ * **Handshake detection** uses
38
+ * {@link HydraniumGlspSubmissionHandler.hasPendingInitialRequest} — true
39
+ * while a `RequestModelAction` has been accepted but the corresponding
40
+ * `SetModelAction` has not yet been dispatched. Adopters that override
41
+ * `submitModel` flow must preserve the `hasPendingInitialRequest` semantics
42
+ * for this logging to remain accurate.
43
+ *
44
+ * **Customisation.** The class is shipped non-abstract because the default
45
+ * body fits common adopters, which bind it directly. Adopters with
46
+ * bounds-specific cleanup subclass and override `applyBounds`.
47
+ */
48
+ @injectable()
49
+ export class HydraniumGlspComputedBoundsActionHandler extends ComputedBoundsActionHandler {
50
+ declare protected readonly modelState: AbstractHydraniumGlspState<AstNode, unknown>;
51
+ declare protected readonly submissionHandler: HydraniumGlspSubmissionHandler<AstNode, unknown>;
52
+
53
+ override execute(action: ComputedBoundsAction): MaybePromise<Action[]> {
54
+ this.modelState.logger.debug(`ComputedBoundsActionHandler.execute entered: action.revision=${action.revision}`);
55
+ const model = this.modelState.root;
56
+ if (action.revision !== model.revision) {
57
+ if (this.submissionHandler.hasPendingInitialRequest()) {
58
+ this.modelState.logger.warn(
59
+ `ComputedBounds rejected (initial handshake): action.revision=${action.revision} model.revision=${model.revision} — ` +
60
+ 'initial SetModelAction will not fire (empty diagram)'
61
+ );
62
+ } else {
63
+ this.modelState.logger.debug(
64
+ `ComputedBounds dropped (stale): action.revision=${action.revision} model.revision=${model.revision} — ` +
65
+ 'bounds belong to a superseded GModel; safe to ignore'
66
+ );
67
+ }
68
+ return [];
69
+ }
70
+ this.modelState.logger.trace(`ComputedBounds accepted: revision=${action.revision}, bounds=${action.bounds?.length ?? 0}`);
71
+ return super.execute(action);
72
+ }
73
+
74
+ /**
75
+ * Defensively filter routes that lack the source+target endpoints upstream
76
+ * `applyRoute` requires (`newRoutingPoints.length >= 2`). Without this,
77
+ * the upstream handler throws `GLSPServerError: Invalid Route!` and the
78
+ * entire `computedBounds` action is rejected — the diagram never settles.
79
+ *
80
+ * The race that produces these routes is benign and common on the
81
+ * initial layout pass: the GModel factory emits an edge with no explicit
82
+ * `routingPoints`, the client schedules the edge for routing but submits
83
+ * `computedBounds` before the routing pass finishes, so the route shows
84
+ * up with 0 or 1 point. Subsequent `computedBounds` actions carry the
85
+ * fully-routed edges; dropping the incomplete ones here is lossless.
86
+ *
87
+ * Adopters with bounds-specific cleanup — clearing ALL routes so the client
88
+ * re-routes on the next layout, say — override and replace this filter.
89
+ */
90
+ protected override applyBounds(root: GModelRoot, action: ComputedBoundsAction): void {
91
+ if (action.routes && action.routes.length > 0) {
92
+ const original = action.routes.length;
93
+ action.routes = action.routes.filter(route => (route.newRoutingPoints?.length ?? 0) >= 2);
94
+ const dropped = original - action.routes.length;
95
+ if (dropped > 0) {
96
+ this.modelState.logger.debug(
97
+ `ComputedBounds dropped ${dropped}/${original} route(s) with <2 routing points — client hasn't finished routing yet`
98
+ );
99
+ }
100
+ }
101
+ super.applyBounds(root, action);
102
+ }
103
+ }
@@ -0,0 +1,10 @@
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
+ export * from './hydranium-glsp-computed-bounds-action-handler.js';
@@ -0,0 +1,10 @@
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
+ export * from './server-action-dispatcher.js';
@@ -0,0 +1,72 @@
1
+ /********************************************************************************
2
+ * Copyright (c) 2023-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 Action, DefaultActionDispatcher } from '@eclipse-glsp/server';
11
+ import { inject, injectable } from 'inversify';
12
+ import { type Tracer } from '@hydranium/protocol';
13
+ import { type GlspClientLogger } from '../logging/glsp-client-logger.js';
14
+ import { HydraniumTypes } from '../state/hydranium-shared-core-services.js';
15
+
16
+ /**
17
+ * Server-side action dispatcher with built-in traffic observability:
18
+ * every {@link DefaultActionDispatcher.dispatch} call is wrapped in a
19
+ * {@link Tracer.time} pair that records the dispatch direction
20
+ * (`→ client` vs `→ server`) and timing.
21
+ *
22
+ * **Why this lives in the framework (vs left as an adopter override).**
23
+ * Debugging GLSP state, storage, and submission flows depends heavily on
24
+ * seeing each action with timing and direction — especially when bringing
25
+ * up a new adopter against an empty/partial diagram before its submission
26
+ * flow is complete. Providing this dispatcher in the framework gives every
27
+ * adopter the same observability primitive from the very first GLSP-head
28
+ * launch.
29
+ *
30
+ * **Extension points.**
31
+ * - {@link loggedKinds} — if set, filter the log lines (the dispatch
32
+ * still happens; only the bracketed timing line is suppressed). Default
33
+ * `undefined` means **every** action's dispatch is timed and logged.
34
+ * - {@link summarize} — return a non-empty string to append `[summary]`
35
+ * to the log line; default returns `''` (no extra bracket).
36
+ *
37
+ * Adopters bind it in a per-diagram module, as the `ActionDispatcher` service.
38
+ */
39
+ @injectable()
40
+ export class HydraniumGlspServerActionDispatcher extends DefaultActionDispatcher {
41
+ /** Narrowed from `Logger`; DI binds {@link GlspClientLogger}. */
42
+ declare protected readonly logger: GlspClientLogger;
43
+
44
+ /** Caller-tagged tracer (auto-componented with this class name, like the logger). */
45
+ @inject(HydraniumTypes.Tracer) protected readonly tracer!: Tracer;
46
+
47
+ /**
48
+ * If set, the timing log line is emitted only when `action.kind` is in
49
+ * this set. Subclasses override via field reassignment.
50
+ */
51
+ protected readonly loggedKinds: ReadonlySet<string> | undefined = undefined;
52
+
53
+ /**
54
+ * Override to enrich the log line with payload-specific detail. Return an
55
+ * empty string to suppress the bracket. Default returns `''`.
56
+ */
57
+ protected summarize(_action: Action): string {
58
+ return '';
59
+ }
60
+
61
+ override dispatch(action: Action): Promise<void> {
62
+ if (this.loggedKinds && !this.loggedKinds.has(action.kind)) {
63
+ return super.dispatch(action);
64
+ }
65
+ const direction = this.clientActionForwarder.shouldForwardToClient(action) ? '→ client' : '→ server';
66
+ const summary = this.summarize(action);
67
+ const label = summary
68
+ ? `Dispatch action '${action.kind}' ${direction} [${summary}]`
69
+ : `Dispatch action '${action.kind}' ${direction}`;
70
+ return this.tracer.time(label, () => super.dispatch(action));
71
+ }
72
+ }
package/src/index.ts ADDED
@@ -0,0 +1,20 @@
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
+ // Public API barrel for `@hydranium/glsp-server` — the GLSP protocol head.
11
+ export * from './command/index.js';
12
+ export * from './computed-bounds/index.js';
13
+ export * from './dispatcher/index.js';
14
+ export * from './launcher/index.js';
15
+ export * from './logging/index.js';
16
+ export * from './state/index.js';
17
+ export * from './storage/index.js';
18
+ export * from './submission/index.js';
19
+ export * from './util/index.js';
20
+ export * from './validation/index.js';
@@ -0,0 +1,117 @@
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 { DiagramModule } from '@eclipse-glsp/server';
11
+ import { injectable, type interfaces } from 'inversify';
12
+ import type { LanguageMetaData } from '@hydranium/langium';
13
+ import { type ServerLanguageServices, type ServerSharedServices, typedMetadata } from '@hydranium/core';
14
+ import { HydraniumTypes } from '../state/hydranium-shared-core-services.js';
15
+
16
+ /**
17
+ * Bind {@link HydraniumTypes}.DiagramLanguage on a GLSP **session** container
18
+ * to the services of `metadata`'s grammar.
19
+ *
20
+ * Exported separately from {@link AbstractHydraniumGlspDiagramModule} for adopters
21
+ * whose diagram module already extends an intermediate base of their own and
22
+ * cannot take the framework base class; call it from `configure` after
23
+ * `super.configure(...)`. The base class is the preferred entry point because
24
+ * it makes the declaration non-optional.
25
+ *
26
+ * **Why the session tier.** GLSP builds its app container once per process,
27
+ * before any document exists, so a per-language service bound there can only
28
+ * ever be one grammar's. The session container is created per open diagram
29
+ * with its `diagramType` known, and a diagram type has exactly one grammar —
30
+ * so the language is a static fact about the module, resolvable with no URI
31
+ * and no ordering hazard. That last part is load-bearing: GLSP's
32
+ * `OperationHandlerRegistryInitializer` constructs every operation handler at
33
+ * `InitializeClientSession`, i.e. BEFORE `RequestModelAction` supplies the
34
+ * source URI, so anything derived from `ModelState.sourceUri` at injection
35
+ * time would throw on every diagram open.
36
+ *
37
+ * **The whole language, not pre-resolved providers.** There is deliberately no
38
+ * token for an individual per-language provider; see {@link HydraniumTypes} for
39
+ * why. Callers reach `modelState.diagramLanguage` or
40
+ * `modelState.languageServicesFor(node)` instead, which name the choice.
41
+ *
42
+ * Resolution is `toDynamicValue` + `inSingletonScope` — deferred so the parent
43
+ * container's `SharedCoreServices` is available, then resolved once per
44
+ * session rather than per injection.
45
+ */
46
+ export function bindDiagramLanguage(bind: interfaces.Bind, metadata: LanguageMetaData): void {
47
+ bind(HydraniumTypes.DiagramLanguage)
48
+ .toDynamicValue(context => resolveDiagramLanguage(context.container, metadata))
49
+ .inSingletonScope();
50
+ }
51
+
52
+ /**
53
+ * The registered services for `metadata`'s language, or a throw naming both
54
+ * the declared id and the registered ones.
55
+ *
56
+ * Failing loudly is the point: the declared language is the one fact this
57
+ * module states that the grammar does not, so it can drift. An unregistered id
58
+ * means the adopter declared a language they never registered — a wiring
59
+ * mistake whose silent form (falling back to some other grammar's providers)
60
+ * would surface much later as references that resolve against the wrong scope.
61
+ */
62
+ function resolveDiagramLanguage(container: interfaces.Container, metadata: LanguageMetaData): ServerLanguageServices {
63
+ const registry = container.get<ServerSharedServices>(HydraniumTypes.SharedCoreServices).ServiceRegistry;
64
+ const language = registry.getServices(typedMetadata<ServerLanguageServices>(metadata));
65
+ if (!language) {
66
+ const registered = registry.all.map(candidate => candidate.LanguageMetaData.languageId);
67
+ throw new Error(
68
+ `Diagram module declares language '${metadata.languageId}', which is not registered on the ServiceRegistry. ` +
69
+ `Registered languages: ${registered.length > 0 ? registered.join(', ') : '(none)'}. ` +
70
+ 'Declare the languageId of a grammar passed to bootstrapLangiumLanguages.'
71
+ );
72
+ }
73
+ return language;
74
+ }
75
+
76
+ /**
77
+ * GLSP {@link DiagramModule} base that declares which grammar its diagram type
78
+ * edits, and binds that language's per-language services on the session
79
+ * container.
80
+ *
81
+ * Subclass instead of `DiagramModule` and implement {@link declareLanguage}
82
+ * alongside `diagramType`; the two are the same kind of fact about a diagram
83
+ * type, and pairing them is what lets one GLSP head serve N grammars.
84
+ *
85
+ * Adopter GLSP components then read the language they mean:
86
+ * `modelState.diagramLanguage.references.CandidateProvider` for a reference
87
+ * written on the canvas, `modelState.languageServicesFor(node).references.X`
88
+ * for anything reached through a reference. No per-language service is bound
89
+ * to a token of its own — see {@link bindDiagramLanguage} for why.
90
+ */
91
+ @injectable()
92
+ export abstract class AbstractHydraniumGlspDiagramModule extends DiagramModule {
93
+ /**
94
+ * The `LanguageMetaData` of the grammar this diagram type edits — pass the
95
+ * generated `<Grammar>LanguageMetaData` constant directly.
96
+ *
97
+ * Abstract rather than optional so a new diagram module cannot silently
98
+ * omit it: a missing declaration is a compile error here, where the fix is
99
+ * obvious, instead of an unbound-token failure at the first diagram open.
100
+ *
101
+ * NOT named `bindLanguage`: every `bindX()` on GLSP's `DiagramModule`
102
+ * returns a `BindingTarget<T>` that GLSP's own `configure` consumes, and a
103
+ * method in that family returning a `LanguageMetaData` invites
104
+ * `{ service: … }`. This one states a fact; the framework does the binding.
105
+ */
106
+ protected abstract declareLanguage(): LanguageMetaData;
107
+
108
+ protected override configure(
109
+ bind: interfaces.Bind,
110
+ unbind: interfaces.Unbind,
111
+ isBound: interfaces.IsBound,
112
+ rebind: interfaces.Rebind
113
+ ): void {
114
+ super.configure(bind, unbind, isBound, rebind);
115
+ bindDiagramLanguage(bind, this.declareLanguage());
116
+ }
117
+ }
@@ -0,0 +1,106 @@
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 { GLSPModule } from '@eclipse-glsp/server';
11
+ import type { BindingContext } from '@eclipse-glsp/protocol/lib/di/inversify-util.js';
12
+ import type { ServerSharedServices } from '@hydranium/core';
13
+ import { type ConflictResolver, ReconcilingConflictResolver } from '@hydranium/protocol';
14
+ import { injectable, type interfaces } from 'inversify';
15
+ import { HydraniumTypes } from '../state/hydranium-shared-core-services.js';
16
+
17
+ /**
18
+ * Configuration accepted by {@link HydraniumGlspAppModule}. Bundles the
19
+ * framework-defined Inversify bindings every adopter wires verbatim in their
20
+ * GLSP server app-module (the `appModules` argument to `startGlspServer`).
21
+ *
22
+ * **App tier only, so nothing per-language belongs here.** This module is
23
+ * loaded into the one-per-process app container, built before any document
24
+ * exists — it cannot know which grammar a request concerns. Per-language
25
+ * services are declared per diagram type instead; see
26
+ * `AbstractHydraniumGlspDiagramModule`.
27
+ */
28
+ export interface HydraniumGlspAppModuleOptions<TShared extends ServerSharedServices = ServerSharedServices> {
29
+ /**
30
+ * The framework's Langium-style shared services tree. Bound to the
31
+ * `HydraniumTypes.SharedCoreServices` symbol that framework state /
32
+ * index classes (`AbstractHydraniumGlspState`, `HydraniumGlspIndex`) `@inject`.
33
+ *
34
+ * Generic over `TShared` so adopters with their own narrower
35
+ * shared-services type (an adopter-specific type extending
36
+ * `ServerSharedServices` with extra slots) preserve that
37
+ * type at `this.options.shared` inside subclass methods without an
38
+ * explicit cast. The Inversify symbol binding still types `@inject`
39
+ * sites as the wider `ServerSharedServices`; specialise the binding
40
+ * with an adopter-specific symbol where injected components want the
41
+ * narrower view.
42
+ */
43
+ readonly shared: TShared;
44
+
45
+ /**
46
+ * Optional {@link ConflictResolver} policy bound to the
47
+ * `HydraniumTypes.ConflictResolver` symbol that `AbstractHydraniumGlspState`
48
+ * injects. Omit to get the framework default
49
+ * ({@link ReconcilingConflictResolver} — field-level three-way merge);
50
+ * pass a `ForceConflictResolver` for
51
+ * last-writer-wins, or a custom resolver. Applies uniformly to every
52
+ * conflict site (forward-write, undo, redo, save).
53
+ */
54
+ readonly conflictResolver?: ConflictResolver;
55
+ }
56
+
57
+ /**
58
+ * DI module that provides the framework-level Inversify bindings every
59
+ * `@hydranium/glsp-server` adopter needs. Pass instances (or instances of
60
+ * subclasses with additional bindings) into `startGlspServer`'s
61
+ * `appModules` array.
62
+ *
63
+ * Bindings provided:
64
+ * - `HydraniumTypes.SharedCoreServices` (constant value)
65
+ * - `HydraniumTypes.ConflictResolver` (constant value — `options.conflictResolver` or a default {@link ReconcilingConflictResolver})
66
+ *
67
+ * Both are language-independent, which is what makes them app-tier. The
68
+ * per-language providers (`ScopeProvider`, `CandidateProvider`) are bound one
69
+ * tier down by `AbstractHydraniumGlspDiagramModule`, from the grammar each
70
+ * diagram type declares.
71
+ *
72
+ * Concrete and usable as-is: adopters that need no extra bindings
73
+ * instantiate {@link HydraniumGlspAppModule} directly. Adopters that do
74
+ * subclass it and override {@link configureAdditionalBindings} to add their
75
+ * own service-identifier carriers.
76
+ *
77
+ * A conventional GLSP module (cf. `@eclipse-glsp/server`'s `GLSPModule`
78
+ * subclasses); the empty `configureAdditionalBindings` hook is the single
79
+ * extension point, so no separate abstract base is needed.
80
+ */
81
+ @injectable()
82
+ export class HydraniumGlspAppModule<TShared extends ServerSharedServices = ServerSharedServices> extends GLSPModule {
83
+ constructor(protected readonly options: HydraniumGlspAppModuleOptions<TShared>) {
84
+ super();
85
+ }
86
+
87
+ protected override configure(
88
+ bind: interfaces.Bind,
89
+ unbind: interfaces.Unbind,
90
+ isBound: interfaces.IsBound,
91
+ rebind: interfaces.Rebind
92
+ ): void {
93
+ bind(HydraniumTypes.SharedCoreServices).toConstantValue(this.options.shared);
94
+ bind(HydraniumTypes.ConflictResolver).toConstantValue(this.options.conflictResolver ?? new ReconcilingConflictResolver());
95
+ this.configureAdditionalBindings({ bind, unbind, isBound, rebind });
96
+ }
97
+
98
+ /**
99
+ * Extension point for adopter-specific bindings (service-identifier
100
+ * carriers for the adopter's own language services).
101
+ * Default implementation is empty.
102
+ */
103
+ protected configureAdditionalBindings(_context: BindingContext): void {
104
+ // empty by default
105
+ }
106
+ }