@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,660 @@
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 { MarkersReason, SetMarkersAction } from '@eclipse-glsp/protocol';
11
+ import {
12
+ type Action,
13
+ ActionDispatcher,
14
+ type ClientSession,
15
+ type ClientSessionListener,
16
+ ClientSessionManager,
17
+ CommandStack,
18
+ type DefaultCommandStack,
19
+ type Disposable,
20
+ EditMode,
21
+ GLSPServerError,
22
+ Logger as GlspLogger,
23
+ type MaybePromise,
24
+ ModelState,
25
+ ModelSubmissionHandler,
26
+ ModelValidator,
27
+ type RequestModelAction,
28
+ SOURCE_URI_ARG,
29
+ type SaveModelAction,
30
+ SetEditModeAction,
31
+ type SourceModelStorage,
32
+ TEMPORARY_CLIENT_ID
33
+ } from '@eclipse-glsp/server';
34
+ import { Debouncer, DisposableCollection } from '@hydranium/protocol';
35
+ import { inject, injectable, optional, postConstruct } from 'inversify';
36
+ import { type AstNode } from '@hydranium/langium';
37
+ import { URI } from '@hydranium/langium';
38
+ import { AstDocument, type AstDocumentSavedEvent, type AstDocumentUpdatedEvent, type ServerSharedServices } from '@hydranium/core';
39
+ import { DiagnosticSeverity } from 'vscode-languageserver-types';
40
+ import { type AbstractHydraniumGlspState } from '../state/abstract-hydranium-glsp-state.js';
41
+ import { type HydraniumGlspSubmissionHandler } from '../submission/hydranium-glsp-submission-handler.js';
42
+ import { HydraniumTypes } from '../state/hydranium-shared-core-services.js';
43
+ import { DEFAULT_SAVE_CONFLICT_POLICY, SaveConflictPolicy } from './save-conflict-policy.js';
44
+
45
+ /** Window (ms) over which back-to-back external rebuilds collapse into one resubmit. */
46
+ const EXTERNAL_SUBMIT_DEBOUNCE_MS = 250;
47
+
48
+ /**
49
+ * A leading URI scheme — two or more characters before the colon.
50
+ *
51
+ * RFC 3986 permits a single-character scheme, but requiring two is what keeps a
52
+ * Windows drive letter from reading as one. No scheme in play here is shorter
53
+ * than two characters, so the heuristic costs nothing and the alternative
54
+ * mis-parses every Windows path.
55
+ */
56
+ const URI_SCHEME = /^[a-zA-Z][a-zA-Z\d+\-.]+:/;
57
+
58
+ /**
59
+ * True when an (untyped) LSP diagnostic marks an unrecoverable syntax problem.
60
+ * Framework-level diagnostics are `unknown` (the shared `ModelService` is typed
61
+ * `<AstNode, unknown>`), so the shape is narrowed defensively before reading the
62
+ * Langium `data.code`.
63
+ */
64
+ function isStructuralDiagnostic(diagnostic: unknown): boolean {
65
+ if (typeof diagnostic !== 'object' || diagnostic === null) {
66
+ return false;
67
+ }
68
+ const candidate = diagnostic as { severity?: number; data?: { code?: unknown } };
69
+ const code = candidate.data?.code;
70
+ return candidate.severity === DiagnosticSeverity.Error && (code === 'lexing-error' || code === 'parsing-error');
71
+ }
72
+
73
+ /**
74
+ * GLSP source-model storage base shared by all hydranium adopters.
75
+ * Provides the DI signature, the {@link ClientSessionListener} +
76
+ * {@link Disposable} lifecycle plumbing (including the
77
+ * {@link DisposableCollection} drain on session disposal), the
78
+ * {@link RequestModelAction} / {@link SaveModelAction} URI-extraction
79
+ * helpers, and the live load/rebuild/resubmit flow with a settled-root
80
+ * invariant.
81
+ *
82
+ * **The settled-root invariant.** Every {@link AbstractHydraniumGlspState.setSourceRoot}
83
+ * the framework performs captures a root that has reached
84
+ * `IntegrityService.SettledState` (post-`Linked`, references indexed, on-build
85
+ * integrity rules applied). The GModel factory therefore always walks a fully
86
+ * linked + reprojected AST. The flow never captures off a transient mid-rebuild
87
+ * snapshot — notably it re-`settled()`s in the resubmit path rather than trusting
88
+ * the `onModelUpdated` event's document, which can arrive while a re-entered
89
+ * document is still being rebuilt.
90
+ *
91
+ * Adopters with richer needs override the seams ({@link isStructurallyBroken},
92
+ * {@link onParseErrorChanged}, {@link onSourceModelSettled}) rather than the
93
+ * whole flow, and select a {@link SaveConflictPolicy} via the bound option to
94
+ * tune how {@link saveSourceModel} reacts to a concurrent edit.
95
+ *
96
+ * **Default `saveSourceModel` flow.** Delegates to `ModelService.save`, which
97
+ * serialises through the language-specific `Serializer` bound at
98
+ * `services.serializer.Serializer` (resolved per-URI via `ServiceRegistry`),
99
+ * updates the multi-client text-document store, drives a rebuild, and writes the
100
+ * result via the `WritableFileSystemProvider`. The bound
101
+ * {@link SaveConflictPolicy} (default {@link DEFAULT_SAVE_CONFLICT_POLICY},
102
+ * `overwrite`) decides the based-on-version guard, await-vs-fire-and-forget, and
103
+ * failure handling.
104
+ *
105
+ * **tempId filter.** GLSP's `DefaultGlobalActionProvider` spins up a
106
+ * throwaway per-diagram-type container with upstream's {@link TEMPORARY_CLIENT_ID}
107
+ * placeholder purely to enumerate action kinds, then calls `unbindAll()`.
108
+ * The {@link postConstruct} hook skips registration for that placeholder so
109
+ * it doesn't linger in {@link ClientSessionManager}.
110
+ *
111
+ * **Lifecycle ordering.** `init` runs after DI resolution and parks the
112
+ * secondary-write-set watch, which outlives any single load.
113
+ * {@link sessionDisposed} calls {@link dispose}, which drains
114
+ * {@link toDispose} idempotently. Every transient subscription created in
115
+ * {@link doLoadSourceModel} is parked on {@link toDispose} so the drain catches
116
+ * them on client-detach.
117
+ */
118
+ @injectable()
119
+ export class HydraniumGlspStorage<TRoot extends AstNode, TSourceModel = string>
120
+ implements SourceModelStorage, ClientSessionListener, Disposable
121
+ {
122
+ @inject(GlspLogger) protected readonly logger!: GlspLogger;
123
+ @inject(HydraniumTypes.SharedCoreServices) protected readonly sharedServices!: ServerSharedServices;
124
+ @inject(ModelState) protected readonly state!: AbstractHydraniumGlspState<TRoot, TSourceModel>;
125
+ @inject(ClientSessionManager) protected readonly sessionManager!: ClientSessionManager;
126
+ @inject(ActionDispatcher) protected readonly actionDispatcher!: ActionDispatcher;
127
+ @inject(ModelSubmissionHandler) protected readonly submissionHandler!: HydraniumGlspSubmissionHandler<TRoot, TSourceModel>;
128
+ @inject(CommandStack) protected readonly commandStack!: DefaultCommandStack;
129
+
130
+ /**
131
+ * Optional {@link ModelValidator} whose markers are pushed to the client by
132
+ * {@link refreshDiagnosticMarkers}. Bound per diagram via
133
+ * `DiagramModule.bindModelValidator()`; absent (no push) when the head binds
134
+ * no validator. The framework default is `HydraniumGlspModelValidator`
135
+ * (LSP diagnostics → markers).
136
+ */
137
+ @inject(ModelValidator) @optional() protected readonly modelValidator?: ModelValidator;
138
+
139
+ /**
140
+ * Selected {@link SaveConflictPolicy} for {@link saveSourceModel}. Bind the
141
+ * {@link SaveConflictPolicy} token in a `DiagramModule` to choose a policy;
142
+ * left unbound it resolves to {@link DEFAULT_SAVE_CONFLICT_POLICY}
143
+ * (`overwrite`, last-write-wins). Read via {@link saveConflictPolicy} so the
144
+ * fallback is applied even when inversify injects `undefined` for an unbound
145
+ * `@optional()` member.
146
+ */
147
+ @inject(SaveConflictPolicy) @optional() protected readonly boundSaveConflictPolicy?: SaveConflictPolicy;
148
+
149
+ /** Disposables created during {@link doLoadSourceModel}; drained on session disposal. */
150
+ protected toDispose = new DisposableCollection();
151
+
152
+ /**
153
+ * Live update subscriptions for the state's SECONDARY documents, keyed by URI.
154
+ * Reconciled by {@link reconcileSecondarySubscriptions} after every capture.
155
+ *
156
+ * Held apart from {@link toDispose} because this set is not fixed for the
157
+ * session: a document can leave the write set, and its subscription has to go
158
+ * with it rather than linger until the client detaches.
159
+ */
160
+ protected readonly secondarySubscriptions = new Map<string, Disposable>();
161
+
162
+ /**
163
+ * Whether an external resubmit has ever been dispatched. Gates the
164
+ * keep-the-last-valid-GModel branch, which needs to know a canvas is already
165
+ * standing — not what is on it. The content comparison that decides whether a
166
+ * resubmit is worth dispatching lives on the submission handler, which is the
167
+ * only place that sees operation submits too.
168
+ */
169
+ protected hasSubmittedExternally = false;
170
+
171
+ /**
172
+ * Trailing-edge debounce for the external resubmit. Rescheduled on each
173
+ * {@link handleModelUpdated} so back-to-back rebuilds (cascade relinks,
174
+ * referenced-document edits) collapse into one {@link doUpdateAndSubmit}.
175
+ * Constructed in {@link init} (the injected {@link ServerSharedServices} —
176
+ * hence its `Clock` — is available by then); disposed on session teardown.
177
+ */
178
+ protected resubmitDebouncer!: Debouncer;
179
+
180
+ /** Latest event document awaiting the debounced resubmit; read by {@link flushResubmit} (last write wins). */
181
+ protected pendingResubmitDocument?: AstDocument<AstNode, unknown>;
182
+
183
+ @postConstruct()
184
+ protected init(): void {
185
+ this.resubmitDebouncer = new Debouncer(this.sharedServices.Clock, () => this.flushResubmit(), {
186
+ delayMs: EXTERNAL_SUBMIT_DEBOUNCE_MS
187
+ });
188
+ if (this.state.clientId === TEMPORARY_CLIENT_ID) {
189
+ return;
190
+ }
191
+ this.sessionManager.addListener(this, this.state.clientId);
192
+ // Watched for this storage's whole lifetime rather than per load. The load
193
+ // flow is a documented override point, so a subscription parked there would
194
+ // go with it — and the write set is discovered by operation handlers, which
195
+ // run long after any load.
196
+ this.toDispose.push(this.state.onSecondaryUrisChanged(() => this.reconcileSecondarySubscriptions()));
197
+ }
198
+
199
+ /**
200
+ * Default load flow — {@link doLoadSourceModel}. Adopters whose load flow
201
+ * needs more than the seams allow override this method.
202
+ */
203
+ loadSourceModel(action: RequestModelAction): MaybePromise<void> {
204
+ return this.doLoadSourceModel(action);
205
+ }
206
+
207
+ protected async doLoadSourceModel(action: RequestModelAction): Promise<void> {
208
+ const sourceUri = this.getSourceUri(action);
209
+ const rootUri = this.toSourceModelUri(sourceUri);
210
+ const modelService = this.sharedServices.model.ModelService;
211
+
212
+ // Open FIRST: a workspace-scanned-but-never-didOpened document rebuilds here,
213
+ // so the capture below settles on a built root rather than a transient
214
+ // mid-rebuild one.
215
+ this.toDispose.push(await modelService.open({ uri: rootUri, clientId: this.state.clientId }));
216
+
217
+ // GLSP's sessionDisposed is unreliable on Theia tab-close; dispose on client
218
+ // detach so reopens don't accumulate stale onModelUpdated listeners.
219
+ this.toDispose.push(
220
+ modelService.onClientClosed(rootUri, this.state.clientId, () => {
221
+ this.logger.info(`Client detached (${this.state.clientId}) — disposing storage subscriptions for ${rootUri}`);
222
+ this.dispose();
223
+ })
224
+ );
225
+
226
+ // React to external rebuilds: settle-gated capture + debounced/deduped resubmit.
227
+ this.toDispose.push(modelService.onModelUpdated(rootUri, event => this.handleModelUpdated(rootUri, event)));
228
+
229
+ // Coordinate the command stack's dirty state when another client saves.
230
+ this.toDispose.push(modelService.onModelSaved(rootUri, event => this.handleModelSaved(event)));
231
+
232
+ // Capture the initial settled root.
233
+ const document = await modelService.settled(rootUri);
234
+ await this.captureSettledRoot(rootUri, document);
235
+ }
236
+
237
+ /**
238
+ * Capture the initial settled root and apply the initial edit mode. `settled()`
239
+ * strips diagnostics by phase contract, so the first edit mode is `EDITABLE`;
240
+ * the first {@link handleModelUpdated} (at `Validated`, with diagnostics) flips
241
+ * it READONLY if the document has structural parse errors. Edit-mode +
242
+ * settle-hook actions are dispatched on a macrotask so the initial
243
+ * `requestModel → setModel` handshake isn't perturbed.
244
+ */
245
+ protected async captureSettledRoot(rootUri: string, document: AstDocument<AstNode, never>): Promise<void> {
246
+ this.state.setSourceRoot(rootUri, document.root as TRoot);
247
+ const actions = [...this.refreshEditMode(document), ...this.onSourceModelSettled(document)];
248
+ if (actions.length > 0) {
249
+ // Defer through the injectable Clock (not raw setTimeout) so the dispatch is
250
+ // testable and cancels on session disposal; parked on toDispose for that.
251
+ this.toDispose.push(this.sharedServices.Clock.setTimer(() => this.actionDispatcher.dispatchAll(actions), 0));
252
+ }
253
+ }
254
+
255
+ /**
256
+ * Reconcile {@link secondarySubscriptions} against the state's current
257
+ * secondary write set: dispose what left it, subscribe what arrived. Driven by
258
+ * {@link AbstractHydraniumGlspState.onSecondaryUrisChanged}, so it runs when
259
+ * the set actually changes rather than at points a caller guessed it might.
260
+ *
261
+ * **Secondaries need a subscription of their own because nothing else
262
+ * notifies for them.** The state records a secondary's version for the
263
+ * write-side conflict gate and adopter GModel factories re-read secondary
264
+ * content on every build, but no rebuild of a secondary reaches the primary's
265
+ * subscription: Langium's affected-documents walk runs from the referenced
266
+ * document to the referencing one, so a document that references the primary
267
+ * is downstream of it and editing it correctly affects nothing upstream.
268
+ * Manufacturing that invalidation to obtain a notification would cost a real
269
+ * rebuild per edit and make the affected set mean two things — the missing
270
+ * piece is a subscription, not an invalidation.
271
+ */
272
+ protected reconcileSecondarySubscriptions(): void {
273
+ const current = new Set(this.state.secondaryUris);
274
+ for (const [uri, subscription] of this.secondarySubscriptions) {
275
+ if (!current.has(uri)) {
276
+ subscription.dispose();
277
+ this.secondarySubscriptions.delete(uri);
278
+ }
279
+ }
280
+ for (const uri of current) {
281
+ if (!this.secondarySubscriptions.has(uri)) {
282
+ this.secondarySubscriptions.set(
283
+ uri,
284
+ this.sharedServices.model.ModelService.onModelUpdated(uri, event => this.handleSecondaryUpdated(uri, event))
285
+ );
286
+ }
287
+ }
288
+ }
289
+
290
+ /**
291
+ * React to a rebuild of a SECONDARY document by resubmitting the diagram, so
292
+ * an external edit to (say) a layout file reaches the canvas.
293
+ *
294
+ * Reuses {@link handleModelUpdated}'s authorship guard, and that is the load-
295
+ * bearing half rather than the resubmit: a diagram interaction that writes a
296
+ * secondary — a drag persisting bounds to a layout file — comes back through
297
+ * this listener as the client's own `changed` edit, and resubmitting on it
298
+ * fights the optimistic client-side move the user is still holding.
299
+ *
300
+ * Schedules the PRIMARY's current document, never the secondary's.
301
+ * {@link doUpdateAndSubmit} reads its event document only for the edit-mode
302
+ * and structurally-broken decisions, both of which are statements about the
303
+ * document the canvas edits: passing the secondary's would let a typo in a
304
+ * layout file flip the canvas READONLY and take the keep-the-last-valid-GModel
305
+ * branch off an unrelated document's parse state. The pending-document field
306
+ * is shared with the primary path, so it would poison a concurrently
307
+ * scheduled primary resubmit too.
308
+ *
309
+ * No marker refresh, unlike the primary path: a secondary's diagnostics reach
310
+ * the client only if an adopter's index registers elements as rendering that
311
+ * document, and refreshing here unconditionally would add a dispatch to every
312
+ * drag this client authors — the guard above gates the resubmit, not the
313
+ * markers.
314
+ */
315
+ protected handleSecondaryUpdated(uri: string, event: AstDocumentUpdatedEvent<AstNode, unknown>): void {
316
+ if (this.disposeIfStale(uri)) {
317
+ return;
318
+ }
319
+ if (this.state.clientId === event.sourceClientId && event.reason === 'changed') {
320
+ return;
321
+ }
322
+ const primary = this.currentPrimaryDocument();
323
+ if (primary === undefined) {
324
+ return;
325
+ }
326
+ this.logger.debug(`Secondary ${uri} rebuilt by ${event.sourceClientId} (${event.reason}) — scheduling resubmit`);
327
+ this.scheduleUpdateAndSubmit(primary);
328
+ }
329
+
330
+ /**
331
+ * The primary document's current state, or `undefined` when it is not
332
+ * registered.
333
+ *
334
+ * Synchronous by construction — the phase-agnostic lookup door plus the shared
335
+ * projection — so reacting to a secondary neither waits nor can force a build.
336
+ * `settled()` is not an alternative: it strips diagnostics by phase contract,
337
+ * and diagnostics are the reason {@link doUpdateAndSubmit} takes a separate
338
+ * event document at all.
339
+ */
340
+ protected currentPrimaryDocument(): AstDocument<AstNode, unknown> | undefined {
341
+ const document = this.sharedServices.model.ModelService.getDocument(this.state.sourceUri);
342
+ return document ? AstDocument.from(document) : undefined;
343
+ }
344
+
345
+ /**
346
+ * React to an external rebuild reaching `Validated`. Self-cleans if the
347
+ * session has vanished but this listener leaked (neither `sessionDisposed`
348
+ * nor `onClientClosed` fired on tab close). Resubmits for any change not
349
+ * authored by this client's own optimistic `changed` edit, then refreshes
350
+ * diagnostic markers for every update — including own edits, which skip
351
+ * the resubmit but can still change diagnostics.
352
+ */
353
+ protected async handleModelUpdated(rootUri: string, event: AstDocumentUpdatedEvent<AstNode, unknown>): Promise<void> {
354
+ if (this.disposeIfStale(rootUri)) {
355
+ return;
356
+ }
357
+ if (this.state.clientId !== event.sourceClientId || event.reason !== 'changed') {
358
+ this.scheduleUpdateAndSubmit(event.document);
359
+ }
360
+ await this.refreshDiagnosticMarkers();
361
+ }
362
+
363
+ /**
364
+ * Whether this storage has outlived its session — disposing it as a side
365
+ * effect when so. Checked from every subscription entry point because GLSP's
366
+ * `sessionDisposed` is unreliable on Theia tab-close and `onClientClosed` can
367
+ * miss it as well; a listener that survives its session otherwise keeps firing
368
+ * for the life of the process. `context` names the document whose event
369
+ * surfaced the leak.
370
+ */
371
+ protected disposeIfStale(context: string): boolean {
372
+ if (this.sessionManager.getSession(this.state.clientId)) {
373
+ return false;
374
+ }
375
+ this.logger.warn(`Stale storage (clientId=${this.state.clientId} not in ClientSessionManager) — self-disposing ${context}`);
376
+ this.dispose();
377
+ return true;
378
+ }
379
+
380
+ /**
381
+ * Record the latest document and (re)arm the trailing-edge
382
+ * {@link resubmitDebouncer}, so a burst of rebuilds runs
383
+ * {@link doUpdateAndSubmit} once with the most recent document.
384
+ */
385
+ protected scheduleUpdateAndSubmit(document: AstDocument<AstNode, unknown>): void {
386
+ this.pendingResubmitDocument = document;
387
+ this.resubmitDebouncer.schedule();
388
+ }
389
+
390
+ /**
391
+ * Debouncer callback: run the resubmit for the latest pending document
392
+ * against this storage's (invariant) source URI and dispatch the result.
393
+ * Fire-and-forget — failures are logged, not surfaced to the timer caller.
394
+ */
395
+ protected flushResubmit(): void {
396
+ const document = this.pendingResubmitDocument;
397
+ if (document === undefined) {
398
+ return;
399
+ }
400
+ const rootUri = this.state.sourceUri;
401
+ this.doUpdateAndSubmit(rootUri, document).then(
402
+ actions => this.actionDispatcher.dispatchAll(actions),
403
+ error => this.logger.error(`Update-and-submit failed for ${rootUri}: ${error instanceof Error ? error.message : String(error)}`)
404
+ );
405
+ }
406
+
407
+ /**
408
+ * Coordinate the command stack when another client persists the document:
409
+ * mark our stack clean so the editor doesn't prompt to re-save identical
410
+ * content. Our own saves already settle the stack through the save flow.
411
+ */
412
+ protected handleModelSaved(event: AstDocumentSavedEvent<AstNode, unknown>): void {
413
+ if (this.state.clientId !== event.sourceClientId) {
414
+ this.commandStack.saveIsDone();
415
+ }
416
+ }
417
+
418
+ /**
419
+ * Re-settle to a guaranteed fully-linked + reprojected root, capture it, and
420
+ * (unless suppressed) resubmit a deduped external GModel. Diagnostics for the
421
+ * edit-mode decision come from the event document (`settled()` strips them by
422
+ * phase contract).
423
+ */
424
+ protected async doUpdateAndSubmit(rootUri: string, eventDocument: AstDocument<AstNode, unknown>): Promise<Action[]> {
425
+ // Settle-gate the capture: never setSourceRoot off the event's possibly-transient
426
+ // snapshot — the event can arrive while a re-entered document is mid-rebuild.
427
+ const document = await this.sharedServices.model.ModelService.settled(rootUri);
428
+ this.state.setSourceRoot(rootUri, document.root as TRoot);
429
+ const editModeActions = this.refreshEditMode(eventDocument);
430
+
431
+ // Skip the external submit until the initial requestModel completes; submitting too
432
+ // early bumps root.revision and the client's stale first computedBounds is dropped,
433
+ // leaving the canvas empty.
434
+ if (this.submissionHandler.hasPendingInitialRequest()) {
435
+ return editModeActions;
436
+ }
437
+ // While the AST is structurally broken, keep the last valid GModel so the canvas
438
+ // doesn't blank mid-typing. READONLY is still flipped for user feedback.
439
+ if (this.isStructurallyBroken(eventDocument) && this.hasSubmittedExternally) {
440
+ return editModeActions;
441
+ }
442
+ // Read the handler's signature BEFORE submitting and again after: it records
443
+ // every submission whatever the reason, so an unmoved signature means this
444
+ // rebuild produced the graph the client already has. Comparing only against
445
+ // our own previous EXTERNAL submit cannot see that, which is why a drag used
446
+ // to be answered by an echo of itself — the operation had delivered the
447
+ // model, and the external comparison had never heard of it.
448
+ const previousSignature = this.submissionHandler.lastSubmittedSignature;
449
+ const submitActions = await this.submissionHandler.submitModel('external');
450
+ if (this.submissionHandler.lastSubmittedSignature === previousSignature) {
451
+ return editModeActions;
452
+ }
453
+ this.hasSubmittedExternally = true;
454
+ return [...submitActions, ...editModeActions];
455
+ }
456
+
457
+ /**
458
+ * Recompute the desired edit mode from the document's structural-error state,
459
+ * flip {@link AbstractHydraniumGlspState.editMode}, and — only on a transition —
460
+ * return the {@link onParseErrorChanged} actions. No transition → no actions.
461
+ */
462
+ protected refreshEditMode(document: AstDocument<AstNode, unknown>): Action[] {
463
+ const broken = this.isStructurallyBroken(document);
464
+ const previousEditMode = this.state.editMode;
465
+ this.state.editMode = broken ? EditMode.READONLY : EditMode.EDITABLE;
466
+ if (previousEditMode === this.state.editMode) {
467
+ return [];
468
+ }
469
+ return this.onParseErrorChanged(document, broken);
470
+ }
471
+
472
+ /**
473
+ * Seam: is the AST structurally broken (lexing/parsing errors)? Drives both
474
+ * READONLY mode and the GModel resubmit skip — keeping the last valid canvas
475
+ * instead of blanking it mid-typing. Default: any error-severity diagnostic
476
+ * carrying a Langium `lexing-error` / `parsing-error` code. Adopters override
477
+ * to compose additional break reasons.
478
+ */
479
+ protected isStructurallyBroken(document: AstDocument<AstNode, unknown>): boolean {
480
+ return document.diagnostics.some(diagnostic => isStructuralDiagnostic(diagnostic));
481
+ }
482
+
483
+ /**
484
+ * Seam: actions to emit when the structural-broken state transitions. Default:
485
+ * a single {@link SetEditModeAction} toggling READONLY ↔ EDITABLE — editing is
486
+ * disabled while the syntax is broken but re-enabled once it parses, a sound
487
+ * generic for any Langium-backed diagram. Adopters override to add or replace
488
+ * the transition feedback. The {@link AbstractHydraniumGlspState.editMode}
489
+ * flip itself is owned by {@link refreshEditMode}.
490
+ */
491
+ protected onParseErrorChanged(_document: AstDocument<AstNode, unknown>, broken: boolean): Action[] {
492
+ return [SetEditModeAction.create(broken ? EditMode.READONLY : EditMode.EDITABLE)];
493
+ }
494
+
495
+ /**
496
+ * Seam: extra actions to dispatch right after the initial settled root is
497
+ * captured (e.g. an adopter-specific status or palette refresh). Default: none.
498
+ */
499
+ protected onSourceModelSettled(_document: AstDocument<AstNode, never>): Action[] {
500
+ return [];
501
+ }
502
+
503
+ /** The effective {@link SaveConflictPolicy}: the bound option, or {@link DEFAULT_SAVE_CONFLICT_POLICY} when unbound. */
504
+ protected get saveConflictPolicy(): SaveConflictPolicy {
505
+ return this.boundSaveConflictPolicy ?? DEFAULT_SAVE_CONFLICT_POLICY;
506
+ }
507
+
508
+ /**
509
+ * Default save flow: route through `ModelService.save`. The framework
510
+ * serialise-and-persist machinery handles serialisation (via the
511
+ * language-specific `Serializer` bound at `services.serializer.Serializer`),
512
+ * the multi-client text-document update, the rebuild, and the eventual
513
+ * `WritableFileSystemProvider.writeFile`.
514
+ *
515
+ * The configured {@link SaveConflictPolicy} (see {@link saveConflictPolicy})
516
+ * decides how a concurrent edit that advanced the document is handled:
517
+ * - `overwrite` (default): no based-on guard, await, propagate failures —
518
+ * last-write-wins, correct for a single-editor head.
519
+ * - `reject`: guard on the captured `state.version`, await, surface a
520
+ * `ConflictError` (and any other failure) to the GLSP save action.
521
+ * - `drop-and-log`: guard, fire-and-forget, log + swallow failures — a stale
522
+ * diagram save is dropped because a concurrent form/code edit already wrote
523
+ * the truth, and GLSP exposes no save-failure back-channel.
524
+ *
525
+ * Returns `MaybePromise<void>` to match upstream `SourceModelStorage`:
526
+ * resolves with the persist under `overwrite`/`reject`, returns synchronously
527
+ * under `drop-and-log`. Adopters that bypass `ModelService` (writing through
528
+ * `WritableFileSystemProvider` directly) still override the whole method.
529
+ */
530
+ saveSourceModel(action: SaveModelAction): MaybePromise<void> {
531
+ // Normalised for the same reason the load path is: `SaveModelAction.fileUri`
532
+ // is client-supplied on a "Save As" and carries whichever form that client
533
+ // uses. The state fallback is already a URI string, and normalising one is
534
+ // idempotent, so this only changes the client-supplied branch. Applied at
535
+ // the call site rather than inside `getFileUri` so that method's contract —
536
+ // return what the action said — is unchanged for adopters overriding it.
537
+ const uri = this.toSourceModelUri(this.getFileUri(action));
538
+ const policy = this.saveConflictPolicy;
539
+ const persisted = this.sharedServices.model.ModelService.save({
540
+ uri,
541
+ model: this.state.sourceRoot,
542
+ clientId: this.state.clientId,
543
+ baseVersion: policy.kind === 'overwrite' ? undefined : this.state.version
544
+ }).then(() => undefined);
545
+
546
+ if (policy.kind === 'drop-and-log') {
547
+ // Fire-and-forget: the diagram save lost the race to a concurrent edit
548
+ // that already persisted the truth, so it must neither block the action
549
+ // nor surface. Log rather than leave an unhandled rejection.
550
+ persisted.catch(error => this.logger.error(`Save failed for ${uri}: ${error instanceof Error ? error.message : String(error)}`));
551
+ return undefined;
552
+ }
553
+ // overwrite / reject: await; a ConflictError (reject) or any other failure
554
+ // propagates to GLSP's save-action handler.
555
+ return persisted;
556
+ }
557
+
558
+ /**
559
+ * Normalise GLSP's `sourceUri` — which may be a filesystem PATH or a URI
560
+ * string — to the URI string the workspace keys documents by.
561
+ *
562
+ * **The protocol does not pin which form a client sends, and the clients in
563
+ * play disagree.** GLSP's VS Code integration sends `document.uri.toString()`;
564
+ * a headless caller passes the path it already has. Committing to either one
565
+ * alone is what this method exists to avoid.
566
+ *
567
+ * **Getting it wrong does not throw where the mistake is.** `URI.file` given
568
+ * a URI string treats the whole thing as a path, percent-encodes the scheme,
569
+ * and yields an `fsPath` with that scheme buried in the middle. Nothing
570
+ * rejects it; it travels down to the filesystem and surfaces as an ENOENT
571
+ * several layers from the coercion that produced it.
572
+ *
573
+ * Adopters whose client sends a third form override this.
574
+ */
575
+ protected toSourceModelUri(sourceUri: string): string {
576
+ return URI_SCHEME.test(sourceUri) ? URI.parse(sourceUri).toString() : URI.file(sourceUri).toString();
577
+ }
578
+
579
+ /**
580
+ * Extract the source URI from a {@link RequestModelAction}. Throws
581
+ * {@link GLSPServerError} if the option is missing or non-string —
582
+ * GLSP's protocol contract requires the client to pass `sourceUri`
583
+ * when initiating a model request.
584
+ *
585
+ * Returns it VERBATIM, in whatever form the client sent;
586
+ * {@link toSourceModelUri} is what normalises it.
587
+ */
588
+ protected getSourceUri(action: RequestModelAction): string {
589
+ const sourceUri = action.options?.[SOURCE_URI_ARG];
590
+ if (typeof sourceUri !== 'string') {
591
+ throw new GLSPServerError(`Invalid RequestModelAction! Missing argument with key '${SOURCE_URI_ARG}'`);
592
+ }
593
+ return sourceUri;
594
+ }
595
+
596
+ /**
597
+ * Resolve the URI to save to: prefer the {@link SaveModelAction.fileUri}
598
+ * (set when the user chose "Save As"), fall back to the
599
+ * {@link SOURCE_URI_ARG} mirrored into the inherited properties map by
600
+ * {@link AbstractHydraniumGlspState.setSourceRoot}.
601
+ *
602
+ * A save action carries no request id, so a throw here reaches the user as a
603
+ * toast built from `message`, with `cause` behind its details control. The
604
+ * two therefore address different readers and must not be collapsed: the
605
+ * message names what failed in the user's terms, and the cause names which
606
+ * lookups came back empty for whom. Neither can name the URI — not having
607
+ * one IS the failure, and both sources are written together, so the typed
608
+ * `sourceUri` is equally empty whenever this fires.
609
+ */
610
+ protected getFileUri(action: SaveModelAction): string {
611
+ const uri = action.fileUri ?? this.state.get<string>(SOURCE_URI_ARG);
612
+ if (!uri) {
613
+ throw new GLSPServerError(
614
+ 'Could not determine where to save this model',
615
+ `no fileUri on the save action and no ${SOURCE_URI_ARG} in the model state (clientId=${this.state.clientId})`
616
+ );
617
+ }
618
+ return uri;
619
+ }
620
+
621
+ /**
622
+ * Recompute the bound {@link ModelValidator}'s markers and push them to the
623
+ * client as a `SetMarkersAction` — asynchronously and independently of model
624
+ * rendering. Called from {@link handleModelUpdated} (which fires at `Validated`),
625
+ * so diagram markers refresh without gating the diagram submit on validation.
626
+ * No-op when no validator is bound.
627
+ *
628
+ * Always dispatched under {@link MarkersReason.BATCH}: the GLSP client's
629
+ * `ValidationFeedbackEmitter` keys feedback by reason and replaces the prior
630
+ * set, so re-pushing the full set clears stale markers (an empty set clears
631
+ * all once the model is valid), and sharing the tool-palette validate
632
+ * command's `BATCH` reason means the two paths replace rather than duplicate.
633
+ */
634
+ async refreshDiagnosticMarkers(): Promise<void> {
635
+ if (!this.modelValidator) {
636
+ return;
637
+ }
638
+ const markers = await this.modelValidator.validate([this.state.root], MarkersReason.BATCH);
639
+ this.actionDispatcher.dispatch(SetMarkersAction.create(markers, { reason: MarkersReason.BATCH }));
640
+ }
641
+
642
+ sessionDisposed(_clientSession: ClientSession): void {
643
+ this.dispose();
644
+ }
645
+
646
+ /**
647
+ * Cancel any pending resubmit and drain every subscription — those registered
648
+ * during {@link doLoadSourceModel} and the per-secondary ones, which live
649
+ * outside {@link toDispose} because they come and go with the write set.
650
+ * Idempotent.
651
+ */
652
+ dispose(): void {
653
+ this.resubmitDebouncer?.dispose();
654
+ for (const subscription of this.secondarySubscriptions.values()) {
655
+ subscription.dispose();
656
+ }
657
+ this.secondarySubscriptions.clear();
658
+ this.toDispose.dispose();
659
+ }
660
+ }
@@ -0,0 +1,11 @@
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-storage.js';
11
+ export * from './save-conflict-policy.js';