@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.
Files changed (39) hide show
  1. package/lib/command/hydranium-glsp-recording-command.d.ts +1 -1
  2. package/lib/command/hydranium-glsp-recording-command.js +1 -1
  3. package/lib/messages/index.d.ts +1 -1
  4. package/lib/messages/index.d.ts.map +1 -1
  5. package/lib/messages/index.js +1 -1
  6. package/lib/messages/index.js.map +1 -1
  7. package/lib/state/abstract-hydranium-glsp-state.d.ts +52 -8
  8. package/lib/state/abstract-hydranium-glsp-state.d.ts.map +1 -1
  9. package/lib/state/abstract-hydranium-glsp-state.js +77 -5
  10. package/lib/state/abstract-hydranium-glsp-state.js.map +1 -1
  11. package/lib/state/full-text-hydranium-glsp-state.d.ts +4 -4
  12. package/lib/state/full-text-hydranium-glsp-state.d.ts.map +1 -1
  13. package/lib/state/full-text-hydranium-glsp-state.js +5 -10
  14. package/lib/state/full-text-hydranium-glsp-state.js.map +1 -1
  15. package/lib/state/reconcile-source-model-write.d.ts +2 -2
  16. package/lib/state/reconcile-source-model-write.d.ts.map +1 -1
  17. package/lib/state/reconcile-source-model-write.js +1 -1
  18. package/lib/state/reconcile-source-model-write.js.map +1 -1
  19. package/lib/state/reconciling-multi-document-glsp-state.d.ts +54 -74
  20. package/lib/state/reconciling-multi-document-glsp-state.d.ts.map +1 -1
  21. package/lib/state/reconciling-multi-document-glsp-state.js +77 -102
  22. package/lib/state/reconciling-multi-document-glsp-state.js.map +1 -1
  23. package/lib/state/reconciling-transfer-hydranium-glsp-state.d.ts +16 -15
  24. package/lib/state/reconciling-transfer-hydranium-glsp-state.d.ts.map +1 -1
  25. package/lib/state/reconciling-transfer-hydranium-glsp-state.js +21 -25
  26. package/lib/state/reconciling-transfer-hydranium-glsp-state.js.map +1 -1
  27. package/lib/storage/hydranium-glsp-storage.d.ts +103 -26
  28. package/lib/storage/hydranium-glsp-storage.d.ts.map +1 -1
  29. package/lib/storage/hydranium-glsp-storage.js +199 -32
  30. package/lib/storage/hydranium-glsp-storage.js.map +1 -1
  31. package/package.json +7 -7
  32. package/src/command/hydranium-glsp-recording-command.ts +1 -1
  33. package/src/messages/index.ts +6 -1
  34. package/src/state/abstract-hydranium-glsp-state.ts +85 -8
  35. package/src/state/full-text-hydranium-glsp-state.ts +5 -10
  36. package/src/state/reconcile-source-model-write.ts +3 -3
  37. package/src/state/reconciling-multi-document-glsp-state.ts +78 -103
  38. package/src/state/reconciling-transfer-hydranium-glsp-state.ts +21 -25
  39. 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
- * `ModelService.update` (which accepts a raw text payload) and captures the
37
- * re-parsed root, gated on the caller's `basedOn`.
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 `baseline` / conflict reconcile: a whole-document model has exactly one
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.sharedServices.model.ModelService.update({
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
- /** Last in-sync projection the user's intent is measured against. */
30
- readonly baseline: TModel | undefined;
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.baseline ?? model, model, () => hooks.refetch());
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
- * **Writes are NOT atomic, deliberately, and the window is real.** Each document
48
- * goes through its own `ModelService.update`, so a failure after the first write
49
- * has landed leaves the set inconsistent on disk. There is no multi-document
50
- * transaction to lean on: `WorkspaceLock` serialises builds but does not roll
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
- * **What makes the window tolerable is write ORDER, and the rule is
55
- * referenced-before-referencing** — not "secondaries first" as such. Write the
56
- * document whose content the others point AT before the documents that point at
57
- * it, so a partial write leaves an unreferenced element (inert, and the direction
58
- * an integrity rule can repair) rather than a reference to something that does
59
- * not exist (a linking error). {@link persist} implements that as
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
- * **The conflict gate covers the primary only.** Secondaries are written without
70
- * a based-on version, so a concurrent foreign edit to one is overwritten rather
71
- * than reconciled. Gating them too would need a per-document reconcile whose
72
- * outcomes can disagree (merge one, conflict another) with no way to un-write the
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 baseline: the state a
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 baseline?: MultiDocumentSourceModel<TPrimary>;
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.baseline = this.sourceModel;
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 baseline is taken (so the baseline includes them).
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 baseline has already been
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 baseline missing half the write set and the
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 primary, reconcile via the injected policy exactly
141
- * as the single-document state does — the orchestration is shared.
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
- baseline: this.baseline,
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 — secondaries first (ungated), primary last (gated on
159
- * `basedOn`). The order is the failure-mode choice documented on the
160
- * class, not incidental: override this when the primary is the document the
161
- * others REFERENCE, since then this order writes the references first.
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.baseline?.secondaries[uri], secondary)) {
166
- await this.persistSecondary(uri, secondary);
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 (!this.hasChanged(this.baseline?.primary, model.primary)) {
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
- return this.persistPrimary(model.primary, basedOn);
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 baseline projection of the same
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 goes through `ModelService.update`, which re-serializes from the AST —
183
- * so writing a document that did not change still rewrites its text in the
184
- * serializer's own layout. Persisting the whole write set unconditionally
185
- * therefore means a pure layout drag reflows the semantic file, a change the
186
- * user never asked for and would struggle to attribute. Comments survive that
187
- * rewrite, but the hand-formatting around them does not.
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(baseline: object | undefined, candidate: object): boolean {
194
- return baseline === undefined || JSON.stringify(baseline) !== JSON.stringify(candidate);
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
- * Write one secondary document, gated on whatever {@link secondaryBasedOn}
210
- * returns. Override alongside {@link persistPrimary} when the round-trip
211
- * differs per document role — a layout file and a semantic file need not
212
- * share a serializer.
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 async persistSecondary(uri: string, model: TransferElement): Promise<void> {
215
- await this.sharedServices.model.ModelService.update({
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 `'anything'` —
225
- * ungated, because gating the whole set needs a per-document reconcile whose
226
- * outcomes can disagree, with no way to un-write an already-merged sibling.
227
- *
228
- * **The hook exists so that opting in is an override rather than a
229
- * reimplementation.** The state already holds what such a check needs
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(_uri: string): BasedOn {
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 settled projection across the write set, used by
246
- * the conflict resolver to replay the user's intent against fresh state.
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 fresh = await this.sharedServices.model.ModelService.validated(this._sourceUri).catch(() => undefined);
253
- if (!fresh) {
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 settled = await this.sharedServices.model.ModelService.validated(uri).catch(() => undefined);
259
- if (settled) {
260
- secondaries[uri] = this.projectRoot(settled.root as unknown as AstNode);
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(fresh.root as unknown as AstNode) as TPrimary, secondaries };
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 baseline} — the last in-sync projection, captured on every
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 (baseline → attempted) against the fresh server root.
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 `ModelService.update` / `.validated` (reachable from the base via
38
- * `sharedServices.model`); adopters whose document round-trip differs
39
- * override these without touching the orchestration.
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 baseline a
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 baseline?: TSourceModel;
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.baseline = this.sourceModel;
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 fresh server root via the bound
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
- baseline: this.baseline,
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 `ModelService.update` (serialize → reparse), opting into
107
- * the `ConflictError` gate when `version` is given. Adopters whose document
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.sharedServices.model.ModelService.update({
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 — reads the current settled server root as a persisted-shape
123
- * projection (or `undefined` when unavailable). The `conflictResolver`
124
- * replays the user's intent against this fresh state. Default uses
125
- * `ModelService.validated` + the framework encoder's `'grammar'` mode;
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 fresh = await this.sharedServices.model.ModelService.validated(this._sourceUri).catch(() => undefined);
130
- return fresh ? (this.sharedServices.model.TransferEncoder.toTransfer(fresh.root, 'grammar') as unknown as TSourceModel) : undefined;
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
  }