@hydranium/glsp-server 1.0.0-next.16 → 1.0.0-next.160
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 +20 -5
- package/lib/browser/start-glsp-server-in-worker.d.ts +30 -39
- package/lib/browser/start-glsp-server-in-worker.d.ts.map +1 -1
- package/lib/browser/start-glsp-server-in-worker.js +45 -11
- package/lib/browser/start-glsp-server-in-worker.js.map +1 -1
- package/lib/command/hydranium-glsp-command-stack.d.ts +27 -0
- package/lib/command/hydranium-glsp-command-stack.d.ts.map +1 -0
- package/lib/command/hydranium-glsp-command-stack.js +53 -0
- package/lib/command/hydranium-glsp-command-stack.js.map +1 -0
- package/lib/command/hydranium-glsp-recording-command.d.ts +22 -22
- package/lib/command/hydranium-glsp-recording-command.d.ts.map +1 -1
- package/lib/command/hydranium-glsp-recording-command.js +25 -26
- package/lib/command/hydranium-glsp-recording-command.js.map +1 -1
- package/lib/command/index.d.ts +1 -0
- package/lib/command/index.d.ts.map +1 -1
- package/lib/command/index.js +1 -0
- package/lib/command/index.js.map +1 -1
- package/lib/launcher/abstract-hydranium-glsp-diagram-module.d.ts +12 -3
- package/lib/launcher/abstract-hydranium-glsp-diagram-module.d.ts.map +1 -1
- package/lib/launcher/abstract-hydranium-glsp-diagram-module.js +18 -2
- package/lib/launcher/abstract-hydranium-glsp-diagram-module.js.map +1 -1
- 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 +14 -10
- package/lib/launcher/hydranium-glsp-server.d.ts.map +1 -1
- package/lib/launcher/hydranium-glsp-server.js +20 -10
- package/lib/launcher/hydranium-glsp-server.js.map +1 -1
- package/lib/messages/index.d.ts +24 -0
- package/lib/messages/index.d.ts.map +1 -0
- package/lib/messages/index.js +24 -0
- package/lib/messages/index.js.map +1 -0
- package/lib/node/start-glsp-server.d.ts +1 -1
- package/lib/node/start-glsp-server.d.ts.map +1 -1
- package/lib/node/start-glsp-server.js +3 -4
- package/lib/node/start-glsp-server.js.map +1 -1
- package/lib/state/abstract-hydranium-glsp-state.d.ts +110 -43
- package/lib/state/abstract-hydranium-glsp-state.d.ts.map +1 -1
- package/lib/state/abstract-hydranium-glsp-state.js +139 -60
- package/lib/state/abstract-hydranium-glsp-state.js.map +1 -1
- package/lib/state/full-text-hydranium-glsp-state.d.ts +6 -6
- package/lib/state/full-text-hydranium-glsp-state.d.ts.map +1 -1
- package/lib/state/full-text-hydranium-glsp-state.js +6 -10
- package/lib/state/full-text-hydranium-glsp-state.js.map +1 -1
- package/lib/state/reconcile-source-model-write.d.ts +9 -8
- package/lib/state/reconcile-source-model-write.d.ts.map +1 -1
- package/lib/state/reconcile-source-model-write.js +8 -8
- package/lib/state/reconcile-source-model-write.js.map +1 -1
- package/lib/state/reconciling-multi-document-glsp-state.d.ts +59 -59
- package/lib/state/reconciling-multi-document-glsp-state.d.ts.map +1 -1
- package/lib/state/reconciling-multi-document-glsp-state.js +84 -82
- package/lib/state/reconciling-multi-document-glsp-state.js.map +1 -1
- package/lib/state/reconciling-transfer-hydranium-glsp-state.d.ts +19 -18
- package/lib/state/reconciling-transfer-hydranium-glsp-state.d.ts.map +1 -1
- package/lib/state/reconciling-transfer-hydranium-glsp-state.js +26 -30
- package/lib/state/reconciling-transfer-hydranium-glsp-state.js.map +1 -1
- 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 +313 -86
- package/lib/storage/hydranium-glsp-storage.d.ts.map +1 -1
- package/lib/storage/hydranium-glsp-storage.js +456 -104
- 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 +46 -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/testing/glsp-harness.js +4 -4
- package/lib/testing/glsp-harness.js.map +1 -1
- package/lib/validation/diagnostic-markers.d.ts +2 -2
- package/lib/validation/diagnostic-markers.d.ts.map +1 -1
- package/lib/validation/diagnostic-markers.js +7 -0
- package/lib/validation/diagnostic-markers.js.map +1 -1
- 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 +17 -8
- package/src/browser/start-glsp-server-in-worker.ts +43 -47
- package/src/command/hydranium-glsp-command-stack.ts +36 -0
- package/src/command/hydranium-glsp-recording-command.ts +26 -26
- package/src/command/index.ts +1 -0
- package/src/launcher/abstract-hydranium-glsp-diagram-module.ts +27 -3
- package/src/launcher/glsp-server-overrides.ts +27 -11
- package/src/launcher/hydranium-glsp-server.ts +20 -10
- package/src/messages/index.ts +30 -0
- package/src/node/start-glsp-server.ts +3 -5
- package/src/state/abstract-hydranium-glsp-state.ts +174 -73
- package/src/state/full-text-hydranium-glsp-state.ts +7 -11
- package/src/state/reconcile-source-model-write.ts +18 -15
- package/src/state/reconciling-multi-document-glsp-state.ts +86 -83
- package/src/state/reconciling-transfer-hydranium-glsp-state.ts +27 -31
- package/src/storage/hydranium-glsp-request-save-model-action-handler.ts +40 -0
- package/src/storage/hydranium-glsp-storage.ts +520 -113
- package/src/storage/index.ts +2 -1
- package/src/storage/save-delivery-policy.ts +44 -0
- package/src/testing/glsp-harness.ts +4 -4
- package/src/validation/diagnostic-markers.ts +10 -3
- package/src/validation/hydranium-glsp-model-validator.ts +3 -3
- 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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"full-text-hydranium-glsp-state.d.ts","sourceRoot":"","sources":["../../src/state/full-text-hydranium-glsp-state.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAElF,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAElD,OAAO,EAAiB,KAAK,YAAY,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"full-text-hydranium-glsp-state.d.ts","sourceRoot":"","sources":["../../src/state/full-text-hydranium-glsp-state.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAElF,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAElD,OAAO,EAAE,KAAK,OAAO,EAAiB,KAAK,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACrF,OAAO,EAAE,0BAA0B,EAAE,MAAM,oCAAoC,CAAC;AAEhF,gFAAgF;AAChF,MAAM,WAAW,mBAAmB;IACjC,IAAI,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,qBACa,0BAA0B,CAAC,KAAK,SAAS,OAAO,CAC1D,SAAQ,0BAA0B,CAAC,KAAK,EAAE,mBAAmB,CAC7D,YAAW,cAAc,CAAC,mBAAmB,CAAC;IAE9C,IAAI,WAAW,IAAI,YAAY,CAAC,mBAAmB,CAAC,CAInD;IAEK,iBAAiB,CAAC,KAAK,EAAE,mBAAmB,EAAE,OAAO,GAAE,OAAsB,GAAG,OAAO,CAAC,IAAI,CAAC;CAIrG"}
|
|
@@ -29,11 +29,11 @@ import { AbstractHydraniumGlspState } from './abstract-hydranium-glsp-state.js';
|
|
|
29
29
|
* `serializer.Serializer` reached via `sharedServices.ServiceRegistry`
|
|
30
30
|
* (so multi-grammar workspaces route to the right serializer). `MaybePromise`
|
|
31
31
|
* because serialization may be async; the sync fast path is preserved.
|
|
32
|
-
* - {@link updateSourceModel} pushes the text back through
|
|
33
|
-
* `
|
|
34
|
-
*
|
|
32
|
+
* - {@link updateSourceModel} pushes the text back through the diagram's
|
|
33
|
+
* session (`update` accepts a raw text payload) and captures the re-parsed
|
|
34
|
+
* root, gated on the caller's `basedOn`; it throws without a session.
|
|
35
35
|
*
|
|
36
|
-
* No `
|
|
36
|
+
* No `base` / conflict reconcile: a whole-document model has exactly one
|
|
37
37
|
* field, so every concurrent edit is a same-field collision and undo / redo
|
|
38
38
|
* degrade to drop-on-divergence. Adopters that need field-level undo use
|
|
39
39
|
* `ReconcilingTransferHydraniumGlspState` instead.
|
|
@@ -44,12 +44,8 @@ let FullTextHydraniumGlspState = class FullTextHydraniumGlspState extends Abstra
|
|
|
44
44
|
const text = serializer.serializeAst(this._sourceRoot);
|
|
45
45
|
return isPromiseLike(text) ? text.then(value => ({ text: value })) : { text };
|
|
46
46
|
}
|
|
47
|
-
async updateSourceModel(model) {
|
|
48
|
-
const document = await this.
|
|
49
|
-
uri: this._sourceUri,
|
|
50
|
-
model: model.text,
|
|
51
|
-
clientId: this.clientId
|
|
52
|
-
});
|
|
47
|
+
async updateSourceModel(model, basedOn = this.basedOn) {
|
|
48
|
+
const document = await this.requireModelSession().update({ uri: this._sourceUri, model: model.text, basedOn });
|
|
53
49
|
this.setSourceRoot(this._sourceUri, document.root);
|
|
54
50
|
}
|
|
55
51
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"full-text-hydranium-glsp-state.js","sourceRoot":"","sources":["../../src/state/full-text-hydranium-glsp-state.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;;;;;;;AAGlF,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAEvC,OAAO,EAAE,GAAG,EAAE,MAAM,oBAAoB,CAAC;AACzC,OAAO,
|
|
1
|
+
{"version":3,"file":"full-text-hydranium-glsp-state.js","sourceRoot":"","sources":["../../src/state/full-text-hydranium-glsp-state.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;;;;;;;AAGlF,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAEvC,OAAO,EAAE,GAAG,EAAE,MAAM,oBAAoB,CAAC;AACzC,OAAO,EAAgB,aAAa,EAAqB,MAAM,qBAAqB,CAAC;AACrF,OAAO,EAAE,0BAA0B,EAAE,MAAM,oCAAoC,CAAC;AAOhF;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEI,IAAM,0BAA0B,GAAhC,MAAM,0BACV,SAAQ,0BAAsD;IAG9D,IAAI,WAAW;QACZ,MAAM,UAAU,GAAG,IAAI,CAAC,cAAc,CAAC,eAAe,CAAC,WAAW,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC,UAAU,CAAC;QACrH,MAAM,IAAI,GAAG,UAAU,CAAC,YAAY,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QACvD,OAAO,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC;IACjF,CAAC;IAED,KAAK,CAAC,iBAAiB,CAAC,KAA0B,EAAE,UAAmB,IAAI,CAAC,OAAO;QAChF,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,mBAAmB,EAAE,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,UAAU,EAAE,KAAK,EAAE,KAAK,CAAC,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;QAC/G,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,UAAU,EAAE,QAAQ,CAAC,IAAa,CAAC,CAAC;IAC/D,CAAC;CACH,CAAA;AAdY,0BAA0B;IADtC,UAAU,EAAE;GACA,0BAA0B,CActC"}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* SPDX-License-Identifier: MIT
|
|
8
8
|
********************************************************************************/
|
|
9
|
-
import { type ConflictResolver, type Logger } from '@hydranium/protocol';
|
|
9
|
+
import { type BasedOn, type ConflictResolver, type Logger } from '@hydranium/protocol';
|
|
10
10
|
/**
|
|
11
11
|
* The I/O and policy a {@link reconcileSourceModelWrite} call needs. Deliberately
|
|
12
12
|
* a plain record rather than a base class: the states that use it differ in what
|
|
@@ -16,15 +16,16 @@ import { type ConflictResolver, type Logger } from '@hydranium/protocol';
|
|
|
16
16
|
*/
|
|
17
17
|
export interface SourceModelWriteHooks<TModel> {
|
|
18
18
|
/**
|
|
19
|
-
* Write `model` and
|
|
20
|
-
* on the first attempt (opting into the `ConflictError` gate) and
|
|
21
|
-
* on the merged / forced retries, where the
|
|
19
|
+
* Write `model` and record the resulting state. Called with the caller's own
|
|
20
|
+
* `basedOn` on the first attempt (opting into the `ConflictError` gate) and
|
|
21
|
+
* with `'anything'` on the merged / forced retries, where the reconcile has
|
|
22
|
+
* already decided to win and the point is to land the write.
|
|
22
23
|
*/
|
|
23
|
-
persist(model: TModel,
|
|
24
|
+
persist(model: TModel, basedOn: BasedOn): Promise<void>;
|
|
24
25
|
/** Current server-side projection, or `undefined` when unavailable. */
|
|
25
26
|
refetch(): Promise<TModel | undefined>;
|
|
26
|
-
/**
|
|
27
|
-
readonly
|
|
27
|
+
/** The last in-sync projection the user's intent is measured against. */
|
|
28
|
+
readonly base: TModel | undefined;
|
|
28
29
|
readonly conflictResolver: ConflictResolver;
|
|
29
30
|
readonly logger: Logger;
|
|
30
31
|
/** Invoked when the edit is dropped, so the caller can resync its own state. */
|
|
@@ -39,5 +40,5 @@ export interface SourceModelWriteHooks<TModel> {
|
|
|
39
40
|
* re-thrown untouched — this function only knows how to handle the specific
|
|
40
41
|
* failure of "the based-on version was superseded".
|
|
41
42
|
*/
|
|
42
|
-
export declare function reconcileSourceModelWrite<TModel extends object>(model: TModel,
|
|
43
|
+
export declare function reconcileSourceModelWrite<TModel extends object>(model: TModel, basedOn: BasedOn, hooks: SourceModelWriteHooks<TModel>): Promise<void>;
|
|
43
44
|
//# sourceMappingURL=reconcile-source-model-write.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reconcile-source-model-write.d.ts","sourceRoot":"","sources":["../../src/state/reconcile-source-model-write.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAElF,OAAO,EAAE,KAAK,gBAAgB,EAAE,KAAK,MAAM,EAAmB,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"reconcile-source-model-write.d.ts","sourceRoot":"","sources":["../../src/state/reconcile-source-model-write.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAElF,OAAO,EAAE,KAAK,OAAO,EAAE,KAAK,gBAAgB,EAAE,KAAK,MAAM,EAAmB,MAAM,qBAAqB,CAAC;AAExG;;;;;;GAMG;AACH,MAAM,WAAW,qBAAqB,CAAC,MAAM;IAC1C;;;;;OAKG;IACH,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxD,uEAAuE;IACvE,OAAO,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IACvC,yEAAyE;IACzE,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,QAAQ,CAAC,gBAAgB,EAAE,gBAAgB,CAAC;IAC5C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,gFAAgF;IAChF,iBAAiB,IAAI,IAAI,CAAC;CAC5B;AAED;;;;;;;;GAQG;AACH,wBAAsB,yBAAyB,CAAC,MAAM,SAAS,MAAM,EAClE,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,qBAAqB,CAAC,MAAM,CAAC,GACpC,OAAO,CAAC,IAAI,CAAC,CA8Bf"}
|
|
@@ -16,30 +16,30 @@ import { isConflictError } from '@hydranium/protocol';
|
|
|
16
16
|
* re-thrown untouched — this function only knows how to handle the specific
|
|
17
17
|
* failure of "the based-on version was superseded".
|
|
18
18
|
*/
|
|
19
|
-
export async function reconcileSourceModelWrite(model,
|
|
19
|
+
export async function reconcileSourceModelWrite(model, basedOn, hooks) {
|
|
20
20
|
try {
|
|
21
|
-
await hooks.persist(model,
|
|
21
|
+
await hooks.persist(model, basedOn);
|
|
22
22
|
}
|
|
23
23
|
catch (err) {
|
|
24
24
|
if (!isConflictError(err)) {
|
|
25
25
|
throw err;
|
|
26
26
|
}
|
|
27
|
-
const outcome = await hooks.conflictResolver.resolve(hooks.
|
|
27
|
+
const outcome = await hooks.conflictResolver.resolve(hooks.base ?? model, model, () => hooks.refetch());
|
|
28
28
|
switch (outcome.status) {
|
|
29
29
|
case 'merged':
|
|
30
|
-
await hooks.persist(outcome.merged);
|
|
30
|
+
await hooks.persist(outcome.merged, 'anything');
|
|
31
31
|
return;
|
|
32
32
|
case 'no-op':
|
|
33
|
-
hooks.logger.debug(`updateSourceModel no-op (v${err.
|
|
33
|
+
hooks.logger.debug(`updateSourceModel no-op (v${err.expectedVersion} → v${err.actualVersion}); already in sync`);
|
|
34
34
|
return;
|
|
35
35
|
case 'conflict':
|
|
36
|
-
hooks.logger.warn(`updateSourceModel conflict (v${err.
|
|
36
|
+
hooks.logger.warn(`updateSourceModel conflict (v${err.expectedVersion} → v${err.actualVersion}); dropping the diagram edit — ` +
|
|
37
37
|
'a foreign writer changed the same field');
|
|
38
38
|
hooks.onConflictDropped();
|
|
39
39
|
return;
|
|
40
40
|
case 'unavailable':
|
|
41
|
-
hooks.logger.warn(`updateSourceModel refetch unavailable (v${err.
|
|
42
|
-
await hooks.persist(model);
|
|
41
|
+
hooks.logger.warn(`updateSourceModel refetch unavailable (v${err.expectedVersion} → v${err.actualVersion}); forcing without version`);
|
|
42
|
+
await hooks.persist(model, 'anything');
|
|
43
43
|
return;
|
|
44
44
|
}
|
|
45
45
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reconcile-source-model-write.js","sourceRoot":"","sources":["../../src/state/reconcile-source-model-write.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAElF,OAAO,
|
|
1
|
+
{"version":3,"file":"reconcile-source-model-write.js","sourceRoot":"","sources":["../../src/state/reconcile-source-model-write.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAElF,OAAO,EAAoD,eAAe,EAAE,MAAM,qBAAqB,CAAC;AA2BxG;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAC5C,KAAa,EACb,OAAgB,EAChB,KAAoC;IAEpC,IAAI,CAAC;QACF,MAAM,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IACvC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACZ,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,EAAE,CAAC;YACzB,MAAM,GAAG,CAAC;QACb,CAAC;QACD,MAAM,OAAO,GAAG,MAAM,KAAK,CAAC,gBAAgB,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,IAAI,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;QACxG,QAAQ,OAAO,CAAC,MAAM,EAAE,CAAC;YACtB,KAAK,QAAQ;gBACV,MAAM,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;gBAChD,OAAO;YACV,KAAK,OAAO;gBACT,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,6BAA6B,GAAG,CAAC,eAAe,OAAO,GAAG,CAAC,aAAa,oBAAoB,CAAC,CAAC;gBACjH,OAAO;YACV,KAAK,UAAU;gBACZ,KAAK,CAAC,MAAM,CAAC,IAAI,CACd,gCAAgC,GAAG,CAAC,eAAe,OAAO,GAAG,CAAC,aAAa,iCAAiC;oBACzG,yCAAyC,CAC9C,CAAC;gBACF,KAAK,CAAC,iBAAiB,EAAE,CAAC;gBAC1B,OAAO;YACV,KAAK,aAAa;gBACf,KAAK,CAAC,MAAM,CAAC,IAAI,CACd,2CAA2C,GAAG,CAAC,eAAe,OAAO,GAAG,CAAC,aAAa,4BAA4B,CACpH,CAAC;gBACF,MAAM,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC;gBACvC,OAAO;QACb,CAAC;IACJ,CAAC;AACJ,CAAC"}
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
********************************************************************************/
|
|
9
9
|
import { type JsonModelState } from '@eclipse-glsp/server';
|
|
10
10
|
import { type AstNode } from '@hydranium/langium';
|
|
11
|
-
import { type
|
|
11
|
+
import { type ClientSession } from '@hydranium/core';
|
|
12
|
+
import { type BasedOn, type TransferElement } from '@hydranium/protocol';
|
|
12
13
|
import { AbstractHydraniumGlspState } from './abstract-hydranium-glsp-state.js';
|
|
13
14
|
/**
|
|
14
15
|
* Source model spanning more than one document: the primary document's
|
|
@@ -39,43 +40,30 @@ export interface MultiDocumentSourceModel<TPrimary extends TransferElement = Tra
|
|
|
39
40
|
* defines them over the primary plus the secondary write set registered through
|
|
40
41
|
* {@link AbstractHydraniumGlspState.trackSecondaryDocument}.
|
|
41
42
|
*
|
|
42
|
-
* **
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* back, and the document store commits per URI. An adopter that cannot accept the
|
|
47
|
-
* window should not span documents in one operation.
|
|
43
|
+
* **The write set is written all or none.** {@link persist} hands every
|
|
44
|
+
* changed document to one `ClientSession.updateAll` on the diagram's session,
|
|
45
|
+
* so a conflict on any document leaves every document of the set as it was,
|
|
46
|
+
* and write order carries no meaning.
|
|
48
47
|
*
|
|
49
|
-
* **
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* secondaries-then-primary, which is correct for the common shape where the
|
|
56
|
-
* primary is the DIAGRAM and the semantics it references are secondaries: the
|
|
57
|
-
* user opened the diagram, so it is also the document the conflict gate should
|
|
58
|
-
* guard. **An adopter whose primary is the referenced document — a semantic file
|
|
59
|
-
* as the diagram source, with layout in a secondary — has the ordering backwards
|
|
60
|
-
* and must override {@link persist} to write the primary first.** Overriding it
|
|
61
|
-
* is the supported route; the base order is a default for the common case, not an
|
|
62
|
-
* invariant of the class.
|
|
48
|
+
* **Every document of the set is gated.** The primary on the caller's
|
|
49
|
+
* `basedOn`, each secondary on {@link secondaryBasedOn}, by default the version
|
|
50
|
+
* it had when the source root was last read. A conflict on any of them is
|
|
51
|
+
* reconciled against the whole write set. A write based on `'anything'` — a
|
|
52
|
+
* merged retry, an undo or redo replaying a patch — forces every document of
|
|
53
|
+
* the set.
|
|
63
54
|
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* merged one — the atomicity problem again, one layer up. The captured versions
|
|
69
|
-
* ARE available via {@link AbstractHydraniumGlspState.capturedVersionOf}, so an
|
|
70
|
-
* adopter that wants a coarser check can compare before writing.
|
|
55
|
+
* A secondary is written only while the diagram's session has it open. The
|
|
56
|
+
* storage opens it as it joins the write set, and {@link openForWrite} opens it
|
|
57
|
+
* again before each write; an adopter whose write set can name a document that
|
|
58
|
+
* does not exist yet overrides {@link openForWrite} to create it.
|
|
71
59
|
*/
|
|
72
60
|
export declare class ReconcilingMultiDocumentGlspState<TRoot extends AstNode, TPrimary extends TransferElement = TransferElement> extends AbstractHydraniumGlspState<TRoot, MultiDocumentSourceModel<TPrimary>> implements JsonModelState<MultiDocumentSourceModel<TPrimary>> {
|
|
73
61
|
/**
|
|
74
62
|
* Last in-sync projection across the whole write set, captured on every
|
|
75
|
-
* {@link setSourceRoot}. Same role as the single-document
|
|
63
|
+
* {@link setSourceRoot}. Same role as the single-document base: the state a
|
|
76
64
|
* forward-write conflict reconciles the user's intent against.
|
|
77
65
|
*/
|
|
78
|
-
protected
|
|
66
|
+
protected base?: MultiDocumentSourceModel<TPrimary>;
|
|
79
67
|
/**
|
|
80
68
|
* Projection of the primary plus every registered secondary, in the framework
|
|
81
69
|
* encoder's `'grammar'` mode (authored state only — cross-references as
|
|
@@ -93,14 +81,14 @@ export declare class ReconcilingMultiDocumentGlspState<TRoot extends AstNode, TP
|
|
|
93
81
|
* Register the secondary documents that belong to the primary at `uri`, via
|
|
94
82
|
* {@link AbstractHydraniumGlspState.trackSecondaryDocument}. Called on every
|
|
95
83
|
* {@link setSourceRoot}, after the primary is captured (so `sourceUri` is
|
|
96
|
-
* current) and BEFORE the
|
|
84
|
+
* current) and BEFORE the base is taken (so the base includes them).
|
|
97
85
|
* Default: no secondaries.
|
|
98
86
|
*
|
|
99
87
|
* This hook exists because that ordering is a trap an adopter would otherwise
|
|
100
88
|
* hit silently. Registering from an overridden `setSourceRoot` *after*
|
|
101
|
-
* `super.setSourceRoot(...)` runs too late — the
|
|
89
|
+
* `super.setSourceRoot(...)` runs too late — the base has already been
|
|
102
90
|
* captured without the secondaries, so the first conflict reconcile measures
|
|
103
|
-
* the user's intent against a
|
|
91
|
+
* the user's intent against a base missing half the write set and the
|
|
104
92
|
* secondary edits look like foreign changes. Registering *before* the super
|
|
105
93
|
* call is too early for a URI derived from the new primary. Overriding this
|
|
106
94
|
* instead removes the choice.
|
|
@@ -108,49 +96,61 @@ export declare class ReconcilingMultiDocumentGlspState<TRoot extends AstNode, TP
|
|
|
108
96
|
protected trackWriteSet(_uri: string): void;
|
|
109
97
|
/**
|
|
110
98
|
* Persist the whole write set, then capture the resulting primary root. On a
|
|
111
|
-
* `ConflictError` from the
|
|
112
|
-
* as the single-document state does — the orchestration is
|
|
99
|
+
* `ConflictError` from any document of the set, reconcile via the injected
|
|
100
|
+
* policy exactly as the single-document state does — the orchestration is
|
|
101
|
+
* shared.
|
|
113
102
|
*/
|
|
114
|
-
updateSourceModel(model: MultiDocumentSourceModel<TPrimary>,
|
|
103
|
+
updateSourceModel(model: MultiDocumentSourceModel<TPrimary>, basedOn?: BasedOn): Promise<void>;
|
|
115
104
|
/**
|
|
116
|
-
* Write hook —
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
105
|
+
* Write hook — every document of `model` that changed, opened first through
|
|
106
|
+
* {@link openForWrite}, in one `updateAll` on the diagram's session: the
|
|
107
|
+
* primary gated on `basedOn`, each secondary on {@link secondaryBasedOn}, or
|
|
108
|
+
* every document on `'anything'` when `basedOn` is `'anything'`. Resolves to
|
|
109
|
+
* the primary's root, the one already captured when the primary did not
|
|
110
|
+
* change. A write that bypasses `updateAll` gives up the all-or-none
|
|
111
|
+
* guarantee the class describes. Throws without a session
|
|
112
|
+
* ({@link requireModelSession}).
|
|
120
113
|
*/
|
|
121
|
-
protected persist(model: MultiDocumentSourceModel<TPrimary>,
|
|
114
|
+
protected persist(model: MultiDocumentSourceModel<TPrimary>, basedOn: BasedOn): Promise<{
|
|
122
115
|
root: TRoot;
|
|
123
116
|
}>;
|
|
124
117
|
/**
|
|
125
|
-
* Whether `candidate` differs from the
|
|
118
|
+
* Whether `candidate` differs from the base projection of the same
|
|
126
119
|
* document, and therefore needs writing.
|
|
127
120
|
*
|
|
128
121
|
* **Skipping unchanged documents is correctness, not an optimisation.** A
|
|
129
|
-
* write
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
122
|
+
* write re-serializes from the AST, so writing a document that did not
|
|
123
|
+
* change still rewrites its text in the serializer's own layout. Persisting
|
|
124
|
+
* the whole write set unconditionally therefore means a pure layout drag
|
|
125
|
+
* reflows the semantic file, a change the user never asked for and would
|
|
126
|
+
* struggle to attribute. Comments survive that rewrite, but the
|
|
127
|
+
* hand-formatting around them does not.
|
|
135
128
|
*
|
|
136
129
|
* Compared by serialised form. Both sides come from the same encoder walking
|
|
137
130
|
* the same shape, so key order is stable and a string compare is sound here;
|
|
138
131
|
* it is also cheap enough to run per document per write.
|
|
139
132
|
*/
|
|
140
|
-
protected hasChanged(
|
|
141
|
-
/**
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
133
|
+
protected hasChanged(base: object | undefined, candidate: object): boolean;
|
|
134
|
+
/**
|
|
135
|
+
* Make sure the diagram's session has `uri` open before the write set is
|
|
136
|
+
* written. Default: open it, a no-op for a document the session already
|
|
137
|
+
* has open. Override to create a document the write set names before it
|
|
138
|
+
* exists, through `session.create`: `updateAll` opens nothing.
|
|
139
|
+
*/
|
|
140
|
+
protected openForWrite(session: ClientSession<AstNode>, uri: string): Promise<void>;
|
|
145
141
|
/**
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
142
|
+
* What a secondary write declares it was based on. Default: the version the
|
|
143
|
+
* secondary had when the source root was last read
|
|
144
|
+
* ({@link AbstractHydraniumGlspState.snapshotVersionOf}), so a foreign edit
|
|
145
|
+
* to it since is reported as a conflict and reconciled rather than
|
|
146
|
+
* overwritten; `'anything'` for a secondary with no recorded version.
|
|
147
|
+
* Return `'anything'` to force secondary writes.
|
|
149
148
|
*/
|
|
150
|
-
protected
|
|
149
|
+
protected secondaryBasedOn(uri: string): BasedOn;
|
|
151
150
|
/**
|
|
152
|
-
* Refetch hook — the current
|
|
153
|
-
*
|
|
151
|
+
* Refetch hook — the current projection across the write set, each document
|
|
152
|
+
* read through {@link AbstractHydraniumGlspState.readCurrentRoot}: theirs,
|
|
153
|
+
* which the conflict resolver replays the user's intent onto.
|
|
154
154
|
* Returns `undefined` when the PRIMARY cannot be read, since a reconcile
|
|
155
155
|
* without it has nothing to merge into; an unreadable secondary is omitted the
|
|
156
156
|
* same way {@link sourceModel} omits one.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reconciling-multi-document-glsp-state.d.ts","sourceRoot":"","sources":["../../src/state/reconciling-multi-document-glsp-state.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAElF,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,KAAK,eAAe,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"reconciling-multi-document-glsp-state.d.ts","sourceRoot":"","sources":["../../src/state/reconciling-multi-document-glsp-state.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAElF,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,KAAK,aAAa,EAA+B,MAAM,iBAAiB,CAAC;AAClF,OAAO,EAAE,KAAK,OAAO,EAAE,KAAK,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACzE,OAAO,EAAE,0BAA0B,EAAE,MAAM,oCAAoC,CAAC;AAGhF;;;;;;;;;GASG;AACH,MAAM,WAAW,wBAAwB,CAAC,QAAQ,SAAS,eAAe,GAAG,eAAe;IACzF,gFAAgF;IAChF,OAAO,EAAE,QAAQ,CAAC;IAClB,0EAA0E;IAC1E,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;CAC/C;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,qBACa,iCAAiC,CAAC,KAAK,SAAS,OAAO,EAAE,QAAQ,SAAS,eAAe,GAAG,eAAe,CACrH,SAAQ,0BAA0B,CAAC,KAAK,EAAE,wBAAwB,CAAC,QAAQ,CAAC,CAC5E,YAAW,cAAc,CAAC,wBAAwB,CAAC,QAAQ,CAAC,CAAC;IAE7D;;;;OAIG;IACH,SAAS,CAAC,IAAI,CAAC,EAAE,wBAAwB,CAAC,QAAQ,CAAC,CAAC;IAEpD;;;;;;;;;;OAUG;IACH,IAAI,WAAW,IAAI,wBAAwB,CAAC,QAAQ,CAAC,CASpD;IAEQ,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,GAAG,IAAI;IAMtD;;;;;;;;;;;;;;;OAeG;IACH,SAAS,CAAC,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAI3C;;;;;OAKG;IACG,iBAAiB,CAAC,KAAK,EAAE,wBAAwB,CAAC,QAAQ,CAAC,EAAE,OAAO,GAAE,OAAsB,GAAG,OAAO,CAAC,IAAI,CAAC;IAclH;;;;;;;;;OASG;cACa,OAAO,CAAC,KAAK,EAAE,wBAAwB,CAAC,QAAQ,CAAC,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,KAAK,CAAA;KAAE,CAAC;IAqB9G;;;;;;;;;;;;;;;OAeG;IACH,SAAS,CAAC,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO;IAI1E;;;;;OAKG;IACH,SAAS,CAAC,YAAY,CAAC,OAAO,EAAE,aAAa,CAAC,OAAO,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAInF;;;;;;;OAOG;IACH,SAAS,CAAC,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAIhD;;;;;;;OAOG;cACa,OAAO,IAAI,OAAO,CAAC,wBAAwB,CAAC,QAAQ,CAAC,GAAG,SAAS,CAAC;IAelF,wFAAwF;IACxF,SAAS,CAAC,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS;IAKnE,oFAAoF;IACpF,SAAS,CAAC,WAAW,CAAC,CAAC,SAAS,eAAe,GAAG,eAAe,EAAE,IAAI,EAAE,OAAO,GAAG,CAAC;CAGtF"}
|
|
@@ -28,43 +28,30 @@ import { reconcileSourceModelWrite } from './reconcile-source-model-write.js';
|
|
|
28
28
|
* defines them over the primary plus the secondary write set registered through
|
|
29
29
|
* {@link AbstractHydraniumGlspState.trackSecondaryDocument}.
|
|
30
30
|
*
|
|
31
|
-
* **
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* back, and the document store commits per URI. An adopter that cannot accept the
|
|
36
|
-
* window should not span documents in one operation.
|
|
31
|
+
* **The write set is written all or none.** {@link persist} hands every
|
|
32
|
+
* changed document to one `ClientSession.updateAll` on the diagram's session,
|
|
33
|
+
* so a conflict on any document leaves every document of the set as it was,
|
|
34
|
+
* and write order carries no meaning.
|
|
37
35
|
*
|
|
38
|
-
* **
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* secondaries-then-primary, which is correct for the common shape where the
|
|
45
|
-
* primary is the DIAGRAM and the semantics it references are secondaries: the
|
|
46
|
-
* user opened the diagram, so it is also the document the conflict gate should
|
|
47
|
-
* guard. **An adopter whose primary is the referenced document — a semantic file
|
|
48
|
-
* as the diagram source, with layout in a secondary — has the ordering backwards
|
|
49
|
-
* and must override {@link persist} to write the primary first.** Overriding it
|
|
50
|
-
* is the supported route; the base order is a default for the common case, not an
|
|
51
|
-
* invariant of the class.
|
|
36
|
+
* **Every document of the set is gated.** The primary on the caller's
|
|
37
|
+
* `basedOn`, each secondary on {@link secondaryBasedOn}, by default the version
|
|
38
|
+
* it had when the source root was last read. A conflict on any of them is
|
|
39
|
+
* reconciled against the whole write set. A write based on `'anything'` — a
|
|
40
|
+
* merged retry, an undo or redo replaying a patch — forces every document of
|
|
41
|
+
* the set.
|
|
52
42
|
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* merged one — the atomicity problem again, one layer up. The captured versions
|
|
58
|
-
* ARE available via {@link AbstractHydraniumGlspState.capturedVersionOf}, so an
|
|
59
|
-
* adopter that wants a coarser check can compare before writing.
|
|
43
|
+
* A secondary is written only while the diagram's session has it open. The
|
|
44
|
+
* storage opens it as it joins the write set, and {@link openForWrite} opens it
|
|
45
|
+
* again before each write; an adopter whose write set can name a document that
|
|
46
|
+
* does not exist yet overrides {@link openForWrite} to create it.
|
|
60
47
|
*/
|
|
61
48
|
let ReconcilingMultiDocumentGlspState = class ReconcilingMultiDocumentGlspState extends AbstractHydraniumGlspState {
|
|
62
49
|
/**
|
|
63
50
|
* Last in-sync projection across the whole write set, captured on every
|
|
64
|
-
* {@link setSourceRoot}. Same role as the single-document
|
|
51
|
+
* {@link setSourceRoot}. Same role as the single-document base: the state a
|
|
65
52
|
* forward-write conflict reconciles the user's intent against.
|
|
66
53
|
*/
|
|
67
|
-
|
|
54
|
+
base;
|
|
68
55
|
/**
|
|
69
56
|
* Projection of the primary plus every registered secondary, in the framework
|
|
70
57
|
* encoder's `'grammar'` mode (authored state only — cross-references as
|
|
@@ -89,20 +76,20 @@ let ReconcilingMultiDocumentGlspState = class ReconcilingMultiDocumentGlspState
|
|
|
89
76
|
setSourceRoot(uri, root) {
|
|
90
77
|
super.setSourceRoot(uri, root);
|
|
91
78
|
this.trackWriteSet(uri);
|
|
92
|
-
this.
|
|
79
|
+
this.base = this.sourceModel;
|
|
93
80
|
}
|
|
94
81
|
/**
|
|
95
82
|
* Register the secondary documents that belong to the primary at `uri`, via
|
|
96
83
|
* {@link AbstractHydraniumGlspState.trackSecondaryDocument}. Called on every
|
|
97
84
|
* {@link setSourceRoot}, after the primary is captured (so `sourceUri` is
|
|
98
|
-
* current) and BEFORE the
|
|
85
|
+
* current) and BEFORE the base is taken (so the base includes them).
|
|
99
86
|
* Default: no secondaries.
|
|
100
87
|
*
|
|
101
88
|
* This hook exists because that ordering is a trap an adopter would otherwise
|
|
102
89
|
* hit silently. Registering from an overridden `setSourceRoot` *after*
|
|
103
|
-
* `super.setSourceRoot(...)` runs too late — the
|
|
90
|
+
* `super.setSourceRoot(...)` runs too late — the base has already been
|
|
104
91
|
* captured without the secondaries, so the first conflict reconcile measures
|
|
105
|
-
* the user's intent against a
|
|
92
|
+
* the user's intent against a base missing half the write set and the
|
|
106
93
|
* secondary edits look like foreign changes. Registering *before* the super
|
|
107
94
|
* call is too early for a URI derived from the new primary. Overriding this
|
|
108
95
|
* instead removes the choice.
|
|
@@ -112,98 +99,113 @@ let ReconcilingMultiDocumentGlspState = class ReconcilingMultiDocumentGlspState
|
|
|
112
99
|
}
|
|
113
100
|
/**
|
|
114
101
|
* Persist the whole write set, then capture the resulting primary root. On a
|
|
115
|
-
* `ConflictError` from the
|
|
116
|
-
* as the single-document state does — the orchestration is
|
|
102
|
+
* `ConflictError` from any document of the set, reconcile via the injected
|
|
103
|
+
* policy exactly as the single-document state does — the orchestration is
|
|
104
|
+
* shared.
|
|
117
105
|
*/
|
|
118
|
-
async updateSourceModel(model,
|
|
119
|
-
return reconcileSourceModelWrite(model,
|
|
120
|
-
persist: async (candidate,
|
|
121
|
-
const { root } = await this.persist(candidate,
|
|
106
|
+
async updateSourceModel(model, basedOn = this.basedOn) {
|
|
107
|
+
return reconcileSourceModelWrite(model, basedOn, {
|
|
108
|
+
persist: async (candidate, candidateBasedOn) => {
|
|
109
|
+
const { root } = await this.persist(candidate, candidateBasedOn);
|
|
122
110
|
this.setSourceRoot(this._sourceUri, root);
|
|
123
111
|
},
|
|
124
112
|
refetch: () => this.refetch(),
|
|
125
|
-
|
|
113
|
+
base: this.base,
|
|
126
114
|
conflictResolver: this.conflictResolver,
|
|
127
115
|
logger: this.logger,
|
|
128
116
|
onConflictDropped: () => this.refreshSourceRoot()
|
|
129
117
|
});
|
|
130
118
|
}
|
|
131
119
|
/**
|
|
132
|
-
* Write hook —
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
120
|
+
* Write hook — every document of `model` that changed, opened first through
|
|
121
|
+
* {@link openForWrite}, in one `updateAll` on the diagram's session: the
|
|
122
|
+
* primary gated on `basedOn`, each secondary on {@link secondaryBasedOn}, or
|
|
123
|
+
* every document on `'anything'` when `basedOn` is `'anything'`. Resolves to
|
|
124
|
+
* the primary's root, the one already captured when the primary did not
|
|
125
|
+
* change. A write that bypasses `updateAll` gives up the all-or-none
|
|
126
|
+
* guarantee the class describes. Throws without a session
|
|
127
|
+
* ({@link requireModelSession}).
|
|
136
128
|
*/
|
|
137
|
-
async persist(model,
|
|
129
|
+
async persist(model, basedOn) {
|
|
130
|
+
const primaryChanged = this.hasChanged(this.base?.primary, model.primary);
|
|
131
|
+
const updates = primaryChanged
|
|
132
|
+
? [{ uri: this._sourceUri, model: model.primary, basedOn }]
|
|
133
|
+
: [];
|
|
138
134
|
for (const [uri, secondary] of Object.entries(model.secondaries)) {
|
|
139
|
-
if (this.hasChanged(this.
|
|
140
|
-
|
|
135
|
+
if (this.hasChanged(this.base?.secondaries[uri], secondary)) {
|
|
136
|
+
updates.push({ uri, model: secondary, basedOn: basedOn === 'anything' ? 'anything' : this.secondaryBasedOn(uri) });
|
|
141
137
|
}
|
|
142
138
|
}
|
|
143
|
-
if (
|
|
144
|
-
// Nothing to write, so nothing to gate either — return the root already
|
|
145
|
-
// captured rather than round-tripping the document for no reason.
|
|
139
|
+
if (updates.length === 0) {
|
|
146
140
|
return { root: this._sourceRoot };
|
|
147
141
|
}
|
|
148
|
-
|
|
142
|
+
const session = this.requireModelSession();
|
|
143
|
+
for (const update of updates) {
|
|
144
|
+
await this.openForWrite(session, update.uri);
|
|
145
|
+
}
|
|
146
|
+
const documents = await session.updateAll({ updates });
|
|
147
|
+
return { root: primaryChanged ? documents[0].root : this._sourceRoot };
|
|
149
148
|
}
|
|
150
149
|
/**
|
|
151
|
-
* Whether `candidate` differs from the
|
|
150
|
+
* Whether `candidate` differs from the base projection of the same
|
|
152
151
|
* document, and therefore needs writing.
|
|
153
152
|
*
|
|
154
153
|
* **Skipping unchanged documents is correctness, not an optimisation.** A
|
|
155
|
-
* write
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
154
|
+
* write re-serializes from the AST, so writing a document that did not
|
|
155
|
+
* change still rewrites its text in the serializer's own layout. Persisting
|
|
156
|
+
* the whole write set unconditionally therefore means a pure layout drag
|
|
157
|
+
* reflows the semantic file, a change the user never asked for and would
|
|
158
|
+
* struggle to attribute. Comments survive that rewrite, but the
|
|
159
|
+
* hand-formatting around them does not.
|
|
161
160
|
*
|
|
162
161
|
* Compared by serialised form. Both sides come from the same encoder walking
|
|
163
162
|
* the same shape, so key order is stable and a string compare is sound here;
|
|
164
163
|
* it is also cheap enough to run per document per write.
|
|
165
164
|
*/
|
|
166
|
-
hasChanged(
|
|
167
|
-
return
|
|
165
|
+
hasChanged(base, candidate) {
|
|
166
|
+
return base === undefined || JSON.stringify(base) !== JSON.stringify(candidate);
|
|
168
167
|
}
|
|
169
|
-
/**
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
return document;
|
|
168
|
+
/**
|
|
169
|
+
* Make sure the diagram's session has `uri` open before the write set is
|
|
170
|
+
* written. Default: open it, a no-op for a document the session already
|
|
171
|
+
* has open. Override to create a document the write set names before it
|
|
172
|
+
* exists, through `session.create`: `updateAll` opens nothing.
|
|
173
|
+
*/
|
|
174
|
+
openForWrite(session, uri) {
|
|
175
|
+
return session.open(uri);
|
|
178
176
|
}
|
|
179
177
|
/**
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
178
|
+
* What a secondary write declares it was based on. Default: the version the
|
|
179
|
+
* secondary had when the source root was last read
|
|
180
|
+
* ({@link AbstractHydraniumGlspState.snapshotVersionOf}), so a foreign edit
|
|
181
|
+
* to it since is reported as a conflict and reconciled rather than
|
|
182
|
+
* overwritten; `'anything'` for a secondary with no recorded version.
|
|
183
|
+
* Return `'anything'` to force secondary writes.
|
|
183
184
|
*/
|
|
184
|
-
|
|
185
|
-
|
|
185
|
+
secondaryBasedOn(uri) {
|
|
186
|
+
return this.snapshotVersionOf(uri) ?? 'anything';
|
|
186
187
|
}
|
|
187
188
|
/**
|
|
188
|
-
* Refetch hook — the current
|
|
189
|
-
*
|
|
189
|
+
* Refetch hook — the current projection across the write set, each document
|
|
190
|
+
* read through {@link AbstractHydraniumGlspState.readCurrentRoot}: theirs,
|
|
191
|
+
* which the conflict resolver replays the user's intent onto.
|
|
190
192
|
* Returns `undefined` when the PRIMARY cannot be read, since a reconcile
|
|
191
193
|
* without it has nothing to merge into; an unreadable secondary is omitted the
|
|
192
194
|
* same way {@link sourceModel} omits one.
|
|
193
195
|
*/
|
|
194
196
|
async refetch() {
|
|
195
|
-
const
|
|
196
|
-
if (!
|
|
197
|
+
const theirs = await this.readCurrentRoot(this._sourceUri);
|
|
198
|
+
if (!theirs) {
|
|
197
199
|
return undefined;
|
|
198
200
|
}
|
|
199
201
|
const secondaries = {};
|
|
200
202
|
for (const uri of this.secondaryUris) {
|
|
201
|
-
const
|
|
202
|
-
if (
|
|
203
|
-
secondaries[uri] = this.projectRoot(
|
|
203
|
+
const root = await this.readCurrentRoot(uri);
|
|
204
|
+
if (root) {
|
|
205
|
+
secondaries[uri] = this.projectRoot(root);
|
|
204
206
|
}
|
|
205
207
|
}
|
|
206
|
-
return { primary: this.projectRoot(
|
|
208
|
+
return { primary: this.projectRoot(theirs), secondaries };
|
|
207
209
|
}
|
|
208
210
|
/** Project a currently-loaded document's root, or `undefined` when it is not loaded. */
|
|
209
211
|
projectDocument(uri) {
|