@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.
- package/README.md +80 -88
- package/lib/browser/start-glsp-server-in-worker.d.ts +43 -42
- package/lib/browser/start-glsp-server-in-worker.d.ts.map +1 -1
- package/lib/browser/start-glsp-server-in-worker.js +62 -13
- package/lib/browser/start-glsp-server-in-worker.js.map +1 -1
- package/lib/command/diagram-edit-dropped-error.d.ts +31 -0
- package/lib/command/diagram-edit-dropped-error.d.ts.map +1 -0
- package/lib/command/diagram-edit-dropped-error.js +48 -0
- package/lib/command/diagram-edit-dropped-error.js.map +1 -0
- package/lib/command/hydranium-glsp-command-stack.d.ts +33 -0
- package/lib/command/hydranium-glsp-command-stack.d.ts.map +1 -0
- package/lib/command/hydranium-glsp-command-stack.js +63 -0
- package/lib/command/hydranium-glsp-command-stack.js.map +1 -0
- package/lib/command/hydranium-glsp-operation-action-handler.d.ts +64 -0
- package/lib/command/hydranium-glsp-operation-action-handler.d.ts.map +1 -0
- package/lib/command/hydranium-glsp-operation-action-handler.js +142 -0
- package/lib/command/hydranium-glsp-operation-action-handler.js.map +1 -0
- package/lib/command/hydranium-glsp-operation-command.d.ts +250 -0
- package/lib/command/hydranium-glsp-operation-command.d.ts.map +1 -0
- package/lib/command/hydranium-glsp-operation-command.js +491 -0
- package/lib/command/hydranium-glsp-operation-command.js.map +1 -0
- package/lib/command/hydranium-glsp-recording-command.d.ts +26 -87
- package/lib/command/hydranium-glsp-recording-command.d.ts.map +1 -1
- package/lib/command/hydranium-glsp-recording-command.js +40 -117
- package/lib/command/hydranium-glsp-recording-command.js.map +1 -1
- package/lib/command/hydranium-glsp-undo-redo-action-handler.d.ts +23 -0
- package/lib/command/hydranium-glsp-undo-redo-action-handler.d.ts.map +1 -0
- package/lib/command/hydranium-glsp-undo-redo-action-handler.js +41 -0
- package/lib/command/hydranium-glsp-undo-redo-action-handler.js.map +1 -0
- package/lib/command/index.d.ts +5 -0
- package/lib/command/index.d.ts.map +1 -1
- package/lib/command/index.js +5 -0
- package/lib/command/index.js.map +1 -1
- package/lib/computed-bounds/hydranium-glsp-computed-bounds-action-handler.d.ts +1 -1
- package/lib/computed-bounds/hydranium-glsp-computed-bounds-action-handler.d.ts.map +1 -1
- package/lib/computed-bounds/hydranium-glsp-computed-bounds-action-handler.js +2 -2
- package/lib/computed-bounds/hydranium-glsp-computed-bounds-action-handler.js.map +1 -1
- package/lib/dispatcher/server-action-dispatcher.d.ts +25 -24
- package/lib/dispatcher/server-action-dispatcher.d.ts.map +1 -1
- package/lib/dispatcher/server-action-dispatcher.js +38 -26
- package/lib/dispatcher/server-action-dispatcher.js.map +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/launcher/abstract-hydranium-glsp-diagram-module.d.ts +35 -59
- package/lib/launcher/abstract-hydranium-glsp-diagram-module.d.ts.map +1 -1
- package/lib/launcher/abstract-hydranium-glsp-diagram-module.js +60 -71
- package/lib/launcher/abstract-hydranium-glsp-diagram-module.js.map +1 -1
- package/lib/launcher/drop-notifications-to-gone-client.d.ts +23 -0
- package/lib/launcher/drop-notifications-to-gone-client.d.ts.map +1 -0
- package/lib/launcher/drop-notifications-to-gone-client.js +43 -0
- package/lib/launcher/drop-notifications-to-gone-client.js.map +1 -0
- package/lib/launcher/glsp-app-module.d.ts +1 -2
- package/lib/launcher/glsp-app-module.d.ts.map +1 -1
- package/lib/launcher/glsp-app-module.js.map +1 -1
- package/lib/launcher/glsp-connection-logger.d.ts +19 -0
- package/lib/launcher/glsp-connection-logger.d.ts.map +1 -0
- package/lib/launcher/glsp-connection-logger.js +24 -0
- package/lib/launcher/glsp-connection-logger.js.map +1 -0
- package/lib/launcher/glsp-server-overrides.d.ts +10 -8
- package/lib/launcher/glsp-server-overrides.d.ts.map +1 -1
- package/lib/launcher/glsp-server-overrides.js +25 -11
- package/lib/launcher/glsp-server-overrides.js.map +1 -1
- package/lib/launcher/hydranium-glsp-server.d.ts +60 -15
- package/lib/launcher/hydranium-glsp-server.d.ts.map +1 -1
- package/lib/launcher/hydranium-glsp-server.js +145 -22
- package/lib/launcher/hydranium-glsp-server.js.map +1 -1
- package/lib/launcher/index.d.ts +2 -0
- package/lib/launcher/index.d.ts.map +1 -1
- package/lib/launcher/index.js +5 -4
- package/lib/launcher/index.js.map +1 -1
- package/lib/logging/glsp-client-logger.d.ts +10 -6
- package/lib/logging/glsp-client-logger.d.ts.map +1 -1
- package/lib/logging/glsp-client-logger.js +14 -7
- package/lib/logging/glsp-client-logger.js.map +1 -1
- package/lib/messages/index.d.ts +11 -6
- package/lib/messages/index.d.ts.map +1 -1
- package/lib/messages/index.js +11 -6
- package/lib/messages/index.js.map +1 -1
- package/lib/node/start-glsp-server.d.ts +50 -15
- package/lib/node/start-glsp-server.d.ts.map +1 -1
- package/lib/node/start-glsp-server.js +97 -30
- package/lib/node/start-glsp-server.js.map +1 -1
- package/lib/state/abstract-hydranium-glsp-state.d.ts +326 -102
- package/lib/state/abstract-hydranium-glsp-state.d.ts.map +1 -1
- package/lib/state/abstract-hydranium-glsp-state.js +529 -139
- package/lib/state/abstract-hydranium-glsp-state.js.map +1 -1
- package/lib/state/diagram-status.d.ts +38 -0
- package/lib/state/diagram-status.d.ts.map +1 -0
- package/lib/state/diagram-status.js +27 -0
- package/lib/state/diagram-status.js.map +1 -0
- package/lib/state/full-text-hydranium-glsp-state.d.ts +22 -9
- package/lib/state/full-text-hydranium-glsp-state.d.ts.map +1 -1
- package/lib/state/full-text-hydranium-glsp-state.js +45 -18
- package/lib/state/full-text-hydranium-glsp-state.js.map +1 -1
- package/lib/state/hydranium-glsp-index.d.ts +40 -29
- package/lib/state/hydranium-glsp-index.d.ts.map +1 -1
- package/lib/state/hydranium-glsp-index.js +82 -45
- package/lib/state/hydranium-glsp-index.js.map +1 -1
- package/lib/state/hydranium-shared-core-services.d.ts +16 -29
- package/lib/state/hydranium-shared-core-services.d.ts.map +1 -1
- package/lib/state/hydranium-shared-core-services.js +15 -28
- package/lib/state/hydranium-shared-core-services.js.map +1 -1
- package/lib/state/index.d.ts +1 -0
- package/lib/state/index.d.ts.map +1 -1
- package/lib/state/index.js +1 -0
- package/lib/state/index.js.map +1 -1
- package/lib/state/model-ready-timeout-error.d.ts.map +1 -1
- package/lib/state/model-ready-timeout-error.js.map +1 -1
- package/lib/state/reconcile-source-model-write.d.ts +13 -27
- package/lib/state/reconcile-source-model-write.d.ts.map +1 -1
- package/lib/state/reconcile-source-model-write.js +44 -32
- package/lib/state/reconcile-source-model-write.js.map +1 -1
- package/lib/state/reconciling-multi-document-glsp-state.d.ts +125 -69
- package/lib/state/reconciling-multi-document-glsp-state.d.ts.map +1 -1
- package/lib/state/reconciling-multi-document-glsp-state.js +242 -100
- package/lib/state/reconciling-multi-document-glsp-state.js.map +1 -1
- package/lib/state/reconciling-transfer-hydranium-glsp-state.d.ts +36 -28
- package/lib/state/reconciling-transfer-hydranium-glsp-state.d.ts.map +1 -1
- package/lib/state/reconciling-transfer-hydranium-glsp-state.js +59 -42
- package/lib/state/reconciling-transfer-hydranium-glsp-state.js.map +1 -1
- package/lib/status/diagram-status-reporter.d.ts +44 -0
- package/lib/status/diagram-status-reporter.d.ts.map +1 -0
- package/lib/status/diagram-status-reporter.js +88 -0
- package/lib/status/diagram-status-reporter.js.map +1 -0
- package/lib/status/hydranium-glsp-request-model-action-handler.d.ts +27 -0
- package/lib/status/hydranium-glsp-request-model-action-handler.d.ts.map +1 -0
- package/lib/status/hydranium-glsp-request-model-action-handler.js +52 -0
- package/lib/status/hydranium-glsp-request-model-action-handler.js.map +1 -0
- package/lib/status/hydranium-glsp-set-edit-mode-action-handler.d.ts +27 -0
- package/lib/status/hydranium-glsp-set-edit-mode-action-handler.d.ts.map +1 -0
- package/lib/status/hydranium-glsp-set-edit-mode-action-handler.js +53 -0
- package/lib/status/hydranium-glsp-set-edit-mode-action-handler.js.map +1 -0
- package/lib/status/index.d.ts +12 -0
- package/lib/status/index.d.ts.map +1 -0
- package/lib/status/index.js +12 -0
- package/lib/status/index.js.map +1 -0
- package/lib/storage/hydranium-glsp-request-save-model-action-handler.d.ts +30 -0
- package/lib/storage/hydranium-glsp-request-save-model-action-handler.d.ts.map +1 -0
- package/lib/storage/hydranium-glsp-request-save-model-action-handler.js +47 -0
- package/lib/storage/hydranium-glsp-request-save-model-action-handler.js.map +1 -0
- package/lib/storage/hydranium-glsp-storage.d.ts +324 -143
- package/lib/storage/hydranium-glsp-storage.d.ts.map +1 -1
- package/lib/storage/hydranium-glsp-storage.js +569 -213
- package/lib/storage/hydranium-glsp-storage.js.map +1 -1
- package/lib/storage/index.d.ts +2 -1
- package/lib/storage/index.d.ts.map +1 -1
- package/lib/storage/index.js +2 -1
- package/lib/storage/index.js.map +1 -1
- package/lib/storage/save-delivery-policy.d.ts +51 -0
- package/lib/storage/save-delivery-policy.d.ts.map +1 -0
- package/lib/storage/{save-conflict-policy.js → save-delivery-policy.js} +7 -8
- package/lib/storage/save-delivery-policy.js.map +1 -0
- package/lib/submission/hydranium-glsp-submission-handler.d.ts +44 -4
- package/lib/submission/hydranium-glsp-submission-handler.d.ts.map +1 -1
- package/lib/submission/hydranium-glsp-submission-handler.js +82 -6
- package/lib/submission/hydranium-glsp-submission-handler.js.map +1 -1
- package/lib/testing/glsp-harness.d.ts +34 -7
- package/lib/testing/glsp-harness.d.ts.map +1 -1
- package/lib/testing/glsp-harness.js +47 -19
- package/lib/testing/glsp-harness.js.map +1 -1
- package/lib/validation/diagnostic-markers.d.ts +3 -3
- package/lib/validation/diagnostic-markers.d.ts.map +1 -1
- package/lib/validation/diagnostic-markers.js +1 -1
- package/lib/validation/diagnostic-markers.js.map +1 -1
- package/lib/validation/hydranium-glsp-model-validator.d.ts +1 -2
- package/lib/validation/hydranium-glsp-model-validator.d.ts.map +1 -1
- package/lib/validation/hydranium-glsp-model-validator.js +1 -1
- package/lib/validation/hydranium-glsp-model-validator.js.map +1 -1
- package/package.json +31 -52
- package/src/browser/start-glsp-server-in-worker.ts +79 -51
- package/src/command/diagram-edit-dropped-error.ts +51 -0
- package/src/command/hydranium-glsp-command-stack.ts +47 -0
- package/src/command/hydranium-glsp-operation-action-handler.ts +141 -0
- package/src/command/hydranium-glsp-operation-command.ts +602 -0
- package/src/command/hydranium-glsp-recording-command.ts +40 -121
- package/src/command/hydranium-glsp-undo-redo-action-handler.ts +29 -0
- package/src/command/index.ts +10 -0
- package/src/computed-bounds/hydranium-glsp-computed-bounds-action-handler.ts +2 -2
- package/src/dispatcher/server-action-dispatcher.ts +39 -31
- package/src/index.ts +1 -0
- package/src/launcher/abstract-hydranium-glsp-diagram-module.ts +79 -91
- package/src/launcher/drop-notifications-to-gone-client.ts +47 -0
- package/src/launcher/glsp-app-module.ts +1 -2
- package/src/launcher/glsp-connection-logger.ts +27 -0
- package/src/launcher/glsp-server-overrides.ts +27 -11
- package/src/launcher/hydranium-glsp-server.ts +153 -23
- package/src/launcher/index.ts +5 -4
- package/src/logging/glsp-client-logger.ts +16 -9
- package/src/messages/index.ts +16 -6
- package/src/node/start-glsp-server.ts +133 -40
- package/src/state/abstract-hydranium-glsp-state.ts +609 -154
- package/src/state/diagram-status.ts +41 -0
- package/src/state/full-text-hydranium-glsp-state.ts +46 -18
- package/src/state/hydranium-glsp-index.ts +85 -41
- package/src/state/hydranium-shared-core-services.ts +16 -29
- package/src/state/index.ts +1 -0
- package/src/state/reconcile-source-model-write.ts +51 -53
- package/src/state/reconciling-multi-document-glsp-state.ts +276 -102
- package/src/state/reconciling-transfer-hydranium-glsp-state.ts +67 -45
- package/src/status/diagram-status-reporter.ts +106 -0
- package/src/status/hydranium-glsp-request-model-action-handler.ts +43 -0
- package/src/status/hydranium-glsp-set-edit-mode-action-handler.ts +38 -0
- package/src/status/index.ts +12 -0
- package/src/storage/hydranium-glsp-request-save-model-action-handler.ts +40 -0
- package/src/storage/hydranium-glsp-storage.ts +647 -223
- package/src/storage/index.ts +2 -1
- package/src/storage/save-delivery-policy.ts +49 -0
- package/src/submission/hydranium-glsp-submission-handler.ts +91 -5
- package/src/testing/glsp-harness.ts +67 -25
- package/src/validation/diagnostic-markers.ts +3 -3
- package/src/validation/hydranium-glsp-model-validator.ts +4 -5
- package/lib/storage/save-conflict-policy.d.ts +0 -56
- package/lib/storage/save-conflict-policy.d.ts.map +0 -1
- package/lib/storage/save-conflict-policy.js.map +0 -1
- package/src/storage/save-conflict-policy.ts +0 -52
|
@@ -10,8 +10,18 @@
|
|
|
10
10
|
import { type JsonModelState } from '@eclipse-glsp/server';
|
|
11
11
|
import { injectable } from 'inversify';
|
|
12
12
|
import { type AstNode } from '@hydranium/langium';
|
|
13
|
-
import { type
|
|
13
|
+
import { type ClientSession, type ClientSessionWriteArgs } from '@hydranium/core';
|
|
14
|
+
import {
|
|
15
|
+
asModelVersion,
|
|
16
|
+
type BaseVersion,
|
|
17
|
+
type ModelVersion,
|
|
18
|
+
type TextVersion,
|
|
19
|
+
type TransferElement,
|
|
20
|
+
type VersionedModel,
|
|
21
|
+
describeErrorWithStack
|
|
22
|
+
} from '@hydranium/protocol';
|
|
14
23
|
import { AbstractHydraniumGlspState } from './abstract-hydranium-glsp-state.js';
|
|
24
|
+
import { type OperationTransition, openOperationOf } from '../command/hydranium-glsp-operation-command.js';
|
|
15
25
|
import { reconcileSourceModelWrite } from './reconcile-source-model-write.js';
|
|
16
26
|
|
|
17
27
|
/**
|
|
@@ -31,6 +41,13 @@ export interface MultiDocumentSourceModel<TPrimary extends TransferElement = Tra
|
|
|
31
41
|
secondaries: Record<string, TransferElement>;
|
|
32
42
|
}
|
|
33
43
|
|
|
44
|
+
/** A write set, with the version of each secondary's text it was projected from, keyed by URI. */
|
|
45
|
+
export interface VersionedMultiDocumentSourceModel<TPrimary extends TransferElement = TransferElement> extends VersionedModel<
|
|
46
|
+
MultiDocumentSourceModel<TPrimary>
|
|
47
|
+
> {
|
|
48
|
+
readonly secondaryVersions: Readonly<Record<string, ModelVersion>>;
|
|
49
|
+
}
|
|
50
|
+
|
|
34
51
|
/**
|
|
35
52
|
* Editable GLSP state for a diagram whose edits span SEVERAL Langium documents —
|
|
36
53
|
* a diagram file plus the semantic file it references, say, where creating a node
|
|
@@ -44,35 +61,23 @@ export interface MultiDocumentSourceModel<TPrimary extends TransferElement = Tra
|
|
|
44
61
|
* defines them over the primary plus the secondary write set registered through
|
|
45
62
|
* {@link AbstractHydraniumGlspState.trackSecondaryDocument}.
|
|
46
63
|
*
|
|
47
|
-
* **
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* back, and the document store commits per URI. An adopter that cannot accept the
|
|
52
|
-
* window should not span documents in one operation.
|
|
64
|
+
* **The write set is written all or none.** {@link persist} hands every
|
|
65
|
+
* changed document to one `ClientSession.updateAll` on the diagram's session,
|
|
66
|
+
* so a conflict on any document leaves every document of the set as it was,
|
|
67
|
+
* and write order carries no meaning.
|
|
53
68
|
*
|
|
54
|
-
* **
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* secondaries-then-primary, which is correct for the common shape where the
|
|
61
|
-
* primary is the DIAGRAM and the semantics it references are secondaries: the
|
|
62
|
-
* user opened the diagram, so it is also the document the conflict gate should
|
|
63
|
-
* guard. **An adopter whose primary is the referenced document — a semantic file
|
|
64
|
-
* as the diagram source, with layout in a secondary — has the ordering backwards
|
|
65
|
-
* and must override {@link persist} to write the primary first.** Overriding it
|
|
66
|
-
* is the supported route; the base order is a default for the common case, not an
|
|
67
|
-
* invariant of the class.
|
|
69
|
+
* **Every document of the set is gated.** The primary on the caller's
|
|
70
|
+
* `baseVersion`, each secondary on {@link secondaryBaseVersion}, by default the version
|
|
71
|
+
* it had when the source root was last read. A conflict on any of them is
|
|
72
|
+
* reconciled against the whole write set, and the merged retry is gated on
|
|
73
|
+
* the versions its refetch read. A write based on `'any'` — an undo or redo
|
|
74
|
+
* applying a recorded patch — forces every document of the set.
|
|
68
75
|
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* ARE available via {@link AbstractHydraniumGlspState.capturedVersionOf}, so an
|
|
75
|
-
* adopter that wants a coarser check can compare before writing.
|
|
76
|
+
* A secondary is written only while the diagram's session has it open. The
|
|
77
|
+
* storage opens it as it joins the write set, and {@link openForWrite} opens it
|
|
78
|
+
* again before each write; an adopter whose write set can name a document that
|
|
79
|
+
* does not exist yet overrides {@link openForWrite} to create it through
|
|
80
|
+
* {@link createSecondaryDocument}.
|
|
76
81
|
*/
|
|
77
82
|
@injectable()
|
|
78
83
|
export class ReconcilingMultiDocumentGlspState<TRoot extends AstNode, TPrimary extends TransferElement = TransferElement>
|
|
@@ -80,11 +85,14 @@ export class ReconcilingMultiDocumentGlspState<TRoot extends AstNode, TPrimary e
|
|
|
80
85
|
implements JsonModelState<MultiDocumentSourceModel<TPrimary>>
|
|
81
86
|
{
|
|
82
87
|
/**
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
88
|
+
* The projection across the whole write set that a forward-write conflict
|
|
89
|
+
* reconciles the user's intent from, taken from the built roots on every
|
|
90
|
+
* capture.
|
|
86
91
|
*/
|
|
87
|
-
protected
|
|
92
|
+
protected base!: MultiDocumentSourceModel<TPrimary>;
|
|
93
|
+
|
|
94
|
+
/** The documents {@link createSecondaryDocument} created for the {@link persist} under way, with the version each was created at. */
|
|
95
|
+
protected readonly createdForWrite = new Map<string, TextVersion>();
|
|
88
96
|
|
|
89
97
|
/**
|
|
90
98
|
* Projection of the primary plus every registered secondary, in the framework
|
|
@@ -105,27 +113,96 @@ export class ReconcilingMultiDocumentGlspState<TRoot extends AstNode, TPrimary e
|
|
|
105
113
|
secondaries[uri] = projection;
|
|
106
114
|
}
|
|
107
115
|
}
|
|
108
|
-
return { primary: this.projectRoot(this.
|
|
116
|
+
return { primary: this.projectRoot(this.sourceRoot), secondaries };
|
|
109
117
|
}
|
|
110
118
|
|
|
111
|
-
override
|
|
112
|
-
super.
|
|
119
|
+
protected override captureSourceRoot(uri: string, root: TRoot): void {
|
|
120
|
+
super.captureSourceRoot(uri, root);
|
|
113
121
|
this.trackWriteSet(uri);
|
|
114
|
-
this.
|
|
122
|
+
this.base = this.sourceModel;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Each document whose ends differ, either end serialized for its URI and
|
|
127
|
+
* parsed back; a document equal on both ends is left as it is, since it
|
|
128
|
+
* stays out of the transition's patch anyway.
|
|
129
|
+
*/
|
|
130
|
+
override async normalizeTransition(
|
|
131
|
+
transition: OperationTransition<MultiDocumentSourceModel<TPrimary>>
|
|
132
|
+
): Promise<OperationTransition<MultiDocumentSourceModel<TPrimary>>> {
|
|
133
|
+
const from = { primary: transition.from.primary, secondaries: { ...transition.from.secondaries } };
|
|
134
|
+
const to = { primary: transition.to.primary, secondaries: { ...transition.to.secondaries } };
|
|
135
|
+
if (JSON.stringify(from.primary) !== JSON.stringify(to.primary)) {
|
|
136
|
+
from.primary = this.projectRoot<TPrimary>(await this.roundTrip(this._sourceUri, from.primary));
|
|
137
|
+
to.primary = this.projectRoot<TPrimary>(await this.roundTrip(this._sourceUri, to.primary));
|
|
138
|
+
}
|
|
139
|
+
for (const uri of new Set([...Object.keys(from.secondaries), ...Object.keys(to.secondaries)])) {
|
|
140
|
+
if (JSON.stringify(from.secondaries[uri]) === JSON.stringify(to.secondaries[uri])) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
for (const end of [from, to]) {
|
|
144
|
+
const secondary = end.secondaries[uri];
|
|
145
|
+
if (secondary !== undefined) {
|
|
146
|
+
end.secondaries[uri] = this.projectRoot(await this.roundTrip(uri, secondary));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return { from, to };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Also takes a secondary first tracked during an operation into the base
|
|
155
|
+
* and into the operation's projection from before its command, both from
|
|
156
|
+
* the root its version was read from: a conflict on it reconciles from the
|
|
157
|
+
* revision the operation's copy describes, and undoing the operation
|
|
158
|
+
* restores it. A document that does not exist yet has no projection to
|
|
159
|
+
* restore, so an undo leaves what the operation wrote into it.
|
|
160
|
+
*/
|
|
161
|
+
override trackSecondaryDocument(uri: string): void {
|
|
162
|
+
const added = uri !== this._sourceUri && !this.secondaryUris.includes(uri);
|
|
163
|
+
super.trackSecondaryDocument(uri);
|
|
164
|
+
const root = this.capturedRootOf(uri);
|
|
165
|
+
if (root === undefined) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const projection = this.projectRoot(root);
|
|
169
|
+
if (this.base !== undefined && !(uri in this.base.secondaries)) {
|
|
170
|
+
this.base = { ...this.base, secondaries: { ...this.base.secondaries, [uri]: projection } };
|
|
171
|
+
}
|
|
172
|
+
if (added) {
|
|
173
|
+
openOperationOf(this)?.amendBefore(before =>
|
|
174
|
+
isMultiDocumentModel(before) ? { ...before, secondaries: { ...before.secondaries, [uri]: projection } } : before
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* During an operation, also drops the untracked documents from its
|
|
181
|
+
* projection from before its command: they are not written, so the
|
|
182
|
+
* operation's undo and redo leave them as they are.
|
|
183
|
+
*/
|
|
184
|
+
override untrackSecondaryDocuments(): void {
|
|
185
|
+
const untracked = this.secondaryUris;
|
|
186
|
+
super.untrackSecondaryDocuments();
|
|
187
|
+
openOperationOf(this)?.amendBefore(before =>
|
|
188
|
+
isMultiDocumentModel(before)
|
|
189
|
+
? { ...before, secondaries: Object.fromEntries(Object.entries(before.secondaries).filter(([uri]) => !untracked.includes(uri))) }
|
|
190
|
+
: before
|
|
191
|
+
);
|
|
115
192
|
}
|
|
116
193
|
|
|
117
194
|
/**
|
|
118
195
|
* Register the secondary documents that belong to the primary at `uri`, via
|
|
119
196
|
* {@link AbstractHydraniumGlspState.trackSecondaryDocument}. Called on every
|
|
120
|
-
*
|
|
121
|
-
*
|
|
197
|
+
* capture, after the primary is captured (so `sourceUri` is current) and
|
|
198
|
+
* BEFORE the base is taken (so the base includes them).
|
|
122
199
|
* Default: no secondaries.
|
|
123
200
|
*
|
|
124
201
|
* This hook exists because that ordering is a trap an adopter would otherwise
|
|
125
|
-
* hit silently. Registering from an overridden `
|
|
126
|
-
* `super.
|
|
202
|
+
* hit silently. Registering from an overridden `captureSourceRoot` *after*
|
|
203
|
+
* `super.captureSourceRoot(...)` runs too late — the base has already been
|
|
127
204
|
* captured without the secondaries, so the first conflict reconcile measures
|
|
128
|
-
* the user's intent against a
|
|
205
|
+
* the user's intent against a base missing half the write set and the
|
|
129
206
|
* secondary edits look like foreign changes. Registering *before* the super
|
|
130
207
|
* call is too early for a URI derived from the new primary. Overriding this
|
|
131
208
|
* instead removes the choice.
|
|
@@ -136,108 +213,201 @@ export class ReconcilingMultiDocumentGlspState<TRoot extends AstNode, TPrimary e
|
|
|
136
213
|
|
|
137
214
|
/**
|
|
138
215
|
* Persist the whole write set, then capture the resulting primary root. On a
|
|
139
|
-
* `ConflictError` from the
|
|
140
|
-
* as the single-document state does — the orchestration is
|
|
216
|
+
* `ConflictError` from any document of the set, reconcile via the injected
|
|
217
|
+
* policy exactly as the single-document state does — the orchestration is
|
|
218
|
+
* shared.
|
|
141
219
|
*/
|
|
142
|
-
async updateSourceModel(model: MultiDocumentSourceModel<TPrimary>,
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
this.
|
|
220
|
+
async updateSourceModel(model: MultiDocumentSourceModel<TPrimary>, baseVersion: BaseVersion = this.baseVersion): Promise<void> {
|
|
221
|
+
let secondaryVersions: Readonly<Record<string, ModelVersion>> | undefined;
|
|
222
|
+
return reconcileSourceModelWrite<MultiDocumentSourceModel<TPrimary>>(model, baseVersion, {
|
|
223
|
+
persist: async (candidate, candidateBaseVersion) => {
|
|
224
|
+
const { root } = await this.persist(candidate, candidateBaseVersion, secondaryVersions);
|
|
225
|
+
this.captureWrittenRoot(root);
|
|
226
|
+
},
|
|
227
|
+
refetch: async () => {
|
|
228
|
+
const refetched = await this.refetch();
|
|
229
|
+
secondaryVersions = refetched?.secondaryVersions;
|
|
230
|
+
return refetched;
|
|
147
231
|
},
|
|
148
|
-
|
|
149
|
-
baseline: this.baseline,
|
|
232
|
+
base: this.base,
|
|
150
233
|
conflictResolver: this.conflictResolver,
|
|
151
234
|
logger: this.logger,
|
|
152
|
-
onConflictDropped: () => this.
|
|
235
|
+
onConflictDropped: () => this.writeDropped(),
|
|
236
|
+
onAlreadyApplied: () => this.captureBuiltRoot(),
|
|
237
|
+
maxWrites: this.maxSourceModelWrites
|
|
153
238
|
});
|
|
154
239
|
}
|
|
155
240
|
|
|
156
241
|
/**
|
|
157
|
-
* Write hook —
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
242
|
+
* Write hook — every document of `model` that changed, opened first through
|
|
243
|
+
* {@link openForWrite}, in one `updateAll` on the diagram's session: the
|
|
244
|
+
* primary gated on `baseVersion`, each secondary on its entry in
|
|
245
|
+
* `secondaryVersions` (a refetch's) or else {@link secondaryBaseVersion}, or
|
|
246
|
+
* every document on `'any'` when `baseVersion` is `'any'`. Resolves to
|
|
247
|
+
* the primary's root, the one already captured when the primary did not
|
|
248
|
+
* change. A write that bypasses `updateAll` gives up the all-or-none
|
|
249
|
+
* guarantee the class describes. Throws without a session
|
|
250
|
+
* ({@link requireModelSession}). An override that serializes otherwise than
|
|
251
|
+
* `ModelService.modelToText` overrides {@link normalizeTransition} alongside.
|
|
252
|
+
*
|
|
253
|
+
* A write that fails closes every document {@link createSecondaryDocument}
|
|
254
|
+
* created for it that {@link closesCreatedDocument} accepts.
|
|
161
255
|
*/
|
|
162
|
-
protected async persist(
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
256
|
+
protected async persist(
|
|
257
|
+
model: MultiDocumentSourceModel<TPrimary>,
|
|
258
|
+
baseVersion: BaseVersion,
|
|
259
|
+
secondaryVersions?: Readonly<Record<string, ModelVersion>>
|
|
260
|
+
): Promise<{ root: TRoot }> {
|
|
261
|
+
const primaryChanged = this.hasChanged(this.base.primary, model.primary);
|
|
262
|
+
const changedSecondaries = Object.entries(model.secondaries).filter(([uri, secondary]) =>
|
|
263
|
+
this.hasChanged(this.base.secondaries[uri], secondary)
|
|
264
|
+
);
|
|
265
|
+
if (!primaryChanged && changedSecondaries.length === 0) {
|
|
171
266
|
return { root: this._sourceRoot };
|
|
172
267
|
}
|
|
173
|
-
|
|
268
|
+
const session = this.requireModelSession();
|
|
269
|
+
this.createdForWrite.clear();
|
|
270
|
+
try {
|
|
271
|
+
if (primaryChanged) {
|
|
272
|
+
await this.openForWrite(session, this._sourceUri);
|
|
273
|
+
}
|
|
274
|
+
for (const [uri] of changedSecondaries) {
|
|
275
|
+
await this.openForWrite(session, uri);
|
|
276
|
+
}
|
|
277
|
+
// Based after the opens, so a secondary created there is gated on the version it was created at.
|
|
278
|
+
const updates: ClientSessionWriteArgs<TransferElement>[] = primaryChanged
|
|
279
|
+
? [{ uri: this._sourceUri, model: model.primary, baseVersion }]
|
|
280
|
+
: [];
|
|
281
|
+
for (const [uri, secondary] of changedSecondaries) {
|
|
282
|
+
updates.push({
|
|
283
|
+
uri,
|
|
284
|
+
model: secondary,
|
|
285
|
+
baseVersion: baseVersion === 'any' ? 'any' : (secondaryVersions?.[uri] ?? this.secondaryBaseVersion(uri))
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
const documents = await session.updateAll({ updates });
|
|
289
|
+
return { root: primaryChanged ? (documents[0].root as unknown as TRoot) : this._sourceRoot };
|
|
290
|
+
} catch (error: unknown) {
|
|
291
|
+
for (const [uri, created] of this.createdForWrite) {
|
|
292
|
+
try {
|
|
293
|
+
if (this.closesCreatedDocument(uri, created)) {
|
|
294
|
+
await session.close(uri);
|
|
295
|
+
}
|
|
296
|
+
} catch (closeError: unknown) {
|
|
297
|
+
this.logger.warn(`Could not close ${uri}, created for a write that failed: ${describeErrorWithStack(closeError)}`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
throw error;
|
|
301
|
+
} finally {
|
|
302
|
+
this.createdForWrite.clear();
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Whether a write that failed closes `uri`, which
|
|
308
|
+
* {@link createSecondaryDocument} created for it at `createdVersion`.
|
|
309
|
+
* Default: while it still holds the text it was created with. Left open, it
|
|
310
|
+
* would stay in the diagram's session, and the diagram's next save would
|
|
311
|
+
* write it to disk though no write landed in it. One whose text moved on,
|
|
312
|
+
* because the set failed after applying or another client wrote to it,
|
|
313
|
+
* stays open: closing it would drop that text.
|
|
314
|
+
*/
|
|
315
|
+
protected closesCreatedDocument(uri: string, createdVersion: TextVersion): boolean {
|
|
316
|
+
return this.sharedServices.workspace.TextDocuments.version(uri) === createdVersion;
|
|
174
317
|
}
|
|
175
318
|
|
|
176
319
|
/**
|
|
177
|
-
* Whether `candidate` differs from the
|
|
320
|
+
* Whether `candidate` differs from the base projection of the same
|
|
178
321
|
* document, and therefore needs writing.
|
|
179
322
|
*
|
|
180
323
|
* **Skipping unchanged documents is correctness, not an optimisation.** A
|
|
181
|
-
* write
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
324
|
+
* write re-serializes from the AST, so writing a document that did not
|
|
325
|
+
* change still rewrites its text in the serializer's own layout. Persisting
|
|
326
|
+
* the whole write set unconditionally therefore means a pure layout drag
|
|
327
|
+
* reflows the semantic file, a change the user never asked for and would
|
|
328
|
+
* struggle to attribute. Comments survive that rewrite, but the
|
|
329
|
+
* hand-formatting around them does not.
|
|
187
330
|
*
|
|
188
331
|
* Compared by serialised form. Both sides come from the same encoder walking
|
|
189
332
|
* the same shape, so key order is stable and a string compare is sound here;
|
|
190
333
|
* it is also cheap enough to run per document per write.
|
|
191
334
|
*/
|
|
192
|
-
protected hasChanged(
|
|
193
|
-
return
|
|
335
|
+
protected hasChanged(base: object | undefined, candidate: object): boolean {
|
|
336
|
+
return base === undefined || JSON.stringify(base) !== JSON.stringify(candidate);
|
|
194
337
|
}
|
|
195
338
|
|
|
196
|
-
/**
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
return document as unknown as { root: TRoot };
|
|
339
|
+
/**
|
|
340
|
+
* Make sure the diagram's session has `uri` open before the write set is
|
|
341
|
+
* written. Default: open it, a no-op for a document the session already
|
|
342
|
+
* has open. Override to create a document the write set names before it
|
|
343
|
+
* exists, through {@link createSecondaryDocument}: `updateAll` opens nothing.
|
|
344
|
+
*/
|
|
345
|
+
protected openForWrite(session: ClientSession<AstNode>, uri: string): Promise<void> {
|
|
346
|
+
return session.open(uri);
|
|
205
347
|
}
|
|
206
348
|
|
|
207
349
|
/**
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
350
|
+
* Create the secondary `uri` holding `text` through `session.create`, and
|
|
351
|
+
* base its write on the version the created document took. The version
|
|
352
|
+
* recorded while it did not exist matches no write, so the write conflicts.
|
|
353
|
+
* Make `text` with `ModelService.modelToText`, not the bare serializer,
|
|
354
|
+
* which leaves the file without the ending a write would give it.
|
|
211
355
|
*/
|
|
212
|
-
protected async
|
|
213
|
-
await
|
|
356
|
+
protected async createSecondaryDocument(session: ClientSession<AstNode>, uri: string, text: string): Promise<void> {
|
|
357
|
+
const created = await session.create(uri, text);
|
|
358
|
+
this.createdForWrite.set(uri, created);
|
|
359
|
+
if (this._secondaryVersions.has(uri)) {
|
|
360
|
+
this._secondaryVersions.set(uri, asModelVersion(created));
|
|
361
|
+
}
|
|
214
362
|
}
|
|
215
363
|
|
|
216
364
|
/**
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
365
|
+
* What a secondary write declares it was based on. Default: the version the
|
|
366
|
+
* secondary had when the source root was last read
|
|
367
|
+
* ({@link AbstractHydraniumGlspState.baseVersionOf}), so a foreign edit
|
|
368
|
+
* to it since is reported as a conflict and reconciled rather than
|
|
369
|
+
* overwritten; `'any'` for a secondary with no recorded version.
|
|
370
|
+
* Return `'any'` to force secondary writes.
|
|
222
371
|
*/
|
|
223
|
-
protected
|
|
224
|
-
|
|
225
|
-
|
|
372
|
+
protected secondaryBaseVersion(uri: string): BaseVersion {
|
|
373
|
+
return this.baseVersionOf(uri) ?? 'any';
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Refetch hook — the current projection across the write set, each document
|
|
378
|
+
* read through {@link AbstractHydraniumGlspState.readCurrentRoot}, with the
|
|
379
|
+
* versions read alongside: theirs, which the conflict resolver replays the
|
|
380
|
+
* user's intent onto. Returns `undefined` when the PRIMARY cannot be read,
|
|
381
|
+
* since a reconcile without it has nothing to merge into; an unreadable
|
|
382
|
+
* secondary is omitted the same way {@link sourceModel} omits one.
|
|
383
|
+
*/
|
|
384
|
+
protected async refetch(): Promise<VersionedMultiDocumentSourceModel<TPrimary> | undefined> {
|
|
385
|
+
const theirs = await this.readCurrentRoot(this._sourceUri);
|
|
386
|
+
if (!theirs) {
|
|
226
387
|
return undefined;
|
|
227
388
|
}
|
|
228
389
|
const secondaries: Record<string, TransferElement> = {};
|
|
390
|
+
const secondaryVersions: Record<string, ModelVersion> = {};
|
|
229
391
|
for (const uri of this.secondaryUris) {
|
|
230
|
-
const
|
|
231
|
-
if (
|
|
232
|
-
secondaries[uri] = this.projectRoot(
|
|
392
|
+
const read = await this.readCurrentRoot(uri);
|
|
393
|
+
if (read) {
|
|
394
|
+
secondaries[uri] = this.projectRoot(read.root);
|
|
395
|
+
secondaryVersions[uri] = read.version;
|
|
233
396
|
}
|
|
234
397
|
}
|
|
235
|
-
return { primary: this.projectRoot(
|
|
398
|
+
return { model: { primary: this.projectRoot<TPrimary>(theirs.root), secondaries }, baseVersion: theirs.version, secondaryVersions };
|
|
236
399
|
}
|
|
237
400
|
|
|
238
|
-
/**
|
|
401
|
+
/**
|
|
402
|
+
* Project a currently-loaded document's root, or `undefined` when it is not
|
|
403
|
+
* loaded: the open operation's copy of it when there is one, which is where
|
|
404
|
+
* a handler's edit of it is, else the root its version was read from.
|
|
405
|
+
*/
|
|
239
406
|
protected projectDocument(uri: string): TransferElement | undefined {
|
|
240
|
-
const root =
|
|
407
|
+
const root =
|
|
408
|
+
this.existingWorkingRootOf(uri) ??
|
|
409
|
+
this.capturedRootOf(uri) ??
|
|
410
|
+
this.sharedServices.model.ModelService.getDocument(uri)?.parseResult?.value;
|
|
241
411
|
return root ? this.projectRoot(root) : undefined;
|
|
242
412
|
}
|
|
243
413
|
|
|
@@ -246,3 +416,7 @@ export class ReconcilingMultiDocumentGlspState<TRoot extends AstNode, TPrimary e
|
|
|
246
416
|
return this.sharedServices.model.TransferEncoder.toTransfer(root, 'grammar') as unknown as T;
|
|
247
417
|
}
|
|
248
418
|
}
|
|
419
|
+
|
|
420
|
+
function isMultiDocumentModel(value: object): value is MultiDocumentSourceModel {
|
|
421
|
+
return 'secondaries' in value && typeof value.secondaries === 'object' && value.secondaries !== null;
|
|
422
|
+
}
|