@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,52 @@
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
+ /**
11
+ * How `HydraniumGlspStorage.saveSourceModel` reacts when a concurrent edit has
12
+ * advanced the document past the version this diagram was last captured at.
13
+ *
14
+ * A save spans otherwise-independent choices — whether to guard on the captured
15
+ * based-on version, whether the GLSP save action awaits the persist, and what
16
+ * happens on failure. Each arm bundles those into one coherent, nameable policy
17
+ * so an adopter selects a whole behaviour rather than assembling an incoherent
18
+ * mix: fire-and-forget plus rethrow would leave an unhandled rejection. It is a
19
+ * *selection of configuration*, delivered as a bound option (see the
20
+ * {@link SaveConflictPolicy} symbol) rather than a behaviour hook.
21
+ */
22
+ export type SaveConflictPolicy =
23
+ /**
24
+ * Last-write-wins: no based-on guard, await the persist, propagate any
25
+ * failure to the GLSP save action. The default; correct for a single-editor
26
+ * head where nothing races the save.
27
+ */
28
+ | { kind: 'overwrite' }
29
+ /**
30
+ * Guard on the captured version, await the persist, and surface a
31
+ * `ConflictError` (and any other failure) to the GLSP save action.
32
+ */
33
+ | { kind: 'reject' }
34
+ /**
35
+ * Guard on the captured version, fire-and-forget, and log + swallow every
36
+ * failure. Correct when another editor of the same document may already have
37
+ * written the truth, so a stale diagram save is benign and must neither
38
+ * block the action nor surface as an error.
39
+ */
40
+ | { kind: 'drop-and-log' };
41
+
42
+ /** The policy applied when no {@link SaveConflictPolicy} option is bound. */
43
+ export const DEFAULT_SAVE_CONFLICT_POLICY: SaveConflictPolicy = { kind: 'overwrite' };
44
+
45
+ /**
46
+ * DI token for the {@link SaveConflictPolicy} option. Bind a constant value in a
47
+ * `DiagramModule` to select a non-default policy; left unbound,
48
+ * `HydraniumGlspStorage` falls back to
49
+ * {@link DEFAULT_SAVE_CONFLICT_POLICY}. Shares its name with the type, the way a
50
+ * `class` is both a value and a type.
51
+ */
52
+ export const SaveConflictPolicy = Symbol('SaveConflictPolicy');
@@ -0,0 +1,170 @@
1
+ /********************************************************************************
2
+ * Copyright (c) 2024-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 DirtyStateChangeReason,
13
+ type GModelRootSchema,
14
+ type LayoutOperation,
15
+ ModelState,
16
+ ModelSubmissionHandler,
17
+ SetDirtyStateAction,
18
+ type SetModelAction
19
+ } from '@eclipse-glsp/server';
20
+ import { IntegrityService } from '@hydranium/core';
21
+ import { inject, injectable } from 'inversify';
22
+ import { type AstNode, type DocumentState } from '@hydranium/langium';
23
+ import { type AbstractHydraniumGlspState } from '../state/abstract-hydranium-glsp-state.js';
24
+
25
+ /**
26
+ * GLSP {@link ModelSubmissionHandler} base shared by all hydranium adopters.
27
+ * Wraps {@link submitModel} with an optional document-state ready-gate and
28
+ * post-action observability logging; preserves the {@link createSetModeAction}
29
+ * handshake log; exposes a {@link hasPendingInitialRequest} guard adopters
30
+ * use to suppress external rebuild reactions while the initial-load
31
+ * `RequestModelAction` is still in flight.
32
+ *
33
+ * **What stays adopter-side.** Translation of the source AST to a GModel is
34
+ * owned by GLSP's existing `GModelFactory` interface, bound separately
35
+ * per `DiagramModule`. The submission handler is strictly orchestration —
36
+ * deliberately no `translateToGModel` hook here; adopters wire their per-
37
+ * diagram-type `GModelFactory` subclass via `bindGModelFactory()`.
38
+ *
39
+ * **Hooks.**
40
+ * - {@link readyEvent} — {@link DocumentState} the {@link submitModel} flow
41
+ * awaits via {@link AbstractHydraniumGlspState.ready} before delegating to
42
+ * `super.submitModel`. Default {@link IntegrityService.SettledState}
43
+ * (`IndexedReferences`) — the GModel-factory walk sees a fully linked AST
44
+ * with cross-document references resolved AND post-build integrity rules
45
+ * applied. Adopters that need a different phase (earlier `Linked` for
46
+ * simpler models, later `Validated` to consume diagnostics, or `undefined`
47
+ * to opt out entirely) override this field.
48
+ * - {@link formatSourceRoot} — adopter-specific AST description used in
49
+ * the submit-log line. Default returns `root.$type`.
50
+ *
51
+ * Logs through {@link AbstractHydraniumGlspState.logger} (the state's
52
+ * URI-tagged tracer), so it carries no log-label field of its own.
53
+ */
54
+ @injectable()
55
+ export class HydraniumGlspSubmissionHandler<TRoot extends AstNode, TSourceModel = string> extends ModelSubmissionHandler {
56
+ @inject(ModelState) declare protected modelState: AbstractHydraniumGlspState<TRoot, TSourceModel>;
57
+
58
+ /**
59
+ * Document-state gate awaited before {@link submitModel} delegates to
60
+ * `super.submitModel`. Default {@link IntegrityService.SettledState} so the
61
+ * GModel factory walks a fully-linked, integrity-settled AST out of the
62
+ * box. Adopters override (including to `undefined` to opt out entirely)
63
+ * when their submission flow doesn't need to wait.
64
+ */
65
+ protected readyEvent: DocumentState | undefined = IntegrityService.SettledState;
66
+
67
+ /**
68
+ * True when a `RequestModelAction` has been accepted but the
69
+ * corresponding {@link SetModelAction} response has not yet been
70
+ * dispatched. Adopter storage uses this to suppress the "external
71
+ * submit" rebuild reaction during the initial-load handshake: that
72
+ * rebuild reaction would call `submitModel('external')`, which rebuilds
73
+ * the GModel and bumps its revision; once the revision changes, the
74
+ * client's first `computedBounds` (still carrying the old revision) is
75
+ * dropped and the initial `SetModelAction` never fires.
76
+ */
77
+ hasPendingInitialRequest(): boolean {
78
+ return this.requestModelAction !== undefined;
79
+ }
80
+
81
+ override async submitModel(reason?: DirtyStateChangeReason, layout?: LayoutOperation): Promise<Action[]> {
82
+ if (this.readyEvent !== undefined) {
83
+ // Adopters with on-build integrity rules wait so the GModel-factory walk sees
84
+ // a settled AST; otherwise the GModel could include elements the build pass is
85
+ // about to rewrite or remove. ready() also refreshes the captured source root
86
+ // via onReadyRefreshed.
87
+ await this.modelState.ready(this.readyEvent);
88
+ }
89
+ const actions = await super.submitModel(reason, layout);
90
+ this._lastSubmittedSignature = this.signatureOf(actions);
91
+ this.logSubmit(reason, actions);
92
+ return actions;
93
+ }
94
+
95
+ /**
96
+ * Content signature of the last submission, whatever its reason.
97
+ *
98
+ * **Tracked across every reason because the CLIENT cannot tell them apart.**
99
+ * An operation submit and a later external rebuild that produce the same
100
+ * graph are the same delivery as far as the canvas is concerned, so a
101
+ * comparison scoped to one reason reports "changed" for a model the client
102
+ * already has. `undefined` until the first submit.
103
+ */
104
+ get lastSubmittedSignature(): string | undefined {
105
+ return this._lastSubmittedSignature;
106
+ }
107
+
108
+ protected _lastSubmittedSignature?: string;
109
+
110
+ /**
111
+ * Serialise the model-bearing actions of a submission into a comparable
112
+ * signature.
113
+ *
114
+ * `requestId` is fresh per action and `revision` is a monotonic `GModelRoot`
115
+ * counter bumped on every rebuild, so neither reflects content.
116
+ *
117
+ * **`SetDirtyStateAction` is dropped whole rather than having its `reason`
118
+ * stripped by key, and the distinction is load-bearing.** Measured: an
119
+ * operation submit and the external rebuild echoing it are byte-identical
120
+ * across the entire GModel and differ only in that action's `reason`, so
121
+ * leaving it in defeats the comparison outright. Stripping the KEY instead
122
+ * would reach further than intended — `reason` carries meaning on other
123
+ * actions, `SetMarkersAction` among them — and would quietly weaken any
124
+ * later comparison that includes one. Dropping the action also keeps
125
+ * `isDirty` out, which is correct: dirty state is not part of "is this the
126
+ * same model", and a genuine dirty-to-clean change must not be suppressed as
127
+ * a duplicate model.
128
+ */
129
+ protected signatureOf(actions: Action[]): string {
130
+ return JSON.stringify(
131
+ actions.filter(action => !SetDirtyStateAction.is(action)),
132
+ (key, value) => (key === 'requestId' || key === 'revision' ? undefined : value)
133
+ );
134
+ }
135
+
136
+ /**
137
+ * Logs the {@link SetModelAction} dispatch so the initial
138
+ * `requestModel → setModel` handshake is observable in the server log
139
+ * alongside `Submit model` and `ComputedBounds accepted`.
140
+ */
141
+ protected override createSetModeAction(newRoot: GModelRootSchema): SetModelAction {
142
+ const action = super.createSetModeAction(newRoot);
143
+ this.modelState.logger.info(
144
+ `Dispatching SetModelAction (response to requestModel #${action.responseId || '?'}) — initial model handshake complete`
145
+ );
146
+ return action;
147
+ }
148
+
149
+ protected logSubmit(reason: DirtyStateChangeReason | undefined, actions: Action[]): void {
150
+ const root = this.modelState.root;
151
+ const gChildren = root?.children?.length ?? 0;
152
+ const sourceRoot = this.modelState.sourceRoot;
153
+ const kinds = actions.map(a => a.kind).join(',');
154
+ this.modelState.logger.info(
155
+ `Submit model [reason=${reason ?? 'initial'}, actions=[${kinds}], ` +
156
+ `gmodel={type=${root?.type ?? 'none'}, children=${gChildren}}, ` +
157
+ `ast={${this.formatSourceRoot(sourceRoot)}}]`
158
+ );
159
+ }
160
+
161
+ /**
162
+ * Hook for adopter-specific AST description in the submit-log line.
163
+ * Default returns `root.$type` (or `'none'` when the source root is
164
+ * undefined). Adopters with multi-shape source roots override to add
165
+ * per-shape detail.
166
+ */
167
+ protected formatSourceRoot(root: TRoot | undefined): string {
168
+ return root?.$type ?? 'none';
169
+ }
170
+ }
@@ -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-submission-handler.js';
@@ -0,0 +1,422 @@
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 'reflect-metadata';
11
+ import {
12
+ type Action,
13
+ type ActionMessage,
14
+ ClientSessionManager,
15
+ EndProgressAction,
16
+ GLSPClientProxy,
17
+ GLSPServer,
18
+ ModelState,
19
+ RequestBoundsAction,
20
+ RequestModelAction,
21
+ SOURCE_URI_ARG,
22
+ SetDirtyStateAction,
23
+ SetModelAction,
24
+ StartProgressAction,
25
+ StatusAction,
26
+ UpdateModelAction,
27
+ UpdateProgressAction
28
+ } from '@eclipse-glsp/server';
29
+ import {
30
+ DefaultGLSPServer,
31
+ InjectionContainer,
32
+ Logger as GlspLogger,
33
+ LoggerFactory,
34
+ NodeActionDispatchScope,
35
+ type ServerModule,
36
+ getRequestParentName
37
+ } from '@eclipse-glsp/server/node.js';
38
+ import { ActionDispatchScope } from '@eclipse-glsp/server';
39
+ import { Container, ContainerModule, type interfaces } from 'inversify';
40
+ import type { AstNode } from '@hydranium/langium';
41
+ import type { ServerSharedServices } from '@hydranium/core';
42
+ import type { Harness } from '@hydranium/protocol/testing';
43
+ import type { AbstractHydraniumGlspState } from '../state/abstract-hydranium-glsp-state.js';
44
+ import { HydraniumTypes } from '../state/hydranium-shared-core-services.js';
45
+ import { createGlspServerOverrides } from '../launcher/glsp-server-overrides.js';
46
+ import { makeNoopGlspLogger } from './make-noop-glsp-logger.js';
47
+
48
+ /**
49
+ * Options for {@link makeGlspHarness}.
50
+ *
51
+ * The harness drives a REAL {@link GLSPServer} in-process: it composes the
52
+ * server container the way `startGlspServer` does (framework default app
53
+ * module + adopter `appModules` + the adopter `serverModule`), but binds
54
+ * {@link GLSPClientProxy} to a capturing stub instead of a socket-backed
55
+ * proxy, so every action the server pushes to the client lands in the
56
+ * harness's `actions` capture. There is no OS socket and no JSON
57
+ * serialization, so nothing here covers the wire layer.
58
+ *
59
+ * `serverModule` + `diagramType` are required; everything else has a
60
+ * test-friendly default. The adopter's shared services / id provider /
61
+ * conflict resolver come from `appModules` (typically an
62
+ * `HydraniumGlspAppModule` subclass) — exactly as in production, so
63
+ * the harness binds none of them itself.
64
+ */
65
+ export interface MakeGlspHarnessOptions {
66
+ /** Adopter {@link ServerModule} with its diagram module(s) pre-configured via `configureDiagramModule`. */
67
+ readonly serverModule: ServerModule;
68
+ /** Diagram type id passed to `initializeClientSession`; must match the configured diagram module. */
69
+ readonly diagramType: string;
70
+ /**
71
+ * Adopter Inversify modules loaded AFTER the framework default module
72
+ * (which binds `InjectionContainer`, GLSP's `Logger`, `LoggerFactory` and
73
+ * `ActionDispatchScope`). Carries the adopter's
74
+ * `HydraniumTypes.SharedCoreServices` / `HydraniumTypes.ConflictResolver` /
75
+ * language-services bindings. Per-language providers come from the diagram
76
+ * module instead — see `AbstractHydraniumGlspDiagramModule`.
77
+ */
78
+ readonly appModules?: ReadonlyArray<ContainerModule>;
79
+ /**
80
+ * GLSP logger factory. Default: a silent logger (the harness keeps the
81
+ * test output clean). Override to capture GLSP framework log lines.
82
+ */
83
+ readonly createLogger?: (caller?: string) => GlspLogger;
84
+ /** `applicationId` for the `initialize` handshake. Default `'test-app'`. */
85
+ readonly applicationId?: string;
86
+ /** Client session id used for `initializeClientSession` + every dispatched `ActionMessage`. Default `'test-session'`. */
87
+ readonly clientSessionId?: string;
88
+ /**
89
+ * Adopter-specific client action kinds to forward IN ADDITION to
90
+ * {@link DEFAULT_CLIENT_ACTION_KINDS}. The server's
91
+ * `ClientActionForwarder` only forwards declared kinds to the capturing
92
+ * `GLSPClientProxy`, so list any custom server→client action a test needs
93
+ * to observe. The standard set covers ordinary round-trips, so most tests
94
+ * leave this unset.
95
+ */
96
+ readonly additionalClientActionKinds?: ReadonlyArray<string>;
97
+ }
98
+
99
+ /**
100
+ * The standard client-bound action kinds a real GLSP client handles — the
101
+ * minimal set needed for any model round-trip. The harness declares these on
102
+ * the session by default so the capturing {@link GLSPClientProxy} receives
103
+ * them and the action dispatcher never errors with "no handler registered"
104
+ * for an ordinary server→client action. Adopter-specific kinds are added via
105
+ * {@link MakeGlspHarnessOptions.additionalClientActionKinds}.
106
+ */
107
+ const DEFAULT_CLIENT_ACTION_KINDS: ReadonlyArray<string> = [
108
+ SetModelAction.KIND,
109
+ UpdateModelAction.KIND,
110
+ RequestBoundsAction.KIND,
111
+ SetDirtyStateAction.KIND,
112
+ StatusAction.KIND,
113
+ StartProgressAction.KIND,
114
+ UpdateProgressAction.KIND,
115
+ EndProgressAction.KIND
116
+ ];
117
+
118
+ /** Options for {@link GlspHarness.nextModelSubmission}. */
119
+ export interface NextModelSubmissionOptions {
120
+ /** How long to wait. Defaults to 2000ms. */
121
+ readonly timeoutMs?: number;
122
+ /**
123
+ * Reject on timeout (default `true`) or resolve `undefined` (`false`). Pair
124
+ * `false` with a short `timeoutMs` — the wait runs to completion in the
125
+ * passing case.
126
+ */
127
+ readonly rejectOnTimeout?: boolean;
128
+ }
129
+
130
+ /**
131
+ * Bundle returned by {@link makeGlspHarness}. Satisfies the uniform
132
+ * {@link Harness} contract — `state` is the **subject** (the resolved adopter
133
+ * state under test), `server` + `dispatch`/`nextAction` are the **seam** tests
134
+ * drive the action round-trip through, `actions` is the **capture** of every
135
+ * outbound action in arrival order, and `dispose()` is the uniform teardown
136
+ * hook. `state` / `sessionContainer` are only valid after {@link start} (the
137
+ * `ModelState` lives in GLSP's per-session child container, created by
138
+ * `initializeClientSession`).
139
+ */
140
+ export interface GlspHarness<TState extends AbstractHydraniumGlspState<AstNode, unknown>> extends Harness {
141
+ /** The real GLSP server under test. */
142
+ readonly server: GLSPServer;
143
+ /** The main (app + server) container. */
144
+ readonly container: interfaces.Container;
145
+ /** The per-session child container `initializeClientSession` created. Throws if read before {@link start}. */
146
+ readonly sessionContainer: interfaces.Container;
147
+ /** The adopter `ModelState` resolved from {@link sessionContainer}. Throws if read before {@link start}. */
148
+ readonly state: TState;
149
+ /** Every action the server pushed to the client, in arrival order. Never cleared by the harness. */
150
+ readonly actions: ReadonlyArray<Action>;
151
+
152
+ /** Drive `initialize` then `initializeClientSession`; resolve once the session container exists. */
153
+ start(): Promise<void>;
154
+ /** Drive `server.shutdown()` (disposes client sessions). */
155
+ shutdown(): Promise<void>;
156
+ /** Send an action to the server as `{ clientId, action }`. Fire-and-forget (GLSP `process` is `void`). */
157
+ dispatch(action: Action): void;
158
+ /**
159
+ * Resolve with the next captured action whose `kind` matches — an
160
+ * already-captured-but-unconsumed match resolves immediately, otherwise
161
+ * waits for the next arrival. Rejects after `timeoutMs` (default 2000) so
162
+ * a missing action fails fast instead of hanging.
163
+ */
164
+ nextAction<T extends Action = Action>(kind: string, timeoutMs?: number): Promise<T>;
165
+ /**
166
+ * FAITHFUL fidelity: open `sourceUri` the way a client does — dispatch
167
+ * `RequestModelAction` with `SOURCE_URI_ARG` — and resolve with the model
168
+ * the server publishes in response.
169
+ *
170
+ * Resolves on **whichever** submission action the diagram produces, because
171
+ * that depends on its `DiagramConfiguration` rather than on the test:
172
+ * `SetModelAction` for a server-laid-out diagram, `RequestBoundsAction` for
173
+ * a client-laid-out one. Awaiting the wrong one is a 2s timeout that names
174
+ * the wrong subsystem, and a test should not have to know which applies.
175
+ *
176
+ * Rejects if nothing is published within `timeoutMs`. Valid only after
177
+ * {@link start}.
178
+ */
179
+ openDocument(sourceUri: string, timeoutMs?: number): Promise<Action>;
180
+ /**
181
+ * Resolve with the next action that publishes a model — the settling point
182
+ * after an operation, since a successful operation re-submits.
183
+ *
184
+ * Kind-agnostic for the same reason as {@link openDocument}. Set
185
+ * `rejectOnTimeout: false` to ask whether a submission happened *at all*.
186
+ */
187
+ nextModelSubmission(options?: NextModelSubmissionOptions): Promise<Action | undefined>;
188
+ /**
189
+ * LIGHT fidelity: seed the state's source root directly via
190
+ * `setSourceRoot`, bypassing source-model storage. Valid only after
191
+ * {@link start}.
192
+ */
193
+ seedSourceRoot(uri: string, root: Parameters<TState['setSourceRoot']>[1]): void;
194
+
195
+ /** Idempotent teardown: `server.shutdown()` then `container.unbindAll()`. */
196
+ dispose(): void;
197
+ }
198
+
199
+ interface PendingWaiter {
200
+ /** Kinds this waiter accepts; the first arrival matching any of them wins. */
201
+ readonly kinds: ReadonlyArray<string>;
202
+ readonly resolve: (action: Action | undefined) => void;
203
+ timer: ReturnType<typeof setTimeout>;
204
+ }
205
+
206
+ /**
207
+ * The actions that mean "the server published a model".
208
+ *
209
+ * Which one arrives depends on the diagram's `DiagramConfiguration`, not on
210
+ * what the test did: a server-laid-out diagram gets `SetModelAction`, a
211
+ * client-laid-out one (`needsClientLayout`) gets `RequestBoundsAction`, and a
212
+ * re-submit after an operation may be `UpdateModelAction`. Tests that only care
213
+ * *that* the model was published should not have to encode that choice — see
214
+ * {@link GlspHarness.nextModelSubmission}.
215
+ *
216
+ * Exported for the tests that must COUNT submissions rather than await the next
217
+ * one (asserting that nothing further was published, say). Re-listing the kinds
218
+ * in such a test is a false-green shape: a kind added here would leave the copy
219
+ * silently under-counting.
220
+ */
221
+ export const MODEL_SUBMISSION_KINDS: ReadonlyArray<string> = [SetModelAction.KIND, UpdateModelAction.KIND, RequestBoundsAction.KIND];
222
+
223
+ /**
224
+ * Wire a real {@link GLSPServer} in-process and drive a GLSP action
225
+ * round-trip against it — dispatch an action → operation/request handler →
226
+ * GModel mutation → response action, captured via a stub
227
+ * {@link GLSPClientProxy}.
228
+ *
229
+ * The harness composes ONE container, collapsing production's parent
230
+ * app-container / per-connection child split, which exists only to share the
231
+ * app container across socket connections.
232
+ */
233
+ export function makeGlspHarness<TState extends AbstractHydraniumGlspState<AstNode, unknown>>(
234
+ options: MakeGlspHarnessOptions
235
+ ): GlspHarness<TState> {
236
+ const applicationId = options.applicationId ?? 'test-app';
237
+ const clientSessionId = options.clientSessionId ?? 'test-session';
238
+ const clientActionKinds = [...DEFAULT_CLIENT_ACTION_KINDS, ...(options.additionalClientActionKinds ?? [])];
239
+ const createLogger = options.createLogger ?? (() => makeNoopGlspLogger());
240
+
241
+ const actions: Action[] = [];
242
+ const consumed = new WeakSet<Action>();
243
+ const waiters: PendingWaiter[] = [];
244
+
245
+ const captureProxy: GLSPClientProxy = {
246
+ process(message: ActionMessage): void {
247
+ const { action } = message;
248
+ actions.push(action);
249
+ const index = waiters.findIndex(waiter => waiter.kinds.includes(action.kind));
250
+ if (index >= 0) {
251
+ const [waiter] = waiters.splice(index, 1);
252
+ consumed.add(action);
253
+ clearTimeout(waiter.timer);
254
+ waiter.resolve(action);
255
+ }
256
+ }
257
+ };
258
+
259
+ /**
260
+ * Resolve with the first unconsumed action matching any of `kinds` — one
261
+ * already captured resolves immediately, otherwise the next arrival wins.
262
+ *
263
+ * `rejectOnTimeout` is what lets a caller distinguish the two questions a
264
+ * test asks. `true` (the default) is "this must happen", and a timeout is a
265
+ * failure naming the kinds. `false` is "did this happen?", resolving
266
+ * `undefined` — needed because a **rejected** operation produces no action
267
+ * at all, so the only observable is the absence of one. Mirrors GLSP's own
268
+ * `ActionDispatcher.requestUntil(action, timeoutMs, rejectOnTimeout)`.
269
+ */
270
+ function waitFor(kinds: ReadonlyArray<string>, timeoutMs: number, rejectOnTimeout: boolean): Promise<Action | undefined> {
271
+ const existing = actions.find(action => kinds.includes(action.kind) && !consumed.has(action));
272
+ if (existing) {
273
+ consumed.add(existing);
274
+ return Promise.resolve(existing);
275
+ }
276
+ return new Promise<Action | undefined>((resolve, reject) => {
277
+ const waiter: PendingWaiter = {
278
+ kinds,
279
+ resolve,
280
+ timer: setTimeout(() => {
281
+ const index = waiters.indexOf(waiter);
282
+ if (index >= 0) {
283
+ waiters.splice(index, 1);
284
+ }
285
+ if (rejectOnTimeout) {
286
+ // Name what DID arrive. A bare "no X within 2000ms" reads as a
287
+ // hang and points at the transport, when the usual cause is an
288
+ // operation handler that threw or declined: the server then
289
+ // emits nothing, or emits only a status/dirty-state action.
290
+ // Distinguishing "nothing happened" from "something else
291
+ // happened" is the difference between a five-minute hunt and a
292
+ // one-line diagnosis.
293
+ const seen = actions.map(captured => captured.kind);
294
+ const context = seen.length === 0 ? 'no actions were captured at all' : `captured since start: ${seen.join(', ')}`;
295
+ reject(
296
+ new Error(
297
+ `makeGlspHarness: no ${kinds.map(kind => `'${kind}'`).join(' / ')} action within ${timeoutMs}ms — ${context}`
298
+ )
299
+ );
300
+ } else {
301
+ resolve(undefined);
302
+ }
303
+ }, timeoutMs)
304
+ };
305
+ waiters.push(waiter);
306
+ });
307
+ }
308
+
309
+ const defaultAppModule = new ContainerModule(bind => {
310
+ bind(InjectionContainer).toDynamicValue(ctx => ctx.container);
311
+ bind(GlspLogger).toDynamicValue(ctx => createLogger(getRequestParentName(ctx)));
312
+ bind(LoggerFactory).toFactory(() => (caller: string) => createLogger(caller));
313
+ // `DefaultActionDispatcher` injects this to tell a reentrant dispatch (from
314
+ // inside a running handler, which must run inline) from an external one
315
+ // (which queues). Mirrors GLSP's own `createAppModule`; without it every
316
+ // dispatch throws on `dispatchScope.isReentrant`.
317
+ bind(ActionDispatchScope).to(NodeActionDispatchScope).inSingletonScope();
318
+ bind(HydraniumTypes.Tracer).toDynamicValue(ctx => {
319
+ const tracer = ctx.container.get<ServerSharedServices>(HydraniumTypes.SharedCoreServices).Tracer;
320
+ const caller = getRequestParentName(ctx);
321
+ return caller ? tracer.for(caller) : tracer;
322
+ });
323
+ });
324
+ const captureProxyModule = new ContainerModule(bind => {
325
+ bind(GLSPClientProxy).toConstantValue(captureProxy);
326
+ });
327
+
328
+ const container = new Container();
329
+ container.load(defaultAppModule, ...(options.appModules ?? []), captureProxyModule);
330
+ container.load(options.serverModule);
331
+ // After the serverModule, which binds `GLSPServer`: the override rebinds that
332
+ // symbol, so it needs the binding to exist. The launchers reach the same tier
333
+ // by passing this to `configure` — a harness that skipped it would answer
334
+ // request failures differently from every real bringup.
335
+ container.load(createGlspServerOverrides());
336
+ const server = container.get<GLSPServer>(GLSPServer);
337
+
338
+ let sessionContainer: interfaces.Container | undefined;
339
+ let state: TState | undefined;
340
+ let disposed = false;
341
+
342
+ return {
343
+ server,
344
+ container,
345
+ actions,
346
+ get sessionContainer(): interfaces.Container {
347
+ if (!sessionContainer) {
348
+ throw new Error('makeGlspHarness: sessionContainer is only available after start()');
349
+ }
350
+ return sessionContainer;
351
+ },
352
+ get state(): TState {
353
+ if (!state) {
354
+ throw new Error('makeGlspHarness: state is only available after start()');
355
+ }
356
+ return state;
357
+ },
358
+
359
+ async start(): Promise<void> {
360
+ await server.initialize({ applicationId, protocolVersion: DefaultGLSPServer.PROTOCOL_VERSION });
361
+ await server.initializeClientSession({ clientSessionId, diagramType: options.diagramType, clientActionKinds });
362
+ const session = container.get<ClientSessionManager>(ClientSessionManager).getSession(clientSessionId);
363
+ if (!session) {
364
+ throw new Error(`makeGlspHarness: no client session '${clientSessionId}' after initializeClientSession`);
365
+ }
366
+ // `session.container` is inversify's concrete `Container`, a nominal type via its
367
+ // private fields — so it clashes across inversify's CJS/ESM dual-package
368
+ // declarations. Typing the harness's container fields as the structural
369
+ // `interfaces.Container` reconciles them with no cast (interfaces compare
370
+ // structurally; the concrete `Container` is only needed for `new Container()`).
371
+ const resolved = session.container;
372
+ sessionContainer = resolved;
373
+ state = resolved.get<TState>(ModelState);
374
+ },
375
+
376
+ async shutdown(): Promise<void> {
377
+ server.shutdown();
378
+ },
379
+
380
+ dispatch(action: Action): void {
381
+ server.process({ clientId: clientSessionId, action });
382
+ },
383
+
384
+ nextAction<T extends Action = Action>(kind: string, timeoutMs = 2000): Promise<T> {
385
+ return waitFor([kind], timeoutMs, true) as Promise<T>;
386
+ },
387
+
388
+ async openDocument(sourceUri: string, timeoutMs = 2000): Promise<Action> {
389
+ if (!state) {
390
+ throw new Error('makeGlspHarness: openDocument() is only valid after start()');
391
+ }
392
+ server.process({
393
+ clientId: clientSessionId,
394
+ action: RequestModelAction.create({ options: { [SOURCE_URI_ARG]: sourceUri } })
395
+ });
396
+ const submission = await waitFor(MODEL_SUBMISSION_KINDS, timeoutMs, true);
397
+ // `waitFor` with rejectOnTimeout only resolves with a real action.
398
+ return submission as Action;
399
+ },
400
+
401
+ nextModelSubmission(options: NextModelSubmissionOptions = {}): Promise<Action | undefined> {
402
+ const { timeoutMs = 2000, rejectOnTimeout = true } = options;
403
+ return waitFor(MODEL_SUBMISSION_KINDS, timeoutMs, rejectOnTimeout);
404
+ },
405
+
406
+ seedSourceRoot(uri: string, root: Parameters<TState['setSourceRoot']>[1]): void {
407
+ if (!state) {
408
+ throw new Error('makeGlspHarness: seedSourceRoot() is only valid after start()');
409
+ }
410
+ state.setSourceRoot(uri, root);
411
+ },
412
+
413
+ dispose(): void {
414
+ if (disposed) {
415
+ return;
416
+ }
417
+ disposed = true;
418
+ server.shutdown();
419
+ container.unbindAll();
420
+ }
421
+ };
422
+ }
@@ -0,0 +1,14 @@
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
+ // Subpath barrel for `@hydranium/glsp-server/testing` — `makeGlspHarness`,
11
+ // which drives a real GLSP server in-process against a capturing client proxy.
12
+
13
+ export * from './glsp-harness.js';
14
+ export * from './make-noop-glsp-logger.js';