@hydranium/glsp-server 1.0.0-next.10
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/LICENSE +21 -0
- package/README.md +97 -0
- package/lib/browser/index.d.ts +10 -0
- package/lib/browser/index.d.ts.map +1 -0
- package/lib/browser/index.js +15 -0
- package/lib/browser/index.js.map +1 -0
- package/lib/browser/start-glsp-server-in-worker.d.ts +121 -0
- package/lib/browser/start-glsp-server-in-worker.d.ts.map +1 -0
- package/lib/browser/start-glsp-server-in-worker.js +102 -0
- package/lib/browser/start-glsp-server-in-worker.js.map +1 -0
- package/lib/command/hydranium-glsp-recording-command.d.ts +132 -0
- package/lib/command/hydranium-glsp-recording-command.d.ts.map +1 -0
- package/lib/command/hydranium-glsp-recording-command.js +159 -0
- package/lib/command/hydranium-glsp-recording-command.js.map +1 -0
- package/lib/command/index.d.ts +10 -0
- package/lib/command/index.d.ts.map +1 -0
- package/lib/command/index.js +10 -0
- package/lib/command/index.js.map +1 -0
- package/lib/computed-bounds/hydranium-glsp-computed-bounds-action-handler.d.ts +61 -0
- package/lib/computed-bounds/hydranium-glsp-computed-bounds-action-handler.d.ts.map +1 -0
- package/lib/computed-bounds/hydranium-glsp-computed-bounds-action-handler.js +93 -0
- package/lib/computed-bounds/hydranium-glsp-computed-bounds-action-handler.js.map +1 -0
- package/lib/computed-bounds/index.d.ts +10 -0
- package/lib/computed-bounds/index.d.ts.map +1 -0
- package/lib/computed-bounds/index.js +10 -0
- package/lib/computed-bounds/index.js.map +1 -0
- package/lib/dispatcher/index.d.ts +10 -0
- package/lib/dispatcher/index.d.ts.map +1 -0
- package/lib/dispatcher/index.js +10 -0
- package/lib/dispatcher/index.js.map +1 -0
- package/lib/dispatcher/server-action-dispatcher.d.ts +52 -0
- package/lib/dispatcher/server-action-dispatcher.d.ts.map +1 -0
- package/lib/dispatcher/server-action-dispatcher.js +79 -0
- package/lib/dispatcher/server-action-dispatcher.js.map +1 -0
- package/lib/index.d.ts +19 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +20 -0
- package/lib/index.js.map +1 -0
- package/lib/launcher/abstract-hydranium-glsp-diagram-module.d.ts +75 -0
- package/lib/launcher/abstract-hydranium-glsp-diagram-module.d.ts.map +1 -0
- package/lib/launcher/abstract-hydranium-glsp-diagram-module.js +100 -0
- package/lib/launcher/abstract-hydranium-glsp-diagram-module.js.map +1 -0
- package/lib/launcher/glsp-app-module.d.ts +87 -0
- package/lib/launcher/glsp-app-module.d.ts.map +1 -0
- package/lib/launcher/glsp-app-module.js +71 -0
- package/lib/launcher/glsp-app-module.js.map +1 -0
- package/lib/launcher/glsp-framework-overrides.d.ts +32 -0
- package/lib/launcher/glsp-framework-overrides.d.ts.map +1 -0
- package/lib/launcher/glsp-framework-overrides.js +49 -0
- package/lib/launcher/glsp-framework-overrides.js.map +1 -0
- package/lib/launcher/glsp-server-overrides.d.ts +39 -0
- package/lib/launcher/glsp-server-overrides.d.ts.map +1 -0
- package/lib/launcher/glsp-server-overrides.js +45 -0
- package/lib/launcher/glsp-server-overrides.js.map +1 -0
- package/lib/launcher/hydranium-glsp-server.d.ts +62 -0
- package/lib/launcher/hydranium-glsp-server.d.ts.map +1 -0
- package/lib/launcher/hydranium-glsp-server.js +101 -0
- package/lib/launcher/hydranium-glsp-server.js.map +1 -0
- package/lib/launcher/index.d.ts +12 -0
- package/lib/launcher/index.d.ts.map +1 -0
- package/lib/launcher/index.js +22 -0
- package/lib/launcher/index.js.map +1 -0
- package/lib/logging/glsp-client-logger.d.ts +110 -0
- package/lib/logging/glsp-client-logger.d.ts.map +1 -0
- package/lib/logging/glsp-client-logger.js +151 -0
- package/lib/logging/glsp-client-logger.js.map +1 -0
- package/lib/logging/index.d.ts +10 -0
- package/lib/logging/index.d.ts.map +1 -0
- package/lib/logging/index.js +10 -0
- package/lib/logging/index.js.map +1 -0
- package/lib/node/index.d.ts +10 -0
- package/lib/node/index.d.ts.map +1 -0
- package/lib/node/index.js +13 -0
- package/lib/node/index.js.map +1 -0
- package/lib/node/start-glsp-server.d.ts +122 -0
- package/lib/node/start-glsp-server.d.ts.map +1 -0
- package/lib/node/start-glsp-server.js +103 -0
- package/lib/node/start-glsp-server.js.map +1 -0
- package/lib/state/abstract-hydranium-glsp-state.d.ts +333 -0
- package/lib/state/abstract-hydranium-glsp-state.d.ts.map +1 -0
- package/lib/state/abstract-hydranium-glsp-state.js +451 -0
- package/lib/state/abstract-hydranium-glsp-state.js.map +1 -0
- package/lib/state/full-text-hydranium-glsp-state.d.ts +43 -0
- package/lib/state/full-text-hydranium-glsp-state.d.ts.map +1 -0
- package/lib/state/full-text-hydranium-glsp-state.js +60 -0
- package/lib/state/full-text-hydranium-glsp-state.js.map +1 -0
- package/lib/state/hydranium-glsp-index.d.ts +220 -0
- package/lib/state/hydranium-glsp-index.d.ts.map +1 -0
- package/lib/state/hydranium-glsp-index.js +320 -0
- package/lib/state/hydranium-glsp-index.js.map +1 -0
- package/lib/state/hydranium-shared-core-services.d.ts +90 -0
- package/lib/state/hydranium-shared-core-services.d.ts.map +1 -0
- package/lib/state/hydranium-shared-core-services.js +89 -0
- package/lib/state/hydranium-shared-core-services.js.map +1 -0
- package/lib/state/index.d.ts +17 -0
- package/lib/state/index.d.ts.map +1 -0
- package/lib/state/index.js +17 -0
- package/lib/state/index.js.map +1 -0
- package/lib/state/model-ready-timeout-error.d.ts +28 -0
- package/lib/state/model-ready-timeout-error.d.ts.map +1 -0
- package/lib/state/model-ready-timeout-error.js +36 -0
- package/lib/state/model-ready-timeout-error.js.map +1 -0
- package/lib/state/reconcile-source-model-write.d.ts +43 -0
- package/lib/state/reconcile-source-model-write.d.ts.map +1 -0
- package/lib/state/reconcile-source-model-write.js +47 -0
- package/lib/state/reconcile-source-model-write.js.map +1 -0
- package/lib/state/reconciling-multi-document-glsp-state.d.ts +164 -0
- package/lib/state/reconciling-multi-document-glsp-state.d.ts.map +1 -0
- package/lib/state/reconciling-multi-document-glsp-state.js +222 -0
- package/lib/state/reconciling-multi-document-glsp-state.js.map +1 -0
- package/lib/state/reconciling-transfer-hydranium-glsp-state.d.ts +91 -0
- package/lib/state/reconciling-transfer-hydranium-glsp-state.d.ts.map +1 -0
- package/lib/state/reconciling-transfer-hydranium-glsp-state.js +129 -0
- package/lib/state/reconciling-transfer-hydranium-glsp-state.js.map +1 -0
- package/lib/storage/hydranium-glsp-storage.d.ts +348 -0
- package/lib/storage/hydranium-glsp-storage.d.ts.map +1 -0
- package/lib/storage/hydranium-glsp-storage.js +634 -0
- package/lib/storage/hydranium-glsp-storage.js.map +1 -0
- package/lib/storage/index.d.ts +11 -0
- package/lib/storage/index.d.ts.map +1 -0
- package/lib/storage/index.js +11 -0
- package/lib/storage/index.js.map +1 -0
- package/lib/storage/save-conflict-policy.d.ts +56 -0
- package/lib/storage/save-conflict-policy.d.ts.map +1 -0
- package/lib/storage/save-conflict-policy.js +19 -0
- package/lib/storage/save-conflict-policy.js.map +1 -0
- package/lib/submission/hydranium-glsp-submission-handler.d.ts +109 -0
- package/lib/submission/hydranium-glsp-submission-handler.d.ts.map +1 -0
- package/lib/submission/hydranium-glsp-submission-handler.js +157 -0
- package/lib/submission/hydranium-glsp-submission-handler.js.map +1 -0
- package/lib/submission/index.d.ts +10 -0
- package/lib/submission/index.d.ts.map +1 -0
- package/lib/submission/index.js +10 -0
- package/lib/submission/index.js.map +1 -0
- package/lib/testing/glsp-harness.d.ts +170 -0
- package/lib/testing/glsp-harness.d.ts.map +1 -0
- package/lib/testing/glsp-harness.js +231 -0
- package/lib/testing/glsp-harness.js.map +1 -0
- package/lib/testing/index.d.ts +11 -0
- package/lib/testing/index.d.ts.map +1 -0
- package/lib/testing/index.js +13 -0
- package/lib/testing/index.js.map +1 -0
- package/lib/testing/make-noop-glsp-logger.d.ts +42 -0
- package/lib/testing/make-noop-glsp-logger.d.ts.map +1 -0
- package/lib/testing/make-noop-glsp-logger.js +47 -0
- package/lib/testing/make-noop-glsp-logger.js.map +1 -0
- package/lib/util/index.d.ts +10 -0
- package/lib/util/index.d.ts.map +1 -0
- package/lib/util/index.js +10 -0
- package/lib/util/index.js.map +1 -0
- package/lib/util/service-identifier.d.ts +46 -0
- package/lib/util/service-identifier.d.ts.map +1 -0
- package/lib/util/service-identifier.js +47 -0
- package/lib/util/service-identifier.js.map +1 -0
- package/lib/validation/diagnostic-markers.d.ts +48 -0
- package/lib/validation/diagnostic-markers.d.ts.map +1 -0
- package/lib/validation/diagnostic-markers.js +68 -0
- package/lib/validation/diagnostic-markers.js.map +1 -0
- package/lib/validation/hydranium-glsp-model-validator.d.ts +51 -0
- package/lib/validation/hydranium-glsp-model-validator.d.ts.map +1 -0
- package/lib/validation/hydranium-glsp-model-validator.js +118 -0
- package/lib/validation/hydranium-glsp-model-validator.js.map +1 -0
- package/lib/validation/index.d.ts +11 -0
- package/lib/validation/index.d.ts.map +1 -0
- package/lib/validation/index.js +11 -0
- package/lib/validation/index.js.map +1 -0
- package/package.json +111 -0
- package/src/browser/index.ts +15 -0
- package/src/browser/start-glsp-server-in-worker.ts +202 -0
- package/src/command/hydranium-glsp-recording-command.ts +183 -0
- package/src/command/index.ts +10 -0
- package/src/computed-bounds/hydranium-glsp-computed-bounds-action-handler.ts +103 -0
- package/src/computed-bounds/index.ts +10 -0
- package/src/dispatcher/index.ts +10 -0
- package/src/dispatcher/server-action-dispatcher.ts +72 -0
- package/src/index.ts +20 -0
- package/src/launcher/abstract-hydranium-glsp-diagram-module.ts +117 -0
- package/src/launcher/glsp-app-module.ts +106 -0
- package/src/launcher/glsp-framework-overrides.ts +51 -0
- package/src/launcher/glsp-server-overrides.ts +46 -0
- package/src/launcher/hydranium-glsp-server.ts +104 -0
- package/src/launcher/index.ts +22 -0
- package/src/logging/glsp-client-logger.ts +184 -0
- package/src/logging/index.ts +10 -0
- package/src/node/index.ts +13 -0
- package/src/node/start-glsp-server.ts +214 -0
- package/src/state/abstract-hydranium-glsp-state.ts +503 -0
- package/src/state/full-text-hydranium-glsp-state.ts +63 -0
- package/src/state/hydranium-glsp-index.ts +337 -0
- package/src/state/hydranium-shared-core-services.ts +92 -0
- package/src/state/index.ts +17 -0
- package/src/state/model-ready-timeout-error.ts +33 -0
- package/src/state/reconcile-source-model-write.ts +77 -0
- package/src/state/reconciling-multi-document-glsp-state.ts +248 -0
- package/src/state/reconciling-transfer-hydranium-glsp-state.ts +132 -0
- package/src/storage/hydranium-glsp-storage.ts +660 -0
- package/src/storage/index.ts +11 -0
- package/src/storage/save-conflict-policy.ts +52 -0
- package/src/submission/hydranium-glsp-submission-handler.ts +170 -0
- package/src/submission/index.ts +10 -0
- package/src/testing/glsp-harness.ts +422 -0
- package/src/testing/index.ts +14 -0
- package/src/testing/make-noop-glsp-logger.ts +66 -0
- package/src/util/index.ts +10 -0
- package/src/util/service-identifier.ts +49 -0
- package/src/validation/diagnostic-markers.ts +93 -0
- package/src/validation/hydranium-glsp-model-validator.ts +105 -0
- package/src/validation/index.ts +11 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2023-2026 CrossBreeze, EclipseSource and others.
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials are made available under the
|
|
5
|
+
* terms of the MIT License which is available in the project root.
|
|
6
|
+
*
|
|
7
|
+
* SPDX-License-Identifier: MIT
|
|
8
|
+
********************************************************************************/
|
|
9
|
+
|
|
10
|
+
import { type GModelElement, GModelIndex } from '@eclipse-glsp/server';
|
|
11
|
+
import { inject, injectable, optional } from 'inversify';
|
|
12
|
+
import { type AstNode, AstUtils, type URI } from '@hydranium/langium';
|
|
13
|
+
import * as uuid from 'uuid';
|
|
14
|
+
import { type ElementKeyProvider, type ServerLanguageServices, type ServerSharedServices } from '@hydranium/core';
|
|
15
|
+
import { HydraniumTypes } from './hydranium-shared-core-services.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* GLSP model index extended with an AST-node ↔ id map so adopters can
|
|
19
|
+
* resolve diagram elements back to their source-language nodes during
|
|
20
|
+
* action handling, layout, validation, and submission.
|
|
21
|
+
*
|
|
22
|
+
* Indexing strategy is hookable via {@link doFindId}: the framework default
|
|
23
|
+
* uses {@link ElementKeyProvider.getElementKey} — a document-scoped lookup
|
|
24
|
+
* token built from the node's named ancestors, falling back to positional
|
|
25
|
+
* segments for unnamed nodes. Under the bound default that key is name-based,
|
|
26
|
+
* so a GModel id survives an edit that does not rename the node or an ancestor
|
|
27
|
+
* and changes when one does; the positional strategy is the rename-stable
|
|
28
|
+
* alternative. Adopters with a different stability boundary override
|
|
29
|
+
* {@link doFindId}.
|
|
30
|
+
*
|
|
31
|
+
* **Duplicate-id degrade.** A GModel tree must have unique element ids. Two
|
|
32
|
+
* AST nodes that collide on their stable id — exactly the state the diagram is
|
|
33
|
+
* meant to *mark* — project to two GModel elements with the same id. The GLSP
|
|
34
|
+
* client's sprotty `SGraphIndex.add` throws on such a duplicate inside
|
|
35
|
+
* `SModelFactory.initializeRoot`, aborting the build and blanking the WHOLE
|
|
36
|
+
* diagram. {@link indexRoot} prunes the colliding element(s) from the tree —
|
|
37
|
+
* the same object `ModelSubmissionHandler`
|
|
38
|
+
* submits — so the wire payload stays well-formed for every client and the
|
|
39
|
+
* surviving (first, document-order) element still carries the diagnostic
|
|
40
|
+
* marker. {@link doIndex} keeps a log-and-skip guard for any path that
|
|
41
|
+
* re-indexes a subtree without going through {@link indexRoot}.
|
|
42
|
+
*/
|
|
43
|
+
@injectable()
|
|
44
|
+
export class HydraniumGlspIndex extends GModelIndex {
|
|
45
|
+
@inject(HydraniumTypes.SharedCoreServices) protected readonly sharedServices!: ServerSharedServices;
|
|
46
|
+
|
|
47
|
+
protected idToSemanticNode = new Map<string, AstNode>();
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The language services of the diagram's OWN document, captured from the
|
|
51
|
+
* source root in {@link indexSourceRoot}. Used only as the fallback for a
|
|
52
|
+
* node that routes nowhere — a synthetic or freshly-built node with no
|
|
53
|
+
* `$document` yet, which a GModel factory legitimately hands us mid-edit.
|
|
54
|
+
*
|
|
55
|
+
* Captured rather than injected because it is the language the loaded
|
|
56
|
+
* document ACTUALLY routes to, which is not necessarily the one the diagram
|
|
57
|
+
* module declared — precisely the drift
|
|
58
|
+
* `AbstractHydraniumGlspState.checkDeclaredLanguage` warns about. (Not
|
|
59
|
+
* a cost argument: {@link HydraniumTypes}.DiagramLanguage is bound
|
|
60
|
+
* `inSingletonScope`, so injecting it would resolve once per session too.)
|
|
61
|
+
*/
|
|
62
|
+
protected diagramLanguage?: ServerLanguageServices;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The grammar the diagram module declared, as the last resort behind
|
|
66
|
+
* {@link diagramLanguage}.
|
|
67
|
+
*
|
|
68
|
+
* Order-independent where the captured one is not: GLSP builds every
|
|
69
|
+
* operation handler at `InitializeClientSession`, so a document-less node
|
|
70
|
+
* can reach {@link createId} before the first {@link indexSourceRoot} — and
|
|
71
|
+
* with no language at all that mints an unstable `fallback_<uuid>` where a
|
|
72
|
+
* stable positional key was available. `@optional()` so a harness that binds
|
|
73
|
+
* no diagram module still resolves.
|
|
74
|
+
*/
|
|
75
|
+
@inject(HydraniumTypes.DiagramLanguage) @optional() protected readonly declaredLanguage?: ServerLanguageServices;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Reverse map: the *stable id* of the AST element a GModel element
|
|
79
|
+
* represents → the GModel id(s) that render it. Multi-valued because one
|
|
80
|
+
* element can be drawn as several GModel nodes — placed twice on the same
|
|
81
|
+
* canvas, or projected by several referencing nodes.
|
|
82
|
+
* Populated explicitly by adopters via {@link registerElementId} (the forward
|
|
83
|
+
* {@link idToSemanticNode} keys by GModel id and can't express the same
|
|
84
|
+
* element rendering more than once). Consumed by diagnostic→marker mapping to
|
|
85
|
+
* find every node to mark for a given element.
|
|
86
|
+
*
|
|
87
|
+
* Keyed by the element's stable id (a string), NOT the `AstNode` itself, so
|
|
88
|
+
* it never retains AST node references across rebuilds. Cleared with the
|
|
89
|
+
* forward index in {@link indexSourceRoot}; re-registration each GModel
|
|
90
|
+
* rebuild is idempotent (a Set), and any id no longer drawn is filtered by
|
|
91
|
+
* the consumer's rendered-check, so stale entries are harmless.
|
|
92
|
+
*/
|
|
93
|
+
protected elementToIds = new Map<string, Set<string>>();
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The uris of the documents whose elements can map to a marker on this
|
|
97
|
+
* diagram: the diagram's own document (seeded in {@link indexSourceRoot})
|
|
98
|
+
* plus every document reached through a reference projection (added in
|
|
99
|
+
* {@link registerElementId}). Lets the diagnostic→marker validator resolve
|
|
100
|
+
* only the documents that actually contribute rendered elements rather than
|
|
101
|
+
* every loaded document. Self-maintaining: every {@link registerElementId}
|
|
102
|
+
* caller auto-includes its document.
|
|
103
|
+
*/
|
|
104
|
+
protected renderedDocUris = new Set<string>();
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Return the GModel id assigned to `node`, or — when no id is found and
|
|
108
|
+
* a `fallback` is supplied — the value of `fallback()`. The two-overload
|
|
109
|
+
* shape preserves the caller's narrowing: with a `() => string` fallback
|
|
110
|
+
* the return is `string`; with a `() => string | undefined` (or no
|
|
111
|
+
* fallback) the return is `string | undefined`.
|
|
112
|
+
*
|
|
113
|
+
* A convenience over {@link doFindId}, NOT the id-strategy hook. Framework
|
|
114
|
+
* internals call `doFindId` directly (see {@link indexSourceRoot}), so an
|
|
115
|
+
* override here changes only what external callers of this method see —
|
|
116
|
+
* override `doFindId` to change how nodes are keyed.
|
|
117
|
+
*/
|
|
118
|
+
findId(node: AstNode | undefined, fallback: () => string): string;
|
|
119
|
+
findId(node: AstNode | undefined, fallback?: () => string | undefined): string | undefined;
|
|
120
|
+
findId(node: AstNode | undefined, fallback: () => string | undefined = () => undefined): string | undefined {
|
|
121
|
+
return this.doFindId(node) ?? fallback();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Hook for the adopter id-assignment strategy. Framework default reads
|
|
126
|
+
* {@link ElementKeyProvider.getElementKey} from the language owning `node`,
|
|
127
|
+
* which under the bound default is name-based and falls back to positional
|
|
128
|
+
* segments for unnamed nodes.
|
|
129
|
+
*
|
|
130
|
+
* `keyProvider` is an optional hint: the provider for THIS node, already
|
|
131
|
+
* resolved by the caller. {@link indexSourceRoot} passes it because every
|
|
132
|
+
* node in a containment walk belongs to the root's document by
|
|
133
|
+
* construction, so the per-node lookup would re-derive one known answer N
|
|
134
|
+
* times. An override that ignores the parameter stays correct, at the cost
|
|
135
|
+
* of that lookup.
|
|
136
|
+
*/
|
|
137
|
+
protected doFindId(node?: AstNode, keyProvider?: ElementKeyProvider): string | undefined {
|
|
138
|
+
return (keyProvider ?? this.elementKeyProviderFor(node))?.getElementKey(node);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* The {@link ElementKeyProvider} of the language owning `node`'s document,
|
|
143
|
+
* falling back to the language the diagram document routes to and then to
|
|
144
|
+
* the one its module declared, for a node that routes nowhere.
|
|
145
|
+
*
|
|
146
|
+
* **Resolved per node on the cross-document entry points.** This index is
|
|
147
|
+
* deliberately cross-document — {@link registerElementId} and
|
|
148
|
+
* {@link findElementIds} key elements reached through a reference, and
|
|
149
|
+
* {@link renderedDocUris} exists precisely because those live in other
|
|
150
|
+
* documents. A single captured provider therefore keys foreign nodes under
|
|
151
|
+
* the wrong grammar as soon as two registered languages differ in their key
|
|
152
|
+
* strategy: a plausible-looking id that matches nothing, so selection,
|
|
153
|
+
* marker mapping and element lookup silently miss. Those entry points are a
|
|
154
|
+
* bounded set the adopter calls explicitly; the full-document walk in
|
|
155
|
+
* {@link indexSourceRoot} needs no per-node lookup and does not pay for one.
|
|
156
|
+
*/
|
|
157
|
+
protected elementKeyProviderFor(node?: AstNode): ElementKeyProvider | undefined {
|
|
158
|
+
const language = this.sharedServices.ServiceRegistry.getServicesFor(node) ?? this.diagramLanguage ?? this.declaredLanguage;
|
|
159
|
+
return language?.references.ElementKeyProvider;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Return the GModel id assigned to `node`, falling back to a fresh `fallback_<uuid>`. */
|
|
163
|
+
createId(node?: AstNode): string {
|
|
164
|
+
return this.findId(node, () => 'fallback_' + uuid.v4());
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Return the GModel id assigned to `node` or throw if none is available.
|
|
169
|
+
*
|
|
170
|
+
* The failure names the node's type and position rather than its source
|
|
171
|
+
* text. Interpolating `$cstNode.text` would put the user's own model content
|
|
172
|
+
* into a string that reaches a log, a client toast and an adopter's
|
|
173
|
+
* telemetry alike, so the exposure does not depend on which surface
|
|
174
|
+
* receives it. Type and position identify the node for whoever is debugging
|
|
175
|
+
* without carrying any of it, and positions are 1-based so they match what
|
|
176
|
+
* an editor shows.
|
|
177
|
+
*/
|
|
178
|
+
assertId(node?: AstNode): string {
|
|
179
|
+
const id = this.findId(node);
|
|
180
|
+
if (!id) {
|
|
181
|
+
const start = node?.$cstNode?.range.start;
|
|
182
|
+
const at = start === undefined ? '' : ` at ${start.line + 1}:${start.character + 1}`;
|
|
183
|
+
throw new Error(`Could not create ID for ${node?.$type ?? 'an absent node'}${at}`);
|
|
184
|
+
}
|
|
185
|
+
return id;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Walk the source root and index every reachable AST node with a resolvable
|
|
190
|
+
* id.
|
|
191
|
+
*
|
|
192
|
+
* `uri` is the diagram document's URI, supplied by
|
|
193
|
+
* `AbstractHydraniumGlspState.setSourceRoot`, and is used only to
|
|
194
|
+
* resolve the diagram's language when `root` carries no `$document` —
|
|
195
|
+
* always the case for a synthesised root, and the more authoritative signal
|
|
196
|
+
* either way since it is what the caller declared it was loading.
|
|
197
|
+
*
|
|
198
|
+
* The walk resolves the key provider ONCE and threads it: `streamAllContents`
|
|
199
|
+
* is containment-only, so every node it yields has `root` as its root node
|
|
200
|
+
* and therefore the language just resolved. Looking it up per node would
|
|
201
|
+
* walk the container chain and Langium's whole lookup ladder — declared
|
|
202
|
+
* languageId, file name, extension — for an answer already in hand, on every
|
|
203
|
+
* diagram open and every source-root refresh.
|
|
204
|
+
*/
|
|
205
|
+
indexSourceRoot(root: AstNode, uri?: URI | string): void {
|
|
206
|
+
this.idToSemanticNode.clear();
|
|
207
|
+
this.elementToIds.clear();
|
|
208
|
+
this.renderedDocUris.clear();
|
|
209
|
+
const registry = this.sharedServices.ServiceRegistry;
|
|
210
|
+
this.diagramLanguage = registry.getServicesFor(root) ?? registry.getServicesFor(uri);
|
|
211
|
+
this.addRenderedDocUri(root);
|
|
212
|
+
const keyProvider = this.elementKeyProviderFor(root);
|
|
213
|
+
AstUtils.streamAllContents(root).forEach(node => this.indexAstNode(node, keyProvider));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Register that the GModel element `id` renders `element` — the reverse of
|
|
218
|
+
* the GModel-id keying. Adopters call this when a GModel element represents
|
|
219
|
+
* an AST element that is *not* keyed by its own id — most importantly a
|
|
220
|
+
* diagram node keyed by its own id while visually standing for the element
|
|
221
|
+
* it references. Additive: registering the same `element` under several ids
|
|
222
|
+
* records every occurrence.
|
|
223
|
+
*/
|
|
224
|
+
registerElementId(element: AstNode, id: string): void {
|
|
225
|
+
const key = this.doFindId(element);
|
|
226
|
+
if (key === undefined) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const ids = this.elementToIds.get(key);
|
|
230
|
+
if (ids) {
|
|
231
|
+
ids.add(id);
|
|
232
|
+
} else {
|
|
233
|
+
this.elementToIds.set(key, new Set([id]));
|
|
234
|
+
}
|
|
235
|
+
this.addRenderedDocUri(element);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** The uris of the documents that contribute elements rendered on this diagram. */
|
|
239
|
+
renderedDocumentUris(): readonly string[] {
|
|
240
|
+
return [...this.renderedDocUris];
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Record `node`'s document uri (if it has one) as contributing rendered elements. */
|
|
244
|
+
protected addRenderedDocUri(node: AstNode): void {
|
|
245
|
+
const uri = AstUtils.findRootNode(node).$document?.uri.toString();
|
|
246
|
+
if (uri !== undefined) {
|
|
247
|
+
this.renderedDocUris.add(uri);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** The GModel ids registered (via {@link registerElementId}) as rendering `element`; empty if none. */
|
|
252
|
+
findElementIds(element?: AstNode): readonly string[] {
|
|
253
|
+
const key = element ? this.doFindId(element) : undefined;
|
|
254
|
+
const ids = key !== undefined ? this.elementToIds.get(key) : undefined;
|
|
255
|
+
return ids ? [...ids] : [];
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** `keyProvider` is {@link doFindId}'s already-resolved hint; see {@link indexSourceRoot}. */
|
|
259
|
+
protected indexAstNode(node: AstNode, keyProvider?: ElementKeyProvider): void {
|
|
260
|
+
const id = this.doFindId(node, keyProvider);
|
|
261
|
+
if (id) {
|
|
262
|
+
this.indexSemanticElement(id, node);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Insert an AST node under `id` directly — for adopters that need to seed entries outside the standard walk. */
|
|
267
|
+
indexSemanticElement<T extends AstNode>(id: string, element: T): void {
|
|
268
|
+
this.idToSemanticNode.set(id, element);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Look up the AST node previously indexed under `id`. */
|
|
272
|
+
findSemanticElement(id: string): AstNode | undefined;
|
|
273
|
+
findSemanticElement<T extends AstNode>(id: string, guard: (item: unknown) => item is T): T | undefined;
|
|
274
|
+
findSemanticElement<T extends AstNode>(id: string, guard?: (item: unknown) => item is T): T | AstNode | undefined {
|
|
275
|
+
const semanticNode = this.idToSemanticNode.get(id);
|
|
276
|
+
if (guard) {
|
|
277
|
+
return guard(semanticNode) ? semanticNode : undefined;
|
|
278
|
+
}
|
|
279
|
+
return semanticNode;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Prune duplicate-id elements from the GModel tree, then index it.
|
|
284
|
+
*
|
|
285
|
+
* {@link GModelIndex.indexRoot} runs on the exact object the submission
|
|
286
|
+
* handler later serializes (`DefaultModelState.updateRoot` assigns
|
|
287
|
+
* `this.root = newRoot` immediately before `this.index.indexRoot(newRoot)`),
|
|
288
|
+
* so removing duplicates here also removes them from the submitted payload —
|
|
289
|
+
* the only place the fix protects every client rather than one tolerant one.
|
|
290
|
+
*/
|
|
291
|
+
override indexRoot(root: GModelElement): void {
|
|
292
|
+
this.pruneDuplicateIds(root);
|
|
293
|
+
super.indexRoot(root);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Remove every element whose id was already seen earlier in a depth-first
|
|
298
|
+
* walk; the first occurrence (document order) wins and a dropped element
|
|
299
|
+
* takes its whole subtree with it. Splices in place so the parent's
|
|
300
|
+
* `children` array identity is preserved; logs each drop via the bound
|
|
301
|
+
* logger. The root itself is the first id seen and can never be a duplicate.
|
|
302
|
+
*/
|
|
303
|
+
protected pruneDuplicateIds(root: GModelElement): void {
|
|
304
|
+
const seen = new Set<string>([root.id]);
|
|
305
|
+
const visit = (element: GModelElement): void => {
|
|
306
|
+
const children = element.children;
|
|
307
|
+
let index = 0;
|
|
308
|
+
while (index < children.length) {
|
|
309
|
+
const child = children[index];
|
|
310
|
+
if (seen.has(child.id)) {
|
|
311
|
+
this.sharedServices.Logger.for('Index').warn('Dropping duplicate element id from submitted graph: ' + child.id);
|
|
312
|
+
children.splice(index, 1); // drop child + subtree; do not advance — the next child shifts into `index`
|
|
313
|
+
} else {
|
|
314
|
+
seen.add(child.id);
|
|
315
|
+
visit(child);
|
|
316
|
+
index++;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
visit(root);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Log-and-skip guard for duplicate GModel ids. {@link indexRoot}'s prune
|
|
325
|
+
* already removes duplicates from the tree before this runs, so in the
|
|
326
|
+
* normal flow this never fires; it stays as a last line of defence for any
|
|
327
|
+
* path that re-indexes a subtree directly (where {@link GModelIndex.doIndex}
|
|
328
|
+
* would otherwise throw).
|
|
329
|
+
*/
|
|
330
|
+
protected override doIndex(element: GModelElement): void {
|
|
331
|
+
if (this.idToElement.has(element.id)) {
|
|
332
|
+
this.sharedServices.Logger.for('Index').error('Duplicate element id in graph: ' + element.id);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
super.doIndex(element);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 CrossBreeze, EclipseSource and others.
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials are made available under the
|
|
5
|
+
* terms of the MIT License which is available in the project root.
|
|
6
|
+
*
|
|
7
|
+
* SPDX-License-Identifier: MIT
|
|
8
|
+
********************************************************************************/
|
|
9
|
+
|
|
10
|
+
import type { ServerLanguageServices, ServerSharedServices } from '@hydranium/core';
|
|
11
|
+
import type { ConflictResolver, Tracer } from '@hydranium/protocol';
|
|
12
|
+
import { serviceIdentifier } from '../util/service-identifier.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Inversify service-identifier registry for the framework services the
|
|
16
|
+
* `@hydranium/glsp-server` runtime exposes to `@injectable()` GLSP
|
|
17
|
+
* components. Framework GLSP-side classes are `@injectable()` and need an
|
|
18
|
+
* Inversify token to receive the Langium-style services the rest of the
|
|
19
|
+
* framework hands around via plain constructor arguments.
|
|
20
|
+
*
|
|
21
|
+
* Every token here is a service whose value is **fixed for the container tier
|
|
22
|
+
* that binds it** — process-wide shared services, a per-session conflict
|
|
23
|
+
* policy, the grammar a diagram type edits. That is the whole membership rule,
|
|
24
|
+
* and it is why the registry is short: see below for what deliberately does
|
|
25
|
+
* NOT get a token.
|
|
26
|
+
*
|
|
27
|
+
* Grouped under one object (rather than top-level `Hydranium<Role>` consts)
|
|
28
|
+
* so the token identity is decoupled from the implementation class name.
|
|
29
|
+
* Mirrors GLSP's own `TYPES` registry idiom.
|
|
30
|
+
*
|
|
31
|
+
* Bound by the adopter from their services trees — the app-tier ones directly,
|
|
32
|
+
* the session-tier language by declaring a grammar on the diagram module.
|
|
33
|
+
* Inject with the real service type as the field type.
|
|
34
|
+
*
|
|
35
|
+
* **Split by tier, because GLSP has no language tier.** GLSP's containers are
|
|
36
|
+
* app (per process) -> server (per connection) -> session (per open diagram);
|
|
37
|
+
* Langium's are shared (per process) -> language (per grammar, routed by URI).
|
|
38
|
+
* Only the outermost pair line up. A per-language service therefore CANNOT be
|
|
39
|
+
* bound at the app tier — that container is built before any document exists,
|
|
40
|
+
* so it can only ever hold one grammar's services. The tokens below are split
|
|
41
|
+
* accordingly:
|
|
42
|
+
*
|
|
43
|
+
* - **SharedCoreServices** — the framework's Langium-style shared services
|
|
44
|
+
* tree ({@link ServerSharedServices}); the services-tree root that state /
|
|
45
|
+
* index classes inject. Bound once at the app tier. Carries
|
|
46
|
+
* `ServiceRegistry`, which is how everything below reaches a language.
|
|
47
|
+
* - **DiagramLanguage** — the {@link ServerLanguageServices} of the grammar a
|
|
48
|
+
* diagram type edits, bound at the SESSION tier by
|
|
49
|
+
* `AbstractHydraniumGlspDiagramModule` from its declared `declareLanguage()`.
|
|
50
|
+
* Static (a diagram type has one grammar), so it is safe for the eagerly
|
|
51
|
+
* constructed handlers GLSP builds at `InitializeClientSession`. Read it
|
|
52
|
+
* through `AbstractHydraniumGlspState.diagramLanguage`.
|
|
53
|
+
* - **ConflictResolver** — the policy the GLSP state consults when a write
|
|
54
|
+
* (forward-write, undo, redo, save) races a foreign edit.
|
|
55
|
+
* `HydraniumGlspAppModule` binds it to `options.conflictResolver`
|
|
56
|
+
* when supplied, else a default `ReconcilingConflictResolver` (field-level
|
|
57
|
+
* three-way merge) unless the adopter opts into `ForceConflictResolver`.
|
|
58
|
+
* - **Tracer** — the measure-and-emit observability handle (timing, memory,
|
|
59
|
+
* profiling). Bound per-request, auto-tagged with the requesting class name
|
|
60
|
+
* (mirroring GLSP's `LoggerFactory` caller-tagging), so an `@injectable()`
|
|
61
|
+
* GLSP class that times just injects it and its timing lines carry its own
|
|
62
|
+
* component — no manual `for(...)`. The non-injectable recording command,
|
|
63
|
+
* which can't inject, borrows the state's `tracer` getter instead.
|
|
64
|
+
*
|
|
65
|
+
* **No per-language SERVICE has a token — only the language does.** There is
|
|
66
|
+
* no `ElementKeyProvider`, `NameProvider`, `ScopeProvider` or
|
|
67
|
+
* `CandidateProvider` symbol, because every one of them is keyed by a document
|
|
68
|
+
* that the container cannot know: the target node's, for naming and keying;
|
|
69
|
+
* the one a reference is WRITTEN in, for scope and candidates. A GLSP
|
|
70
|
+
* component asks about several documents over its life — the diagram's own,
|
|
71
|
+
* the element it is editing, a drop target — so an injected provider instance
|
|
72
|
+
* answers as if there were one right answer and the routing decision vanishes
|
|
73
|
+
* from the call site. `this.candidateProvider` reads as language-neutral, and
|
|
74
|
+
* nothing prompts the author to ask which document the reference lives in;
|
|
75
|
+
* that is exactly how a foreign node ends up named under the diagram's
|
|
76
|
+
* grammar, producing a plausible string that matches nothing.
|
|
77
|
+
*
|
|
78
|
+
* So callers name the language they mean: `modelState.diagramLanguage` for a
|
|
79
|
+
* reference written on the canvas, `modelState.languageServicesFor(node)` for
|
|
80
|
+
* anything reached through a reference. `ServiceRegistry.getServicesFor(target)`
|
|
81
|
+
* serves the same purpose where no model state is at hand.
|
|
82
|
+
*
|
|
83
|
+
* The app-tier tokens are bound by `HydraniumGlspAppModule`, the
|
|
84
|
+
* session-tier one by `AbstractHydraniumGlspDiagramModule`. A multi-grammar
|
|
85
|
+
* adopter declares a language per diagram module and needs nothing else.
|
|
86
|
+
*/
|
|
87
|
+
export const HydraniumTypes = {
|
|
88
|
+
SharedCoreServices: serviceIdentifier<ServerSharedServices>('HydraniumSharedCoreServices'),
|
|
89
|
+
DiagramLanguage: serviceIdentifier<ServerLanguageServices>('HydraniumDiagramLanguage'),
|
|
90
|
+
ConflictResolver: serviceIdentifier<ConflictResolver>('HydraniumConflictResolver'),
|
|
91
|
+
Tracer: serviceIdentifier<Tracer>('HydraniumTracer')
|
|
92
|
+
} as const;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 CrossBreeze, EclipseSource and others.
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials are made available under the
|
|
5
|
+
* terms of the MIT License which is available in the project root.
|
|
6
|
+
*
|
|
7
|
+
* SPDX-License-Identifier: MIT
|
|
8
|
+
********************************************************************************/
|
|
9
|
+
|
|
10
|
+
export * from './hydranium-glsp-index.js';
|
|
11
|
+
export * from './abstract-hydranium-glsp-state.js';
|
|
12
|
+
export * from './reconcile-source-model-write.js';
|
|
13
|
+
export * from './reconciling-transfer-hydranium-glsp-state.js';
|
|
14
|
+
export * from './reconciling-multi-document-glsp-state.js';
|
|
15
|
+
export * from './full-text-hydranium-glsp-state.js';
|
|
16
|
+
export * from './hydranium-shared-core-services.js';
|
|
17
|
+
export * from './model-ready-timeout-error.js';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 CrossBreeze, EclipseSource and others.
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials are made available under the
|
|
5
|
+
* terms of the MIT License which is available in the project root.
|
|
6
|
+
*
|
|
7
|
+
* SPDX-License-Identifier: MIT
|
|
8
|
+
********************************************************************************/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Thrown by `AbstractHydraniumGlspState.ready` when the wait for a document
|
|
12
|
+
* build state exceeds `AbstractHydraniumGlspState.readyTimeoutMs`. A dedicated
|
|
13
|
+
* error class so callers can distinguish a genuine hang from other failures
|
|
14
|
+
* via `instanceof`.
|
|
15
|
+
*
|
|
16
|
+
* The optional `diagnostic` carries a build-status snapshot so a captured log
|
|
17
|
+
* line is self-explanatory without correlating the timeout entry against
|
|
18
|
+
* earlier rebuild traces. `AbstractHydraniumGlspState.buildReadyTimeoutError`
|
|
19
|
+
* fills it from the document builder; a caller constructing the error directly
|
|
20
|
+
* may leave it empty.
|
|
21
|
+
*/
|
|
22
|
+
export class ModelReadyTimeoutError extends Error {
|
|
23
|
+
constructor(
|
|
24
|
+
public readonly uri: string,
|
|
25
|
+
public readonly targetState: string,
|
|
26
|
+
public readonly elapsedMs: number,
|
|
27
|
+
public readonly diagnostic: string = ''
|
|
28
|
+
) {
|
|
29
|
+
const suffix = diagnostic ? ` ${diagnostic}` : '';
|
|
30
|
+
super(`Timed out after ${elapsedMs}ms waiting for state '${targetState}' on ${uri}.${suffix}`);
|
|
31
|
+
this.name = 'ModelReadyTimeoutError';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 CrossBreeze, EclipseSource and others.
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials are made available under the
|
|
5
|
+
* terms of the MIT License which is available in the project root.
|
|
6
|
+
*
|
|
7
|
+
* SPDX-License-Identifier: MIT
|
|
8
|
+
********************************************************************************/
|
|
9
|
+
|
|
10
|
+
import { type ConflictResolver, type Logger, isConflictError } from '@hydranium/protocol';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The I/O and policy a {@link reconcileSourceModelWrite} call needs. Deliberately
|
|
14
|
+
* a plain record rather than a base class: the states that use it differ in what
|
|
15
|
+
* "persist" and "project" mean but not at all in how a conflict is handled, and
|
|
16
|
+
* pulling the orchestration out is what keeps that second half from being
|
|
17
|
+
* written once per state.
|
|
18
|
+
*/
|
|
19
|
+
export interface SourceModelWriteHooks<TModel> {
|
|
20
|
+
/**
|
|
21
|
+
* Write `model` and capture the resulting state. Called with a `baseVersion`
|
|
22
|
+
* on the first attempt (opting into the `ConflictError` gate) and without one
|
|
23
|
+
* on the merged / forced retries, where the point is to land the write.
|
|
24
|
+
*/
|
|
25
|
+
persist(model: TModel, baseVersion?: number): Promise<void>;
|
|
26
|
+
/** Current server-side projection, or `undefined` when unavailable. */
|
|
27
|
+
refetch(): Promise<TModel | undefined>;
|
|
28
|
+
/** Last in-sync projection the user's intent is measured against. */
|
|
29
|
+
readonly baseline: TModel | undefined;
|
|
30
|
+
readonly conflictResolver: ConflictResolver;
|
|
31
|
+
readonly logger: Logger;
|
|
32
|
+
/** Invoked when the edit is dropped, so the caller can resync its own state. */
|
|
33
|
+
onConflictDropped(): void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Persist `model`, and on a `ConflictError` reconcile the user's intent against
|
|
38
|
+
* the current server state via the injected policy and act on the outcome.
|
|
39
|
+
*
|
|
40
|
+
* One declarative policy shared by forward-write, undo and redo: force =
|
|
41
|
+
* last-writer-wins, reconciling = field-level merge. A non-conflict error is
|
|
42
|
+
* re-thrown untouched — this function only knows how to handle the specific
|
|
43
|
+
* failure of "the based-on version was superseded".
|
|
44
|
+
*/
|
|
45
|
+
export async function reconcileSourceModelWrite<TModel extends object>(
|
|
46
|
+
model: TModel,
|
|
47
|
+
version: number | undefined,
|
|
48
|
+
hooks: SourceModelWriteHooks<TModel>
|
|
49
|
+
): Promise<void> {
|
|
50
|
+
try {
|
|
51
|
+
await hooks.persist(model, version);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
if (!isConflictError(err)) {
|
|
54
|
+
throw err;
|
|
55
|
+
}
|
|
56
|
+
const outcome = await hooks.conflictResolver.resolve(hooks.baseline ?? model, model, () => hooks.refetch());
|
|
57
|
+
switch (outcome.status) {
|
|
58
|
+
case 'merged':
|
|
59
|
+
await hooks.persist(outcome.merged);
|
|
60
|
+
return;
|
|
61
|
+
case 'no-op':
|
|
62
|
+
hooks.logger.debug(`updateSourceModel no-op (v${err.expected} → v${err.actual}); already in sync`);
|
|
63
|
+
return;
|
|
64
|
+
case 'conflict':
|
|
65
|
+
hooks.logger.warn(
|
|
66
|
+
`updateSourceModel conflict (v${err.expected} → v${err.actual}); dropping the diagram edit — ` +
|
|
67
|
+
'a foreign writer changed the same field'
|
|
68
|
+
);
|
|
69
|
+
hooks.onConflictDropped();
|
|
70
|
+
return;
|
|
71
|
+
case 'unavailable':
|
|
72
|
+
hooks.logger.warn(`updateSourceModel refetch unavailable (v${err.expected} → v${err.actual}); forcing without version`);
|
|
73
|
+
await hooks.persist(model);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|