@hydranium/glsp-server 1.0.0-next.12 → 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 (71) hide show
  1. package/README.md +13 -2
  2. package/lib/command/hydranium-glsp-recording-command.d.ts +22 -22
  3. package/lib/command/hydranium-glsp-recording-command.d.ts.map +1 -1
  4. package/lib/command/hydranium-glsp-recording-command.js +25 -26
  5. package/lib/command/hydranium-glsp-recording-command.js.map +1 -1
  6. package/lib/launcher/hydranium-glsp-server.d.ts +4 -3
  7. package/lib/launcher/hydranium-glsp-server.d.ts.map +1 -1
  8. package/lib/launcher/hydranium-glsp-server.js +4 -3
  9. package/lib/launcher/hydranium-glsp-server.js.map +1 -1
  10. package/lib/messages/index.d.ts +24 -0
  11. package/lib/messages/index.d.ts.map +1 -0
  12. package/lib/messages/index.js +24 -0
  13. package/lib/messages/index.js.map +1 -0
  14. package/lib/node/start-glsp-server.js +1 -1
  15. package/lib/node/start-glsp-server.js.map +1 -1
  16. package/lib/state/abstract-hydranium-glsp-state.d.ts +110 -43
  17. package/lib/state/abstract-hydranium-glsp-state.d.ts.map +1 -1
  18. package/lib/state/abstract-hydranium-glsp-state.js +130 -41
  19. package/lib/state/abstract-hydranium-glsp-state.js.map +1 -1
  20. package/lib/state/full-text-hydranium-glsp-state.d.ts +6 -6
  21. package/lib/state/full-text-hydranium-glsp-state.d.ts.map +1 -1
  22. package/lib/state/full-text-hydranium-glsp-state.js +6 -10
  23. package/lib/state/full-text-hydranium-glsp-state.js.map +1 -1
  24. package/lib/state/reconcile-source-model-write.d.ts +9 -8
  25. package/lib/state/reconcile-source-model-write.d.ts.map +1 -1
  26. package/lib/state/reconcile-source-model-write.js +8 -8
  27. package/lib/state/reconcile-source-model-write.js.map +1 -1
  28. package/lib/state/reconciling-multi-document-glsp-state.d.ts +59 -59
  29. package/lib/state/reconciling-multi-document-glsp-state.d.ts.map +1 -1
  30. package/lib/state/reconciling-multi-document-glsp-state.js +84 -82
  31. package/lib/state/reconciling-multi-document-glsp-state.js.map +1 -1
  32. package/lib/state/reconciling-transfer-hydranium-glsp-state.d.ts +19 -18
  33. package/lib/state/reconciling-transfer-hydranium-glsp-state.d.ts.map +1 -1
  34. package/lib/state/reconciling-transfer-hydranium-glsp-state.js +26 -30
  35. package/lib/state/reconciling-transfer-hydranium-glsp-state.js.map +1 -1
  36. package/lib/storage/hydranium-glsp-storage.d.ts +271 -76
  37. package/lib/storage/hydranium-glsp-storage.d.ts.map +1 -1
  38. package/lib/storage/hydranium-glsp-storage.js +383 -87
  39. package/lib/storage/hydranium-glsp-storage.js.map +1 -1
  40. package/lib/storage/index.d.ts +1 -1
  41. package/lib/storage/index.js +1 -1
  42. package/lib/storage/save-delivery-policy.d.ts +46 -0
  43. package/lib/storage/save-delivery-policy.d.ts.map +1 -0
  44. package/lib/storage/{save-conflict-policy.js → save-delivery-policy.js} +7 -8
  45. package/lib/storage/save-delivery-policy.js.map +1 -0
  46. package/lib/validation/diagnostic-markers.d.ts +2 -2
  47. package/lib/validation/diagnostic-markers.d.ts.map +1 -1
  48. package/lib/validation/diagnostic-markers.js +7 -0
  49. package/lib/validation/diagnostic-markers.js.map +1 -1
  50. package/lib/validation/hydranium-glsp-model-validator.d.ts.map +1 -1
  51. package/lib/validation/hydranium-glsp-model-validator.js +1 -1
  52. package/lib/validation/hydranium-glsp-model-validator.js.map +1 -1
  53. package/package.json +17 -8
  54. package/src/command/hydranium-glsp-recording-command.ts +26 -26
  55. package/src/launcher/hydranium-glsp-server.ts +4 -3
  56. package/src/messages/index.ts +30 -0
  57. package/src/node/start-glsp-server.ts +1 -1
  58. package/src/state/abstract-hydranium-glsp-state.ts +162 -51
  59. package/src/state/full-text-hydranium-glsp-state.ts +7 -11
  60. package/src/state/reconcile-source-model-write.ts +18 -15
  61. package/src/state/reconciling-multi-document-glsp-state.ts +86 -83
  62. package/src/state/reconciling-transfer-hydranium-glsp-state.ts +27 -31
  63. package/src/storage/hydranium-glsp-storage.ts +445 -97
  64. package/src/storage/index.ts +1 -1
  65. package/src/storage/save-delivery-policy.ts +44 -0
  66. package/src/validation/diagnostic-markers.ts +10 -3
  67. package/src/validation/hydranium-glsp-model-validator.ts +3 -3
  68. package/lib/storage/save-conflict-policy.d.ts +0 -56
  69. package/lib/storage/save-conflict-policy.d.ts.map +0 -1
  70. package/lib/storage/save-conflict-policy.js.map +0 -1
  71. package/src/storage/save-conflict-policy.ts +0 -52
@@ -10,7 +10,8 @@
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 TransferElement } from '@hydranium/protocol';
13
+ import { type ClientSession, type ClientSessionWriteArgs } from '@hydranium/core';
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';
16
17
 
@@ -44,35 +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. The captured versions
74
- * ARE available via {@link AbstractHydraniumGlspState.capturedVersionOf}, so an
75
- * adopter that wants a coarser check can compare before writing.
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.
76
64
  */
77
65
  @injectable()
78
66
  export class ReconcilingMultiDocumentGlspState<TRoot extends AstNode, TPrimary extends TransferElement = TransferElement>
@@ -81,10 +69,10 @@ export class ReconcilingMultiDocumentGlspState<TRoot extends AstNode, TPrimary e
81
69
  {
82
70
  /**
83
71
  * Last in-sync projection across the whole write set, captured on every
84
- * {@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
85
73
  * forward-write conflict reconciles the user's intent against.
86
74
  */
87
- protected baseline?: MultiDocumentSourceModel<TPrimary>;
75
+ protected base?: MultiDocumentSourceModel<TPrimary>;
88
76
 
89
77
  /**
90
78
  * Projection of the primary plus every registered secondary, in the framework
@@ -111,21 +99,21 @@ export class ReconcilingMultiDocumentGlspState<TRoot extends AstNode, TPrimary e
111
99
  override setSourceRoot(uri: string, root: TRoot): void {
112
100
  super.setSourceRoot(uri, root);
113
101
  this.trackWriteSet(uri);
114
- this.baseline = this.sourceModel;
102
+ this.base = this.sourceModel;
115
103
  }
116
104
 
117
105
  /**
118
106
  * Register the secondary documents that belong to the primary at `uri`, via
119
107
  * {@link AbstractHydraniumGlspState.trackSecondaryDocument}. Called on every
120
108
  * {@link setSourceRoot}, after the primary is captured (so `sourceUri` is
121
- * 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).
122
110
  * Default: no secondaries.
123
111
  *
124
112
  * This hook exists because that ordering is a trap an adopter would otherwise
125
113
  * hit silently. Registering from an overridden `setSourceRoot` *after*
126
- * `super.setSourceRoot(...)` runs too late — the baseline has already been
114
+ * `super.setSourceRoot(...)` runs too late — the base has already been
127
115
  * captured without the secondaries, so the first conflict reconcile measures
128
- * 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
129
117
  * secondary edits look like foreign changes. Registering *before* the super
130
118
  * call is too early for a URI derived from the new primary. Overriding this
131
119
  * instead removes the choice.
@@ -136,17 +124,18 @@ export class ReconcilingMultiDocumentGlspState<TRoot extends AstNode, TPrimary e
136
124
 
137
125
  /**
138
126
  * Persist the whole write set, then capture the resulting primary root. On a
139
- * `ConflictError` from the primary, reconcile via the injected policy exactly
140
- * 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.
141
130
  */
142
- async updateSourceModel(model: MultiDocumentSourceModel<TPrimary>, version?: number): Promise<void> {
143
- return reconcileSourceModelWrite<MultiDocumentSourceModel<TPrimary>>(model, version, {
144
- persist: async (candidate, baseVersion) => {
145
- const { root } = await this.persist(candidate, baseVersion);
131
+ async updateSourceModel(model: MultiDocumentSourceModel<TPrimary>, basedOn: BasedOn = this.basedOn): Promise<void> {
132
+ return reconcileSourceModelWrite<MultiDocumentSourceModel<TPrimary>>(model, basedOn, {
133
+ persist: async (candidate, candidateBasedOn) => {
134
+ const { root } = await this.persist(candidate, candidateBasedOn);
146
135
  this.setSourceRoot(this._sourceUri, root);
147
136
  },
148
137
  refetch: () => this.refetch(),
149
- baseline: this.baseline,
138
+ base: this.base,
150
139
  conflictResolver: this.conflictResolver,
151
140
  logger: this.logger,
152
141
  onConflictDropped: () => this.refreshSourceRoot()
@@ -154,85 +143,99 @@ export class ReconcilingMultiDocumentGlspState<TRoot extends AstNode, TPrimary e
154
143
  }
155
144
 
156
145
  /**
157
- * Write hook — secondaries first (ungated), primary last (gated on
158
- * `baseVersion`). The order is the failure-mode choice documented on the
159
- * class, not incidental: override this when the primary is the document the
160
- * 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}).
161
154
  */
162
- protected async persist(model: MultiDocumentSourceModel<TPrimary>, baseVersion?: number): Promise<{ root: TRoot }> {
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
+ : [];
163
160
  for (const [uri, secondary] of Object.entries(model.secondaries)) {
164
- if (this.hasChanged(this.baseline?.secondaries[uri], secondary)) {
165
- 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) });
166
163
  }
167
164
  }
168
- if (!this.hasChanged(this.baseline?.primary, model.primary)) {
169
- // Nothing to write, so nothing to gate either — return the root already
170
- // captured rather than round-tripping the document for no reason.
165
+ if (updates.length === 0) {
171
166
  return { root: this._sourceRoot };
172
167
  }
173
- return this.persistPrimary(model.primary, baseVersion);
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 };
174
174
  }
175
175
 
176
176
  /**
177
- * Whether `candidate` differs from the baseline projection of the same
177
+ * Whether `candidate` differs from the base projection of the same
178
178
  * document, and therefore needs writing.
179
179
  *
180
180
  * **Skipping unchanged documents is correctness, not an optimisation.** A
181
- * write goes through `ModelService.update`, which re-serializes from the AST —
182
- * so writing a document that did not change still rewrites its text, and a
183
- * serializer is free to normalise formatting and cannot preserve comments.
184
- * Persisting the whole write set unconditionally therefore means a pure layout
185
- * drag reflows the semantic file and strips its comments, which is a data
186
- * loss the user never asked for and would struggle to attribute.
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.
187
187
  *
188
188
  * Compared by serialised form. Both sides come from the same encoder walking
189
189
  * the same shape, so key order is stable and a string compare is sound here;
190
190
  * it is also cheap enough to run per document per write.
191
191
  */
192
- protected hasChanged(baseline: object | undefined, candidate: object): boolean {
193
- return baseline === undefined || JSON.stringify(baseline) !== JSON.stringify(candidate);
192
+ protected hasChanged(base: object | undefined, candidate: object): boolean {
193
+ return base === undefined || JSON.stringify(base) !== JSON.stringify(candidate);
194
194
  }
195
195
 
196
- /** Write the primary document, opting into the conflict gate when `baseVersion` is given. */
197
- protected async persistPrimary(model: TPrimary, baseVersion?: number): Promise<{ root: TRoot }> {
198
- const document = await this.sharedServices.model.ModelService.update({
199
- uri: this._sourceUri,
200
- model,
201
- clientId: this.clientId,
202
- baseVersion
203
- });
204
- return document as unknown as { root: TRoot };
196
+ /**
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.
201
+ */
202
+ protected openForWrite(session: ClientSession<AstNode>, uri: string): Promise<void> {
203
+ return session.open(uri);
205
204
  }
206
205
 
207
206
  /**
208
- * Write one secondary document. Ungated by design (see the class doc); override
209
- * alongside {@link persistPrimary} when the round-trip differs per document
210
- * role — a layout file and a semantic file need not share a serializer.
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.
211
213
  */
212
- protected async persistSecondary(uri: string, model: TransferElement): Promise<void> {
213
- await this.sharedServices.model.ModelService.update({ uri, model, clientId: this.clientId });
214
+ protected secondaryBasedOn(uri: string): BasedOn {
215
+ return this.snapshotVersionOf(uri) ?? 'anything';
214
216
  }
215
217
 
216
218
  /**
217
- * Refetch hook — the current settled projection across the write set, used by
218
- * 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.
219
222
  * Returns `undefined` when the PRIMARY cannot be read, since a reconcile
220
223
  * without it has nothing to merge into; an unreadable secondary is omitted the
221
224
  * same way {@link sourceModel} omits one.
222
225
  */
223
226
  protected async refetch(): Promise<MultiDocumentSourceModel<TPrimary> | undefined> {
224
- const fresh = await this.sharedServices.model.ModelService.validated(this._sourceUri).catch(() => undefined);
225
- if (!fresh) {
227
+ const theirs = await this.readCurrentRoot(this._sourceUri);
228
+ if (!theirs) {
226
229
  return undefined;
227
230
  }
228
231
  const secondaries: Record<string, TransferElement> = {};
229
232
  for (const uri of this.secondaryUris) {
230
- const settled = await this.sharedServices.model.ModelService.validated(uri).catch(() => undefined);
231
- if (settled) {
232
- 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);
233
236
  }
234
237
  }
235
- return { primary: this.projectRoot(fresh.root as unknown as AstNode) as TPrimary, secondaries };
238
+ return { primary: this.projectRoot<TPrimary>(theirs), secondaries };
236
239
  }
237
240
 
238
241
  /** Project a currently-loaded document's root, or `undefined` when it is not loaded. */
@@ -10,7 +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 TransferElement } from '@hydranium/protocol';
13
+ import { type BasedOn, type TransferElement } from '@hydranium/protocol';
14
14
  import { AbstractHydraniumGlspState } from './abstract-hydranium-glsp-state.js';
15
15
  import { reconcileSourceModelWrite } from './reconcile-source-model-write.js';
16
16
 
@@ -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,28 +73,28 @@ 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.
86
86
  */
87
- async updateSourceModel(model: TSourceModel, version?: number): Promise<void> {
87
+ async updateSourceModel(model: TSourceModel, basedOn: BasedOn = this.basedOn): Promise<void> {
88
88
  // Orchestration lives in `reconcileSourceModelWrite` so the multi-document
89
89
  // state gets the identical conflict handling; this method supplies only
90
90
  // the single-document meaning of persist / project.
91
- return reconcileSourceModelWrite<TSourceModel>(model, version, {
92
- persist: async (candidate, baseVersion) => {
93
- const { root } = await this.persist(candidate, baseVersion);
91
+ return reconcileSourceModelWrite<TSourceModel>(model, basedOn, {
92
+ persist: async (candidate, candidateBasedOn) => {
93
+ const { root } = await this.persist(candidate, candidateBasedOn);
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
- protected async persist(model: TSourceModel, baseVersion?: number): Promise<{ root: TRoot }> {
112
- const document = await this.sharedServices.model.ModelService.update({
113
- uri: this._sourceUri,
114
- model,
115
- clientId: this.clientId,
116
- baseVersion
117
- });
112
+ protected async persist(model: TSourceModel, basedOn: BasedOn): Promise<{ root: TRoot }> {
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
  }