@hydranium/glsp-server 1.0.0-next.31 → 1.0.0-next.315
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 +80 -88
- package/lib/browser/start-glsp-server-in-worker.d.ts +43 -42
- package/lib/browser/start-glsp-server-in-worker.d.ts.map +1 -1
- package/lib/browser/start-glsp-server-in-worker.js +62 -13
- package/lib/browser/start-glsp-server-in-worker.js.map +1 -1
- package/lib/command/diagram-edit-dropped-error.d.ts +31 -0
- package/lib/command/diagram-edit-dropped-error.d.ts.map +1 -0
- package/lib/command/diagram-edit-dropped-error.js +48 -0
- package/lib/command/diagram-edit-dropped-error.js.map +1 -0
- package/lib/command/hydranium-glsp-command-stack.d.ts +33 -0
- package/lib/command/hydranium-glsp-command-stack.d.ts.map +1 -0
- package/lib/command/hydranium-glsp-command-stack.js +63 -0
- package/lib/command/hydranium-glsp-command-stack.js.map +1 -0
- package/lib/command/hydranium-glsp-operation-action-handler.d.ts +64 -0
- package/lib/command/hydranium-glsp-operation-action-handler.d.ts.map +1 -0
- package/lib/command/hydranium-glsp-operation-action-handler.js +142 -0
- package/lib/command/hydranium-glsp-operation-action-handler.js.map +1 -0
- package/lib/command/hydranium-glsp-operation-command.d.ts +250 -0
- package/lib/command/hydranium-glsp-operation-command.d.ts.map +1 -0
- package/lib/command/hydranium-glsp-operation-command.js +491 -0
- package/lib/command/hydranium-glsp-operation-command.js.map +1 -0
- package/lib/command/hydranium-glsp-recording-command.d.ts +26 -87
- package/lib/command/hydranium-glsp-recording-command.d.ts.map +1 -1
- package/lib/command/hydranium-glsp-recording-command.js +40 -117
- package/lib/command/hydranium-glsp-recording-command.js.map +1 -1
- package/lib/command/hydranium-glsp-undo-redo-action-handler.d.ts +23 -0
- package/lib/command/hydranium-glsp-undo-redo-action-handler.d.ts.map +1 -0
- package/lib/command/hydranium-glsp-undo-redo-action-handler.js +41 -0
- package/lib/command/hydranium-glsp-undo-redo-action-handler.js.map +1 -0
- package/lib/command/index.d.ts +5 -0
- package/lib/command/index.d.ts.map +1 -1
- package/lib/command/index.js +5 -0
- package/lib/command/index.js.map +1 -1
- package/lib/computed-bounds/hydranium-glsp-computed-bounds-action-handler.d.ts +1 -1
- package/lib/computed-bounds/hydranium-glsp-computed-bounds-action-handler.d.ts.map +1 -1
- package/lib/computed-bounds/hydranium-glsp-computed-bounds-action-handler.js +2 -2
- package/lib/computed-bounds/hydranium-glsp-computed-bounds-action-handler.js.map +1 -1
- package/lib/dispatcher/server-action-dispatcher.d.ts +25 -24
- package/lib/dispatcher/server-action-dispatcher.d.ts.map +1 -1
- package/lib/dispatcher/server-action-dispatcher.js +38 -26
- package/lib/dispatcher/server-action-dispatcher.js.map +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/launcher/abstract-hydranium-glsp-diagram-module.d.ts +35 -59
- package/lib/launcher/abstract-hydranium-glsp-diagram-module.d.ts.map +1 -1
- package/lib/launcher/abstract-hydranium-glsp-diagram-module.js +60 -71
- package/lib/launcher/abstract-hydranium-glsp-diagram-module.js.map +1 -1
- package/lib/launcher/drop-notifications-to-gone-client.d.ts +23 -0
- package/lib/launcher/drop-notifications-to-gone-client.d.ts.map +1 -0
- package/lib/launcher/drop-notifications-to-gone-client.js +43 -0
- package/lib/launcher/drop-notifications-to-gone-client.js.map +1 -0
- package/lib/launcher/glsp-app-module.d.ts +1 -2
- package/lib/launcher/glsp-app-module.d.ts.map +1 -1
- package/lib/launcher/glsp-app-module.js.map +1 -1
- package/lib/launcher/glsp-connection-logger.d.ts +19 -0
- package/lib/launcher/glsp-connection-logger.d.ts.map +1 -0
- package/lib/launcher/glsp-connection-logger.js +24 -0
- package/lib/launcher/glsp-connection-logger.js.map +1 -0
- 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 +60 -15
- package/lib/launcher/hydranium-glsp-server.d.ts.map +1 -1
- package/lib/launcher/hydranium-glsp-server.js +145 -22
- package/lib/launcher/hydranium-glsp-server.js.map +1 -1
- package/lib/launcher/index.d.ts +2 -0
- package/lib/launcher/index.d.ts.map +1 -1
- package/lib/launcher/index.js +5 -4
- package/lib/launcher/index.js.map +1 -1
- package/lib/logging/glsp-client-logger.d.ts +10 -6
- package/lib/logging/glsp-client-logger.d.ts.map +1 -1
- package/lib/logging/glsp-client-logger.js +14 -7
- package/lib/logging/glsp-client-logger.js.map +1 -1
- package/lib/messages/index.d.ts +11 -6
- package/lib/messages/index.d.ts.map +1 -1
- package/lib/messages/index.js +11 -6
- package/lib/messages/index.js.map +1 -1
- package/lib/node/start-glsp-server.d.ts +50 -15
- package/lib/node/start-glsp-server.d.ts.map +1 -1
- package/lib/node/start-glsp-server.js +97 -30
- package/lib/node/start-glsp-server.js.map +1 -1
- package/lib/state/abstract-hydranium-glsp-state.d.ts +326 -102
- package/lib/state/abstract-hydranium-glsp-state.d.ts.map +1 -1
- package/lib/state/abstract-hydranium-glsp-state.js +529 -139
- package/lib/state/abstract-hydranium-glsp-state.js.map +1 -1
- package/lib/state/diagram-status.d.ts +38 -0
- package/lib/state/diagram-status.d.ts.map +1 -0
- package/lib/state/diagram-status.js +27 -0
- package/lib/state/diagram-status.js.map +1 -0
- package/lib/state/full-text-hydranium-glsp-state.d.ts +22 -9
- package/lib/state/full-text-hydranium-glsp-state.d.ts.map +1 -1
- package/lib/state/full-text-hydranium-glsp-state.js +45 -18
- package/lib/state/full-text-hydranium-glsp-state.js.map +1 -1
- package/lib/state/hydranium-glsp-index.d.ts +40 -29
- package/lib/state/hydranium-glsp-index.d.ts.map +1 -1
- package/lib/state/hydranium-glsp-index.js +82 -45
- package/lib/state/hydranium-glsp-index.js.map +1 -1
- package/lib/state/hydranium-shared-core-services.d.ts +16 -29
- package/lib/state/hydranium-shared-core-services.d.ts.map +1 -1
- package/lib/state/hydranium-shared-core-services.js +15 -28
- package/lib/state/hydranium-shared-core-services.js.map +1 -1
- package/lib/state/index.d.ts +1 -0
- package/lib/state/index.d.ts.map +1 -1
- package/lib/state/index.js +1 -0
- package/lib/state/index.js.map +1 -1
- package/lib/state/model-ready-timeout-error.d.ts.map +1 -1
- package/lib/state/model-ready-timeout-error.js.map +1 -1
- package/lib/state/reconcile-source-model-write.d.ts +13 -27
- package/lib/state/reconcile-source-model-write.d.ts.map +1 -1
- package/lib/state/reconcile-source-model-write.js +44 -32
- package/lib/state/reconcile-source-model-write.js.map +1 -1
- package/lib/state/reconciling-multi-document-glsp-state.d.ts +125 -69
- package/lib/state/reconciling-multi-document-glsp-state.d.ts.map +1 -1
- package/lib/state/reconciling-multi-document-glsp-state.js +242 -100
- package/lib/state/reconciling-multi-document-glsp-state.js.map +1 -1
- package/lib/state/reconciling-transfer-hydranium-glsp-state.d.ts +36 -28
- package/lib/state/reconciling-transfer-hydranium-glsp-state.d.ts.map +1 -1
- package/lib/state/reconciling-transfer-hydranium-glsp-state.js +59 -42
- package/lib/state/reconciling-transfer-hydranium-glsp-state.js.map +1 -1
- package/lib/status/diagram-status-reporter.d.ts +44 -0
- package/lib/status/diagram-status-reporter.d.ts.map +1 -0
- package/lib/status/diagram-status-reporter.js +88 -0
- package/lib/status/diagram-status-reporter.js.map +1 -0
- package/lib/status/hydranium-glsp-request-model-action-handler.d.ts +27 -0
- package/lib/status/hydranium-glsp-request-model-action-handler.d.ts.map +1 -0
- package/lib/status/hydranium-glsp-request-model-action-handler.js +52 -0
- package/lib/status/hydranium-glsp-request-model-action-handler.js.map +1 -0
- package/lib/status/hydranium-glsp-set-edit-mode-action-handler.d.ts +27 -0
- package/lib/status/hydranium-glsp-set-edit-mode-action-handler.d.ts.map +1 -0
- package/lib/status/hydranium-glsp-set-edit-mode-action-handler.js +53 -0
- package/lib/status/hydranium-glsp-set-edit-mode-action-handler.js.map +1 -0
- package/lib/status/index.d.ts +12 -0
- package/lib/status/index.d.ts.map +1 -0
- package/lib/status/index.js +12 -0
- package/lib/status/index.js.map +1 -0
- 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 +324 -143
- package/lib/storage/hydranium-glsp-storage.d.ts.map +1 -1
- package/lib/storage/hydranium-glsp-storage.js +569 -213
- 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 +51 -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/submission/hydranium-glsp-submission-handler.d.ts +44 -4
- package/lib/submission/hydranium-glsp-submission-handler.d.ts.map +1 -1
- package/lib/submission/hydranium-glsp-submission-handler.js +82 -6
- package/lib/submission/hydranium-glsp-submission-handler.js.map +1 -1
- package/lib/testing/glsp-harness.d.ts +34 -7
- package/lib/testing/glsp-harness.d.ts.map +1 -1
- package/lib/testing/glsp-harness.js +47 -19
- package/lib/testing/glsp-harness.js.map +1 -1
- package/lib/validation/diagnostic-markers.d.ts +3 -3
- package/lib/validation/diagnostic-markers.d.ts.map +1 -1
- package/lib/validation/diagnostic-markers.js +1 -1
- package/lib/validation/diagnostic-markers.js.map +1 -1
- package/lib/validation/hydranium-glsp-model-validator.d.ts +1 -2
- 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 +31 -52
- package/src/browser/start-glsp-server-in-worker.ts +79 -51
- package/src/command/diagram-edit-dropped-error.ts +51 -0
- package/src/command/hydranium-glsp-command-stack.ts +47 -0
- package/src/command/hydranium-glsp-operation-action-handler.ts +141 -0
- package/src/command/hydranium-glsp-operation-command.ts +602 -0
- package/src/command/hydranium-glsp-recording-command.ts +40 -121
- package/src/command/hydranium-glsp-undo-redo-action-handler.ts +29 -0
- package/src/command/index.ts +10 -0
- package/src/computed-bounds/hydranium-glsp-computed-bounds-action-handler.ts +2 -2
- package/src/dispatcher/server-action-dispatcher.ts +39 -31
- package/src/index.ts +1 -0
- package/src/launcher/abstract-hydranium-glsp-diagram-module.ts +79 -91
- package/src/launcher/drop-notifications-to-gone-client.ts +47 -0
- package/src/launcher/glsp-app-module.ts +1 -2
- package/src/launcher/glsp-connection-logger.ts +27 -0
- package/src/launcher/glsp-server-overrides.ts +27 -11
- package/src/launcher/hydranium-glsp-server.ts +153 -23
- package/src/launcher/index.ts +5 -4
- package/src/logging/glsp-client-logger.ts +16 -9
- package/src/messages/index.ts +16 -6
- package/src/node/start-glsp-server.ts +133 -40
- package/src/state/abstract-hydranium-glsp-state.ts +609 -154
- package/src/state/diagram-status.ts +41 -0
- package/src/state/full-text-hydranium-glsp-state.ts +46 -18
- package/src/state/hydranium-glsp-index.ts +85 -41
- package/src/state/hydranium-shared-core-services.ts +16 -29
- package/src/state/index.ts +1 -0
- package/src/state/reconcile-source-model-write.ts +51 -53
- package/src/state/reconciling-multi-document-glsp-state.ts +276 -102
- package/src/state/reconciling-transfer-hydranium-glsp-state.ts +67 -45
- package/src/status/diagram-status-reporter.ts +106 -0
- package/src/status/hydranium-glsp-request-model-action-handler.ts +43 -0
- package/src/status/hydranium-glsp-set-edit-mode-action-handler.ts +38 -0
- package/src/status/index.ts +12 -0
- package/src/storage/hydranium-glsp-request-save-model-action-handler.ts +40 -0
- package/src/storage/hydranium-glsp-storage.ts +647 -223
- package/src/storage/index.ts +2 -1
- package/src/storage/save-delivery-policy.ts +49 -0
- package/src/submission/hydranium-glsp-submission-handler.ts +91 -5
- package/src/testing/glsp-harness.ts +67 -25
- package/src/validation/diagnostic-markers.ts +3 -3
- package/src/validation/hydranium-glsp-model-validator.ts +4 -5
- 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
|
@@ -15,35 +15,65 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
15
15
|
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
16
16
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
17
17
|
};
|
|
18
|
-
import { MarkersReason, SetMarkersAction } from '@eclipse-glsp/
|
|
19
|
-
import {
|
|
20
|
-
import { Debouncer, defineMessage, DisposableCollection } from '@hydranium/protocol';
|
|
18
|
+
import { ActionDispatcher, ClientSessionManager, CommandStack, GLSPServerError, Logger as GlspLogger, MarkersReason, MessageAction, ModelState, ModelSubmissionHandler, ModelValidator, SOURCE_URI_ARG, SetDirtyStateAction, SetMarkersAction, TEMPORARY_CLIENT_ID } from '@eclipse-glsp/server';
|
|
19
|
+
import { Debouncer, defineMessage, DisposableCollection, isDuplicateClientIdError, isSessionClosedError, RESUME_TOKEN_ARG, SessionClosedError, describeError, describeErrorWithStack } from '@hydranium/protocol';
|
|
21
20
|
import { inject, injectable, optional, postConstruct } from 'inversify';
|
|
22
|
-
import { URI } from '@hydranium/langium';
|
|
23
|
-
import { AstDocument } from '@hydranium/core';
|
|
21
|
+
import { URI, UriUtils } from '@hydranium/langium';
|
|
24
22
|
import { DiagnosticSeverity } from 'vscode-languageserver-types';
|
|
23
|
+
import { DiagramStatus } from '../state/diagram-status.js';
|
|
25
24
|
import { HydraniumTypes } from '../state/hydranium-shared-core-services.js';
|
|
26
|
-
import {
|
|
25
|
+
import { DEFAULT_SAVE_DELIVERY_POLICY, SaveDeliveryPolicy } from './save-delivery-policy.js';
|
|
27
26
|
/**
|
|
28
|
-
* A save action arrived with nowhere to write to.
|
|
27
|
+
* A save action arrived with nowhere to write to. A save action carries no
|
|
28
|
+
* request id, so it falls through to the error handler and `message` becomes
|
|
29
|
+
* the toast text.
|
|
29
30
|
*
|
|
30
|
-
*
|
|
31
|
-
* action
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* `cause?.toString?.()`, so a code there would sit inside human text.
|
|
31
|
+
* Rendered at the raise site rather than by a carrier method, because GLSP's
|
|
32
|
+
* action protocol has no slot for an identity anywhere — every member of its
|
|
33
|
+
* message, status and reject actions is prose or an enum, and
|
|
34
|
+
* `MessageAction.details` is not a substitute since it is prose populated from
|
|
35
|
+
* `cause?.toString?.()`, and a Theia host shows it to a user on demand. So the
|
|
36
|
+
* throw is the last place that still knows which message this is.
|
|
37
37
|
*/
|
|
38
38
|
export const SAVE_TARGET_UNKNOWN = defineMessage('hydranium/glsp-server/save-target-unknown', 'Could not determine where to save this model');
|
|
39
39
|
/**
|
|
40
40
|
* A model request arrived without the source URI it is required to carry.
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* protocol has no slot for an identity, so the English is what a client
|
|
44
|
-
* receives.
|
|
41
|
+
* Rendered at the raise site, like its save-path sibling and for the same
|
|
42
|
+
* reason.
|
|
45
43
|
*/
|
|
46
44
|
export const SOURCE_URI_MISSING = defineMessage('hydranium/glsp-server/source-uri-missing', 'Could not open this model: the request did not say which document to load');
|
|
45
|
+
/**
|
|
46
|
+
* Why the canvas has stopped accepting edits.
|
|
47
|
+
*
|
|
48
|
+
* **A READONLY canvas is otherwise indistinguishable from a broken one.** The
|
|
49
|
+
* client's answer to a read-only edit mode is to withdraw the tool palette,
|
|
50
|
+
* so the surface a reader was working in silently loses the only control it had,
|
|
51
|
+
* with the cause — a syntax error in a document that may not even be open —
|
|
52
|
+
* nowhere on screen. The mode flip is the mechanism; this is the only part of it
|
|
53
|
+
* a user can see.
|
|
54
|
+
*
|
|
55
|
+
* Rendered at the raise site, like its two siblings above and for the same
|
|
56
|
+
* reason: every member of GLSP's status action is prose or an enum.
|
|
57
|
+
*
|
|
58
|
+
* It names the document and the recovery rather than the fault, because the
|
|
59
|
+
* fault already has a surface — the squiggle and the problems list, both of which
|
|
60
|
+
* say WHICH character — and repeating it here would put a second, less precise
|
|
61
|
+
* account of the same error on screen. What no other surface says is that the
|
|
62
|
+
* diagram is waiting on it, and the document it waits on need not be the one
|
|
63
|
+
* the diagram was opened on.
|
|
64
|
+
*/
|
|
65
|
+
export const DIAGRAM_READONLY_PARSE_ERROR = defineMessage('hydranium/glsp-server/diagram-readonly-parse-error', 'Read-only: {document} has a syntax error. Fix it to edit the diagram again.');
|
|
66
|
+
/**
|
|
67
|
+
* The diagram's GLSP client id is still live as another participant's client
|
|
68
|
+
* id once the load has waited for it to free, so the diagram does not load.
|
|
69
|
+
*
|
|
70
|
+
* Rendered at the raise site, as {@link SAVE_TARGET_UNKNOWN} is and for the
|
|
71
|
+
* same reason, and dispatched to the client as a message of its own: a model
|
|
72
|
+
* request fails as a rejection whose detail is the English cause, which no user
|
|
73
|
+
* sees. It names the recovery: a host that numbers its diagram clients gives a
|
|
74
|
+
* reopened diagram a fresh id.
|
|
75
|
+
*/
|
|
76
|
+
export const DIAGRAM_SESSION_REFUSED = defineMessage('hydranium/glsp-server/diagram-session-refused', 'Could not open this diagram: its identifier is still in use by another editor. Close the diagram and open it again.');
|
|
47
77
|
/** Window (ms) over which back-to-back external rebuilds collapse into one resubmit. */
|
|
48
78
|
const EXTERNAL_SUBMIT_DEBOUNCE_MS = 250;
|
|
49
79
|
/**
|
|
@@ -69,6 +99,13 @@ function isStructuralDiagnostic(diagnostic) {
|
|
|
69
99
|
const code = candidate.data?.code;
|
|
70
100
|
return candidate.severity === DiagnosticSeverity.Error && (code === 'lexing-error' || code === 'parsing-error');
|
|
71
101
|
}
|
|
102
|
+
/** Whether `parseResult` holds what Langium's validator reports as an error-severity `lexing-error` or `parsing-error`. */
|
|
103
|
+
function hasStructuralErrors(parseResult) {
|
|
104
|
+
const lexing = parseResult.lexerReport?.diagnostics ?? [];
|
|
105
|
+
return (parseResult.parserErrors.length > 0 ||
|
|
106
|
+
parseResult.lexerErrors.length > 0 ||
|
|
107
|
+
lexing.some(diagnostic => (diagnostic.severity ?? 'error') === 'error'));
|
|
108
|
+
}
|
|
72
109
|
/**
|
|
73
110
|
* GLSP source-model storage base shared by all hydranium adopters.
|
|
74
111
|
* Provides the DI signature, the {@link ClientSessionListener} +
|
|
@@ -79,27 +116,37 @@ function isStructuralDiagnostic(diagnostic) {
|
|
|
79
116
|
* invariant.
|
|
80
117
|
*
|
|
81
118
|
* **The settled-root invariant.** Every {@link AbstractHydraniumGlspState.setSourceRoot}
|
|
82
|
-
* the
|
|
119
|
+
* the storage performs captures a root that has reached
|
|
83
120
|
* `IntegrityService.SettledState` (post-`Linked`, references indexed, on-build
|
|
84
|
-
* integrity rules applied). The
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
* document
|
|
121
|
+
* integrity rules applied). The end of an operation captures the root its
|
|
122
|
+
* write produced, settled or not, and the operation's own submit waits for the
|
|
123
|
+
* settled one through `ready()`. The GModel factory therefore always walks a
|
|
124
|
+
* fully linked + reprojected AST. The resubmit path re-`settled()`s rather
|
|
125
|
+
* than keeping the `onModelUpdated` event's document: the event's document
|
|
126
|
+
* holds only while its listener runs, and the resubmit runs after a debounce,
|
|
127
|
+
* by which time another build can have reset it.
|
|
89
128
|
*
|
|
90
129
|
* Adopters with richer needs override the seams ({@link isStructurallyBroken},
|
|
91
|
-
* {@link
|
|
92
|
-
* whole flow, and select a {@link
|
|
93
|
-
* tune how {@link saveSourceModel}
|
|
130
|
+
* {@link parseErrorStatus}, {@link onSourceModelSettled}) rather than the
|
|
131
|
+
* whole flow, and select a {@link SaveDeliveryPolicy} via the bound option to
|
|
132
|
+
* tune how {@link saveSourceModel} delivers its result.
|
|
94
133
|
*
|
|
95
|
-
* **
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
* {@link
|
|
101
|
-
*
|
|
102
|
-
*
|
|
134
|
+
* **One client session per GLSP client session.** The load registers the GLSP
|
|
135
|
+
* client id as a client session ({@link registerModelSession}), with the
|
|
136
|
+
* request's {@link RESUME_TOKEN_ARG}, and hands it to the state as
|
|
137
|
+
* `modelSession`. The primary is opened through it, and
|
|
138
|
+
* every document that joins the write set as it joins
|
|
139
|
+
* ({@link openSecondary}); a document that leaves the write set stays open
|
|
140
|
+
* until the next save, so leaving never reverts the diagram's unsaved edits to
|
|
141
|
+
* it. Disposing the storage ends the session, which closes everything it has
|
|
142
|
+
* open.
|
|
143
|
+
*
|
|
144
|
+
* **Default `saveSourceModel` flow.** Flushes the stored text of every
|
|
145
|
+
* document the diagram's session has open through the session's `persist`, with
|
|
146
|
+
* no serializer in the path — the update path already put the settled text in
|
|
147
|
+
* the store. The bound {@link SaveDeliveryPolicy} (default
|
|
148
|
+
* {@link DEFAULT_SAVE_DELIVERY_POLICY}, `await`) decides
|
|
149
|
+
* await-vs-fire-and-forget and failure handling.
|
|
103
150
|
*
|
|
104
151
|
* **tempId filter.** GLSP's `DefaultGlobalActionProvider` spins up a
|
|
105
152
|
* throwaway per-diagram-type container with upstream's {@link TEMPORARY_CLIENT_ID}
|
|
@@ -110,9 +157,12 @@ function isStructuralDiagnostic(diagnostic) {
|
|
|
110
157
|
* **Lifecycle ordering.** `init` runs after DI resolution and parks the
|
|
111
158
|
* secondary-write-set watch, which outlives any single load.
|
|
112
159
|
* {@link sessionDisposed} calls {@link dispose}, which drains
|
|
113
|
-
* {@link toDispose} idempotently
|
|
114
|
-
*
|
|
115
|
-
*
|
|
160
|
+
* {@link toDispose} idempotently and then ends the client session. The
|
|
161
|
+
* framework's GLSP server disposes the storage as lost first when it shuts
|
|
162
|
+
* down, which is how the client's connection ending reaches it, so the
|
|
163
|
+
* diagram's unsaved text waits out the release grace. Every
|
|
164
|
+
* transient subscription created in {@link doLoadSourceModel} is parked on
|
|
165
|
+
* {@link toDispose} so the drain catches them on client-detach.
|
|
116
166
|
*/
|
|
117
167
|
let HydraniumGlspStorage = class HydraniumGlspStorage {
|
|
118
168
|
logger;
|
|
@@ -131,14 +181,13 @@ let HydraniumGlspStorage = class HydraniumGlspStorage {
|
|
|
131
181
|
*/
|
|
132
182
|
modelValidator;
|
|
133
183
|
/**
|
|
134
|
-
* Selected {@link
|
|
135
|
-
* {@link
|
|
136
|
-
* left unbound it resolves to {@link
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
* `@optional()` member.
|
|
184
|
+
* Selected {@link SaveDeliveryPolicy} for {@link saveSourceModel}. Bind the
|
|
185
|
+
* {@link SaveDeliveryPolicy} token in a `DiagramModule` to choose a policy;
|
|
186
|
+
* left unbound it resolves to {@link DEFAULT_SAVE_DELIVERY_POLICY}. Read via
|
|
187
|
+
* {@link saveDeliveryPolicy} so the fallback is applied even when inversify
|
|
188
|
+
* injects `undefined` for an unbound `@optional()` member.
|
|
140
189
|
*/
|
|
141
|
-
|
|
190
|
+
boundSaveDeliveryPolicy;
|
|
142
191
|
/** Disposables created during {@link doLoadSourceModel}; drained on session disposal. */
|
|
143
192
|
toDispose = new DisposableCollection();
|
|
144
193
|
/**
|
|
@@ -151,13 +200,18 @@ let HydraniumGlspStorage = class HydraniumGlspStorage {
|
|
|
151
200
|
*/
|
|
152
201
|
secondarySubscriptions = new Map();
|
|
153
202
|
/**
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
* only place that sees operation submits too.
|
|
203
|
+
* Documents that left the write set since the last save. They stay open in
|
|
204
|
+
* the diagram's session until that save has persisted them, and are closed
|
|
205
|
+
* after it: closing a document as it leaves would revert the diagram's
|
|
206
|
+
* unsaved edits to it where the diagram was its last client.
|
|
159
207
|
*/
|
|
160
|
-
|
|
208
|
+
departedSecondaries = new Set();
|
|
209
|
+
/** How long a load without a resume token waits for a held client id to free; see {@link loadModelSession}. */
|
|
210
|
+
sessionWaitMs = 2_000;
|
|
211
|
+
/** Set by {@link dispose}; a disposed storage registers no session again. */
|
|
212
|
+
disposed = false;
|
|
213
|
+
/** Saves of the diagram's own that GLSP's save handler awaits; see {@link handleDirtyChanged}. */
|
|
214
|
+
ownSavesPending = 0;
|
|
161
215
|
/**
|
|
162
216
|
* Trailing-edge debounce for the external resubmit. Rescheduled on each
|
|
163
217
|
* {@link handleModelUpdated} so back-to-back rebuilds (cascade relinks,
|
|
@@ -166,8 +220,6 @@ let HydraniumGlspStorage = class HydraniumGlspStorage {
|
|
|
166
220
|
* hence its `Clock` — is available by then); disposed on session teardown.
|
|
167
221
|
*/
|
|
168
222
|
resubmitDebouncer;
|
|
169
|
-
/** Latest event document awaiting the debounced resubmit; read by {@link flushResubmit} (last write wins). */
|
|
170
|
-
pendingResubmitDocument;
|
|
171
223
|
init() {
|
|
172
224
|
this.resubmitDebouncer = new Debouncer(this.sharedServices.Clock, () => this.flushResubmit(), {
|
|
173
225
|
delayMs: EXTERNAL_SUBMIT_DEBOUNCE_MS
|
|
@@ -182,6 +234,101 @@ let HydraniumGlspStorage = class HydraniumGlspStorage {
|
|
|
182
234
|
// run long after any load.
|
|
183
235
|
this.toDispose.push(this.state.onSecondaryUrisChanged(() => this.reconcileSecondarySubscriptions()));
|
|
184
236
|
}
|
|
237
|
+
/**
|
|
238
|
+
* Register the diagram's client session under the GLSP client id, or
|
|
239
|
+
* `undefined` when another participant holds that id, which is logged, and
|
|
240
|
+
* the diagram then refuses to load ({@link requireModelSession}). Taking
|
|
241
|
+
* the id over would end that participant's session under it, and working
|
|
242
|
+
* without a session would share its opens and echoes, so its close would
|
|
243
|
+
* take the diagram's documents with it.
|
|
244
|
+
*
|
|
245
|
+
* The exception is a `resumeToken` the holder registered with, which takes
|
|
246
|
+
* its session over: a diagram that reconnected or reloaded, whose old
|
|
247
|
+
* connection the server has not seen close yet.
|
|
248
|
+
*
|
|
249
|
+
* A client id the framework reserves throws `ReservedClientIdError`:
|
|
250
|
+
* whoever chose the GLSP client id has a bug, and no later registration can
|
|
251
|
+
* succeed.
|
|
252
|
+
*/
|
|
253
|
+
registerModelSession(resumeToken) {
|
|
254
|
+
try {
|
|
255
|
+
return this.sharedServices.model.ModelService.createSession('diagram', this.state.clientId, { resumeToken });
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
if (!isDuplicateClientIdError(error)) {
|
|
259
|
+
throw error;
|
|
260
|
+
}
|
|
261
|
+
// Routine while a reconnecting diagram's old session has not closed yet.
|
|
262
|
+
this.logger.debug(`Client id ${this.state.clientId} is held by another participant: this diagram has no session yet`);
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* The diagram's client session. Throws `SessionClosedError` once the
|
|
268
|
+
* storage is disposed, and the refusal before a load has registered one.
|
|
269
|
+
* Registering here instead would start the session without the load's
|
|
270
|
+
* resume token, which a later resume then fails to match.
|
|
271
|
+
*/
|
|
272
|
+
requireModelSession() {
|
|
273
|
+
if (this.disposed) {
|
|
274
|
+
throw new SessionClosedError(this.state.clientId, 'The diagram has closed; its client session has ended.');
|
|
275
|
+
}
|
|
276
|
+
const session = this.state.modelSession;
|
|
277
|
+
if (!session) {
|
|
278
|
+
throw new GLSPServerError(this.sharedServices.MessageRenderer.renderMessage(DIAGRAM_SESSION_REFUSED), `clientId=${this.state.clientId} has no client session: no load has registered it, or another participant holds the id`);
|
|
279
|
+
}
|
|
280
|
+
return session;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* {@link requireModelSession} for the load, registering with the request's
|
|
284
|
+
* resume token, and telling the user when the id is refused. A load with
|
|
285
|
+
* no token waits up to {@link sessionWaitMs} for a held id to free: a
|
|
286
|
+
* client that resumes no session reconnects under its old id before the
|
|
287
|
+
* server has seen the old connection close.
|
|
288
|
+
*/
|
|
289
|
+
async loadModelSession(action) {
|
|
290
|
+
const option = action.options?.[RESUME_TOKEN_ARG];
|
|
291
|
+
const token = typeof option === 'string' ? option : undefined;
|
|
292
|
+
if (!this.state.modelSession && !this.disposed) {
|
|
293
|
+
this.state.modelSession = this.registerModelSession(token);
|
|
294
|
+
}
|
|
295
|
+
if (!this.state.modelSession && !this.disposed && token === undefined) {
|
|
296
|
+
await this.waitForSessionEnd(this.state.clientId, this.sessionWaitMs);
|
|
297
|
+
if (!this.state.modelSession && !this.disposed) {
|
|
298
|
+
this.state.modelSession = this.registerModelSession();
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
try {
|
|
302
|
+
return this.requireModelSession();
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
// A diagram closed meanwhile has no client left to tell.
|
|
306
|
+
if (isSessionClosedError(error)) {
|
|
307
|
+
throw error;
|
|
308
|
+
}
|
|
309
|
+
// The error names the held id, and whoever ends the request logs it.
|
|
310
|
+
this.actionDispatcher
|
|
311
|
+
.dispatch(MessageAction.create(this.sharedServices.MessageRenderer.renderMessage(DIAGRAM_SESSION_REFUSED), { severity: 'ERROR' }))
|
|
312
|
+
.catch((dispatchError) => this.logger.warn(`Could not tell the client the diagram was refused: ${describeErrorWithStack(dispatchError)}`));
|
|
313
|
+
throw error;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
/** Resolve once the client session under `clientId` ends, or after `timeoutMs`. */
|
|
317
|
+
waitForSessionEnd(clientId, timeoutMs) {
|
|
318
|
+
return new Promise(resolve => {
|
|
319
|
+
const subscriptions = new DisposableCollection();
|
|
320
|
+
const done = () => {
|
|
321
|
+
subscriptions.dispose();
|
|
322
|
+
resolve();
|
|
323
|
+
};
|
|
324
|
+
subscriptions.push(this.sharedServices.Clock.setTimer(done, timeoutMs));
|
|
325
|
+
// A holder this service did not start is waited out by the timer.
|
|
326
|
+
const holder = this.sharedServices.model.ModelService.getSession(clientId);
|
|
327
|
+
if (holder) {
|
|
328
|
+
subscriptions.push(holder.onDidDispose(done));
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
}
|
|
185
332
|
/**
|
|
186
333
|
* Default load flow — {@link doLoadSourceModel}. Adopters whose load flow
|
|
187
334
|
* needs more than the seams allow override this method.
|
|
@@ -195,37 +342,44 @@ let HydraniumGlspStorage = class HydraniumGlspStorage {
|
|
|
195
342
|
const modelService = this.sharedServices.model.ModelService;
|
|
196
343
|
// Open FIRST: a workspace-scanned-but-never-didOpened document rebuilds here,
|
|
197
344
|
// so the capture below settles on a built root rather than a transient
|
|
198
|
-
// mid-rebuild one.
|
|
199
|
-
|
|
345
|
+
// mid-rebuild one. Closed when the session ends.
|
|
346
|
+
await (await this.loadModelSession(action)).open(rootUri);
|
|
200
347
|
// GLSP's sessionDisposed is unreliable on Theia tab-close; dispose on client
|
|
201
348
|
// detach so reopens don't accumulate stale onModelUpdated listeners.
|
|
202
|
-
this.toDispose.push(modelService.onClientClosed(
|
|
203
|
-
this.logger.
|
|
349
|
+
this.toDispose.push(modelService.onClientClosed(() => {
|
|
350
|
+
this.logger.debug(`Client detached (${this.state.clientId}) — disposing storage subscriptions for ${rootUri}`);
|
|
204
351
|
this.dispose();
|
|
205
|
-
}));
|
|
352
|
+
}, { uri: rootUri, clientId: this.state.clientId }));
|
|
206
353
|
// React to external rebuilds: settle-gated capture + debounced/deduped resubmit.
|
|
207
|
-
this.toDispose.push(modelService.onModelUpdated(
|
|
208
|
-
//
|
|
209
|
-
this.toDispose.push(modelService.onModelSaved(
|
|
354
|
+
this.toDispose.push(modelService.onModelUpdated(event => this.handleModelUpdated(rootUri, event), { uri: rootUri }));
|
|
355
|
+
// GLSP's own command stack counts commands; tell it of another client's save.
|
|
356
|
+
this.toDispose.push(modelService.onModelSaved(event => this.handleModelSaved(event), { uri: rootUri }));
|
|
357
|
+
// Tell the client of each dirty flip, one the diagram did not cause included.
|
|
358
|
+
this.toDispose.push(modelService.onDirtyChanged(event => this.handleDirtyChanged(event)));
|
|
210
359
|
// Capture the initial settled root.
|
|
211
360
|
const document = await modelService.settled(rootUri);
|
|
212
361
|
await this.captureSettledRoot(rootUri, document);
|
|
213
362
|
}
|
|
214
363
|
/**
|
|
215
|
-
* Capture the initial settled root and
|
|
216
|
-
*
|
|
217
|
-
* the first {@link handleModelUpdated} (at `Validated`, with diagnostics) flips
|
|
218
|
-
* it READONLY if the document has structural parse errors. Edit-mode +
|
|
219
|
-
* settle-hook actions are dispatched on a macrotask so the initial
|
|
364
|
+
* Capture the initial settled root and its parse-error status. Settle-hook
|
|
365
|
+
* actions are dispatched on a macrotask so the initial
|
|
220
366
|
* `requestModel → setModel` handshake isn't perturbed.
|
|
367
|
+
*
|
|
368
|
+
* The initial status is decided from the root's own parse (see
|
|
369
|
+
* {@link isStructurallyBroken}), not from diagnostics: `settled()` can hand
|
|
370
|
+
* back a document it drove to the integrity landmark, before validation,
|
|
371
|
+
* with no diagnostics yet.
|
|
221
372
|
*/
|
|
222
373
|
async captureSettledRoot(rootUri, document) {
|
|
223
|
-
this.state.setSourceRoot(rootUri, document.root);
|
|
224
|
-
|
|
374
|
+
await this.state.runExclusive(() => this.state.setSourceRoot(rootUri, document.root));
|
|
375
|
+
this.refreshParseErrorStatus(document);
|
|
376
|
+
const actions = this.onSourceModelSettled(document);
|
|
225
377
|
if (actions.length > 0) {
|
|
226
378
|
// Defer through the injectable Clock (not raw setTimeout) so the dispatch is
|
|
227
379
|
// testable and cancels on session disposal; parked on toDispose for that.
|
|
228
|
-
this.toDispose.push(this.sharedServices.Clock.setTimer(() => this.actionDispatcher
|
|
380
|
+
this.toDispose.push(this.sharedServices.Clock.setTimer(() => this.actionDispatcher
|
|
381
|
+
.dispatchAll(actions)
|
|
382
|
+
.catch((error) => this.logger.warn(`Could not send the settled model of ${rootUri}: ${describeErrorWithStack(error)}`)), 0));
|
|
229
383
|
}
|
|
230
384
|
}
|
|
231
385
|
/**
|
|
@@ -251,33 +405,50 @@ let HydraniumGlspStorage = class HydraniumGlspStorage {
|
|
|
251
405
|
if (!current.has(uri)) {
|
|
252
406
|
subscription.dispose();
|
|
253
407
|
this.secondarySubscriptions.delete(uri);
|
|
408
|
+
this.departedSecondaries.add(uri);
|
|
254
409
|
}
|
|
255
410
|
}
|
|
256
411
|
for (const uri of current) {
|
|
257
412
|
if (!this.secondarySubscriptions.has(uri)) {
|
|
258
|
-
this.
|
|
413
|
+
this.departedSecondaries.delete(uri);
|
|
414
|
+
this.secondarySubscriptions.set(uri, this.sharedServices.model.ModelService.onModelUpdated(event => this.handleSecondaryUpdated(uri, event), { uri }));
|
|
415
|
+
this.openSecondary(uri);
|
|
259
416
|
}
|
|
260
417
|
}
|
|
261
418
|
}
|
|
419
|
+
/**
|
|
420
|
+
* Open a document that joined the write set through the diagram's session,
|
|
421
|
+
* where it stays open until the diagram's next save after it leaves the set,
|
|
422
|
+
* or the diagram's end. A document that does not exist yet is left to the
|
|
423
|
+
* write that creates it, and a failed open is logged: the write that needs
|
|
424
|
+
* the document opens it again and fails there.
|
|
425
|
+
*/
|
|
426
|
+
openSecondary(uri) {
|
|
427
|
+
const session = this.state.modelSession;
|
|
428
|
+
if (!session || !this.sharedServices.model.ModelService.getDocument(uri)) {
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
session.open(uri).catch((error) => {
|
|
432
|
+
const detail = `Could not open ${uri} for ${this.state.clientId}: ${describeError(error)}`;
|
|
433
|
+
// A session ending while the open reads is the diagram closing.
|
|
434
|
+
if (isSessionClosedError(error)) {
|
|
435
|
+
this.logger.debug(detail);
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
this.logger.warn(detail);
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
}
|
|
262
442
|
/**
|
|
263
443
|
* React to a rebuild of a SECONDARY document by resubmitting the diagram, so
|
|
264
444
|
* an external edit to (say) a layout file reaches the canvas.
|
|
265
445
|
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
446
|
+
* Applies {@link handleModelUpdated}'s guard, and that is the load-bearing
|
|
447
|
+
* half rather than the resubmit: a diagram interaction that writes a
|
|
268
448
|
* secondary — a drag persisting bounds to a layout file — comes back through
|
|
269
|
-
* this listener
|
|
449
|
+
* this listener caused by the client's own write, and resubmitting on it
|
|
270
450
|
* fights the optimistic client-side move the user is still holding.
|
|
271
451
|
*
|
|
272
|
-
* Schedules the PRIMARY's current document, never the secondary's.
|
|
273
|
-
* {@link doUpdateAndSubmit} reads its event document only for the edit-mode
|
|
274
|
-
* and structurally-broken decisions, both of which are statements about the
|
|
275
|
-
* document the canvas edits: passing the secondary's would let a typo in a
|
|
276
|
-
* layout file flip the canvas READONLY and take the keep-the-last-valid-GModel
|
|
277
|
-
* branch off an unrelated document's parse state. The pending-document field
|
|
278
|
-
* is shared with the primary path, so it would poison a concurrently
|
|
279
|
-
* scheduled primary resubmit too.
|
|
280
|
-
*
|
|
281
452
|
* No marker refresh, unlike the primary path: a secondary's diagnostics reach
|
|
282
453
|
* the client only if an adopter's index registers elements as rendering that
|
|
283
454
|
* document, and refreshing here unconditionally would add a dispatch to every
|
|
@@ -288,44 +459,44 @@ let HydraniumGlspStorage = class HydraniumGlspStorage {
|
|
|
288
459
|
if (this.disposeIfStale(uri)) {
|
|
289
460
|
return;
|
|
290
461
|
}
|
|
291
|
-
if (this.state.clientId
|
|
292
|
-
return;
|
|
293
|
-
}
|
|
294
|
-
const primary = this.currentPrimaryDocument();
|
|
295
|
-
if (primary === undefined) {
|
|
462
|
+
if (event.causedBy === this.state.clientId || this.currentPrimaryDocument() === undefined) {
|
|
296
463
|
return;
|
|
297
464
|
}
|
|
298
|
-
this.logger.debug(`Secondary ${uri} rebuilt by ${event.
|
|
299
|
-
this.scheduleUpdateAndSubmit(
|
|
465
|
+
this.logger.debug(`Secondary ${uri} rebuilt, caused by ${event.causedBy ?? 'an unknown client'} (${event.reason}) — scheduling resubmit`);
|
|
466
|
+
this.scheduleUpdateAndSubmit();
|
|
300
467
|
}
|
|
301
468
|
/**
|
|
302
469
|
* The primary document's current state, or `undefined` when it is not
|
|
303
470
|
* registered.
|
|
304
471
|
*
|
|
305
|
-
* Synchronous by construction
|
|
306
|
-
*
|
|
307
|
-
* `settled()` is not an alternative: it strips diagnostics by phase contract,
|
|
308
|
-
* and diagnostics are the reason {@link doUpdateAndSubmit} takes a separate
|
|
309
|
-
* event document at all.
|
|
472
|
+
* Synchronous by construction, so the parse-error status reads the text as
|
|
473
|
+
* it stands rather than waiting for a build.
|
|
310
474
|
*/
|
|
311
475
|
currentPrimaryDocument() {
|
|
312
476
|
const document = this.sharedServices.model.ModelService.getDocument(this.state.sourceUri);
|
|
313
|
-
return document ?
|
|
477
|
+
return document ? this.sharedServices.workspace.AstDocumentManager.toAstDocument(document) : undefined;
|
|
314
478
|
}
|
|
315
479
|
/**
|
|
316
480
|
* React to an external rebuild reaching `Validated`. Self-cleans if the
|
|
317
481
|
* session has vanished but this listener leaked (neither `sessionDisposed`
|
|
318
|
-
* nor `onClientClosed` fired on tab close). Resubmits for any
|
|
319
|
-
*
|
|
320
|
-
*
|
|
321
|
-
*
|
|
482
|
+
* nor `onClientClosed` fired on tab close). Resubmits for any update this
|
|
483
|
+
* client's own write did not cause alone, then refreshes diagnostic markers
|
|
484
|
+
* for every update — including own writes, which skip the resubmit but can
|
|
485
|
+
* still change diagnostics. An event without `causedBy` is resubmitted.
|
|
486
|
+
*
|
|
487
|
+
* Own writes include a primary that the build of a write to a secondary
|
|
488
|
+
* swept in, which arrives `rebuilt`. Skipping it loses nothing the canvas
|
|
489
|
+
* shows: the operation behind the write submits once the write has
|
|
490
|
+
* answered, and a build steps its whole batch through each phase, so that
|
|
491
|
+
* submit already renders the relinked primary. What the later `Validated`
|
|
492
|
+
* adds are diagnostics, which the marker refresh carries.
|
|
322
493
|
*/
|
|
323
494
|
async handleModelUpdated(rootUri, event) {
|
|
324
495
|
if (this.disposeIfStale(rootUri)) {
|
|
325
496
|
return;
|
|
326
497
|
}
|
|
327
|
-
if (
|
|
328
|
-
this.scheduleUpdateAndSubmit(
|
|
498
|
+
if (event.causedBy !== this.state.clientId) {
|
|
499
|
+
this.scheduleUpdateAndSubmit();
|
|
329
500
|
}
|
|
330
501
|
await this.refreshDiagnosticMarkers();
|
|
331
502
|
}
|
|
@@ -341,64 +512,111 @@ let HydraniumGlspStorage = class HydraniumGlspStorage {
|
|
|
341
512
|
if (this.sessionManager.getSession(this.state.clientId)) {
|
|
342
513
|
return false;
|
|
343
514
|
}
|
|
344
|
-
this.logger.
|
|
515
|
+
this.logger.debug(`Stale storage (clientId=${this.state.clientId} not in ClientSessionManager) — self-disposing ${context}`);
|
|
345
516
|
this.dispose();
|
|
346
517
|
return true;
|
|
347
518
|
}
|
|
348
519
|
/**
|
|
349
|
-
*
|
|
350
|
-
* {@link
|
|
351
|
-
* {@link doUpdateAndSubmit} once with the most recent document.
|
|
520
|
+
* (Re)arm the trailing-edge {@link resubmitDebouncer}, so a burst of
|
|
521
|
+
* rebuilds runs {@link doUpdateAndSubmit} once.
|
|
352
522
|
*/
|
|
353
|
-
scheduleUpdateAndSubmit(
|
|
354
|
-
this.pendingResubmitDocument = document;
|
|
523
|
+
scheduleUpdateAndSubmit() {
|
|
355
524
|
this.resubmitDebouncer.schedule();
|
|
356
525
|
}
|
|
357
526
|
/**
|
|
358
|
-
* Debouncer callback: run the resubmit
|
|
359
|
-
*
|
|
360
|
-
*
|
|
527
|
+
* Debouncer callback: run the resubmit against this storage's (invariant)
|
|
528
|
+
* source URI and dispatch the result. Fire-and-forget — failures are
|
|
529
|
+
* logged, not surfaced to the timer caller.
|
|
361
530
|
*/
|
|
362
531
|
flushResubmit() {
|
|
363
|
-
const document = this.pendingResubmitDocument;
|
|
364
|
-
if (document === undefined) {
|
|
365
|
-
return;
|
|
366
|
-
}
|
|
367
532
|
const rootUri = this.state.sourceUri;
|
|
368
|
-
this.doUpdateAndSubmit(rootUri
|
|
533
|
+
this.doUpdateAndSubmit(rootUri).then(actions => this.actionDispatcher
|
|
534
|
+
.dispatchAll(actions)
|
|
535
|
+
.catch((error) => this.logger.warn(`Could not send the resubmitted model of ${rootUri}: ${describeErrorWithStack(error)}`)), error => this.logger.error(`Update-and-submit failed for ${rootUri}: ${describeErrorWithStack(error)}`));
|
|
369
536
|
}
|
|
370
537
|
/**
|
|
371
|
-
*
|
|
372
|
-
* mark
|
|
373
|
-
*
|
|
538
|
+
* For a diagram module on GLSP's own command stack, which counts commands:
|
|
539
|
+
* mark it clean when another client persists the document.
|
|
540
|
+
* `HydraniumGlspCommandStack` reads the text store instead, and this does
|
|
541
|
+
* not move it.
|
|
374
542
|
*/
|
|
375
543
|
handleModelSaved(event) {
|
|
376
544
|
if (this.state.clientId !== event.sourceClientId) {
|
|
377
545
|
this.commandStack.saveIsDone();
|
|
378
546
|
}
|
|
379
547
|
}
|
|
548
|
+
/**
|
|
549
|
+
* Send the diagram's dirty state when a document it has open turns dirty or
|
|
550
|
+
* clean, the answer being the command stack's. GLSP sends it only with a
|
|
551
|
+
* model submission or a save of the diagram's own, so a save by another
|
|
552
|
+
* client, an editor included, or an edit that changes nothing the diagram
|
|
553
|
+
* shows would otherwise leave the client's marker wrong until the next
|
|
554
|
+
* gesture. A flip the diagram's own operation causes is sent twice, once
|
|
555
|
+
* here and once with the submission, and the client keeps the one state.
|
|
556
|
+
*
|
|
557
|
+
* Nothing is sent while a save of the diagram's own is awaited: GLSP's save
|
|
558
|
+
* handler sends the state once the save settles, reason `save`, and GLSP's
|
|
559
|
+
* client keeps a state only when it changes, so the save's own flip sent
|
|
560
|
+
* here first would leave that answer changing nothing. GLSP's saveable
|
|
561
|
+
* waits for exactly that answer, and times out without it.
|
|
562
|
+
*/
|
|
563
|
+
handleDirtyChanged(event) {
|
|
564
|
+
if (this.ownSavesPending > 0 || !this.sharedServices.workspace.TextDocuments.isOpenInClient(event.uri, this.state.clientId)) {
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
this.sendDirtyState();
|
|
568
|
+
}
|
|
569
|
+
/** Send the command stack's dirty state, reason `external`. */
|
|
570
|
+
sendDirtyState() {
|
|
571
|
+
this.actionDispatcher
|
|
572
|
+
.dispatch(SetDirtyStateAction.create(this.commandStack.isDirty, { reason: 'external' }))
|
|
573
|
+
.catch((error) => this.logger.warn(`Could not send the dirty state of ${this.state.sourceUri}: ${describeErrorWithStack(error)}`));
|
|
574
|
+
}
|
|
380
575
|
/**
|
|
381
576
|
* Re-settle to a guaranteed fully-linked + reprojected root, capture it, and
|
|
382
|
-
* (unless suppressed) resubmit a deduped external GModel.
|
|
383
|
-
*
|
|
384
|
-
*
|
|
577
|
+
* (unless suppressed) resubmit a deduped external GModel.
|
|
578
|
+
*
|
|
579
|
+
* The settle is awaited outside {@link AbstractHydraniumGlspState.runExclusive},
|
|
580
|
+
* so an operation does not wait on a build; the capture and the render run
|
|
581
|
+
* inside it, the render's own wait for the document included, since an
|
|
582
|
+
* operation opened between that wait and the GModel build would be rendered.
|
|
385
583
|
*/
|
|
386
|
-
async doUpdateAndSubmit(rootUri
|
|
387
|
-
// Settle-gate the capture: never setSourceRoot off the event's possibly-transient
|
|
388
|
-
// snapshot — the event can arrive while a re-entered document is mid-rebuild.
|
|
584
|
+
async doUpdateAndSubmit(rootUri) {
|
|
389
585
|
const document = await this.sharedServices.model.ModelService.settled(rootUri);
|
|
390
|
-
this.state.
|
|
391
|
-
|
|
586
|
+
return this.state.runExclusive(() => this.captureAndSubmit(rootUri, document.root));
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* The part of {@link doUpdateAndSubmit} that runs inside the boundary.
|
|
590
|
+
* `root` is not captured when the state already holds a later one: an
|
|
591
|
+
* operation that ended while the settle was awaited captured its own
|
|
592
|
+
* write, which `root` predates.
|
|
593
|
+
*
|
|
594
|
+
* The parse-error status is the primary's as it stands now, which can be
|
|
595
|
+
* past `root`: a status taken from an older document would let an operation
|
|
596
|
+
* write into text that no longer parses.
|
|
597
|
+
*/
|
|
598
|
+
async captureAndSubmit(rootUri, root) {
|
|
599
|
+
if (this.sharedServices.workspace.ModelLedger.versionOf(root) >= this.state.version) {
|
|
600
|
+
this.state.setSourceRoot(rootUri, root);
|
|
601
|
+
}
|
|
602
|
+
const primary = this.currentPrimaryDocument();
|
|
603
|
+
if (!primary) {
|
|
604
|
+
// A primary deleted since the settle leaves nothing to be broken.
|
|
605
|
+
this.state.setStatus(DiagramStatus.PARSE_ERROR, undefined);
|
|
606
|
+
}
|
|
607
|
+
const broken = primary ? this.refreshParseErrorStatus(primary) : [];
|
|
392
608
|
// Skip the external submit until the initial requestModel completes; submitting too
|
|
393
609
|
// early bumps root.revision and the client's stale first computedBounds is dropped,
|
|
394
610
|
// leaving the canvas empty.
|
|
395
611
|
if (this.submissionHandler.hasPendingInitialRequest()) {
|
|
396
|
-
return
|
|
612
|
+
return [];
|
|
397
613
|
}
|
|
398
|
-
// While the
|
|
399
|
-
//
|
|
400
|
-
|
|
401
|
-
|
|
614
|
+
// While a document of the write set is structurally broken, keep the GModel the
|
|
615
|
+
// client has, which the initial request has delivered by now, so the canvas
|
|
616
|
+
// doesn't blank or lose its layout mid-typing. The parse-error status still
|
|
617
|
+
// makes it read-only.
|
|
618
|
+
if (broken.length > 0) {
|
|
619
|
+
return [];
|
|
402
620
|
}
|
|
403
621
|
// Read the handler's signature BEFORE submitting and again after: it records
|
|
404
622
|
// every submission whatever the reason, so an unmoved signature means this
|
|
@@ -407,47 +625,72 @@ let HydraniumGlspStorage = class HydraniumGlspStorage {
|
|
|
407
625
|
// to be answered by an echo of itself — the operation had delivered the
|
|
408
626
|
// model, and the external comparison had never heard of it.
|
|
409
627
|
const previousSignature = this.submissionHandler.lastSubmittedSignature;
|
|
628
|
+
// The number this submission takes as it starts.
|
|
629
|
+
const submission = this.submissionHandler.startedSubmissions + 1;
|
|
410
630
|
const submitActions = await this.submissionHandler.submitModel('external');
|
|
411
631
|
if (this.submissionHandler.lastSubmittedSignature === previousSignature) {
|
|
412
|
-
|
|
632
|
+
this.submissionHandler.withdrawLastSubmission(submission);
|
|
633
|
+
return [];
|
|
413
634
|
}
|
|
414
|
-
|
|
415
|
-
return [...submitActions, ...editModeActions];
|
|
635
|
+
return submitActions;
|
|
416
636
|
}
|
|
417
637
|
/**
|
|
418
|
-
*
|
|
419
|
-
*
|
|
420
|
-
*
|
|
638
|
+
* Set {@link DiagramStatus.PARSE_ERROR} to {@link parseErrorStatus} while any
|
|
639
|
+
* document the diagram writes is structurally broken, and withdraw it once
|
|
640
|
+
* they all parse. Answers the documents that are broken.
|
|
641
|
+
*
|
|
642
|
+
* The secondaries count as much as the primary: an operation writes them from
|
|
643
|
+
* the AST their parse recovered, which drops what the parser skipped, so a
|
|
644
|
+
* write into a broken secondary overwrites the user's text.
|
|
421
645
|
*/
|
|
422
|
-
|
|
423
|
-
const broken = this.isStructurallyBroken(document);
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
646
|
+
refreshParseErrorStatus(primary) {
|
|
647
|
+
const broken = [primary, ...this.secondaryDocuments(primary.uri)].filter(document => this.isStructurallyBroken(document));
|
|
648
|
+
this.state.setStatus(DiagramStatus.PARSE_ERROR, broken.length > 0 ? this.parseErrorStatus(broken) : undefined);
|
|
649
|
+
return broken;
|
|
650
|
+
}
|
|
651
|
+
/** The write-set secondaries that exist, other than `primaryUri`. */
|
|
652
|
+
secondaryDocuments(primaryUri) {
|
|
653
|
+
const { ModelService } = this.sharedServices.model;
|
|
654
|
+
return this.state.secondaryUris
|
|
655
|
+
.filter(uri => uri !== primaryUri)
|
|
656
|
+
.map(uri => ModelService.getDocument(uri))
|
|
657
|
+
.filter(document => document !== undefined)
|
|
658
|
+
.map(document => this.sharedServices.workspace.AstDocumentManager.toAstDocument(document));
|
|
430
659
|
}
|
|
431
660
|
/**
|
|
432
661
|
* Seam: is the AST structurally broken (lexing/parsing errors)? Drives both
|
|
433
|
-
*
|
|
434
|
-
* instead of blanking it mid-typing.
|
|
435
|
-
*
|
|
436
|
-
*
|
|
662
|
+
* the {@link DiagramStatus.PARSE_ERROR} status and the GModel resubmit
|
|
663
|
+
* skip — keeping the last valid canvas instead of blanking it mid-typing.
|
|
664
|
+
* Default: the lexer and parser errors of
|
|
665
|
+
* the root's own parse, which exist from the parse on, so the answer does
|
|
666
|
+
* not wait for validation; for a root its document has since replaced, any
|
|
667
|
+
* error-severity diagnostic carrying a Langium `lexing-error` /
|
|
668
|
+
* `parsing-error` code. Adopters override to compose additional break
|
|
669
|
+
* reasons.
|
|
437
670
|
*/
|
|
438
671
|
isStructurallyBroken(document) {
|
|
439
|
-
|
|
672
|
+
const parsed = document.root.$document?.parseResult;
|
|
673
|
+
if (parsed?.value === document.root) {
|
|
674
|
+
return hasStructuralErrors(parsed);
|
|
675
|
+
}
|
|
676
|
+
return document.diagnostics?.some(diagnostic => isStructuralDiagnostic(diagnostic)) ?? false;
|
|
440
677
|
}
|
|
441
678
|
/**
|
|
442
|
-
* Seam:
|
|
443
|
-
*
|
|
444
|
-
*
|
|
445
|
-
*
|
|
446
|
-
*
|
|
447
|
-
*
|
|
679
|
+
* Seam: the {@link DiagramStatus.PARSE_ERROR} status while `brokenDocuments`
|
|
680
|
+
* do not parse, the primary first when it is among them. Default: a read-only
|
|
681
|
+
* error whose {@link DIAGRAM_READONLY_PARSE_ERROR} message names the first.
|
|
682
|
+
*
|
|
683
|
+
* A read-only status needs a message. Read-only withdraws the tool palette,
|
|
684
|
+
* and without a message nothing on the canvas says why.
|
|
448
685
|
*/
|
|
449
|
-
|
|
450
|
-
return
|
|
686
|
+
parseErrorStatus(brokenDocuments) {
|
|
687
|
+
return {
|
|
688
|
+
message: this.sharedServices.MessageRenderer.renderMessage(DIAGRAM_READONLY_PARSE_ERROR, {
|
|
689
|
+
document: UriUtils.basename(URI.parse(brokenDocuments[0].uri))
|
|
690
|
+
}),
|
|
691
|
+
severity: 'ERROR',
|
|
692
|
+
readonly: true
|
|
693
|
+
};
|
|
451
694
|
}
|
|
452
695
|
/**
|
|
453
696
|
* Seam: extra actions to dispatch right after the initial settled root is
|
|
@@ -456,31 +699,48 @@ let HydraniumGlspStorage = class HydraniumGlspStorage {
|
|
|
456
699
|
onSourceModelSettled(_document) {
|
|
457
700
|
return [];
|
|
458
701
|
}
|
|
459
|
-
/** The effective {@link
|
|
460
|
-
get
|
|
461
|
-
return this.
|
|
702
|
+
/** The effective {@link SaveDeliveryPolicy}: the bound option, or {@link DEFAULT_SAVE_DELIVERY_POLICY} when unbound. */
|
|
703
|
+
get saveDeliveryPolicy() {
|
|
704
|
+
return this.boundSaveDeliveryPolicy ?? DEFAULT_SAVE_DELIVERY_POLICY;
|
|
462
705
|
}
|
|
463
706
|
/**
|
|
464
|
-
* Default save flow:
|
|
465
|
-
*
|
|
466
|
-
*
|
|
467
|
-
*
|
|
468
|
-
*
|
|
707
|
+
* Default save flow: persist the store's current text for every document the
|
|
708
|
+
* diagram's session has open.
|
|
709
|
+
*
|
|
710
|
+
* **A save persists, it does not author.** Every diagram gesture already
|
|
711
|
+
* reached the store through the diagram's session, so the store holds the
|
|
712
|
+
* settled text and disk is the only thing behind. Re-serializing from the AST
|
|
713
|
+
* here cannot improve on that text and can only damage it: a serializer
|
|
714
|
+
* normalises formatting, and the comments the write path carries over keep
|
|
715
|
+
* none of the hand-formatting around them, so a save would reflow a document
|
|
716
|
+
* nothing changed — which is what a pure bounds drag does to the semantic
|
|
717
|
+
* file when only its layout moved.
|
|
718
|
+
*
|
|
719
|
+
* **What is saved is the primary, every tracked secondary
|
|
720
|
+
* (`AbstractHydraniumGlspState.trackSecondaryDocument`) and every document
|
|
721
|
+
* that left the write set since the last save**, so a document the diagram
|
|
722
|
+
* wrote is persisted whether or not it is the one the client named. One the
|
|
723
|
+
* diagram's session does not have open is skipped, even when another client
|
|
724
|
+
* has it open: that client's unsaved edits are not the diagram's to persist.
|
|
469
725
|
*
|
|
470
|
-
*
|
|
471
|
-
*
|
|
472
|
-
*
|
|
473
|
-
*
|
|
474
|
-
*
|
|
475
|
-
*
|
|
476
|
-
*
|
|
477
|
-
*
|
|
478
|
-
*
|
|
726
|
+
* **A save therefore names documents the gesture did not aim at**, which is
|
|
727
|
+
* why an unchanged one is not rewritten even though the user asked for a
|
|
728
|
+
* save: the mtime would move on a file they never touched. With the default
|
|
729
|
+
* session, `AstDocumentManager.save` decides that per document and
|
|
730
|
+
* announces the save either way.
|
|
731
|
+
*
|
|
732
|
+
* The configured {@link SaveDeliveryPolicy} (see {@link saveDeliveryPolicy})
|
|
733
|
+
* decides await-vs-fire-and-forget and failure handling. The save persists
|
|
734
|
+
* at `'any'`: the base-version guard exists to stop a stale writer
|
|
735
|
+
* overwriting a newer document, and a flush persists the store — which
|
|
736
|
+
* already holds every other client's change, including the one that
|
|
737
|
+
* advanced the version.
|
|
479
738
|
*
|
|
480
739
|
* Returns `MaybePromise<void>` to match upstream `SourceModelStorage`:
|
|
481
|
-
* resolves with the
|
|
482
|
-
*
|
|
483
|
-
* `
|
|
740
|
+
* resolves with the flush under `await`, returns synchronously under
|
|
741
|
+
* `fire-and-forget`. To change how each document is written, override the
|
|
742
|
+
* session class's `persistDocument`; an override of this method that writes
|
|
743
|
+
* past the session skips that override.
|
|
484
744
|
*/
|
|
485
745
|
saveSourceModel(action) {
|
|
486
746
|
// Normalised for the same reason the load path is: `SaveModelAction.fileUri`
|
|
@@ -490,23 +750,90 @@ let HydraniumGlspStorage = class HydraniumGlspStorage {
|
|
|
490
750
|
// the call site rather than inside `getFileUri` so that method's contract —
|
|
491
751
|
// return what the action said — is unchanged for adopters overriding it.
|
|
492
752
|
const uri = this.toSourceModelUri(this.getFileUri(action));
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
baseVersion: policy.kind === 'overwrite' ? undefined : this.state.version
|
|
499
|
-
}).then(() => undefined);
|
|
500
|
-
if (policy.kind === 'drop-and-log') {
|
|
501
|
-
// Fire-and-forget: the diagram save lost the race to a concurrent edit
|
|
502
|
-
// that already persisted the truth, so it must neither block the action
|
|
503
|
-
// nor surface. Log rather than leave an unhandled rejection.
|
|
504
|
-
persisted.catch(error => this.logger.error(`Save failed for ${uri}: ${error instanceof Error ? error.message : String(error)}`));
|
|
753
|
+
if (this.saveDeliveryPolicy.kind === 'fire-and-forget') {
|
|
754
|
+
// Log rather than leave an unhandled rejection: the promise is not
|
|
755
|
+
// returned, so nothing else will observe a failure. Named for the
|
|
756
|
+
// diagram, since the document that failed may be a secondary.
|
|
757
|
+
this.flushWriteSet(uri).catch(error => this.logger.error(`Diagram save failed for ${uri}: ${describeErrorWithStack(error)}`));
|
|
505
758
|
return undefined;
|
|
506
759
|
}
|
|
507
|
-
//
|
|
508
|
-
//
|
|
509
|
-
|
|
760
|
+
// Awaited: any failure propagates to GLSP's save-action handler, which
|
|
761
|
+
// then sends no dirty state, so the one the save held back goes here.
|
|
762
|
+
// An override that throws before it returns a promise throws inside the
|
|
763
|
+
// async function too, so the hold always ends.
|
|
764
|
+
this.ownSavesPending++;
|
|
765
|
+
return (async () => {
|
|
766
|
+
try {
|
|
767
|
+
await this.flushWriteSet(uri);
|
|
768
|
+
}
|
|
769
|
+
catch (error) {
|
|
770
|
+
this.sendDirtyState();
|
|
771
|
+
throw error;
|
|
772
|
+
}
|
|
773
|
+
finally {
|
|
774
|
+
this.ownSavesPending--;
|
|
775
|
+
}
|
|
776
|
+
})();
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* Save the stored text of `primaryUri`, every tracked secondary and every
|
|
780
|
+
* document that left the write set since the last save, each only where
|
|
781
|
+
* the diagram's session has it open; then close those that left. Each goes
|
|
782
|
+
* through the session's `persist`, so an override of the session class's
|
|
783
|
+
* `persistDocument` applies to diagram saves too. Each saved document is
|
|
784
|
+
* then settled in the language client, so the save does not answer before
|
|
785
|
+
* that client has answered the push of what it wrote.
|
|
786
|
+
*
|
|
787
|
+
* Deduplicated, because a state that tracks its own primary as a secondary
|
|
788
|
+
* would otherwise save it twice and fire two save notifications for one
|
|
789
|
+
* save. Every save is called in one synchronous step, so each takes its text
|
|
790
|
+
* before anything can close a document of the set: saved one after another,
|
|
791
|
+
* a GLSP session ending during the first write closes the rest unsaved. A
|
|
792
|
+
* `persistDocument` override that awaits before calling the base gives that
|
|
793
|
+
* up, and a session ending during its await fails the rest with
|
|
794
|
+
* `DocumentNotOpenError`.
|
|
795
|
+
*/
|
|
796
|
+
async flushWriteSet(primaryUri) {
|
|
797
|
+
// Refused like the load: without a session the client id's opens are
|
|
798
|
+
// another participant's, and this would save them.
|
|
799
|
+
const session = this.requireModelSession();
|
|
800
|
+
const textDocuments = this.sharedServices.workspace.TextDocuments;
|
|
801
|
+
const clientId = this.state.clientId;
|
|
802
|
+
// Taken with the targets: a document leaving the set during the writes
|
|
803
|
+
// was not saved by them, and stays open for the next save.
|
|
804
|
+
const departed = [...this.departedSecondaries];
|
|
805
|
+
const targets = [...new Set([primaryUri, ...this.state.secondaryUris, ...departed])].filter(uri => textDocuments.isOpenInClient(uri, clientId));
|
|
806
|
+
// Async, so a persist that throws instead of rejecting fails its own
|
|
807
|
+
// document rather than leaving the rest of the set uncalled. Settled in
|
|
808
|
+
// full before failing, or the save's dirty-state hold ends while the
|
|
809
|
+
// rest are still writing.
|
|
810
|
+
const modelService = this.sharedServices.model.ModelService;
|
|
811
|
+
const results = await Promise.allSettled(targets.map(async (target) => {
|
|
812
|
+
await session.persist({ uri: target, baseVersion: 'any' });
|
|
813
|
+
await modelService.settleLanguageClient(target);
|
|
814
|
+
}));
|
|
815
|
+
const failures = results.flatMap((result, i) => (result.status === 'rejected' ? [{ uri: targets[i], reason: result.reason }] : []));
|
|
816
|
+
if (failures.length > 0) {
|
|
817
|
+
// A persist's error need not name its document, so the save's names each one.
|
|
818
|
+
const named = failures.map(({ uri, reason }) => `${uri} (${describeError(reason)})`).join(', ');
|
|
819
|
+
throw new Error(`Save failed for ${named}`, { cause: failures[0].reason });
|
|
820
|
+
}
|
|
821
|
+
for (const uri of departed) {
|
|
822
|
+
if (this.departedSecondaries.delete(uri) && uri !== this.state.sourceUri) {
|
|
823
|
+
try {
|
|
824
|
+
await this.state.modelSession?.close(uri);
|
|
825
|
+
}
|
|
826
|
+
catch (error) {
|
|
827
|
+
// A session that ended during the writes closed everything it
|
|
828
|
+
// had open; refusing the close must not fail a save that wrote.
|
|
829
|
+
// Caught around the call, since an ended session throws
|
|
830
|
+
// before it returns a promise.
|
|
831
|
+
if (!isSessionClosedError(error)) {
|
|
832
|
+
throw error;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
510
837
|
}
|
|
511
838
|
/**
|
|
512
839
|
* Normalise GLSP's `sourceUri` — which may be a filesystem PATH or a URI
|
|
@@ -540,15 +867,18 @@ let HydraniumGlspStorage = class HydraniumGlspStorage {
|
|
|
540
867
|
* A model request DOES carry a request id, so a throw here takes the
|
|
541
868
|
* client-request path rather than the toast path — and on that path `detail`
|
|
542
869
|
* comes from `cause?.toString?.()`. A single-argument throw therefore reaches
|
|
543
|
-
* neither the
|
|
544
|
-
* are addressed separately, as on the save path: `message`
|
|
545
|
-
* in the user's terms, `cause` names the
|
|
546
|
-
* missing.
|
|
870
|
+
* neither the server log nor the client console: both print `undefined`. So
|
|
871
|
+
* the two readers are addressed separately, as on the save path: `message`
|
|
872
|
+
* names what failed in the user's terms and is rendered, `cause` names the
|
|
873
|
+
* action and the option key that was missing and stays English. On THIS path
|
|
874
|
+
* the cause reaches only logs — `RejectAction.detail` is read by nothing but
|
|
875
|
+
* the client action-dispatcher's `logger.warn` — which is not what makes it
|
|
876
|
+
* English; its content is.
|
|
547
877
|
*/
|
|
548
878
|
getSourceUri(action) {
|
|
549
879
|
const sourceUri = action.options?.[SOURCE_URI_ARG];
|
|
550
880
|
if (typeof sourceUri !== 'string') {
|
|
551
|
-
throw new GLSPServerError(
|
|
881
|
+
throw new GLSPServerError(this.sharedServices.MessageRenderer.renderMessage(SOURCE_URI_MISSING), `no '${SOURCE_URI_ARG}' option on the ${action.kind} action (received ${typeof sourceUri})`);
|
|
552
882
|
}
|
|
553
883
|
return sourceUri;
|
|
554
884
|
}
|
|
@@ -559,17 +889,23 @@ let HydraniumGlspStorage = class HydraniumGlspStorage {
|
|
|
559
889
|
* {@link AbstractHydraniumGlspState.setSourceRoot}.
|
|
560
890
|
*
|
|
561
891
|
* A save action carries no request id, so a throw here reaches the user as a
|
|
562
|
-
* toast built from `message`,
|
|
563
|
-
*
|
|
564
|
-
*
|
|
565
|
-
*
|
|
566
|
-
*
|
|
567
|
-
*
|
|
892
|
+
* toast built from `message`, while `cause` travels in
|
|
893
|
+
* `MessageAction.details` — **which a Theia host DOES put in front of a
|
|
894
|
+
* user**, as a "Show details" button on the toast opening a dialog. So the
|
|
895
|
+
* two are not split by reachability; they are split by AUDIENCE, which is
|
|
896
|
+
* the older rule and the one that holds here. The message names what failed
|
|
897
|
+
* in the user's terms and is rendered; the cause names which lookups came
|
|
898
|
+
* back empty for whom, and stays English because a wire action kind, an
|
|
899
|
+
* option key and a client id address whoever composes the system — a
|
|
900
|
+
* translated developer string is a worse outcome than an untranslated one.
|
|
901
|
+
* Neither can name the URI — not having one IS the failure, and both sources
|
|
902
|
+
* are written together, so the typed `sourceUri` is equally empty whenever
|
|
903
|
+
* this fires.
|
|
568
904
|
*/
|
|
569
905
|
getFileUri(action) {
|
|
570
906
|
const uri = action.fileUri ?? this.state.get(SOURCE_URI_ARG);
|
|
571
907
|
if (!uri) {
|
|
572
|
-
throw new GLSPServerError(
|
|
908
|
+
throw new GLSPServerError(this.sharedServices.MessageRenderer.renderMessage(SAVE_TARGET_UNKNOWN), `no fileUri on the save action and no ${SOURCE_URI_ARG} in the model state (clientId=${this.state.clientId})`);
|
|
573
909
|
}
|
|
574
910
|
return uri;
|
|
575
911
|
}
|
|
@@ -585,30 +921,50 @@ let HydraniumGlspStorage = class HydraniumGlspStorage {
|
|
|
585
921
|
* set, so re-pushing the full set clears stale markers (an empty set clears
|
|
586
922
|
* all once the model is valid), and sharing the tool-palette validate
|
|
587
923
|
* command's `BATCH` reason means the two paths replace rather than duplicate.
|
|
924
|
+
*
|
|
925
|
+
* Never rejects: the update listener that calls it is not awaited. A
|
|
926
|
+
* validator that throws is logged as an error, markers that cannot be sent
|
|
927
|
+
* as a warning.
|
|
588
928
|
*/
|
|
589
929
|
async refreshDiagnosticMarkers() {
|
|
590
930
|
if (!this.modelValidator) {
|
|
591
931
|
return;
|
|
592
932
|
}
|
|
593
|
-
|
|
594
|
-
|
|
933
|
+
let markers;
|
|
934
|
+
try {
|
|
935
|
+
markers = await this.modelValidator.validate([this.state.root], MarkersReason.BATCH);
|
|
936
|
+
}
|
|
937
|
+
catch (error) {
|
|
938
|
+
this.logger.error(`Validation of ${this.state.sourceUri} failed: ${describeErrorWithStack(error)}`);
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
this.actionDispatcher
|
|
942
|
+
.dispatch(SetMarkersAction.create(markers, { reason: MarkersReason.BATCH }))
|
|
943
|
+
.catch((error) => this.logger.warn(`Could not send the markers of ${this.state.sourceUri}: ${describeErrorWithStack(error)}`));
|
|
595
944
|
}
|
|
596
945
|
sessionDisposed(_clientSession) {
|
|
597
946
|
this.dispose();
|
|
598
947
|
}
|
|
599
948
|
/**
|
|
600
|
-
* Cancel any pending resubmit
|
|
949
|
+
* Cancel any pending resubmit, drain every subscription — those registered
|
|
601
950
|
* during {@link doLoadSourceModel} and the per-secondary ones, which live
|
|
602
|
-
* outside {@link toDispose} because they come and go with the write set
|
|
603
|
-
*
|
|
951
|
+
* outside {@link toDispose} because they come and go with the write set —
|
|
952
|
+
* and end the diagram's client session with `cause`, closing everything it
|
|
953
|
+
* has open. Idempotent.
|
|
954
|
+
*
|
|
955
|
+
* The session ends last, once the detach listener is gone: its closes would
|
|
956
|
+
* otherwise report this storage's own teardown as a client detaching.
|
|
604
957
|
*/
|
|
605
|
-
dispose() {
|
|
958
|
+
dispose(cause = 'closed') {
|
|
606
959
|
this.resubmitDebouncer?.dispose();
|
|
607
960
|
for (const subscription of this.secondarySubscriptions.values()) {
|
|
608
961
|
subscription.dispose();
|
|
609
962
|
}
|
|
610
963
|
this.secondarySubscriptions.clear();
|
|
611
964
|
this.toDispose.dispose();
|
|
965
|
+
this.disposed = true;
|
|
966
|
+
this.state.modelSession?.dispose(cause);
|
|
967
|
+
this.state.modelSession = undefined;
|
|
612
968
|
}
|
|
613
969
|
};
|
|
614
970
|
__decorate([
|
|
@@ -645,10 +1001,10 @@ __decorate([
|
|
|
645
1001
|
__metadata("design:type", Object)
|
|
646
1002
|
], HydraniumGlspStorage.prototype, "modelValidator", void 0);
|
|
647
1003
|
__decorate([
|
|
648
|
-
inject(
|
|
1004
|
+
inject(SaveDeliveryPolicy),
|
|
649
1005
|
optional(),
|
|
650
1006
|
__metadata("design:type", Object)
|
|
651
|
-
], HydraniumGlspStorage.prototype, "
|
|
1007
|
+
], HydraniumGlspStorage.prototype, "boundSaveDeliveryPolicy", void 0);
|
|
652
1008
|
__decorate([
|
|
653
1009
|
postConstruct(),
|
|
654
1010
|
__metadata("design:type", Function),
|