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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/README.md +80 -88
  2. package/lib/browser/start-glsp-server-in-worker.d.ts +43 -42
  3. package/lib/browser/start-glsp-server-in-worker.d.ts.map +1 -1
  4. package/lib/browser/start-glsp-server-in-worker.js +62 -13
  5. package/lib/browser/start-glsp-server-in-worker.js.map +1 -1
  6. package/lib/command/diagram-edit-dropped-error.d.ts +31 -0
  7. package/lib/command/diagram-edit-dropped-error.d.ts.map +1 -0
  8. package/lib/command/diagram-edit-dropped-error.js +48 -0
  9. package/lib/command/diagram-edit-dropped-error.js.map +1 -0
  10. package/lib/command/hydranium-glsp-command-stack.d.ts +33 -0
  11. package/lib/command/hydranium-glsp-command-stack.d.ts.map +1 -0
  12. package/lib/command/hydranium-glsp-command-stack.js +63 -0
  13. package/lib/command/hydranium-glsp-command-stack.js.map +1 -0
  14. package/lib/command/hydranium-glsp-operation-action-handler.d.ts +64 -0
  15. package/lib/command/hydranium-glsp-operation-action-handler.d.ts.map +1 -0
  16. package/lib/command/hydranium-glsp-operation-action-handler.js +142 -0
  17. package/lib/command/hydranium-glsp-operation-action-handler.js.map +1 -0
  18. package/lib/command/hydranium-glsp-operation-command.d.ts +250 -0
  19. package/lib/command/hydranium-glsp-operation-command.d.ts.map +1 -0
  20. package/lib/command/hydranium-glsp-operation-command.js +491 -0
  21. package/lib/command/hydranium-glsp-operation-command.js.map +1 -0
  22. package/lib/command/hydranium-glsp-recording-command.d.ts +26 -87
  23. package/lib/command/hydranium-glsp-recording-command.d.ts.map +1 -1
  24. package/lib/command/hydranium-glsp-recording-command.js +40 -117
  25. package/lib/command/hydranium-glsp-recording-command.js.map +1 -1
  26. package/lib/command/hydranium-glsp-undo-redo-action-handler.d.ts +23 -0
  27. package/lib/command/hydranium-glsp-undo-redo-action-handler.d.ts.map +1 -0
  28. package/lib/command/hydranium-glsp-undo-redo-action-handler.js +41 -0
  29. package/lib/command/hydranium-glsp-undo-redo-action-handler.js.map +1 -0
  30. package/lib/command/index.d.ts +5 -0
  31. package/lib/command/index.d.ts.map +1 -1
  32. package/lib/command/index.js +5 -0
  33. package/lib/command/index.js.map +1 -1
  34. package/lib/computed-bounds/hydranium-glsp-computed-bounds-action-handler.d.ts +1 -1
  35. package/lib/computed-bounds/hydranium-glsp-computed-bounds-action-handler.d.ts.map +1 -1
  36. package/lib/computed-bounds/hydranium-glsp-computed-bounds-action-handler.js +2 -2
  37. package/lib/computed-bounds/hydranium-glsp-computed-bounds-action-handler.js.map +1 -1
  38. package/lib/dispatcher/server-action-dispatcher.d.ts +25 -24
  39. package/lib/dispatcher/server-action-dispatcher.d.ts.map +1 -1
  40. package/lib/dispatcher/server-action-dispatcher.js +38 -26
  41. package/lib/dispatcher/server-action-dispatcher.js.map +1 -1
  42. package/lib/index.d.ts +1 -0
  43. package/lib/index.d.ts.map +1 -1
  44. package/lib/index.js +1 -0
  45. package/lib/index.js.map +1 -1
  46. package/lib/launcher/abstract-hydranium-glsp-diagram-module.d.ts +35 -59
  47. package/lib/launcher/abstract-hydranium-glsp-diagram-module.d.ts.map +1 -1
  48. package/lib/launcher/abstract-hydranium-glsp-diagram-module.js +60 -71
  49. package/lib/launcher/abstract-hydranium-glsp-diagram-module.js.map +1 -1
  50. package/lib/launcher/drop-notifications-to-gone-client.d.ts +23 -0
  51. package/lib/launcher/drop-notifications-to-gone-client.d.ts.map +1 -0
  52. package/lib/launcher/drop-notifications-to-gone-client.js +43 -0
  53. package/lib/launcher/drop-notifications-to-gone-client.js.map +1 -0
  54. package/lib/launcher/glsp-app-module.d.ts +1 -2
  55. package/lib/launcher/glsp-app-module.d.ts.map +1 -1
  56. package/lib/launcher/glsp-app-module.js.map +1 -1
  57. package/lib/launcher/glsp-connection-logger.d.ts +19 -0
  58. package/lib/launcher/glsp-connection-logger.d.ts.map +1 -0
  59. package/lib/launcher/glsp-connection-logger.js +24 -0
  60. package/lib/launcher/glsp-connection-logger.js.map +1 -0
  61. package/lib/launcher/glsp-server-overrides.d.ts +10 -8
  62. package/lib/launcher/glsp-server-overrides.d.ts.map +1 -1
  63. package/lib/launcher/glsp-server-overrides.js +25 -11
  64. package/lib/launcher/glsp-server-overrides.js.map +1 -1
  65. package/lib/launcher/hydranium-glsp-server.d.ts +60 -15
  66. package/lib/launcher/hydranium-glsp-server.d.ts.map +1 -1
  67. package/lib/launcher/hydranium-glsp-server.js +145 -22
  68. package/lib/launcher/hydranium-glsp-server.js.map +1 -1
  69. package/lib/launcher/index.d.ts +2 -0
  70. package/lib/launcher/index.d.ts.map +1 -1
  71. package/lib/launcher/index.js +5 -4
  72. package/lib/launcher/index.js.map +1 -1
  73. package/lib/logging/glsp-client-logger.d.ts +10 -6
  74. package/lib/logging/glsp-client-logger.d.ts.map +1 -1
  75. package/lib/logging/glsp-client-logger.js +14 -7
  76. package/lib/logging/glsp-client-logger.js.map +1 -1
  77. package/lib/messages/index.d.ts +11 -6
  78. package/lib/messages/index.d.ts.map +1 -1
  79. package/lib/messages/index.js +11 -6
  80. package/lib/messages/index.js.map +1 -1
  81. package/lib/node/start-glsp-server.d.ts +50 -15
  82. package/lib/node/start-glsp-server.d.ts.map +1 -1
  83. package/lib/node/start-glsp-server.js +97 -30
  84. package/lib/node/start-glsp-server.js.map +1 -1
  85. package/lib/state/abstract-hydranium-glsp-state.d.ts +326 -102
  86. package/lib/state/abstract-hydranium-glsp-state.d.ts.map +1 -1
  87. package/lib/state/abstract-hydranium-glsp-state.js +529 -139
  88. package/lib/state/abstract-hydranium-glsp-state.js.map +1 -1
  89. package/lib/state/diagram-status.d.ts +38 -0
  90. package/lib/state/diagram-status.d.ts.map +1 -0
  91. package/lib/state/diagram-status.js +27 -0
  92. package/lib/state/diagram-status.js.map +1 -0
  93. package/lib/state/full-text-hydranium-glsp-state.d.ts +22 -9
  94. package/lib/state/full-text-hydranium-glsp-state.d.ts.map +1 -1
  95. package/lib/state/full-text-hydranium-glsp-state.js +45 -18
  96. package/lib/state/full-text-hydranium-glsp-state.js.map +1 -1
  97. package/lib/state/hydranium-glsp-index.d.ts +40 -29
  98. package/lib/state/hydranium-glsp-index.d.ts.map +1 -1
  99. package/lib/state/hydranium-glsp-index.js +82 -45
  100. package/lib/state/hydranium-glsp-index.js.map +1 -1
  101. package/lib/state/hydranium-shared-core-services.d.ts +16 -29
  102. package/lib/state/hydranium-shared-core-services.d.ts.map +1 -1
  103. package/lib/state/hydranium-shared-core-services.js +15 -28
  104. package/lib/state/hydranium-shared-core-services.js.map +1 -1
  105. package/lib/state/index.d.ts +1 -0
  106. package/lib/state/index.d.ts.map +1 -1
  107. package/lib/state/index.js +1 -0
  108. package/lib/state/index.js.map +1 -1
  109. package/lib/state/model-ready-timeout-error.d.ts.map +1 -1
  110. package/lib/state/model-ready-timeout-error.js.map +1 -1
  111. package/lib/state/reconcile-source-model-write.d.ts +13 -27
  112. package/lib/state/reconcile-source-model-write.d.ts.map +1 -1
  113. package/lib/state/reconcile-source-model-write.js +44 -32
  114. package/lib/state/reconcile-source-model-write.js.map +1 -1
  115. package/lib/state/reconciling-multi-document-glsp-state.d.ts +125 -69
  116. package/lib/state/reconciling-multi-document-glsp-state.d.ts.map +1 -1
  117. package/lib/state/reconciling-multi-document-glsp-state.js +242 -100
  118. package/lib/state/reconciling-multi-document-glsp-state.js.map +1 -1
  119. package/lib/state/reconciling-transfer-hydranium-glsp-state.d.ts +36 -28
  120. package/lib/state/reconciling-transfer-hydranium-glsp-state.d.ts.map +1 -1
  121. package/lib/state/reconciling-transfer-hydranium-glsp-state.js +59 -42
  122. package/lib/state/reconciling-transfer-hydranium-glsp-state.js.map +1 -1
  123. package/lib/status/diagram-status-reporter.d.ts +44 -0
  124. package/lib/status/diagram-status-reporter.d.ts.map +1 -0
  125. package/lib/status/diagram-status-reporter.js +88 -0
  126. package/lib/status/diagram-status-reporter.js.map +1 -0
  127. package/lib/status/hydranium-glsp-request-model-action-handler.d.ts +27 -0
  128. package/lib/status/hydranium-glsp-request-model-action-handler.d.ts.map +1 -0
  129. package/lib/status/hydranium-glsp-request-model-action-handler.js +52 -0
  130. package/lib/status/hydranium-glsp-request-model-action-handler.js.map +1 -0
  131. package/lib/status/hydranium-glsp-set-edit-mode-action-handler.d.ts +27 -0
  132. package/lib/status/hydranium-glsp-set-edit-mode-action-handler.d.ts.map +1 -0
  133. package/lib/status/hydranium-glsp-set-edit-mode-action-handler.js +53 -0
  134. package/lib/status/hydranium-glsp-set-edit-mode-action-handler.js.map +1 -0
  135. package/lib/status/index.d.ts +12 -0
  136. package/lib/status/index.d.ts.map +1 -0
  137. package/lib/status/index.js +12 -0
  138. package/lib/status/index.js.map +1 -0
  139. package/lib/storage/hydranium-glsp-request-save-model-action-handler.d.ts +30 -0
  140. package/lib/storage/hydranium-glsp-request-save-model-action-handler.d.ts.map +1 -0
  141. package/lib/storage/hydranium-glsp-request-save-model-action-handler.js +47 -0
  142. package/lib/storage/hydranium-glsp-request-save-model-action-handler.js.map +1 -0
  143. package/lib/storage/hydranium-glsp-storage.d.ts +324 -143
  144. package/lib/storage/hydranium-glsp-storage.d.ts.map +1 -1
  145. package/lib/storage/hydranium-glsp-storage.js +569 -213
  146. package/lib/storage/hydranium-glsp-storage.js.map +1 -1
  147. package/lib/storage/index.d.ts +2 -1
  148. package/lib/storage/index.d.ts.map +1 -1
  149. package/lib/storage/index.js +2 -1
  150. package/lib/storage/index.js.map +1 -1
  151. package/lib/storage/save-delivery-policy.d.ts +51 -0
  152. package/lib/storage/save-delivery-policy.d.ts.map +1 -0
  153. package/lib/storage/{save-conflict-policy.js → save-delivery-policy.js} +7 -8
  154. package/lib/storage/save-delivery-policy.js.map +1 -0
  155. package/lib/submission/hydranium-glsp-submission-handler.d.ts +44 -4
  156. package/lib/submission/hydranium-glsp-submission-handler.d.ts.map +1 -1
  157. package/lib/submission/hydranium-glsp-submission-handler.js +82 -6
  158. package/lib/submission/hydranium-glsp-submission-handler.js.map +1 -1
  159. package/lib/testing/glsp-harness.d.ts +34 -7
  160. package/lib/testing/glsp-harness.d.ts.map +1 -1
  161. package/lib/testing/glsp-harness.js +47 -19
  162. package/lib/testing/glsp-harness.js.map +1 -1
  163. package/lib/validation/diagnostic-markers.d.ts +3 -3
  164. package/lib/validation/diagnostic-markers.d.ts.map +1 -1
  165. package/lib/validation/diagnostic-markers.js +1 -1
  166. package/lib/validation/diagnostic-markers.js.map +1 -1
  167. package/lib/validation/hydranium-glsp-model-validator.d.ts +1 -2
  168. package/lib/validation/hydranium-glsp-model-validator.d.ts.map +1 -1
  169. package/lib/validation/hydranium-glsp-model-validator.js +1 -1
  170. package/lib/validation/hydranium-glsp-model-validator.js.map +1 -1
  171. package/package.json +31 -52
  172. package/src/browser/start-glsp-server-in-worker.ts +79 -51
  173. package/src/command/diagram-edit-dropped-error.ts +51 -0
  174. package/src/command/hydranium-glsp-command-stack.ts +47 -0
  175. package/src/command/hydranium-glsp-operation-action-handler.ts +141 -0
  176. package/src/command/hydranium-glsp-operation-command.ts +602 -0
  177. package/src/command/hydranium-glsp-recording-command.ts +40 -121
  178. package/src/command/hydranium-glsp-undo-redo-action-handler.ts +29 -0
  179. package/src/command/index.ts +10 -0
  180. package/src/computed-bounds/hydranium-glsp-computed-bounds-action-handler.ts +2 -2
  181. package/src/dispatcher/server-action-dispatcher.ts +39 -31
  182. package/src/index.ts +1 -0
  183. package/src/launcher/abstract-hydranium-glsp-diagram-module.ts +79 -91
  184. package/src/launcher/drop-notifications-to-gone-client.ts +47 -0
  185. package/src/launcher/glsp-app-module.ts +1 -2
  186. package/src/launcher/glsp-connection-logger.ts +27 -0
  187. package/src/launcher/glsp-server-overrides.ts +27 -11
  188. package/src/launcher/hydranium-glsp-server.ts +153 -23
  189. package/src/launcher/index.ts +5 -4
  190. package/src/logging/glsp-client-logger.ts +16 -9
  191. package/src/messages/index.ts +16 -6
  192. package/src/node/start-glsp-server.ts +133 -40
  193. package/src/state/abstract-hydranium-glsp-state.ts +609 -154
  194. package/src/state/diagram-status.ts +41 -0
  195. package/src/state/full-text-hydranium-glsp-state.ts +46 -18
  196. package/src/state/hydranium-glsp-index.ts +85 -41
  197. package/src/state/hydranium-shared-core-services.ts +16 -29
  198. package/src/state/index.ts +1 -0
  199. package/src/state/reconcile-source-model-write.ts +51 -53
  200. package/src/state/reconciling-multi-document-glsp-state.ts +276 -102
  201. package/src/state/reconciling-transfer-hydranium-glsp-state.ts +67 -45
  202. package/src/status/diagram-status-reporter.ts +106 -0
  203. package/src/status/hydranium-glsp-request-model-action-handler.ts +43 -0
  204. package/src/status/hydranium-glsp-set-edit-mode-action-handler.ts +38 -0
  205. package/src/status/index.ts +12 -0
  206. package/src/storage/hydranium-glsp-request-save-model-action-handler.ts +40 -0
  207. package/src/storage/hydranium-glsp-storage.ts +647 -223
  208. package/src/storage/index.ts +2 -1
  209. package/src/storage/save-delivery-policy.ts +49 -0
  210. package/src/submission/hydranium-glsp-submission-handler.ts +91 -5
  211. package/src/testing/glsp-harness.ts +67 -25
  212. package/src/validation/diagnostic-markers.ts +3 -3
  213. package/src/validation/hydranium-glsp-model-validator.ts +4 -5
  214. package/lib/storage/save-conflict-policy.d.ts +0 -56
  215. package/lib/storage/save-conflict-policy.d.ts.map +0 -1
  216. package/lib/storage/save-conflict-policy.js.map +0 -1
  217. package/src/storage/save-conflict-policy.ts +0 -52
@@ -0,0 +1,602 @@
1
+ /********************************************************************************
2
+ * Copyright (c) 2026 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, Command, CompoundCommand, type MaybePromise } from '@eclipse-glsp/server';
11
+ import {
12
+ type AstNode,
13
+ type AstNodeDescription,
14
+ AstUtils,
15
+ type GenericAstNode,
16
+ isMultiReference,
17
+ type LinkingError,
18
+ type MultiReference,
19
+ type MultiReferenceItem,
20
+ type Reference
21
+ } from '@hydranium/langium';
22
+ import { type ModelVersion, describeErrorWithStack } from '@hydranium/protocol';
23
+ import { DiagramEditDroppedError } from './diagram-edit-dropped-error.js';
24
+ import { type HydraniumGlspRecordingState } from './hydranium-glsp-recording-command.js';
25
+
26
+ /**
27
+ * What becomes of an operation {@link HydraniumGlspOperationCommand} ran: its
28
+ * command written, its write dropped, or no command.
29
+ */
30
+ export type OperationOutcome = 'executed' | 'dropped' | 'none';
31
+
32
+ /**
33
+ * One executed side effect the operation undoes and redoes: a recording
34
+ * command's bridge, or a command that does not record.
35
+ */
36
+ export interface OperationSideEffect {
37
+ undo(): MaybePromise<void>;
38
+ redo(): MaybePromise<void>;
39
+ }
40
+
41
+ /** The source model an operation changed, before and after it; a redo applies `from → to`, an undo the reverse. */
42
+ export interface OperationTransition<TSourceModel> {
43
+ readonly from: TSourceModel;
44
+ readonly to: TSourceModel;
45
+ }
46
+
47
+ /** Whether an undo or a redo of an operation runs. */
48
+ export type UndoRedoDirection = 'undo' | 'redo';
49
+
50
+ /**
51
+ * The members of an operation the framework's state, recording command and
52
+ * operation action handler call; reached through {@link openOperationOf} and
53
+ * {@link runOperation}, not through the operation itself.
54
+ */
55
+ export interface OperationInternals {
56
+ run(create: () => MaybePromise<Command | undefined>): Promise<OperationOutcome>;
57
+ workingRootOf(key: string, built: AstNode): AstNode;
58
+ existingWorkingRootOf(key: string): AstNode | undefined;
59
+ workingUriOf(node: AstNode): string | undefined;
60
+ builtNodeOf<T extends AstNode>(node: T): T;
61
+ recordWrittenRoot(root: AstNode): void;
62
+ recordDropped(current: AstNode | undefined): void;
63
+ recordExecuted(step: OperationSideEffect): void;
64
+ assertNotDuringUndoRedo(label: string): void;
65
+ amendBefore(update: (before: AnyObject) => AnyObject): void;
66
+ }
67
+
68
+ const internals = new WeakMap<object, OperationInternals>();
69
+ /** The canonical URI of every working root an operation made, by root. */
70
+ const workingRootUris = new WeakMap<AstNode, string>();
71
+
72
+ /**
73
+ * The canonical URI of the document whose working copy `node` belongs to;
74
+ * `undefined` for a node of no copy. A copy has no `$document`, so a lookup
75
+ * that routes by a node's document routes a copy node through this instead.
76
+ */
77
+ export function workingUriOfCopy(node: AstNode): string | undefined {
78
+ return workingRootUris.get(AstUtils.findRootNode(node));
79
+ }
80
+ const openOperations = new WeakMap<object, OperationInternals>();
81
+
82
+ /** The operation open on `state`, executing, undoing or redoing its side effects; `undefined` between operations. */
83
+ export function openOperationOf(state: object): OperationInternals | undefined {
84
+ return openOperations.get(state);
85
+ }
86
+
87
+ /** Run `operation` as {@link HydraniumGlspOperationCommand} describes. */
88
+ export function runOperation<TSourceModel extends AnyObject>(
89
+ operation: HydraniumGlspOperationCommand<TSourceModel>,
90
+ create: () => MaybePromise<Command | undefined>
91
+ ): Promise<OperationOutcome> {
92
+ return membersOf(operation).run(create);
93
+ }
94
+
95
+ function membersOf(operation: object): OperationInternals {
96
+ const members = internals.get(operation);
97
+ if (!members) {
98
+ throw new Error('Not a HydraniumGlspOperationCommand');
99
+ }
100
+ return members;
101
+ }
102
+
103
+ /**
104
+ * One GLSP operation as a transaction, and the command-stack entry that undoes
105
+ * and redoes it.
106
+ *
107
+ * {@link runOperation} executes the operation's command against copies of the
108
+ * built roots and writes their projection once; a reader of a built root never
109
+ * sees an edit its write has not committed, and an edit the write drops stays
110
+ * out of every built root. Call it inside `runExclusive`: the copies, the write
111
+ * and the capture that follows assume no other operation, undo or storage
112
+ * capture runs meanwhile.
113
+ *
114
+ * The entry holds one model transition, from the copies' projection before the
115
+ * command to their projection after it, both as the documents hold them after
116
+ * a write (see `normalizeTransition`), and the side effects that executed, in
117
+ * order: each command that does not record, a `CompoundCommand`'s children
118
+ * counted one by one, and the bridge of each recording command at any depth.
119
+ * Rollback and undo run them in reverse, redo in order. A recording command
120
+ * executed inside an open operation writes nothing, and its own undo and redo
121
+ * do nothing there.
122
+ */
123
+ export class HydraniumGlspOperationCommand<TSourceModel extends AnyObject = AnyObject> implements Command {
124
+ /** Working copies by canonical URI. */
125
+ protected readonly workingRoots = new Map<string, AstNode>();
126
+ /** Built node to copy and copy to built node, across every working root, as `AstUtils.copyAstNode` records it. */
127
+ protected readonly trace = new Map<AstNode, AstNode>();
128
+ /** The source root captured when the operation opened. */
129
+ protected sourceRootAtOpen?: AstNode;
130
+ /** The version of {@link sourceRootAtOpen}, which the write is gated on. */
131
+ protected baseVersionAtOpen?: ModelVersion;
132
+ /**
133
+ * The primary root to capture when the operation closes: the one its write
134
+ * produced, or the document's built one after a dropped write or one the
135
+ * document already held.
136
+ */
137
+ protected writtenRoot?: AstNode;
138
+ /** Whether the write's reconcile dropped the edit. */
139
+ protected dropped = false;
140
+ /** The command the operation executed. */
141
+ protected command?: Command;
142
+ /** The side effects that executed, in execution order. */
143
+ protected readonly steps: OperationSideEffect[] = [];
144
+ /**
145
+ * The copies' projection when the operation opened, amended as documents
146
+ * join or leave the write set during it.
147
+ */
148
+ protected before?: TSourceModel;
149
+ protected transition?: OperationTransition<TSourceModel>;
150
+ /** Whether an undo or redo is running the side effects, on copies it discards. */
151
+ protected undoingOrRedoing = false;
152
+
153
+ constructor(protected readonly modelState: HydraniumGlspRecordingState<TSourceModel>) {
154
+ const members: OperationInternals = {
155
+ run: create => this.run(create),
156
+ workingRootOf: (key, built) => this.workingRootOf(key, built),
157
+ existingWorkingRootOf: key => this.workingRoots.get(key),
158
+ workingUriOf: node => this.workingUriOf(node),
159
+ builtNodeOf: node => this.builtNodeOf(node),
160
+ recordWrittenRoot: root => {
161
+ this.writtenRoot = root;
162
+ },
163
+ recordDropped: current => {
164
+ this.dropped = true;
165
+ this.writtenRoot = current;
166
+ },
167
+ recordExecuted: step => {
168
+ this.steps.push(step);
169
+ },
170
+ amendBefore: update => {
171
+ if (this.before !== undefined) {
172
+ this.before = update(this.before) as TSourceModel;
173
+ }
174
+ },
175
+ assertNotDuringUndoRedo: label => {
176
+ if (this.undoingOrRedoing) {
177
+ throw new Error(
178
+ `Recording command '${label}' executed during an undo or redo, which writes only the recorded transition, ` +
179
+ 'so its edit would be discarded. Execute it in an operation of its own.'
180
+ );
181
+ }
182
+ }
183
+ };
184
+ internals.set(this, members);
185
+ }
186
+
187
+ /**
188
+ * Open the operation, execute the command `create` makes, write the result
189
+ * once, and close the operation.
190
+ *
191
+ * `create` runs inside the operation, because handlers resolve their
192
+ * targets there. On any failure, `create`, the command or the write, the
193
+ * executed commands' side effects are undone, nothing is written, and the
194
+ * error is rethrown; after a write whose reconcile dropped the edit, the
195
+ * side effects are undone the same way and this answers `'dropped'`. The
196
+ * operation closes either way: the copies are dropped and the source root
197
+ * is captured again, with the base version, every tracked version and the
198
+ * index.
199
+ */
200
+ protected async run(create: () => MaybePromise<Command | undefined>): Promise<'executed' | 'dropped' | 'none'> {
201
+ this.open();
202
+ let recorded: OperationTransition<TSourceModel> | undefined;
203
+ try {
204
+ this.before = await this.project();
205
+ await this.checkpoint('create');
206
+ const command = await create();
207
+ if (!command) {
208
+ return 'none';
209
+ }
210
+ await this.tracked(command).execute();
211
+ this.command = command;
212
+ await this.checkpoint('project');
213
+ const written = await this.project();
214
+ if (written !== undefined && !(await this.write(written))) {
215
+ await this.rollback();
216
+ return 'dropped';
217
+ }
218
+ if (this.before !== undefined && written !== undefined) {
219
+ recorded = { from: this.before, to: written };
220
+ }
221
+ } catch (error: unknown) {
222
+ await this.rollback();
223
+ throw error;
224
+ } finally {
225
+ this.close();
226
+ }
227
+ // After the write landed, so a failure here leaves it in place: the
228
+ // transition is then kept as recorded, and an undo may conflict.
229
+ try {
230
+ this.transition = recorded && (await this.modelState.normalizeTransition(recorded));
231
+ } catch (error: unknown) {
232
+ this.modelState.logger.warn(`Normalizing an operation's transition failed: ${describeErrorWithStack(error)}`);
233
+ this.transition = recorded;
234
+ }
235
+ return 'executed';
236
+ }
237
+
238
+ /**
239
+ * Awaited where the operation, or an undo or redo of it, yields to code it
240
+ * does not control: before creating the command, before each side effect
241
+ * executes, is undone or redone, rolls back or is compensated, before
242
+ * projecting the copies, before resolving an undo or redo. Does nothing; a
243
+ * test overrides it to land a foreign edit or a failure there.
244
+ */
245
+ protected async checkpoint(
246
+ _point: 'create' | 'execute' | 'project' | 'resolve' | UndoRedoDirection | 'rollback' | 'compensate'
247
+ ): Promise<void> {
248
+ // A seam for tests.
249
+ }
250
+
251
+ /**
252
+ * Does nothing: {@link runOperation} executed the command; the command stack
253
+ * calls this when it pushes the entry.
254
+ */
255
+ async execute(): Promise<void> {
256
+ // Executed by runOperation().
257
+ }
258
+
259
+ async undo(): Promise<void> {
260
+ await this.undoOrRedo('undo');
261
+ }
262
+
263
+ async redo(): Promise<void> {
264
+ await this.undoOrRedo('redo');
265
+ }
266
+
267
+ canUndo(): boolean {
268
+ return this.command?.canUndo?.() ?? true;
269
+ }
270
+
271
+ /** The working copy of `built`, the root of the document whose canonical URI is `key`, made on the first request. */
272
+ protected workingRootOf(key: string, built: AstNode): AstNode {
273
+ let working = this.workingRoots.get(key);
274
+ if (!working) {
275
+ working = AstUtils.copyAstNode(
276
+ built,
277
+ (_node, _property, _refNode, _refText, original) => this.buildWorkingReference(original),
278
+ this.trace
279
+ );
280
+ this.rebuildWorkingMultiReferences(working);
281
+ this.workingRoots.set(key, working);
282
+ workingRootUris.set(working, key);
283
+ this.modelState.index.remapSemanticAliases(node =>
284
+ AstUtils.findRootNode(node) === built ? (this.trace.get(node) ?? node) : node
285
+ );
286
+ }
287
+ return working;
288
+ }
289
+
290
+ /** The canonical URI of the working root `node` belongs to; `undefined` for a node of no copy. */
291
+ protected workingUriOf(node: AstNode): string | undefined {
292
+ const root = AstUtils.findRootNode(node);
293
+ for (const [key, working] of this.workingRoots) {
294
+ if (working === root) {
295
+ return key;
296
+ }
297
+ }
298
+ return undefined;
299
+ }
300
+
301
+ /**
302
+ * The built node `node` was copied from; `node` itself for a node of no copy
303
+ * and for a node the operation created.
304
+ */
305
+ protected builtNodeOf<T extends AstNode>(node: T): T {
306
+ if (this.workingUriOf(node) === undefined) {
307
+ return node;
308
+ }
309
+ const built = this.trace.get(node);
310
+ return built === undefined ? node : (built as T);
311
+ }
312
+
313
+ /**
314
+ * Copy the source root, and point the index at the copy, so the nodes a
315
+ * handler resolves by id are the nodes it edits.
316
+ */
317
+ protected open(): void {
318
+ this.sourceRootAtOpen = this.modelState.sourceRoot;
319
+ this.baseVersionAtOpen = this.modelState.baseVersion;
320
+ openOperations.set(this.modelState, membersOf(this));
321
+ if (this.sourceRootAtOpen !== undefined) {
322
+ this.modelState.index.reindexSemanticElements(this.modelState.sourceRoot, this.modelState.sourceUri);
323
+ }
324
+ }
325
+
326
+ /**
327
+ * Drop the copies, move every id added through `indexSemanticElement` from
328
+ * a copy back to the built node it was copied from, and capture the source
329
+ * root again, whether or not the write changed it.
330
+ */
331
+ protected close(): void {
332
+ const copies = new Set(this.workingRoots.values());
333
+ this.modelState.index.remapSemanticAliases(node => (copies.has(AstUtils.findRootNode(node)) ? this.trace.get(node) : node));
334
+ openOperations.delete(this.modelState);
335
+ const root = this.writtenRoot ?? this.sourceRootAtOpen;
336
+ if (root !== undefined) {
337
+ this.modelState.setSourceRoot(this.modelState.sourceUri, root);
338
+ }
339
+ }
340
+
341
+ /**
342
+ * Undo the side effects of the executed commands, newest first, each step
343
+ * on its own: a step that fails is logged and the rest are still undone.
344
+ * The operation's own error is the one thrown.
345
+ */
346
+ protected async rollback(): Promise<void> {
347
+ await this.runEach(
348
+ [...this.steps].reverse().map(step => () => step.undo()),
349
+ 'Rolling back a failed operation',
350
+ 'rollback'
351
+ );
352
+ }
353
+
354
+ /**
355
+ * Run each of `actions`, each reverting a side effect of its own: a failure
356
+ * is logged as `what` failing and the rest still run, so one step that
357
+ * cannot be reverted leaves no other step applied. The caller reports its
358
+ * own failure, not these.
359
+ */
360
+ protected async runEach(
361
+ actions: ReadonlyArray<() => MaybePromise<void>>,
362
+ what: string,
363
+ point: 'rollback' | 'compensate'
364
+ ): Promise<void> {
365
+ for (const action of actions) {
366
+ try {
367
+ await this.checkpoint(point);
368
+ await action();
369
+ } catch (error: unknown) {
370
+ this.modelState.logger.error(`${what} failed at one step: ${describeErrorWithStack(error)}`);
371
+ }
372
+ }
373
+ }
374
+
375
+ /**
376
+ * `command` with every side effect it executes recorded as a step: the
377
+ * command itself, or for a `CompoundCommand` each child, at any depth. A
378
+ * tracked command's own undo and redo do nothing, so a `CompoundCommand`
379
+ * reverting its children when a later one throws leaves that to the steps.
380
+ *
381
+ * The children are read from GLSP's protected `CompoundCommand.commands`;
382
+ * should that field stop being readable, a compound becomes one step and
383
+ * its children's side effects no longer interleave with the bridges.
384
+ */
385
+ protected tracked(command: Command): Command {
386
+ const children: unknown = command instanceof CompoundCommand ? Reflect.get(command, 'commands') : undefined;
387
+ if (isCommandArray(children)) {
388
+ children.forEach((child, i) => {
389
+ children[i] = this.tracked(child);
390
+ });
391
+ return command;
392
+ }
393
+ return {
394
+ execute: async () => {
395
+ await this.checkpoint('execute');
396
+ await command.execute();
397
+ this.steps.push({ undo: () => command.undo(), redo: () => command.redo() });
398
+ },
399
+ undo: () => undefined,
400
+ redo: () => undefined,
401
+ canUndo: () => Command.canUndo(command)
402
+ };
403
+ }
404
+
405
+ /** The model projected from the working copies, cloned; `undefined` for a state that projects none. */
406
+ protected async project(): Promise<TSourceModel | undefined> {
407
+ const model: TSourceModel | undefined = await this.modelState.sourceModel;
408
+ return model === undefined ? undefined : structuredClone(model);
409
+ }
410
+
411
+ /**
412
+ * Resolve the transition onto the current model, run the commands' side
413
+ * effects, then write once, gated on the versions the model was read at. A
414
+ * collision does nothing but warn; a transition with nothing left to apply,
415
+ * or one the model already holds, still runs the side effects.
416
+ *
417
+ * The caller holds `runExclusive`, which nothing here takes again.
418
+ */
419
+ protected async undoOrRedo(direction: UndoRedoDirection): Promise<void> {
420
+ // Captured again so the model the transition is resolved onto, the base
421
+ // a conflict reconciles from and the versions the write is gated on are
422
+ // read together.
423
+ if (this.modelState.sourceRoot !== undefined) {
424
+ this.modelState.setSourceRoot(this.modelState.sourceUri, this.modelState.sourceRoot);
425
+ }
426
+ await this.checkpoint('resolve');
427
+ let merged: TSourceModel | undefined;
428
+ const { transition } = this.inDirection(direction);
429
+ if (transition) {
430
+ const outcome = await this.modelState.conflictResolver.resolve<TSourceModel>(
431
+ transition.from,
432
+ transition.to,
433
+ async () => this.modelState.sourceModel
434
+ );
435
+ if (outcome.status === 'merged') {
436
+ merged = outcome.merged;
437
+ } else if (outcome.status !== 'no-op' && outcome.status !== 'already-applied') {
438
+ this.modelState.logger.warn(
439
+ `${direction === 'undo' ? 'Undo' : 'Redo'} of an operation skipped (${outcome.status}); a foreign edit changed a shared field`
440
+ );
441
+ return;
442
+ }
443
+ }
444
+ await this.undoOrRedoSideEffects(direction, merged);
445
+ }
446
+
447
+ /**
448
+ * Write `model` once through the state's write path, gated on the versions
449
+ * read when the operation opened, a conflict reconciled as for any write of
450
+ * the state; `false` when the reconcile dropped it.
451
+ */
452
+ protected async write(model: TSourceModel): Promise<boolean> {
453
+ this.dropped = false;
454
+ await this.modelState.updateSourceModel(model, this.baseVersionAtOpen);
455
+ return !this.dropped;
456
+ }
457
+
458
+ /**
459
+ * Run the recorded steps of an undo or redo against fresh copies of the
460
+ * built roots, made on demand as in an operation and discarded unwritten,
461
+ * then write `merged`, the resolved transition, through {@link write}: that
462
+ * write is what changes the model, and a step that edits `sourceRoot` must
463
+ * not reach the root every reader shares. A recording command executed here
464
+ * throws.
465
+ *
466
+ * When a step or the write throws, or the write's reconcile drops it, the
467
+ * steps already run are run the other way, newest first, and the error is
468
+ * rethrown with nothing written; GLSP's command stack then flushes, as it
469
+ * does for any undo or redo that fails.
470
+ */
471
+ protected async undoOrRedoSideEffects(direction: UndoRedoDirection, merged?: TSourceModel): Promise<void> {
472
+ this.workingRoots.clear();
473
+ this.trace.clear();
474
+ this.writtenRoot = undefined;
475
+ this.undoingOrRedoing = true;
476
+ this.open();
477
+ const { steps, run } = this.inDirection(direction);
478
+ const done: OperationSideEffect[] = [];
479
+ try {
480
+ for (const step of steps) {
481
+ await this.checkpoint(direction);
482
+ await run(step);
483
+ done.unshift(step);
484
+ }
485
+ if (merged === undefined) {
486
+ // Nothing to write, but a step may have rebuilt the document: close on its built root, not the one opened on.
487
+ this.modelState.captureBuiltRoot();
488
+ } else if (!(await this.write(merged))) {
489
+ throw new DiagramEditDroppedError(`The ${direction} of an operation was dropped: a foreign edit changed a field it writes`);
490
+ }
491
+ } catch (error: unknown) {
492
+ await this.compensate(done, direction);
493
+ throw error;
494
+ } finally {
495
+ this.close();
496
+ this.undoingOrRedoing = false;
497
+ }
498
+ }
499
+
500
+ /**
501
+ * Run `steps`, which a failed undo or redo already ran, the other way, each
502
+ * on its own as {@link rollback} does; the undo or redo's own error is the
503
+ * one thrown.
504
+ */
505
+ protected async compensate(steps: readonly OperationSideEffect[], direction: UndoRedoDirection): Promise<void> {
506
+ const { revert } = this.inDirection(direction);
507
+ await this.runEach(
508
+ steps.map(step => () => revert(step)),
509
+ `Reverting a failed ${direction}`,
510
+ 'compensate'
511
+ );
512
+ }
513
+
514
+ /**
515
+ * The transition and the recorded steps as `direction` runs them: an undo
516
+ * runs the transition backwards and each step's undo, newest first, and is
517
+ * reverted by each step's redo; a redo the other way round.
518
+ */
519
+ protected inDirection(direction: UndoRedoDirection): {
520
+ readonly transition?: OperationTransition<TSourceModel>;
521
+ readonly steps: readonly OperationSideEffect[];
522
+ run(step: OperationSideEffect): MaybePromise<void>;
523
+ revert(step: OperationSideEffect): MaybePromise<void>;
524
+ } {
525
+ if (direction === 'redo') {
526
+ return { transition: this.transition, steps: [...this.steps], run: step => step.redo(), revert: step => step.undo() };
527
+ }
528
+ return {
529
+ transition: this.transition && { from: this.transition.to, to: this.transition.from },
530
+ steps: [...this.steps].reverse(),
531
+ run: step => step.undo(),
532
+ revert: step => step.redo()
533
+ };
534
+ }
535
+
536
+ /**
537
+ * The copy's stand-in for `original`: its target is the copy of
538
+ * `original`'s target once one exists, read when asked so a copy made later
539
+ * in the operation is found, else that target itself. A handler compares a
540
+ * target by identity with nodes it resolved from a copy.
541
+ */
542
+ protected buildWorkingReference(original: Reference): Reference {
543
+ const trace = this.trace;
544
+ return {
545
+ $refText: original.$refText,
546
+ $refNode: original.$refNode,
547
+ get ref(): AstNode | undefined {
548
+ const target = original.ref;
549
+ return target && (trace.get(target) ?? target);
550
+ },
551
+ get error(): LinkingError | undefined {
552
+ return original.error;
553
+ },
554
+ get $nodeDescription(): AstNodeDescription | undefined {
555
+ return original.$nodeDescription;
556
+ }
557
+ };
558
+ }
559
+
560
+ /** {@link buildWorkingReference} for a reference with several targets. */
561
+ protected buildWorkingMultiReference(original: MultiReference): MultiReference {
562
+ const trace = this.trace;
563
+ return {
564
+ $refText: original.$refText,
565
+ $refNode: original.$refNode,
566
+ get items(): MultiReferenceItem[] {
567
+ return original.items.map(item => ({ ref: trace.get(item.ref) ?? item.ref, $nodeDescription: item.$nodeDescription }));
568
+ },
569
+ get error(): LinkingError | undefined {
570
+ return original.error;
571
+ }
572
+ };
573
+ }
574
+
575
+ /**
576
+ * Replace every multi-reference under `working` with a working one.
577
+ * `AstUtils.copyAstNode` rebuilds single references only and leaves a
578
+ * multi-reference object shared with the built root, whose targets are
579
+ * built nodes.
580
+ */
581
+ protected rebuildWorkingMultiReferences(working: AstNode): void {
582
+ for (const node of AstUtils.streamAst(working)) {
583
+ const properties = node as GenericAstNode;
584
+ for (const [name, value] of Object.entries(properties)) {
585
+ if (name.startsWith('$')) {
586
+ continue;
587
+ }
588
+ if (isMultiReference(value)) {
589
+ properties[name] = this.buildWorkingMultiReference(value);
590
+ } else if (Array.isArray(value) && value.some(element => isMultiReference(element))) {
591
+ properties[name] = value.map((element: unknown) =>
592
+ isMultiReference(element) ? this.buildWorkingMultiReference(element) : element
593
+ );
594
+ }
595
+ }
596
+ }
597
+ }
598
+ }
599
+
600
+ function isCommandArray(value: unknown): value is Command[] {
601
+ return Array.isArray(value) && value.every(element => typeof element === 'object' && element !== null && 'execute' in element);
602
+ }