@hydranium/glsp-server 1.0.0-next.117 → 1.0.0-next.123
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/lib/command/hydranium-glsp-recording-command.d.ts +1 -1
- package/lib/command/hydranium-glsp-recording-command.js +1 -1
- package/lib/messages/index.d.ts +1 -1
- package/lib/messages/index.d.ts.map +1 -1
- package/lib/messages/index.js +1 -1
- package/lib/messages/index.js.map +1 -1
- package/lib/state/abstract-hydranium-glsp-state.d.ts +52 -8
- package/lib/state/abstract-hydranium-glsp-state.d.ts.map +1 -1
- package/lib/state/abstract-hydranium-glsp-state.js +77 -5
- package/lib/state/abstract-hydranium-glsp-state.js.map +1 -1
- package/lib/state/full-text-hydranium-glsp-state.d.ts +4 -4
- package/lib/state/full-text-hydranium-glsp-state.d.ts.map +1 -1
- package/lib/state/full-text-hydranium-glsp-state.js +5 -10
- package/lib/state/full-text-hydranium-glsp-state.js.map +1 -1
- package/lib/state/reconcile-source-model-write.d.ts +2 -2
- package/lib/state/reconcile-source-model-write.d.ts.map +1 -1
- package/lib/state/reconcile-source-model-write.js +1 -1
- package/lib/state/reconcile-source-model-write.js.map +1 -1
- package/lib/state/reconciling-multi-document-glsp-state.d.ts +54 -74
- package/lib/state/reconciling-multi-document-glsp-state.d.ts.map +1 -1
- package/lib/state/reconciling-multi-document-glsp-state.js +77 -102
- package/lib/state/reconciling-multi-document-glsp-state.js.map +1 -1
- package/lib/state/reconciling-transfer-hydranium-glsp-state.d.ts +16 -15
- package/lib/state/reconciling-transfer-hydranium-glsp-state.d.ts.map +1 -1
- package/lib/state/reconciling-transfer-hydranium-glsp-state.js +21 -25
- package/lib/state/reconciling-transfer-hydranium-glsp-state.js.map +1 -1
- package/lib/storage/hydranium-glsp-storage.d.ts +103 -26
- package/lib/storage/hydranium-glsp-storage.d.ts.map +1 -1
- package/lib/storage/hydranium-glsp-storage.js +199 -32
- package/lib/storage/hydranium-glsp-storage.js.map +1 -1
- package/package.json +7 -7
- package/src/command/hydranium-glsp-recording-command.ts +1 -1
- package/src/messages/index.ts +6 -1
- package/src/state/abstract-hydranium-glsp-state.ts +85 -8
- package/src/state/full-text-hydranium-glsp-state.ts +5 -10
- package/src/state/reconcile-source-model-write.ts +3 -3
- package/src/state/reconciling-multi-document-glsp-state.ts +78 -103
- package/src/state/reconciling-transfer-hydranium-glsp-state.ts +21 -25
- package/src/storage/hydranium-glsp-storage.ts +236 -33
|
@@ -32,11 +32,11 @@ export interface FullTextSourceModel {
|
|
|
32
32
|
* `serializer.Serializer` reached via `sharedServices.ServiceRegistry`
|
|
33
33
|
* (so multi-grammar workspaces route to the right serializer). `MaybePromise`
|
|
34
34
|
* because serialization may be async; the sync fast path is preserved.
|
|
35
|
-
* - {@link updateSourceModel} pushes the text back through
|
|
36
|
-
* `
|
|
37
|
-
*
|
|
35
|
+
* - {@link updateSourceModel} pushes the text back through the diagram's
|
|
36
|
+
* session (`update` accepts a raw text payload) and captures the re-parsed
|
|
37
|
+
* root, gated on the caller's `basedOn`; it throws without a session.
|
|
38
38
|
*
|
|
39
|
-
* No `
|
|
39
|
+
* No `base` / conflict reconcile: a whole-document model has exactly one
|
|
40
40
|
* field, so every concurrent edit is a same-field collision and undo / redo
|
|
41
41
|
* degrade to drop-on-divergence. Adopters that need field-level undo use
|
|
42
42
|
* `ReconcilingTransferHydraniumGlspState` instead.
|
|
@@ -53,12 +53,7 @@ export class FullTextHydraniumGlspState<TRoot extends AstNode>
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
async updateSourceModel(model: FullTextSourceModel, basedOn: BasedOn = this.basedOn): Promise<void> {
|
|
56
|
-
const document = await this.
|
|
57
|
-
uri: this._sourceUri,
|
|
58
|
-
model: model.text,
|
|
59
|
-
clientId: this.clientId,
|
|
60
|
-
basedOn
|
|
61
|
-
});
|
|
56
|
+
const document = await this.requireModelSession().update({ uri: this._sourceUri, model: model.text, basedOn });
|
|
62
57
|
this.setSourceRoot(this._sourceUri, document.root as TRoot);
|
|
63
58
|
}
|
|
64
59
|
}
|
|
@@ -26,8 +26,8 @@ export interface SourceModelWriteHooks<TModel> {
|
|
|
26
26
|
persist(model: TModel, basedOn: BasedOn): Promise<void>;
|
|
27
27
|
/** Current server-side projection, or `undefined` when unavailable. */
|
|
28
28
|
refetch(): Promise<TModel | undefined>;
|
|
29
|
-
/**
|
|
30
|
-
readonly
|
|
29
|
+
/** The last in-sync projection the user's intent is measured against. */
|
|
30
|
+
readonly base: TModel | undefined;
|
|
31
31
|
readonly conflictResolver: ConflictResolver;
|
|
32
32
|
readonly logger: Logger;
|
|
33
33
|
/** Invoked when the edit is dropped, so the caller can resync its own state. */
|
|
@@ -54,7 +54,7 @@ export async function reconcileSourceModelWrite<TModel extends object>(
|
|
|
54
54
|
if (!isConflictError(err)) {
|
|
55
55
|
throw err;
|
|
56
56
|
}
|
|
57
|
-
const outcome = await hooks.conflictResolver.resolve(hooks.
|
|
57
|
+
const outcome = await hooks.conflictResolver.resolve(hooks.base ?? model, model, () => hooks.refetch());
|
|
58
58
|
switch (outcome.status) {
|
|
59
59
|
case 'merged':
|
|
60
60
|
await hooks.persist(outcome.merged, 'anything');
|
|
@@ -10,6 +10,7 @@
|
|
|
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 ClientSession, type ClientSessionWriteArgs } from '@hydranium/core';
|
|
13
14
|
import { type BasedOn, type TransferElement } from '@hydranium/protocol';
|
|
14
15
|
import { AbstractHydraniumGlspState } from './abstract-hydranium-glsp-state.js';
|
|
15
16
|
import { reconcileSourceModelWrite } from './reconcile-source-model-write.js';
|
|
@@ -44,36 +45,22 @@ export interface MultiDocumentSourceModel<TPrimary extends TransferElement = Tra
|
|
|
44
45
|
* defines them over the primary plus the secondary write set registered through
|
|
45
46
|
* {@link AbstractHydraniumGlspState.trackSecondaryDocument}.
|
|
46
47
|
*
|
|
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.
|
|
48
|
+
* **The write set is written all or none.** {@link persist} hands every
|
|
49
|
+
* changed document to one `ClientSession.updateAll` on the diagram's session,
|
|
50
|
+
* so a conflict on any document leaves every document of the set as it was,
|
|
51
|
+
* and write order carries no meaning.
|
|
53
52
|
*
|
|
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.
|
|
53
|
+
* **Every document of the set is gated.** The primary on the caller's
|
|
54
|
+
* `basedOn`, each secondary on {@link secondaryBasedOn}, by default the version
|
|
55
|
+
* it had when the source root was last read. A conflict on any of them is
|
|
56
|
+
* reconciled against the whole write set. A write based on `'anything'` — a
|
|
57
|
+
* merged retry, an undo or redo replaying a patch — forces every document of
|
|
58
|
+
* the set.
|
|
68
59
|
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* merged one — the atomicity problem again, one layer up. An adopter that wants
|
|
74
|
-
* a coarser check overrides {@link secondaryBasedOn} to return the version
|
|
75
|
-
* {@link AbstractHydraniumGlspState.snapshotVersionOf} already holds, and takes
|
|
76
|
-
* on the partial-write window named there.
|
|
60
|
+
* A secondary is written only while the diagram's session has it open. The
|
|
61
|
+
* storage opens it as it joins the write set, and {@link openForWrite} opens it
|
|
62
|
+
* again before each write; an adopter whose write set can name a document that
|
|
63
|
+
* does not exist yet overrides {@link openForWrite} to create it.
|
|
77
64
|
*/
|
|
78
65
|
@injectable()
|
|
79
66
|
export class ReconcilingMultiDocumentGlspState<TRoot extends AstNode, TPrimary extends TransferElement = TransferElement>
|
|
@@ -82,10 +69,10 @@ export class ReconcilingMultiDocumentGlspState<TRoot extends AstNode, TPrimary e
|
|
|
82
69
|
{
|
|
83
70
|
/**
|
|
84
71
|
* Last in-sync projection across the whole write set, captured on every
|
|
85
|
-
* {@link setSourceRoot}. Same role as the single-document
|
|
72
|
+
* {@link setSourceRoot}. Same role as the single-document base: the state a
|
|
86
73
|
* forward-write conflict reconciles the user's intent against.
|
|
87
74
|
*/
|
|
88
|
-
protected
|
|
75
|
+
protected base?: MultiDocumentSourceModel<TPrimary>;
|
|
89
76
|
|
|
90
77
|
/**
|
|
91
78
|
* Projection of the primary plus every registered secondary, in the framework
|
|
@@ -112,21 +99,21 @@ export class ReconcilingMultiDocumentGlspState<TRoot extends AstNode, TPrimary e
|
|
|
112
99
|
override setSourceRoot(uri: string, root: TRoot): void {
|
|
113
100
|
super.setSourceRoot(uri, root);
|
|
114
101
|
this.trackWriteSet(uri);
|
|
115
|
-
this.
|
|
102
|
+
this.base = this.sourceModel;
|
|
116
103
|
}
|
|
117
104
|
|
|
118
105
|
/**
|
|
119
106
|
* Register the secondary documents that belong to the primary at `uri`, via
|
|
120
107
|
* {@link AbstractHydraniumGlspState.trackSecondaryDocument}. Called on every
|
|
121
108
|
* {@link setSourceRoot}, after the primary is captured (so `sourceUri` is
|
|
122
|
-
* current) and BEFORE the
|
|
109
|
+
* current) and BEFORE the base is taken (so the base includes them).
|
|
123
110
|
* Default: no secondaries.
|
|
124
111
|
*
|
|
125
112
|
* This hook exists because that ordering is a trap an adopter would otherwise
|
|
126
113
|
* hit silently. Registering from an overridden `setSourceRoot` *after*
|
|
127
|
-
* `super.setSourceRoot(...)` runs too late — the
|
|
114
|
+
* `super.setSourceRoot(...)` runs too late — the base has already been
|
|
128
115
|
* captured without the secondaries, so the first conflict reconcile measures
|
|
129
|
-
* the user's intent against a
|
|
116
|
+
* the user's intent against a base missing half the write set and the
|
|
130
117
|
* secondary edits look like foreign changes. Registering *before* the super
|
|
131
118
|
* call is too early for a URI derived from the new primary. Overriding this
|
|
132
119
|
* instead removes the choice.
|
|
@@ -137,8 +124,9 @@ export class ReconcilingMultiDocumentGlspState<TRoot extends AstNode, TPrimary e
|
|
|
137
124
|
|
|
138
125
|
/**
|
|
139
126
|
* Persist the whole write set, then capture the resulting primary root. On a
|
|
140
|
-
* `ConflictError` from the
|
|
141
|
-
* as the single-document state does — the orchestration is
|
|
127
|
+
* `ConflictError` from any document of the set, reconcile via the injected
|
|
128
|
+
* policy exactly as the single-document state does — the orchestration is
|
|
129
|
+
* shared.
|
|
142
130
|
*/
|
|
143
131
|
async updateSourceModel(model: MultiDocumentSourceModel<TPrimary>, basedOn: BasedOn = this.basedOn): Promise<void> {
|
|
144
132
|
return reconcileSourceModelWrite<MultiDocumentSourceModel<TPrimary>>(model, basedOn, {
|
|
@@ -147,7 +135,7 @@ export class ReconcilingMultiDocumentGlspState<TRoot extends AstNode, TPrimary e
|
|
|
147
135
|
this.setSourceRoot(this._sourceUri, root);
|
|
148
136
|
},
|
|
149
137
|
refetch: () => this.refetch(),
|
|
150
|
-
|
|
138
|
+
base: this.base,
|
|
151
139
|
conflictResolver: this.conflictResolver,
|
|
152
140
|
logger: this.logger,
|
|
153
141
|
onConflictDropped: () => this.refreshSourceRoot()
|
|
@@ -155,112 +143,99 @@ export class ReconcilingMultiDocumentGlspState<TRoot extends AstNode, TPrimary e
|
|
|
155
143
|
}
|
|
156
144
|
|
|
157
145
|
/**
|
|
158
|
-
* Write hook —
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
146
|
+
* Write hook — every document of `model` that changed, opened first through
|
|
147
|
+
* {@link openForWrite}, in one `updateAll` on the diagram's session: the
|
|
148
|
+
* primary gated on `basedOn`, each secondary on {@link secondaryBasedOn}, or
|
|
149
|
+
* every document on `'anything'` when `basedOn` is `'anything'`. Resolves to
|
|
150
|
+
* the primary's root, the one already captured when the primary did not
|
|
151
|
+
* change. A write that bypasses `updateAll` gives up the all-or-none
|
|
152
|
+
* guarantee the class describes. Throws without a session
|
|
153
|
+
* ({@link requireModelSession}).
|
|
162
154
|
*/
|
|
163
155
|
protected async persist(model: MultiDocumentSourceModel<TPrimary>, basedOn: BasedOn): Promise<{ root: TRoot }> {
|
|
156
|
+
const primaryChanged = this.hasChanged(this.base?.primary, model.primary);
|
|
157
|
+
const updates: ClientSessionWriteArgs<TransferElement>[] = primaryChanged
|
|
158
|
+
? [{ uri: this._sourceUri, model: model.primary, basedOn }]
|
|
159
|
+
: [];
|
|
164
160
|
for (const [uri, secondary] of Object.entries(model.secondaries)) {
|
|
165
|
-
if (this.hasChanged(this.
|
|
166
|
-
|
|
161
|
+
if (this.hasChanged(this.base?.secondaries[uri], secondary)) {
|
|
162
|
+
updates.push({ uri, model: secondary, basedOn: basedOn === 'anything' ? 'anything' : this.secondaryBasedOn(uri) });
|
|
167
163
|
}
|
|
168
164
|
}
|
|
169
|
-
if (
|
|
170
|
-
// Nothing to write, so nothing to gate either — return the root already
|
|
171
|
-
// captured rather than round-tripping the document for no reason.
|
|
165
|
+
if (updates.length === 0) {
|
|
172
166
|
return { root: this._sourceRoot };
|
|
173
167
|
}
|
|
174
|
-
|
|
168
|
+
const session = this.requireModelSession();
|
|
169
|
+
for (const update of updates) {
|
|
170
|
+
await this.openForWrite(session, update.uri);
|
|
171
|
+
}
|
|
172
|
+
const documents = await session.updateAll({ updates });
|
|
173
|
+
return { root: primaryChanged ? (documents[0].root as unknown as TRoot) : this._sourceRoot };
|
|
175
174
|
}
|
|
176
175
|
|
|
177
176
|
/**
|
|
178
|
-
* Whether `candidate` differs from the
|
|
177
|
+
* Whether `candidate` differs from the base projection of the same
|
|
179
178
|
* document, and therefore needs writing.
|
|
180
179
|
*
|
|
181
180
|
* **Skipping unchanged documents is correctness, not an optimisation.** A
|
|
182
|
-
* write
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
181
|
+
* write re-serializes from the AST, so writing a document that did not
|
|
182
|
+
* change still rewrites its text in the serializer's own layout. Persisting
|
|
183
|
+
* the whole write set unconditionally therefore means a pure layout drag
|
|
184
|
+
* reflows the semantic file, a change the user never asked for and would
|
|
185
|
+
* struggle to attribute. Comments survive that rewrite, but the
|
|
186
|
+
* hand-formatting around them does not.
|
|
188
187
|
*
|
|
189
188
|
* Compared by serialised form. Both sides come from the same encoder walking
|
|
190
189
|
* the same shape, so key order is stable and a string compare is sound here;
|
|
191
190
|
* it is also cheap enough to run per document per write.
|
|
192
191
|
*/
|
|
193
|
-
protected hasChanged(
|
|
194
|
-
return
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/** Write the primary document, opting into the conflict gate unless `basedOn` is `'anything'`. */
|
|
198
|
-
protected async persistPrimary(model: TPrimary, basedOn: BasedOn): Promise<{ root: TRoot }> {
|
|
199
|
-
const document = await this.sharedServices.model.ModelService.update({
|
|
200
|
-
uri: this._sourceUri,
|
|
201
|
-
model,
|
|
202
|
-
clientId: this.clientId,
|
|
203
|
-
basedOn
|
|
204
|
-
});
|
|
205
|
-
return document as unknown as { root: TRoot };
|
|
192
|
+
protected hasChanged(base: object | undefined, candidate: object): boolean {
|
|
193
|
+
return base === undefined || JSON.stringify(base) !== JSON.stringify(candidate);
|
|
206
194
|
}
|
|
207
195
|
|
|
208
196
|
/**
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
197
|
+
* Make sure the diagram's session has `uri` open before the write set is
|
|
198
|
+
* written. Default: open it, a no-op for a document the session already
|
|
199
|
+
* has open. Override to create a document the write set names before it
|
|
200
|
+
* exists, through `session.create`: `updateAll` opens nothing.
|
|
213
201
|
*/
|
|
214
|
-
protected
|
|
215
|
-
|
|
216
|
-
uri,
|
|
217
|
-
model,
|
|
218
|
-
clientId: this.clientId,
|
|
219
|
-
basedOn: this.secondaryBasedOn(uri)
|
|
220
|
-
});
|
|
202
|
+
protected openForWrite(session: ClientSession<AstNode>, uri: string): Promise<void> {
|
|
203
|
+
return session.open(uri);
|
|
221
204
|
}
|
|
222
205
|
|
|
223
206
|
/**
|
|
224
|
-
* What a secondary write declares it was based on. Default
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
* ({@link AbstractHydraniumGlspState.snapshotVersionOf}), and returning that
|
|
231
|
-
* value here is the whole opt-in; without it the only place to put the
|
|
232
|
-
* comparison is a copy of {@link persistSecondary}, which then has to be kept
|
|
233
|
-
* in step with the framework's write call.
|
|
234
|
-
*
|
|
235
|
-
* **Opting in buys a conflict report and costs atomicity.** A gated secondary
|
|
236
|
-
* throws mid-set, after earlier secondaries have already been written, and if
|
|
237
|
-
* the reconcile then drops the edit those writes stay. Weigh that against the
|
|
238
|
-
* default, which lets a foreign edit to a secondary be overwritten silently.
|
|
207
|
+
* What a secondary write declares it was based on. Default: the version the
|
|
208
|
+
* secondary had when the source root was last read
|
|
209
|
+
* ({@link AbstractHydraniumGlspState.snapshotVersionOf}), so a foreign edit
|
|
210
|
+
* to it since is reported as a conflict and reconciled rather than
|
|
211
|
+
* overwritten; `'anything'` for a secondary with no recorded version.
|
|
212
|
+
* Return `'anything'` to force secondary writes.
|
|
239
213
|
*/
|
|
240
|
-
protected secondaryBasedOn(
|
|
241
|
-
return 'anything';
|
|
214
|
+
protected secondaryBasedOn(uri: string): BasedOn {
|
|
215
|
+
return this.snapshotVersionOf(uri) ?? 'anything';
|
|
242
216
|
}
|
|
243
217
|
|
|
244
218
|
/**
|
|
245
|
-
* Refetch hook — the current
|
|
246
|
-
*
|
|
219
|
+
* Refetch hook — the current projection across the write set, each document
|
|
220
|
+
* read through {@link AbstractHydraniumGlspState.readCurrentRoot}: theirs,
|
|
221
|
+
* which the conflict resolver replays the user's intent onto.
|
|
247
222
|
* Returns `undefined` when the PRIMARY cannot be read, since a reconcile
|
|
248
223
|
* without it has nothing to merge into; an unreadable secondary is omitted the
|
|
249
224
|
* same way {@link sourceModel} omits one.
|
|
250
225
|
*/
|
|
251
226
|
protected async refetch(): Promise<MultiDocumentSourceModel<TPrimary> | undefined> {
|
|
252
|
-
const
|
|
253
|
-
if (!
|
|
227
|
+
const theirs = await this.readCurrentRoot(this._sourceUri);
|
|
228
|
+
if (!theirs) {
|
|
254
229
|
return undefined;
|
|
255
230
|
}
|
|
256
231
|
const secondaries: Record<string, TransferElement> = {};
|
|
257
232
|
for (const uri of this.secondaryUris) {
|
|
258
|
-
const
|
|
259
|
-
if (
|
|
260
|
-
secondaries[uri] = this.projectRoot(
|
|
233
|
+
const root = await this.readCurrentRoot(uri);
|
|
234
|
+
if (root) {
|
|
235
|
+
secondaries[uri] = this.projectRoot(root);
|
|
261
236
|
}
|
|
262
237
|
}
|
|
263
|
-
return { primary: this.projectRoot(
|
|
238
|
+
return { primary: this.projectRoot<TPrimary>(theirs), secondaries };
|
|
264
239
|
}
|
|
265
240
|
|
|
266
241
|
/** Project a currently-loaded document's root, or `undefined` when it is not loaded. */
|
|
@@ -27,16 +27,16 @@ import { reconcileSourceModelWrite } from './reconcile-source-model-write.js';
|
|
|
27
27
|
* field-level shape is what lets `fast-json-patch` diff per field, so undo /
|
|
28
28
|
* redo and forward-write reconcile act per field instead of clobbering the
|
|
29
29
|
* whole document.
|
|
30
|
-
* - {@link
|
|
30
|
+
* - {@link base} — the last in-sync projection, captured on every
|
|
31
31
|
* {@link setSourceRoot}. A forward-write conflict reconciles the user's
|
|
32
|
-
* intent (
|
|
32
|
+
* intent (base → ours) against the server's current root (theirs).
|
|
33
33
|
* - {@link updateSourceModel} — the concrete reconcile template: persist,
|
|
34
34
|
* and on a `ConflictError` consult the injected `conflictResolver` and act
|
|
35
35
|
* on the merged / no-op / conflict / unavailable outcome.
|
|
36
36
|
* - {@link persist} / {@link refetch} — the only I/O seams. Defaults route
|
|
37
|
-
* through `
|
|
38
|
-
*
|
|
39
|
-
*
|
|
37
|
+
* through the diagram session's `update` and `ModelService.validated`;
|
|
38
|
+
* adopters whose document round-trip differs override these without
|
|
39
|
+
* touching the orchestration.
|
|
40
40
|
*
|
|
41
41
|
* Adopters whose source model is whole-document text (a bare `{ text }`
|
|
42
42
|
* source model) or that are read-only do NOT extend this class — they extend
|
|
@@ -52,12 +52,12 @@ export class ReconcilingTransferHydraniumGlspState<TRoot extends AstNode, TSourc
|
|
|
52
52
|
{
|
|
53
53
|
/**
|
|
54
54
|
* Last in-sync source-model projection, captured on every
|
|
55
|
-
* {@link setSourceRoot} (initial load + post-update). The
|
|
55
|
+
* {@link setSourceRoot} (initial load + post-update). The base a
|
|
56
56
|
* forward-write conflict reconciles against: it stays the pre-command
|
|
57
57
|
* state because operation handlers mutate `_sourceRoot` in place during
|
|
58
58
|
* `execute` while `setSourceRoot` only re-runs once the write commits.
|
|
59
59
|
*/
|
|
60
|
-
protected
|
|
60
|
+
protected base?: TSourceModel;
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
63
|
* Persisted-shape projection of the current source root, consumed by GLSP's
|
|
@@ -73,13 +73,13 @@ export class ReconcilingTransferHydraniumGlspState<TRoot extends AstNode, TSourc
|
|
|
73
73
|
|
|
74
74
|
override setSourceRoot(uri: string, root: TRoot): void {
|
|
75
75
|
super.setSourceRoot(uri, root);
|
|
76
|
-
this.
|
|
76
|
+
this.base = this.sourceModel;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
/**
|
|
80
80
|
* Persist `model` back to the document store, then capture the resulting
|
|
81
81
|
* AST root. On a `ConflictError` (the based-on version was superseded),
|
|
82
|
-
* reconcile the user's intent against the
|
|
82
|
+
* reconcile the user's intent against the server's current root via the bound
|
|
83
83
|
* `conflictResolver` and act on the outcome — one declarative policy
|
|
84
84
|
* (force = last-writer-wins, reconciling = field-level merge) shared with
|
|
85
85
|
* undo / redo.
|
|
@@ -94,7 +94,7 @@ export class ReconcilingTransferHydraniumGlspState<TRoot extends AstNode, TSourc
|
|
|
94
94
|
this.setSourceRoot(this._sourceUri, root);
|
|
95
95
|
},
|
|
96
96
|
refetch: () => this.refetch(),
|
|
97
|
-
|
|
97
|
+
base: this.base,
|
|
98
98
|
conflictResolver: this.conflictResolver,
|
|
99
99
|
logger: this.logger,
|
|
100
100
|
onConflictDropped: () => this.refreshSourceRoot()
|
|
@@ -103,30 +103,26 @@ export class ReconcilingTransferHydraniumGlspState<TRoot extends AstNode, TSourc
|
|
|
103
103
|
|
|
104
104
|
/**
|
|
105
105
|
* Persist hook — the only write-side I/O. Default routes the structured
|
|
106
|
-
* model through `
|
|
107
|
-
* the `ConflictError` gate
|
|
106
|
+
* model through the diagram session's `update` (serialize → reparse),
|
|
107
|
+
* opting into the `ConflictError` gate unless `basedOn` is `'anything'`,
|
|
108
|
+
* and throws without a session. Adopters whose document
|
|
108
109
|
* round-trip differs override this; the orchestration in
|
|
109
110
|
* {@link updateSourceModel} is unchanged.
|
|
110
111
|
*/
|
|
111
112
|
protected async persist(model: TSourceModel, basedOn: BasedOn): Promise<{ root: TRoot }> {
|
|
112
|
-
const document = await this.
|
|
113
|
-
uri: this._sourceUri,
|
|
114
|
-
model,
|
|
115
|
-
clientId: this.clientId,
|
|
116
|
-
basedOn
|
|
117
|
-
});
|
|
113
|
+
const document = await this.requireModelSession().update({ uri: this._sourceUri, model, basedOn });
|
|
118
114
|
return document as unknown as { root: TRoot };
|
|
119
115
|
}
|
|
120
116
|
|
|
121
117
|
/**
|
|
122
|
-
* Refetch hook —
|
|
123
|
-
* projection (or `undefined` when unavailable)
|
|
124
|
-
* replays the user's intent
|
|
125
|
-
*
|
|
126
|
-
* adopters override alongside {@link persist}.
|
|
118
|
+
* Refetch hook — the stored text parsed afresh, as a persisted-shape
|
|
119
|
+
* projection (or `undefined` when unavailable): theirs, which the
|
|
120
|
+
* `conflictResolver` replays the user's intent onto. Default uses
|
|
121
|
+
* {@link AbstractHydraniumGlspState.readCurrentRoot} + the framework
|
|
122
|
+
* encoder's `'grammar'` mode; adopters override alongside {@link persist}.
|
|
127
123
|
*/
|
|
128
124
|
protected async refetch(): Promise<TSourceModel | undefined> {
|
|
129
|
-
const
|
|
130
|
-
return
|
|
125
|
+
const theirs = await this.readCurrentRoot(this._sourceUri);
|
|
126
|
+
return theirs ? (this.sharedServices.model.TransferEncoder.toTransfer(theirs, 'grammar') as unknown as TSourceModel) : undefined;
|
|
131
127
|
}
|
|
132
128
|
}
|